From bacc7e45a9760171b66f328ef95331005ba6357e Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 21 Jul 2026 10:16:14 +0200 Subject: [PATCH] docs(architecture): plan live session ownership slice --- ...6-07-21-gamewindow-slice-3-live-session.md | 371 ++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 docs/plans/2026-07-21-gamewindow-slice-3-live-session.md diff --git a/docs/plans/2026-07-21-gamewindow-slice-3-live-session.md b/docs/plans/2026-07-21-gamewindow-slice-3-live-session.md new file mode 100644 index 00000000..cbed3d14 --- /dev/null +++ b/docs/plans/2026-07-21-gamewindow-slice-3-live-session.md @@ -0,0 +1,371 @@ +# GameWindow Slice 3 — complete live-session ownership + +**Status:** Active 2026-07-21. +**Parent program:** [`docs/architecture/code-structure.md`](../architecture/code-structure.md), Slice 3. +**Baseline:** `9a150e24`; `GameWindow.cs` is 14,546 lines, 277 fields, and +190 methods before this slice. +**Behavior rule:** Preserve the accepted connect, EnterWorld, inbound dispatch, +chat/command, and retail graceful-close behavior while correcting the +named-retail character-list, unattended-selection, canonical-account, and +disconnect-state gaps discovered by the slice audit. This slice changes App +ownership and fixes proven session-lifetime defects; it does not add a retained +character-selection UI or redesign the transport. + +## 1. Outcome + +`LiveSessionController` becomes the sole App owner of one exact +`WorldSession` lifetime: + +- endpoint resolution and session construction; +- event routing before the first network receive; +- Connect → CharacterList validation → first-character selection → EnterWorld; +- active command-bus publication; +- per-frame Tick; +- graceful stop, replacement/reconnect, and disposal; +- session-generation and reentrant-operation gates; +- failure convergence back to a clean offline state. + +`LiveSessionEventRouter` owns typed session subscriptions and exact +unsubscription. It routes into focused entity/environment/chat/vital and Core +state handlers but owns no entity map, renderer, UI model, or gameplay state. +`LiveSessionResetPlan` owns the ordered, failure-isolated reset transaction but +does not become a second state owner. + +At slice exit, `GameWindow` retains one `_liveSessionController` field plus +narrow composition/lifecycle callbacks. `_liveSession`, `_commandBus`, +`_combatChatTranslator`, `TryStartLiveSession`, `ClearInboundEntityState`, and +`WireLiveSessionEvents` are removed. Every outbound caller resolves the current +borrowed session from the controller at call time; no feature caches it. + +## 2. Retail and shipped oracles + +The accepted transport behavior remains pinned to: + +- [`2026-07-20-retail-graceful-logout-pseudocode.md`](../research/2026-07-20-retail-graceful-logout-pseudocode.md); +- `Proto_UI::LogOffCharacter @ 0x00546A20`; +- `CharacterSet::UnPack @ 0x004FE340`; +- `gmCharacterManagementUI::EnterGame @ 0x004ED440`; +- `CPlayerSystem::LogOnCharacter @ 0x0055F890`; +- `CPlayerSystem::RequestLogOff @ 0x00562DD0`; +- inbound login-message dispatch `@ 0x0055C963`; +- `CPlayerSystem::ExecuteLogOff @ 0x0055D780`; +- `ClientNet::ExitWorldDisconnect @ 0x00541E00`; +- `ClientNet::LogOffServer @ 0x00543EF0`; +- `SharedNet::SendOptionalHeader @ 0x00543160`; +- ACE `CharacterHandler.CharacterLogOff` and + `Session.SendFinalLogOffMessages`; +- Holtburger's connect/PlayerCreate/LoginComplete flow as a protocol + cross-check, not as the logout oracle where its shortcut differs from retail; +- [`2026-07-20-automated-world-lifecycle-gate.md`](2026-07-20-automated-world-lifecycle-gate.md), + whose accepted connected trace proves F653 confirmation, transport + disconnect, endpoint release, and fresh-process reconnect. + +The controller must preserve these fixed facts: + +1. Complete routing is installed before `Connect`; Connect can synchronously + publish initial server time before character selection. +2. Retail's CharacterList contains separate active and deleted collections. + EnterGame requires a selected GUID in the active collection whose + `GetGreyedOutFor` result is not positive. acdream's unattended auto-entry is + an explicit adaptation: select the first active, non-greyed character and + reject an empty/all-greyed list. A future retained character-select screen + is separate M4/UI work. +3. `WorldSession.EnterWorld` retains its shipped request → ServerReady → + CharacterEnterWorld order. F657 uses `CharacterList.AccountName`, the + canonical server-returned spelling, rather than the startup username. +4. Closing an in-world session sends the eight-byte `[0xF653, active GUID]` + request, drains without App world callbacks until the server's exact + four-byte opcode-only F653 confirmation, sends transport Disconnect, and + only then releases the socket. Once ConnectRequest negotiation has supplied + receiver id/iteration, character-select, entering-world, and failed + post-negotiation sessions also send transport Disconnect before socket + release. `WorldSession.Dispose` remains the one wire owner. +5. The existing close-only adaptation in divergence row AD-44 remains: acdream + exits instead of returning to a retained character-select connection. +6. The registered fixed ConnectResponse delay (UN-6) and initial + LoginComplete-readiness gap (TS-28) remain explicit Core.Net follow-ups. + They are not copied into, disguised by, or claimed by the App controller. + +No new automatic network retry or logout shortcut is introduced. Core.Net +changes stay surgical: retail CharacterSet parsing/selection data, exact F653 +confirmation, and negotiated-state Disconnect symmetry. The accepted packet +codec, handshake loops, confirmation wait, and receive-thread design are not +rewritten. + +## 3. Proven baseline defects closed by this slice + +The pre-slice audit found real lifetime defects, not merely style debt: + +- repeated startup can overwrite an active controller/session without stopping + the exact prior lifetime; +- no-character and failure paths can leave a `LiveCommandBus` whose delegates + capture a disposed session; +- the no-character path leaks `CombatChatTranslator` subscriptions; +- anonymous event wiring cannot be explicitly detached; +- `GameEventDispatcher.Unregister(type)` cannot safely distinguish an old + registration from a replacement; +- the reset boundary leaves player identity, Core character state, Turbine + rooms/cookie, shortcuts/components, combat/item mana/social state, motion + metadata, and player-mode references from the prior session; +- one reset-stage failure can prevent later network/effect cleanup; +- a synchronous handler can request stop/reconnect while `Tick`, Connect, or + EnterWorld is still unwinding. + +Each is fixed at its owner. No suppression flag, delay, retry loop, or stale-GUID +filter is acceptable. + +## 4. Architecture and interfaces + +### 4.1 Controller state + +`LiveSessionController` exposes: + +```csharp +LiveSessionStartResult Start(RuntimeOptions options, ILiveSessionLifecycleHost host); +LiveSessionStartResult Reconnect(RuntimeOptions options, ILiveSessionLifecycleHost host); +void Stop(); +void Tick(); + +WorldSession? CurrentSession { get; } // borrowed; never cache +ICommandBus Commands { get; } // NullCommandBus outside active InWorld +bool IsInWorld { get; } +ulong SessionGeneration { get; } +``` + +The controller uses an injected internal operations seam for deterministic +tests (resolve, create, connect, select, enter, tick, dispose). Production +operations call the real `WorldSession`; tests do not open UDP sockets. This is +an App orchestration seam, not a duplicate protocol implementation. + +Every start attempt and accepted session has a monotonically increasing +generation. Lifecycle calls made synchronously from a host/event callback mark +the current attempt non-accepting and defer stop/reconnect until the outer +operation unwinds. The outer attempt rechecks its generation after every +callback and blocking network phase, so it cannot publish an old session after +reentrant replacement. + +Replacement order is fixed: + +1. disable outbound commands and mark the router non-accepting; +2. detach exact subscriptions and session-scoped translators; +3. gracefully dispose the exact old `WorldSession`; +4. detach the borrowed session from the host; +5. execute the complete reset plan to convergence; +6. create and bind the new session transactionally; +7. Connect, validate/select, EnterWorld; +8. publish active commands only after success. + +### 4.2 Lifecycle host + +`ILiveSessionLifecycleHost` is a narrow composition boundary, not a context +bag. It has focused lifecycle calls only: + +- bind one exact session and return an owned `LiveSessionBinding`; +- reset session-scoped App state; +- report connecting/connected status; +- apply the selected character identity; +- apply post-EnterWorld UI/settings state; +- detach the exact session. + +Entity, environment, and effect packets continue through focused sink +interfaces. Later Slice 4 replaces the GameWindow entity sink with the two +planned live-entity integration controllers without changing the session +controller. + +### 4.3 Exact subscription ownership + +`LiveSessionEventRouter` stores named delegates against one exact event source, +sets `Accepting = false` before teardown, and unsubscribes in reverse order. +Construction is transactional: a partially created router unwinds every prior +registration. + +`ObjectTableWiring.Wire`, `CombatStateWiring.Wire`, and +`GameEventWiring.WireAll` return idempotent owned registrations. The GameEvent +dispatcher gains registration tokens that restore/remove only when their exact +handler is still current. Nested registration A → B is safe when A disposes +first: disposing B skips the retired A node and restores the session-owned +predecessor (not a dead callback). + +The router groups dependencies by focused responsibility: + +- live entity/physics/effect sink; +- environment/time sink; +- Core object/combat/spell/player/social state bridge; +- chat/vital bridge; +- command router. + +There is no `GameWindowContext`, GUID table, renderer registry, or generic +service locator. + +### 4.4 Command lifetime + +`LiveSessionCommandRouter` owns one `LiveCommandBus`, the server/chat routing, +and the exact session captured by its handlers. The controller exposes this bus +only while that session is the active in-world generation. Before stop or +replacement it returns `NullCommandBus`, so a retained panel can never send via +the displaced socket. + +Turbine channel resolution and labels move with the chat command/event owner. +`ClientCommandController` remains the retail command behavior owner; GameWindow +supplies its existing focused bindings during composition. + +### 4.5 Reset convergence + +`LiveSessionResetPlan` is an ordered list of named owner-reset operations. It +runs every stage even when one fails, aggregates errors, and forbids a new +session from starting until all stages converge. The plan clears: + +- mouse-look, player mode/cameras, auto-entry, and teleport/reveal transit; +- session origin/counters and selected-character identity; +- equipped/external-container projections; +- object, spell, magic, combat, item-mana, local-player, friends, squelch, + Turbine-chat, shortcut, and desired-component state; +- interaction/selection and selection-presentation state; +- liveness and canonical live entities (including GpuWorldState persistence); +- remote teleport, pending F754/F755 effects, and deferred animation hooks. + +`LocalPlayerState.Clear`, `TurbineChatState.Reset`, and `SquelchState.Clear` +are added to their real state owners. Chat history is intentionally retained +across an in-process reconnect, matching the existing UI behavior, but its +local-player GUID resets to zero. Vitals GUID, diagnostic player GUID, run/jump +skills, character options, motion table, movement truth/shadow, active toon, +shortcuts, and desired components reset to pre-login values. + +## 5. Implementation sequence + +### Commit A0 — retail lifecycle conformance corrections + +- Write one consolidated pseudocode note for `CharacterSet::UnPack`, + `gmCharacterManagementUI::EnterGame`, `CPlayerSystem::LogOnCharacter`, and + negotiated logout/disconnect ordering. +- Parse active and deleted CharacterList collections with cursor/truncation + tests and preserve status/account/slot/boolean fields. +- Add the unattended first-active-non-greyed selector and canonical-account + EnterWorld input. +- Require an exact four-byte F653 confirmation and send transport Disconnect + for every negotiated session state. +- Keep UN-6, TS-28, and AD-44 explicitly registered and unchanged. + +### Commit A — owned subscription primitives + +- Add nested-safe owned registrations to `GameEventDispatcher`. +- Make ObjectTable, CombatState, and GameEvent wiring return disposables. +- Add exact teardown, nested replacement, reverse disposal, and idempotence + tests in Core.Net. + +### Commit B — session event and command routers + +- Add focused live-session event source/sink contracts. +- Route every current direct WorldSession event exactly once. +- Move chat/Turbine/vital routing and the live command bus out of GameWindow. +- Own `CombatChatTranslator` and all wiring registrations transactionally. +- Test post-dispose silence, partial-construction unwind, nested events, and + router A/B replacement. + +### Commit C — convergent session reset + +- Add missing reset APIs to Core state owners. +- Add `LiveSessionResetPlan` with named, failure-isolated stages. +- Compose the complete current session manifest and test mutated A → reset → + clean B state, including one throwing stage. + +### Commit D — complete lifecycle controller + +- Move resolve/create/Connect/CharacterList/selection/EnterWorld/stop/reconnect + into `LiveSessionController`. +- Add generation and operation-depth gates. +- Add typed start results for Disabled, MissingCredentials, NoCharacters, + Connected, Deferred, and Failed. +- Test ordering, duplicate/reentrant start/stop/reconnect, Connect and + EnterWorld failures, tick generation, and exact-once cleanup. + +### Commit E — GameWindow cutover + +- Implement the narrow host and packet sink interfaces. +- Replace all direct session field reads with borrowed controller resolution. +- Replace both UI command-bus providers with `controller.Commands`. +- Delete the three old lifecycle/wiring methods and the displaced fields. +- Preserve OnLoad, frame Tick, and shutdown stage order. + +### Commit F — review corrections and documentation + +- Run three independent read-only reviews: retail conformance, + architecture/lifetime, and adversarial tests. +- Fix every confirmed issue and repeat review until clean. +- Run focused App/Core.Net tests, Release build, and the full Release suite. +- Run the existing connected login/command/chat/portal/graceful-close/ + fresh-process-reconnect gate when local ACE is available. +- Update code structure, roadmap, milestones, issues/divergence if needed, + durable session memory, `AGENTS.md`, and `CLAUDE.md`. + +## 6. Automated acceptance matrix + +### Controller + +- Routing is complete before Connect's synchronous server-time callback. +- First active non-greyed CharacterList entry, GUID, name, canonical account, + and active-list index are exact; deleted/greyed entries are rejected. +- Empty list, Connect failure, EnterWorld failure, and binding failure leave no + active session/router/bus and clean exactly once. +- Duplicate `Start` is idempotent and cannot disturb a healthy active scope; + explicit `Reconnect` gracefully stops A before B state is reset/wired. +- Stop/reconnect requested during bind, Connect, selected-character callback, + EnterWorld, or Tick cannot resurrect the outer generation. +- Tick reaches only the exact current session; stopped/disposed Tick is a no-op. +- Stop and Dispose are idempotent. + +### Router and command bus + +- Every direct session event routes once while accepting. +- Dispose is reverse-order, idempotent, and post-dispose events are ignored. +- Partial construction unwinds prior registrations. +- GameEvent A/B nested ownership restores the nearest live predecessor. +- Old command delegates are unreachable before old-session disposal and cannot + send after replacement. +- Domain-handler exceptions follow an explicit policy without corrupting + subscription ownership. + +### Reset + +- Every named stage is attempted once even when another throws. +- Failure blocks construction of session B and reports all stage errors. +- Player identity, Vitals/Chat identity, diagnostic GUID, selected-character + metadata, Core state, social/chat-room state, projections, live runtime, + pending effects, and hooks all return to pre-login values. +- Chat history is retained but no old own-GUID classification remains. +- Live runtime convergence removes the old persistent player classification. + +### Integration + +- `GameWindow` has no `_liveSession`, `_commandBus`, or + `_combatChatTranslator` field. +- `TryStartLiveSession`, `ClearInboundEntityState`, and + `WireLiveSessionEvents` no longer exist. +- No substantial session callback body remains in `GameWindow`. +- WorldSession retains the 8-byte request → exact 4-byte confirmation → + Disconnect order and extends Disconnect symmetry to every negotiated state. +- Release build and full suite are green. + +## 7. Connected gate + +Run a normal capped Release client against local ACE and prove: + +1. login selects the same first character and reveals a complete world; +2. system chat, say/tell/channel/server commands, vitals, combat state, and + shortcuts/components still update; +3. one portal/recall completes with the same reveal lifecycle; +4. native close logs the F653 request/confirmation and transport Disconnect; +5. ACE releases the exact endpoint; +6. a fresh process reconnects cleanly with no old character state, command + route, entities, effects, or persistent GUIDs. + +The gate is behavioral and resource-oriented. It does not create a new visual +approval pause; final campaign visual acceptance remains after Slice 8. + +## 8. Review policy + +One primary agent performs edits. Read-only agents independently audit retail +conformance, architecture/lifetime, and adversarial tests after each ownership +commit. Confirmed findings are fixed at their root and re-reviewed. No reviewer +edits the shared worktree, and no callback facade back into a substantial old +GameWindow body counts as extraction completion.