Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Debug tools

The port can be built with a small command interpreter (port/debug/zbdebug.cpp) for getting into any state quickly: jump to a scene, set the levels, fill the party, edit the saved state, assert on the result. It is port code, not decompiled code, and the game's own cheats stay as they are (their codes are compared by hash, see Hidden scenes; the tools set the debugging flag directly instead).

How it is built in

decomp/game.cpp:gameFrame calls zbDebugFrame() at its start, inside #ifdef ZB_DEBUG. Neither the matching build (uv run match) nor the Windows 98 rebuild defines ZB_DEBUG, so the compiled code is unchanged; grep -rn ZB_DEBUG decomp/ finds every hook. CMake's ZB_DEBUG option (default on) adds port/debug/ to the game and defines it. uv run port build and run build it in; uv run port package leaves it out of the published site unless given --debug-tools.

Giving commands

Commands are separated by ; or newlines (# starts a comment) and queued to run when the game is at rest: active, no dialog, no scene change pending. "At rest" also means that none of a scene's own callbacks (open, close, frame, key) is running: they often run the main loop themselves while they wait for a sound or an animation, which calls the frame hook again from inside them, and a command run there would change the scene under them. zbdebug.cpp wraps each scene's callbacks to count them (before the game starts). A command that changes the scene holds up the ones after it until the new scene is open. The game starts in scene 0 (the intro), so a first command like scene 1 waits until the intro's frames run.

WhereHow
command linezoombinis --cmd "scene 9; party 8" (repeatable), --script commands.txt
uv run port run--cmd, --script, with --headless, --screenshot, --seconds
browser?cmd=scene%209 in the URL (repeatable); zbDebug("scene 9") in the console (it queues the text and returns; the game picks it up on its next frame)
uv run port run --headless --seconds 30 --screenshot build/port/shot.bmp \
    --cmd "debug on; level 1 3; party 8; scene 9; wait 2000; assert scene 9; dump"

assert makes quit (and so the process) exit with status 1 if any assertion or command failed; results print to the console prefixed [zbdebug].

Concepts

The commands name things from the game's own structure; this is what they are (the gameplay chapters have the detail).

  • Scene. The game is a set of numbered screens, and only one runs at a time (currentScene). The table below lists them. Changing scene is what scene N does.
  • Group. The twelve puzzle scenes (7-18) come in four groups of three, played in order: group 1 is scenes 7-9, group 2 is 10-12, group 3 is 13-15 and group 4 is 16-18. Clearing a group's last puzzle (9, 12, 15 or 18) sends the party to a camp or Zoombiniville. Groups 2 and 3 are alternatives: Shelter Rock offers either, and finishing either opens Shade Tree. See Modules and scenes.
  • Level. Each group has its own level, 1 to 4, and every puzzle in the group reads it to choose its rules (what it asks of the player, the number of choices, and so on), so level 1 is the first and level 4 the last, hardest tier. Each time the player clears a group's last puzzle that group's level goes up by one (to a maximum of 4), so a second journey through the same puzzles plays harder. level 2 3 makes scenes 10-12 play as level 3. The game stores levels as 0-3 in gameState, the commands use 1-4.
  • Practice mode. A mode the map offers (Ctrl-P, then a key 1-4): any puzzle is played at one chosen level whatever the groups' levels are, with a practice party, and nothing is saved to the journey. While it is on, leaving a puzzle goes back to the map. It overrides level. practice L turns it on (L 1-4) or off (0).
  • Party. The Zoombinis that set out on a journey, up to 16 (party() in gameState). Puzzle scenes make one view per party member when they open. Zoombini Isle and the two camps keep their own lists of waiting Zoombinis, which party does not touch, so use party before jumping to a puzzle scene (7-18), not to fill a camp.
  • Journey map. Between most scenes the game shows the party travelling over the map (scene 2). scene N skips it unless given map.
  • Camps and their "unlock bits". Shelter Rock (4), Shade Tree (5) and Zoombiniville (6) can be visited from the map only after the group before them has been cleared. The game remembers this as bits in gameState: for each group, one bit per level it has been left at. Shelter Rock opens on gameState[0x50] & 0xf (group 1), Shade Tree on 0x52 (the low nibble is group 2, the high nibble group 3) and Zoombiniville on 0x51 (group 4). The map reads such a nibble as a level number while the group is still at level 1, so only 0 (not cleared) and 1 (cleared) are valid there: a nibble of 15 makes the map index past its four levels' views and crash (openMap, in runViewScript). unlock therefore sets only the lowest bit of each group's nibble (0x50 and 0x51 to 1, 0x52 to 0x11), as the game's own debug key @ does, so every camp can be chosen on the map. It doesn't change the levels or the records.
  • Records. Zoombiniville's monuments (scene 6) each commemorate one journey the player completed: a date, a group and a level. gameState holds up to 16 of them, and the town draws one monument per record. The game adds one when the last puzzle of a group is cleared at a level it hasn't recorded yet. records N makes the first N slots (in order: group 1 at levels 1-4, then group 2, and so on) into completed journeys dated 1 January 1996, and clears the rest, so records 0 empties the town and records 16 fills it.
  • Waiting. Commands run in order, but only when the game is at rest (running, no dialog, no scene change under way). scene N also holds up the commands after it until scene N has opened (it may pass through the journey map first). wait scene N holds them until scene N is the current scene, which is for changes that something else makes: a --click, a key, a puzzle finishing. It continues at once if the game is already in N, and the rest of the queue stalls if N never comes. wait MS holds them for that many milliseconds. A wait after scene is what gives a scene time to start and draw before a --screenshot or assert.
  • Debug mode. debug on turns on the game's own debugging keys (below), the ones its hashed cheat enables.

Scenes

#SceneWhat it isNotes for scene N
0introthe logo movie, then onthe game starts here
1mapthe world map
2journeythe party travelling between placesentered with a route chosen by the game; not useful to jump to
3isleZoombini Isle: making the Zoombinis
4campShelter Rock, the first camp
5camp 2Shade Tree, the second camp
6townZoombiniville, with the monumentsdraws the records
7bridgeAllergic Cliffs (group 1)
8tunnelsStone Cold Caves (group 1)
9pizzaPizza Pass (group 1, the last)
10ferryCaptain Cajun's Ferryboat (group 2)
11lillyTitanic Tattooed Toads (group 2)
12slidesStone Rise (group 2, the last)
13fleensFleens! (group 3)
14hotelHotel Dimensia (group 3)
15netMudball Wall (group 3, the last)
16cavesThe Lion's Lair (group 4)
17smokeMirror Machine (group 4)
18mazeBubblewonder Abyss (group 4, the last)
19, 21catcha hidden throwing mini-gamesee Hidden scenes
20targetsa hidden target-shooting mini-game

The puzzles are described in Gameplay and the code, and the table's source, with the module and archive of each, is in Modules and scenes.

Commands

CommandDoes
debug on|offturns the game's debugging keys on or off (below)
transitions on|offthe options' "transitions" (Ctrl-T): the game starts with it on, and off is what shows the journey screen (scene 2) between scenes
scene N [map]leave the current scene (the way the game does, closing it) and open scene N (0-21), skipping the journey map unless map; holds up the commands after it until N is open
level G Lgroup G (1-4) is at level L (1-4): its three puzzles now play at that level
practice Lpractice mode at level L (1-4); 0 leaves it
party Nthe party that sets out: N Zoombinis (up to 16), all on board, each a different kind, with names made the way the game makes them. It is made again on every later scene, as leaving a scene can empty it
unlockmarks every group as cleared (the lowest unlock bit of each), so the map's camp hotspots can be chosen
records Nthe first N of the town's 16 monument records are completed journeys; the rest are cleared
state get OFFSET [SIZE], state set OFFSET VALUE [SIZE]read or write 1, 2 or 4 bytes (default 1) of gameState at a byte offset, for what has no command (layout); numbers can be decimal or 0x hex. Nothing checks the values: one the game doesn't expect (such as a camp nibble above 1) can crash it
cheatcode HASH CODEset the game's cheat tracker (cheatHash, cheatCode) to those values, so that the next isCheat(HASH, CODE) test passes. This suits tests made when a hotspot is clicked (the map's hidden scenes, though scene 19 and 20 are simpler); a key typed afterwards shifts the tracker, so it can't trigger the key-driven ones
click X Y [press|move|release]click the mouse at a point of the screen now, or only press, move or release the button (a drag is click X Y press; wait 300; click X2 Y2 move; wait 300; click X2 Y2 release)
key CODEgives the game the key CODE as if typed: ASCII for characters (key 0x4e is N), 1-26 for Ctrl-A to Ctrl-Z. The cheat tracker sees it too: key 1; key 109; key 105; key 100; key 105; key 32 types the real code Ctrl-A midi (the game's MIDI test)
roster save, roster loadwrite gameState to the current saved game, or read it back
wait MS, wait scene Nhold up the commands after, see Waiting
get NAME, assert NAME VALUEprint or check a value; the names are scene, pending (the scene about to open, -1 if none), practice, party (the count), debug, dialog (non-zero while a dialog is up), transitions, due (the scene the current one is about to leave for), clock and viewclock (the game's clock in 60ths of a second, and the time since the last input), level1-level4 (1-4) and state:OFFSET[:SIZE]
paldiffprint which entries of the scene's own palette (loadedPalette) differ from the one the screen fades to (targetPalette): none when a scene's colours are right; for finding scenes that don't copy their palette
dump, help, quitprint the main state, list the commands, exit (status 1 if any assertion or command failed)

click and --click are the mouse: click X Y (on the 640×480 screen) clicks now, and press, move and release after it make a drag with waits between; --click MS:X,Y does the same at a time after the start.

Examples

# Allergic Cliffs at level 3, with a party of eight
level 1 3; party 8; scene 7

# The town with a full set of monuments
records 16; scene 6; wait 1000; dump

# Practice mode in Pizza Pass at level 2
practice 2; party 8; scene 9

# A test: it must end in the Mirror Machine
party 8; scene 17; wait 500; assert scene 17; quit

Debug keys

With debug on the game's own debugging keys work (mainloop.cpp:gameKey), and key CODE sends them: N (0x4e) draws the paths, P (0x50) shows the frame rate, S (0x53) toggles sound tests, ^ (0x5e) shows memory statistics, & (0x26) draws the palette chart, ] (0x5d) and [ (0x5b) turn step mode on and off, Tab (9) chooses every Zoombini on Zoombini Isle, Ctrl-R (18) shows the dragged Zoombini's position, Ctrl-Z (26) fills the views, @ (0x40) sets the lowest camp bits. In the map + and - change the practice party size.

Cautions

  • On the web, C: is persisted in IndexedDB: a command that changes the state, and any scene the game then enters, can end up saved over the player's roster. Use a private window for experiments.
  • scene N skips the game's own setup of that scene; give it a party (party) and levels (level) first, and note that # comments run to the end of a ;-separated command, not the line.
  • The commands were written without a runtime to hand: if one misbehaves, fix it here and in this table.