Log In  

Picotool is a library and toolset for manipulating Pico-8 cart data, useful for build workflows, as well as cart generation and transformation experiments. Many Pico-8 devs use it for its code minification feature (p8tool luamin), which can be handy when your game's code is near the uncompressed character limit.

A more recent feature is "p8tool build", a tool to assemble carts from multiple files. This tool takes a cart and replaces a type of its data from another file. For example, you can copy the spritesheet from another cart like so, such as to collaborate with an artist working in a separate cart file:

% p8tool build mygame.p8.png --gfx sprites.p8.png

You can pull in Lua code from a .lua file with the --lua argument:

% p8tool build mygame.p8.png --lua mygame.lua

When you use --lua, you get an especially powerful feature: build-time support for Lua's require() statement. The main .lua file can call require() to include code from additional .lua files, such as reusable libraries. It supports several features of the official Lua require() statement, such as only including a module once, and module return values.

You can get Picotool from its Github repo, as well as file bug reports, here:

https://github.com/dansanderson/picotool

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:

3dlib
3dlib.lua
/home/dan/p8libs/3dlib/3dlib.p8

To prevent malicious code from accessing arbitrary files on your hard drive (unlikely but it's nice to prevent it), the require() string cannot refer to files in parent directories with ../. It can refer to child directories, such as require("math/linear").

As with Lua, packages are remembered by the string name used with require(). This means it is possible to have two copies of the same package, each known by a different name, if it can be reached two ways with the lookup path. For example, if the file is named foo.lua and the lookup path is ?;?.lua, require("foo") and require("foo.lua") treat the same file as two different packages.

Packages and game loop functions

When you write a library of routines for Pico-8, you probably want to write test code for those routines. picotool assumes that this test code would be executed in a Pico-8 game loop, such that the library can be in its own test cart. For this purpose, you can write your library file with _init(), _update() or _update60(), and _draw() functions that test the library. By default, require() will strip the game loop functions from the library when including it in your game code so they don't cause conflicts or consume tokens.

For example:

local HandyPackage = {
  handyfunc = function(x, y)
    return x + y
  end,
  handynumber = 3.14,
}

function _update()
  test1 = HandyPackage.handyfunc(2, 3)
end
function _draw()
  cls()
  print('test1 = '..test1)
end

return HandyPackage

If you want to keep the game loop functions present in a package, you can request them with a second argument to require(), like so:

require("mylib", {use_game_loop=true})

How require() actually works

Of course, Pico-8 does not actually load packages from disk when it runs the cartridge. Instead, picotool inserts each package into the cartridge code in a special way that replicates the behavior of the Lua require() feature.

When you run p8tool build with the --lua=... argument, picotool scans the code for calls to the require() function. If it sees any, it loads and parses the file associated with the string name, and does so again if the required file also has require() calls.

Each required library is stored once as a function object in a table inserted at the top of the final cartridge's code. A definition of the require() function is also inserted that finds and evaluates the package code in the table as needed.

To match Lua's behavior, require() maintains a table named package.loaded that maps string names to return values. As with Lua, you can reset this value to nil to force a require() to reevaluate a package.

This feature incurs a small amount of overhead in terms of tokens. Each library uses tokens for its own code, plus a few additional tokens for storing it in the table. The definition for require() is another 40 tokens or so. Naturally, the inserted code also consumes characters.

Formatting or minifying Lua in a built cart

You can tell p8tool build to format or minify the code in the built output using the --lua-format or --lua-minify command line arguments, respectively.

% ./picotool-master/p8tool build mycart.p8.png --lua=mygame.lua --lua-format

This is equivalent to building the cart then running p8tool luafmt or p8tool luamin on the result.

Future build features

I actually implemented require() support back in March of 2017. I didn't announce it at the time because I wanted to implement "dead code elimination," a feature that would prune unused code from included libraries, thereby saving tokens and making reusable libraries more practical. It'll be a fancy feature to implement, and I need to overhaul the parser to do it right, so that project is on hold for now. I'm announcing require() support now in case anyone has a use for it.

Let me know if you have any questions. Feel free to file bug reports and feature requests in Picotool's issue tracker on Github.

-- Dan

P#46443 2017-11-18 03:18 ( Edited 2017-11-18 08:18)

how do i get picotool to work, i downloaded it but don't know how to run it

P#81609 2020-09-08 17:56

Make sure you've done the steps in the Installing and Using sections of the documentation: https://github.com/dansanderson/picotool

I should mention that picotool is a bit behind the latest PICO-8 features and I haven't tested it in a while. Its main features should still work but don't be surprised if there are a few glitches, like recent new keywords getting minimized incorrectly, etc. I have some catching up to do but no time to do it. :)

P#81614 2020-09-08 18:26

[Please log in to post a comment]

Follow Lexaloffle:          
Generated 2024-03-28 11:56:01 | 0.007s | Q:13