diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 20ddaeea..f000be6c 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -35,9 +35,10 @@ What does NOT go here: every late edge explicitly. I.8a now owns the focused session factory, command targets, and sole gameplay input subscriber before frame publication. I.8b now owns the complete update/render construction, exact atomic frame - publication, and lifecycle resource snapshot source. `GameWindow.OnUpdate` - and `GameWindow.OnRender` are typed orchestration handoffs and the class is - 1,919 raw lines. I.8c–I.9 plus the remaining + publication, and lifecycle resource snapshot source. I.8c makes session + start the terminal `OnLoad` operation. `GameWindow.OnUpdate` and + `GameWindow.OnRender` are typed orchestration handoffs and the class is + 1,910 raw lines. I.9 plus the remaining shutdown/closeout cleanup is active in [`docs/architecture/code-structure.md`](architecture/code-structure.md). diff --git a/docs/architecture/code-structure.md b/docs/architecture/code-structure.md index 98f2df11..edb42abb 100644 --- a/docs/architecture/code-structure.md +++ b/docs/architecture/code-structure.md @@ -739,10 +739,11 @@ diagnostic command targets, and sole gameplay input subscriber into Phase 7 before frame publication. I.8b moves the complete update/render construction body into `FrameRootCompositionPhase`, publishes the pair through an exact lease, and gives lifecycle resource sampling a focused source. `GameWindow` is -1,919 raw lines. I.8c–L remain active, with terminal session start next. The -App gate passes 3,429 tests / 3 intentional skips and the complete Release suite -passes 7,801 tests / 5 intentional skips. The clean solution build retains only -the 17 test-project warnings tracked by #228; the I.8b corrected-diff review is +1,910 raw lines after I.8c makes live-session start the terminal `OnLoad` +operation. I.9–L remain active, with the complete Checkpoint-I audit next. The +App gate passes 3,430 tests / 3 intentional skips and the complete Release suite +passes 7,802 tests / 5 intentional skips. The clean solution build retains only +the 17 test-project warnings tracked by #228; the I.8c corrected-diff review is clean. ### 4.4 Exit criteria diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index d94d8760..b2e4d8f2 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -104,11 +104,11 @@ controllers, selection/radar, landblock publishers, and portal/sky/particle resources have transactional phase ownership and exact-owner late bindings. I.7 adds the complete streaming/session/hydration/local-player/teleport phase, including explicit reversible ownership for every late edge. `GameWindow` is -now 1,919 raw lines after I.8b, which also composes and atomically publishes the -complete update/render pair and extracts lifecycle resource sampling. I.8c–I.9, -shutdown, canonical soak snapshots, and closeout checkpoints J–L remain. The -App gate passes 3,429 tests / 3 intentional skips, the complete Release suite -passes 7,801 tests / 5 skips, and the I.8b corrected-diff review is clean. +now 1,910 raw lines after I.8c, which makes session start the terminal `OnLoad` +operation. I.9, shutdown, canonical soak snapshots, and closeout checkpoints +J–L remain. The App gate passes 3,430 tests / 3 intentional skips, the complete +Release suite passes 7,802 tests / 5 skips, and the I.8c corrected-diff review +is clean. This is a behavior-preserving structural program. Severe regressions still get root-cause fixes in separate commits; ordinary feature work resumes with M4 diff --git a/docs/plans/2026-05-12-milestones.md b/docs/plans/2026-05-12-milestones.md index 6099b225..762a2866 100644 --- a/docs/plans/2026-05-12-milestones.md +++ b/docs/plans/2026-05-12-milestones.md @@ -62,11 +62,12 @@ session, hydration, local-player, combat, and teleport startup graph, including named reversible late edges. I.8a adds the focused live-session runtime factory and attaches command/input owners before frame publication. I.8b composes and atomically publishes the complete frame pair and extracts lifecycle resource -sampling. `GameWindow` is 1,919 raw lines. I.8c–I.9 and checkpoints J–L remain -active. Issue #232 tracks +sampling. I.8c makes session start the terminal `OnLoad` operation. +`GameWindow` is 1,910 raw lines. I.9 and checkpoints J–L remain active. Issue +#232 tracks process-residency variance in the soak without loosening its leak threshold. -The App gate passes 3,429 tests / 3 skips, the complete Release suite passes -7,801 tests / 5 skips, and the I.8b corrected-diff review is clean. +The App gate passes 3,430 tests / 3 skips, the complete Release suite passes +7,802 tests / 5 skips, and the I.8c corrected-diff review is clean. Carried: #153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus diff --git a/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md b/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md index 7987ad4a..72625f7f 100644 --- a/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md +++ b/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md @@ -297,8 +297,10 @@ ledger only after the complete pipeline is live. input subscriber now belong to Phase 7 and publish before any frame root. I.8b composes the unchanged update/render graph in a focused phase, publishes the pair through an exact owned slot lease, and moves lifecycle resource - sampling out of the window. `GameWindow.cs` is 1,919 raw lines. I.8c terminal - session start is the active cut. + sampling out of the window. I.8c makes the exact session start and its + existing diagnostics the terminal `OnLoad` operation. `GameWindow.cs` is + 1,910 raw lines. I.9 corrected-diff review and documentation closeout is the + active cut. Checkpoint I remains active until I.9 closes the complete nine-phase pipeline. ## 6. Automated acceptance diff --git a/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md b/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md index 8e300d61..1653b1b7 100644 --- a/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md +++ b/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md @@ -1,6 +1,6 @@ # GameWindow Slice 8 — Checkpoint I.8 Frame Roots and Session Start -**Status:** Active — I.8a–I.8b complete; I.8c terminal start next +**Status:** Complete 2026-07-22 **Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md` @@ -144,6 +144,12 @@ shutdown. Replacement is rejected atomically. ## 4. I.8c — terminal start and window cutover +**Completed 2026-07-22.** `SessionStartCompositionPhase` invokes the exact +Phase-7 host through the Phase-8 result and owns the existing missing-credential +and failed-start diagnostics. The phase call is the final statement in +`GameWindow.OnLoad`; no binding, publication, allocation, or diagnostics setup +follows it. + Add `SessionStartCompositionPhase` implementing `ISessionStartCompositionPhase`. It invokes only `LiveSessionHost.Start(RuntimeOptions)` and preserves the existing diagnostics diff --git a/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md b/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md index 202d6d33..01456b7a 100644 --- a/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md +++ b/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md @@ -29,8 +29,8 @@ audit. - [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots explicit single ownership and transfer seams. - [ ] I — group `OnLoad` into small ordered, fakeable composition phases with - transactional partial-acquisition rollback. I.1–I.8b are complete; I.8c - terminal session start is active. Detailed plan: + transactional partial-acquisition rollback. I.1–I.8 are complete; I.9 + corrected-diff review and checkpoint closeout is active. Detailed plan: [`2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`](2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md). - [ ] J — move the exact retryable shutdown manifest to a focused lifetime owner and prove all partial-load/reentrant/retry paths. diff --git a/src/AcDream.App/Composition/SessionStartComposition.cs b/src/AcDream.App/Composition/SessionStartComposition.cs new file mode 100644 index 00000000..2a994e79 --- /dev/null +++ b/src/AcDream.App/Composition/SessionStartComposition.cs @@ -0,0 +1,47 @@ +using AcDream.App.Net; + +namespace AcDream.App.Composition; + +internal sealed record SessionStartDependencies( + RuntimeOptions Options, + Action Log); + +/// +/// Terminal startup phase. Every callback, command target, and frame root is +/// already published when this owner asks the canonical session host to start. +/// +internal sealed class SessionStartCompositionPhase + : ISessionStartCompositionPhase +{ + private readonly SessionStartDependencies _dependencies; + + public SessionStartCompositionPhase(SessionStartDependencies dependencies) => + _dependencies = dependencies + ?? throw new ArgumentNullException(nameof(dependencies)); + + public void Start(FrameRootResult frame) + { + ArgumentNullException.ThrowIfNull(frame); + LiveSessionStartResult result = + frame.SessionHost.Start(_dependencies.Options); + Report(result, _dependencies.Log); + } + + internal static void Report( + LiveSessionStartResult result, + Action log) + { + ArgumentNullException.ThrowIfNull(result); + ArgumentNullException.ThrowIfNull(log); + switch (result.Status) + { + case LiveSessionStartStatus.MissingCredentials: + log( + "live: ACDREAM_LIVE set but TEST_USER/TEST_PASS missing; skipping"); + break; + case LiveSessionStartStatus.Failed: + log($"live: session failed: {result.Error}"); + break; + } + } +} diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 0fae2fd9..9953d71d 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1420,18 +1420,9 @@ public sealed class GameWindow : livePresentation, sessionPlayer); - AcDream.App.Net.LiveSessionStartResult liveStart = - frameRoots.SessionHost.Start(_options); - switch (liveStart.Status) - { - case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials: - Console.WriteLine( - "live: ACDREAM_LIVE set but TEST_USER/TEST_PASS missing; skipping"); - break; - case AcDream.App.Net.LiveSessionStartStatus.Failed: - Console.WriteLine($"live: session failed: {liveStart.Error}"); - break; - } + new SessionStartCompositionPhase( + new SessionStartDependencies(_options, Console.WriteLine)) + .Start(frameRoots); } private void OnUpdate(double dt) diff --git a/tests/AcDream.App.Tests/Composition/SessionStartCompositionTests.cs b/tests/AcDream.App.Tests/Composition/SessionStartCompositionTests.cs new file mode 100644 index 00000000..fddddfc6 --- /dev/null +++ b/tests/AcDream.App.Tests/Composition/SessionStartCompositionTests.cs @@ -0,0 +1,38 @@ +using AcDream.App.Composition; +using AcDream.App.Net; + +namespace AcDream.App.Tests.Composition; + +public sealed class SessionStartCompositionTests +{ + [Fact] + public void DiagnosticsPreserveMissingCredentialAndFailureMessagesOnly() + { + var messages = new List(); + + SessionStartCompositionPhase.Report( + new LiveSessionStartResult( + LiveSessionStartStatus.MissingCredentials), + messages.Add); + SessionStartCompositionPhase.Report( + new LiveSessionStartResult( + LiveSessionStartStatus.Failed, + Error: new InvalidOperationException("boom")), + messages.Add); + SessionStartCompositionPhase.Report( + new LiveSessionStartResult(LiveSessionStartStatus.Disabled), + messages.Add); + SessionStartCompositionPhase.Report( + new LiveSessionStartResult(LiveSessionStartStatus.Connected), + messages.Add); + + Assert.Equal(2, messages.Count); + Assert.Equal( + "live: ACDREAM_LIVE set but TEST_USER/TEST_PASS missing; skipping", + messages[0]); + Assert.StartsWith( + "live: session failed: System.InvalidOperationException: boom", + messages[1], + StringComparison.Ordinal); + } +} diff --git a/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs b/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs index 1730082f..a5484285 100644 --- a/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs +++ b/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs @@ -66,7 +66,8 @@ public sealed class GameWindowSlice8BoundaryTests "this).Compose(", "new SessionPlayerCompositionPhase(", "FrameRootResult frameRoots = new FrameRootCompositionPhase(", - "frameRoots.SessionHost.Start(_options)"); + "new SessionStartCompositionPhase(", + ".Start(frameRoots);"); string framePhase = File.ReadAllText(Path.Combine( FindRepoRoot(), @@ -97,7 +98,7 @@ public sealed class GameWindowSlice8BoundaryTests "_publication.PublishSessionPlayer(result);"); Assert.Equal(1, CountOccurrences( body, - "frameRoots.SessionHost.Start(_options)")); + ".Start(frameRoots);")); string phaseOne = Slice( File.ReadAllText(Path.Combine( FindRepoRoot(), @@ -117,11 +118,22 @@ public sealed class GameWindowSlice8BoundaryTests "if (firstKeyboard is not null && firstMouse is not null)", phaseOne, StringComparison.Ordinal); + string startPhase = File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Composition", + "SessionStartComposition.cs")); + AssertAppearsInOrder( + startPhase, + "frame.SessionHost.Start(_dependencies.Options)", + "switch (result.Status)", + "case LiveSessionStartStatus.MissingCredentials:", + "case LiveSessionStartStatus.Failed:"); int start = body.IndexOf( - "frameRoots.SessionHost.Start(_options)", + ".Start(frameRoots);", StringComparison.Ordinal); - string postStart = body[start..]; - Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal); + string postStart = body[(start + ".Start(frameRoots);".Length)..]; Assert.Empty(System.Text.RegularExpressions.Regex.Matches( postStart, @"(?m)^\s+_[A-Za-z]\w*\s*(?:\?\?=|=)")); @@ -343,7 +355,8 @@ public sealed class GameWindowSlice8BoundaryTests "new WorldRenderCompositionPhase(", "new SessionPlayerCompositionPhase(", "new FrameRootCompositionPhase(", - "frameRoots.SessionHost.Start(_options)"); + "new SessionStartCompositionPhase(", + ".Start(frameRoots);"); string sessionPhase = File.ReadAllText(Path.Combine( FindRepoRoot(), "src",