refactor(runtime): cut graphical host over to canonical root

Make every App composition phase borrow one GameRuntime, retire the duplicate view/event adapters, and dispose the root only after its graphical borrowers release. This preserves synchronous UI commands while giving shutdown one exact ownership ledger.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-26 19:06:09 +02:00
parent ecb9f79444
commit ce41efb9e5
29 changed files with 613 additions and 778 deletions

View file

@ -109,6 +109,8 @@ public sealed class ContentEffectsAudioCompositionTests
Assert.Throws<InvalidOperationException>(() => fixture.Phase().Compose(
fixture.Platform,
fixture.Host));
Assert.False(
fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested);
Assert.NotNull(fixture.Publication.Audio);
Assert.False(fixture.Publication.Audio!.Engine.IsDisposalComplete);
@ -127,6 +129,8 @@ public sealed class ContentEffectsAudioCompositionTests
Assert.Throws<InvalidOperationException>(() => fixture.Phase().Compose(
fixture.Platform,
fixture.Host));
Assert.False(
fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested);
Assert.Equal(
Enum.GetValues<ContentEffectsAudioCompositionPoint>()
@ -258,7 +262,7 @@ public sealed class ContentEffectsAudioCompositionTests
ResidencyBudgetOptions.Default,
new PhysicsDataCache(),
false,
new RuntimeCharacterState(),
GameRuntimeTestFactory.Create(),
Router,
Poses,
new DeferredEntityEffectAdvanceSource(),
@ -293,7 +297,7 @@ public sealed class ContentEffectsAudioCompositionTests
public void Dispose()
{
Publication.Dispose();
Dependencies.Character.Dispose();
Dependencies.Runtime.Dispose();
}
}

View file

@ -10,6 +10,7 @@ using AcDream.App.World;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Spells;
using AcDream.Runtime;
using AcDream.Runtime.Gameplay;
namespace AcDream.App.Tests.Composition;
@ -33,7 +34,7 @@ public sealed class InteractionRetainedUiCompositionTests
[Fact]
public void EnabledUiPublishesOneExactResultAfterFrozenConstructionOrder()
{
var fixture = new Fixture(retailUi: true);
using var fixture = new Fixture(retailUi: true);
InteractionRetainedUiResult result = fixture.Compose();
@ -56,7 +57,7 @@ public sealed class InteractionRetainedUiCompositionTests
[Fact]
public void DisabledUiAcquiresNoRetainedFrontendResource()
{
var fixture = new Fixture(retailUi: false);
using var fixture = new Fixture(retailUi: false);
InteractionRetainedUiResult result = fixture.Compose();
@ -81,11 +82,13 @@ public sealed class InteractionRetainedUiCompositionTests
int pointValue)
{
var point = (InteractionRetainedUiCompositionPoint)pointValue;
var fixture = new Fixture(retailUi: true, failurePoint: point);
using var fixture = new Fixture(retailUi: true, failurePoint: point);
Assert.Throws<InvalidOperationException>(fixture.Compose);
Assert.Equal(point, fixture.Points[^1]);
Assert.False(
fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested);
if (point == InteractionRetainedUiCompositionPoint.ResultPublished)
{
Assert.Empty(fixture.Factory.Releases);
@ -129,9 +132,11 @@ public sealed class InteractionRetainedUiCompositionTests
[Fact]
public void PublicationFailureRollsBackCompleteUnpublishedPrefix()
{
var fixture = new Fixture(retailUi: true, publicationFailure: true);
using var fixture = new Fixture(retailUi: true, publicationFailure: true);
Assert.Throws<InvalidOperationException>(fixture.Compose);
Assert.False(
fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested);
Assert.Equal(
[
@ -161,7 +166,7 @@ public sealed class InteractionRetainedUiCompositionTests
Assert.DoesNotContain("private uint? PickWorldGuidAtCursor(", source);
}
private sealed class Fixture
private sealed class Fixture : IDisposable
{
private readonly InteractionRetainedUiCompositionPoint? _failurePoint;
@ -175,9 +180,7 @@ public sealed class InteractionRetainedUiCompositionTests
Publication = new Publication(publicationFailure);
RuntimeOptions options = RuntimeOptions.Parse("dat", static _ => null)
with { RetailUi = retailUi };
var actions = new RuntimeActionState(
new InventoryTransactionState(new ClientObjectTable()),
new Spellbook(),
GameRuntime runtime = GameRuntimeTestFactory.Create(
new NoopCombatOperations(),
new NoopCombatTargetOperations(),
new NoopCombatModeOperations(),
@ -196,19 +199,15 @@ public sealed class InteractionRetainedUiCompositionTests
RetainedInputCapture: null!,
InputDispatcher: null,
Settings: null!,
Actions: actions,
Runtime: runtime,
CombatAttackOperations: new NoopCombatOperations(),
CombatTargetOperations: new RuntimeCombatTargetOperationsSlot(),
SpellCastOperations: new RuntimeSpellCastOperationsSlot(),
Inventory: null!,
MagicCatalog: null!,
Character: null!,
Communication: null!,
StackSplitQuantity: null!,
UiRegistry: null,
CombatModeCommands: null!,
PlayerIdentity: null!,
PlayerController: null!,
PlayerMode: null!,
SelectionCameraFactory: static _ => Stub<SelectionCameraSource>(),
FrameDiagnostics: new DeferredRenderFrameDiagnosticsSource(),
@ -235,6 +234,8 @@ public sealed class InteractionRetainedUiCompositionTests
if (_failurePoint == point)
throw new InvalidOperationException($"fault at {point}");
}).Compose();
public void Dispose() => Dependencies.Runtime.Dispose();
}
private sealed class FakeFactory : IInteractionRetainedUiCompositionFactory

View file

@ -88,7 +88,10 @@ public sealed class SessionPlayerCompositionTests
window);
Assert.DoesNotContain("IsSpawnClaimUnhydratable", window);
Assert.Contains("LandblockStreamer.CreateForRequests(", phase);
Assert.Contains("new LiveSessionController()", phase);
Assert.Contains(
"LiveSessionController liveSession = d.Runtime.Session;",
phase);
Assert.DoesNotContain("new LiveSessionController()", phase);
Assert.Contains("new LocalPlayerTeleportController(", phase);
Assert.Contains("new DatSpawnClaimHydrationClassifier(", phase);
}
@ -109,7 +112,7 @@ public sealed class SessionPlayerCompositionTests
"streamerLease.Resource.Start();",
"new StreamingController(",
"new WorldRevealCoordinator(",
"new LiveSessionController()",
"LiveSessionController liveSession = d.Runtime.Session;",
"new LiveEntityHydrationController(",
"new LiveEntityNetworkUpdateController(",
"new GameplayInputFrameController(",

View file

@ -33,6 +33,8 @@ public sealed class SettingsDevToolsCompositionTests
SettingsDevToolsResult result = fixture.Compose();
Assert.Null(result.DevTools);
Assert.False(
fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested);
Assert.Equal(1, fixture.Startup.DisplayCalls);
Assert.Equal(1, fixture.Startup.AudioCalls);
Assert.Equal(0, fixture.Factory.Calls);
@ -229,9 +231,7 @@ public sealed class SettingsDevToolsCompositionTests
Settings,
Startup,
new HostQuiescenceGate(),
new RuntimeCommunicationState(),
new CombatState(),
new LocalPlayerState(new Spellbook()),
GameRuntimeTestFactory.Create(),
enabled
? new SettingsDevToolsOptionalDependencies(
new Facts(),
@ -273,6 +273,7 @@ public sealed class SettingsDevToolsCompositionTests
public void Dispose()
{
Dependencies.Runtime.Dispose();
Publication.Owner?.Dispose();
_dispatcher.Dispose();
_profiler.Dispose();