refactor(app): complete session startup composition

Move the live-session reset and routing graph, combat and diagnostic command targets, and the sole gameplay input subscriber into Phase 7 before frame publication. Add exact retryable ownership for late bindings so partial startup cannot strand session or component teardown edges.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 18:49:31 +02:00
parent 7fa60971e2
commit 826f9ea9b5
22 changed files with 924 additions and 412 deletions

View file

@ -122,6 +122,25 @@ public sealed class LiveCombatModeCommandControllerTests
Assert.Throws<ObjectDisposedException>(() => slot.Bind(target));
}
[Fact]
public void DeferredSlotOwnedBindingReleasesExactlyAndAllowsRebind()
{
var slot = new LiveCombatModeCommandSlot();
var first = new FakeCommand();
var second = new FakeCommand();
IDisposable firstBinding = slot.BindOwned(first);
Assert.Throws<InvalidOperationException>(() => slot.BindOwned(second));
firstBinding.Dispose();
using IDisposable secondBinding = slot.BindOwned(second);
firstBinding.Dispose();
slot.Toggle();
Assert.Equal(0, first.ToggleCount);
Assert.Equal(1, second.ToggleCount);
}
private sealed class Harness
{
public Harness()

View file

@ -15,6 +15,23 @@ namespace AcDream.App.Tests.Composition;
public sealed class InteractionUiRuntimeSourcesTests
{
[Fact]
public void DesiredComponentSnapshotNormalizesNullAndClearsExactly()
{
var state = new DesiredComponentSnapshotState();
IReadOnlyList<(uint Id, uint Amount)> items =
[(0x01020304u, 7u)];
Assert.Empty(state.Items);
state.Items = items;
Assert.Same(items, state.Items);
state.Clear();
Assert.Empty(state.Items);
state.Items = null!;
Assert.Empty(state.Items);
}
[Fact]
public void SessionAuthorityDefaultsBindUnbindRebindAndDeactivateExactly()
{

View file

@ -31,6 +31,24 @@ public sealed class LivePresentationCompositionTests
bindings.Adopt("late", new RetryBinding("late", calls, 0)));
}
[Fact]
public void TransferableAdoptionRollsBackExactlyAndRetriesFailure()
{
var calls = new List<string>();
var bindings = new LivePresentationRuntimeBindings();
bindings.Adopt("stable", new RetryBinding("stable", calls, 0));
IDisposable adoption = bindings.AdoptOwned(
"candidate",
new RetryBinding("candidate", calls, 1));
Assert.Throws<InvalidOperationException>(adoption.Dispose);
adoption.Dispose();
adoption.Dispose();
bindings.Dispose();
Assert.Equal(["candidate", "candidate", "stable"], calls);
}
[Fact]
public void CanonicalRuntimeSlotUsesExactOwnerBinding()
{

View file

@ -116,7 +116,19 @@ public sealed class SessionPlayerCompositionTests
"new PlayerModeController(",
"d.PortalTunnelFallback.Transfer(",
"d.TeleportSink.BindOwned(localTeleport)",
"sessionRuntimeFactory.Create(liveSession)",
"d.CombatModeCommands.BindOwned(combatCommand)",
"d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)",
"GameplayInputActionRouter.Create(",
"gameplayActions.Attach();",
"_publication.PublishSessionPlayer(result);");
Assert.DoesNotContain("GameWindow", File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Net",
"LiveSessionRuntimeFactory.cs")), StringComparison.Ordinal);
}
private sealed class RetryBinding(

View file

@ -108,6 +108,25 @@ public sealed class RuntimeDiagnosticCommandControllerTests
Assert.Throws<ObjectDisposedException>(() => slot.Bind(target));
}
[Fact]
public void DeferredSlotOwnedBindingReleasesExactlyAndAllowsRebind()
{
var slot = new RuntimeDiagnosticCommandSlot();
var first = new FakeCommands();
var second = new FakeCommands();
IDisposable firstBinding = slot.BindOwned(first);
Assert.Throws<InvalidOperationException>(() => slot.BindOwned(second));
firstBinding.Dispose();
using IDisposable secondBinding = slot.BindOwned(second);
firstBinding.Dispose();
slot.CycleWeather();
Assert.Equal(0, first.CallCount);
Assert.Equal(1, second.CallCount);
}
[Fact]
public void NullToast_DoesNotSuppressDiagnosticStateChanges()
{

View file

@ -47,7 +47,7 @@ public sealed class GameWindowSlice8BoundaryTests
{
string body = MethodBody(
"private void OnLoad()",
"private AcDream.App.Net.LiveSessionHost");
"private void OnUpdate(double dt)");
AssertAppearsInOrder(
body,
@ -64,16 +64,29 @@ public sealed class GameWindowSlice8BoundaryTests
"LivePresentationResult livePresentation =",
"new LivePresentationCompositionPhase(",
"this).Compose(",
"new SessionPlayerCompositionPhase(",
"var renderFrameOrchestrator =",
"var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(",
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);",
"_liveSessionHost = CreateLiveSessionHost();",
"_liveCombatModeCommands.Bind(combatCommand);",
"_runtimeDiagnosticCommands.Bind(runtimeDiagnostics);",
"_gameplayInputActions = AcDream.App.Input.GameplayInputActionRouter.Create(",
"_gameplayInputActions.Attach();",
"_liveSessionHost.Start(_options)");
Assert.Equal(1, CountOccurrences(body, "_liveSessionHost.Start(_options)"));
"sessionPlayer.SessionHost.Start(_options)");
string sessionPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"SessionPlayerComposition.cs"));
AssertAppearsInOrder(
sessionPhase,
"sessionRuntimeFactory.Create(liveSession)",
"d.CombatModeCommands.BindOwned(combatCommand)",
"d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)",
"GameplayInputActionRouter.Create(",
"gameplayActions.Attach();",
"_publication.PublishSessionPlayer(result);");
Assert.Equal(1, CountOccurrences(
body,
"sessionPlayer.SessionHost.Start(_options)"));
string phaseOne = Slice(
File.ReadAllText(Path.Combine(
FindRepoRoot(),
@ -93,7 +106,9 @@ public sealed class GameWindowSlice8BoundaryTests
"if (firstKeyboard is not null && firstMouse is not null)",
phaseOne,
StringComparison.Ordinal);
int start = body.IndexOf("_liveSessionHost.Start(_options)", StringComparison.Ordinal);
int start = body.IndexOf(
"sessionPlayer.SessionHost.Start(_options)",
StringComparison.Ordinal);
string postStart = body[start..];
Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal);
Assert.Empty(System.Text.RegularExpressions.Regex.Matches(
@ -113,10 +128,17 @@ public sealed class GameWindowSlice8BoundaryTests
string source = GameWindowSource();
string load = MethodBody(
"private void OnLoad()",
"private AcDream.App.Net.LiveSessionHost");
string sessionFactory = MethodBody(
"private AcDream.App.Net.LiveSessionEventRouter CreateLiveSessionEventRouter(",
"private AcDream.App.Net.LiveInventorySessionBindings");
"private void OnUpdate(double dt)");
string sessionFactorySource = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Net",
"LiveSessionRuntimeFactory.cs"));
string sessionFactory = Slice(
sessionFactorySource,
"private LiveSessionEventRouter CreateEventRouter(",
"private LiveInventorySessionBindings CreateInventoryBindings()");
string worldPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
@ -143,9 +165,9 @@ public sealed class GameWindowSlice8BoundaryTests
Assert.Contains("environment.Initialize(region);", worldPhase, StringComparison.Ordinal);
AssertAppearsInOrder(
sessionFactory,
"new AcDream.App.Net.LiveEnvironmentSessionSink(",
"_worldEnvironment.ApplyAdminEnvirons,",
"_worldEnvironment.SynchronizeFromServer)");
"new LiveEnvironmentSessionSink(",
"_world.Environment.ApplyAdminEnvirons,",
"_world.Environment.SynchronizeFromServer)");
Assert.DoesNotContain("_loadedSkyDesc", source, StringComparison.Ordinal);
Assert.DoesNotContain("_loadedSkyDayIndex", source, StringComparison.Ordinal);
Assert.DoesNotContain("private void RefreshSkyForCurrentDay()", source, StringComparison.Ordinal);
@ -164,10 +186,16 @@ public sealed class GameWindowSlice8BoundaryTests
"debugVm.CycleTimeOfDay = _dependencies.DiagnosticCommands.CycleTimeOfDay;",
"debugVm.CycleWeather = _dependencies.DiagnosticCommands.CycleWeather;",
"debugVm.ToggleCollisionWires =");
string sessionPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"SessionPlayerComposition.cs"));
AssertAppearsInOrder(
load,
"new AcDream.App.Diagnostics.RuntimeDiagnosticCommandController(",
"_runtimeDiagnosticCommands.Bind(runtimeDiagnostics);");
sessionPhase,
"new RuntimeDiagnosticCommandController(",
"d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)");
}
[Fact]
@ -176,7 +204,13 @@ public sealed class GameWindowSlice8BoundaryTests
string source = GameWindowSource();
string load = MethodBody(
"private void OnLoad()",
"private AcDream.App.Net.LiveSessionHost");
"private void OnUpdate(double dt)");
string sessionPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"SessionPlayerComposition.cs"));
string shutdown = MethodBody(
"private ResourceShutdownTransaction CreateShutdownTransaction()",
"private void OnFocusChanged(bool focused)");
@ -187,9 +221,11 @@ public sealed class GameWindowSlice8BoundaryTests
Assert.DoesNotContain("private void OnUiDragReleasedOutside(", source, StringComparison.Ordinal);
Assert.DoesNotContain("DragReleasedOutsideUi +=", source, StringComparison.Ordinal);
Assert.Equal(1, CountOccurrences(
load,
"AcDream.App.Input.GameplayInputActionRouter.Create("));
Assert.Equal(1, CountOccurrences(load, "_gameplayInputActions.Attach();"));
sessionPhase,
"GameplayInputActionRouter.Create("));
Assert.Equal(1, CountOccurrences(
sessionPhase,
"gameplayActions.Attach();"));
Assert.Equal(1, CountOccurrences(
LivePresentationSource(),
"RetainedUiGameplayBinding.Create("));
@ -214,7 +250,7 @@ public sealed class GameWindowSlice8BoundaryTests
{
string load = MethodBody(
"private void OnLoad()",
"private AcDream.App.Net.LiveSessionHost");
"private void OnUpdate(double dt)");
string body = MethodBody(
"private void OnFramebufferResize(",
"private void OnClosing()");
@ -251,7 +287,7 @@ public sealed class GameWindowSlice8BoundaryTests
string run = MethodBody("public void Run()", "private void OnLoad()");
string load = MethodBody(
"private void OnLoad()",
"private AcDream.App.Net.LiveSessionHost");
"private void OnUpdate(double dt)");
string shutdown = MethodBody(
"private ResourceShutdownTransaction CreateShutdownTransaction()",
"private void OnFocusChanged(bool focused)");
@ -295,7 +331,7 @@ public sealed class GameWindowSlice8BoundaryTests
"this).Compose(platform, hostInputCamera, contentEffectsAudio);",
"new WorldRenderCompositionPhase(",
"new SessionPlayerCompositionPhase(",
"_liveSessionHost.Start(_options)");
"sessionPlayer.SessionHost.Start(_options)");
string sessionPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
@ -464,7 +500,7 @@ public sealed class GameWindowSlice8BoundaryTests
string source = GameWindowSource();
string load = MethodBody(
"private void OnLoad()",
"private AcDream.App.Net.LiveSessionHost");
"private void OnUpdate(double dt)");
string shutdown = Slice(
source,
"private ResourceShutdownTransaction CreateShutdownTransaction()",

View file

@ -179,9 +179,9 @@ public sealed class GameWindowLiveEntityCompositionTests
public void SessionReset_ClosesEveryStreamingReadinessOwner()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs"));
FindRepoRoot(), "src", "AcDream.App", "Net", "LiveSessionRuntimeFactory.cs"));
Assert.Contains("_playerModeController?.ResetSession();", source,
Assert.Contains("_interaction.PlayerMode.ResetSession();", source,
StringComparison.Ordinal);
string playerModeSource = File.ReadAllText(Path.Combine(
FindRepoRoot(),
@ -192,10 +192,10 @@ public sealed class GameWindowLiveEntityCompositionTests
Assert.Contains("_mode.ResetSession();", playerModeSource,
StringComparison.Ordinal);
Assert.Contains(
"_liveEntityNetworkUpdates?.ResetSessionState();",
"_world.NetworkUpdates.ResetSessionState();",
source,
StringComparison.Ordinal);
Assert.Contains("_liveWorldOrigin.Reset();", source, StringComparison.Ordinal);
Assert.Contains("_player.WorldOrigin.Reset();", source, StringComparison.Ordinal);
}
private static string FindRepoRoot()

View file

@ -807,6 +807,12 @@ public sealed class UpdateFrameOrchestratorTests
"AcDream.App",
"Composition",
"LivePresentationComposition.cs"));
string sessionRuntime = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Net",
"LiveSessionRuntimeFactory.cs"));
Assert.DoesNotContain("PublishLocalPhysicsTimestamps", source,
StringComparison.Ordinal);
Assert.DoesNotContain("LoginWorldReady", source,
@ -823,7 +829,11 @@ public sealed class UpdateFrameOrchestratorTests
StringComparison.Ordinal);
Assert.Contains("d.WorldOrigin.CellLocalForSeed", livePresentation,
StringComparison.Ordinal);
Assert.Contains("Live entity session routing was not composed.", source,
Assert.DoesNotContain("CreateLiveSessionEventRouter", source,
StringComparison.Ordinal);
Assert.Contains("_world.EntitySession.CreateSink()", sessionRuntime,
StringComparison.Ordinal);
Assert.DoesNotContain("GameWindow", sessionRuntime,
StringComparison.Ordinal);
}