refactor(physics): extract remote motion runtime

This commit is contained in:
Erik 2026-07-21 13:17:58 +02:00
parent d68c83d1a5
commit 5882b308c1
12 changed files with 368 additions and 361 deletions

View file

@ -5,7 +5,7 @@ using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
using RemoteMotion = AcDream.App.Rendering.GameWindow.RemoteMotion;
using RemoteMotion = AcDream.App.Physics.RemoteMotion;
namespace AcDream.App.Physics;

View file

@ -0,0 +1,307 @@
using AcDream.App.Rendering;
using AcDream.App.World;
namespace AcDream.App.Physics;
/// <summary>
/// Per-remote-entity physics + motion stack — verbatim application of
/// retail's client-side motion pipeline to every remote. Mirrors retail
/// <c>CPhysicsObj::update_object</c> (0x00515D10) →
/// <c>CPhysicsObj::UpdateObjectInternal</c> (0x005156B0) →
/// <c>CPhysicsObj::UpdatePositionInternal</c> (0x00512C30) →
/// <c>CPhysicsObj::UpdatePhysicsInternal</c> (0x00510700), and ACE's
/// <c>PhysicsObj.cs</c> port.
///
/// <para>
/// Retail has NO special "interpolator" for remote entities — it runs
/// the full motion state machine on every entity, local or remote,
/// and reconciles via hard-snap on UpdatePosition. This class simply
/// pairs a <see cref="AcDream.Core.Physics.PhysicsBody"/> with its
/// <see cref="AcDream.Core.Physics.MotionInterpreter"/> so each
/// remote gets the same treatment as the local player.
/// </para>
/// </summary>
internal sealed class RemoteMotion :
AcDream.App.World.ILiveEntityRemotePlacementRuntime
{
public AcDream.Core.Physics.PhysicsBody Body { get; }
AcDream.Core.Physics.PhysicsBody AcDream.App.World.ILiveEntityRemoteMotionRuntime.Body => Body;
/// <summary>R5-V5: retail <c>CPhysicsObj::movement_manager</c> — the
/// ONE per-entity owner of the interp + moveto pair (acclient.h
/// <c>/* 3463 */</c>). Constructed here with the interp; the
/// MoveToManager side arrives via <c>MoveToFactory</c> +
/// <c>MakeMoveToManager()</c> in EnsureRemoteMotionBindings.
/// <see cref="Motion"/>/<see cref="MoveTo"/> below are views of its
/// children (retail <c>get_minterp</c>-style access), kept so the
/// dozens of existing call sites read unchanged.</summary>
public AcDream.Core.Physics.Motion.MovementManager Movement;
public AcDream.Core.Physics.MotionInterpreter Motion => Movement.Minterp;
/// <summary>R3-W4: the persistent per-remote dispatch sink (created
/// once by EnsureRemoteMotionBindings; also bound as
/// Motion.DefaultSink so LeaveGround/HitGround re-applies dispatch
/// cycles through the motion-table backend).</summary>
public AcDream.Core.Physics.Motion.MotionTableDispatchSink? Sink;
/// <summary>Last UpdatePosition timestamp — drives body.update_object sub-stepping.</summary>
public double LastServerPosTime;
/// <summary>Last known server position — kept for diagnostics / HUD.</summary>
public System.Numerics.Vector3 LastServerPos;
/// <summary>
/// #184: the body position at which this remote's collision SHADOW was
/// last (re-)registered. The per-tick shadow-follows-resolved sync
/// (<c>SyncRemoteShadowToBody</c>) skips re-flooding when the body has
/// not moved &gt; ε since this — a resting/idle-town crowd doesn't churn
/// the registry, while a moving/de-overlapping crowd re-registers every
/// tick. Sentinel <c>Zero</c> forces the first sync.
/// </summary>
public System.Numerics.Vector3 LastShadowSyncPos;
/// <summary>
/// Complete root orientation paired with <see cref="LastShadowSyncPos"/>.
/// Multipart and off-centre Setup collision geometry must be re-flooded
/// when the object turns in place, not only when its origin translates.
/// The default zero quaternion is the first-sync sentinel.
/// </summary>
public System.Numerics.Quaternion LastShadowSyncOrientation;
/// <summary>
/// Latest server-authoritative velocity for NPC/monster smoothing.
/// Prefer the HasVelocity vector from UpdatePosition; when ACE omits
/// it for a server-controlled creature, derive it from consecutive
/// authoritative positions instead of guessing from player RUM state.
/// </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. R5-V5:
/// owned by <see cref="Movement"/>; this is the child view.</summary>
public AcDream.Core.Physics.Motion.MoveToManager? MoveTo => Movement.MoveTo;
// R5-V2: the entity's CPhysicsObj stand-in — owns the TargetManager
// voyeur system (retail CPhysicsObj::target_manager). Replaces the
// AP-79 TrackedTarget* poll fields: the MoveToManager's set_target/
// clear_target/quantum seams route here, HandleTargetting ticks per
// frame, and OTHER entities resolve this one via GameWindow._physicsHosts.
public EntityPhysicsHost? Host;
/// <summary>
/// True while a server MoveToObject/MoveToPosition packet is the
/// active locomotion source. Retail runs these through MoveToManager
/// and CMotionInterp; the per-tick remote driver consults this to
/// decide whether to feed body steering through
/// <see cref="AcDream.Core.Physics.RemoteMoveToDriver"/> instead of
/// the InterpretedMotionState path.
/// </summary>
public bool ServerMoveToActive;
/// <summary>
/// True once a MoveTo packet's full path payload (Origin + thresholds)
/// has been parsed and the world-converted destination is stored on
/// <see cref="MoveToDestinationWorld"/>. Cleared on arrival or when
/// the next non-MoveTo UpdateMotion replaces the locomotion source.
/// Phase L.1c (2026-04-28).
/// </summary>
public bool HasMoveToDestination;
/// <summary>
/// World-space destination from the most recent MoveTo packet's
/// <c>Origin</c> field, converted via the same landblock-grid
/// arithmetic <c>OnLivePositionUpdated</c> uses.
/// </summary>
public System.Numerics.Vector3 MoveToDestinationWorld;
/// <summary>
/// <c>min_distance</c> from the MoveTo packet's MovementParameters.
/// Used by <see cref="AcDream.Core.Physics.RemoteMoveToDriver"/> as
/// the chase-arrival threshold per retail
/// <c>MoveToManager::HandleMoveToPosition</c>.
/// </summary>
public float MoveToMinDistance;
/// <summary>
/// <c>distance_to_object</c> from the MoveTo packet. Reserved for
/// the flee branch (<c>move_away</c>); chase uses
/// <see cref="MoveToMinDistance"/>.
/// </summary>
public float MoveToDistanceToObject;
/// <summary>
/// True if MovementParameters bit 9 (<c>move_towards</c>, mask
/// <c>0x200</c>) is set on the active packet — i.e. this is a
/// chase. False = flee (<c>move_away</c>) or static target.
/// </summary>
public bool MoveToMoveTowards;
/// <summary>
/// Seconds-since-epoch timestamp of the most recent MoveTo packet
/// for this entity. Used by the per-tick driver to give up
/// steering when no refresh has arrived for
/// <see cref="AcDream.Core.Physics.RemoteMoveToDriver.StaleDestinationSeconds"/>
/// — typically because the entity left our streaming view and
/// the server stopped broadcasting its MoveTo updates.
/// </summary>
public double LastMoveToPacketTime;
/// <summary>
/// Full 32-bit cell ID from the latest UpdatePosition. High 16 bits
/// = landblock (LBx,LBy), low 16 bits = cell index (outdoor 0x0001-
/// 0x0040, indoor 0x0100+). Fed into
/// <see cref="AcDream.Core.Physics.PhysicsEngine.ResolveWithTransition"/>
/// so the retail sphere-sweep can look up the right terrain/EnvCell
/// polygons for each remote's per-frame motion. Zero until the first
/// UP lands, which disables the transition step for that frame
/// (Euler alone, matching pre-wire behavior).
/// </summary>
private uint _cellId;
private Func<uint>? _canonicalCellReader;
private Action<uint>? _canonicalCellWriter;
/// <summary>
/// Canonical full cell for this one live CPhysicsObj. Ordinary remotes
/// retain the local backing value. A MovementManager attached to an
/// existing projectile delegates reads and writes to
/// <see cref="LiveEntityRuntime"/>, so projectile prediction,
/// Movement packets, and spatial rebucketing cannot develop competing
/// cell identities.
/// </summary>
public uint CellId
{
get => _canonicalCellReader?.Invoke() ?? _cellId;
set
{
_cellId = value;
_canonicalCellWriter?.Invoke(value);
}
}
System.Numerics.Vector3 AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastServerPosition
{
get => LastServerPos;
set => LastServerPos = value;
}
double AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastServerPositionTime
{
get => LastServerPosTime;
set => LastServerPosTime = value;
}
System.Numerics.Vector3 AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastShadowSyncPosition
{
get => LastShadowSyncPos;
set => LastShadowSyncPos = value;
}
System.Numerics.Quaternion AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastShadowSyncOrientation
{
get => LastShadowSyncOrientation;
set => LastShadowSyncOrientation = value;
}
bool AcDream.App.World.ILiveEntityRemotePlacementRuntime.Airborne
{
get => Airborne;
set => Airborne = value;
}
void AcDream.App.World.ILiveEntityRemotePlacementRuntime.HitGround() =>
Movement.HitGround();
void AcDream.App.World.ILiveEntityRemotePlacementRuntime.LeaveGround() =>
Motion.LeaveGround();
/// <summary>
/// K-fix9 (2026-04-26): true while the remote is airborne (jump
/// arc in flight). Set when a 0xF74E VectorUpdate arrives with
/// non-trivial +Z velocity; cleared when the next UpdatePosition
/// snaps to a new ground location. While true, the per-tick
/// remote update SKIPS the "force OnWalkable + apply_current_movement"
/// step that would otherwise stomp the body's Z velocity each
/// frame, AND enables gravity so the parabolic arc actually plays
/// out between server snaps.
/// </summary>
public bool Airborne;
/// <summary>
/// Per-remote position-waypoint queue + catch-up math (retail's
/// CPhysicsObj::InterpolateTo + InterpolationManager::adjust_offset).
/// Drives per-tick body translation for grounded player remotes
/// via <see cref="Position"/>.
/// </summary>
public AcDream.Core.Physics.InterpolationManager Interp { get; } =
new AcDream.Core.Physics.InterpolationManager();
/// <summary>
/// Per-frame combiner for animation root motion + InterpolationManager
/// correction. Mirrors retail UpdatePositionInternal @ 0x00512c30:
/// queue catch-up REPLACES anim when active; anim stands when queue
/// is idle.
/// </summary>
public AcDream.Core.Physics.RemoteMotionCombiner Position { get; } =
new AcDream.Core.Physics.RemoteMotionCombiner();
/// <summary>Reusable complete delta frame shared by interpolation,
/// Sticky/Constraint, and the final body composition.</summary>
public AcDream.Core.Physics.Motion.MotionDeltaFrame PositionManagerDeltaScratch { get; } =
new AcDream.Core.Physics.Motion.MotionDeltaFrame();
/// <summary>
/// Diagnostic-only (gated on <c>ACDREAM_REMOTE_VEL_DIAG=1</c>): the
/// previous UpdatePosition's world position + timestamp. The per-tick
/// path compares the server's broadcast pace with the literal root
/// displacement emitted by <c>CSequence::update</c>. The old inferred
/// walk/run velocity was the source of the periodic overshoot this
/// diagnostic was introduced to find.
/// </summary>
public System.Numerics.Vector3 PrevServerPos;
public double PrevServerPosTime;
public double LastOmegaDiagLogTime;
/// <summary>
/// Diagnostic-only: maximum scaled CSequence root-motion speed
/// observed since the last UpdatePosition arrival. The next UP
/// compares it with the server's observed pace. Reset on each UP.
/// </summary>
public float MaxRootMotionSpeedSinceLastUP;
public RemoteMotion(AcDream.Core.Physics.PhysicsBody? sharedBody = null)
{
Body = sharedBody ?? new AcDream.Core.Physics.PhysicsBody
{
// Remotes don't simulate gravity — server owns Z. Force
// Contact + OnWalkable + Active so apply_current_movement
// writes velocity through every tick (the gate in
// MotionInterpreter.apply_current_movement is
// PhysicsObj.OnWalkable).
State = AcDream.Core.Physics.PhysicsStateFlags.ReportCollisions,
TransientState = AcDream.Core.Physics.TransientStateFlags.Contact
| AcDream.Core.Physics.TransientStateFlags.OnWalkable
| AcDream.Core.Physics.TransientStateFlags.Active,
// R4-V5 door-swing fix: a RemoteMotion exists only for a
// WORLD entity — retail's physics_obj->cell is non-null the
// moment the object is placed. Without this, the interp's
// detached-object guard stripped every dispatched
// transition link (see PhysicsBody.InWorld).
InWorld = true,
};
// R5-V5: the interp is owned by the MovementManager facade
// (retail CPhysicsObj::movement_manager -> motion_interpreter).
// acdream constructs it eagerly here — retail's lazy-create
// window is never observable (see MovementManager's class doc).
Movement = new AcDream.Core.Physics.Motion.MovementManager(
new AcDream.Core.Physics.MotionInterpreter(Body)
{
// R4-V5 #160 fix: retail remotes carry a weenie whose
// InqRunRate FAILS, landing apply_run_to_command on
// my_run_rate (the M13 wire feed). A null weenie took the
// degenerate 1.0 branch — observer-side run movetos played
// and moved in slow motion. See RemoteWeenie.
WeenieObj = new AcDream.Core.Physics.RemoteWeenie(),
});
}
public void BindCanonicalCell(Func<uint> read, Action<uint> write)
{
_canonicalCellReader = read ?? throw new ArgumentNullException(nameof(read));
_canonicalCellWriter = write ?? throw new ArgumentNullException(nameof(write));
}
}

