Storage, hydrate, wire — areas.ground, the hydrate file, hello_ack, protocol 4
ground-types/002 · created 2026-08-17 · updated 2026-08-18
What to do
Give ground a home in the database and carry it to the world process and the client.
game/director/src/schema.sql:ALTER TABLE areas ADD COLUMN IF NOT EXISTS ground SMALLINT[];— nullable (NULL = never painted), row-major(layer*h + y)*w + x, the same indexing asAreaGrid. Range and length are checked in TypeScript; this schema bans bareADD CONSTRAINT.areas.ts:getAreaGround(pool, id)andsetAreaGround(pool, id, cells)(1..64, lengthw*h*layers). Not added toAREA_COLUMNS/rowToArea— every Area query would drag ~10 KB per row.createAreawrites a plain fill;ensureHomeAreawrites the seeded home paint in its sizeUPDATE.- New
groundPaint.ts(seeded PRNG, e.g. mulberry32, seed = hash of the area id):paintPlain,sprinkleVariants(cells, w, h, seed, density, lower),paintHome;paintUnpaintedAreas(pool)called fromdb.ts migrate()— NULL or wrong length → homes sprinkled grass, everything else plain; log[migrate] painted ground for N area(s); idempotent (a second boot logs nothing). areaInstance.ts spawnInstance():groundin the hydrate object (dropped with a warning if the length is wrong, so the world process falls back to plain rather than crashing).area_server.gd:apply_groundright afterbuild_for_areaand before_hydrate(portals must win over paint); cache the marshalled array once (_ground_wire) rather than rebuilding it per peer;_area_payload()gainsground. Annotate the “when authored terrain exists it gets acellskey right here” comment as closed — don’t delete it.- PROTOCOL_VERSION 3 → 4 in
protocol.ts,protocol.gdandtest/helpers/gameClient.ts— an old client can’t see mountains and would walk into invisible walls; the 015 “update needed” path makes the bump survivable. Deploy order (task 008): director, then web export, then APK. client_main.gd _apply_area: the dedupe key gainshash(ground)— otherwise a same-size repaint keeps stale terrain across a respawn.
Definition of done
portal-authored.test.ts:hello_ack.area.ground.length == w*h; an intent into a Mountain cell isintent_rejected.home-portal.test.ts: a home’s ground has more than one distinct hexagram, all Earth-lower; spawn (1,1) and the landing cell (w/2, h−2) are passable.schema.test.ts: column present; migrate paints NULLs and re-paints wrong lengths; a second run is a no-op.stale-client.test.tsstill green at version 4.- Committed and pushed in
land-of-lor.
Where the work lands
land-of-lor/game/director/, game/world/.
What was done (2026-08-17, laptop session)
schema.sql:areas.ground SMALLINT[](nullable; range/length checked in TS).areas.ts:getAreaGround/setAreaGround/ensureAreaGround/paintUnpaintedAreas(not inAREA_COLUMNS);ensureHomeAreapaints the home after sizing it. NewgroundPaint.ts: mulberry32 over FNV-1a,paintPlain,sprinkleVariants,paintHome,defaultPaintFor,encodeGround(the one wire codec).db.ts migrate()→[migrate] painted ground for N area(s).areaInstance.ts spawnInstanceputsgroundin the hydrate file viaensureAreaGround; the loader re-ensures after its size UPDATE.area_server.gd:apply_groundbefore_hydrate,_ground_wirecached,_area_payload().ground; the old “gets acellskey right here” comment annotated as closed.client_main.gd:hash(ground)in the_apply_areakey.- PROTOCOL_VERSION 4 in
protocol.ts,protocol.gd,test/helpers/gameClient.ts. - Tests:
schema.test.ts(column, migrate paints NULL and re-paints a resize, idempotent,setAreaGroundrefuses bad input),portal-authored.test.ts(ground on hello_ack; a step into a Mountain isBLOCKED, into Water lands),home-portal.test.ts(home scatter, deterministic per id);stale-client,loadWorldContent,groundTypesstill green. - Assumption: the wire array is plain JSON (~10 KB for 60×60) —
encodeGround/decode_groundare the two places to change if that ever matters.
View source in repository · also available as raw markdown.