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();

View file

@ -0,0 +1,62 @@
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Spells;
using AcDream.Runtime;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Session;
namespace AcDream.App.Tests;
internal static class GameRuntimeTestFactory
{
public static GameRuntime Create(
IRuntimeCombatAttackOperations? combatAttack = null,
IRuntimeCombatTargetOperations? combatTarget = null,
IRuntimeCombatModeOperations? combatMode = null,
IRuntimeSpellCastOperations? spellCast = null,
ILiveSessionOperations? session = null,
Func<double>? combatTime = null)
{
var fallback = new NoopOperations();
return new GameRuntime(new GameRuntimeDependencies(
combatAttack ?? fallback,
combatTarget ?? fallback,
combatMode ?? fallback,
spellCast ?? fallback,
SessionOperations: session,
CombatTime: combatTime));
}
private sealed class NoopOperations :
IRuntimeCombatAttackOperations,
IRuntimeCombatTargetOperations,
IRuntimeCombatModeOperations,
IRuntimeSpellCastOperations
{
public bool CanStartAttack() => false;
public void PrepareAttackRequest() { }
public bool SendAttack(AttackHeight height, float power) => false;
public void SendCancelAttack() { }
public bool IsDualWield => false;
public bool PlayerReadyForAttack => false;
public bool AutoRepeatAttack => false;
public bool AutoTarget => false;
public uint? SelectClosestTarget() => null;
public bool IsInWorld => false;
public IReadOnlyList<ClientObject> GetOrderedEquipment() => [];
public void NotifyExplicitCombatModeRequest() { }
public void SendChangeCombatMode(CombatMode mode) { }
public uint LocalPlayerId => 0u;
public bool CanSend => false;
public bool HasRequiredComponents(uint spellId) => false;
public bool IsTargetCompatible(
uint targetId,
SpellMetadata spell,
bool showMessage) => false;
public void StopCompletely() { }
public void SendUntargeted(uint spellId) { }
public void SendTargeted(uint targetId, uint spellId) { }
public void DisplayMessage(string message) { }
public void IncrementBusy() { }
}
}

View file

@ -13,18 +13,22 @@ public sealed class GameWindowLiveSessionOwnershipTests
BindingFlags.Instance | BindingFlags.NonPublic;
[Fact]
public void GameWindowRetainsControllerAndFocusedHostButNoMirroredSession()
public void GameWindowRetainsCanonicalRuntimeAndFocusedHostButNoMirroredSession()
{
FieldInfo[] fields = typeof(GameWindow).GetFields(PrivateInstance);
Assert.Contains(
fields,
field => field.Name == "_liveSessionController"
&& field.FieldType == typeof(LiveSessionController));
field => field.Name == "_runtime"
&& field.FieldType == typeof(GameRuntime));
Assert.Contains(
fields,
field => field.Name == "_liveSessionHost"
&& field.FieldType == typeof(LiveSessionHost));
Assert.DoesNotContain(
fields,
field => field.Name == "_liveSessionController"
|| field.FieldType == typeof(LiveSessionController));
Assert.DoesNotContain(fields, field => field.Name == "_liveSession");
Assert.DoesNotContain(fields, field => field.FieldType == typeof(WorldSession));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(LiveSessionResetPlan));
@ -54,6 +58,48 @@ public sealed class GameWindowLiveSessionOwnershipTests
|| field.FieldType == typeof(ulong));
}
[Fact]
public void ProductionWindowConstructsOnlyTheCanonicalRuntimeRoot()
{
string root = FindRepositoryRoot();
string source = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
Assert.Equal(
1,
CountOccurrences(source, "new GameRuntime("));
Assert.Contains(
"private readonly GameRuntime _runtime;",
source,
StringComparison.Ordinal);
Assert.Contains(
"_runtimeHostLease = _runtime.AcquireHostLease(",
source,
StringComparison.Ordinal);
string[] forbidden =
[
"new RuntimeEntityObjectLifetime(",
"new RuntimeInventoryState(",
"new RuntimeCharacterState(",
"new RuntimeCommunicationState(",
"new RuntimeActionState(",
"new RuntimeLocalPlayerMovementState(",
"new RuntimeWorldTransitState(",
"new LiveSessionController(",
"new GameRuntimeClock(",
];
Assert.All(
forbidden,
value => Assert.DoesNotContain(
value,
source,
StringComparison.Ordinal));
}
[Theory]
[InlineData("TryStartLiveSession")]
[InlineData("ClearInboundEntityState")]
@ -66,4 +112,32 @@ public sealed class GameWindowLiveSessionOwnershipTests
{
Assert.Null(typeof(GameWindow).GetMethod(methodName, PrivateInstance));
}
private static int CountOccurrences(string source, string value)
{
int count = 0;
int cursor = 0;
while ((cursor = source.IndexOf(
value,
cursor,
StringComparison.Ordinal)) >= 0)
{
count++;
cursor += value.Length;
}
return count;
}
private static string FindRepositoryRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
if (File.Exists(Path.Combine(current.FullName, "AcDream.slnx")))
return current.FullName;
current = current.Parent;
}
throw new DirectoryNotFoundException("AcDream.slnx was not found.");
}
}

