using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; namespace AcDream.Core.Tests.Physics.Motion; /// /// R4-V2 — shared scripted fake interp-sink/provider harness for /// conformance tests (r4-port-plan.md §3 V2: /// "Use a scripted fake interp-sink/provider harness — NO real sequencer /// needed; the manager drives the interp seams; assert the call sequences /// + state"). Wraps a REAL bound to a /// minimal always-grounded (so /// _DoMotion/_StopMotion's adjust_motion + /// DoInterpretedMotion/StopInterpretedMotion chain runs for /// real, dispatch treated as always-succeeding since no /// is wired — matching /// DoInterpretedMotion's documented null-sink posture), and exposes /// every ctor seam as a mutable, inspectable /// field so tests can script position/heading/contact/target-tracker /// behavior and assert on call sequences. /// internal sealed class MoveToManagerHarness { public readonly MotionInterpreter Interp = new(); public readonly PhysicsBody Body = new(); /// Scripted world position + cell (defaults to cell 1, origin). public Position WorldPosition = new(1u, Vector3.Zero, Quaternion.Identity); /// Scripted compass heading, degrees (P5 convention). public float Heading; /// Records every SetHeading(heading, send) call. public readonly List<(float Heading, bool Send)> SetHeadingCalls = new(); public float OwnRadius = 0.5f; public float OwnHeight = 2.0f; public bool ContactValue = true; public bool IsInterpolatingValue; public Vector3 Velocity = Vector3.Zero; public uint SelfId = 0x50000001u; /// Records every StopCompletely() call (count only — /// retail's CPhysicsObj::StopCompletely takes no args at this /// seam level). public int StopCompletelyCalls; public readonly List<(uint ContextId, uint ObjectId, float Radius, double Quantum)> SetTargetCalls = new(); public int ClearTargetCalls; public double TargetQuantum; public readonly List SetTargetQuantumCalls = new(); public int UnstickCalls; public readonly List<(uint Tlid, float Radius, float Height)> StickToCalls = new(); public readonly List MoveToCompleteCalls = new(); /// Scripted clock — advances by only /// when a test calls ; reading CurTime alone /// (e.g. multiple reads within one manager call) does NOT advance it, /// matching retail's Timer::cur_time being a stable snapshot for /// the duration of one dispatch. public double CurTime; public const double TickSeconds = 1.0 / 30.0; public readonly MoveToManager Manager; public MoveToManagerHarness() { Interp.PhysicsObj = Body; Body.TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable | TransientStateFlags.Active; Manager = new MoveToManager( Interp, stopCompletely: () => StopCompletelyCalls++, getPosition: () => WorldPosition, getHeading: () => Heading, setHeading: (h, send) => { SetHeadingCalls.Add((h, send)); Heading = h; }, getOwnRadius: () => OwnRadius, getOwnHeight: () => OwnHeight, contact: () => ContactValue, isInterpolating: () => IsInterpolatingValue, getVelocity: () => Velocity, getSelfId: () => SelfId, setTarget: (ctx, obj, radius, quantum) => SetTargetCalls.Add((ctx, obj, radius, quantum)), clearTarget: () => ClearTargetCalls++, getTargetQuantum: () => TargetQuantum, setTargetQuantum: q => { TargetQuantum = q; SetTargetQuantumCalls.Add(q); }, curTime: () => CurTime); Manager.StickTo = (tlid, radius, height) => StickToCalls.Add((tlid, radius, height)); Manager.MoveToComplete = err => MoveToCompleteCalls.Add(err); Manager.Unstick = () => UnstickCalls++; } /// Advance the scripted clock by one physics tick (1/30 s). public void Tick() => CurTime += TickSeconds; /// Advance the scripted clock by an arbitrary amount. public void Advance(double seconds) => CurTime += seconds; /// /// Drains the REAL 's /// pending_motions queue via synthetic MotionDone /// callbacks — standing in for "the dispatched motion's animation-table /// cycle finished", which a live AnimationSequencer/ /// MotionTableManager would signal in production. Every /// _DoMotion/_StopMotion call that succeeds enqueues a /// node (retail AddToQueue, decomp's DoInterpretedMotion /// body); without draining, /// stays true forever in this bare harness, which would wedge /// 's wait-for-anims gate /// and Phase 1's /// "animating, stop aux" branch permanently. Call after any manager /// method that dispatches a motion, before asserting on the NEXT tick's /// behavior. /// public void DrainPendingMotions() { while (Interp.MotionsPending()) Interp.MotionDone(0, true); } /// Current interpreted forward command — the observable proxy /// for "what motion did MoveToManager just dispatch via _DoMotion", /// since /// writes through to /// when ModifyInterpretedState is set (default true). public uint ForwardCommand => Interp.InterpretedState.ForwardCommand; public uint TurnCommand => Interp.InterpretedState.TurnCommand; public float ForwardSpeed => Interp.InterpretedState.ForwardSpeed; }