Plan and status

25 of 25 tasks done in Ruins, the first stage

The ruined city is built, on the desktop and in the browser, but it isn't hosted for players yet. 136 of 136 checklist steps are ticked. Every task in the plan is done.

Tasks in build order. Filled is done, outlined is next, hollow is waiting. Select one to jump to it.

  1. 0
  2. 0a
  3. 2
  4. 1
  5. 0b
  6. 3
  7. 4
  8. 5
  9. 6
  10. 7
  11. 8
  12. 9
  13. 10
  14. 11
  15. 12
  16. 13
  17. 14
  18. 13a
  19. 15
  20. 16
  21. 16a
  22. 17
  23. 18
  24. 19
  25. 20

What done looks like

You open the client, pick the Vancouver region and a seed, spawn on a downtown street in first person, and walk through the ruins: real terrain and streets, a recognisable skyline with landmark towers in their real places, regenerated mid-rises, floors and stair cores you can climb, and a damage pass that flattens some blocks and leaves others standing. Changing the seed changes the ruins, not the city. Two machines with the same seed produce the same world.

Task list

Read from the plan file's checkboxes. A task is done when every one of its steps is ticked, and each tick lands in the same commit as the code.

0Workspace scaffold and CIrepo6/6 steps, done
  1. Done: Write the workspace Cargo.toml
  2. Done: Write rust-toolchain.toml
  3. Done: Write .github/workflows/ci.yml
  4. Done: Run cargo build --workspace
  5. Done: Run cargo test --workspace
  6. Done: Commit

Lands as chore: scaffold cargo workspace and CI

0aScaffold follow-uprepo5/5 steps, done
  1. Done: Edit Cargo.toml
  2. Done: Edit ci.yml
  3. Done: Edit .gitignore
  4. Done: Run
  5. Done: Commit

Lands as chore: pin CI toolchain, add missing workspace deps, bevy-free check

2geo-h3 crategeo-h36/6 steps, done
  1. Done: Write failing tests
  2. Done: Run
  3. Done: Implement
  4. Done: Write examples/cells_of_zone.rs
  5. Done: Run
  6. Done: Commit

Lands as feat(geo-h3): resolutions, rings, cell polygons, local frame

1Data coverage check (verification, no library code)repo6/6 steps, done
  1. Done: Write coverage.sql
  2. Done: Write coverage.sh
  3. Done: Run the Copernicus inventory
  4. Done: Run coverage.sh
  5. Done: Write the note
  6. Done: Commit

Lands as docs: record Vancouver data coverage and Copernicus inventory

0bWeb-target gates (spec Amendment A1)repo6/6 steps, done
  1. Done: Write the failing test
  2. Done: Run
  3. Done: Implement
  4. Done: Add the clippy files and tools/wasm-check.sh
  5. Done: Edit ci.yml
  6. Done: Commit

Lands as ci: wasm32 gates for engine-free crates; libm-backed h3o

3worldsource types, trait and fixture sourceworldsource5/5 steps, done
  1. Done: Write failing tests
  2. Done: Run
  3. Done: Implement
  4. Done: Run
  5. Done: Commit

Lands as feat(worldsource): skeleton types, WorldSource trait, fixture source

4PgWorldSource (Overture warehouse)worldsource6/6 steps, done
  1. Done: Write failing tests
  2. Done: Run
  3. Done: Implement
  4. Done: Run
  5. Done: Write examples/pg_probe.rs
  6. Done: Commit

Lands as feat(worldsource): Overture warehouse source

5RegionDb (SQLite) and SqliteWorldSourceworldsource5/5 steps, done
  1. Done: Write failing tests
  2. Done: Run
  3. Done: Implement
  4. Done: Run
  5. Done: Commit

Lands as feat(worldsource): per-region SQLite store

6Terrain tile codecterrain5/5 steps, done
  1. Done: Write failing tests
  2. Done: Run
  3. Done: Implement
  4. Done: Run
  5. Done: Commit

Lands as feat(terrain): tile codec

7Copernicus GeoTIFF sourceterrain6/6 steps, done
  1. Done: Write failing tests
  2. Done: Run
  3. Done: Implement
  4. Done: Run
  5. Done: Manual verification
  6. Done: Commit

Lands as feat(terrain): Copernicus GLO-30 source

8Terrain bake and runtimeterrain5/5 steps, done
  1. Done: Write failing tests
  2. Done: Run
  3. Done: Implement
  4. Done: Run
  5. Done: Commit

Lands as feat(terrain): zone bake, flattening, runtime sampling and cell meshes

9worldgen quantisation, params, seeding and blocksworldgen5/5 steps, done
  1. Done: Write failing tests
  2. Done: Run
  3. Done: Implement
  4. Done: Run
  5. Done: Commit

Lands as feat(worldgen): quantisation, params, seeding, block polygonization

10Block statistics, landmarks, building samplingworldgen5/5 steps, done
  1. Done: Write failing tests
  2. Done: Run
  3. Done: Implement
  4. Done: Run
  5. Done: Commit

Lands as feat(worldgen): block statistics, landmarks, building sampling

11Structure (floors, core, walls, openings)worldgen5/5 steps, done
  1. Done: Write failing tests
  2. Done: Run
  3. Done: Implement
  4. Done: Run
  5. Done: Commit

Lands as feat(worldgen): building structure

12Damage pass and generate_cell with golden testworldgen6/6 steps, done
  1. Done: Build the fixture
  2. Done: Write failing tests
  3. Done: Run
  4. Done: Implement
  5. Done: Run
  6. Done: Commit

Lands as feat(worldgen): damage pass, generate_cell, golden fixture

13bake-region CLIbake-region6/6 steps, done
  1. Done: Write failing tests
  2. Done: Run
  3. Done: Implement
  4. Done: Run
  5. Done: Real bake
  6. Done: Commit

Lands as feat(bake-region): bake a zone into a region SQLite

14Client scaffold, config, static groundclient5/5 steps, done
  1. Done: Write a failing test
  2. Done: Run
  3. Done: Implement
  4. Done: Run
  5. Done: Commit

Lands as feat(client): app scaffold, config, region handles

13aClip area features to cellsbake-region6/6 steps, done
  1. Done: Write failing tests
  2. Done: Run
  3. Done: Implement
  4. Done: Run
  5. Done: Re-bake the downtown zone
  6. Done: Commit

Lands as feat(bake-region): clip area features to cell plus margin

15Cell streamingclient5/5 steps, done
  1. Done: Write failing tests
  2. Done: Run
  3. Done: Implement
  4. Done: Run
  5. Done: Commit

Lands as feat(client): ring-based cell streaming on worker threads

16Meshingclient5/5 steps, done
  1. Done: Write failing tests
  2. Done: Run
  3. Done: Implement
  4. Done: Run
  5. Done: Commit

Lands as feat(client): terrain, road and building meshing with LOD

16aWalkable core stair (amendment, 2026-11-09)worldgen6/6 steps, done
  1. Done: Write failing tests
  2. Done: Run
  3. Done: Implement
  4. Done: Update the golden
  5. Done: Run
  6. Done: Commit

Lands as fix(worldgen): walkable core stair (entry/exit doors, 35° bound); new golden

17First-person playerclient, sim6/6 steps, done
  1. Done: Write a failing test
  2. Done: Run
  3. Done: Implement
  4. Done: Bake a second zone
  5. Done: Run
  6. Done: Commit

Lands as feat(client,sim): first-person kinematic controller

18Debug overlays and generation budgetclient4/4 steps, done
  1. Done: Write failing tests
  2. Done: Implement
  3. Done: Run
  4. Done: Commit

Lands as feat(client): debug overlays and generation budget stats

19Smoke list, README, mirrorclient5/5 steps, done
  1. Done: Step 0 (controller ruling 2026-11-09, before the smoke run): chunk the static colliders
  2. Done: Write docs/SMOKE.md
  3. Done: Update README.md
  4. Done: Run the smoke list
  5. Done: Commit

Lands as perf(client): chunk static colliders and insert them under a frame budget

20Multi-threaded web client (spec Amendment A1)client6/6 steps, done
  1. Done: Write failing tests
  2. Done: Run
  3. Done: Implement
  4. Done: Run it
  5. Done: Write the web section of docs/SMOKE.md
  6. Done: Add the CI web job

Lands as feat(client): multi-threaded web build

Five stages

