test: replace gameplay owner source freezes

This commit is contained in:
Erik 2026-08-18 16:49:38 +02:00
parent 9b94050229
commit 84034f732c
6 changed files with 473 additions and 388 deletions

View file

@ -1447,3 +1447,54 @@ R3 work:
- a targeted scan confirms neither owning test file retains a source reader or - a targeted scan confirms neither owning test file retains a source reader or
its source-slicing helper. The final full inventory and twelve-assembly its source-slicing helper. The final full inventory and twelve-assembly
Release gate are intentionally deferred to the one R3 closeout run. 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.

View file

@ -56,6 +56,57 @@ internal static class CompiledCallGraph
.ToArray(); .ToArray();
} }
/// <summary>
/// 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.
/// </summary>
public static IReadOnlyList<CompiledCall> 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<MethodBase>()
.Concat(owner.GetConstructors(flags)))
.Where(method => method.GetMethodBody() is not null)
.ToArray();
var pending = new Queue<MethodBase>(roots);
var seen = new HashSet<MethodBase>(roots);
var calls = new List<CompiledCall>();
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<Type> 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<string> ReadStringLiterals(MethodBase method) public static IReadOnlyList<string> ReadStringLiterals(MethodBase method)
{ {
ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(method);

View file

@ -1,4 +1,13 @@
using System.Reflection;
using System.Text.RegularExpressions; 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; namespace AcDream.App.Tests.Runtime;
@ -81,71 +90,83 @@ public sealed class RuntimeActionOwnershipTests
[Fact] [Fact]
public void UiSessionRuntimeAndShutdownBorrowTheExactActionChildren() public void UiSessionRuntimeAndShutdownBorrowTheExactActionChildren()
{ {
string root = FindRepositoryRoot(); IReadOnlyList<CompiledCall> ui =
string ui = ReadAppSource( CompiledCallGraph.ReadOwned(typeof(RetailInteractionRetainedUiCompositionFactory));
root, AssertActionChildren(
"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",
ui, ui,
StringComparison.Ordinal); "get_Interaction",
Assert.Contains("d.Actions.SpellCast", ui, StringComparison.Ordinal); "get_Transactions",
Assert.Contains("d.Actions.Selection", session, StringComparison.Ordinal); "get_Selection",
Assert.Contains("d.Actions.Combat", session, StringComparison.Ordinal); "get_Combat",
Assert.Contains("_domain.Actions.Combat", liveSession, StringComparison.Ordinal); "get_SpellCast");
AssertActionChildren(
CompiledCallGraph.ReadOwned(typeof(InteractionRetainedUiCompositionPhase)),
"get_CombatAttack");
IReadOnlyList<CompiledCall> 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( Assert.Contains(
"_domain.Actions.Transactions.CompleteUse(error)", CompiledCallGraph.ReadDeclared(typeof(CurrentGameRuntimeCommandAdapter)),
liveSession, call => call.Target.DeclaringType == typeof(RuntimeActionState)
StringComparison.Ordinal); && call.Target.Name == "get_Selection");
Assert.Contains("_actions.Selection", commands, StringComparison.Ordinal);
Assert.Contains( FieldInfo transactions = Assert.Single(
"RuntimeInteractionTransactionState runtimeTransactions,", typeof(ItemInteractionController).GetFields(
itemInteraction, BindingFlags.Instance | BindingFlags.NonPublic),
StringComparison.Ordinal); field => field.Name == "_runtimeTransactions");
Assert.Equal(typeof(RuntimeInteractionTransactionState), transactions.FieldType);
Assert.DoesNotContain( Assert.DoesNotContain(
"new InteractionState", CompiledCallGraph.ReadDeclared(typeof(ItemInteractionController)),
itemInteraction, call => call.Target.DeclaringType == typeof(InteractionState)
StringComparison.Ordinal); && call.Target.IsConstructor);
Assert.Contains(
"new ResourceShutdownStage(\"game runtime root\"", IReadOnlyList<string> labels = CompiledCallGraph.ReadStringLiterals(
shutdown, typeof(GameWindowShutdownManifest).GetMethod(
StringComparison.Ordinal); nameof(GameWindowShutdownManifest.Create),
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
?? throw new MissingMethodException());
Assert.Contains("game runtime root", labels);
Assert.DoesNotContain( Assert.DoesNotContain(
"RuntimeActionState Actions", typeof(IngressShutdownRoots).GetProperties(),
shutdown, property => property.PropertyType == typeof(RuntimeActionState));
StringComparison.Ordinal); Assert.DoesNotContain(
Assert.False(File.Exists(Path.Combine( typeof(LiveShutdownRoots).GetProperties(),
root, property => property.PropertyType == typeof(RuntimeActionState));
"src",
"AcDream.App", Assert.DoesNotContain(
"UI", typeof(GameWindow).Assembly.GetTypes(),
"InteractionState.cs"))); type => type.FullName == "AcDream.App.UI.InteractionState");
}
private static void AssertActionChildren(
IReadOnlyList<CompiledCall> 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) => private static string ReadAppSource(string root, params string[] relative) =>

View file

@ -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; namespace AcDream.App.Tests.Runtime;
public sealed class RuntimeCharacterOwnershipTests public sealed class RuntimeCharacterOwnershipTests
{ {
private const BindingFlags Declared = BindingFlags.Instance
| BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
| BindingFlags.DeclaredOnly;
[Fact] [Fact]
public void ProductionConstructsOnlyTheRuntimeCharacterOwner() 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( Type[] childOwners =
"private readonly GameRuntime _runtime;", [
source, typeof(RuntimeCharacterState),
StringComparison.Ordinal); typeof(Spellbook),
Assert.Contains( typeof(LocalPlayerState),
"_runtime = new GameRuntime(new GameRuntimeDependencies(", ];
source,
StringComparison.Ordinal);
Assert.DoesNotContain( Assert.DoesNotContain(
"new RuntimeCharacterState(", CompiledCallGraph.ReadDeclared(typeof(GameWindow)),
source, call => call.Target.IsConstructor
StringComparison.Ordinal); && childOwners.Contains(call.Target.DeclaringType));
Assert.DoesNotContain(
"new AcDream.Core.Spells.Spellbook", AssertGetterRoutesToCharacterChild("SpellBook", "get_Spellbook");
source, AssertGetterRoutesToCharacterChild("LocalPlayer", "get_LocalPlayer");
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);
} }
[Fact] [Fact]
public void SessionRoutingAndShutdownBorrowTheExactRuntimeOwner() public void SessionRoutingAndShutdownBorrowTheExactRuntimeOwner()
{ {
string session = ReadSource("Net", "LiveSessionRuntimeFactory.cs"); PropertyInfo character = typeof(LiveSessionDomainRuntime).GetProperty(
string router = ReadRuntimeSource( "Character",
"Session", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
"LiveSessionEventRouter.cs"); ?? throw new MissingMemberException(typeof(LiveSessionDomainRuntime).FullName,
string content = ReadSource( "Character");
"Composition", Assert.Equal(typeof(RuntimeCharacterState), character.PropertyType);
"ContentEffectsAudioComposition.cs");
string ui = ReadSource(
"Composition",
"InteractionRetainedUiComposition.cs");
string inventory = ReadRuntimeSource(
"Gameplay",
"RuntimeInventoryState.cs");
string shutdown = ReadSource("Rendering", "GameWindowLifetime.cs");
IReadOnlyList<CompiledCall> 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( Assert.Contains(
"RuntimeCharacterState Character", CompiledCallGraph.Read(install),
session, call => call.Target.DeclaringType == typeof(RuntimeCharacterState)
StringComparison.Ordinal); && call.Target.Name == nameof(RuntimeCharacterState.InstallSpellMetadata));
Assert.Contains(
"character.Character.Spellbook", IReadOnlyList<CompiledCall> ui =
router, CompiledCallGraph.ReadOwned(typeof(RetailInteractionRetainedUiCompositionFactory));
StringComparison.Ordinal); AssertCharacterChildren(ui, "get_Spellbook", "get_LocalPlayer");
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);
Assert.DoesNotContain( Assert.DoesNotContain(
"DesiredComponentState", typeof(RuntimeInventoryState).GetFields(Declared),
inventory, field => field.FieldType.Name == "DesiredComponentState");
StringComparison.Ordinal);
Assert.Contains( IReadOnlyList<string> labels = CompiledCallGraph.ReadStringLiterals(
"new ResourceShutdownStage(\"game runtime root\"", typeof(GameWindowShutdownManifest).GetMethod(
shutdown, nameof(GameWindowShutdownManifest.Create),
StringComparison.Ordinal); BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
?? throw new MissingMethodException());
Assert.Contains("game runtime root", labels);
Assert.DoesNotContain( Assert.DoesNotContain(
"RuntimeCharacterState Character", typeof(IngressShutdownRoots).GetProperties(),
shutdown, property => property.PropertyType == typeof(RuntimeCharacterState));
StringComparison.Ordinal); Assert.DoesNotContain(
typeof(LiveShutdownRoots).GetProperties(),
property => property.PropertyType == typeof(RuntimeCharacterState));
} }
[Fact] [Fact]
public void AppCharacterOptionAndMovementSkillOwnersWereDeleted() public void AppCharacterOptionAndMovementSkillOwnersWereDeleted()
{ {
string gameWindow = ReadSource("Rendering", "GameWindow.cs"); HashSet<string> removed =
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(
"PlayerCharacterOptionsState", "PlayerCharacterOptionsState",
gameWindow + sources + session,
StringComparison.Ordinal);
Assert.DoesNotContain(
"LocalPlayerSkillState", "LocalPlayerSkillState",
gameWindow + playerState + session, ];
StringComparison.Ordinal); Assert.DoesNotContain(
typeof(GameWindow).Assembly.GetTypes(),
type => type.Name is not null && removed.Contains(type.Name));
IReadOnlyList<CompiledCall> router =
CompiledCallGraph.ReadOwned(typeof(LiveSessionEventRouter));
Assert.Contains( Assert.Contains(
"character.Character.Options.Replace",
router, router,
StringComparison.Ordinal); call => call.Target.DeclaringType == typeof(RuntimeCharacterOptionsState)
&& call.Target.Name == nameof(RuntimeCharacterOptionsState.Replace));
Assert.Contains( Assert.Contains(
"character.Character.MovementSkills.Update",
router, router,
StringComparison.Ordinal); call => call.Target.DeclaringType == typeof(RuntimeMovementSkillState)
&& call.Target.Name.StartsWith("Update", StringComparison.Ordinal));
} }
private static string ReadSource(params string[] relative) => private static void AssertGetterRoutesToCharacterChild(
File.ReadAllText(Path.Combine( string property,
[FindRepositoryRoot(), "src", "AcDream.App", .. relative])); string childGetter)
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()
{ {
var current = new DirectoryInfo(AppContext.BaseDirectory); MethodInfo getter = typeof(GameWindow).GetProperty(property, Declared)?.GetMethod
while (current is not null) ?? throw new MissingMemberException(typeof(GameWindow).FullName, property);
IReadOnlyList<CompiledCall> 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<CompiledCall> calls,
params string[] getters)
{
foreach (string getter in getters)
{ {
if (File.Exists(Path.Combine(current.FullName, "AcDream.slnx"))) Assert.True(
return current.FullName; calls.Any(call => call.Target.DeclaringType == typeof(RuntimeCharacterState)
current = current.Parent; && 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.");
} }
} }

View file

@ -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; namespace AcDream.App.Tests.Runtime;
public sealed class RuntimeInventoryOwnershipTests public sealed class RuntimeInventoryOwnershipTests
{ {
private const BindingFlags Declared = BindingFlags.Instance
| BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
| BindingFlags.DeclaredOnly;
[Fact] [Fact]
public void ProductionConstructsOnlyTheRuntimeInventoryOwner() 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( HashSet<string> displacedConstructors =
"private readonly GameRuntime _runtime;", [
source, nameof(RuntimeInventoryState),
StringComparison.Ordinal); nameof(ItemManaState),
Assert.DoesNotContain( "DesiredComponentSnapshotState",
"new RuntimeInventoryState(", "ShortcutSnapshotState",
source, nameof(ExternalContainerState),
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(
"DesiredComponentState", "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] [Fact]
public void UiSessionAndShutdownBorrowTheExactRuntimeOwner() public void UiSessionAndShutdownBorrowTheExactRuntimeOwner()
{ {
string ui = ReadSource( IReadOnlyList<CompiledCall> ui =
"Composition", CompiledCallGraph.ReadOwned(typeof(RetailInteractionRetainedUiCompositionFactory));
"InteractionRetainedUiComposition.cs"); AssertRuntimeCall(ui, typeof(RuntimeActionState), "get_Transactions");
string session = ReadSource("Net", "LiveSessionRuntimeFactory.cs"); Assert.DoesNotContain(
string shutdown = ReadSource("Rendering", "GameWindowLifetime.cs"); ui,
string itemInteraction = ReadSource( call => call.Target.DeclaringType == typeof(InventoryTransactionState)
"UI", && call.Target.IsConstructor);
"ItemInteractionController.cs");
Assert.Contains( FieldInfo transactions = Assert.Single(
"d.Actions.Transactions", typeof(ItemInteractionController).GetFields(Declared),
ui, field => field.Name == "_runtimeTransactions");
StringComparison.Ordinal); Assert.Equal(typeof(RuntimeInteractionTransactionState), transactions.FieldType);
Assert.DoesNotContain( Assert.DoesNotContain(
"new InventoryTransactionState(d.Inventory.Objects)", typeof(ItemInteractionController).GetFields(Declared),
ui, field => field.Name == "_ownsTransactions");
StringComparison.Ordinal);
Assert.DoesNotContain( Assert.DoesNotContain(
"new InventoryTransactionState", CompiledCallGraph.ReadDeclared(typeof(ItemInteractionController)),
itemInteraction, call => call.Target.DeclaringType == typeof(InventoryTransactionState)
StringComparison.Ordinal); && call.Target.IsConstructor);
IReadOnlyList<CompiledCall> 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<string> 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( Assert.DoesNotContain(
"_ownsTransactions", typeof(IngressShutdownRoots).GetProperties(),
itemInteraction, property => property.PropertyType == typeof(RuntimeInventoryState));
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);
Assert.DoesNotContain( Assert.DoesNotContain(
"_domain.Inventory.DesiredComponents", typeof(LiveShutdownRoots).GetProperties(),
session, property => property.PropertyType == typeof(RuntimeInventoryState));
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);
} }
[Fact] [Fact]
public void ToolbarBorrowsRuntimeShortcutManagerWithoutAProviderOrMirror() public void ToolbarBorrowsRuntimeShortcutManagerWithoutAProviderOrMirror()
{ {
string toolbar = ReadSource( FieldInfo store = Assert.Single(
"UI", typeof(ToolbarController).GetFields(Declared),
"Layout", field => field.Name == "_store");
"ToolbarController.cs"); Assert.Equal(typeof(ShortcutStore), store.FieldType);
string ui = ReadSource(
"Composition",
"InteractionRetainedUiComposition.cs");
Assert.Contains(
"private readonly ShortcutStore _store;",
toolbar,
StringComparison.Ordinal);
Assert.Contains(
"_store = shortcuts ?? throw",
toolbar,
StringComparison.Ordinal);
Assert.DoesNotContain( Assert.DoesNotContain(
"new ShortcutStore()", CompiledCallGraph.ReadDeclared(typeof(ToolbarController)),
toolbar, call => call.Target.DeclaringType == typeof(ShortcutStore)
StringComparison.Ordinal); && call.Target.IsConstructor);
Assert.DoesNotContain( Assert.DoesNotContain(
"Func<IReadOnlyList<ShortcutEntry>>", typeof(ToolbarController).GetFields(Declared),
toolbar, field => field.Name == "_storeLoaded"
StringComparison.Ordinal); || IsShortcutProvider(field.FieldType));
Assert.DoesNotContain( Assert.DoesNotContain(
"_storeLoaded", typeof(ToolbarController).GetConstructors(Declared)
toolbar, .SelectMany(constructor => constructor.GetParameters()),
StringComparison.Ordinal); parameter => IsShortcutProvider(parameter.ParameterType));
Assert.Contains( AssertRuntimeCall(
"d.Inventory.Shortcuts,", CompiledCallGraph.ReadOwned(typeof(RetailInteractionRetainedUiCompositionFactory)),
ui, typeof(RuntimeInventoryState),
StringComparison.Ordinal); "get_Shortcuts");
} }
[Fact] [Fact]
public void SpellUiDoesNotApplyASecondLocalCommandMutation() public void SpellUiDoesNotApplyASecondLocalCommandMutation()
{ {
string spellbook = ReadSource( HashSet<string> duplicateMutations =
"UI", [
"Layout", "SetSpellbookFilters",
"SpellbookWindowController.cs"); "SetDesiredComponent",
string spellcasting = ReadSource( "SetFavorite",
"UI", "RemoveFavorite",
"Layout", ];
"SpellcastingUiController.cs");
Assert.DoesNotContain( Assert.DoesNotContain(
"_spellbook.SetSpellbookFilters(filters);", CompiledCallGraph.ReadOwned(typeof(SpellbookWindowController))
spellbook, .Concat(CompiledCallGraph.ReadOwned(typeof(SpellcastingUiController))),
StringComparison.Ordinal); call => duplicateMutations.Contains(call.Target.Name));
Assert.DoesNotContain(
"_spellbook.SetDesiredComponent(componentId, parsed);",
spellbook,
StringComparison.Ordinal);
Assert.DoesNotContain(
"_spellbook.SetFavorite(",
spellcasting,
StringComparison.Ordinal);
Assert.DoesNotContain(
"_spellbook.RemoveFavorite(",
spellcasting,
StringComparison.Ordinal);
} }
private static string ReadSource(params string[] relative) private static bool IsShortcutProvider(Type type) =>
{ type.IsGenericType
string root = FindRepositoryRoot(); && type.GetGenericTypeDefinition() == typeof(Func<>)
return File.ReadAllText(Path.Combine( && type.GenericTypeArguments[0].IsGenericType
[root, "src", "AcDream.App", .. relative])); && type.GenericTypeArguments[0].GetGenericTypeDefinition()
} == typeof(IReadOnlyList<>);
private static string FindRepositoryRoot() private static void AssertRuntimeCall(
{ IReadOnlyList<CompiledCall> calls,
var current = new DirectoryInfo(AppContext.BaseDirectory); Type owner,
while (current is not null) string name) =>
{ Assert.True(
if (File.Exists(Path.Combine(current.FullName, "AcDream.slnx"))) calls.Any(call => call.Target.DeclaringType == owner
return current.FullName; && call.Target.Name == name),
current = current.Parent; $"Missing {owner.Name}.{name}. Runtime calls: "
} + string.Join(", ", calls
throw new DirectoryNotFoundException("AcDream.slnx was not found."); .Where(call => call.Target.DeclaringType?.Namespace?.StartsWith(
} "AcDream.Runtime", StringComparison.Ordinal) == true)
.Select(call => $"{call.Target.DeclaringType?.Name}.{call.Target.Name}")
private static void AssertAppearsInOrder( .Distinct()));
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;
}
}
} }

View file

@ -1,4 +1,11 @@
using System.Reflection;
using System.Text.RegularExpressions; 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; namespace AcDream.App.Tests.Runtime;
@ -57,65 +64,67 @@ public sealed class RuntimeMovementOwnershipTests
[Fact] [Fact]
public void GraphicalInputRuntimeViewsAndShutdownBorrowTheExactOwner() public void GraphicalInputRuntimeViewsAndShutdownBorrowTheExactOwner()
{ {
string root = FindRepositoryRoot(); PropertyInfo movementProperty = typeof(GameWindow).GetProperty(
string gameWindow = ReadAppSource(root, "Rendering", "GameWindow.cs"); "_playerControllerSlot",
string input = ReadAppSource( BindingFlags.Instance | BindingFlags.NonPublic)
root, ?? throw new MissingMemberException(typeof(GameWindow).FullName,
"Input", "_playerControllerSlot");
"DispatcherMovementInputSource.cs"); Assert.Equal(typeof(RuntimeLocalPlayerMovementState),
string adapter = ReadAppSource( movementProperty.PropertyType);
root, Assert.Contains(
"Runtime", CompiledCallGraph.Read(movementProperty.GetMethod!),
"CurrentGameRuntimeAdapter.cs"); call => call.Target.DeclaringType == typeof(GameRuntime)
string commands = ReadAppSource( && call.Target.Name == "get_MovementOwner");
root,
"Runtime",
"CurrentGameRuntimeCommandAdapter.cs");
string lifetime = ReadAppSource(
root,
"Rendering",
"GameWindowLifetime.cs");
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( Assert.Contains(
"RuntimeLocalPlayerMovementState _playerControllerSlot =>", CompiledCallGraph.Read(window),
gameWindow, call => call.Target.DeclaringType == typeof(GameWindow)
StringComparison.Ordinal); && 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( Assert.Contains(
"_runtime.MovementOwner;", CompiledCallGraph.ReadDeclared(typeof(DispatcherMovementInputSource)),
gameWindow, call => call.Target.DeclaringType == typeof(RuntimeLocalPlayerMovementState)
StringComparison.Ordinal); && 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( Assert.Contains(
"new AcDream.App.Input.DispatcherMovementInputSource(", CompiledCallGraph.ReadDeclared(typeof(CurrentGameRuntimeCommandAdapter)),
gameWindow, call => call.Target.DeclaringType == typeof(RuntimeLocalPlayerMovementState)
StringComparison.Ordinal); && 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( Assert.Contains(
"_playerControllerSlot,", "game runtime root",
gameWindow, CompiledCallGraph.ReadStringLiterals(
StringComparison.Ordinal); typeof(GameWindowShutdownManifest).GetMethod(
Assert.Contains( nameof(GameWindowShutdownManifest.Create),
"RuntimeLocalPlayerMovementState movement,", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
input, ?? throw new MissingMethodException()));
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);
} }
private static string ReadAppSource(string root, params string[] relative) =>
File.ReadAllText(Path.Combine(
[root, "src", "AcDream.App", .. relative]));
private static string FindRepositoryRoot() private static string FindRepositoryRoot()
{ {
var current = new DirectoryInfo(AppContext.BaseDirectory); var current = new DirectoryInfo(AppContext.BaseDirectory);