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.
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
- Done: Write the workspace
Cargo.toml - Done: Write
rust-toolchain.toml - Done: Write
.github/workflows/ci.yml - Done: Run
cargo build --workspace - Done: Run
cargo test --workspace - Done: Commit
Lands as chore: scaffold cargo workspace and CI
0aScaffold follow-uprepo5/5 steps, done
- Done: Edit
Cargo.toml - Done: Edit
ci.yml - Done: Edit
.gitignore - Done: Run
- Done: Commit
Lands as chore: pin CI toolchain, add missing workspace deps, bevy-free check
2geo-h3 crategeo-h36/6 steps, done
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Write
examples/cells_of_zone.rs - Done: Run
- Done: Commit
Lands as feat(geo-h3): resolutions, rings, cell polygons, local frame
1Data coverage check (verification, no library code)repo6/6 steps, done
- Done: Write
coverage.sql - Done: Write
coverage.sh - Done: Run the Copernicus inventory
- Done: Run
coverage.sh - Done: Write the note
- Done: Commit
Lands as docs: record Vancouver data coverage and Copernicus inventory
0bWeb-target gates (spec Amendment A1)repo6/6 steps, done
- Done: Write the failing test
- Done: Run
- Done: Implement
- Done: Add the clippy files and
tools/wasm-check.sh - Done: Edit
ci.yml - Done: Commit
Lands as ci: wasm32 gates for engine-free crates; libm-backed h3o
3worldsource types, trait and fixture sourceworldsource5/5 steps, done
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Run
- Done: Commit
Lands as feat(worldsource): skeleton types, WorldSource trait, fixture source
4PgWorldSource (Overture warehouse)worldsource6/6 steps, done
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Run
- Done: Write
examples/pg_probe.rs - Done: Commit
Lands as feat(worldsource): Overture warehouse source
5RegionDb (SQLite) and SqliteWorldSourceworldsource5/5 steps, done
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Run
- Done: Commit
Lands as feat(worldsource): per-region SQLite store
6Terrain tile codecterrain5/5 steps, done
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Run
- Done: Commit
Lands as feat(terrain): tile codec
7Copernicus GeoTIFF sourceterrain6/6 steps, done
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Run
- Done: Manual verification
- Done: Commit
Lands as feat(terrain): Copernicus GLO-30 source
8Terrain bake and runtimeterrain5/5 steps, done
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Run
- Done: Commit
Lands as feat(terrain): zone bake, flattening, runtime sampling and cell meshes
9worldgen quantisation, params, seeding and blocksworldgen5/5 steps, done
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Run
- Done: Commit
Lands as feat(worldgen): quantisation, params, seeding, block polygonization
10Block statistics, landmarks, building samplingworldgen5/5 steps, done
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Run
- Done: Commit
Lands as feat(worldgen): block statistics, landmarks, building sampling
11Structure (floors, core, walls, openings)worldgen5/5 steps, done
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Run
- Done: Commit
Lands as feat(worldgen): building structure
12Damage pass and generate_cell with golden testworldgen6/6 steps, done
- Done: Build the fixture
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Run
- Done: Commit
Lands as feat(worldgen): damage pass, generate_cell, golden fixture
13bake-region CLIbake-region6/6 steps, done
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Run
- Done: Real bake
- Done: Commit
Lands as feat(bake-region): bake a zone into a region SQLite
14Client scaffold, config, static groundclient5/5 steps, done
- Done: Write a failing test
- Done: Run
- Done: Implement
- Done: Run
- Done: Commit
Lands as feat(client): app scaffold, config, region handles
13aClip area features to cellsbake-region6/6 steps, done
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Run
- Done: Re-bake the downtown zone
- Done: Commit
Lands as feat(bake-region): clip area features to cell plus margin
15Cell streamingclient5/5 steps, done
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Run
- Done: Commit
Lands as feat(client): ring-based cell streaming on worker threads
16Meshingclient5/5 steps, done
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Run
- Done: Commit
Lands as feat(client): terrain, road and building meshing with LOD
16aWalkable core stair (amendment, 2026-11-09)worldgen6/6 steps, done
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Update the golden
- Done: Run
- Done: Commit
Lands as fix(worldgen): walkable core stair (entry/exit doors, 35° bound); new golden
17First-person playerclient, sim6/6 steps, done
- Done: Write a failing test
- Done: Run
- Done: Implement
- Done: Bake a second zone
- Done: Run
- Done: Commit
Lands as feat(client,sim): first-person kinematic controller
18Debug overlays and generation budgetclient4/4 steps, done
- Done: Write failing tests
- Done: Implement
- Done: Run
- Done: Commit
Lands as feat(client): debug overlays and generation budget stats
19Smoke list, README, mirrorclient5/5 steps, done
- Done: Step 0 (controller ruling 2026-11-09, before the smoke run): chunk the static colliders
- Done: Write
docs/SMOKE.md - Done: Update
README.md - Done: Run the smoke list
- 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
- Done: Write failing tests
- Done: Run
- Done: Implement
- Done: Run it
- Done: Write the web section of
docs/SMOKE.md - Done: Add the CI
webjob
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.
Ruins
In progress, 25 of 25 tasks doneWalk 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.
Presence
In progress, 25 of 25 tasks doneSeveral players in the same ruins; zone server; border transfer.
Milestone notes
- Zone server binary (
servercrate) runningsimheadless under BevyMinimalPlugins; one process per res-5 zone; config from the region SQLite plus azoneargument. - Spike first:
lightyearvsbevy_repliconon 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,
accountschema). - Auth: start with a dev token; Steam later.
- Zone server binary (
Claim and build
Not startedClaim 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 (
piecetable): 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.
- Claim a res-7 cell's floor or plot; ownership rules in
Survive
Not startedResources, inventory, crafting, loot, Cold Storage.
Milestone notes
- Resources and loot placement driven by damage class; inventory; crafting from
reciperows; 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.
- Resources and loot placement driven by damage class; inventory; crafting from
Cycle
Not startedWipe 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.
Genre
survival MMO, no story campaign. Cosmic Library fiction is a mood board only.
Why. Solo developer; the loop is the product.
Stack
Rust throughout. Bevy for client and headless server. A shared
simcrate 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.
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.
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.
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.
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).
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.
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.
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.
World data sourcing
WorldBuilder's Postgres (Overture warehouse, OSM planet) is read read-only through a
WorldSourcetrait 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.
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.
Region catalogue with an
availableflag. 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.
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.
| Figure | Value | What it means |
|---|---|---|
| Cells in the first zone | 49 | Level-7 hexagons in the downtown Vancouver zone, the first build target. |
| Cells in the region | 343 | Seven zones of 49 cells each. |
| Buildings in the zone | 151,876 | Distinct Overture building footprints (release 2026-10-21). |
| Road segments in the zone | 57,563 | Distinct Overture road segments, same release. |
| Pass mark | 2,000 / 500 | Minimum buildings and road segments for the warehouse to count as a usable source. It passes by a wide margin. |
| Buildings with a height | 58.4% | 35,688 of 61,133 footprints in an earlier, smaller extract. The generator defaults heights by class for the rest. |
| Elevation tiles on hand | 26,475 | Copernicus 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.
| Piece | Native (Linux, later Windows) | Web (browser) |
|---|---|---|
geo-h3, worldgen, terrain runtime, sim | same code | same code, compiled to wasm32; must produce the same geometry_hash |
| Region data | per-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 meshing | Bevy AsyncComputeTaskPool | a wasm-bindgen-rayon pool of Web Workers (Bevy's own task pools are single-threaded on wasm) |
| ECS schedule, rendering, input | Bevy, multi-threaded executor | Bevy on the browser main thread, single-threaded executor; wgpu types are not Send, so rendering stays there |
| Graphics backend | Vulkan | WebGL2 for the alpha (widest browser support on Linux and Windows); WebGPU is a later switch |
bake-region, PgWorldSource/Postgres, Copernicus GeoTIFF reading, golden-hash regeneration | native only | not 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).