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;
}