← World map and portals · all tasks

One body everywhere — entity ids move from the world to the account, and the on-screen keyboard finally runs at all

done priority 1 estimate M

world-map-and-portals/014 · created 2026-08-16 · updated 2026-08-18

1. Entity ids belong to the account

Kris, reading 013’s write-up: “entity ids absolutely should not be per-world, obviously they should be per account.” Right — and it is the root cause one level below 013. A stale body could only exist because the same player wore a different id in every Area.

accounts.entity_id is minted once, at account creation, and travels to the world process with the join grant beside display_name and character_seed. The world uses it verbatim and only mints one when it wasn’t given one (a standalone or load-test run with no director).

Two portal tests asserted “a fresh entity gets a new, different random id” — that assertion was the per-world assumption. They now assert the id is the same, and that what proves the entity left is its position: a fresh body on the spawn cell rather than the transferred one where it walked to.

2. The on-screen keyboard had never worked, anywhere

Kris asked for a way to enter a name in Chrome on an iPad. There already was one, shipped that morning and marked “not verified on a real iPad”. It had never worked on any device in any browser. Three separate faults, each fatal on its own, and none of them findable by reading:

  1. The feature was never on. The availability probe tested probe is bool, and JavaScriptBridge hands a JS boolean back as an int (probe=1, TYPE_INT). available() was false everywhere, so attach() returned immediately and no field was ever hooked up. This is why no amount of reasoning about iOS gesture policy explained the iPad: nothing was running to be refused.
  2. focus() was always a frame late. WebKit — every browser on iPadOS, Chrome included — opens the keyboard only for a focus() inside the gesture’s own DOM handler. The old design focused from Godot’s focus_entered, on the stated assumption that the signal fires inside the tap handler. It does not: Godot buffers input and flushes it at the top of the next frame. The engine cannot win this, so it stopped trying — it publishes where its fields are (as fractions of the viewport) and the browser hit-tests touchstart itself.
  3. The engine took the focus back. Godot’s web export calls canvas.focus() from its own touch listener, and focuses a hidden IME div whenever a LineEdit takes GUI focus — either one blurs the input and shuts the keyboard a frame after it opened. preventDefault does not stop propagation, so a tap landing on a field is now swallowed outright, gesture and all.

The consequence is the shape of the whole thing: the DOM input IS the input, and the LineEdit mirrors it through one callback registered once. Nothing gives the field GUI focus.

shell/verify-touch-keyboard.mjs is the new check, and the reason this one is trustworthy: it drives the real exported build in headless Chrome over CDP with touch emulation, taps through to the signup field, and asserts the input is focused inside the touch event, that the bar survives a second (the canvas-steals-focus failure), and that typing reaches the engine. Every one of the three faults was caught by it, not by reading the code. 8/8.

What it still cannot check is WebKit’s own policy — no iPad here — but nothing in the design now depends on a browser being lenient about it, which is exactly what the old one did.

Verified

Director 155 tests, Godot 116, verify-touch-keyboard 8/8, and on production after deploying: verify-home-door 10/10, verify-accounts-e2e 32/32. Shipped as APK v6 and a web re-export at db6a49b.

Housekeeping: the DoorProbe and MapProbe characters were cleared from the Village (the recurring live-verifier hazard first noted in 010) — which also cleared Kris’s own bodies in the Village and The Badlands, so he respawns at the entry cell there. Home, account and collection untouched.

View source in repository · also available as raw markdown.