The retail movement-manager family the R4 MoveToManager port left as do-not-invent seams (decomp §9f/§9g). Faithful C# ports of retail's PositionManager facade + StickyManager + ConstraintManager + the TargetManager voyeur system, with full conformance tests. NO wiring yet — purely additive, no behavior change. Wiring (retiring TS-39 sticky + AP-79 target adapter) is R5-V2/V3. New Core classes (src/AcDream.Core/Physics/Motion/): - StickyManager (0x00555400): follow-a-target steering. adjust_offset's dense x87 mush decoded via ACE (StickyRadius 0.3, StickyTime 1.0, follow speed ×5 / fallback 15) — speed-clamped signed-distance steer + bounded turn-to-face; 1 s watchdog; Ok→initialized / non-Ok→teardown. - ConstraintManager (0x00556090): the server-position rubber-band leash. 90% IsFullyConstrained jump gate + grounded linear brake taper. Structural only — acdream never ARMS it (retail arms from SmartBox::HandleReceivedPosition, which acdream lacks, with two x87 constants BN elided). IsFullyConstrained stays false = TS-35 behavior; leash-arming + the unknown constants are a deferred issue. - PositionManager facade (0x00555160): lazy Sticky/Constraint + fan-out. - TargetManager (0x0051a370) + TargettedVoyeurInfo: the peer-to-peer voyeur subscription system (0.5 s throttle, 10 s staleness, send-on-drift-past-radius, dead-reckon GetInterpolatedPosition). A faithful superset of the AP-79 adapter — SetTarget subscribes ON the target; the target's HandleTargetting pushes updates back. - IPhysicsObjHost: the CPhysicsObj back-pointer seam (position/velocity/ radius/contact/GetObjectA + target-tracking fan-out) the App wires per entity in V2/V3. MotionDeltaFrame: mutable retail-Frame delta accumulator. Supporting: - TargetInfo extended to the full retail 10-field struct (additive defaults keep the R4 4-arg call sites compiling). - MoveToMath: signed CylinderDistanceNoZ, NormalizeCheckSmall, GlobalToLocalVec. - Rename: the misnamed AcDream.Core.Physics.PositionManager (a remote anim+interp per-frame combiner, NOT the retail facade) → RemoteMotion Combiner, freeing the name and removing the ambiguity that breaks every file importing both Physics + Physics.Motion (GameWindow will in V2/V3). Tests: 42 new conformance cases (Sticky/Constraint/Position facade + TargetManager incl. the full cross-entity voyeur round-trip). Full suite 4006 green (+2 skipped), no regressions. Decomp + ACE cross-ref + port plan: docs/research/2026-07-03-r5-managers/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
98 lines
4.7 KiB
C#
98 lines
4.7 KiB
C#
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Physics.Motion;
|
|
|
|
namespace AcDream.Core.Tests.Physics.Motion;
|
|
|
|
/// <summary>
|
|
/// R5 conformance harness — a scriptable <see cref="IPhysicsObjHost"/> fake
|
|
/// backed by a shared <see cref="World"/> so <see cref="GetObjectA"/> resolves
|
|
/// OTHER hosts (the cross-entity seam the voyeur round-trip needs). Each host
|
|
/// lazily owns a <see cref="TargetManager"/> and a <see cref="PositionManager"/>
|
|
/// (the two R5 managers under test), and records every
|
|
/// <see cref="HandleUpdateTarget"/> the managers fan out so tests can assert on
|
|
/// delivery.
|
|
///
|
|
/// <para>Position/velocity/radius/contact/max-speed and both clocks are mutable
|
|
/// fields tests drive directly (retail's <c>CPhysicsObj</c> accessors). The
|
|
/// <c>set_target</c>/<c>clear_target</c>/<c>add_voyeur</c>/<c>remove_voyeur</c>/
|
|
/// <c>receive_target_update</c> seams forward to the owned
|
|
/// <see cref="TargetManager"/> exactly as retail's <c>CPhysicsObj</c> does.</para>
|
|
/// </summary>
|
|
internal sealed class R5Host : IPhysicsObjHost
|
|
{
|
|
public readonly Dictionary<uint, R5Host> World;
|
|
|
|
public R5Host(uint id, Dictionary<uint, R5Host> 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; }
|
|
|
|
/// <summary>Set to false to simulate a target that isn't currently
|
|
/// resolvable (out of streaming view) — <see cref="GetObjectA"/> returns
|
|
/// null for it even while it's in <see cref="World"/>.</summary>
|
|
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<TargetInfo> 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;
|
|
}
|
|
}
|