89 lines
3.8 KiB
C#
89 lines
3.8 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// C3c-F2 (2026-08-02): compiled architecture check for the production
|
|
/// player-mode auto-entry precondition. <c>PlayerModeAutoEntry</c> is a
|
|
/// ONE-SHOT that disarms before invoking its callback, and the production
|
|
/// 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 —
|
|
/// the second link of the connected-gate login wedge
|
|
/// (logs/connected-world-gate-20260802-130455: one "not committed yet" line,
|
|
/// then event=complete with materialized=False and 5,577
|
|
/// readiness-after-terminal rejections). The production context therefore has
|
|
/// to report the same commit <c>PlayerModeController.TryEnter</c> requires.
|
|
/// The guard's own one-shot/latch behavior is covered behaviorally by
|
|
/// AcDream.Core.Tests.Input.AutoEnterPlayerModeTests; only the production
|
|
/// context's dependency graph (a ~15-dependency PlayerModeController) has no
|
|
/// focused harness, hence the metadata check.
|
|
/// </summary>
|
|
public sealed class C3cF2AutoEntryWiringTests
|
|
{
|
|
[Fact]
|
|
public void ProductionAutoEntryRequiresTheRuntimePublishedController()
|
|
{
|
|
MethodInfo readiness = typeof(LivePlayerModeAutoEntryContext)
|
|
.GetProperty(
|
|
"IsPlayerControllerReady",
|
|
BindingFlags.Instance | BindingFlags.Public)!
|
|
.GetMethod!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(readiness);
|
|
|
|
Assert.Contains(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(PlayerMovementController)
|
|
&& call.Target.Name == "get_IsRuntimePublished");
|
|
Assert.Contains(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(LiveEntityRuntime)
|
|
&& 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]
|
|
public void PlayerPresentationAttach_DrainsMatchedAnimationsBeforeStartupMotionSuffix()
|
|
{
|
|
MethodInfo enter = typeof(PlayerModeController).GetMethod(
|
|
"BuildControllerAndCamera",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?? throw new MissingMethodException(
|
|
typeof(PlayerModeController).FullName,
|
|
"BuildControllerAndCamera");
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(enter);
|
|
int attach = CompiledCallGraph.IndexOf(
|
|
calls,
|
|
typeof(MotionInterpreter),
|
|
"set_DefaultSink");
|
|
int animationDrain = CompiledCallGraph.IndexOf(
|
|
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(
|
|
animationDrain > attach,
|
|
"Matching PartArray animations must drain after attaching the sink.");
|
|
Assert.True(
|
|
unmatchedMotionDrain > animationDrain,
|
|
"Only the unmatched pre-attach interpreter suffix may drain last.");
|
|
}
|
|
}
|