371 lines
17 KiB
Markdown
371 lines
17 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.
|
|
|
|
## 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.
|