View file

@ -258,7 +258,7 @@ public sealed class GameWindowSlice8BoundaryTests
"Hard(\"diagnostic command slot\", ingress.DiagnosticCommands.Deactivate)",
"Hard(\"retained gameplay\", () => ingress.RetainedGameplay?.Deactivate())",
"Hard(\"gameplay actions\", () => ingress.GameplayActions?.Deactivate())",
"Hard(\"live session\", () => DisposeLiveSession(ingress.LiveSession))",
"Hard(\"game runtime session\", ingress.Runtime.StopSession)",
"new ResourceShutdownStage(\"physical ingress cleanup\"",
"Soft(\"retained gameplay\", () => DisposeRetainedGameplay(ingress.RetainedGameplay))",
"Soft(\"gameplay actions\", () => DisposeGameplayActions(ingress.GameplayActions))",
@ -469,12 +469,11 @@ public sealed class GameWindowSlice8BoundaryTests
"new ResourceShutdownStage(\"frame borrowers\"",
"new ResourceShutdownStage(\"session dependents\"",
"new ResourceShutdownStage(\"live entities\"",
"new ResourceShutdownStage(\"runtime entity/object stream\"",
"new ResourceShutdownStage(\"effect dispatch edges\"",
"new ResourceShutdownStage(\"live entity dependents\"",
"new ResourceShutdownStage(\"submitted GPU work\"",
"new ResourceShutdownStage(\"render frontends\"",
"new ResourceShutdownStage(\"runtime communication state\"",
"new ResourceShutdownStage(\"game runtime root\"",
"new ResourceShutdownStage(\"shared texture owners\"",
"new ResourceShutdownStage(\"mesh adapter\"",
"new ResourceShutdownStage(\"remaining render owners\"",
@ -496,7 +495,7 @@ public sealed class GameWindowSlice8BoundaryTests
"Hard(\"retained gameplay\", () => ingress.RetainedGameplay?.Deactivate())",
"Hard(\"gameplay actions\", () => ingress.GameplayActions?.Deactivate())",
"Hard(\"camera pointer\", () => ingress.CameraPointer?.Deactivate())",
"Hard(\"live session\", () => DisposeLiveSession(ingress.LiveSession))",
"Hard(\"game runtime session\", ingress.Runtime.StopSession)",
"new ResourceShutdownStage(\"physical ingress cleanup\"",
"Soft(\"retained gameplay\", () => DisposeRetainedGameplay(ingress.RetainedGameplay))",
"Soft(\"gameplay actions\", () => DisposeGameplayActions(ingress.GameplayActions))",

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(

View file

@ -363,7 +363,7 @@ public sealed class LandblockBuildOriginTests
Assert.True(sessionStage >= 0);
int sessionOperation = source.IndexOf(
"Hard(\"live session\", () => DisposeLiveSession(ingress.LiveSession))",
"Hard(\"game runtime session\", ingress.Runtime.StopSession)",
sessionStage,
StringComparison.Ordinal);
Assert.True(sessionOperation > sessionStage);
@ -380,25 +380,30 @@ public sealed class LandblockBuildOriginTests
StringComparison.Ordinal);
Assert.True(streamerDispose > dependentStage);
int helper = source.IndexOf(
"private static void DisposeLiveSession(LiveSessionController? controller)",
string runtime = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.Runtime",
"GameRuntime.cs"));
int helper = runtime.IndexOf(
"public void StopSession()",
StringComparison.Ordinal);
Assert.True(helper > streamerDispose);
int sessionDispose = source.IndexOf(
"controller.Dispose()",
Assert.True(helper >= 0);
int sessionDispose = runtime.IndexOf(
"Session.Dispose();",
helper,
StringComparison.Ordinal);
Assert.True(sessionDispose > helper);
int disposalCompletionBarrier = source.IndexOf(
"if (!controller.IsDisposalComplete)",
int disposalCompletionBarrier = runtime.IndexOf(
"if (!Session.IsDisposalComplete)",
sessionDispose,
StringComparison.Ordinal);
Assert.True(disposalCompletionBarrier > sessionDispose);
Assert.Contains(
"Live-session disposal was deferred by a reentrant lifecycle callback.",
source[disposalCompletionBarrier..],
"The Runtime session shutdown was deferred by a re-entrant callback.",
runtime[disposalCompletionBarrier..],
StringComparison.Ordinal);
}

View file

@ -236,34 +236,18 @@ public sealed class RuntimeEntityOwnershipTests
[Fact]
public void CurrentRuntimeAdapters_DoNotRetainEntityOrInventoryMirrors()
{
FieldInfo[] viewFields = typeof(CurrentGameRuntimeViewAdapter)
FieldInfo[] adapterFields = typeof(CurrentGameRuntimeAdapter)
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
Assert.Contains(
viewFields,
field => field.FieldType == typeof(IRuntimeEntityView));
Assert.Contains(
viewFields,
field => field.FieldType == typeof(IRuntimeInventoryView));
adapterFields,
field => field.FieldType == typeof(GameRuntime));
Assert.DoesNotContain(
viewFields,
field => field.FieldType == typeof(LiveEntityRuntime)
|| field.FieldType == typeof(ClientObjectTable));
Assert.DoesNotContain(
typeof(CurrentGameRuntimeViewAdapter).GetNestedTypes(
BindingFlags.NonPublic),
type => type.Name is "EntityView" or "InventoryView");
FieldInfo[] eventFields = typeof(CurrentGameRuntimeEventAdapter)
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
Assert.DoesNotContain(
eventFields,
adapterFields,
field => field.FieldType == typeof(LiveEntityRuntime)
|| field.FieldType == typeof(ClientObjectTable)
|| field.FieldType == typeof(RuntimeEventSequencer));
Assert.Contains(
eventFields,
field => field.FieldType
== typeof(RuntimeEntityObjectLifetime));
|| field.FieldType == typeof(IRuntimeEntityView)
|| field.FieldType == typeof(IRuntimeInventoryView)
|| field.FieldType == typeof(RuntimeEntityObjectLifetime));
string root = FindRepositoryRoot();
string liveSource = File.ReadAllText(Path.Combine(
@ -276,19 +260,34 @@ public sealed class RuntimeEntityOwnershipTests
Assert.DoesNotContain("_directory.TryApply", liveSource);
Assert.DoesNotContain("_directory.RemoveActive", liveSource);
string viewSource = File.ReadAllText(Path.Combine(
string appRuntimeRoot = Path.Combine(
root,
"src",
"AcDream.App",
"Runtime",
"CurrentGameRuntimeViewAdapter.cs"));
"Runtime");
Assert.False(File.Exists(Path.Combine(
appRuntimeRoot,
"CurrentGameRuntimeViewAdapter.cs")));
Assert.False(File.Exists(Path.Combine(
appRuntimeRoot,
"CurrentGameRuntimeEventAdapter.cs")));
string rootSource = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.Runtime",
"GameRuntime.cs"));
Assert.Contains(
"_entityView.MaterializedCount",
viewSource,
"public RuntimeEntityObjectLifetime EntityObjects",
rootSource,
StringComparison.Ordinal);
Assert.DoesNotContain(
"_entities.MaterializedCount",
viewSource,
Assert.Contains(
"public IRuntimeEntityView Entities => EntityObjects.EntityView;",
rootSource,
StringComparison.Ordinal);
Assert.Contains(
"public IRuntimeInventoryView Inventory => EntityObjects.InventoryView;",
rootSource,
StringComparison.Ordinal);
}