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>
This commit is contained in:
parent
a144e87318
commit
7016b26ce7
10 changed files with 248 additions and 883 deletions
|
|
@ -429,6 +429,21 @@ public sealed class GameWindow : IDisposable
|
|||
/// </summary>
|
||||
public System.Numerics.Vector3 ServerVelocity;
|
||||
public bool HasServerVelocity;
|
||||
|
||||
/// <summary>R4-V4: the entity's verbatim retail MoveToManager
|
||||
/// (server-directed movement), constructed + seam-bound by
|
||||
/// EnsureRemoteMotionBindings beside the R2-Q5/R3 binds.</summary>
|
||||
public AcDream.Core.Physics.Motion.MoveToManager? MoveTo;
|
||||
|
||||
// R4-V4 (pin P4): the minimal TargetTracker adapter state — the
|
||||
// manager's setTarget/clearTarget seams store the tracked guid here;
|
||||
// the per-tick block feeds HandleUpdateTarget(Ok/ExitWorld) from the
|
||||
// live entity table. Full TargetManager port is R5 (register row).
|
||||
public uint TrackedTargetGuid;
|
||||
public float TrackedTargetRadius;
|
||||
public System.Numerics.Vector3 TrackedTargetLastFedPos;
|
||||
public bool TrackedTargetFedOnce;
|
||||
public double TargetQuantum;
|
||||
/// <summary>
|
||||
/// True while a server MoveToObject/MoveToPosition packet is the
|
||||
/// active locomotion source. Retail runs these through MoveToManager
|
||||
|
|
@ -4191,7 +4206,7 @@ public sealed class GameWindow : IDisposable
|
|||
/// the VectorUpdate path regardless of arrival order.
|
||||
/// </summary>
|
||||
private AcDream.Core.Physics.Motion.MotionTableDispatchSink? EnsureRemoteMotionBindings(
|
||||
RemoteMotion rm, AnimatedEntity ae)
|
||||
RemoteMotion rm, AnimatedEntity ae, uint serverGuid)
|
||||
{
|
||||
if (ae.Sequencer is null)
|
||||
return rm.Sink;
|
||||
|
|
@ -4218,6 +4233,44 @@ public sealed class GameWindow : IDisposable
|
|||
// R3-W5: the per-op zero-tick flush (CPhysicsObj::CheckForCompletedMotions
|
||||
// 0x0050fe30 -> MotionTableManager.CheckForCompletedMotions).
|
||||
rm.Motion.CheckForCompletedMotions = ae.Sequencer.Manager.CheckForCompletedMotions;
|
||||
|
||||
// R4-V4: the verbatim MoveToManager, seam-bound per the V2 harness
|
||||
// wiring (the conformance-tested reference). Positions are WORLD
|
||||
// space on both sides (getPosition + the MovementStruct positions
|
||||
// the UM router builds) — one consistent space, so the manager's
|
||||
// distance/heading math matches the harness exactly.
|
||||
var rmT = rm;
|
||||
var mtBody = rm.Body;
|
||||
rm.MoveTo = new AcDream.Core.Physics.Motion.MoveToManager(
|
||||
rm.Motion,
|
||||
stopCompletely: () => rmT.Motion.StopCompletely(),
|
||||
getPosition: () => new AcDream.Core.Physics.Position(
|
||||
rmT.CellId, mtBody.Position, mtBody.Orientation),
|
||||
getHeading: () => AcDream.Core.Physics.Motion.MoveToMath.GetHeading(
|
||||
mtBody.Orientation),
|
||||
setHeading: (h, _) => mtBody.Orientation =
|
||||
AcDream.Core.Physics.Motion.MoveToMath.SetHeading(mtBody.Orientation, h),
|
||||
getOwnRadius: () => 0f, // pin P4 note: setup cylsphere radius lands with R5's TargetManager port
|
||||
getOwnHeight: () => 0f,
|
||||
contact: () => mtBody.OnWalkable,
|
||||
isInterpolating: () => rmT.Interp.IsActive,
|
||||
getVelocity: () => mtBody.Velocity,
|
||||
getSelfId: () => serverGuid,
|
||||
setTarget: (_, tlid, radius, _) =>
|
||||
{
|
||||
rmT.TrackedTargetGuid = tlid;
|
||||
rmT.TrackedTargetRadius = radius;
|
||||
rmT.TrackedTargetFedOnce = false;
|
||||
},
|
||||
clearTarget: () => rmT.TrackedTargetGuid = 0,
|
||||
getTargetQuantum: () => rmT.TargetQuantum,
|
||||
setTargetQuantum: q => rmT.TargetQuantum = q);
|
||||
// TS-36 (remote side): the interp's interrupt seam is retail's
|
||||
// interrupt_current_movement → MovementManager::CancelMoveTo(0x36)
|
||||
// chain (V2's reentrancy tests prove the wiring is inert-safe).
|
||||
rm.Motion.InterruptCurrentMovement =
|
||||
() => rmT.MoveTo?.CancelMoveTo(
|
||||
AcDream.Core.Physics.WeenieError.ActionCancelled);
|
||||
return rm.Sink;
|
||||
}
|
||||
|
||||
|
|
@ -4400,24 +4453,13 @@ public sealed class GameWindow : IDisposable
|
|||
// otherwise → resolve class byte and use full cmd
|
||||
float speedMod = update.MotionState.ForwardSpeed ?? 1f;
|
||||
uint fullMotion;
|
||||
if ((!command.HasValue || command.Value == 0)
|
||||
&& update.MotionState.IsServerControlledMoveTo)
|
||||
{
|
||||
// Retail MoveToManager::BeginMoveForward calls
|
||||
// MovementParameters::get_command (0x0052AA00), then
|
||||
// _DoMotion -> adjust_motion. With CanRun and enough
|
||||
// distance, WalkForward + HoldKey_Run becomes RunForward,
|
||||
// and CMotionInterp::apply_run_to_command (0x00527BE0)
|
||||
// multiplies speed by the packet's runRate.
|
||||
var seed = AcDream.Core.Physics.ServerControlledLocomotion
|
||||
.PlanMoveToStart(
|
||||
update.MotionState.MoveToSpeed ?? 1f,
|
||||
update.MotionState.MoveToRunRate ?? 1f,
|
||||
update.MotionState.MoveToCanRun);
|
||||
fullMotion = seed.Motion;
|
||||
speedMod = seed.SpeedMod;
|
||||
}
|
||||
else if (!command.HasValue || command.Value == 0)
|
||||
// R4-V4: the PlanMoveToStart seed is DELETED — MoveTo UMs no
|
||||
// longer flow through the interpreted funnel at all (retail
|
||||
// unpack_movement routes types 6-9 to MoveToManager; only type
|
||||
// 0 does the interpreted-state copy). The manager's own
|
||||
// BeginMoveForward -> get_command -> _DoMotion produces the
|
||||
// cycle through the same sink every other motion uses.
|
||||
if (!command.HasValue || command.Value == 0)
|
||||
{
|
||||
fullMotion = 0x41000003u;
|
||||
}
|
||||
|
|
@ -4510,7 +4552,7 @@ public sealed class GameWindow : IDisposable
|
|||
&& update.MotionState.MoveToPath is { } pathData)
|
||||
{
|
||||
// Translate landblock-local origin → world space.
|
||||
var destWorld = AcDream.Core.Physics.RemoteMoveToDriver
|
||||
var destWorld = AcDream.Core.Physics.Motion.MoveToMath
|
||||
.OriginToWorld(
|
||||
pathData.OriginCellId,
|
||||
pathData.OriginX,
|
||||
|
|
@ -4563,13 +4605,97 @@ public sealed class GameWindow : IDisposable
|
|||
// contact_allows_move gate, the retail mechanism behind the
|
||||
// old K-fix17 guard).
|
||||
//
|
||||
// MoveTo packets (mt 6/7) reuse the PlanMoveToStart seed as
|
||||
// the funnel's forward command (fullMotion/speedMod computed
|
||||
// above) and additionally capture the path for the per-tick
|
||||
// remote driver.
|
||||
// R4-V4: retail unpack_movement dispatch (0x00524440) — the
|
||||
// head-interrupt fires for EVERY movement type, then types
|
||||
// 6/7/8/9 route to MoveToManager.PerformMovement (which
|
||||
// cancels again itself — retail does both) while ONLY type 0
|
||||
// flows through the interpreted-state funnel below.
|
||||
if (_remoteDeadReckon.TryGetValue(update.Guid, out var remoteMot))
|
||||
{
|
||||
remoteMot.ServerMoveToActive = update.MotionState.IsServerControlledMoveTo;
|
||||
var sink = EnsureRemoteMotionBindings(remoteMot, ae, update.Guid);
|
||||
|
||||
// unpack_movement head (@300566): interrupt + unstick.
|
||||
remoteMot.Motion.InterruptCurrentMovement?.Invoke();
|
||||
remoteMot.Motion.UnstickFromObject?.Invoke();
|
||||
|
||||
if (update.MotionState.IsServerControlledMoveTo
|
||||
&& update.MotionState.MoveToPath is { } path
|
||||
&& remoteMot.MoveTo is { } moveMgr)
|
||||
{
|
||||
// my_run_rate write (unpack_movement @300603).
|
||||
if (update.MotionState.MoveToRunRate is { } mtRunRate)
|
||||
remoteMot.Motion.MyRunRate = mtRunRate;
|
||||
|
||||
var destWorld = AcDream.Core.Physics.Motion.MoveToMath
|
||||
.OriginToWorld(
|
||||
path.OriginCellId, path.OriginX, path.OriginY, path.OriginZ,
|
||||
_liveCenterX, _liveCenterY);
|
||||
var mp = AcDream.Core.Physics.Motion.MovementParameters.FromWire(
|
||||
path.Bitfield,
|
||||
path.DistanceToObject,
|
||||
path.MinDistance,
|
||||
path.FailDistance,
|
||||
update.MotionState.MoveToSpeed ?? 1f,
|
||||
path.WalkRunThreshold,
|
||||
path.DesiredHeading);
|
||||
|
||||
var ms = new AcDream.Core.Physics.MovementStruct
|
||||
{
|
||||
Params = mp,
|
||||
};
|
||||
// mt 6 with a resolvable target → MoveToObject (the
|
||||
// P4 tracker feeds position updates per tick); else
|
||||
// degrade to MoveToPosition at the wire origin (§2f).
|
||||
if (update.MotionState.MovementType == 6
|
||||
&& path.TargetGuid is { } tgtGuid
|
||||
&& _entitiesByServerGuid.TryGetValue(tgtGuid, out var tgtEnt))
|
||||
{
|
||||
ms.Type = AcDream.Core.Physics.MovementType.MoveToObject;
|
||||
ms.ObjectId = tgtGuid;
|
||||
ms.TopLevelId = tgtGuid;
|
||||
ms.Pos = new AcDream.Core.Physics.Position(
|
||||
remoteMot.CellId, tgtEnt.Position,
|
||||
System.Numerics.Quaternion.Identity);
|
||||
}
|
||||
else
|
||||
{
|
||||
ms.Type = AcDream.Core.Physics.MovementType.MoveToPosition;
|
||||
ms.Pos = new AcDream.Core.Physics.Position(
|
||||
remoteMot.CellId, destWorld,
|
||||
System.Numerics.Quaternion.Identity);
|
||||
}
|
||||
moveMgr.PerformMovement(ms);
|
||||
return;
|
||||
}
|
||||
|
||||
if (update.MotionState.IsServerControlledTurnTo
|
||||
&& update.MotionState.TurnToPath is { } turnPath
|
||||
&& remoteMot.MoveTo is { } turnMgr)
|
||||
{
|
||||
var mp = AcDream.Core.Physics.Motion.MovementParameters.FromWireTurnTo(
|
||||
turnPath.Bitfield,
|
||||
turnPath.Speed,
|
||||
turnPath.DesiredHeading);
|
||||
|
||||
var ms = new AcDream.Core.Physics.MovementStruct { Params = mp };
|
||||
if (update.MotionState.MovementType == 8
|
||||
&& turnPath.TargetGuid is { } turnTgt
|
||||
&& _entitiesByServerGuid.TryGetValue(turnTgt, out var turnEnt))
|
||||
{
|
||||
ms.Type = AcDream.Core.Physics.MovementType.TurnToObject;
|
||||
ms.ObjectId = turnTgt;
|
||||
ms.TopLevelId = turnTgt;
|
||||
ms.Pos = new AcDream.Core.Physics.Position(
|
||||
remoteMot.CellId, turnEnt.Position,
|
||||
System.Numerics.Quaternion.Identity);
|
||||
}
|
||||
else
|
||||
{
|
||||
ms.Type = AcDream.Core.Physics.MovementType.TurnToHeading;
|
||||
}
|
||||
turnMgr.PerformMovement(ms);
|
||||
return;
|
||||
}
|
||||
|
||||
// [FWD_WIRE] + observed-velocity history invalidation on
|
||||
// a forward-command change (pre-S2 behavior, unchanged:
|
||||
|
|
@ -4588,30 +4714,9 @@ public sealed class GameWindow : IDisposable
|
|||
remoteMot.PrevServerPosTime = 0.0;
|
||||
}
|
||||
|
||||
if (update.MotionState.IsServerControlledMoveTo
|
||||
&& update.MotionState.MoveToPath is { } path)
|
||||
{
|
||||
remoteMot.MoveToDestinationWorld = AcDream.Core.Physics.RemoteMoveToDriver
|
||||
.OriginToWorld(
|
||||
path.OriginCellId, path.OriginX, path.OriginY, path.OriginZ,
|
||||
_liveCenterX, _liveCenterY);
|
||||
remoteMot.MoveToMinDistance = path.MinDistance;
|
||||
remoteMot.MoveToDistanceToObject = path.DistanceToObject;
|
||||
remoteMot.MoveToMoveTowards = update.MotionState.MoveTowards;
|
||||
remoteMot.HasMoveToDestination = true;
|
||||
remoteMot.LastMoveToPacketTime =
|
||||
(System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
}
|
||||
else if (!update.MotionState.IsServerControlledMoveTo)
|
||||
{
|
||||
// Off MoveTo — clear stale destination so the
|
||||
// per-tick driver doesn't keep steering.
|
||||
remoteMot.HasMoveToDestination = false;
|
||||
}
|
||||
|
||||
// Build the funnel input. fullMotion/speedMod already
|
||||
// encode retail's absent-field forward defaults (null/0
|
||||
// → Ready; MoveTo → PlanMoveToStart seed). Style:
|
||||
// → Ready; mt 6-9 never reach here post-V4). Style:
|
||||
// retail's InterpretedMotionState::UnPack (0x0051f400)
|
||||
// defaults an absent stance to NonCombat 0x8000003D —
|
||||
// NOT keep-current (S0 trace: every empty UM applied
|
||||
|
|
@ -4670,11 +4775,7 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
// R2-Q5/R3-W4: funnel dispatches go STRAIGHT into the
|
||||
// motion-table stack (GetObjectSequence + is_allowed
|
||||
// decide). The sink is PERSISTENT per remote and also
|
||||
// bound as Motion.DefaultSink + the RemoveLink/InitTables
|
||||
// seams (EnsureRemoteMotionBindings) so LeaveGround/
|
||||
// HitGround dispatch through the same backend.
|
||||
var sink = EnsureRemoteMotionBindings(remoteMot, ae);
|
||||
// decide) — mt-0 only post-V4 (types 6-9 returned above).
|
||||
remoteMot.Motion.MoveToInterpretedState(ims, sink);
|
||||
}
|
||||
}
|
||||
|
|
@ -4854,7 +4955,7 @@ public sealed class GameWindow : IDisposable
|
|||
&& _animatedEntities.TryGetValue(ent.Id, out var ae)
|
||||
&& ae.Sequencer is not null)
|
||||
{
|
||||
EnsureRemoteMotionBindings(rm, ae);
|
||||
EnsureRemoteMotionBindings(rm, ae, update.Guid);
|
||||
rm.Motion.LeaveGround();
|
||||
rm.Body.Velocity = update.Velocity;
|
||||
rm.Body.Omega = update.Omega;
|
||||
|
|
@ -4910,11 +5011,10 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
if (rm.Airborne) return;
|
||||
if (ae.Sequencer is null) return;
|
||||
// MoveTo packets already seeded the retail speed/runRate cycle.
|
||||
// Keep UpdatePosition-derived velocity for render position only;
|
||||
// using it to choose the cycle reverts fast chases back to slow
|
||||
// velocity-estimated animation.
|
||||
if (rm.ServerMoveToActive) return;
|
||||
// R4-V4: an active MoveToManager owns the cycle (BeginMoveForward's
|
||||
// get_command dispatch). Velocity-estimated cycle planning would
|
||||
// fight it — same rationale as the pre-V4 ServerMoveToActive guard.
|
||||
if (rm.MoveTo is { MovementTypeState: not AcDream.Core.Physics.MovementType.Invalid }) return;
|
||||
|
||||
if (IsPlayerGuid(serverGuid))
|
||||
{
|
||||
|
|
@ -4960,7 +5060,7 @@ public sealed class GameWindow : IDisposable
|
|||
+ $"|v|={velocity.Length():F2} "
|
||||
+ $"-> motion=0x{plan.Motion:X8} speedMod={plan.SpeedMod:F2} "
|
||||
+ $"prev=0x{currentMotion:X8} "
|
||||
+ $"airborne={rm.Airborne} moveTo={rm.ServerMoveToActive}");
|
||||
+ $"airborne={rm.Airborne} moveTo={rm.MoveTo?.MovementTypeState ?? AcDream.Core.Physics.MovementType.Invalid}");
|
||||
}
|
||||
ae.Sequencer.SetCycle(style, plan.Motion, plan.SpeedMod);
|
||||
}
|
||||
|
|
@ -5326,7 +5426,6 @@ public sealed class GameWindow : IDisposable
|
|||
// carries no stop information for our ACE.
|
||||
if (svel.LengthSquared() < 0.04f)
|
||||
{
|
||||
rmState.ServerMoveToActive = false;
|
||||
rmState.Motion.StopCompletely();
|
||||
if (_animatedEntities.TryGetValue(entity.Id, out var aeForStop)
|
||||
&& aeForStop.Sequencer is not null)
|
||||
|
|
@ -9591,93 +9690,52 @@ public sealed class GameWindow : IDisposable
|
|||
rm.Body.Velocity = rm.ServerVelocity;
|
||||
}
|
||||
}
|
||||
else if (!IsPlayerGuid(serverGuid) && rm.ServerMoveToActive
|
||||
&& rm.HasMoveToDestination)
|
||||
{
|
||||
// Phase L.1c port of retail MoveToManager per-tick
|
||||
// steering (HandleMoveToPosition @ 0x00529d80).
|
||||
// Steer body orientation toward the latest
|
||||
// server-supplied destination, then let
|
||||
// apply_current_movement set Velocity from the
|
||||
// RunForward cycle through the now-correct heading.
|
||||
|
||||
// Stale-destination guard (2026-04-28): if no
|
||||
// MoveTo packet has refreshed the destination
|
||||
// recently, the entity has likely left our
|
||||
// streaming view or the server cancelled the
|
||||
// move without us seeing the cancel UM. Continuing
|
||||
// to steer toward a stale point produces the
|
||||
// "monster runs in place after popping back into
|
||||
// view" symptom. Clear and stand down.
|
||||
double moveToAge = nowSec - rm.LastMoveToPacketTime;
|
||||
if (moveToAge > AcDream.Core.Physics.RemoteMoveToDriver.StaleDestinationSeconds)
|
||||
{
|
||||
rm.HasMoveToDestination = false;
|
||||
rm.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
var driveResult = AcDream.Core.Physics.RemoteMoveToDriver
|
||||
.Drive(
|
||||
rm.Body.Position,
|
||||
rm.Body.Orientation,
|
||||
rm.MoveToDestinationWorld,
|
||||
rm.MoveToMinDistance,
|
||||
rm.MoveToDistanceToObject,
|
||||
(float)dt,
|
||||
rm.MoveToMoveTowards,
|
||||
out var steeredOrientation);
|
||||
rm.Body.Orientation = steeredOrientation;
|
||||
|
||||
if (driveResult == AcDream.Core.Physics.RemoteMoveToDriver
|
||||
.DriveResult.Arrived)
|
||||
{
|
||||
// Within arrival window — zero velocity until the
|
||||
// next MoveTo packet refreshes the destination
|
||||
// (or the server explicitly stops us with an
|
||||
// interpreted-motion UM cmd=Ready).
|
||||
rm.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Steering active — apply_current_movement reads
|
||||
// InterpretedState.ForwardCommand=RunForward (set
|
||||
// when the MoveTo packet arrived) and emits
|
||||
// velocity along +Y in body local space. Our
|
||||
// updated orientation rotates that into the right
|
||||
// world direction toward the target.
|
||||
rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false);
|
||||
|
||||
// Clamp horizontal velocity so we don't overshoot
|
||||
// the arrival threshold during the final tick of
|
||||
// approach. Without this, a 4 m/s body advances
|
||||
// ~6 cm/tick and visibly runs slightly through
|
||||
// the target before the swing UM lands.
|
||||
float arrivalThreshold = rm.MoveToMoveTowards
|
||||
? rm.MoveToDistanceToObject
|
||||
: rm.MoveToMinDistance;
|
||||
rm.Body.Velocity = AcDream.Core.Physics.RemoteMoveToDriver
|
||||
.ClampApproachVelocity(
|
||||
rm.Body.Position,
|
||||
rm.Body.Velocity,
|
||||
rm.MoveToDestinationWorld,
|
||||
arrivalThreshold,
|
||||
(float)dt,
|
||||
rm.MoveToMoveTowards);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!IsPlayerGuid(serverGuid) && rm.ServerMoveToActive)
|
||||
{
|
||||
// MoveTo flag set but we haven't seen a path payload
|
||||
// yet (e.g. truncated packet, or a brand-new entity
|
||||
// whose first cycle UM is still in flight). Hold
|
||||
// velocity at zero — same conservative stance as the
|
||||
// 882a07c stabilizer for incomplete state.
|
||||
rm.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
// R4-V4: the retail MoveToManager drives
|
||||
// server-directed movement per tick — the P4
|
||||
// TargetTracker adapter feeds HandleUpdateTarget
|
||||
// from the live entity table (full TargetManager
|
||||
// port is R5; register row), then UseTime runs
|
||||
// HandleMoveToPosition/HandleTurnToHeading
|
||||
// (steering + arrival + fail-distance), and
|
||||
// apply_current_movement recomputes velocity from
|
||||
// whatever the manager dispatched — the same
|
||||
// per-tick shape ordinary locomotion uses.
|
||||
if (!IsPlayerGuid(serverGuid) && rm.MoveTo is { } mtm)
|
||||
{
|
||||
if (rm.TrackedTargetGuid != 0)
|
||||
{
|
||||
if (_entitiesByServerGuid.TryGetValue(rm.TrackedTargetGuid, out var trackedEnt))
|
||||
{
|
||||
var tpos = trackedEnt.Position;
|
||||
if (!rm.TrackedTargetFedOnce
|
||||
|| System.Numerics.Vector3.Distance(tpos, rm.TrackedTargetLastFedPos)
|
||||
> rm.TrackedTargetRadius)
|
||||
{
|
||||
rm.TrackedTargetFedOnce = true;
|
||||
rm.TrackedTargetLastFedPos = tpos;
|
||||
var tp = new AcDream.Core.Physics.Position(
|
||||
rm.CellId, tpos, System.Numerics.Quaternion.Identity);
|
||||
mtm.HandleUpdateTarget(new AcDream.Core.Physics.Motion.TargetInfo(
|
||||
rm.TrackedTargetGuid,
|
||||
AcDream.Core.Physics.Motion.TargetStatus.Ok,
|
||||
tp, tp));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var lp = new AcDream.Core.Physics.Position(
|
||||
rm.CellId, rm.TrackedTargetLastFedPos,
|
||||
System.Numerics.Quaternion.Identity);
|
||||
mtm.HandleUpdateTarget(new AcDream.Core.Physics.Motion.TargetInfo(
|
||||
rm.TrackedTargetGuid,
|
||||
AcDream.Core.Physics.Motion.TargetStatus.ExitWorld,
|
||||
lp, lp));
|
||||
}
|
||||
}
|
||||
mtm.UseTime();
|
||||
}
|
||||
rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue