acdream/docs/plans/2026-07-21-gamewindow-slice-3-live-session.md

404 lines
19 KiB
Markdown

# 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.
**Progress ledger:**
- [x] 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.
- [x] 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.
- [x] 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.
- [x] 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.
- [x] 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 (in progress).
- [ ] F — final review, connected gate, and documentation closeout.
## 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.