diff --git a/docs/reviews/2026-08-18-r3-test-truth-ledger.md b/docs/reviews/2026-08-18-r3-test-truth-ledger.md
index 0e2ad557..5a9eefd0 100644
--- a/docs/reviews/2026-08-18-r3-test-truth-ledger.md
+++ b/docs/reviews/2026-08-18-r3-test-truth-ledger.md
@@ -1447,3 +1447,54 @@ R3 work:
- a targeted scan confirms neither owning test file retains a source reader or
its source-slicing helper. The final full inventory and twelve-assembly
Release gate are intentionally deferred to the one R3 closeout run.
+
+## Batch AE Runtime gameplay-owner wiring replacement
+
+Batch AE converts the nine approved consumer-wiring source readers across
+`RuntimeActionOwnershipTests`, `RuntimeCharacterOwnershipTests`,
+`RuntimeInventoryOwnershipTests`, and `RuntimeMovementOwnershipTests`. No
+product source changes and no test is removed. The two approved whole-tree
+construction policies—one action-owner policy and one movement-owner policy—
+remain source-aware by design and are not part of the 85 staged replacements.
+
+The replacements use reflected fields, properties, constructor parameters,
+compiled child access, and shutdown-root metadata to prove that:
+
+- retained UI, session composition, graphical command adapters, and item
+ interaction borrow the canonical action children and the exact Runtime
+ transaction owner, with no App `InteractionState` or UI-side transaction
+ construction;
+- `GameWindow` owns one `GameRuntime`, constructs no character child root,
+ and projects its Spellbook/LocalPlayer properties through
+ `RuntimeCharacterState`; content installation, session routing, retained UI,
+ options, and movement-skill updates all consume that owner;
+- the deleted App character-option and movement-skill owner types remain
+ absent, while Runtime's option replacement and movement-skill update edges
+ remain live;
+- `GameWindow` constructs none of the displaced inventory/item-mana/container/
+ shortcut snapshot owners; retained UI and item interaction use the Runtime
+ action transaction, session bindings use the Runtime shortcut store and use
+ completion, and shutdown carries only `GameRuntime`;
+- `ToolbarController` retains the exact `ShortcutStore` without constructing a
+ mirror, provider delegate, or loaded-state latch, and spell UI has no second
+ local mutation calls for filters, desired components, or favorites; and
+- graphical input, both runtime adapters, and both shutdown roots borrow the
+ exact `RuntimeLocalPlayerMovementState`, including its autorun and typed
+ command execution edges.
+
+`CompiledCallGraph.ReadOwned` was added as a test-only metadata facility. It
+reads declared owner methods plus compiler-generated acquisition/callback
+closures, so a typed dependency carried by a UI factory remains inspectable
+without freezing the lambda's source spelling.
+
+Verification follows the proportionate R3 policy:
+
+- all 11 focused ownership methods pass, including the two retained whole-tree
+ policies adjacent to the nine replacements;
+- the complete `AcDream.App.Tests` hermetic lane passes 5,381/5,381 with zero
+ skips or failures;
+- the complete locked Release build covers all 44 projects with zero warnings
+ and zero errors; and
+- a targeted scan finds source reads only in the two explicitly retained
+ whole-tree policy methods. The final inventory and twelve-assembly Release
+ gate remain deferred to closeout.
diff --git a/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs b/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs
index ca563bdf..de085749 100644
--- a/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs
+++ b/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs
@@ -56,6 +56,57 @@ internal static class CompiledCallGraph
.ToArray();
}
+ ///
+ /// Reads a type's declared methods plus compiler-generated closure methods
+ /// nested beneath it. Composition tests use this when an owned callback or
+ /// acquisition factory carries the typed edge under review.
+ ///
+ public static IReadOnlyList ReadOwned(Type type)
+ {
+ ArgumentNullException.ThrowIfNull(type);
+ const BindingFlags flags = BindingFlags.Instance
+ | BindingFlags.Static
+ | BindingFlags.Public
+ | BindingFlags.NonPublic
+ | BindingFlags.DeclaredOnly;
+ MethodBase[] roots = EnumerateOwnedTypes(type)
+ .SelectMany(owner => owner.GetMethods(flags)
+ .Cast()
+ .Concat(owner.GetConstructors(flags)))
+ .Where(method => method.GetMethodBody() is not null)
+ .ToArray();
+ var pending = new Queue(roots);
+ var seen = new HashSet(roots);
+ var calls = new List();
+ while (pending.TryDequeue(out MethodBase? method))
+ {
+ foreach (CompiledCall call in ReadMethodReferences(method))
+ {
+ calls.Add(call);
+ if (call.Target.Module == type.Module
+ && call.Target.Name.Contains('<', StringComparison.Ordinal)
+ && call.Target.GetMethodBody() is not null
+ && seen.Add(call.Target))
+ {
+ pending.Enqueue(call.Target);
+ }
+ }
+ }
+
+ return calls;
+ }
+
+ private static IEnumerable EnumerateOwnedTypes(Type root)
+ {
+ yield return root;
+ foreach (Type nested in root.GetNestedTypes(
+ BindingFlags.Public | BindingFlags.NonPublic))
+ {
+ foreach (Type owned in EnumerateOwnedTypes(nested))
+ yield return owned;
+ }
+ }
+
public static IReadOnlyList ReadStringLiterals(MethodBase method)
{
ArgumentNullException.ThrowIfNull(method);
diff --git a/tests/AcDream.App.Tests/Runtime/RuntimeActionOwnershipTests.cs b/tests/AcDream.App.Tests/Runtime/RuntimeActionOwnershipTests.cs
index 0458ecc6..14030ec1 100644
--- a/tests/AcDream.App.Tests/Runtime/RuntimeActionOwnershipTests.cs
+++ b/tests/AcDream.App.Tests/Runtime/RuntimeActionOwnershipTests.cs
@@ -1,4 +1,13 @@
+using System.Reflection;
using System.Text.RegularExpressions;
+using AcDream.App.Composition;
+using AcDream.App.Net;
+using AcDream.App.Rendering;
+using AcDream.App.Runtime;
+using AcDream.App.Tests.Architecture;
+using AcDream.App.UI;
+using AcDream.Runtime;
+using AcDream.Runtime.Gameplay;
namespace AcDream.App.Tests.Runtime;
@@ -81,71 +90,83 @@ public sealed class RuntimeActionOwnershipTests
[Fact]
public void UiSessionRuntimeAndShutdownBorrowTheExactActionChildren()
{
- string root = FindRepositoryRoot();
- string ui = ReadAppSource(
- root,
- "Composition",
- "InteractionRetainedUiComposition.cs");
- string session = ReadAppSource(
- root,
- "Composition",
- "SessionPlayerComposition.cs");
- string liveSession = ReadAppSource(
- root,
- "Net",
- "LiveSessionRuntimeFactory.cs");
- string commands = ReadAppSource(
- root,
- "Runtime",
- "CurrentGameRuntimeCommandAdapter.cs");
- string itemInteraction = ReadAppSource(
- root,
- "UI",
- "ItemInteractionController.cs");
- string shutdown = ReadAppSource(
- root,
- "Rendering",
- "GameWindowLifetime.cs");
-
- Assert.Contains("d.Actions.Interaction,", ui, StringComparison.Ordinal);
- Assert.Contains("d.Actions.Transactions,", ui, StringComparison.Ordinal);
- Assert.Contains("d.Actions.Selection", ui, StringComparison.Ordinal);
- Assert.Contains("d.Actions.Combat", ui, StringComparison.Ordinal);
- Assert.Contains(
- "_dependencies.Actions.CombatAttack",
+ IReadOnlyList ui =
+ CompiledCallGraph.ReadOwned(typeof(RetailInteractionRetainedUiCompositionFactory));
+ AssertActionChildren(
ui,
- StringComparison.Ordinal);
- Assert.Contains("d.Actions.SpellCast", ui, StringComparison.Ordinal);
- Assert.Contains("d.Actions.Selection", session, StringComparison.Ordinal);
- Assert.Contains("d.Actions.Combat", session, StringComparison.Ordinal);
- Assert.Contains("_domain.Actions.Combat", liveSession, StringComparison.Ordinal);
+ "get_Interaction",
+ "get_Transactions",
+ "get_Selection",
+ "get_Combat",
+ "get_SpellCast");
+ AssertActionChildren(
+ CompiledCallGraph.ReadOwned(typeof(InteractionRetainedUiCompositionPhase)),
+ "get_CombatAttack");
+
+ IReadOnlyList session =
+ CompiledCallGraph.ReadOwned(typeof(SessionPlayerCompositionPhase));
+ AssertActionChildren(session, "get_Selection", "get_Combat");
+
+ PropertyInfo domainActions = typeof(LiveSessionDomainRuntime).GetProperty(
+ "Actions",
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
+ ?? throw new MissingMemberException(typeof(LiveSessionDomainRuntime).FullName, "Actions");
+ Assert.Equal(typeof(RuntimeActionState), domainActions.PropertyType);
+
+ FieldInfo commandActions = Assert.Single(
+ typeof(CurrentGameRuntimeCommandAdapter).GetFields(
+ BindingFlags.Instance | BindingFlags.NonPublic),
+ field => field.Name == "_actions");
+ Assert.Equal(typeof(RuntimeActionState), commandActions.FieldType);
Assert.Contains(
- "_domain.Actions.Transactions.CompleteUse(error)",
- liveSession,
- StringComparison.Ordinal);
- Assert.Contains("_actions.Selection", commands, StringComparison.Ordinal);
- Assert.Contains(
- "RuntimeInteractionTransactionState runtimeTransactions,",
- itemInteraction,
- StringComparison.Ordinal);
+ CompiledCallGraph.ReadDeclared(typeof(CurrentGameRuntimeCommandAdapter)),
+ call => call.Target.DeclaringType == typeof(RuntimeActionState)
+ && call.Target.Name == "get_Selection");
+
+ FieldInfo transactions = Assert.Single(
+ typeof(ItemInteractionController).GetFields(
+ BindingFlags.Instance | BindingFlags.NonPublic),
+ field => field.Name == "_runtimeTransactions");
+ Assert.Equal(typeof(RuntimeInteractionTransactionState), transactions.FieldType);
Assert.DoesNotContain(
- "new InteractionState",
- itemInteraction,
- StringComparison.Ordinal);
- Assert.Contains(
- "new ResourceShutdownStage(\"game runtime root\"",
- shutdown,
- StringComparison.Ordinal);
+ CompiledCallGraph.ReadDeclared(typeof(ItemInteractionController)),
+ call => call.Target.DeclaringType == typeof(InteractionState)
+ && call.Target.IsConstructor);
+
+ IReadOnlyList labels = CompiledCallGraph.ReadStringLiterals(
+ typeof(GameWindowShutdownManifest).GetMethod(
+ nameof(GameWindowShutdownManifest.Create),
+ BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
+ ?? throw new MissingMethodException());
+ Assert.Contains("game runtime root", labels);
Assert.DoesNotContain(
- "RuntimeActionState Actions",
- shutdown,
- StringComparison.Ordinal);
- Assert.False(File.Exists(Path.Combine(
- root,
- "src",
- "AcDream.App",
- "UI",
- "InteractionState.cs")));
+ typeof(IngressShutdownRoots).GetProperties(),
+ property => property.PropertyType == typeof(RuntimeActionState));
+ Assert.DoesNotContain(
+ typeof(LiveShutdownRoots).GetProperties(),
+ property => property.PropertyType == typeof(RuntimeActionState));
+
+ Assert.DoesNotContain(
+ typeof(GameWindow).Assembly.GetTypes(),
+ type => type.FullName == "AcDream.App.UI.InteractionState");
+ }
+
+ private static void AssertActionChildren(
+ IReadOnlyList calls,
+ params string[] getters)
+ {
+ foreach (string getter in getters)
+ {
+ Assert.True(
+ calls.Any(call => call.Target.DeclaringType == typeof(RuntimeActionState)
+ && call.Target.Name == getter),
+ $"Missing RuntimeActionState.{getter}. Runtime calls: "
+ + string.Join(", ", calls
+ .Where(call => call.Target.DeclaringType?.Namespace?.StartsWith(
+ "AcDream.Runtime", StringComparison.Ordinal) == true)
+ .Select(call => $"{call.Target.DeclaringType?.Name}.{call.Target.Name}")
+ .Distinct()));
+ }
}
private static string ReadAppSource(string root, params string[] relative) =>
diff --git a/tests/AcDream.App.Tests/Runtime/RuntimeCharacterOwnershipTests.cs b/tests/AcDream.App.Tests/Runtime/RuntimeCharacterOwnershipTests.cs
index 77e2a1bb..cd5d924f 100644
--- a/tests/AcDream.App.Tests/Runtime/RuntimeCharacterOwnershipTests.cs
+++ b/tests/AcDream.App.Tests/Runtime/RuntimeCharacterOwnershipTests.cs
@@ -1,149 +1,145 @@
+using System.Reflection;
+using AcDream.App.Composition;
+using AcDream.App.Net;
+using AcDream.App.Rendering;
+using AcDream.App.Tests.Architecture;
+using AcDream.Core.Player;
+using AcDream.Core.Spells;
+using AcDream.Runtime;
+using AcDream.Runtime.Gameplay;
+using AcDream.Runtime.Session;
+
namespace AcDream.App.Tests.Runtime;
public sealed class RuntimeCharacterOwnershipTests
{
+ private const BindingFlags Declared = BindingFlags.Instance
+ | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
+ | BindingFlags.DeclaredOnly;
+
[Fact]
public void ProductionConstructsOnlyTheRuntimeCharacterOwner()
{
- string source = ReadSource("Rendering", "GameWindow.cs");
+ FieldInfo runtime = typeof(GameWindow).GetField("_runtime", Declared)
+ ?? throw new MissingFieldException(typeof(GameWindow).FullName, "_runtime");
+ Assert.Equal(typeof(GameRuntime), runtime.FieldType);
+ Assert.Single(
+ CompiledCallGraph.ReadDeclared(typeof(GameWindow)),
+ call => call.Target.DeclaringType == typeof(GameRuntime)
+ && call.Target.IsConstructor);
- Assert.Contains(
- "private readonly GameRuntime _runtime;",
- source,
- StringComparison.Ordinal);
- Assert.Contains(
- "_runtime = new GameRuntime(new GameRuntimeDependencies(",
- source,
- StringComparison.Ordinal);
+ Type[] childOwners =
+ [
+ typeof(RuntimeCharacterState),
+ typeof(Spellbook),
+ typeof(LocalPlayerState),
+ ];
Assert.DoesNotContain(
- "new RuntimeCharacterState(",
- source,
- StringComparison.Ordinal);
- Assert.DoesNotContain(
- "new AcDream.Core.Spells.Spellbook",
- source,
- StringComparison.Ordinal);
- Assert.DoesNotContain(
- "new AcDream.Core.Player.LocalPlayerState",
- source,
- StringComparison.Ordinal);
- Assert.Contains(
- "SpellBook =>\n _runtimeCharacter.Spellbook;",
- Normalize(source),
- StringComparison.Ordinal);
- Assert.Contains(
- "LocalPlayer =>\n _runtimeCharacter.LocalPlayer;",
- Normalize(source),
- StringComparison.Ordinal);
+ CompiledCallGraph.ReadDeclared(typeof(GameWindow)),
+ call => call.Target.IsConstructor
+ && childOwners.Contains(call.Target.DeclaringType));
+
+ AssertGetterRoutesToCharacterChild("SpellBook", "get_Spellbook");
+ AssertGetterRoutesToCharacterChild("LocalPlayer", "get_LocalPlayer");
}
[Fact]
public void SessionRoutingAndShutdownBorrowTheExactRuntimeOwner()
{
- string session = ReadSource("Net", "LiveSessionRuntimeFactory.cs");
- string router = ReadRuntimeSource(
- "Session",
- "LiveSessionEventRouter.cs");
- string content = ReadSource(
- "Composition",
- "ContentEffectsAudioComposition.cs");
- string ui = ReadSource(
- "Composition",
- "InteractionRetainedUiComposition.cs");
- string inventory = ReadRuntimeSource(
- "Gameplay",
- "RuntimeInventoryState.cs");
- string shutdown = ReadSource("Rendering", "GameWindowLifetime.cs");
+ PropertyInfo character = typeof(LiveSessionDomainRuntime).GetProperty(
+ "Character",
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
+ ?? throw new MissingMemberException(typeof(LiveSessionDomainRuntime).FullName,
+ "Character");
+ Assert.Equal(typeof(RuntimeCharacterState), character.PropertyType);
+ IReadOnlyList router =
+ CompiledCallGraph.ReadOwned(typeof(LiveSessionEventRouter));
+ AssertCharacterChildren(router, "get_Spellbook", "get_LocalPlayer");
+
+ MethodInfo install = typeof(RetailContentEffectsAudioCompositionFactory).GetMethod(
+ nameof(RetailContentEffectsAudioCompositionFactory.InstallSpellMetadata),
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
+ ?? throw new MissingMethodException();
Assert.Contains(
- "RuntimeCharacterState Character",
- session,
- StringComparison.Ordinal);
- Assert.Contains(
- "character.Character.Spellbook",
- router,
- StringComparison.Ordinal);
- Assert.Contains(
- "character.Character.LocalPlayer",
- router,
- StringComparison.Ordinal);
- Assert.Contains(
- "_factory.InstallSpellMetadata(_dependencies.Character, magic)",
- content,
- StringComparison.Ordinal);
- Assert.Contains(
- "d.Character.Spellbook",
- ui,
- StringComparison.Ordinal);
- Assert.Contains(
- "d.Character.LocalPlayer",
- ui,
- StringComparison.Ordinal);
+ CompiledCallGraph.Read(install),
+ call => call.Target.DeclaringType == typeof(RuntimeCharacterState)
+ && call.Target.Name == nameof(RuntimeCharacterState.InstallSpellMetadata));
+
+ IReadOnlyList ui =
+ CompiledCallGraph.ReadOwned(typeof(RetailInteractionRetainedUiCompositionFactory));
+ AssertCharacterChildren(ui, "get_Spellbook", "get_LocalPlayer");
+
Assert.DoesNotContain(
- "DesiredComponentState",
- inventory,
- StringComparison.Ordinal);
- Assert.Contains(
- "new ResourceShutdownStage(\"game runtime root\"",
- shutdown,
- StringComparison.Ordinal);
+ typeof(RuntimeInventoryState).GetFields(Declared),
+ field => field.FieldType.Name == "DesiredComponentState");
+
+ IReadOnlyList labels = CompiledCallGraph.ReadStringLiterals(
+ typeof(GameWindowShutdownManifest).GetMethod(
+ nameof(GameWindowShutdownManifest.Create),
+ BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
+ ?? throw new MissingMethodException());
+ Assert.Contains("game runtime root", labels);
Assert.DoesNotContain(
- "RuntimeCharacterState Character",
- shutdown,
- StringComparison.Ordinal);
+ typeof(IngressShutdownRoots).GetProperties(),
+ property => property.PropertyType == typeof(RuntimeCharacterState));
+ Assert.DoesNotContain(
+ typeof(LiveShutdownRoots).GetProperties(),
+ property => property.PropertyType == typeof(RuntimeCharacterState));
}
[Fact]
public void AppCharacterOptionAndMovementSkillOwnersWereDeleted()
{
- string gameWindow = ReadSource("Rendering", "GameWindow.cs");
- string sources = ReadSource(
- "Composition",
- "InteractionUiRuntimeSources.cs");
- string playerState = ReadSource("Input", "LocalPlayerRuntimeState.cs");
- string session = ReadSource("Net", "LiveSessionRuntimeFactory.cs");
- string router = ReadRuntimeSource(
- "Session",
- "LiveSessionEventRouter.cs");
-
- Assert.DoesNotContain(
+ HashSet removed =
+ [
"PlayerCharacterOptionsState",
- gameWindow + sources + session,
- StringComparison.Ordinal);
- Assert.DoesNotContain(
"LocalPlayerSkillState",
- gameWindow + playerState + session,
- StringComparison.Ordinal);
+ ];
+ Assert.DoesNotContain(
+ typeof(GameWindow).Assembly.GetTypes(),
+ type => type.Name is not null && removed.Contains(type.Name));
+
+ IReadOnlyList router =
+ CompiledCallGraph.ReadOwned(typeof(LiveSessionEventRouter));
Assert.Contains(
- "character.Character.Options.Replace",
router,
- StringComparison.Ordinal);
+ call => call.Target.DeclaringType == typeof(RuntimeCharacterOptionsState)
+ && call.Target.Name == nameof(RuntimeCharacterOptionsState.Replace));
Assert.Contains(
- "character.Character.MovementSkills.Update",
router,
- StringComparison.Ordinal);
+ call => call.Target.DeclaringType == typeof(RuntimeMovementSkillState)
+ && call.Target.Name.StartsWith("Update", StringComparison.Ordinal));
}
- private static string ReadSource(params string[] relative) =>
- File.ReadAllText(Path.Combine(
- [FindRepositoryRoot(), "src", "AcDream.App", .. relative]));
-
- private static string ReadRuntimeSource(params string[] relative) =>
- File.ReadAllText(Path.Combine(
- [FindRepositoryRoot(), "src", "AcDream.Runtime", .. relative]));
-
- private static string Normalize(string source) =>
- source.Replace("\r\n", "\n", StringComparison.Ordinal);
-
- private static string FindRepositoryRoot()
+ private static void AssertGetterRoutesToCharacterChild(
+ string property,
+ string childGetter)
{
- var current = new DirectoryInfo(AppContext.BaseDirectory);
- while (current is not null)
+ MethodInfo getter = typeof(GameWindow).GetProperty(property, Declared)?.GetMethod
+ ?? throw new MissingMemberException(typeof(GameWindow).FullName, property);
+ IReadOnlyList calls = CompiledCallGraph.Read(getter);
+ Assert.Contains(calls, call => call.Target.DeclaringType == typeof(GameWindow)
+ && call.Target.Name == "get__runtimeCharacter");
+ Assert.Contains(calls, call => call.Target.DeclaringType == typeof(RuntimeCharacterState)
+ && call.Target.Name == childGetter);
+ }
+
+ private static void AssertCharacterChildren(
+ IReadOnlyList calls,
+ params string[] getters)
+ {
+ foreach (string getter in getters)
{
- if (File.Exists(Path.Combine(current.FullName, "AcDream.slnx")))
- return current.FullName;
- current = current.Parent;
+ Assert.True(
+ calls.Any(call => call.Target.DeclaringType == typeof(RuntimeCharacterState)
+ && call.Target.Name == getter),
+ $"Missing RuntimeCharacterState.{getter}. Runtime calls: "
+ + string.Join(", ", calls
+ .Where(call => call.Target.DeclaringType?.Namespace?.StartsWith(
+ "AcDream.Runtime", StringComparison.Ordinal) == true)
+ .Select(call => $"{call.Target.DeclaringType?.Name}.{call.Target.Name}")
+ .Distinct()));
}
- throw new DirectoryNotFoundException("AcDream.slnx was not found.");
}
}
diff --git a/tests/AcDream.App.Tests/Runtime/RuntimeInventoryOwnershipTests.cs b/tests/AcDream.App.Tests/Runtime/RuntimeInventoryOwnershipTests.cs
index fc3a0357..fff4823f 100644
--- a/tests/AcDream.App.Tests/Runtime/RuntimeInventoryOwnershipTests.cs
+++ b/tests/AcDream.App.Tests/Runtime/RuntimeInventoryOwnershipTests.cs
@@ -1,199 +1,156 @@
+using System.Reflection;
+using AcDream.App.Composition;
+using AcDream.App.Net;
+using AcDream.App.Rendering;
+using AcDream.App.Tests.Architecture;
+using AcDream.App.UI;
+using AcDream.App.UI.Layout;
+using AcDream.Core.Items;
+using AcDream.Runtime;
+using AcDream.Runtime.Gameplay;
+
namespace AcDream.App.Tests.Runtime;
public sealed class RuntimeInventoryOwnershipTests
{
+ private const BindingFlags Declared = BindingFlags.Instance
+ | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
+ | BindingFlags.DeclaredOnly;
+
[Fact]
public void ProductionConstructsOnlyTheRuntimeInventoryOwner()
{
- string source = ReadSource("Rendering", "GameWindow.cs");
+ FieldInfo runtime = typeof(GameWindow).GetField("_runtime", Declared)
+ ?? throw new MissingFieldException(typeof(GameWindow).FullName, "_runtime");
+ Assert.Equal(typeof(GameRuntime), runtime.FieldType);
- Assert.Contains(
- "private readonly GameRuntime _runtime;",
- source,
- StringComparison.Ordinal);
- Assert.DoesNotContain(
- "new RuntimeInventoryState(",
- source,
- StringComparison.Ordinal);
- Assert.DoesNotContain(
- "new AcDream.Core.Items.ItemManaState",
- source,
- StringComparison.Ordinal);
- Assert.DoesNotContain(
- "new DesiredComponentSnapshotState",
- source,
- StringComparison.Ordinal);
- Assert.DoesNotContain(
- "new ShortcutSnapshotState",
- source,
- StringComparison.Ordinal);
- Assert.DoesNotContain(
- "new AcDream.Core.Items.ExternalContainerState",
- source,
- StringComparison.Ordinal);
- Assert.DoesNotContain(
+ HashSet displacedConstructors =
+ [
+ nameof(RuntimeInventoryState),
+ nameof(ItemManaState),
+ "DesiredComponentSnapshotState",
+ "ShortcutSnapshotState",
+ nameof(ExternalContainerState),
"DesiredComponentState",
- source,
- StringComparison.Ordinal);
+ ];
+ Assert.DoesNotContain(
+ CompiledCallGraph.ReadDeclared(typeof(GameWindow)),
+ call => call.Target.IsConstructor
+ && call.Target.DeclaringType?.Name is { } name
+ && displacedConstructors.Contains(name));
+ Assert.DoesNotContain(
+ typeof(GameWindow).GetFields(Declared),
+ field => displacedConstructors.Contains(field.FieldType.Name));
}
[Fact]
public void UiSessionAndShutdownBorrowTheExactRuntimeOwner()
{
- string ui = ReadSource(
- "Composition",
- "InteractionRetainedUiComposition.cs");
- string session = ReadSource("Net", "LiveSessionRuntimeFactory.cs");
- string shutdown = ReadSource("Rendering", "GameWindowLifetime.cs");
- string itemInteraction = ReadSource(
- "UI",
- "ItemInteractionController.cs");
+ IReadOnlyList ui =
+ CompiledCallGraph.ReadOwned(typeof(RetailInteractionRetainedUiCompositionFactory));
+ AssertRuntimeCall(ui, typeof(RuntimeActionState), "get_Transactions");
+ Assert.DoesNotContain(
+ ui,
+ call => call.Target.DeclaringType == typeof(InventoryTransactionState)
+ && call.Target.IsConstructor);
- Assert.Contains(
- "d.Actions.Transactions",
- ui,
- StringComparison.Ordinal);
+ FieldInfo transactions = Assert.Single(
+ typeof(ItemInteractionController).GetFields(Declared),
+ field => field.Name == "_runtimeTransactions");
+ Assert.Equal(typeof(RuntimeInteractionTransactionState), transactions.FieldType);
Assert.DoesNotContain(
- "new InventoryTransactionState(d.Inventory.Objects)",
- ui,
- StringComparison.Ordinal);
+ typeof(ItemInteractionController).GetFields(Declared),
+ field => field.Name == "_ownsTransactions");
Assert.DoesNotContain(
- "new InventoryTransactionState",
- itemInteraction,
- StringComparison.Ordinal);
+ CompiledCallGraph.ReadDeclared(typeof(ItemInteractionController)),
+ call => call.Target.DeclaringType == typeof(InventoryTransactionState)
+ && call.Target.IsConstructor);
+
+ IReadOnlyList bindings =
+ CompiledCallGraph.ReadOwned(typeof(LiveSessionRuntimeFactory));
+ Assert.Contains(bindings, call => call.Target.DeclaringType == typeof(ShortcutStore)
+ && call.Target.Name == nameof(ShortcutStore.Load));
+ Assert.Contains(bindings, call =>
+ call.Target.DeclaringType == typeof(RuntimeInteractionTransactionState)
+ && call.Target.Name == nameof(RuntimeInteractionTransactionState.CompleteUse));
+ Assert.DoesNotContain(bindings, call =>
+ call.Target.DeclaringType == typeof(RuntimeInventoryState)
+ && call.Target.Name == "get_DesiredComponents");
+
+ IReadOnlyList labels = CompiledCallGraph.ReadStringLiterals(
+ typeof(GameWindowShutdownManifest).GetMethod(
+ nameof(GameWindowShutdownManifest.Create),
+ BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
+ ?? throw new MissingMethodException());
+ Assert.Contains("game runtime root", labels);
+ Assert.Contains("game runtime", labels);
Assert.DoesNotContain(
- "_ownsTransactions",
- itemInteraction,
- StringComparison.Ordinal);
- Assert.Contains(
- "RuntimeInteractionTransactionState runtimeTransactions,",
- itemInteraction,
- StringComparison.Ordinal);
- Assert.Contains(
- "OnShortcuts: _domain.Inventory.Shortcuts.Load",
- session,
- StringComparison.Ordinal);
- Assert.Contains(
- "_domain.Actions.Transactions.CompleteUse(error)",
- session,
- StringComparison.Ordinal);
+ typeof(IngressShutdownRoots).GetProperties(),
+ property => property.PropertyType == typeof(RuntimeInventoryState));
Assert.DoesNotContain(
- "_domain.Inventory.DesiredComponents",
- session,
- StringComparison.Ordinal);
- Assert.Contains(
- "new ResourceShutdownStage(\"game runtime root\"",
- shutdown,
- StringComparison.Ordinal);
- Assert.Contains(
- "Hard(\"game runtime\", () => DisposeGameRuntime(live.Runtime))",
- shutdown,
- StringComparison.Ordinal);
- Assert.DoesNotContain(
- "RuntimeInventoryState Inventory",
- shutdown,
- StringComparison.Ordinal);
+ typeof(LiveShutdownRoots).GetProperties(),
+ property => property.PropertyType == typeof(RuntimeInventoryState));
}
[Fact]
public void ToolbarBorrowsRuntimeShortcutManagerWithoutAProviderOrMirror()
{
- string toolbar = ReadSource(
- "UI",
- "Layout",
- "ToolbarController.cs");
- string ui = ReadSource(
- "Composition",
- "InteractionRetainedUiComposition.cs");
-
- Assert.Contains(
- "private readonly ShortcutStore _store;",
- toolbar,
- StringComparison.Ordinal);
- Assert.Contains(
- "_store = shortcuts ?? throw",
- toolbar,
- StringComparison.Ordinal);
+ FieldInfo store = Assert.Single(
+ typeof(ToolbarController).GetFields(Declared),
+ field => field.Name == "_store");
+ Assert.Equal(typeof(ShortcutStore), store.FieldType);
Assert.DoesNotContain(
- "new ShortcutStore()",
- toolbar,
- StringComparison.Ordinal);
+ CompiledCallGraph.ReadDeclared(typeof(ToolbarController)),
+ call => call.Target.DeclaringType == typeof(ShortcutStore)
+ && call.Target.IsConstructor);
Assert.DoesNotContain(
- "Func>",
- toolbar,
- StringComparison.Ordinal);
+ typeof(ToolbarController).GetFields(Declared),
+ field => field.Name == "_storeLoaded"
+ || IsShortcutProvider(field.FieldType));
Assert.DoesNotContain(
- "_storeLoaded",
- toolbar,
- StringComparison.Ordinal);
- Assert.Contains(
- "d.Inventory.Shortcuts,",
- ui,
- StringComparison.Ordinal);
+ typeof(ToolbarController).GetConstructors(Declared)
+ .SelectMany(constructor => constructor.GetParameters()),
+ parameter => IsShortcutProvider(parameter.ParameterType));
+ AssertRuntimeCall(
+ CompiledCallGraph.ReadOwned(typeof(RetailInteractionRetainedUiCompositionFactory)),
+ typeof(RuntimeInventoryState),
+ "get_Shortcuts");
}
[Fact]
public void SpellUiDoesNotApplyASecondLocalCommandMutation()
{
- string spellbook = ReadSource(
- "UI",
- "Layout",
- "SpellbookWindowController.cs");
- string spellcasting = ReadSource(
- "UI",
- "Layout",
- "SpellcastingUiController.cs");
-
+ HashSet duplicateMutations =
+ [
+ "SetSpellbookFilters",
+ "SetDesiredComponent",
+ "SetFavorite",
+ "RemoveFavorite",
+ ];
Assert.DoesNotContain(
- "_spellbook.SetSpellbookFilters(filters);",
- spellbook,
- StringComparison.Ordinal);
- Assert.DoesNotContain(
- "_spellbook.SetDesiredComponent(componentId, parsed);",
- spellbook,
- StringComparison.Ordinal);
- Assert.DoesNotContain(
- "_spellbook.SetFavorite(",
- spellcasting,
- StringComparison.Ordinal);
- Assert.DoesNotContain(
- "_spellbook.RemoveFavorite(",
- spellcasting,
- StringComparison.Ordinal);
+ CompiledCallGraph.ReadOwned(typeof(SpellbookWindowController))
+ .Concat(CompiledCallGraph.ReadOwned(typeof(SpellcastingUiController))),
+ call => duplicateMutations.Contains(call.Target.Name));
}
- private static string ReadSource(params string[] relative)
- {
- string root = FindRepositoryRoot();
- return File.ReadAllText(Path.Combine(
- [root, "src", "AcDream.App", .. relative]));
- }
+ private static bool IsShortcutProvider(Type type) =>
+ type.IsGenericType
+ && type.GetGenericTypeDefinition() == typeof(Func<>)
+ && type.GenericTypeArguments[0].IsGenericType
+ && type.GenericTypeArguments[0].GetGenericTypeDefinition()
+ == typeof(IReadOnlyList<>);
- 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.");
- }
-
- private static void AssertAppearsInOrder(
- string source,
- params string[] fragments)
- {
- int cursor = 0;
- foreach (string fragment in fragments)
- {
- int index = source.IndexOf(
- fragment,
- cursor,
- StringComparison.Ordinal);
- Assert.True(index >= 0, $"Missing source fragment: {fragment}");
- cursor = index + fragment.Length;
- }
- }
+ private static void AssertRuntimeCall(
+ IReadOnlyList calls,
+ Type owner,
+ string name) =>
+ Assert.True(
+ calls.Any(call => call.Target.DeclaringType == owner
+ && call.Target.Name == name),
+ $"Missing {owner.Name}.{name}. Runtime calls: "
+ + string.Join(", ", calls
+ .Where(call => call.Target.DeclaringType?.Namespace?.StartsWith(
+ "AcDream.Runtime", StringComparison.Ordinal) == true)
+ .Select(call => $"{call.Target.DeclaringType?.Name}.{call.Target.Name}")
+ .Distinct()));
}
diff --git a/tests/AcDream.App.Tests/Runtime/RuntimeMovementOwnershipTests.cs b/tests/AcDream.App.Tests/Runtime/RuntimeMovementOwnershipTests.cs
index 376cea1a..d752ff7e 100644
--- a/tests/AcDream.App.Tests/Runtime/RuntimeMovementOwnershipTests.cs
+++ b/tests/AcDream.App.Tests/Runtime/RuntimeMovementOwnershipTests.cs
@@ -1,4 +1,11 @@
+using System.Reflection;
using System.Text.RegularExpressions;
+using AcDream.App.Input;
+using AcDream.App.Rendering;
+using AcDream.App.Runtime;
+using AcDream.App.Tests.Architecture;
+using AcDream.Runtime;
+using AcDream.Runtime.Gameplay;
namespace AcDream.App.Tests.Runtime;
@@ -57,65 +64,67 @@ public sealed class RuntimeMovementOwnershipTests
[Fact]
public void GraphicalInputRuntimeViewsAndShutdownBorrowTheExactOwner()
{
- string root = FindRepositoryRoot();
- string gameWindow = ReadAppSource(root, "Rendering", "GameWindow.cs");
- string input = ReadAppSource(
- root,
- "Input",
- "DispatcherMovementInputSource.cs");
- string adapter = ReadAppSource(
- root,
- "Runtime",
- "CurrentGameRuntimeAdapter.cs");
- string commands = ReadAppSource(
- root,
- "Runtime",
- "CurrentGameRuntimeCommandAdapter.cs");
- string lifetime = ReadAppSource(
- root,
- "Rendering",
- "GameWindowLifetime.cs");
+ PropertyInfo movementProperty = typeof(GameWindow).GetProperty(
+ "_playerControllerSlot",
+ BindingFlags.Instance | BindingFlags.NonPublic)
+ ?? throw new MissingMemberException(typeof(GameWindow).FullName,
+ "_playerControllerSlot");
+ Assert.Equal(typeof(RuntimeLocalPlayerMovementState),
+ movementProperty.PropertyType);
+ Assert.Contains(
+ CompiledCallGraph.Read(movementProperty.GetMethod!),
+ call => call.Target.DeclaringType == typeof(GameRuntime)
+ && call.Target.Name == "get_MovementOwner");
+ ConstructorInfo window = Assert.Single(
+ typeof(GameWindow).GetConstructors(
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic),
+ constructor => CompiledCallGraph.Read(constructor).Any(call =>
+ call.Target.DeclaringType == typeof(DispatcherMovementInputSource)
+ && call.Target.IsConstructor));
Assert.Contains(
- "RuntimeLocalPlayerMovementState _playerControllerSlot =>",
- gameWindow,
- StringComparison.Ordinal);
+ CompiledCallGraph.Read(window),
+ call => call.Target.DeclaringType == typeof(GameWindow)
+ && call.Target.Name == "get__playerControllerSlot");
+
+ FieldInfo inputMovement = Assert.Single(
+ typeof(DispatcherMovementInputSource).GetFields(
+ BindingFlags.Instance | BindingFlags.NonPublic),
+ field => field.Name == "_movement");
+ Assert.Equal(typeof(RuntimeLocalPlayerMovementState), inputMovement.FieldType);
Assert.Contains(
- "_runtime.MovementOwner;",
- gameWindow,
- StringComparison.Ordinal);
+ CompiledCallGraph.ReadDeclared(typeof(DispatcherMovementInputSource)),
+ call => call.Target.DeclaringType == typeof(RuntimeLocalPlayerMovementState)
+ && call.Target.Name == "get_AutoRunActive");
+
+ FieldInfo adapterRuntime = Assert.Single(
+ typeof(CurrentGameRuntimeAdapter).GetFields(
+ BindingFlags.Instance | BindingFlags.NonPublic),
+ field => field.Name == "_runtime");
+ Assert.Equal(typeof(GameRuntime), adapterRuntime.FieldType);
+ FieldInfo commandMovement = Assert.Single(
+ typeof(CurrentGameRuntimeCommandAdapter).GetFields(
+ BindingFlags.Instance | BindingFlags.NonPublic),
+ field => field.Name == "_movement");
+ Assert.Equal(typeof(RuntimeLocalPlayerMovementState), commandMovement.FieldType);
Assert.Contains(
- "new AcDream.App.Input.DispatcherMovementInputSource(",
- gameWindow,
- StringComparison.Ordinal);
+ CompiledCallGraph.ReadDeclared(typeof(CurrentGameRuntimeCommandAdapter)),
+ call => call.Target.DeclaringType == typeof(RuntimeLocalPlayerMovementState)
+ && call.Target.Name == nameof(RuntimeLocalPlayerMovementState.Execute));
+
+ Assert.Equal(typeof(GameRuntime),
+ typeof(IngressShutdownRoots).GetProperty("Runtime")?.PropertyType);
+ Assert.Equal(typeof(GameRuntime),
+ typeof(LiveShutdownRoots).GetProperty("Runtime")?.PropertyType);
Assert.Contains(
- "_playerControllerSlot,",
- gameWindow,
- StringComparison.Ordinal);
- Assert.Contains(
- "RuntimeLocalPlayerMovementState movement,",
- input,
- StringComparison.Ordinal);
- Assert.Contains("_movement.AutoRunActive", input, StringComparison.Ordinal);
- Assert.Contains(
- "private readonly GameRuntime _runtime;",
- adapter,
- StringComparison.Ordinal);
- Assert.Contains("_movement.Execute(command)", commands, StringComparison.Ordinal);
- Assert.Contains(
- "GameRuntime Runtime",
- lifetime,
- StringComparison.Ordinal);
- Assert.Contains(
- "\"game runtime root\"",
- lifetime,
- StringComparison.Ordinal);
+ "game runtime root",
+ CompiledCallGraph.ReadStringLiterals(
+ typeof(GameWindowShutdownManifest).GetMethod(
+ nameof(GameWindowShutdownManifest.Create),
+ BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
+ ?? throw new MissingMethodException()));
}
- private static string ReadAppSource(string root, params string[] relative) =>
- File.ReadAllText(Path.Combine(
- [root, "src", "AcDream.App", .. relative]));
-
private static string FindRepositoryRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);