Tutorial: Authoring a World
Two routes to a world: a Python generator in tools/worlds/ (how every campaign world is made) or the in-game 3D editor (how players make theirs).
Route A: a Python generator
Every campaign world is the output of a script, tools/worlds/worldNNN.py, and the committed
worlds3d/worldNNN.vwr is its build product. Change the script, never the file. The worked example
here is mission 012, the crate lesson of the blue machine world.
1. Read the brief and the reference
plans/briefs/world012.md records what the original teaches (with the gameplay video's time span),
the 3D design, and the checks that were run. For world 012: rooms climbing to a ledge too high to jump, each
solved with crates (push one to the red dot, pull one out of a corner, stack two, then three). The 3D design
turns the rooms into five machine terraces climbing away from the start, each higher than the last by its
lesson's ledge. The original's map is worlds/world012.txt in the sibling repository
mobile-eggbert; tools/world_inventory.py summarises every world.
2. The generator
A generator has a docstring naming its brief and how to run it, a main() that builds a
World and saves it, and, where the checker cannot model a gate, a variant that writes the "solved"
state. Shortened from tools/worlds/world012.py:
sys.path.insert(0, str(Path(__file__).resolve().parent))
from kit import MACHINE_FACES, MACHINE_FLOOR, MONITOR, O, World, props, rect, wall # noqa: E402
RED_DOT = 3 # the red ball tile marking where a crate goes
X0, X1 = 40, 58
# Terraces: (z0, z1, top); Blupi walks at top + 1.
T0 = (62, 70, 0)
T1 = (50, 61, 3)
...
SPAWN = (48, 1, 68)
def main(argv) -> int:
solved = len(argv) > 1 and argv[1] == "--solved"
rng = random.Random(12)
w = World(mission=12, sky=0)
for z0, z1, top in (T0, T1, T2, T3, T4):
terrace(w, z0, z1, top, rng)
...
def crate(x, y, z, final):
w.place(O.CRATE, *(final if solved else (x, y, z)))
# 1. Push the crate forward (-Z) to the red dot under the 3-high ledge of T1.
w.set(48, 1, T1[1], RED_DOT)
crate(48, 1, 66, (48, 1, 62))
w.hint_zone(0, X0, 1, T0[0], X1, 3, T0[1])
...
w.place(O.EXIT, 49, top, 18)
w.spawn = SPAWN
out = Path(argv[2]) if solved else Path(__file__).resolve().parents[2] / "worlds3d" / "world012.vwr"
w.save(out)
The authoring API is tools/worldkit.py: World(mission, sky), set,
get, clear, fill, box, stairs,
place (objects, with an optional patrol end), bigdecor,
hint_zone, rotate_plate, spawn, save. Coordinates are the raw
grid (0-99, Y up); a block at height y makes y + 1 the walking level. Block numbers are icons of
object-m.png; object types are Def::ObjectType values (ObjectType in
worldkit names the common ones). The kits add building pieces: tools/worlds/kit.py for theme 1
(wall, slab, rail, props, lift, mine,
sign_niche, rect, line), and one module per theme
(palace.py, village.py, cheese.py, station.py,
slime.py, toys.py, skyworks.py, clods.py,
cobbles.py). The world format is described on The .vwr
Format.
3. Regenerate
python3 tools/worlds/world012.py # writes worlds3d/world012.vwr
python3 tools/worlds/world012.py --solved solved.vwr # crates where the lessons put them
git diff --stat worlds3d/
Generators are deterministic (any randomness is seeded), so re-running an unchanged script reproduces the committed file byte for byte; a diff after you changed nothing is a bug in the script.
4. Check it
python3 tools/levelcheck.py worlds3d/world012.vwr # FAIL by design: the crates must move first
python3 tools/levelcheck.py solved.vwr # the exit is reachable
python3 tools/campaigncheck.py
python3 tools/watercheck.py worlds3d/world012.vwr
python3 tools/audit/patrol_envelope.py
python3 tools/audit/lift_envelope.py
ctest --test-dir build-cna-opengl3 -R VerifyShippedWorlds
levelcheck.py is conservative: it models walking, jumps of up to two blocks or two cells,
falls, lifts, swimming and teleporters, but not crates, dynamite, vehicles or timing. That is why world 012
needs its solved variant (both results were reproduced on 2026-11-07). Use flightcheck.py when a
helicopter matters. Other generators have staged variants with their own options; read their docstrings.
5. Play it
cd build-cna-opengl3
./GalaxyEggbert --mission 12
./GalaxyEggbert --mission 12 --spawn 48,1,68,0 # x,y,z in raw grid, yaw in degrees
The build directory holds a staged copy of worlds3d/, refreshed by a post-build step of the
game target, so it does not change when only a world file changed: copy the regenerated file over the staged
one, or look at it with tools/qa_live.sh, which links the repository's worlds3d/
directly (Testing & QA).
6. Script the playthrough
Every campaign mission has a scripted playthrough in tools/autoplay/. The start of
world012.txt:
# Mission 012 played through (plans/briefs/world012.md).
wait 30
expect mission = 12
expect lives >= 3
# 1. Push the crate four cells -Z to the red dot under T1 (it stops at T1's face), climb it.
go 48.00 62.89 tol 0.05 max 400
go 48 62 jump 1.4
until ground = 1
expect y >= 1.95
tools/autoplay.sh build-cna-opengl3 tools/autoplay/world012.txt 12
Write a section, run it, read the trace, repeat (trace N logs every tick, shots N
saves frames). Jumps need a run-up; pickups need Blupi within about 0.6; synchronise on cycles with
temp, blitz or crush, and on lifts with lift X Y Z. A changed
route needs its waits retuned: mine, lift and cycle phases are tick-bound. A theme hub's script
(world010.txt) includes its missions' scripts, so replay the hub too.
7. Finish
Update the brief (reference, design, checks) and the audit rows the change affects
(python3 tools/audit/world_lessons.py --check, python3 tools/audit/lesson_inventory.py).
Commit the script, the .vwr, the playthrough and the brief together, one world per commit, saying
what the original does and how the 3D version does it.
Rules for a fair 3D world
- Movement. Blupi jumps up to two blocks and across up to two cells; he does not step up (every one-block step is a jump, needing two free cells above). Corridors three cells wide; no long one-wide, one-high tunnels (the chase camera needs room). Look for shortcuts: a crate beside a ledge is a stair, a wall top next to a goal is a path.
- Tiles. Look up a tile's class in
tools/data/blocks.csv. Tiles with an empty collision mask and no role are passable scenery: never walls, floors or ceilings. Tiles 15-31 and 49-60 have transparent parts. Block 214 is an invisible barrier that stops Blupi on foot but lets the helicopter and overcraft pass. - Solid BigDecor. The palms, trees, plants, houses, machines, toys, palace pieces and the
statue (
kSolidBigDecorIcons) block Blupi as large as their models: keep them off required routes. - Creatures and lifts.
World.placeapplies each creature type's original speed and rest (CREATURE_TIMING), whatever you pass. Usekit.liftwith the original's tick timings; no lift may be faster than the original's fastest, and a lift's end must not be inside a block. - Water. A swimming Blupi gets out only onto a bank level with the water's top cell; build every pool's water up to its banks.
- Ends and hints. A theme's last mission ends with the golden key (type 21), the others at the red arrow (type 7). Training hints exist only for missions 11-14, placed as hint zones.
- Music and sky.
World(mission=N, sky=region)takes the original's region; the music defaults to the original mission's (ORIGINAL_MUSIC).
Route B: the in-game 3D editor
The main menu's 3D World Editor button opens the slot browser: ten slots for each of the three players. Inside a world you fly the camera, aim with the mouse (or touch, or a gamepad) and place the game's own blocks, objects, BigDecor, Blupi's start, the background and the music from an icon palette in Eggbert 2's order; undo and redo cover every edit, and a box can be filled, copied and stamped. The dice saves and starts a play-test; Pause's Back returns to the editor. The controls are on 3D World Editor.
- A slot is an ordinary
.vwr:customworlds/gamerN/custom_001.vwrtocustom_010.vwrin the player's folder (N0-2), with a thumbnail beside it and three generations of backups. - Export copies a slot to
customworlds/shared/(a download on the web); every.vwrthere is listed in the browser, and a click copies it into a free slot. That is also how a world written by a Python script could be brought into a slot (CustomWorldStorage.hpp; not separately verified for generator output). - Worlds made in the editor can become places of the player's own galaxy (Your Own Galaxy).
- An editor world has no generator, so it is not a campaign world. Shipping one as
worldNNN.vwrwould need its mission number to match the file name (VerifyShippedWorldsenforces it).
xnjttjpo
opens the editor on the world being played and saves over the shipped file (keeping a backup). That is a player
feature. For the repository, a campaign world's source is its generator: change the script.