using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; namespace AcDream.Core.Tests.Physics.Motion; /// /// R5 conformance harness — a scriptable fake /// backed by a shared so resolves /// OTHER hosts (the cross-entity seam the voyeur round-trip needs). Each host /// lazily owns a and a /// (the two R5 managers under test), and records every /// the managers fan out so tests can assert on /// delivery. /// /// Position/velocity/radius/contact/max-speed and both clocks are mutable /// fields tests drive directly (retail's CPhysicsObj accessors). The /// set_target/clear_target/add_voyeur/remove_voyeur/ /// receive_target_update seams forward to the owned /// exactly as retail's CPhysicsObj does. /// internal sealed class R5Host : IPhysicsObjHost { public readonly Dictionary World; public R5Host(uint id, Dictionary world) { Id = id; World = world; World[id] = this; } // ── scriptable CPhysicsObj state ─────────────────────────────────────── public uint Id { get; } public Position Position { get; set; } = new(1u, Vector3.Zero, Quaternion.Identity); public Vector3 Velocity { get; set; } = Vector3.Zero; public float Radius { get; set; } = 0.5f; public bool InContact { get; set; } = true; public float? MinterpMaxSpeed { get; set; } = 1.0f; public double CurTime { get; set; } public double PhysicsTimerTime { get; set; } /// Set to false to simulate a target that isn't currently /// resolvable (out of streaming view) — returns /// null for it even while it's in . public bool Resolvable { get; set; } = true; // ── owned R5 managers ────────────────────────────────────────────────── private TargetManager? _targetManager; public TargetManager TargetManager => _targetManager ??= new TargetManager(this); public TargetManager? TargetManagerOrNull => _targetManager; private PositionManager? _positionManager; public PositionManager PositionManager => _positionManager ??= new PositionManager(this); // ── recorded fan-outs ────────────────────────────────────────────────── public readonly List HandleUpdateTargetCalls = new(); public int InterruptCurrentMovementCalls; // ── IPhysicsObjHost ──────────────────────────────────────────────────── public IPhysicsObjHost? GetObjectA(uint id) => World.TryGetValue(id, out var h) && h.Resolvable ? h : null; public void HandleUpdateTarget(TargetInfo info) { HandleUpdateTargetCalls.Add(info); // Retail CPhysicsObj::HandleUpdateTarget also fans to the position // manager's sticky sub-manager (context 0). Mirror that so sticky tests // that rely on the full CPhysicsObj fan-out see the callback. if (info.ContextId == 0) _positionManager?.HandleUpdateTarget(info); } public void InterruptCurrentMovement() => InterruptCurrentMovementCalls++; public void SetTarget(uint contextId, uint objectId, float radius, double quantum) => TargetManager.SetTarget(contextId, objectId, radius, quantum); public void ClearTarget() => _targetManager?.ClearTarget(); public void ReceiveTargetUpdate(TargetInfo info) => _targetManager?.ReceiveUpdate(info); public void AddVoyeur(uint watcherId, float radius, double quantum) => TargetManager.AddVoyeur(watcherId, radius, quantum); public void RemoveVoyeur(uint watcherId) => _targetManager?.RemoveVoyeur(watcherId); // ── test helpers ─────────────────────────────────────────────────────── public void SetOrigin(Vector3 origin) => Position = new Position(Position.ObjCellId, origin, Position.Frame.Orientation); public void AdvanceClocks(double seconds) { CurTime += seconds; PhysicsTimerTime += seconds; } }