From the roadmap. Only the first has a plan. The others are milestones, and each gets its own spec before anyone starts it.

  1. Ruins

    In progress, 25 of 25 tasks done

    Walk through deterministically generated ruins of downtown Vancouver on real terrain and streets, natively and in a multi-threaded browser build (spec A1).

    Sub-project 1 is the task list above.

  2. Presence

    In progress, 25 of 25 tasks done

    Several players in the same ruins; zone server; border transfer.

    Milestone notes
    • Zone server binary (server crate) running sim headless under Bevy MinimalPlugins; one process per res-5 zone; config from the region SQLite plus a zone argument.
    • Spike first: lightyear vs bevy_replicon on Bevy 0.20 for authoritative movement with client prediction. Decision criteria: works on both; interest management by res-7 cell; measured tick cost at 50, 100, 200 simulated clients.
    • The client also runs in the browser (spec Amendment A1), so the chosen crate must have a transport that works from wasm (WebTransport or WebSocket), and cell packets arrive in the region-pack packet format Ruins Task 13 defines.
    • Movement replication; player join/leave; the first capacity numbers (players per zone process on the dev server and on commodity hardware).
    • Zone border transfer: client hits border → origin zone serialises live state → destination zone loads it → client reconnects. Account tier appears here as the hand-off store (Postgres, account schema).
    • Auth: start with a dev token; Steam later.
  3. Claim and build

    Not started

    Claim a floor or plot, place pieces, raid by destruction.

    Milestone notes
    • Claim a res-7 cell's floor or plot; ownership rules in sim.
    • Building pieces as content rows (piece table): wall, door, floor, stairs, barricade; placement snapping to generated structure (a floor slab, a breach).
    • Destruction model: piece health, tool damage; raiding is destruction, nothing else.
    • Player-made deltas per cell streamed from the zone server for the same ring the client loads.
    • Contested claim timer (owner, 2026-11-09): every new player starts with a claim stake; placing it starts a progress bar everyone nearby sees. It fills fast with nobody else around and slower for each other player within range (for example rate = base / (1 + nearby_others), pausing when contested); team-mates do not slow it once factions exist. Breaking or pulling the stake before it fills stops the claim. Server-side in integer ticks (sim), counted from the zone's interest set; one claim-progress message for players in view. Open questions for the spec: unclaimed land only or also abandoned claims, what "nearby" means (range or line of sight), whether leaving the stake cancels.
    • Start from the WorldBuilder reuse guide (docs/reference/worldbuilder-reuse-guide.md): cell claims and ownership (port code + schema), terrain edits and their journal (fix the 200-row cap and the float summing), roads cut into terrain, lots between roads and the lot ledger, game config as template → snapshot. Not built there, so designed fresh: piece health and raiding, inventory and crafting, player health and hunger, shard coordinator, relocation, factions, chat. Tunnels (later): fixed underground networks where only entrances cut the ground.
  4. Survive

    Not started

    Resources, inventory, crafting, loot, Cold Storage.

    Milestone notes
    • Resources and loot placement driven by damage class; inventory; crafting from recipe rows; tools.
    • Health, hunger, fall damage.
    • "Find the building plans": a loot item that reveals a specific building's structural weak points (the breach locations the generator already knows).
    • Cold Storage: the craftable object whose contents survive a wipe (program decision 8).
    • Client UI as detachable windows (requested 2026-11-08): the game view stays clean on the primary monitor; inventory, map, crafting and later faction panels each open as a separate OS window the player can drag to another monitor. Bevy supports multiple windows and cameras, so the UI is designed as independent panels with their own window from the start rather than retrofitted. Single-monitor players get the same panels as overlays. The web client (spec A1) cannot open OS windows, so it always uses the overlays.
  5. Cycle

    Not started

    Wipe countdown, shard coordinator, decay, tokens, factions, more regions.

    Milestone notes
    • Shard coordinator process: shard clock, wipe countdown broadcast, Cold Storage snapshot at zero, faction state, cross-zone chat.
    • Wipe kinds unified (scheduled, new shard, unhealthy zone restart).
    • Decay of unattended structures.
    • Relocation tokens (move claim + Cold Storage to another zone or shard).
    • Factions holding zones; the zone-full border rule as a control mechanic.
    • Region catalogue growth: second region (Tokyo or London, by data coverage), baked from the WorldBuilder Overture/OSM warehouse like Vancouver. No copy of the warehouse (owner, 2026-11-09): it stays queryable, read-only, behind WorldSource, and only baked cells are stored here. The reuse guide's "port the Overture/OSM loaders" item (§5.7) is therefore not planned.

Later polish, no stage yet. - Chat translation (owner, 2026-11-09): each player reads chat in their own language. Chat lines carry the original text plus a language tag (a protocol version bump); the receiving client translates, original one click away; the server still sanitises and moderates the original. Default: small offline models (Bergamot/Marian class) on a worker thread, native and wasm, so frame time is untouched and chat never leaves the game. Optional: a larger model on the player's GPU (WebGPU compute in the browser, wgpu-backed burn natively) once the WebGPU web build exists, at low priority or only while chat is open. Server or cloud translation is a later choice (dev server GPU is shared; cloud costs money and sends chat to a third party). - Voice chat, if ever: through a relay server, never peer to peer (peer to peer exposes players' IP addresses to each other, which a PvP game cannot allow). ComfyUI textures and props through the asset pipeline (glTF only); vegetation; vehicles; sound; Windows packaging; Steam.

