← World map and portals · all tasks
The door that went nowhere — a portal frame lost to the disconnect behind it, the test map that flashed on every launch, and your own home missing from your own map
world-map-and-portals/012 · created 2026-08-16 · updated 2026-08-18
What prompted it
Kris played v4 on the phone on 2026-08-16, with the readable Area name and the world map’s “You are here” marker newly in place — and those two things are exactly what made the real bug legible. Five screenshots and a recorded transcript. Three findings, all real.
1. The door out of your own home went nowhere
Step into the portal at the bottom of your home and you reappear in the top-left corner of your own home. Step in again and nothing happens at all.
The server was never wrong. A TypeScript client walks home → Village against the live
droplet and gets portal_result with the right destination every time. The droplet’s own
logs show the give-away: across the whole session only the home world process was ever
spawned. No Village process, so no transfer ever completed.
What breaks is one frame. world_server.gd sent portal_result and called
disconnect_peer() in the same tick, and this Godot WebSocketPeer reproducibly only
ever surfaces the close — never the data frame behind it. The client then took its “the
connection died” branch, whose recovery is a fresh join_world against the world it was in.
But transfer step 1 had already erased its entity from that world, so the world built a
brand-new one at the default spawn, (1,1). The top-left corner. And the second attempt was
a different, fresh entity standing on a door cell it had never stepped onto — portals
fire on the transition, not on standing there.
Reproduced exactly with the real Godot client, headless, against production before anything was touched. That reproduction is the whole reason this was a one-sitting fix rather than a week of guessing.
client_main.gd’s own comment on _on_game_disconnected() had diagnosed this race in
detail and concluded it was unfixable from the read side. It was right. The fix belongs
on the write side, which is where nobody had looked:
world_server.gddefers the hang-up. A departing peer goes intodeparting_peerswith a 1.5s deadline instead of being cut off mid-frame. The entity bindings still drop immediately, so a latermove_intentstill answersNOT_HELLOexactly as before. A well-behaved client closes the socket itself the moment it appliesportal_result, so the timer almost never fires — it only stops a client that ignored us from lingering.- The client’s portal fallback arms from the live grid. It armed from the
--portal-cellCLI list, which only a test scenario ever has — so the one mitigation that existed could not fire in the shipped game. It now arms fromlocal_grid’s own portal cells, which arrive with the Area precisely so they can be drawn. - And it now has somewhere correct to fall back to. New
resume_portalRPC: the client is deliberately never told where a portal leads, so it asks the director — which already remembers every completed transfer — and arrives exactly asportal_resultwould have landed it. It answers only the account whose entity it was.
2. The ancient test map flashed up on every single launch
A small green grid with one grey block, for about half a second, every time the game started.
It is grid.gd’s build_test_map() — 10×10, four blocked cells at y=5 — the fixture both
sides hardcoded back in the multiplayer POC. client_main builds it in _ready() so
movement code always has a grid, which is fine; the bug was that it also drew it, in
the gap before hello_ack named the real Area. Nothing is drawn now until the server has
named an Area.
Worth recording while it was asked: blocked cells today are one int per (x, y, layer) in a
flat PackedInt32Array, rendered as grey boxes. Authored areas have none —
build_for_area() makes an open field of the right dimensions, because ground types aren’t
designed yet (world-traversal-and-ground). The test map is the only thing in the codebase
that has ever had a wall in it.
3. Your own home wasn’t on your own map
Not fogged — absent. Homes have no slug, and the map query was WHERE slug IS NOT NULL.
That is exactly what keeps every other player’s home off the map, and it stays. But the
person standing in one has to be able to find it, and with the new “You are here” marker
there was no ball for it to sit in.
The map now carries the caller’s own home, and only theirs: a fixed opaque id
your-home (never a database id), named, ownerLabel: "yours", positioned beside the hub it
opens onto, with its door drawn. The two “Locked realm” balls Kris asked about are Deep Cut
and the Wizard Battle Pit — both restricted, neither a home.
On Kris’s expectation
Right about the first hop: the home door leads to the Village of Lor. The Badlands is one further — Village → Beginner Arena → Badlands. There is no direct Village→Badlands portal in the authored world.
Verified
New npm run verify-home-door, 10/10 against the live droplet: a real client out of a
real home, into the Village, arriving as the same character on the hub entry cell, plus the
map it sees on arrival and what a stranger sees. verify-accounts-e2e 32/32,
verify-world-map 10/10 — no regression. Director 153 tests, Godot 116. And the actual
Godot client, headless against the droplet, landing in Village of Lor 40×40 at (20,38).
Shipped as APK v5 (installed to Kris’s phone over adb) and a re-exported web build —
/play had been serving fbf8bed, which predates all of this.
View source in repository · also available as raw markdown.