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