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
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.

View file

@ -56,6 +56,57 @@ internal static class CompiledCallGraph
.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)
{
ArgumentNullException.ThrowIfNull(method);

View file

@ -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<CompiledCall> 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<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(
"_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<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.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<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) =>

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;
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<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(
"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<CompiledCall> 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<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.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<string> 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<CompiledCall> 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<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")))
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.");
}
}

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;
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<string> 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<CompiledCall> 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<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(
"_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<IReadOnlyList<ShortcutEntry>>",
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<string> 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<CompiledCall> 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()));
}

View file

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