docs(architecture): plan live session ownership slice
This commit is contained in:
parent
9a150e2421
commit
bacc7e45a9
1 changed files with 371 additions and 0 deletions
371
docs/plans/2026-07-21-gamewindow-slice-3-live-session.md
Normal file
371
docs/plans/2026-07-21-gamewindow-slice-3-live-session.md
Normal file
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue