test: replace input and physics source freezes
This commit is contained in:
parent
0ad2ee1cdf
commit
caa5eb8b2b
6 changed files with 313 additions and 238 deletions
|
|
@ -1149,3 +1149,62 @@ Verification:
|
||||||
attributed methods, and reduces direct/total source readers from 63/98 to
|
attributed methods, and reduces direct/total source readers from 63/98 to
|
||||||
57/92. The remaining 92 reconcile to 22 approved retained
|
57/92. The remaining 92 reconcile to 22 approved retained
|
||||||
policies/contracts and 70 staged replacements.
|
policies/contracts and 70 staged replacements.
|
||||||
|
|
||||||
|
## Batch Y input, physics, and inbound-network source-freeze replacement
|
||||||
|
|
||||||
|
Batch Y converts the nine approved source-text readers in the input/physics/
|
||||||
|
network wiring group. Seven remain as compiled-metadata architecture checks;
|
||||||
|
two redundant source assertions are removed. No product source changes.
|
||||||
|
|
||||||
|
The exact decisions are:
|
||||||
|
|
||||||
|
- the ForcePosition generic-tail suite now inspects the built
|
||||||
|
`TryApplyGenericRemoteRenderPose` body for its one `WorldEntity.SetPosition`
|
||||||
|
edge and proves the committed/deferred local-position observer call is
|
||||||
|
immediately followed by a compiled return. The negative assertion that the
|
||||||
|
already-deleted `LocalForcePositionTransaction` class name was absent is
|
||||||
|
removed: the Runtime-owned transaction and its displaced-authority behavior
|
||||||
|
are already exercised by
|
||||||
|
`RuntimeAcceptedPositionDriveControllerTests`, while an absent identifier is
|
||||||
|
not behavior;
|
||||||
|
- the contact-versus-walkability source assertion is removed. The dedicated
|
||||||
|
`SteepContactBody_InterpolatesInsteadOfSnapping` and
|
||||||
|
`FreeFlightBodyWithNoContact_StillSnaps` behavioral tests exercise the one
|
||||||
|
discriminating state and the opposite boundary through the real routing
|
||||||
|
method, so retaining a string check would add brittleness rather than
|
||||||
|
coverage. The AP-140 retail rationale remains beside those behavioral tests;
|
||||||
|
- local inbound SetState routing now verifies the compiled controller graph has
|
||||||
|
exactly one `PlayerMovementController.ApplyServerPhysicsState` edge and no
|
||||||
|
direct `ApplyPhysicsState` edge;
|
||||||
|
- the #270 movement-stats contract reflects the single
|
||||||
|
`StaminaExhaustionEdgeTracker` owner and follows the compiled Observe,
|
||||||
|
ReportExhaustion, Reset, and two factory Apply edges. Existing tracker and
|
||||||
|
Runtime movement-state behavior suites remain the behavioral oracle;
|
||||||
|
- remote spawn settling now identifies both compiled call sites
|
||||||
|
(`DispatchRemoteInboundMotion` and `OnPosition`), verifies the repeated
|
||||||
|
inbound route checks `PhysicsBody.InContact` before reseeding, and verifies
|
||||||
|
the shared seed helper calls `SpawnPlacementSettler.TrySettle`;
|
||||||
|
- production auto-entry readiness now follows compiled calls to the published
|
||||||
|
controller, live-record lookup, and physics-host view and verifies the exact
|
||||||
|
`EntityPhysicsHost` type operand; and
|
||||||
|
- player presentation attachment now verifies compiled call order from the
|
||||||
|
animation sink assignment through matched animation drain to the unmatched
|
||||||
|
interpreter suffix drain.
|
||||||
|
|
||||||
|
`CompiledCallGraph` gained reusable instruction and type-reference readers.
|
||||||
|
They allow exact return and `isinst`/cast boundaries to be checked without
|
||||||
|
making comments, whitespace, local names, or source paths part of the oracle.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
|
||||||
|
- the directly affected and adjacent behavioral App suites pass 31/31, and
|
||||||
|
the two Runtime owner suites pass 52/52;
|
||||||
|
- the complete locked Release build covers all 44 projects with zero warnings
|
||||||
|
and zero errors;
|
||||||
|
- the no-retry complete hermetic Release gate passes 14,347/14,347 with zero
|
||||||
|
skips or failures across all 12 test assemblies. The exact two-case
|
||||||
|
reduction is the two redundant source assertions above; and
|
||||||
|
- the regenerated 1,254-file inventory parses every file, reports 11,415
|
||||||
|
attributed methods, and reduces total direct/helper source readers from 92
|
||||||
|
to 83 while direct readers remain 57. The remaining 83 reconcile to the 22
|
||||||
|
approved retained policies/contracts and 61 staged replacements.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using System.Reflection.Emit;
|
||||||
namespace AcDream.App.Tests.Architecture;
|
namespace AcDream.App.Tests.Architecture;
|
||||||
|
|
||||||
internal readonly record struct CompiledCall(int Offset, MethodBase Target);
|
internal readonly record struct CompiledCall(int Offset, MethodBase Target);
|
||||||
|
internal readonly record struct CompiledInstruction(int Offset, OpCode OpCode);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads compiled call/new-object edges from a method body. Architecture tests
|
/// Reads compiled call/new-object edges from a method body. Architecture tests
|
||||||
|
|
@ -70,6 +71,70 @@ internal static class CompiledCallGraph
|
||||||
return literals;
|
return literals;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the compiled instruction sequence without exposing operands. This
|
||||||
|
/// is sufficient for architecture tests that need to distinguish an
|
||||||
|
/// immediate return from fall-through without freezing source formatting.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<CompiledInstruction> ReadInstructions(
|
||||||
|
MethodBase method)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(method);
|
||||||
|
byte[] il = method.GetMethodBody()?.GetILAsByteArray()
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"{method.DeclaringType?.FullName}.{method.Name} has no compiled body.");
|
||||||
|
var instructions = new List<CompiledInstruction>();
|
||||||
|
|
||||||
|
for (int cursor = 0; cursor < il.Length;)
|
||||||
|
{
|
||||||
|
int instructionOffset = cursor;
|
||||||
|
OpCode opCode = ReadOpCode(il, ref cursor);
|
||||||
|
instructions.Add(new CompiledInstruction(instructionOffset, opCode));
|
||||||
|
cursor += OperandSize(opCode.OperandType, il, cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instructions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads types named by compiled type operands such as casts, boxing, and
|
||||||
|
/// <c>isinst</c>. This lets tests retain an exact type boundary without
|
||||||
|
/// depending on the source expression used to spell it.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<Type> ReadTypeReferences(MethodBase method)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(method);
|
||||||
|
byte[] il = method.GetMethodBody()?.GetILAsByteArray()
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"{method.DeclaringType?.FullName}.{method.Name} has no compiled body.");
|
||||||
|
Type[]? declaringArguments = method.DeclaringType?.IsGenericType == true
|
||||||
|
? method.DeclaringType.GetGenericArguments()
|
||||||
|
: null;
|
||||||
|
Type[]? methodArguments = method.IsGenericMethod
|
||||||
|
? method.GetGenericArguments()
|
||||||
|
: null;
|
||||||
|
var types = new List<Type>();
|
||||||
|
|
||||||
|
for (int cursor = 0; cursor < il.Length;)
|
||||||
|
{
|
||||||
|
OpCode opCode = ReadOpCode(il, ref cursor);
|
||||||
|
if (opCode.OperandType == OperandType.InlineType)
|
||||||
|
{
|
||||||
|
int token = BitConverter.ToInt32(il, cursor);
|
||||||
|
Type? type = method.Module.ResolveType(
|
||||||
|
token,
|
||||||
|
declaringArguments,
|
||||||
|
methodArguments);
|
||||||
|
if (type is not null)
|
||||||
|
types.Add(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor += OperandSize(opCode.OperandType, il, cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
public static int IndexOf(
|
public static int IndexOf(
|
||||||
IReadOnlyList<CompiledCall> calls,
|
IReadOnlyList<CompiledCall> calls,
|
||||||
Type declaringType,
|
Type declaringType,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using AcDream.App.Input;
|
||||||
|
using AcDream.App.Tests.Architecture;
|
||||||
|
using AcDream.App.World;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Core.Physics.Motion;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Input;
|
namespace AcDream.App.Tests.Input;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// C3c-F2 (2026-08-02): source pin for the production player-mode auto-entry
|
/// C3c-F2 (2026-08-02): compiled architecture check for the production
|
||||||
/// precondition. <c>PlayerModeAutoEntry</c> is a ONE-SHOT that disarms before
|
/// player-mode auto-entry precondition. <c>PlayerModeAutoEntry</c> is a
|
||||||
/// invoking its callback, and the production callback completes the world
|
/// ONE-SHOT that disarms before invoking its callback, and the production
|
||||||
/// reveal, so an attempt made before the Runtime first-entry conductor has
|
/// callback completes the world reveal, so an attempt made before the Runtime
|
||||||
|
/// first-entry conductor has
|
||||||
/// committed permanently seals the reveal with the player never in world —
|
/// committed permanently seals the reveal with the player never in world —
|
||||||
/// the second link of the connected-gate login wedge
|
/// the second link of the connected-gate login wedge
|
||||||
/// (logs/connected-world-gate-20260802-130455: one "not committed yet" line,
|
/// (logs/connected-world-gate-20260802-130455: one "not committed yet" line,
|
||||||
|
|
@ -14,45 +22,61 @@ namespace AcDream.App.Tests.Input;
|
||||||
/// The guard's own one-shot/latch behavior is covered behaviorally by
|
/// The guard's own one-shot/latch behavior is covered behaviorally by
|
||||||
/// AcDream.Core.Tests.Input.AutoEnterPlayerModeTests; only the production
|
/// AcDream.Core.Tests.Input.AutoEnterPlayerModeTests; only the production
|
||||||
/// context's dependency graph (a ~15-dependency PlayerModeController) has no
|
/// context's dependency graph (a ~15-dependency PlayerModeController) has no
|
||||||
/// focused harness, hence the source pin.
|
/// focused harness, hence the metadata check.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class C3cF2AutoEntryWiringTests
|
public sealed class C3cF2AutoEntryWiringTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ProductionAutoEntryRequiresTheRuntimePublishedController()
|
public void ProductionAutoEntryRequiresTheRuntimePublishedController()
|
||||||
{
|
{
|
||||||
string source = ReadSource("Input", "PlayerModeAutoEntry.cs");
|
MethodInfo readiness = typeof(LivePlayerModeAutoEntryContext)
|
||||||
|
.GetProperty(
|
||||||
|
"IsPlayerControllerReady",
|
||||||
|
BindingFlags.Instance | BindingFlags.Public)!
|
||||||
|
.GetMethod!;
|
||||||
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(readiness);
|
||||||
|
|
||||||
Assert.DoesNotContain(
|
|
||||||
"public bool IsPlayerControllerReady => true;",
|
|
||||||
source,
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"IsRuntimePublished: true",
|
calls,
|
||||||
source,
|
call => call.Target.DeclaringType == typeof(PlayerMovementController)
|
||||||
StringComparison.Ordinal);
|
&& call.Target.Name == "get_IsRuntimePublished");
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"record.PhysicsHost is EntityPhysicsHost",
|
calls,
|
||||||
source,
|
call => call.Target.DeclaringType == typeof(LiveEntityRuntime)
|
||||||
StringComparison.Ordinal);
|
&& call.Target.Name == nameof(LiveEntityRuntime.TryGetRecord));
|
||||||
|
Assert.Contains(
|
||||||
|
calls,
|
||||||
|
call => call.Target.DeclaringType == typeof(LiveEntityRecord)
|
||||||
|
&& call.Target.Name == "get_PhysicsHost");
|
||||||
|
Assert.Contains(
|
||||||
|
typeof(EntityPhysicsHost),
|
||||||
|
CompiledCallGraph.ReadTypeReferences(readiness));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PlayerPresentationAttach_DrainsMatchedAnimationsBeforeStartupMotionSuffix()
|
public void PlayerPresentationAttach_DrainsMatchedAnimationsBeforeStartupMotionSuffix()
|
||||||
{
|
{
|
||||||
string source = ReadSource("Input", "PlayerModeController.cs");
|
MethodInfo enter = typeof(PlayerModeController).GetMethod(
|
||||||
|
"BuildControllerAndCamera",
|
||||||
int attach = source.IndexOf(
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
||||||
"controller.Motion.DefaultSink =",
|
?? throw new MissingMethodException(
|
||||||
StringComparison.Ordinal);
|
typeof(PlayerModeController).FullName,
|
||||||
int animationDrain = source.IndexOf(
|
"BuildControllerAndCamera");
|
||||||
"sequencer.Manager.HandleEnterWorld();",
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(enter);
|
||||||
attach,
|
int attach = CompiledCallGraph.IndexOf(
|
||||||
StringComparison.Ordinal);
|
calls,
|
||||||
int unmatchedMotionDrain = source.IndexOf(
|
typeof(MotionInterpreter),
|
||||||
"controller.Motion.HandleExitWorld();",
|
"set_DefaultSink");
|
||||||
animationDrain,
|
int animationDrain = CompiledCallGraph.IndexOf(
|
||||||
StringComparison.Ordinal);
|
calls,
|
||||||
|
typeof(MotionTableManager),
|
||||||
|
nameof(MotionTableManager.HandleEnterWorld),
|
||||||
|
attach + 1);
|
||||||
|
int unmatchedMotionDrain = CompiledCallGraph.IndexOf(
|
||||||
|
calls,
|
||||||
|
typeof(MotionInterpreter),
|
||||||
|
nameof(MotionInterpreter.HandleExitWorld),
|
||||||
|
animationDrain + 1);
|
||||||
|
|
||||||
Assert.True(attach >= 0, "The player animation sink was not attached.");
|
Assert.True(attach >= 0, "The player animation sink was not attached.");
|
||||||
Assert.True(
|
Assert.True(
|
||||||
|
|
@ -62,24 +86,4 @@ public sealed class C3cF2AutoEntryWiringTests
|
||||||
unmatchedMotionDrain > animationDrain,
|
unmatchedMotionDrain > animationDrain,
|
||||||
"Only the unmatched pre-attach interpreter suffix may drain last.");
|
"Only the unmatched pre-attach interpreter suffix may drain last.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ReadSource(params string[] relativePath)
|
|
||||||
{
|
|
||||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
|
||||||
while (directory is not null)
|
|
||||||
{
|
|
||||||
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
|
||||||
{
|
|
||||||
return File.ReadAllText(Path.Combine(
|
|
||||||
directory.FullName,
|
|
||||||
"src",
|
|
||||||
"AcDream.App",
|
|
||||||
Path.Combine(relativePath)));
|
|
||||||
}
|
|
||||||
|
|
||||||
directory = directory.Parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
using System.Text.RegularExpressions;
|
using AcDream.App.Tests.Architecture;
|
||||||
|
using AcDream.App.Physics;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Physics;
|
namespace AcDream.App.Tests.Physics;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// C3c-F1 (2026-08-02): source pins for the App-side halves of the
|
/// C3c-F1 (2026-08-02): compiled architecture checks for the App-side halves
|
||||||
/// movement-owner application seams whose production graphs have no
|
/// of the movement-owner application seams whose production graphs have no
|
||||||
/// focused harness (the inbound network-update controller's dependency
|
/// focused harness (the inbound network-update controller's dependency
|
||||||
/// set is composition-only). The Runtime lifecycle matrices carry the
|
/// set is composition-only). The Runtime lifecycle matrices carry the
|
||||||
/// behavioral coverage; these pins keep the App wiring routed through the
|
/// behavioral coverage; these checks keep the App wiring routed through the
|
||||||
/// owner's typed entries instead of the throwing direct mutations that
|
/// owner's typed entries instead of the throwing direct mutations that
|
||||||
/// crashed the connected lifecycle gate twice
|
/// crashed the connected lifecycle gate twice
|
||||||
/// (logs/connected-world-gate-20260802-122749 — SetCharacterSkills;
|
/// (logs/connected-world-gate-20260802-122749 — SetCharacterSkills;
|
||||||
|
|
@ -18,36 +20,17 @@ public sealed class C3cF1ProductionWiringTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LocalPlayerInboundSetState_RoutesThroughTheTypedOwnerEntry()
|
public void LocalPlayerInboundSetState_RoutesThroughTheTypedOwnerEntry()
|
||||||
{
|
{
|
||||||
string source = ReadSource(
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.ReadDeclared(
|
||||||
"Physics",
|
typeof(LiveEntityNetworkUpdateController));
|
||||||
"LiveEntityNetworkUpdateController.cs");
|
|
||||||
|
|
||||||
Assert.Single(
|
Assert.Single(
|
||||||
Regex.Matches(source, @"ApplyServerPhysicsState\(")
|
calls,
|
||||||
.Cast<Match>());
|
call =>
|
||||||
|
call.Target.DeclaringType == typeof(PlayerMovementController)
|
||||||
|
&& call.Target.Name == "ApplyServerPhysicsState");
|
||||||
Assert.DoesNotContain(
|
Assert.DoesNotContain(
|
||||||
".ApplyPhysicsState(",
|
calls,
|
||||||
source,
|
call => call.Target.DeclaringType == typeof(PlayerMovementController)
|
||||||
StringComparison.Ordinal);
|
&& call.Target.Name == nameof(PlayerMovementController.ApplyPhysicsState));
|
||||||
}
|
|
||||||
|
|
||||||
private static string ReadSource(params string[] relativePath)
|
|
||||||
{
|
|
||||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
|
||||||
while (directory is not null)
|
|
||||||
{
|
|
||||||
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
|
||||||
{
|
|
||||||
return File.ReadAllText(Path.Combine(
|
|
||||||
directory.FullName,
|
|
||||||
"src",
|
|
||||||
"AcDream.App",
|
|
||||||
Path.Combine(relativePath)));
|
|
||||||
}
|
|
||||||
|
|
||||||
directory = directory.Parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
using System.Text.RegularExpressions;
|
using System.Reflection;
|
||||||
|
using AcDream.App.Net;
|
||||||
|
using AcDream.App.Physics;
|
||||||
|
using AcDream.App.Tests.Architecture;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Physics;
|
namespace AcDream.App.Tests.Physics;
|
||||||
|
|
||||||
|
|
@ -13,77 +18,98 @@ public sealed class Issue270ProductionWiringTests
|
||||||
// the factory's deleted ApplyMovementStats body into
|
// the factory's deleted ApplyMovementStats body into
|
||||||
// LiveMovementStatsApplier, which routes through the Runtime
|
// LiveMovementStatsApplier, which routes through the Runtime
|
||||||
// movement owner's typed seam instead of touching the controller.
|
// movement owner's typed seam instead of touching the controller.
|
||||||
string applier = ReadSource("Net", "LiveMovementStatsApplier.cs");
|
_ = Assert.Single(
|
||||||
string factory = ReadSource("Net", "LiveSessionRuntimeFactory.cs");
|
typeof(LiveMovementStatsApplier).GetFields(
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic),
|
||||||
|
field => field.FieldType == typeof(StaminaExhaustionEdgeTracker));
|
||||||
|
|
||||||
Assert.Contains(
|
IReadOnlyList<CompiledCall> applierCalls =
|
||||||
"_staminaExhaustion.Observe(snapshot.CurrentStamina)",
|
CompiledCallGraph.ReadDeclared(typeof(LiveMovementStatsApplier));
|
||||||
applier,
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
Assert.Single(
|
Assert.Single(
|
||||||
Regex.Matches(
|
applierCalls,
|
||||||
applier,
|
call =>
|
||||||
@"_movement\.ReportExhaustion\(\);")
|
call.Target.DeclaringType == typeof(StaminaExhaustionEdgeTracker)
|
||||||
.Cast<Match>());
|
&& call.Target.Name == nameof(StaminaExhaustionEdgeTracker.Observe));
|
||||||
Assert.Contains(
|
Assert.Single(
|
||||||
"_staminaExhaustion.Reset();",
|
applierCalls,
|
||||||
applier,
|
call =>
|
||||||
StringComparison.Ordinal);
|
call.Target.DeclaringType
|
||||||
Assert.Contains(
|
== typeof(RuntimeLocalPlayerMovementState)
|
||||||
"_movementStats.Reset();",
|
&& call.Target.Name == nameof(
|
||||||
factory,
|
RuntimeLocalPlayerMovementState.ReportExhaustion));
|
||||||
StringComparison.Ordinal);
|
Assert.Single(
|
||||||
|
applierCalls,
|
||||||
|
call =>
|
||||||
|
call.Target.DeclaringType == typeof(StaminaExhaustionEdgeTracker)
|
||||||
|
&& call.Target.Name == nameof(StaminaExhaustionEdgeTracker.Reset));
|
||||||
|
|
||||||
|
IReadOnlyList<CompiledCall> factoryCalls =
|
||||||
|
CompiledCallGraph.ReadDeclared(typeof(LiveSessionRuntimeFactory));
|
||||||
|
Assert.Single(
|
||||||
|
factoryCalls,
|
||||||
|
call =>
|
||||||
|
call.Target.DeclaringType == typeof(LiveMovementStatsApplier)
|
||||||
|
&& call.Target.Name == nameof(LiveMovementStatsApplier.Reset));
|
||||||
// The factory keeps zero direct controller mutations: both stat
|
// The factory keeps zero direct controller mutations: both stat
|
||||||
// callbacks route through the applier's seam.
|
// callbacks route through the applier's seam.
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
2,
|
2,
|
||||||
Regex.Matches(factory, @"_movementStats\.Apply\(").Count);
|
factoryCalls.Count(call =>
|
||||||
|
call.Target.DeclaringType == typeof(LiveMovementStatsApplier)
|
||||||
|
&& call.Target.Name == nameof(LiveMovementStatsApplier.Apply)));
|
||||||
Assert.DoesNotContain(
|
Assert.DoesNotContain(
|
||||||
"Motion.ReportExhaustion",
|
factoryCalls,
|
||||||
factory,
|
call => call.Target.DeclaringType == typeof(MotionInterpreter)
|
||||||
StringComparison.Ordinal);
|
&& call.Target.Name == nameof(MotionInterpreter.ReportExhaustion));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RemoteSpawnSettle_IsRetriedAndCoversBothCreationRoutes()
|
public void RemoteSpawnSettle_IsRetriedAndCoversBothCreationRoutes()
|
||||||
{
|
{
|
||||||
string source = ReadSource(
|
const BindingFlags flags = BindingFlags.Instance
|
||||||
"Physics",
|
| BindingFlags.Static
|
||||||
"LiveEntityNetworkUpdateController.cs");
|
| BindingFlags.Public
|
||||||
|
| BindingFlags.NonPublic
|
||||||
|
| BindingFlags.DeclaredOnly;
|
||||||
|
MethodInfo[] callers = typeof(LiveEntityNetworkUpdateController)
|
||||||
|
.GetMethods(flags)
|
||||||
|
.Where(method => method.GetMethodBody() is not null)
|
||||||
|
.Where(method => CompiledCallGraph.Read(method).Any(call =>
|
||||||
|
call.Target.DeclaringType
|
||||||
|
== typeof(LiveEntityNetworkUpdateController)
|
||||||
|
&& call.Target.Name == "SeedRemoteSpawnPlacement"))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
Assert.Contains(
|
Assert.Equal(2, callers.Length);
|
||||||
"if (!remote.Body.InContact)",
|
|
||||||
source,
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
3,
|
["DispatchRemoteInboundMotion", "OnPosition"],
|
||||||
Regex.Matches(source, @"SeedRemoteSpawnPlacement\(").Count);
|
callers.Select(method => method.Name).Order().ToArray());
|
||||||
|
MethodInfo retryingInboundRoute = Assert.Single(
|
||||||
|
callers,
|
||||||
|
method => method.Name == "DispatchRemoteInboundMotion");
|
||||||
|
IReadOnlyList<CompiledCall> retryCalls =
|
||||||
|
CompiledCallGraph.Read(retryingInboundRoute);
|
||||||
|
int contact = CompiledCallGraph.IndexOf(
|
||||||
|
retryCalls,
|
||||||
|
typeof(PhysicsBody),
|
||||||
|
"get_InContact");
|
||||||
|
int settle = CompiledCallGraph.IndexOf(
|
||||||
|
retryCalls,
|
||||||
|
typeof(LiveEntityNetworkUpdateController),
|
||||||
|
"SeedRemoteSpawnPlacement");
|
||||||
|
Assert.True(contact >= 0 && settle > contact);
|
||||||
// C3c-F5: the settle helper moved to Core (SpawnPlacementSettler) so
|
// C3c-F5: the settle helper moved to Core (SpawnPlacementSettler) so
|
||||||
// the local player's Runtime first-entry activation shares the same
|
// the local player's Runtime first-entry activation shares the same
|
||||||
// tested compressed-first-gravity-frame sweep.
|
// tested compressed-first-gravity-frame sweep.
|
||||||
|
MethodInfo seed = typeof(LiveEntityNetworkUpdateController).GetMethod(
|
||||||
|
"SeedRemoteSpawnPlacement",
|
||||||
|
flags)
|
||||||
|
?? throw new MissingMethodException(
|
||||||
|
typeof(LiveEntityNetworkUpdateController).FullName,
|
||||||
|
"SeedRemoteSpawnPlacement");
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"SpawnPlacementSettler.TrySettle(",
|
CompiledCallGraph.Read(seed),
|
||||||
source,
|
call => call.Target.DeclaringType == typeof(SpawnPlacementSettler)
|
||||||
StringComparison.Ordinal);
|
&& call.Target.Name == nameof(SpawnPlacementSettler.TrySettle));
|
||||||
}
|
|
||||||
|
|
||||||
private static string ReadSource(params string[] relativePath)
|
|
||||||
{
|
|
||||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
|
||||||
while (directory is not null)
|
|
||||||
{
|
|
||||||
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
|
||||||
{
|
|
||||||
return File.ReadAllText(Path.Combine(
|
|
||||||
directory.FullName,
|
|
||||||
"src",
|
|
||||||
"AcDream.App",
|
|
||||||
Path.Combine(relativePath)));
|
|
||||||
}
|
|
||||||
|
|
||||||
directory = directory.Parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
using System.Text.RegularExpressions;
|
using System.Reflection;
|
||||||
|
using System.Reflection.Emit;
|
||||||
|
using AcDream.App.Tests.Architecture;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.Session;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Physics;
|
namespace AcDream.App.Tests.Physics;
|
||||||
|
|
||||||
|
|
@ -57,14 +61,14 @@ public sealed class LiveEntityNetworkBranchRoutingTests
|
||||||
// there: tests/AcDream.Runtime.Tests/Session/RuntimeAcceptedPositionDriveControllerTests.cs.
|
// there: tests/AcDream.Runtime.Tests/Session/RuntimeAcceptedPositionDriveControllerTests.cs.
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// R8 review fix (2026-08-03): source pins for the generic-tail
|
/// R8 review fix (2026-08-03): assembly checks for the generic-tail
|
||||||
/// double-write guard in <c>LiveEntityNetworkUpdateController.OnPosition</c>.
|
/// double-write guard in <c>LiveEntityNetworkUpdateController.OnPosition</c>.
|
||||||
/// A full behavioral fixture is impractical here for the SAME reason
|
/// A full behavioral fixture is impractical here for the SAME reason
|
||||||
/// <c>C3cF1ProductionWiringTests</c> gives — the controller's dependency
|
/// <c>C3cF1ProductionWiringTests</c> gives — the controller's dependency
|
||||||
/// set is composition-only (67+ collaborators wired only by
|
/// set is composition-only (67+ collaborators wired only by
|
||||||
/// <c>SessionPlayerComposition</c>) — so this follows that file's exact
|
/// <c>SessionPlayerComposition</c>) — so this follows that file's exact
|
||||||
/// established pattern: assert the STRUCTURE of the production source
|
/// established pattern: assert the compiled call/return structure rather
|
||||||
/// rather than construct the class. The Runtime-level behavioral
|
/// than construct the class. The Runtime-level behavioral
|
||||||
/// coverage for the seam itself lives in
|
/// coverage for the seam itself lives in
|
||||||
/// RuntimeAcceptedPositionDriveControllerTests; these pins are what stop
|
/// RuntimeAcceptedPositionDriveControllerTests; these pins are what stop
|
||||||
/// this App-layer call site from silently reintroducing the retired
|
/// this App-layer call site from silently reintroducing the retired
|
||||||
|
|
@ -74,122 +78,56 @@ public sealed class LiveEntityNetworkBranchRoutingTests
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class LiveEntityNetworkUpdateControllerForcePositionWiringTests
|
public sealed class LiveEntityNetworkUpdateControllerForcePositionWiringTests
|
||||||
{
|
{
|
||||||
[Fact]
|
|
||||||
public void LocalForcePositionTransactionIsNeverCalledFromThisFile()
|
|
||||||
{
|
|
||||||
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
|
|
||||||
|
|
||||||
// The name may still appear in a comment explaining what
|
|
||||||
// replaced it (contract §"the deleted LocalForcePositionTransaction");
|
|
||||||
// what must be gone is any actual call into it.
|
|
||||||
Assert.DoesNotContain(
|
|
||||||
"LocalForcePositionTransaction.Apply(",
|
|
||||||
source,
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GenericTailWriteIsNeverDuplicatedForTheLocalForcePositionPath()
|
public void GenericTailWriteIsNeverDuplicatedForTheLocalForcePositionPath()
|
||||||
{
|
{
|
||||||
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
|
|
||||||
|
|
||||||
// The generic render-tail's WorldEntity write is the ONE
|
// The generic render-tail's WorldEntity write is the ONE
|
||||||
// remaining writer of an accepted Position — it must serve
|
// remaining writer of an accepted Position — it must serve
|
||||||
// remotes only, never a second local-player write alongside the
|
// remotes only, never a second local-player write alongside the
|
||||||
// Runtime-committed one.
|
// Runtime-committed one.
|
||||||
|
MethodInfo genericTail = typeof(LiveEntityNetworkUpdateController)
|
||||||
|
.GetMethod(
|
||||||
|
"TryApplyGenericRemoteRenderPose",
|
||||||
|
BindingFlags.Static | BindingFlags.NonPublic)
|
||||||
|
?? throw new MissingMethodException(
|
||||||
|
typeof(LiveEntityNetworkUpdateController).FullName,
|
||||||
|
"TryApplyGenericRemoteRenderPose");
|
||||||
Assert.Single(
|
Assert.Single(
|
||||||
Regex.Matches(source, @"entity\.SetPosition\(worldPos\);")
|
CompiledCallGraph.Read(genericTail),
|
||||||
.Cast<Match>());
|
call => call.Target.DeclaringType == typeof(WorldEntity)
|
||||||
|
&& call.Target.Name == nameof(WorldEntity.SetPosition));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CommittedOrDeferredCellReturnsBeforeReachingTheGenericTail()
|
public void CommittedOrDeferredCellReturnsBeforeReachingTheGenericTail()
|
||||||
{
|
{
|
||||||
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
|
|
||||||
|
|
||||||
// The Committed/DeferredCell branch must still return
|
// The Committed/DeferredCell branch must still return
|
||||||
// immediately after its two preserved side effects — a missing
|
// immediately after its two preserved side effects — a missing
|
||||||
// `return` here would fall through into the generic tail below
|
// `return` here would fall through into the generic tail below
|
||||||
// and resurrect the double-write.
|
// and resurrect the double-write.
|
||||||
Assert.Matches(
|
MethodInfo onPosition = typeof(LiveEntityNetworkUpdateController)
|
||||||
new Regex(
|
.GetMethod(
|
||||||
@"ObserveAcceptedLocalPosition\(\s*"
|
"OnPosition",
|
||||||
+ @"update\.Position\.LandblockId\);\s*return;",
|
BindingFlags.Instance | BindingFlags.Public)
|
||||||
RegexOptions.Singleline),
|
?? throw new MissingMethodException(
|
||||||
source);
|
typeof(LiveEntityNetworkUpdateController).FullName,
|
||||||
}
|
"OnPosition");
|
||||||
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(onPosition);
|
||||||
|
CompiledCall drive = Assert.Single(calls, call =>
|
||||||
|
call.Target.DeclaringType
|
||||||
|
== typeof(RuntimeAcceptedPositionDriveController)
|
||||||
|
&& call.Target.Name == "TryExecuteAcceptedLocalPosition");
|
||||||
|
CompiledCall observe = calls.First(call =>
|
||||||
|
call.Offset > drive.Offset
|
||||||
|
&& call.Target.Name == "ObserveAcceptedLocalPosition");
|
||||||
|
IReadOnlyList<CompiledInstruction> instructions =
|
||||||
|
CompiledCallGraph.ReadInstructions(onPosition);
|
||||||
|
int observeInstruction = instructions
|
||||||
|
.Select((instruction, index) => (instruction, index))
|
||||||
|
.Single(pair => pair.instruction.Offset == observe.Offset)
|
||||||
|
.index;
|
||||||
|
|
||||||
/// <summary>
|
Assert.Equal(OpCodes.Ret, instructions[observeInstruction + 1].OpCode);
|
||||||
/// AP-140 (retired 2026-08-04): the accepted-Position routing gate
|
|
||||||
/// for the free-flight/landing decision must select the hard snap on
|
|
||||||
/// retail's CONTACT predicate
|
|
||||||
/// (<c>InterpolationManager::adjust_offset</c> @0x00555D30 gates its
|
|
||||||
/// whole body on <c>transient_state & 1</c> @0x00555D52, and bit 0
|
|
||||||
/// is <c>CONTACT_TS</c>), not on the client <c>Airborne</c> flag,
|
|
||||||
/// which is <c>!Body.OnWalkable</c> — WALKABILITY, a strictly wider
|
|
||||||
/// set that also captures a remote sliding on a steep face.
|
|
||||||
///
|
|
||||||
/// <para>
|
|
||||||
/// C4 route 4b-3's OnPosition collapse (2026-08-04) dissolved
|
|
||||||
/// <c>OnPosition</c>'s former standalone player-remote LANDING
|
|
||||||
/// TRANSITION block — which used to carry its own
|
|
||||||
/// <c>if (!rmState.Body.InContact)</c> copy of this gate — into
|
|
||||||
/// <c>ApplyRemoteContactRouting</c>'s free-flight carve-out, now the
|
|
||||||
/// ONE site (for every guid) that decides this. The gate itself did
|
|
||||||
/// not move in spirit, only in address: this pin follows it there.
|
|
||||||
/// </para>
|
|
||||||
///
|
|
||||||
/// <para>
|
|
||||||
/// A source pin rather than a behavioural fixture for the reason this
|
|
||||||
/// class already documents: the controller's dependency set is
|
|
||||||
/// composition-only. The gate inside <c>ApplyRemoteContactRouting</c>
|
|
||||||
/// — a static method, so reachable — IS covered behaviourally, in
|
|
||||||
/// <c>LiveEntityNetworkRemoteSteadyStateIntegrationTests</c>. Restore
|
|
||||||
/// <c>if (rmState.Airborne)</c> or <c>if (remote.Airborne)</c> here
|
|
||||||
/// and this test fails.
|
|
||||||
/// </para>
|
|
||||||
/// </summary>
|
|
||||||
[Fact]
|
|
||||||
public void PlayerRemoteLandingSnapSelectsOnContactNotOnWalkability()
|
|
||||||
{
|
|
||||||
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
|
|
||||||
|
|
||||||
Assert.Contains(
|
|
||||||
"if (!remote.Body.InContact)",
|
|
||||||
source,
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
// `rmState.Airborne` survives as a WRITE target and in prose (the
|
|
||||||
// block's own comment explains why it is deliberately not cleared
|
|
||||||
// there); what must never come back is reading it as the gate.
|
|
||||||
Assert.DoesNotContain(
|
|
||||||
"if (rmState.Airborne)",
|
|
||||||
source,
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
Assert.DoesNotContain(
|
|
||||||
"if (remote.Airborne)",
|
|
||||||
source,
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ReadSource(string fileName)
|
|
||||||
{
|
|
||||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
|
||||||
while (directory is not null)
|
|
||||||
{
|
|
||||||
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
|
||||||
{
|
|
||||||
return File.ReadAllText(Path.Combine(
|
|
||||||
directory.FullName,
|
|
||||||
"src",
|
|
||||||
"AcDream.App",
|
|
||||||
"Physics",
|
|
||||||
fileName));
|
|
||||||
}
|
|
||||||
|
|
||||||
directory = directory.Parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue