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

@ -762,43 +762,40 @@ public sealed class CurrentGameRuntimeAdapterTests
private readonly ItemInteractionController _items;
private readonly LiveSessionController _session;
private readonly IDisposable _combatModeBinding;
private readonly GameRuntime _gameRuntime;
public Harness()
{
Identity = new LocalPlayerIdentityState();
EntityObjects = new RuntimeEntityObjectLifetime();
InventoryState = new RuntimeInventoryState(EntityObjects);
Character = new RuntimeCharacterState();
Entities = new LiveEntityRuntime(
new GpuWorldState(),
new NoopEntityResources(),
EntityObjects);
Objects = EntityObjects.Objects;
Communication = new RuntimeCommunicationState();
CombatAttackOperations = new CombatAttackOperationsSlot();
CombatModeOperations = new RuntimeCombatModeOperationsSlot();
SpellCastOperations = new RuntimeSpellCastOperationsSlot();
Actions = new RuntimeActionState(
InventoryState.Transactions,
Character.Spellbook,
Options = LiveOptions();
Commands = new RecordingCommandRouting();
Transport = new TestTransport();
_gameRuntime = GameRuntimeTestFactory.Create(
CombatAttackOperations,
new RuntimeCombatTargetOperationsSlot(),
CombatModeOperations,
SpellCastOperations,
now: static () => 0d);
new SessionOperations(Transport),
combatTime: static () => 0d);
_session = _gameRuntime.Session;
Identity = new LocalPlayerIdentityState(
_gameRuntime.PlayerIdentity);
Entities = new LiveEntityRuntime(
new GpuWorldState(),
new NoopEntityResources(),
EntityObjects);
CombatMode = new RecordingCombatModeOperations();
_combatModeBinding =
CombatModeOperations.BindOwned(CombatMode);
MovementState = new RuntimeLocalPlayerMovementState();
Environment = new RuntimeWorldEnvironmentState();
MovementInput = new DispatcherMovementInputSource(MovementState);
GameplayInput = new GameplayInputFrameController(
dispatcher: null,
MovementInput,
mouseLook: null,
new NoopCombatInput());
Clock = new UpdateFrameClock();
WorldTransit = new RuntimeWorldTransitState();
Clock = new UpdateFrameClock(_gameRuntime.Clock);
WorldReveal = new WorldRevealCoordinator(
WorldTransit,
static (_, _) => true,
@ -826,50 +823,42 @@ public sealed class CurrentGameRuntimeAdapterTests
new SelectionTransport(() => _session?.IsInWorld == true),
new NoopInteractionMovement());
Options = LiveOptions();
Commands = new RecordingCommandRouting();
Transport = new TestTransport();
_session = new LiveSessionController(
new SessionOperations(Transport));
Host = CreateHost(_session, Commands, Identity);
Runtime = new CurrentGameRuntimeAdapter(
_session,
_gameRuntime,
Host,
Commands,
Identity,
EntityObjects,
InventoryState,
Character,
Communication,
Actions,
MovementState,
Environment,
WorldTransit,
Clock,
selectionController);
}
public RuntimeOptions Options { get; }
public LocalPlayerIdentityState Identity { get; }
public RuntimeEntityObjectLifetime EntityObjects { get; }
public RuntimeInventoryState InventoryState { get; }
public RuntimeCharacterState Character { get; }
public RuntimeEntityObjectLifetime EntityObjects =>
_gameRuntime.EntityObjects;
public RuntimeInventoryState InventoryState =>
_gameRuntime.InventoryOwner;
public RuntimeCharacterState Character =>
_gameRuntime.CharacterOwner;
public LiveEntityRuntime Entities { get; }
public ClientObjectTable Objects { get; }
public RuntimeCommunicationState Communication { get; }
public ClientObjectTable Objects => EntityObjects.Objects;
public RuntimeCommunicationState Communication =>
_gameRuntime.CommunicationOwner;
public ChatLog Chat => Communication.Chat;
public CombatAttackOperationsSlot CombatAttackOperations { get; }
public RuntimeCombatModeOperationsSlot CombatModeOperations { get; }
public RuntimeSpellCastOperationsSlot SpellCastOperations { get; }
public RuntimeActionState Actions { get; }
public RuntimeActionState Actions => _gameRuntime.ActionOwner;
public SelectionState Selection => Actions.Selection;
public RuntimeLocalPlayerMovementState MovementState { get; }
public RuntimeWorldEnvironmentState Environment { get; }
public RuntimeLocalPlayerMovementState MovementState =>
_gameRuntime.MovementOwner;
public RuntimeWorldEnvironmentState Environment =>
_gameRuntime.EnvironmentOwner;
public DispatcherMovementInputSource MovementInput { get; }
public GameplayInputFrameController GameplayInput { get; }
public RecordingCombatModeOperations CombatMode { get; }
public UpdateFrameClock Clock { get; }
public RuntimeWorldTransitState WorldTransit { get; }
public RuntimeWorldTransitState WorldTransit =>
_gameRuntime.TransitOwner;
public WorldRevealCoordinator WorldReveal { get; }
public RecordingCommandRouting Commands { get; }
public TestTransport Transport { get; }
@ -879,15 +868,11 @@ public sealed class CurrentGameRuntimeAdapterTests
public void Dispose()
{
Runtime.Dispose();
Character.Dispose();
Communication.Dispose();
_session.Dispose();
_items.Dispose();
_combatModeBinding.Dispose();
Actions.Dispose();
InventoryState.Dispose();
Entities.Clear();
MovementState.Dispose();
WorldReveal.ResetSession();
_gameRuntime.Dispose();
}
}

