A cart is a zip file with main.wasm at the root, plus whatever assets you ship with it. The host loads that wasm, hands it the null0 API, and calls back into it as the game runs. The extension is .null0, but it is an ordinary zip - go ahead and open one.
Here is one, now:
Your game is made of callbacks. You do not have to implement them all - the host skips any you leave out.
lifecycle
Three callbacks bracket your game. In C-like pseudo-code:
// called once, when the cart is loaded (in C, main() works too)
void load();
// called on every frame - this is where your game lives
void update();
// called when the cart is unloaded
void unload();
Each language spells these its own way. See languages for yours.
input
buttons
Button input is mapped from libretro keys. It helps to imagine every controller is an SNES pad: use buttonUp/buttonDown and your game works with a keyboard or a gamepad, for any player. When the event came from a keyboard, player is 9999.
- D-pad: ▲◀︎▶︎▼
- A: z
- B: x
- X: a
- Y: s
- L: q
- R: w
- SELECT: shift
- START: enter
Here is an input demo - press some keys:
the input callbacks
These are the rest of the callbacks:
// mapped controller (keys and gamepad both trigger these)
void buttonDown(GamepadButton button, u32 player);
void buttonUp(GamepadButton button, u32 player);
// raw keyboard
void keyDown(Key key);
void keyUp(Key key);
// mouse
void mouseDown(MouseButton button);
void mouseUp(MouseButton button);
void mouseMoved(f32 x, f32 y);
The API reference has the authoritative list, generated from the engine.
files
Anything in the zip is readable, both through the null0 image/sound/tilemap functions and through ordinary WASI file calls. Put logo.png in your cart folder and load_image("logo.png") finds it.
Writes go to a per-cart save directory, so a cart can keep a high-score file without being able to touch anything else on the machine. There is no working directory - use absolute paths (/save.txt) if your language's file API wants one.
running
The native runtime defaults to main.null0, but takes an argument:
null0 some_cart_directory
null0 some_cart.null0
A directory works as well as a zip, which is handy while developing.
distribution
Ship the .null0 and let players download the runtime. Or name your cart main.null0, put it next to the runtime, and rename the runtime to your game.
You can also glue them into one executable:
# linux or mac
cat null0 mygame.null0 > mygame && chmod +x mygame
# windows
copy /b null0.exe+mygame.null0 mygame.exe
This is much the same as how love2d does it. It works because the runtime looks for a zip appended to its own executable, so the cart bytes are identical whether they sit in a .null0 or on the end of a binary.
You do not have to do it by hand. The templates ship a Release workflow that, for every github release you cut, downloads the current runtime for each platform and attaches mygame_linux_x86-64.zip, mygame_macos.zip and mygame_windows_x64.zip alongside the .null0.
For the web, the templates also publish a playable page to github-pages on every push.