View file

@ -1,4 +1,4 @@
using RemoteMotion = AcDream.App.Rendering.GameWindow.RemoteMotion;
using RemoteMotion = AcDream.App.Physics.RemoteMotion;
using LiveEntityAnimationState = AcDream.App.Rendering.LiveEntityAnimationState;
namespace AcDream.App.Physics;

View file

@ -1,4 +1,5 @@
using AcDream.Core.Plugins;
using AcDream.App.Physics;
using AcDream.App.World;
using AcDream.Content;
using DatReaderWriter;
@ -421,307 +422,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
private AcDream.App.World.LiveEntityRuntime? _liveEntities;
private AcDream.App.World.LiveEntityLivenessController? _liveEntityLiveness;
/// <summary>
/// Per-remote-entity physics + motion stack — verbatim application of
/// retail's client-side motion pipeline to every remote. Mirrors
/// retail <c>FUN_00515020</c> <c>update_object</c> → <c>FUN_00513730</c>
/// <c>UpdatePositionInternal</c> → <c>FUN_005111D0</c>
/// <c>UpdatePhysicsInternal</c>, and ACE's <c>PhysicsObj.cs</c> port.
///
/// <para>
/// Retail has NO special "interpolator" for remote entities — it runs
/// the full motion state machine on every entity, local or remote,
/// and reconciles via hard-snap on UpdatePosition. This class simply
/// pairs a <see cref="AcDream.Core.Physics.PhysicsBody"/> with its
/// <see cref="AcDream.Core.Physics.MotionInterpreter"/> so each
/// remote gets the same treatment as the local player.
/// </para>
/// </summary>
internal sealed class RemoteMotion :
AcDream.App.World.ILiveEntityRemotePlacementRuntime
{
public AcDream.Core.Physics.PhysicsBody Body { get; }
AcDream.Core.Physics.PhysicsBody AcDream.App.World.ILiveEntityRemoteMotionRuntime.Body => Body;
/// <summary>R5-V5: retail <c>CPhysicsObj::movement_manager</c> — the
/// ONE per-entity owner of the interp + moveto pair (acclient.h
/// <c>/* 3463 */</c>). Constructed here with the interp; the
/// MoveToManager side arrives via <c>MoveToFactory</c> +
/// <c>MakeMoveToManager()</c> in EnsureRemoteMotionBindings.
/// <see cref="Motion"/>/<see cref="MoveTo"/> below are views of its
/// children (retail <c>get_minterp</c>-style access), kept so the
/// dozens of existing call sites read unchanged.</summary>
public AcDream.Core.Physics.Motion.MovementManager Movement;
public AcDream.Core.Physics.MotionInterpreter Motion => Movement.Minterp;
/// <summary>R3-W4: the persistent per-remote dispatch sink (created
/// once by EnsureRemoteMotionBindings; also bound as
/// Motion.DefaultSink so LeaveGround/HitGround re-applies dispatch
/// cycles through the motion-table backend).</summary>
public AcDream.Core.Physics.Motion.MotionTableDispatchSink? Sink;
/// <summary>Last UpdatePosition timestamp — drives body.update_object sub-stepping.</summary>
public double LastServerPosTime;
/// <summary>Last known server position — kept for diagnostics / HUD.</summary>
public System.Numerics.Vector3 LastServerPos;
/// <summary>
/// #184: the body position at which this remote's collision SHADOW was
/// last (re-)registered. The per-tick shadow-follows-resolved sync
/// (<c>SyncRemoteShadowToBody</c>) skips re-flooding when the body has
/// not moved &gt; ε since this — a resting/idle-town crowd doesn't churn
/// the registry, while a moving/de-overlapping crowd re-registers every
/// tick. Sentinel <c>Zero</c> forces the first sync.
/// </summary>
public System.Numerics.Vector3 LastShadowSyncPos;
/// <summary>
/// Complete root orientation paired with <see cref="LastShadowSyncPos"/>.
/// Multipart and off-centre Setup collision geometry must be re-flooded
/// when the object turns in place, not only when its origin translates.
/// The default zero quaternion is the first-sync sentinel.
/// </summary>
public System.Numerics.Quaternion LastShadowSyncOrientation;
/// <summary>
/// Latest server-authoritative velocity for NPC/monster smoothing.
/// Prefer the HasVelocity vector from UpdatePosition; when ACE omits
/// it for a server-controlled creature, derive it from consecutive
/// authoritative positions instead of guessing from player RUM state.
/// </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. R5-V5:
/// owned by <see cref="Movement"/>; this is the child view.</summary>
public AcDream.Core.Physics.Motion.MoveToManager? MoveTo => Movement.MoveTo;
// R5-V2: the entity's CPhysicsObj stand-in — owns the TargetManager
// voyeur system (retail CPhysicsObj::target_manager). Replaces the
// AP-79 TrackedTarget* poll fields: the MoveToManager's set_target/
// clear_target/quantum seams route here, HandleTargetting ticks per
// frame, and OTHER entities resolve this one via GameWindow._physicsHosts.
public EntityPhysicsHost? Host;
/// <summary>
/// True while a server MoveToObject/MoveToPosition packet is the
/// active locomotion source. Retail runs these through MoveToManager
/// and CMotionInterp; the per-tick remote driver consults this to
/// decide whether to feed body steering through
/// <see cref="AcDream.Core.Physics.RemoteMoveToDriver"/> instead of
/// the InterpretedMotionState path.
/// </summary>
public bool ServerMoveToActive;
/// <summary>
/// True once a MoveTo packet's full path payload (Origin + thresholds)
/// has been parsed and the world-converted destination is stored on
/// <see cref="MoveToDestinationWorld"/>. Cleared on arrival or when
/// the next non-MoveTo UpdateMotion replaces the locomotion source.
/// Phase L.1c (2026-04-28).
/// </summary>
public bool HasMoveToDestination;
/// <summary>
/// World-space destination from the most recent MoveTo packet's
/// <c>Origin</c> field, converted via the same landblock-grid
/// arithmetic <c>OnLivePositionUpdated</c> uses.
/// </summary>
public System.Numerics.Vector3 MoveToDestinationWorld;
/// <summary>
/// <c>min_distance</c> from the MoveTo packet's MovementParameters.
/// Used by <see cref="AcDream.Core.Physics.RemoteMoveToDriver"/> as
/// the chase-arrival threshold per retail
/// <c>MoveToManager::HandleMoveToPosition</c>.
/// </summary>
public float MoveToMinDistance;
/// <summary>
/// <c>distance_to_object</c> from the MoveTo packet. Reserved for
/// the flee branch (<c>move_away</c>); chase uses
/// <see cref="MoveToMinDistance"/>.
/// </summary>
public float MoveToDistanceToObject;
/// <summary>
/// True if MovementParameters bit 9 (<c>move_towards</c>, mask
/// <c>0x200</c>) is set on the active packet — i.e. this is a
/// chase. False = flee (<c>move_away</c>) or static target.
/// </summary>
public bool MoveToMoveTowards;
/// <summary>
/// Seconds-since-epoch timestamp of the most recent MoveTo packet
/// for this entity. Used by the per-tick driver to give up
/// steering when no refresh has arrived for
/// <see cref="AcDream.Core.Physics.RemoteMoveToDriver.StaleDestinationSeconds"/>
/// — typically because the entity left our streaming view and
/// the server stopped broadcasting its MoveTo updates.
/// </summary>
public double LastMoveToPacketTime;
/// <summary>
/// Full 32-bit cell ID from the latest UpdatePosition. High 16 bits
/// = landblock (LBx,LBy), low 16 bits = cell index (outdoor 0x0001-
/// 0x0040, indoor 0x0100+). Fed into
/// <see cref="AcDream.Core.Physics.PhysicsEngine.ResolveWithTransition"/>
/// so the retail sphere-sweep can look up the right terrain/EnvCell
/// polygons for each remote's per-frame motion. Zero until the first
/// UP lands, which disables the transition step for that frame
/// (Euler alone, matching pre-wire behavior).
/// </summary>
private uint _cellId;
private Func<uint>? _canonicalCellReader;
private Action<uint>? _canonicalCellWriter;
/// <summary>
/// Canonical full cell for this one live CPhysicsObj. Ordinary remotes
/// retain the local backing value. A MovementManager attached to an
/// existing projectile delegates reads and writes to
/// <see cref="LiveEntityRuntime"/>, so projectile prediction,
/// Movement packets, and spatial rebucketing cannot develop competing
/// cell identities.
/// </summary>
public uint CellId
{
get => _canonicalCellReader?.Invoke() ?? _cellId;
set
{
_cellId = value;
_canonicalCellWriter?.Invoke(value);
}
}
System.Numerics.Vector3 AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastServerPosition
{
get => LastServerPos;
set => LastServerPos = value;
}
double AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastServerPositionTime
{
get => LastServerPosTime;
set => LastServerPosTime = value;
}
System.Numerics.Vector3 AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastShadowSyncPosition
{
get => LastShadowSyncPos;
set => LastShadowSyncPos = value;
}
System.Numerics.Quaternion AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastShadowSyncOrientation
{
get => LastShadowSyncOrientation;
set => LastShadowSyncOrientation = value;
}
bool AcDream.App.World.ILiveEntityRemotePlacementRuntime.Airborne
{
get => Airborne;
set => Airborne = value;
}
void AcDream.App.World.ILiveEntityRemotePlacementRuntime.HitGround() =>
Movement.HitGround();
void AcDream.App.World.ILiveEntityRemotePlacementRuntime.LeaveGround() =>
Motion.LeaveGround();
/// <summary>
/// K-fix9 (2026-04-26): true while the remote is airborne (jump
/// arc in flight). Set when a 0xF74E VectorUpdate arrives with
/// non-trivial +Z velocity; cleared when the next UpdatePosition
/// snaps to a new ground location. While true, the per-tick
/// remote update SKIPS the "force OnWalkable + apply_current_movement"
/// step that would otherwise stomp the body's Z velocity each
/// frame, AND enables gravity so the parabolic arc actually plays
/// out between server snaps.
/// </summary>
public bool Airborne;
/// <summary>
/// Per-remote position-waypoint queue + catch-up math (retail's
/// CPhysicsObj::InterpolateTo + InterpolationManager::adjust_offset).
/// Drives per-tick body translation for grounded player remotes
/// via <see cref="Position"/>.
/// </summary>
public AcDream.Core.Physics.InterpolationManager Interp { get; } =
new AcDream.Core.Physics.InterpolationManager();
/// <summary>
/// Per-frame combiner for animation root motion + InterpolationManager
/// correction. Mirrors retail UpdatePositionInternal @ 0x00512c30:
/// queue catch-up REPLACES anim when active; anim stands when queue
/// is idle.
/// </summary>
public AcDream.Core.Physics.RemoteMotionCombiner Position { get; } =
new AcDream.Core.Physics.RemoteMotionCombiner();
/// <summary>Reusable complete delta frame shared by interpolation,
/// Sticky/Constraint, and the final body composition.</summary>
public AcDream.Core.Physics.Motion.MotionDeltaFrame PositionManagerDeltaScratch { get; } =
new AcDream.Core.Physics.Motion.MotionDeltaFrame();
/// <summary>
/// Diagnostic-only (gated on <c>ACDREAM_REMOTE_VEL_DIAG=1</c>): the
/// previous UpdatePosition's world position + timestamp. The per-tick
/// path compares the server's broadcast pace with the literal root
/// displacement emitted by <c>CSequence::update</c>. The old inferred
/// walk/run velocity was the source of the periodic overshoot this
/// diagnostic was introduced to find.
/// </summary>
public System.Numerics.Vector3 PrevServerPos;
public double PrevServerPosTime;
public double LastOmegaDiagLogTime;
/// <summary>
/// Diagnostic-only: maximum scaled CSequence root-motion speed
/// observed since the last UpdatePosition arrival. The next UP
/// compares it with the server's observed pace. Reset on each UP.
/// </summary>
public float MaxRootMotionSpeedSinceLastUP;
public RemoteMotion(AcDream.Core.Physics.PhysicsBody? sharedBody = null)
{
Body = sharedBody ?? new AcDream.Core.Physics.PhysicsBody
{
// Remotes don't simulate gravity — server owns Z. Force
// Contact + OnWalkable + Active so apply_current_movement
// writes velocity through every tick (the gate in
// MotionInterpreter.apply_current_movement is
// PhysicsObj.OnWalkable).
State = AcDream.Core.Physics.PhysicsStateFlags.ReportCollisions,
TransientState = AcDream.Core.Physics.TransientStateFlags.Contact
| AcDream.Core.Physics.TransientStateFlags.OnWalkable
| AcDream.Core.Physics.TransientStateFlags.Active,
// R4-V5 door-swing fix: a RemoteMotion exists only for a
// WORLD entity — retail's physics_obj->cell is non-null the
// moment the object is placed. Without this, the interp's
// detached-object guard stripped every dispatched
// transition link (see PhysicsBody.InWorld).
InWorld = true,
};
// R5-V5: the interp is owned by the MovementManager facade
// (retail CPhysicsObj::movement_manager -> motion_interpreter).
// acdream constructs it eagerly here — retail's lazy-create
// window is never observable (see MovementManager's class doc).
Movement = new AcDream.Core.Physics.Motion.MovementManager(
new AcDream.Core.Physics.MotionInterpreter(Body)
{
// R4-V5 #160 fix: retail remotes carry a weenie whose
// InqRunRate FAILS, landing apply_run_to_command on
// my_run_rate (the M13 wire feed). A null weenie took the
// degenerate 1.0 branch — observer-side run movetos played
// and moved in slow motion. See RemoteWeenie.
WeenieObj = new AcDream.Core.Physics.RemoteWeenie(),
});
}
public void BindCanonicalCell(Func<uint> read, Action<uint> write)
{
_canonicalCellReader = read ?? throw new ArgumentNullException(nameof(read));
_canonicalCellWriter = write ?? throw new ArgumentNullException(nameof(write));
}
}
/// <summary>Soft-snap decay rate (1/sec). At this rate the residual
/// halves every 1/rate seconds. 8.0 → ~100ms half-life, so even a
/// 2m residual fades within ~300ms without visible snap.</summary>

View file

@ -5,7 +5,7 @@ using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using AcDream.Core.World;
using DatReaderWriter.Types;
using RemoteMotion = AcDream.App.Rendering.GameWindow.RemoteMotion;
using RemoteMotion = AcDream.App.Physics.RemoteMotion;
namespace AcDream.App.Rendering;