View file

@ -15,11 +15,11 @@ public sealed class RuntimeActionOwnershipTests
string program = ReadAppSource(root, "Program.cs");
Assert.Contains(
"private readonly RuntimeActionState _runtimeActions;",
"private readonly GameRuntime _runtime;",
gameWindow,
StringComparison.Ordinal);
Assert.Contains(
"_runtimeActions = new RuntimeActionState(",
"_runtime = new GameRuntime(new GameRuntimeDependencies(",
gameWindow,
StringComparison.Ordinal);
Assert.Contains(
@ -73,14 +73,9 @@ public sealed class RuntimeActionOwnershipTests
Assert.Empty(Regex.Matches(
production,
@"\bnew\s+(?:AcDream\.Runtime\.Gameplay\.)?RuntimeSpellCastState\s*\("));
Assert.Equal(
1,
Regex.Matches(
production,
@"\bnew\s+RuntimeActionState\s*\(").Count
+ Regex.Matches(
production,
@"RuntimeActionState\s+\w+\s*=\s*new\s*\(\s*\)").Count);
Assert.Empty(Regex.Matches(
production,
@"\bnew\s+RuntimeActionState\s*\("));
}
[Fact]
@ -138,14 +133,13 @@ public sealed class RuntimeActionOwnershipTests
itemInteraction,
StringComparison.Ordinal);
Assert.Contains(
"\"runtime action state\"",
"new ResourceShutdownStage(\"game runtime root\"",
shutdown,
StringComparison.Ordinal);
AssertAppearsInOrder(
Assert.DoesNotContain(
"RuntimeActionState Actions",
shutdown,
"\"runtime action state\"",
"\"runtime inventory state\"",
"\"runtime entity/object lifetime\"");
StringComparison.Ordinal);
Assert.False(File.Exists(Path.Combine(
root,
"src",

View file

@ -8,7 +8,15 @@ public sealed class RuntimeCharacterOwnershipTests
string source = ReadSource("Rendering", "GameWindow.cs");
Assert.Contains(
"_runtimeCharacter = new RuntimeCharacterState();",
"private readonly GameRuntime _runtime;",
source,
StringComparison.Ordinal);
Assert.Contains(
"_runtime = new GameRuntime(new GameRuntimeDependencies(",
source,
StringComparison.Ordinal);
Assert.DoesNotContain(
"new RuntimeCharacterState(",
source,
StringComparison.Ordinal);
Assert.DoesNotContain(
@ -76,7 +84,11 @@ public sealed class RuntimeCharacterOwnershipTests
inventory,
StringComparison.Ordinal);
Assert.Contains(
"\"runtime character state\"",
"new ResourceShutdownStage(\"game runtime root\"",
shutdown,
StringComparison.Ordinal);
Assert.DoesNotContain(
"RuntimeCharacterState Character",
shutdown,
StringComparison.Ordinal);
}

View file

@ -8,7 +8,11 @@ public sealed class RuntimeInventoryOwnershipTests
string source = ReadSource("Rendering", "GameWindow.cs");
Assert.Contains(
"_runtimeInventory = new RuntimeInventoryState(_runtimeEntityObjects);",
"private readonly GameRuntime _runtime;",
source,
StringComparison.Ordinal);
Assert.DoesNotContain(
"new RuntimeInventoryState(",
source,
StringComparison.Ordinal);
Assert.DoesNotContain(
@ -77,11 +81,18 @@ public sealed class RuntimeInventoryOwnershipTests
"_domain.Inventory.DesiredComponents",
session,
StringComparison.Ordinal);
AssertAppearsInOrder(
Assert.Contains(
"new ResourceShutdownStage(\"game runtime root\"",
shutdown,
"\"runtime action state\"",
"\"runtime inventory state\"",
"\"runtime entity/object lifetime\"");
StringComparison.Ordinal);
Assert.Contains(
"Hard(\"game runtime\", () => DisposeGameRuntime(live.Runtime))",
shutdown,
StringComparison.Ordinal);
Assert.DoesNotContain(
"RuntimeInventoryState Inventory",
shutdown,
StringComparison.Ordinal);
}
[Fact]

View file

@ -36,10 +36,14 @@ public sealed class RuntimeMovementOwnershipTests
app,
StringComparison.Ordinal);
Assert.Empty(Regex.Matches(app, @"\bbool\s+_autoRunActive\b"));
Assert.Single(Regex.Matches(
Assert.Empty(Regex.Matches(
app,
@"RuntimeLocalPlayerMovementState\s+\w+\s*=\s*new\s*\(")
.Cast<Match>());
Assert.Single(Regex.Matches(
runtime,
@"new\s+RuntimeLocalPlayerMovementState\s*\(")
.Cast<Match>());
Assert.DoesNotContain(
"ACDREAM_RUN_SKILL",
runtime,
@ -59,10 +63,10 @@ public sealed class RuntimeMovementOwnershipTests
root,
"Input",
"DispatcherMovementInputSource.cs");
string view = ReadAppSource(
string adapter = ReadAppSource(
root,
"Runtime",
"CurrentGameRuntimeViewAdapter.cs");
"CurrentGameRuntimeAdapter.cs");
string commands = ReadAppSource(
root,
"Runtime",
@ -73,7 +77,11 @@ public sealed class RuntimeMovementOwnershipTests
"GameWindowLifetime.cs");
Assert.Contains(
"RuntimeLocalPlayerMovementState _playerControllerSlot = new();",
"RuntimeLocalPlayerMovementState _playerControllerSlot =>",
gameWindow,
StringComparison.Ordinal);
Assert.Contains(
"_runtime.MovementOwner;",
gameWindow,
StringComparison.Ordinal);
Assert.Contains(
@ -90,16 +98,16 @@ public sealed class RuntimeMovementOwnershipTests
StringComparison.Ordinal);
Assert.Contains("_movement.AutoRunActive", input, StringComparison.Ordinal);
Assert.Contains(
"movement ?? throw new ArgumentNullException(nameof(movement))).View",
view,
"private readonly GameRuntime _runtime;",
adapter,
StringComparison.Ordinal);
Assert.Contains("_movement.Execute(command)", commands, StringComparison.Ordinal);
Assert.Contains(
"RuntimeLocalPlayerMovementState Movement",
"GameRuntime Runtime",
lifetime,
StringComparison.Ordinal);
Assert.Contains(
"\"runtime local movement state\"",
"\"game runtime root\"",
lifetime,
StringComparison.Ordinal);
}

View file

@ -17,6 +17,11 @@ public sealed class RuntimeWorldTransitOwnershipTests
"\n",
Directory.EnumerateFiles(appRoot, "*.cs", SearchOption.AllDirectories)
.Select(File.ReadAllText));
string runtimeRoot = Path.Combine(root, "src", "AcDream.Runtime");
string runtime = string.Join(
"\n",
Directory.EnumerateFiles(runtimeRoot, "*.cs", SearchOption.AllDirectories)
.Select(File.ReadAllText));
Assert.False(File.Exists(Path.Combine(
appRoot,
@ -38,10 +43,14 @@ public sealed class RuntimeWorldTransitOwnershipTests
"record struct WorldRevealLifecycleSnapshot",
app,
StringComparison.Ordinal);
Assert.Single(Regex.Matches(
Assert.Empty(Regex.Matches(
app,
@"new\s+RuntimeWorldTransitState\s*\(")
.Cast<Match>());
Assert.Single(Regex.Matches(
runtime,
@"new\s+RuntimeWorldTransitState\s*\(")
.Cast<Match>());
}
[Fact]
@ -56,19 +65,17 @@ public sealed class RuntimeWorldTransitOwnershipTests
typeof(RuntimeWorldTransitState),
availabilityFields[0].FieldType);
var viewFields = typeof(CurrentGameRuntimeViewAdapter)
var adapterFields = typeof(CurrentGameRuntimeAdapter)
.GetFields(
System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.NonPublic);
Assert.Contains(
viewFields,
field => field.FieldType == typeof(IRuntimePortalView));
adapterFields,
field => field.FieldType == typeof(GameRuntime));
Assert.DoesNotContain(
typeof(CurrentGameRuntimeViewAdapter).GetNestedTypes(
System.Reflection.BindingFlags.NonPublic),
type => type.Name.Contains(
"PortalView",
StringComparison.Ordinal));
adapterFields,
field => field.FieldType == typeof(IRuntimePortalView)
|| field.FieldType == typeof(RuntimeWorldTransitState));
var coordinatorFields = typeof(WorldRevealCoordinator)
.GetFields(