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:
parent
7fa60971e2
commit
826f9ea9b5
22 changed files with 924 additions and 412 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue