← Connection and sync — never pretend to be online · all tasks
The client connection state machine — freeze rather than fake, reconnect visibly, give up honestly
connection-and-sync/002 · created 2026-08-18 · updated 2026-08-18
What to do
- New
game/world/client/game_channel.gd(RefCounted, table-driven, pure where possible):IDLE → CONNECTING → HELLO_SENT → LIVE → STALLED → DEAD(+LEAVINGfor portal / exit). Inputs: socket state, a received frame, ack / reject /SUPERSEDED, a send failure, silence (6 s), oldest-unacked-intent age (2 s), portal events, explicit leave, app pause / resume. Ownsgame_ws, the timers,pending_intents(bounded, stamped with send time, discarded on reconnect — the server’s position wins), the closing-peer bookkeeping. Emitsstate_changed(from, to, reason). client_main.gdconsumes it:send_moveonly inLIVE; inSTALLEDinput is accepted but not predicted — the avatar stops — and a “Reconnecting…” overlay shows;DEAD→ rejoin through the director with a 15 s budget in amultiplayerArea, then a “Connection lost” screen (Retry / Main menu) and no play untilLIVEagain.- New
client/director_channel.gd— a view overaccount_session.gd(ONLINE / OFFLINE / RECONNECTING) thatclient_mainlistens to: a director drop whileLIVEis fine; whileDEADit blocks the rejoin, and says so. - Lifecycle:
NOTIFICATION_APPLICATION_PAUSED / RESUMED,FOCUS_OUT / IN,WM_GO_BACK_REQUEST(Android back = confirm,leave_area, clean close). On resume both channels are probed: game — a frame within 2 s orSTALLED→ reconnect; director — a 5 swhoami. - Configure
WebSocketPeerbuffers (inbound_buffer_size,max_queued_packets) beforeconnect_to_url; build the ground mesh outside the packet-drain frame (deferred, chunked).
Definition of done
test_runner.gd drives every transition of the table; vitest with the fake director/Area (from
001) covers: silence, unacked-only stall (pongs and updates flow, intents refused),
close-without-frame, portal hop then drop, director dead during rejoin, resume probes,
first-frame-late connects. The Godot-backed suites and the client harness still pass; 012’s
rule holds (a drop during a pending portal defers to the portal fallback, never rejoins the
origin).
Where the work lands
land-of-lor/game/world/client/, game/director/test/.
What was done (2026-08-18, laptop session)
client/game_channel.gd: the machine (IDLE / CONNECTING / HELLO_SENT / LIVE / STALLED / DEAD / LEAVING) with STALL_MS 2 s, SILENCE_DEAD_MS 6 s, UNACKED_DEAD_MS 6 s, GIVE_UP_MS 15 s, PING_MS 3 s;tick()returnsping/declare_dead/give_up; 23 checks intest_runner.gd(199 total).client_main.gdobeys it:_send_movepredicts only in LIVE (move_heldlogged once per stall); STALLED snaps the avatar toconfirmed_pos; DEAD closes and re-joins with retries inside the budget (rejoin_retry), then_give_up()→area_lostwith a sentence on the title; every transition loggedtype=connection from=… to=… reason=…; buffers 1 MiB / 8192 packets; ground mesh built off the drain frame; app resume probes the game channel (on_resumed);connection_changedsignal → HUD notice (hud.set_connection_notice, “Reconnecting…”).- Android back (
quit_on_go_back=false+app._notification) leaves cleanly and tells the director (leave_area); on the title it quits. - Scenarios added to
client-liveness.test.ts: unacked (frames flow, moves unanswered → STALLED, avatar held, DEAD, re-join) and give-up (dies, director refuses the re-join → retries, thengive_upat ~15 s from the stall). Seven scenarios green; harness and Godot-backed suites green. - Not done here (folded into 004): the director-channel view object —
client_mainstill reads_session.is_director_open()directly; the wait is budgeted. - Shipped with 003: director + world
cee0ccd, web on/play, APK v13.
View source in repository · also available as raw markdown.