acdream/src/AcDream.Core/Physics/ServerControlledLocomotion.cs
Erik 7016b26ce7 feat(R4-V4): REMOTE cutover - per-remote MoveToManager; RemoteMoveToDriver + PlanMoveToStart DELETED (closes M1-remote, M4/M5/M6/M8-remote; retires AD-8, AD-9, AP-8, AP-9)
Every remote's server-directed movement now runs the verbatim retail
MoveToManager. EnsureRemoteMotionBindings constructs it per remote with
the full V2 seam set (position/heading/velocity providers, the P4
TargetTracker adapter via setTarget/clearTarget storing the tracked
guid on RemoteMotion) and binds InterruptCurrentMovement ->
CancelMoveTo(0x36) - the retail interrupt chain (TS-36 remote side).

UM routing is retail's unpack_movement dispatch (0x00524440): the
head-interrupt + unstick fire for EVERY movement type, then mt 6/7
build MovementParameters.FromWire(raw bitfield - now surfaced by the
parser) + a MovementStruct (mt 6 resolves the target guid against the
entity table, degrading to MoveToPosition at the wire origin per the
plan's 2f; my_run_rate written from MoveToRunRate per @300603) ->
MoveTo.PerformMovement; mt 8/9 likewise via FromWireTurnTo; ONLY mt 0
flows through the interpreted funnel (the PlanMoveToStart seed is
DELETED - retail never routes MoveTo types through the interpreted
state copy).

Per tick: the P4 tracker feeds HandleUpdateTarget(Ok/ExitWorld) from
the live entity table, then UseTime runs the manager's steering/
arrival/fail handlers, then apply_current_movement recomputes velocity
- the same shape ordinary locomotion uses. The legacy driver branches,
the stale-destination timer, and the ClampApproachVelocity band-aid
are gone; arrival now uses retail cylinder distance (watch melee-range
stop distance in the visual pass).

DELETED: RemoteMoveToDriver.cs + its tests (OriginToWorld relocated
verbatim to MoveToMath; the B.6 auto-walk's two surviving constants
inlined into PlayerMovementController, dying with it in V5);
ServerControlledLocomotion.PlanMoveToStart + tests (PlanFromVelocity
survives - new AP-80 row); the RemoteMotion MoveTo capture fields.
Registers: AD-8/AD-9/AP-8/AP-9 retired; AP-79 (TargetTracker adapter,
retire R5) + AP-80 added.

Full suite green: 3,948 passed. Live smoke launched - NPC
chase/wander behavior pending user verification (recorded in the
session handoff).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:18:05 +02:00

73 lines
2.5 KiB
C#

using System;
using System.Numerics;
namespace AcDream.Core.Physics;
/// <summary>
/// Chooses the visible locomotion cycle for server-controlled remotes whose
/// UpdateMotion packet is a MoveToObject/MoveToPosition union rather than an
/// InterpretedMotionState.
///
/// Retail references:
/// <list type="bullet">
/// <item><description>
/// <c>MovementManager::PerformMovement</c> (0x00524440) dispatches movement
/// types 6/7 into <c>MoveToManager::MoveToObject/MoveToPosition</c> instead
/// of unpacking an InterpretedMotionState.
/// </description></item>
/// <item><description>
/// <c>MovementParameters::UnPackNet</c> (0x0052AC50) shows MoveTo packets
/// carry movement params + run rate, not a ForwardCommand field.
/// </description></item>
/// <item><description>
/// ACE <c>MovementData.Write</c> uses the same movement type union; holtburger
/// documents the matching <c>MovementType::MoveToPosition = 7</c>.
/// </description></item>
/// </list>
/// </summary>
public static class ServerControlledLocomotion
{
public const float StopSpeed = 0.20f;
public const float RunThreshold = 1.25f;
public const float MinSpeedMod = 0.25f;
public const float MaxSpeedMod = 3.00f;
// R4-V4: PlanMoveToStart DELETED - MoveTo UMs route to the verbatim
// MoveToManager (BeginMoveForward -> get_command -> _DoMotion produces
// the cycle through the same sink every other motion uses).
// PlanFromVelocity below survives (M16 register row, retires in R6).
public static LocomotionCycle PlanFromVelocity(Vector3 worldVelocity)
{
float horizontalSpeed = MathF.Sqrt(
worldVelocity.X * worldVelocity.X +
worldVelocity.Y * worldVelocity.Y);
if (horizontalSpeed < StopSpeed)
return new LocomotionCycle(MotionCommand.Ready, 1f, false);
if (horizontalSpeed < RunThreshold)
{
float speedMod = Math.Clamp(
horizontalSpeed / MotionInterpreter.WalkAnimSpeed,
MinSpeedMod,
MaxSpeedMod);
return new LocomotionCycle(MotionCommand.WalkForward, speedMod, true);
}
return new LocomotionCycle(
MotionCommand.RunForward,
Math.Clamp(horizontalSpeed / MotionInterpreter.RunAnimSpeed, MinSpeedMod, MaxSpeedMod),
true);
}
public readonly record struct LocomotionCycle(
uint Motion,
float SpeedMod,
bool IsMoving);
private static float SanitizePositive(float value)
{
return float.IsFinite(value) && value > 0f ? value : 1f;
}
}