Thirteen decisions that bind every stage

Settled in the design spec and not reopened casually. A wrong one gets a written amendment, not a quiet workaround.

  1. Genre

    survival MMO, no story campaign. Cosmic Library fiction is a mood board only.

    Why. Solo developer; the loop is the product.

  2. Stack

    Rust throughout. Bevy for client and headless server. A shared sim crate holds every game rule. Replication via an existing Bevy networking crate, chosen by a spike in sub-project 2.

    Why. Ecosystem leverage; client and server cannot disagree about rules they share.

  3. World model

    skeleton + flesh. Skeleton is real and fixed (terrain, water, roads, land use, places). Flesh is procedural, seeded per shard and wipe (which buildings stand on a block, interiors, damage, loot, props). Deterministic from the seed.

    Why. Recognisable places, un-memorisable ruins, tiny data footprint, no client-side disagreement.

  4. Addressing

    H3 everywhere. Res 4 = region, res 5 = zone, res 7 = claim, data and streaming cell.

    Why. One ID scheme from the globe to a building lot.

  5. Scale model

    region → zones → shards. A region is fixed geography. A zone is one of its seven res-5 children and runs as its own server process with its own player cap. A shard is an instance of a region: seven zone processes plus a coordinator (shard clock, wipe countdown, faction state, cross-zone chat). Popular regions get more shards.

    Why. One process never has to hold a whole region; avoids seamless-handoff netcode.

  6. Zone borders are hard but crossable on foot: an explicit transfer with a short sync pause (origin zone serialises the player's live state to the account tier, destination loads it). A full zone refuses entry at the border. Relocation tokens move a player's claim and Cold Storage to another zone or shard; they are the only way to change shards.

    Why. Keeps "ranch in the country, travel to the city"; tokens are an economy lever (Evony-style).

  7. Two persistence tiers. Shard tier: the world, dropped on any wipe. Account tier: identity, progression, Cold Storage snapshots; never wiped. Wipe kinds (scheduled, new shard, unhealthy server) are one operation.

    Why. Makes "unhealthy → wipe" safe.

  8. Cold Storage is the only item persistence across wipes: a craftable base object; at countdown zero its contents snapshot to the account tier. Items on the body or in ordinary containers are lost. The countdown is visible in-world.

    Why. End-of-cycle scramble is the loop's best moment; raiding Cold Storage before it seals is the counter-play.

  9. DB-driven end to end. Server master data in Postgres. Client carries one embedded SQLite per region, filled by small packets (a cell's skeleton tens of KB; a terrain tile 1-40 KB). Content (items, recipes, pieces, loot tables, generator parameters, building styles) is versioned table data pushed as packets, not code. Only engine/rules changes need a binary update.

    Why. Balance and content updates without client patches; preloadable.

  10. World data sourcing

    WorldBuilder's Postgres (Overture warehouse, OSM planet) is read read-only through a WorldSource trait today; our own databases replace it behind the same trait later. WorldBuilder code is a reference, never a dependency.

    Why. WorldBuilder is a separate live product with its own problems.

  11. Assets

    glTF only. ComfyUI for textures and props; Blender headless as converter; the Unreal editor only as an exporter for Fab packs whose licence allows it.

    Why. Rust graphics stack is glTF-native.

  12. Region catalogue with an available flag. Vancouver (the res-4 cell containing 49.282, -123.12) is the first entry marked available. Nothing region-specific is hardcoded.

    Why. The world is all of Earth from day one.

  13. Development

    Internal working practice, not published.

What the real data covers

Before any generator code, the plan's first verification task counted what the map data actually holds for Vancouver. These are the headline results.

FigureValueWhat it means
Cells in the first zone49Level-7 hexagons in the downtown Vancouver zone, the first build target.
Cells in the region343Seven zones of 49 cells each.
Buildings in the zone151,876Distinct Overture building footprints (release 2026-10-21).
Road segments in the zone57,563Distinct Overture road segments, same release.
Pass mark2,000 / 500Minimum buildings and road segments for the warehouse to count as a usable source. It passes by a wide margin.
Buildings with a height58.4%35,688 of 61,133 footprints in an earlier, smaller extract. The generator defaults heights by class for the rest.
Elevation tiles on hand26,475Copernicus GLO-30 one-degree tiles, 30 m resolution. Both tiles that cover Vancouver are present.

Amendment A1: a multi-threaded web client

The client also ships as a multi-threaded browser build (wasm32-unknown-unknown, wasm threads over SharedArrayBuffer) so a small alpha group on Linux or Windows can play from a URL with no per-OS install. Native Linux stays the primary development target and the determinism reference; native Windows comes later (unchanged). The web build is delivered at the end of sub-project 1 (plan Task 20); everything before it is designed so that adding it is a new entry point plus a storage adapter, not a rewrite.

PieceNative (Linux, later Windows)Web (browser)
geo-h3, worldgen, terrain runtime, simsame codesame code, compiled to wasm32; must produce the same geometry_hash
Region dataper-region SQLite (RegionDb, rusqlite)region pack: a static directory written by bake-region (manifest + one packet per res-7 cell + one file per terrain tile), fetched over HTTP. RegionDb/rusqlite are not compiled for wasm32
Cell generation and meshingBevy AsyncComputeTaskPoola wasm-bindgen-rayon pool of Web Workers (Bevy's own task pools are single-threaded on wasm)
ECS schedule, rendering, inputBevy, multi-threaded executorBevy on the browser main thread, single-threaded executor; wgpu types are not Send, so rendering stays there
Graphics backendVulkanWebGL2 for the alpha (widest browser support on Linux and Windows); WebGPU is a later switch
bake-region, PgWorldSource/Postgres, Copernicus GeoTIFF reading, golden-hash regenerationnative onlynot built

Rules that bind every task from here on

1Threads build

wasm threads need a nightly compiler and a rebuilt std: -Z build-std=panic_abort,std, -C target-feature=+atomics,+bulk-memory, and the link args --shared-memory --import-memory --max-memory=<bytes> --export=__wasm_init_tls --export=__tls_size --export=__tls_align --export=__tls_base (verified 2026-11-08: without the explicit link args the module's memory is not shared). The web build pins its own dated nightly in its build script; rust-toolchain.toml keeps the native 1.99.1 pin. The wasm-bindgen CLI version must equal the wasm-bindgen crate version exactly.

2Cross-origin isolation

Every response that serves the web client carries Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp; without them SharedArrayBuffer does not exist and the pool cannot start. The page initialises the worker pool (initThreadPool) before the Bevy app starts.

3The browser main thread never blocks

Atomics.wait is disallowed there, so a contended std::sync::Mutex::lock, a blocking channel recv, rayon::join/scope/par_iter, or any wait on a worker traps or stalls the page. On web the main thread only calls rayon::spawn and polls results with a non-blocking try_recv. Shared caches that workers use (terrain tile LRU, Copernicus raster LRU) take their lock with try_lock and fall back to uncached work, so calling them from any thread never waits. File and network reads on web are async fetch on the main thread; the bytes are then handed to a worker by value.

4Determinism across targets

IEEE + − × ÷ and sqrt are exact everywhere, but transcendental functions (sin, cos, atan2, exp, powf, powi, …) come from the platform libm and can differ in the last bit between glibc, MSVC and wasm; a value next to a mm quantisation boundary then flips, so mm snapping alone does not guarantee equal hashes. Rule: geo-h3, terrain and worldgen call transcendental functions only through the libm crate (pure Rust, the same bits on every target), enforced by per-crate clippy disallowed-methods, and h3o is built without its std feature so it uses libm too. Measured 2026-11-08: the bit-hash of all 343 Vancouver region cell boundaries differed between native (std h3o) and wasm, and matched once h3o used libm. The golden-hash test also runs on wasm32-wasip1 under wasmtime (no browser needed) in CI.

5Time

std::time::Instant::now() panics on wasm32-unknown-unknown. Timing uses web_time::Instant (or Bevy's bevy::platform::time::Instant), enforced by clippy disallowed-methods.

6Compression

Tiles and packets are zstd frames. Decoding uses the pure-Rust ruzstd on every target (one decode path, tested natively); encoding happens only in bake-region (C zstd for terrain tiles, so the size targets of 3.4 hold).

7Memory

Shared wasm memory needs a declared maximum. The web build declares the wasm32 limit, 4 GiB (65,536 pages): it reserves address space only, shared memory cannot shrink, and 19 resident cells of meshes, colliders and Bevy's working set plus allocator fragmentation do not fit comfortably in 1 GiB. Hitting the maximum is a hard out-of-memory abort, so the web smoke test records peak memory.

8Randomness

Engine-free crates never pull getrandom (no OS randomness in generation); the wasm check fails if they do. Bevy's own use of getrandom on wasm is configured in the client per that crate's documentation.

Not in sub-project 1 (web). Running Bevy entirely off the main thread (OffscreenCanvas in a worker), WebGPU, a persistent IndexedDB cache of region packs, a hosting/CDN choice for the alpha, mobile browsers, audio on web, and detachable OS windows (roadmap sub-project 4: on web those panels are overlays).