20 KiB
GameWindow Slice 3 — complete live-session ownership
Status: Complete 2026-07-21.
Parent program: docs/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.
Progress ledger:
- A0 — retail lifecycle conformance corrections (
aea957f8): retail active/deleted CharacterSet parsing, validated unattended selection, canonical-account F657, exact F653 confirmation, negotiated-state Disconnect symmetry, closed-channel/flood-safe confirmation wait, and deterministic shutdown tests. Release: 6,605 passed / 5 skipped. - A — owned subscription primitives (
7d452aa6): exact nested GameEvent registration tokens, reverse/idempotent subscription ownership, transactional wiring construction, and adversarial unregister, reentrancy, replacement, and concurrent-dispose coverage. Release: 6,621 passed / 5 skipped. - B — session event and command routers (
961bdd07): focused inbound event and outbound command owners, exact reverse teardown, copied/in-flight callback acceptance gates, inactive-before-EnterWorld publication, concurrent and reentrant disposal serialization, and delayed-dialog indirection that makes displaced transports both inert and collectible. Three-agent review clean. Release: 6,636 passed / 5 skipped. - C — convergent session reset (
4f31a508): fixed ordered manifest, all-attempt named transaction, retry-observable owner resets, canonical live-runtime convergence/identity gate, retained-world preservation, retail dialog completion semantics, and actual-owner A → failed drain → retry → clean-state coverage. Three-agent review clean. Release: 6,659 passed / 5 skipped. - D — complete lifecycle controller (
d9ccf8a6): sole App ownership of resolve/create/bind/Connect/select/EnterWorld/Tick/stop/reconnect, generation-gated reentrancy, pre-publication command activation, convergent staged teardown, transport-free deterministic session tests, and invalid-endpoint validation before UDP allocation. Three-agent review clean. Release: 6,707 passed / 5 skipped. - E — GameWindow cutover (
6a5d9e2e): one controller field, focused lifecycle host, transactional event/command binding, call-time borrowed session resolution, controller-backed retained/dev UI command providers, unified shutdown, and structural regression gates proving the displaced fields and lifecycle bodies remain absent. Three-agent review clean. Release: 6,714 passed / 5 skipped. - F — final review, connected gate, and documentation closeout. All three independent review tracks finished clean. Release: 6,714 passed / 5 skipped. The 306-second connected lifecycle gate passed capped login, five travel/ revisit checkpoints, exact graceful exit, and uncapped fresh-process reconnect; both client processes exited normally. The sole warning was 25 expected world-edge landblock misses already classified by the gate.
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;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.CharacterLogOffandSession.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, whose accepted connected trace proves F653 confirmation, transport disconnect, endpoint release, and fresh-process reconnect.
The controller must preserve these fixed facts:
- Complete routing is installed before
Connect; Connect can synchronously publish initial server time before character selection. - Retail's CharacterList contains separate active and deleted collections.
EnterGame requires a selected GUID in the active collection whose
GetGreyedOutForresult 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. WorldSession.EnterWorldretains its shipped request → ServerReady → CharacterEnterWorld order. F657 usesCharacterList.AccountName, the canonical server-returned spelling, rather than the startup username.- 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.Disposeremains the one wire owner. - The existing close-only adaptation in divergence row AD-44 remains: acdream exits instead of returning to a retained character-select connection.
- 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
LiveCommandBuswhose delegates capture a disposed session; - the no-character path leaks
CombatChatTranslatorsubscriptions; - 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:
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:
- disable outbound commands and mark the router non-accepting;
- detach exact subscriptions and session-scoped translators;
- gracefully dispose the exact old
WorldSession; - detach the borrowed session from the host;
- execute the complete reset plan to convergence;
- create and bind the new session transactionally;
- Connect, validate/select, EnterWorld;
- 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
CombatChatTranslatorand 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
LiveSessionResetPlanwith 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, andCLAUDE.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
Startis idempotent and cannot disturb a healthy active scope; explicitReconnectgracefully 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
GameWindowhas no_liveSession,_commandBus, or_combatChatTranslatorfield.TryStartLiveSession,ClearInboundEntityState, andWireLiveSessionEventsno 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:
- login selects the same first character and reveals a complete world;
- system chat, say/tell/channel/server commands, vitals, combat state, and shortcuts/components still update;
- one portal/recall completes with the same reveal lifecycle;
- native close logs the F653 request/confirmation and transport Disconnect;
- ACE releases the exact endpoint;
- 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.