Picotool
Tools and Python libraries for manipulating Pico-8 game files. http://www.lexaloffle.com/pico-8.php
Install / Use
/learn @dansanderson/PicotoolREADME
picotool: Tools and Python libraries for manipulating PICO-8 game files
PICO-8 is a fantasy game console by Lexaloffle Games. The PICO-8 runtime environment runs cartridges (or carts): game files containing code, graphics, sound, and music data. The console includes a built-in editor for writing games. Game cartridge files can be played in a browser, and can be posted to the Lexaloffle bulletin board or exported to any website.
picotool is a suite of tools and libraries for building and manipulating PICO-8 game cartridge files. The suite is implemented in, and requires, Python 3. The tools can examine and transform cartridges in various ways, and you can implement your own tools to access and modify cartridge data with the Python libraries.
Useful tools include:
p8tool build: assembles cartridges from multiple sources, as part of a game development workflowp8tool stats: reports statistics on one or many cartridge filesp8tool listlua: prints the Lua code of a cartridgep8tool luafind: searches the Lua code of a collection of cartridgesp8tool luafmt: formats the Lua code of a cartridge to make it easier to read
There are additional tools that are mostly useful for demonstrating and troubleshooting the library: writep8, listtokens, printast, luamin. A separate demo, p8upsidedown, uses picotool to transform the code and data of a game to turn it upsidedown.
picotool supports reading and writing both of PICO-8's cartridge file formats: the text-based format .p8, and the PNG-based binary format .p8.png.
How do most people use picotool?
Most people use picotool for the luamin command, which condenses the Lua code of a cart to use as few characters as possible. This is useful if you have a large game whose code is above PICO-8's character limit but below the token limit. In this rare case, luamin helps you keep your developer version easy to read and modify while you polish the game for publication. In most cases, your cart will exceed the token limit first, and minification doesn't help with unusual cases such as long string data.
I originally created picotool for general purpose build workflows, especially the build tool. Since then, PICO-8 has added features such as #include support, making some of the build features unnecessary. (#include doesn't quite work like require() but it's good enough for most things!) I'm still interested in the potential for token optimization features such as dead code elimination, but these are not yet implemented.
I hope picotool can be the basis for your custom workflows and cart manipulation experiments, so you don't have to write your own cart read/write routines. If there's a feature you'd like to see in the libraries or the tools, please let me know!
Installing picotool
To install the picotool tools and libraries:
-
Install Python 3 version 3.4 or later, if necessary. (picotool has not been tested with Python 2.)
-
Download and unpack the zip archive, or use Git to clone the Github repository.
- Unpacking the zip archive creates a root directory named
picotool-master. - When cloning the repo, this is just
picotool, or whatever you named it when you cloned it.
- Unpacking the zip archive creates a root directory named
-
Change to the unpacked archive or clone directory from the above step.
-
Install the software. The dot (
.) tellspip installto use the current working directory, which should bepicotool-master(from the .zip file) orpicotool(from git clone).pip install .
You should now have a p8tool command in your path.
Using picotool
To use a tool, you run the p8tool command with the appropriate arguments. Without arguments, it prints a help message. The first argument is the name of the tool to run (such as stats), followed by the arguments expected by that tool.
For example, to print statistics about a cart named helloworld.p8.png:
p8tool stats helloworld.p8.png
p8tool build
The build tool creates or updates a cartridge file using other files as sources. It is intended as a part of a game development workflow, producing the final output cartridge.
The tool takes the filename of the output cartridge, with additional arguments describing the build. If the output cartridge does not exist, the build starts with an empty cartridge. Otherwise, it uses the existing cartridge as the default, and overwrites sections of it based on the arguments.
For example, you can create a cartridge in PICO-8, use PICO-8's built-in graphics and sound editors, then use p8tool build to replace the Lua code with the contents of a .lua file:
p8tool build mygame.p8.png --lua mygame.lua
As another example, to create a new cartridge using the spritesheet (gfx) from one cartridge file, music (sfx, music) from another, and Lua code from a .lua file:
p8tool build mygame.p8.png --gfx mygamegfx.p8 --sfx radsnds.p8.png --music radsnds.p8.png --lua mygame.lua
You can also erase a section of an existing cart with an argument such as --empty-map.
The available arguments are as follows:
--lua LUA: use Lua code from the given cart or.luafile--gfx GFX: use spritesheet from the given cart--gff GFF: use sprite flags from the given cart--map MAP: use map from the given cart--sfx SFX: use sound effects from the given cart--music MUSIC: use music patterns from the given cart--empty-lua: use an empty Lua code section--empty-gfx: use an empty spritesheet--empty-gff: use empty sprite flags--empty-map: use an empty map--empty-sfx: use empty sound effects--empty-music: use empty music patterns
If the output cart filename ends with .p8.png, the result is a cartridge with a label image. If the file already exists, the cartridge label is reused. If the file does not exist, an empty cartridge label is used. To use a non-empty label, you must open the cart in PICO-8, take a screenshot (press F6 while running), set the title and byline in the first two lines of code (as Lua comments), then save the .p8.png file from PICO-8. Future runs of p8tool build will reuse the label.
Packages and the require() function
p8tool build supports a special feature for organizing your Lua code, called packages. When loading Lua code from a file with the --lua mygame.lua argument, your program can call a function named require() to load Lua code from another file. This is similar to the require() function available in some other Lua environments, with some subtle differences due to how picotool does this at build time instead of at run time.
Consider the following simple example. Say you have a function you like to use in several games in a file called mylib.lua:
function handyfunc(x, y)
return x + y
end
handynumber = 3.14
Your main game code is in a file named mygame.lua. To use the handyfunc() function within mygame.lua, call require() to load it:
require("mylib")
result = handyfunc(2, 3)
print(result)
r = 5
area = handynumber * r * r
All globals defined in the required file are set as globals in your program when require() is called. While this is easy enough to understand, this has the disadvantage of polluting the main program's global namespace.
A more typical way to write a Lua package is to put everything intended to be used by other programs in a table:
HandyPackage = {
handyfunc = function(x, y)
return x + y
end,
handynumber = 3.14,
}
Then in mygame.lua:
require("mylib")
result = HandyPackage.handyfunc(2, 3)
This is cleaner, but still has the disadvantage that the package must be known by the global name HandyPackage wihtin mygame.lua. To fix this, Lua packages can return a value with the return statement. This becomes the return value for the require() call. Furthermore, Lua packages can declare local variables that are not accessible to the main program. You can use these features to hide explicit names and return the table for the package:
local HandyPackage = {
handyfunc = function(x, y)
return x + y
end,
handynumber = 3.14,
}
return HandyPackage
The main program uses the return value of require() to access the package:
HandyPackage = require("mylib")
result = HandyPackage.handyfunc(2, 3)
The require() function only evaluates the package's code once. Subsequent calls to require() with the same string name do not reevaluate the code. They just return the package's return value. Packages can safely require other packages, and only the first encountered require() call evaluates the package's code.
Where packages are located
The first argument to require() is a string name. picotool finds the file that goes with the string name using a library lookup path. This is a semicolon-delimited (;) list of filesystem path patterns, where each pattern uses a question mark (?) where the string name would go.
The default lookup path is ?;?.lua. With this path, require("mylib") would check for a file named mylib, then for a file named mylib.lua, each in the same directory as the file containing the require() call. The lookup path can also use absolute filesystem paths (such as /usr/share/pico8/lib/?.lua). You can customize the lookup path either by passing the --lua-path=... argument on the command line, or by setting the PICO8_LUA_PATH environment variable.
For example, with this environment variable set:
PICO8_LUA_PATH=?;?.lua;/home/dan/p8libs/?/?.p8
The require("3dlib") statement would look for these files, in this order, with paths relative to the file containing the require() statement:
3dli
Related Skills
node-connect
341.6kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
84.6kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
341.6kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
84.6kCommit, push, and open a PR
