using AcDream.App.Combat;
using AcDream.App.Input;
using AcDream.App.Interaction;
using AcDream.App.Net;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
using AcDream.App.Update;
using AcDream.App.World;
using AcDream.Content;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Items;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Session;
using AcDream.Core.Selection;
using AcDream.Core.World;
using DatReaderWriter;
namespace AcDream.App.Physics;
///
/// Update-thread owner of retail SmartBox's accepted Movement, Vector, State,
/// and Position presentation transactions. Identity and timestamp authority
/// remain canonical in ; this controller only
/// routes an accepted exact incarnation into its App-layer owners.
///
internal sealed class LiveEntityNetworkUpdateController
: ILiveEntityNetworkUpdateSink,
ILiveEntitySameGenerationUpdateSink,
ILocalPlayerLandblockSource
{
private readonly LiveEntityRuntime _liveEntities;
private readonly ClientObjectTable _objects;
private readonly LiveEntityHydrationController _liveEntityHydration;
private readonly EntityEffectController _entityEffects;
private readonly LiveEntityPresentationController _liveEntityPresentation;
private readonly LiveEntityLightController _liveEntityLights;
private readonly EquippedChildRenderController _equippedChildRenderer;
private readonly ProjectileController _projectileController;
private readonly RemoteTeleportController _remoteTeleportController;
private readonly LiveEntityAnimationRuntimeView _animatedEntities;
private readonly RemoteMovementObservationTracker _remoteMovementObservations;
private readonly RemotePhysicsUpdater _remotePhysicsUpdater;
private readonly RemoteInboundMotionDispatcher _remoteInboundMotion;
private readonly LiveEntityMotionRuntimeController _motionRuntime;
private readonly PhysicsEngine _physicsEngine;
private readonly IDatReaderWriter _dats;
private readonly IAnimationLoader _animLoader;
private readonly RuntimeCombatTargetState? _combatTargetController;
private readonly LiveWorldOriginState _origin;
private readonly AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink
_localPlayerTeleport;
private readonly IRuntimeLocalPlayerControllerSource _playerControllerSource;
private readonly LocalPlayerOutboundController _localPlayerOutbound;
private readonly ILocalPlayerPhysicsHostSource _playerHostSource;
private readonly ILocalPlayerIdentitySource _playerIdentity;
private readonly IPhysicsScriptTimeSource _gameTime;
private readonly ILiveWorldSessionSource _session;
private readonly LiveEntityInboundAuthorityGate _authorityGate;
private readonly IMovementTruthDiagnosticSink _movementTruthDiagnostics;
private readonly InventoryWorldDropProjectionController?
_worldDropProjection;
private readonly RuntimeAcceptedPositionDriveController _acceptedPositionDrive;
///
/// C4 route 4b-2: the Runtime-owned remote placement seam. Route 4b-1
/// landed it with no production caller; the remote far snap
/// (SetPositionSimple, player_distance >= 96 m) is its
/// first, so its ownership ledger stops being tautologically zero here.
///
private readonly RuntimeRemotePlacementDriveController _remotePlacementDrive;
private PlayerMovementController? _playerController => _playerControllerSource.Controller;
private EntityPhysicsHost? _playerHost => _playerHostSource.Host;
private uint _playerServerGuid => _playerIdentity.ServerGuid;
private double _physicsScriptGameTime => _gameTime.CurrentScriptTime;
internal uint? LastLivePlayerLandblockId =>
_authorityGate.LastLivePlayerLandblockId;
uint? ILocalPlayerLandblockSource.LastKnownLandblockId =>
LastLivePlayerLandblockId;
public LiveEntityNetworkUpdateController(
LiveEntityRuntime liveEntities,
ClientObjectTable objects,
LiveEntityHydrationController liveEntityHydration,
EntityEffectController entityEffects,
LiveEntityPresentationController liveEntityPresentation,
LiveEntityLightController liveEntityLights,
EquippedChildRenderController equippedChildRenderer,
ProjectileController projectileController,
RemoteTeleportController remoteTeleportController,
LiveEntityAnimationRuntimeView animatedEntities,
RemoteMovementObservationTracker remoteMovementObservations,
RemotePhysicsUpdater remotePhysicsUpdater,
RemoteInboundMotionDispatcher remoteInboundMotion,
LiveEntityMotionRuntimeController motionRuntime,
PhysicsEngine physicsEngine,
IDatReaderWriter dats,
IAnimationLoader animLoader,
RuntimeCombatTargetState? combatTargetController,
LiveWorldOriginState origin,
AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink localPlayerTeleport,
IRuntimeLocalPlayerControllerSource playerControllerSource,
LocalPlayerOutboundController localPlayerOutbound,
ILocalPlayerPhysicsHostSource playerHostSource,
ILocalPlayerIdentitySource playerIdentity,
IPhysicsScriptTimeSource gameTime,
ILiveWorldSessionSource session,
Action publishTimestamps,
IMovementTruthDiagnosticSink movementTruthDiagnostics,
RuntimeAcceptedPositionDriveController acceptedPositionDrive,
RuntimeRemotePlacementDriveController remotePlacementDrive,
InventoryWorldDropProjectionController? worldDropProjection = null)
{
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
_liveEntityHydration = liveEntityHydration ?? throw new ArgumentNullException(nameof(liveEntityHydration));
_entityEffects = entityEffects ?? throw new ArgumentNullException(nameof(entityEffects));
_liveEntityPresentation = liveEntityPresentation ?? throw new ArgumentNullException(nameof(liveEntityPresentation));
_liveEntityLights = liveEntityLights ?? throw new ArgumentNullException(nameof(liveEntityLights));
_equippedChildRenderer = equippedChildRenderer ?? throw new ArgumentNullException(nameof(equippedChildRenderer));
_projectileController = projectileController ?? throw new ArgumentNullException(nameof(projectileController));
_remoteTeleportController = remoteTeleportController ?? throw new ArgumentNullException(nameof(remoteTeleportController));
_animatedEntities = animatedEntities ?? throw new ArgumentNullException(nameof(animatedEntities));
_remoteMovementObservations = remoteMovementObservations ?? throw new ArgumentNullException(nameof(remoteMovementObservations));
_remotePhysicsUpdater = remotePhysicsUpdater ?? throw new ArgumentNullException(nameof(remotePhysicsUpdater));
_remoteInboundMotion = remoteInboundMotion ?? throw new ArgumentNullException(nameof(remoteInboundMotion));
_motionRuntime = motionRuntime ?? throw new ArgumentNullException(nameof(motionRuntime));
_physicsEngine = physicsEngine ?? throw new ArgumentNullException(nameof(physicsEngine));
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
_animLoader = animLoader ?? throw new ArgumentNullException(nameof(animLoader));
_combatTargetController = combatTargetController;
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
_localPlayerTeleport = localPlayerTeleport
?? throw new ArgumentNullException(nameof(localPlayerTeleport));
_playerControllerSource = playerControllerSource ?? throw new ArgumentNullException(nameof(playerControllerSource));
_localPlayerOutbound = localPlayerOutbound
?? throw new ArgumentNullException(nameof(localPlayerOutbound));
_playerHostSource = playerHostSource ?? throw new ArgumentNullException(nameof(playerHostSource));
_playerIdentity = playerIdentity ?? throw new ArgumentNullException(nameof(playerIdentity));
_gameTime = gameTime ?? throw new ArgumentNullException(nameof(gameTime));
_session = session ?? throw new ArgumentNullException(nameof(session));
_authorityGate = new LiveEntityInboundAuthorityGate(
liveEntities,
publishTimestamps);
_movementTruthDiagnostics = movementTruthDiagnostics
?? throw new ArgumentNullException(nameof(movementTruthDiagnostics));
_acceptedPositionDrive = acceptedPositionDrive
?? throw new ArgumentNullException(nameof(acceptedPositionDrive));
_remotePlacementDrive = remotePlacementDrive
?? throw new ArgumentNullException(nameof(remotePlacementDrive));
_worldDropProjection = worldDropProjection;
}
internal void ResetSessionState() => _authorityGate.ResetSessionState();
private static bool IsPlayerGuid(uint guid) =>
(guid & 0xFF000000u) == 0x50000000u;
private static bool IsDoorName(string? name) => name == "Door";
///
/// #270 (2026-07-30): retail spawns run the placement transition
/// (CPhysicsObj::SetPosition → SetPositionInternal
/// 0x00515330), which establishes CONTACT/ON_WALKABLE from the floor the
/// creature stands on. A raw position seed leaves the fresh body
/// airborne-flagged, and contact_allows_move (0x00528dd0) then
/// silently refuses every action animation — a spawned-standing monster's
/// attack swings never played until it first moved (the [MT-FAIL]
/// Falling-substitution spam was the same body state surfacing through
/// apply_interpreted_movement). Mirrors
/// 's commit with spawn-shaped
/// inputs (no prior contact).
///
private void SeedRemoteSpawnPlacement(
RemoteMotion remote,
uint serverGuid,
AcDream.Core.World.WorldEntity entity,
System.Numerics.Vector3 worldPos,
uint cellId)
{
var (radius, height) = _motionRuntime.GetSetupCylinder(serverGuid, entity);
if (radius < 0.05f)
{
radius = 0.48f;
height = 1.835f;
}
var moverFlags = IsPlayerGuid(serverGuid)
? AcDream.Core.Physics.ObjectInfoState.IsPlayer
| AcDream.Core.Physics.ObjectInfoState.EdgeSlide
: AcDream.Core.Physics.ObjectInfoState.EdgeSlide;
// Retail's spawn contact comes from the FIRST GRAVITY FRAME, not the
// placement itself: every retail CPhysicsObj simulates, so a freshly
// placed creature falls the few centimetres onto the floor and the
// transition's touch grants the contact plane. Our remotes reach that
// state SLOWLY or not at all — the DR tick only sweeps when the
// composed candidate actually moved, so a remote spawned exactly on
// its floor never sweeps and a remote spawned above one needs however
// many ticks gravity takes to close the gap. The settle is therefore
// compressed here: a short downward sweep from the server position.
// Its touch handler produces exactly the state retail's first frame
// would (position snapped onto the floor, contact plane +
// CONTACT/ON_WALKABLE committed below). A sweep that finds no floor
// (true airborne spawn) leaves the body airborne.
//
// Bug B (2026-08-04) weakened — but did not remove — the reason this
// exists. The deleted per-tick `Contact | OnWalkable` forge used to
// make a stationary remote's transients permanent, so a contact-free
// remote could NEVER settle on its own; now gravity survives and one
// WILL settle by itself after a few ticks of falling. This compressed
// settle is what keeps it from spending those ticks visibly
// contact-free, which is the #270 window (`contact_allows_move`
// @0x00528dd0 refuses action animations without both transients).
if (!AcDream.Core.Physics.SpawnPlacementSettler.TrySettle(
_physicsEngine,
remote.Body,
worldPos,
cellId,
radius,
height,
moverFlags,
entity.Id,
remote.Movement.HitGround,
remote.Motion.LeaveGround))
{
return; // no floor within reach — stays airborne like retail's fall
}
remote.Airborne = !remote.Body.OnWalkable;
}
private bool RunRemoteTeleportHook(
uint serverGuid,
uint localEntityId,
Func isCurrent)
{
_liveEntities.TryGetRemoteMotionRuntime(
serverGuid,
out IRuntimeRemoteMotion? remoteRuntime);
RemoteMotion? remote = remoteRuntime as RemoteMotion;
EntityPhysicsHost? host =
_liveEntities.TryGetPhysicsHost(serverGuid, out var registered)
? registered as EntityPhysicsHost
: null;
return RemoteTeleportHook.Execute(
new RemoteTeleportHookActions(
CancelMoveTo: error => remote?.Movement.CancelMoveTo(error),
UnStick: () => host?.PositionManager.UnStick(),
StopInterpolating: () => remote?.Interp.Clear(),
UnConstrain: () => host?.PositionManager.UnConstrain(),
NotifyTeleported: () => host?.NotifyTeleported(),
ReportCollisionEnd: () => _physicsEngine.ShadowObjects.Suspend(localEntityId)),
isCurrent);
}
public void ApplySameGeneration(
SameGenerationCreateObjectEvents refresh) =>
LiveEntitySameGenerationUpdateRouter.Apply(refresh, this);
void ILiveEntitySameGenerationUpdateSink.OnDescription(
uint ownerGuid,
PhysicsSpawnData description)
{
if (_liveEntities.TryGetEffectProfile(
ownerGuid,
out var effectProfile)
&& effectProfile is EntityEffectProfile liveProfile)
{
liveProfile.ApplyNetworkDescription(description);
_entityEffects.OnLiveEntityDescriptionChanged(ownerGuid);
}
}
void ILiveEntitySameGenerationUpdateSink.OnAppearance(
AcDream.Core.Net.Messages.ObjDescEvent.Parsed appearance) =>
_liveEntityHydration.OnAppearance(appearance);
void ILiveEntitySameGenerationUpdateSink.OnParent(CreateParentUpdate parent) =>
_liveEntityHydration.OnCreateParentAccepted(parent);
void ILiveEntitySameGenerationUpdateSink.OnPosition(
WorldSession.EntityPositionUpdate position) => OnPosition(position);
void ILiveEntitySameGenerationUpdateSink.OnPickup(
AcDream.Core.Net.Messages.PickupEvent.Parsed pickup) =>
_liveEntityHydration.OnPickup(pickup);
void ILiveEntitySameGenerationUpdateSink.OnMovement(
WorldSession.EntityMotionUpdate movement) => OnMotion(movement);
void ILiveEntitySameGenerationUpdateSink.OnState(
AcDream.Core.Net.Messages.SetState.Parsed state) => OnState(state);
void ILiveEntitySameGenerationUpdateSink.OnVector(
AcDream.Core.Net.Messages.VectorUpdate.Parsed vector) => OnVector(vector);
public void OnMotion(AcDream.Core.Net.WorldSession.EntityMotionUpdate update)
{
// L.2g S1 (DEV-6): retail staleness gate — BEFORE any state mutation.
// Retail drops stale/duplicate/superseded movement events at
// DispatchSmartBoxEvent (INSTANCE_TS, pseudo-C:357214) +
// CPhysics::SetObjectMovement (MOVEMENT_TS strictly-newer +
// SERVER_CONTROLLED_MOVE_TS, 0x00509690). Without this, a reordered
// straggler re-applies an old gait or un-stops a stop.
bool retainPayload = update.Guid != _playerServerGuid || !update.IsAutonomous;
if (!_authorityGate.TryAcceptMotion(
update,
retainPayload,
out AcceptedMotionNetworkUpdate accepted,
out bool timestampAccepted))
{
if (!timestampAccepted
&& (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1"
|| Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
)
{
Console.WriteLine(
$"[UM_STALE] guid={update.Guid:X8} inst={update.InstanceSequence} "
+ $"mov={update.MovementSequence} sc={update.ServerControlSequence} dropped");
}
return;
}
// R4-V5 (pin P1): retail CPhysics::SetObjectMovement's autonomous
// gate (0x00509690 @0050972e, raw 271370-271431) — a movement event
// whose wire autonomous byte is set is DROPPED ENTIRELY (no state
// application, no interrupt) when the addressed object IsThePlayer.
// ACE reflects the client's own outbound MoveToState back to the
// sender with IsAutonomous=1 hardcoded (MovementData.cs:162 +
// Player_Networking.cs:365) and retail never lets that echo reach
// unpack_movement — which is what makes the unconditional
// unpack-head interrupt in the player branch below safe against
// ACE. Order matches retail: the sequence gates above run FIRST.
// last_move_was_autonomous is NOT stored for dropped events (stored
// only on the unpack path). This retires the row-less "don't cancel
// on non-MoveTo UM" adaptation that lived here pre-V5 (its causal
// story was stale — V0-pins.md P1). Run-rate sync is re-anchored to
// retail's own feeds: PlayerDescription skills (SetCharacterSkills,
// K-fix7) + the mt-6/7 my_run_rate wire write below (M13) — the
// former ApplyServerRunRate echo tap is deleted, not gated.
LiveEntityRecord acceptedMotionRecord = accepted.Record;
ulong acceptedMovementAuthorityVersion =
accepted.MovementAuthorityVersion;
ulong acceptedMovementVelocityAuthorityVersion =
accepted.VelocityAuthorityVersion;
if (!_liveEntities.TryGetWorldEntity(update.Guid, out var entity)) return;
if (!_animatedEntities.TryGetValue(entity.Id, out var ae))
{
DispatchRemoteInboundMotion(
update,
entity,
ae: null,
acceptedMotionRecord,
acceptedMovementAuthorityVersion,
acceptedMovementVelocityAuthorityVersion);
return;
}
if (_dats is null) return;
// Re-resolve using the new stance/command. Keep the setup and
// motion-table we already know about — the server's motion
// updates override state within the same table, not swap tables.
//
// IMPORTANT: stance and command are BOTH optional. Remote-player
// autonomous broadcasts frequently set only one flag (e.g. just
// ForwardCommand) with currentStyle=0x0000 meaning "no stance
// change — keep current." Treating stance=0 as "default stance"
// drops the real state; instead we preserve the sequencer's
// current style.
ushort stance = update.MotionState.Stance;
ushort? command = update.MotionState.ForwardCommand;
// A.1 (Commit A.1 2026-05-03): UM_RAW — every inbound UM, one line,
// gated on ACDREAM_REMOTE_VEL_DIAG=1. Skips the local player. Tells
// us the actual UM arrival rate per remote and which fields are set
// on each. The bug-suspect is "ACE sends UMs without ForwardCommand
// bit during running, our picker resolves to Ready, SetCycle(Ready)
// resets the cycle". This diag lets us count how often that happens.
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1"
&& update.Guid != _playerServerGuid)
{
string cmdStrRaw = command.HasValue ? $"0x{command.Value:X4}" : "null";
string sideStr = update.MotionState.SideStepCommand is { } s ? $"0x{s:X4}" : "null";
string turnStr = update.MotionState.TurnCommand is { } t ? $"0x{t:X4}" : "null";
string fwdSpdStr = update.MotionState.ForwardSpeed is { } fs ? $"{fs:F2}" : "null";
uint seqMot = ae.Sequencer?.CurrentMotion ?? 0;
System.Console.WriteLine(
$"[UM_RAW] guid={update.Guid:X8} stance=0x{stance:X4} fwd={cmdStrRaw} fwdSpd={fwdSpdStr} "
+ $"side={sideStr} turn={turnStr} mt=0x{update.MotionState.MovementType:X2} "
+ $"isMoveTo={update.MotionState.IsServerControlledMoveTo} "
+ $"seq.CurrentMotion=0x{seqMot:X8}");
}
// Diagnostic: dump every inbound UpdateMotion so we can trace why
// remote chars don't transition off RunForward when they stop.
// Enable with ACDREAM_DUMP_MOTION=1.
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1"
&& update.Guid != _playerServerGuid)
{
string cmdStr = command.HasValue ? $"0x{command.Value:X4}" : "null";
float spd = update.MotionState.ForwardSpeed
?? ((update.MotionState.MoveToSpeed ?? 0f)
* (update.MotionState.MoveToRunRate ?? 0f));
uint seqStyle = ae.Sequencer?.CurrentStyle ?? 0;
uint seqMotion = ae.Sequencer?.CurrentMotion ?? 0;
Console.WriteLine(
$"UM guid=0x{update.Guid:X8} mt=0x{update.MotionState.MovementType:X2} stance=0x{stance:X4} cmd={cmdStr} spd={spd:F2} " +
$"| seq now style=0x{seqStyle:X8} motion=0x{seqMotion:X8}");
}
// Per-Door UM dispatch trail; grep [door-cycle] in launch.log to verify door animation.
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeBuildingEnabled
&& IsDoorName(_objects.Get(update.Guid)?.Name))
{
Console.WriteLine(System.FormattableString.Invariant(
$"[door-cycle] guid=0x{update.Guid:X8} stance=0x{stance:X4} cmd=0x{(command ?? 0u):X4}"));
}
// ── Sequencer path (preferred) ──────────────────────────────────
// Call SetCycle directly. The sequencer already handles:
// - left→right / backward→forward remapping via adjust_motion
// - style and motion as u32 MotionCommand values
// - fast-path for identical state
//
// When the server omits a field (stance flag not set, or command
// flag not set), "no change" means we must preserve the sequencer's
// current state, NOT fall back to a table default.
if (ae.Sequencer is not null)
{
uint fullStyle = stance != 0
? (0x80000000u | (uint)stance)
: ae.Sequencer.CurrentStyle;
// ACE's stop signal: ForwardCommand flag CLEARED on the wire.
// Per ACE InterpretedMotionState(MovementData) ctor + BuildMovementFlags,
// when the player releases keys the InterpretedMotionState has
// ForwardCommand = Invalid (default) and BuildMovementFlags doesn't
// set bit 0x02 — so the field is absent. Retail's decompiled
// handler (FUN_005295D0 → FUN_0051F260 @ chunk_00510000.c:13957)
// bulk-copies Invalid/0 into the physics obj, which StopCompletely
// treats as "return to style default (Ready)."
//
// command == null → retail stop signal → Ready
// command.Value == 0 → explicit 0 (rare) → Ready
// otherwise → resolve class byte and use full cmd
float speedMod = update.MotionState.ForwardSpeed ?? 1f;
uint fullMotion;
// 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;
}
else
{
// Use MotionCommandResolver to restore the proper class
// byte from the wire's 16-bit ForwardCommand.
uint resolved = AcDream.Core.Physics.MotionCommandResolver
.ReconstructFullCommand(command.Value);
fullMotion = resolved != 0
? resolved
: (ae.Sequencer.CurrentMotion & 0xFF000000u) | (uint)command.Value;
if (fullMotion == (uint)command.Value) // no class bits yet
fullMotion = 0x40000000u | (uint)command.Value;
}
// ForwardSpeed from the InterpretedMotionState (flag 0x04).
// ACE omits this field when speed == 1.0 (only sets the flag
// when ForwardSpeed != 1.0 — InterpretedMotionState.cs:101).
// So:
// - field absent → default 1.0 (normal speed)
// - field present → USE THE VALUE, including zero.
//
// Zero is a VALID stop signal: when the retail client releases
// W, ACE broadcasts WalkForward with ForwardSpeed=0 (via
// apply_run_to_command). Treating zero as "unspecified / 1.0"
// produces "slow walk that never stops" — exactly what the
// stop bug looked like.
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1"
&& update.Guid != _playerServerGuid)
Console.WriteLine(
$"UM ↳ SetCycle(style=0x{fullStyle:X8}, motion=0x{fullMotion:X8}, speed={speedMod:F2})");
// No-op if same; the sequencer's fast path guards against that.
uint priorMotion = ae.Sequencer.CurrentMotion;
// The SetObjectMovement gate above already rejects local
// autonomous echoes. A local event reaching this point is
// server-authored, so retail applies it through the interpreted
// funnel. This is especially important for attacks: ACE chooses
// the exact swing and carries it in Commands[].
if (update.Guid == _playerServerGuid)
{
// B.6 slice 1 (2026-05-14): trace inbound motion for the
// local player. One line per inbound UM, gated on
// ACDREAM_PROBE_AUTOWALK=1 (name kept through R4-V5).
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
{
string cmdHex = command.HasValue ? $"0x{command.Value:X4}" : "null";
string pathStr = update.MotionState.MoveToPath is { } p
? $"path=cell=0x{p.OriginCellId:X8},xyz=({p.OriginX:F2},{p.OriginY:F2},{p.OriginZ:F2}),minDist={p.MinDistance:F2},objDist={p.DistanceToObject:F2}"
: "path=null";
string spd = update.MotionState.ForwardSpeed is { } fs
? $"fwdSpd={fs:F2}"
: "fwdSpd=null";
string mtsSpd = update.MotionState.MoveToSpeed is { } ms
? $"mtSpd={ms:F2}"
: "mtSpd=null";
string mtsRun = update.MotionState.MoveToRunRate is { } mr
? $"mtRun={mr:F2}"
: "mtRun=null";
Console.WriteLine(System.FormattableString.Invariant(
$"[autowalk-mt] stance=0x{stance:X4} cmd={cmdHex} mt=0x{update.MotionState.MovementType:X2} isMoveTo={update.MotionState.IsServerControlledMoveTo} moveTowards={update.MotionState.MoveTowards} {pathStr} {spd} {mtsSpd} {mtsRun}"));
}
// R4-V5: retail unpack_movement dispatch for the local
// player — the SAME shape the remote branch uses below.
// Head (@300566): interrupt + unstick fire for EVERY
// movement event that reached unpack (the P1 gate above
// already dropped the autonomous echoes that would have
// made this unsafe against ACE); then types 6-9 route to
// the player's MoveToManager. mt-0 falls through to the
// interpreted-state copy below; LastMoveWasAutonomous=false
// is the local equivalent of LoseControlToServer until the
// next user-input edge takes control back.
if (_playerController is not null)
{
// P1 tail (00509730): the unpack path stores the wire
// autonomous byte BEFORE unpack_movement — always false
// here (the gate above dropped autonomous events). This
// is what routes the controller's per-tick pump (A3
// dual dispatch) to the INTERPRETED branch during a
// server moveto. LOCAL PLAYER ONLY for now: remotes'
// interps have no WeenieObj, which A3 treats as
// IsThePlayer — storing a remote player's autonomous
// byte would flip their per-tick apply onto the raw
// branch and clobber their funnel state; the remote
// store lands with real remote weenies (R5+).
_playerController.SetLastMoveWasAutonomous(update.IsAutonomous);
bool IsCurrentLocalMotion() =>
_liveEntities.IsCurrentMovementAuthority(
acceptedMotionRecord,
acceptedMovementAuthorityVersion)
&& _liveEntities.IsCurrentVelocityAuthority(
acceptedMotionRecord,
acceptedMovementVelocityAuthorityVersion)
&& ReferenceEquals(
acceptedMotionRecord.WorldEntity,
entity);
if (!IsCurrentLocalMotion())
return;
// Local and remote packets now share the literal
// MovementManager::unpack_movement funnel. Besides
// removing duplicate retail ordering, the authority
// predicate is rechecked after every callback boundary;
// a nested newer packet can never be overwritten by the
// tail of this older one.
AcDream.App.Physics.RemoteInboundMotionDispatchResult localDispatch =
_remoteInboundMotion.Apply(
update,
_playerController.Movement,
_playerController.Motion.DefaultSink,
_playerHost,
_playerController.CellId,
ae.Sequencer.CurrentMotion & 0xFF000000u,
IsCurrentLocalMotion);
if (localDispatch.Superseded
|| !IsCurrentLocalMotion())
{
return;
}
if (localDispatch.RoutedMoveTo)
{
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
{
Console.WriteLine(System.FormattableString.Invariant(
$"[autowalk-begin] mt=0x{update.MotionState.MovementType:X2} movingTo={_playerController.Movement.IsMovingTo()} type={_playerController.MoveTo?.MovementTypeState}"));
}
return;
}
if (!localDispatch.AppliedInterpretedState)
return;
fullMotion = localDispatch.CurrentForwardCommand;
}
}
else
{
// One packet owner handles both PartArray-backed remotes and
// AP-77's animation-less body fallback. GameWindow performs
// only live-owner lookup and supplies the optional sink.
AcDream.App.Physics.RemoteInboundMotionDispatchResult dispatch =
DispatchRemoteInboundMotion(
update,
entity,
ae,
acceptedMotionRecord,
acceptedMovementAuthorityVersion,
acceptedMovementVelocityAuthorityVersion);
if (dispatch.Superseded
|| dispatch.RoutedMoveTo
|| !dispatch.AppliedInterpretedState)
return;
fullMotion = dispatch.CurrentForwardCommand;
}
// Authoritative Dead motion invalidates a selected combat target.
// The controller clears shared selection, whose SelectionChanged
// consumer ports retail's post-clear AutoTarget behavior.
_combatTargetController?.OnMotionApplied(
update.Guid, ae.Sequencer.CurrentMotion);
if (!_liveEntities.IsCurrentMovementAuthority(
acceptedMotionRecord,
acceptedMovementAuthorityVersion)
|| !_liveEntities.IsCurrentVelocityAuthority(
acceptedMotionRecord,
acceptedMovementVelocityAuthorityVersion))
{
return;
}
// CRITICAL: when we enter a locomotion cycle (Walk/Run/etc),
// stamp the remote observation timestamp to "now". Without this,
// the stop-detection loop in TickAnimations sees the previous
// observation timestamp (set by the last UpdatePosition,
// often >300ms ago during idle) and fires the stop signal
// IMMEDIATELY — flipping the sequencer straight back to Ready.
// The visible symptom was "remote char never animates; just
// stands there, teleporting position every UpdatePosition."
// Fresh timestamp gives the stop-timer a full 300ms window to
// observe genuine position stagnation before reverting.
uint newLo = fullMotion & 0xFFu;
bool enteringLocomotion = newLo == 0x05 || newLo == 0x06
|| newLo == 0x07
|| newLo == 0x0F || newLo == 0x10;
uint oldLo = priorMotion & 0xFFu;
bool wasLocomotion = oldLo == 0x05 || oldLo == 0x06
|| oldLo == 0x07
|| oldLo == 0x0F || oldLo == 0x10;
if (enteringLocomotion && !wasLocomotion && update.Guid != _playerServerGuid)
{
// Reset both stop signals so stop-detection starts a fresh
// window from this transition. Without this, the entity
// starts its run animation and is instantly interrupted.
var refreshedTime = System.DateTime.UtcNow;
if (acceptedMotionRecord.ProjectionKey is { } motionKey
&& _remoteMovementObservations.TryGetValue(
motionKey,
out var prev))
{
_remoteMovementObservations[motionKey] =
(prev.Pos, refreshedTime);
}
if (_liveEntities.TryGetRemoteMotionRuntime(
update.Guid,
out IRuntimeRemoteMotion? remoteRuntime)
&& remoteRuntime is RemoteMotion dr)
dr.LastServerPosTime = (refreshedTime - System.DateTime.UnixEpoch).TotalSeconds;
}
return;
}
// ── Legacy path (entities without a sequencer) ──────────────────
// Here we DO use GetIdleCycle because the legacy tick loop needs
// a concrete Animation + frame range. Only swap when the resolver
// returns a clearly-better cycle.
var newCycle = AcDream.Core.Meshing.MotionResolver.GetIdleCycle(
ae.Setup, _dats, _animLoader!,
motionTableIdOverride: null,
stanceOverride: stance,
commandOverride: command);
bool newCycleIsGood = newCycle is not null
&& newCycle.Framerate != 0f
&& newCycle.HighFrame >= newCycle.LowFrame
&& newCycle.Animation.PartFrames.Count >= 1;
if (!newCycleIsGood) return;
ae.Animation = newCycle!.Animation;
ae.LowFrame = Math.Max(0, newCycle.LowFrame);
ae.HighFrame = Math.Min(newCycle.HighFrame, newCycle.Animation.PartFrames.Count - 1);
ae.Framerate = newCycle.Framerate;
ae.CurrFrame = ae.LowFrame;
}
///
/// Resolves one live remote owner and delegates retail's entire
/// unpack_movement body to the shared animation-optional packet
/// dispatcher. The render PartArray contributes only its optional sink.
///
private AcDream.App.Physics.RemoteInboundMotionDispatchResult
DispatchRemoteInboundMotion(
AcDream.Core.Net.WorldSession.EntityMotionUpdate update,
AcDream.Core.World.WorldEntity entity,
LiveEntityAnimationState? ae,
LiveEntityRecord acceptedRecord,
ulong acceptedMovementAuthorityVersion,
ulong acceptedVelocityAuthorityVersion)
{
if (update.Guid == _playerServerGuid)
return default;
bool IsCurrentOwner(RemoteMotion? expectedRemote = null) =>
_liveEntities is { } live
&& live.IsCurrentMovementAuthority(
acceptedRecord,
acceptedMovementAuthorityVersion)
&& live.IsCurrentVelocityAuthority(
acceptedRecord,
acceptedVelocityAuthorityVersion)
&& ReferenceEquals(acceptedRecord.WorldEntity, entity)
&& (ae is null
? acceptedRecord.AnimationRuntime is null
: ReferenceEquals(acceptedRecord.AnimationRuntime, ae))
&& (expectedRemote is null
|| ReferenceEquals(
acceptedRecord.RemoteMotionRuntime,
expectedRemote));
if (!IsCurrentOwner())
return default;
if (!_liveEntities.TryGetRemoteMotionRuntime(
update.Guid,
out IRuntimeRemoteMotion? remoteRuntime)
|| remoteRuntime is not RemoteMotion remote)
{
remote = _liveEntities.GetOrCreateRemoteMotionRuntime(
update.Guid);
remote.Body.Orientation = entity.Rotation;
remote.Body.Position = entity.Position;
}
// #270: run the retail spawn settle so the body has real ground
// contact BEFORE the funnel below dispatches this packet's actions —
// an attack swing needs contact_allows_move true to animate. Retried
// (not creation-only) while the body lacks the CONTACT transient
// (the flag contact_allows_move reads — NOT ContactPlaneValid, which
// a DR writeback can set from last-known plane data without real
// contact): creation during the login flood can precede streaming
// residency or cell hydration, and retail's own answer to "object
// addressed before its cell exists" is the CObjectMaint lost-cell
// list — park it, re-place when the cell is available. One grounded
// settle ends the retries; a genuinely airborne remote (mid-jump
// player) fails the floor probe harmlessly until it lands.
if (!remote.Body.InContact)
{
SeedRemoteSpawnPlacement(
remote,
update.Guid,
entity,
remote.Body.Position,
// #282: one owner for "which cell is this in" (see
// WorldEntity.VisibilityCellId). 0 → helper no-ops.
entity.VisibilityCellId ?? 0u);
}
if (!IsCurrentOwner(remote))
return default;
var sink = _motionRuntime.EnsureRemoteMotionBindings(remote, ae, update.Guid);
uint commandClass = ae?.Sequencer?.CurrentMotion & 0xFF000000u
?? remote.Motion.InterpretedState.ForwardCommand & 0xFF000000u;
if (commandClass == 0u)
commandClass = 0x41000000u;
AcDream.App.Physics.RemoteInboundMotionDispatchResult result =
_remoteInboundMotion.Apply(
update,
remote.Movement,
sink,
remote.Host,
remote.CellId,
commandClass,
() => IsCurrentOwner(remote));
if (result.Superseded || !IsCurrentOwner(remote))
return result with { Superseded = true };
if (result.ForwardCommandChanged)
{
if (System.Environment.GetEnvironmentVariable(
"ACDREAM_REMOTE_VEL_DIAG") == "1")
{
System.Console.WriteLine(
$"[FWD_WIRE] guid={update.Guid:X8} "
+ $"oldCmd=0x{result.PreviousForwardCommand:X8} "
+ $"newCmd=0x{result.CurrentForwardCommand:X8} "
+ $"newLow=0x{result.CurrentForwardCommand & 0xFFu:X2} "
+ $"speed={update.MotionState.ForwardSpeed ?? 1f:F3}");
}
remote.PrevServerPosTime = 0.0;
}
if (result.AppliedInterpretedState && ae is null)
{
_combatTargetController?.OnMotionApplied(
update.Guid,
result.CurrentForwardCommand);
if (!IsCurrentOwner(remote))
return result with { Superseded = true };
}
return result;
}
///
/// Phase 6.7: the server says an entity moved. Translate its new
/// landblock-local position into acdream world space (same math as
/// CreateObject hydration) and update the entity's Position/Rotation
/// in place so the next Draw picks up the new transform.
///
/// Phase B.3 extension: if the player controller is in PortalSpace and
/// this update is for our own character, detect a large position change
/// (different landblock or > 100 units distance). If detected, recenter
/// the streaming controller, resolve the new position through physics,
/// snap the player entity + controller, and return to InWorld. Also sends
/// LoginComplete so the server knows the client has loaded the destination.
///
///
/// Reports whether the exact remote component currently belongs to the
/// visible ordinary-object workset that consumes interpolation targets.
///
private bool WillAdvanceRemoteMotion(uint serverGuid, RemoteMotion remote)
{
return _liveEntities is { } runtime
&& runtime.TryGetRecord(serverGuid, out LiveEntityRecord record)
&& ReferenceEquals(record.RemoteMotionRuntime, remote)
&& (record.FinalPhysicsState
& AcDream.Core.Physics.PhysicsStateFlags.Static) == 0
&& runtime.GetRootObjectClockDisposition(serverGuid)
is AcDream.Core.Physics.RetailObjectClockDisposition.Advance
&& runtime.IsCurrentSpatialRemoteMotion(record, remote);
}
///
/// C4 route 4a: asks Runtime to classify one remote's accepted Position.
/// App contributes only retail's player_distance — the live
/// physics-controller distance, and (never a
/// fabricated Vector3.Zero) when no controller exists yet, which
/// makes Runtime decline. Callers must only invoke this for a genuinely
/// remote (never local-player) entity whose
/// remotePlacementRequired gate is already false.
///
///
/// C4 route 4b-2 review fix — this comment used to end "and leaves the
/// legacy path untouched". There is no legacy path left: the duplicated
/// App-side near/far blocks were deleted with this slice, and a declined
/// classification now takes the stated UnroutedCatchUp policy
/// (AP-137) through ApplyRemoteContactRouting's default arm. The
/// return is still "Runtime has no opinion", never
/// "rejected"; what changed is what the caller does with it.
///
///
private RuntimeAuthoritativePositionRoute? ClassifyRemoteAcceptedPosition(
AcDream.Core.Net.WorldSession.EntityPositionUpdate update,
RuntimeEntityRecord canonical,
AcDream.Core.Physics.PositionTimestampDisposition timestampDisposition,
AcceptedPhysicsTimestamps timestamps,
System.Numerics.Vector3 worldPos) =>
_liveEntities.ClassifyRemoteAcceptedPosition(
canonical,
update,
timestampDisposition,
timestamps,
_playerController is { } controller
? System.Numerics.Vector3.Distance(worldPos, controller.Position)
: null);
///
/// C4 route 4a: the generic top-of-OnPosition render-pose write and
/// its ONE suppression rule, extracted so the rule is exercised by
/// production and by test through the same entry point rather than
/// restated in a test body.
///
///
/// For the two classifications route 4a owns, the canonical body — not
/// the raw wire packet — is the only writer of the render entity: the
/// near-interpolate branch's tail syncs the entity to the resolved body,
/// and the airborne no-op writes nothing at all (retail
/// MoveOrTeleport 0x00516330 returns 0 @0x0051636D). Writing the
/// wire pose here first would be the second writer route 2's original
/// defect consisted of.
///
///
///
/// C4 route 4b-2 review fix — this comment used to end "Every other
/// classification keeps the pre-existing write, unchanged, until route
/// 4b", which is now false in both halves. The gate is still
/// OwnsSteadyState, so the FAR snap (4b-2's own arm) DOES take the
/// wire-pose write here even though it goes on to place canonically. That
/// is deliberate and is not a second writer in the route 2 sense: the far
/// arm's tail re-syncs the render entity from the RESOLVED body
/// afterwards, so this write only covers the window before the placement
/// commits, exactly as it did before the slice. Route 4b-3 revisits the
/// gate when it takes the cell-less half.
///
///
///
/// The spatial bucket transaction deliberately does NOT live behind this
/// gate: unlike route 2, neither 4a branch performs a placement, so there
/// is no committed placement receipt to project in its stead. The per-UP
/// RebucketLiveEntity is the only site that moves an ordinary
/// moving remote's draw bucket, commits its canonical FullCellId,
/// and recovers a pending bucket promotion, and it must keep running for
/// both 4a classifications.
///
///
///
/// The result is this seam's observable outcome and
/// is what the acceptance tests assert on; production does not need it.
/// Do not delete it as dead — returning nothing would leave the
/// suppression rule unobservable, which is the #292 gap this closes.
/// Returns true when the wire pose was written.
///
///
internal static bool TryApplyGenericRemoteRenderPose(
AcDream.Core.World.WorldEntity entity,
RuntimeAuthoritativePositionRoute? route,
System.Numerics.Vector3 worldPos,
uint landblockId,
System.Numerics.Quaternion rotation)
{
ArgumentNullException.ThrowIfNull(entity);
if (RuntimeRemoteSteadyStatePosition.OwnsSteadyState(route))
return false;
entity.SetPosition(worldPos);
entity.ParentCellId = landblockId;
entity.Rotation = rotation;
return true;
}
/// Which arm of the remote contact routing claimed a packet. The
/// value is the seam's observable outcome, asserted by the acceptance
/// tests; production only distinguishes
/// (which alone can be
/// re-entrant), but the finer result is what makes the PRECEDENCE and the
/// arm selection testable and must not be collapsed to a bool.
internal enum RemoteContactArm : byte
{
/// The body was airborne. Hard-snapped, exactly as before
/// route 4a. Gated on remote.Airborne ALONE — the wire contact
/// bit is never read here. The common case is the landing packet, but
/// a not-in-contact packet also reaches this arm whenever the
/// classification is one route 4a does not own (null, cell-less
/// SetPosition, or rejected), because the
/// IsAirborneNoOperation early return fires only for
/// classifications it does own. That matches pre-4a behaviour.
AirborneSnap,
/// Route 4a's near InterpolateTo branch.
SteadyStateInterpolate,
/// C4 route 4b-2: retail's far snap — StopInterpolating
/// @0x005163CB then SetPositionSimple @0x005163D9 — executed
/// through the canonical Runtime placement owner.
FarSnapPlacement,
/// The acdream-only leftover set (null, Rejected*, the
/// cell-less SetPosition half 4b-3 will own). See
/// for
/// the stated policy.
UnroutedCatchUp,
}
///
/// The complete observable outcome of one
/// call: which arm claimed the
/// packet, and — for
/// alone — what the
/// canonical Runtime placement actually did.
///
///
/// C4 route 4b-2 review fix: the placement status used to be discarded at
/// the call site (_ = placementDrive.…), which made the far arm's
/// non-commit outcomes invisible from outside and hid the freeze
/// the review found. Production still takes no DECISION from it —
/// retail's MoveOrTeleport likewise discards
/// SetPositionSimple's SetPositionError and returns 1
/// @0x005163E8 — but the value is now carried out of the seam so the
/// acceptance tests assert the commit path and the
/// store_position fallback path apart from each other.
/// is for every arm that
/// performs no placement.
///
///
internal readonly record struct RemoteContactRouting(
RemoteContactArm Arm,
RuntimeRemotePlacementExecutionStatus? Placement);
///
/// The complete remote grounded/contact routing for ONE accepted Position,
/// shared by the player-remote and NPC-remote arms — retail's
/// CPhysicsObj::MoveOrTeleport (0x00516330) makes no
/// this == player distinction on any of these branches.
///
///
/// C4 route 4a contributed the ORDERING carve-out: an airborne body's
/// contact packet keeps its pre-existing authoritative hard-snap and is
/// decided BEFORE the near-Interpolate branch can claim it. A landing
/// packet classifies Interpolate, so if the 4a test came first it
/// would ENQUEUE a body that must PLANT, and a creature knocked off a
/// ledge would glide down over a packet interval. The player-remote caller
/// reaches this method only with Airborne == false (its landing
/// block sits ahead of its routing and returns), so the carve-out is inert
/// there and the two callers stay one decision.
///
///
///
/// C4 route 4b-2 added and
/// deleted the two duplicated App-side near/far blocks that used to follow
/// this call. The far arm is the only re-entrant one — a canonical
/// placement publishes its Place receipt synchronously, and a
/// non-commit outcome publishes a cancellation receipt just as
/// synchronously, and the production placement-projection sink can delete
/// or replace the incarnation from inside either — so a caller MUST
/// re-validate position ownership after this returns
/// , on EVERY placement
/// status, before writing anything else for the packet. That includes the
/// ConstrainTo leash: both arms therefore run the re-validation
/// FIRST and arm second (see AP-138).
///
///
internal static RemoteContactRouting ApplyRemoteContactRouting(
RuntimeRemotePlacementDriveController placementDrive,
RuntimeEntityRecord canonical,
RemoteMotion remote,
RuntimeAuthoritativePositionRoute? route,
System.Numerics.Vector3 worldPos,
System.Numerics.Quaternion rotation,
bool willBeDrTicked)
{
ArgumentNullException.ThrowIfNull(placementDrive);
ArgumentNullException.ThrowIfNull(canonical);
ArgumentNullException.ThrowIfNull(remote);
// Bug B (2026-08-04): stamp the GUID that any [remote-slide-*] line
// emitted from inside this synchronous routing window belongs to —
// ApplyInterpolate (blip producer Candidate 1) has no GUID of its own.
// TEMPORARY — strip with the ACDREAM_PROBE_REMOTE_SLIDE family.
AcDream.Core.Physics.PhysicsDiagnostics.BeginRemoteSlideAttribution(
canonical.ServerGuid);
if (remote.Airborne)
{
// Verbatim from the pre-4a branch, queue deliberately NOT
// cleared: the arc integrates locally (K-fix15), and clearing
// stale waypoints is owned by the per-tick LANDING detection —
// the `!previousOnWalkable && finalOnWalkable` arm of
// RuntimeRemotePhysicsUpdater.Tick's SetPositionInternal commit,
// whose `rm.Interp.Clear()` is register row AP-139 — not by this
// snap. Cited by SYMBOL on purpose: the same reference was a line
// range twice and went stale both times, once within a single
// review round.
//
// Do NOT restate this as "the queue is already empty here" — it
// is not. Nothing that sets Airborne clears the queue except the
// teleport hook's StopInterpolating: neither the 0xF74E
// VectorUpdate (OnVector, below) nor any of the five
// `Airborne = !Body.OnWalkable` sites do. Those five are
// SettleSpawnedRemoteContact (this file),
// RemoteTeleportPlacement.Apply,
// RuntimeSetPositionState's canonical placement commit, and
// RuntimeRemotePhysicsUpdater's two — the SetPositionInternal
// commit in Tick and the TickHidden resolve. A walking NPC can
// enqueue a near waypoint and then step off a lip, arriving here
// with a populated queue. That is exactly why the landing clear
// exists, and a reader who believes the queue is empty here could
// delete it.
remote.Body.Position = worldPos;
remote.Body.Orientation = rotation;
return new RemoteContactRouting(
RemoteContactArm.AirborneSnap, Placement: null);
}
switch (RuntimeRemoteFarSnapPosition.ResolveArm(route))
{
case RuntimeRemoteAcceptedPositionArm.FarSnapPlacement:
return new RemoteContactRouting(
RemoteContactArm.FarSnapPlacement,
placementDrive.ApplyAcceptedRemoteFarSnap(
canonical,
remote,
route!.Value));
case RuntimeRemoteAcceptedPositionArm.NearInterpolate:
RuntimeRemoteSteadyStatePosition.ApplyInterpolate(
remote,
worldPos,
rotation,
isMovingTo: remote.Movement.IsMovingTo(),
willBeDrTicked);
return new RemoteContactRouting(
RemoteContactArm.SteadyStateInterpolate, Placement: null);
case RuntimeRemoteAcceptedPositionArm.AirborneNoOperation:
// R10 review fix: explicit rather than folded into `default`,
// where the comment ASSERTED unreachability that no code
// enforced. Retail's arg4 == 0 branch writes NOTHING at all
// (@0x0051636D returns 0), so there is no operation this
// method could perform; both production callers early-return
// on IsAirborneNoOperation before they route (the player arm's
// AIRBORNE NO-OP block, the NPC arm's mirror of it — the two
// `IsAirborneNoOperation` call sites in this file, cited here
// by name because line numbers went stale within one review
// round). Reaching here means a caller
// skipped that gate, and the only faithful answer is to say
// so — ApplyInterpolate's own doc likewise forbids being
// called for this disposition.
throw new InvalidOperationException(
"A NoPositionOperation (airborne no-op) classification "
+ "must be handled by the caller's own early return "
+ "before routing; retail's MoveOrTeleport writes nothing "
+ "at all on that branch (@0x0051636D).");
default:
// UnroutedCatchUp takes the SAME AP-87 catch-up the near
// branch uses — the stated policy (AP-137), and the reason
// the App's two duplicated 96 m / 4 m constant pairs and both
// fabricated Vector3.Zero player positions are gone.
RuntimeRemoteSteadyStatePosition.ApplyInterpolate(
remote,
worldPos,
rotation,
isMovingTo: remote.Movement.IsMovingTo(),
willBeDrTicked);
return new RemoteContactRouting(
RemoteContactArm.UnroutedCatchUp, Placement: null);
}
}
///
/// C4 route 4b-2: the NPC-remote arm's post-routing wire-cell adoption,
/// extracted so its ONE suppression rule is exercised by production and by
/// test through the same entry point rather than restated in a test body.
///
///
/// writes THROUGH to the canonical
/// FullCellId (RuntimePhysicsState.CommitCanonicalCell).
/// After a far snap the canonical placement is the cell authority — retail
/// CPhysicsObj::SetPositionInternal (0x00515BD0) resolves the
/// destination cell through AdjustPosition/set_cell and
/// nothing writes the wire cell over it afterwards — so this write is
/// suppressed for that arm alone. Unlike the player arm, whose identical
/// write sits BEFORE its routing, the NPC one sits after; leaving it
/// unguarded would discard a resolved cell that differs from the wire
/// cell. Every other arm performs no placement, so the wire cell is still
/// the newest truth there.
///
///
///
/// Scope, stated precisely (C4 route 4b-2 review; corrected at the
/// delta review). The suppression bites whenever the canonical
/// placement RESOLVED a cell different from the wire cell. That is the
/// commit, and also the RejectedByPlacement shape where
/// CommitCanonical settled the body (and wrote
/// record.FullCellId) before the projection ownership was
/// displaced — the earlier "only when the placement COMMITTED" wording
/// missed that one. It also bites on Deferred, which round 3
/// (correction m1) adds to this enumeration: ParkDeferred snaps
/// the body to the PARKED result cell and
/// RestoreParkWithdrawal re-commits residency from
/// body.CellPosition.ObjCellId, which for a post-sweep park is the
/// swept/settled cell and need not be the wire cell. The remaining
/// outcomes — Refused, Contention,
/// RejectedPreparation, NotApplicable, and the
/// RejectedByPlacement shape the engine's own sweep refused —
/// resolve no cell, and there the suppression is a no-op: the per-UP
/// RebucketLiveEntity above already committed the wire full cell
/// to canonical, and RemoteMotion.CellId reads through to the same
/// FullCellId, so the suppressed write would have written the
/// value that is already there. Keying on the ARM rather than the
/// placement status is therefore exact as well as simpler — and the
/// body/cell divergence a refusal used to produce was the frozen body,
/// which the store_position fallback fixes at its source.
///
///
///
/// Completion of the "already there" justification for the park case
/// (2026-08-04). The no-op argument above is a claim about the four
/// cell-resolving-nothing outcomes only, and it does NOT extend to
/// Deferred. A park runs WithdrawCanonical, which ZEROES
/// record.FullCellId; CommitCanonicalCell early-returns only
/// on equality, so nothing about "the value is already there" survives a
/// park. Deferred is nevertheless suppressed correctly, but for the
/// FIRST reason in this doc rather than the second: the park snapped the
/// body to a resolved cell that need not be the wire cell, and
/// RestoreParkWithdrawal re-commits residency from that body cell
/// (or, in this controller's shipped order, leaves in place the full cell
/// the per-UP RebucketLiveEntity above committed before routing).
/// Adopting the wire cell into RemoteMotion.CellId afterwards would
/// contradict whichever of those two the entity actually holds.
///
///
///
/// Returns true when the wire cell was adopted.
///
///
internal static bool TryAdoptWireCellAfterRouting(
RemoteMotion remote,
RemoteContactArm arm,
uint wireCellId)
{
ArgumentNullException.ThrowIfNull(remote);
if (arm is RemoteContactArm.FarSnapPlacement)
return false;
remote.CellId = wireCellId;
return true;
}
///
/// K-fix9 (2026-04-26): handle 0xF74E VectorUpdate from remote jumps.
/// The payload seeds the world-space launch velocity and angular velocity.
///
public void OnVector(AcDream.Core.Net.Messages.VectorUpdate.Parsed update)
{
bool payloadIsValid = _projectileController?.CanAcceptVectorPayload(
update.Guid,
update.Velocity,
update.Omega) != false;
if (!_authorityGate.TryAcceptVector(
update,
payloadIsValid,
out AcceptedVectorNetworkUpdate accepted))
{
return;
}
LiveEntityRecord acceptedVectorRecord = accepted.Record;
ulong acceptedVectorAuthorityVersion =
accepted.VectorAuthorityVersion;
ulong acceptedVectorVelocityAuthorityVersion =
accepted.VelocityAuthorityVersion;
LiveEntityVectorRouter.Route(
() => _projectileController?.ApplyAuthoritativeVector(
acceptedVectorRecord,
acceptedVectorAuthorityVersion,
acceptedVectorVelocityAuthorityVersion,
update.Velocity,
update.Omega,
_physicsScriptGameTime) == true,
() =>
{
// A Physics-Static animation owner can own the canonical
// CPhysicsObj before any MovementManager exists. F74E writes
// directly to that body and must not manufacture a remote.
if (update.Guid == _playerServerGuid
|| acceptedVectorRecord.RemoteMotionRuntime is not null
|| acceptedVectorRecord.PhysicsBody is not { } canonicalBody)
{
return false;
}
_liveEntities.TryCommitAuthoritativeVector(
acceptedVectorRecord,
canonicalBody,
update.Velocity,
update.Omega,
_physicsScriptGameTime);
return true;
},
() => ApplyOrdinaryVector(
update,
acceptedVectorRecord,
acceptedVectorAuthorityVersion,
acceptedVectorVelocityAuthorityVersion));
}
private void ApplyOrdinaryVector(
AcDream.Core.Net.Messages.VectorUpdate.Parsed update,
LiveEntityRecord acceptedVectorRecord,
ulong acceptedVectorAuthorityVersion,
ulong acceptedVectorVelocityAuthorityVersion)
{
if (!_liveEntities.ContainsWorldEntity(update.Guid)) return;
if (update.Guid == _playerServerGuid) return; // local jump uses our own physics
if (!_liveEntities.TryGetRemoteMotionRuntime(
update.Guid,
out IRuntimeRemoteMotion? remoteRuntime)
|| remoteRuntime is not RemoteMotion rm)
{
return;
}
LiveEntityRecord remoteRecord = acceptedVectorRecord;
// World-space velocity. Apply directly to the body — the per-tick
// remote update will integrate Position += Velocity × dt + 0.5 × Accel × dt².
// L.3.1 Task 6: apply Omega too. LiveEntityRuntime commits both
// writes to the one canonical CPhysicsObj and wakes its retained
// update_time clock on the same non-Static edge.
if (!_liveEntities.TryCommitAuthoritativeVector(
remoteRecord,
rm.Body,
update.Velocity,
update.Omega,
_physicsScriptGameTime))
{
return;
}
// Bug B (2026-08-04) — [remote-slide-vec]. NOT ESTABLISHED #4 asks
// whether ACE relays a 0xF74E at all while a sender slides; the
// ABSENCE of these lines across a captured slide window is the
// answer, so this sits on the committed path rather than inside the
// +Z airborne branch below (a downhill slide has Velocity.Z < 0 and
// would never reach it). willMarkAirborne restates that branch's own
// test so the log states the outcome rather than making the reader
// re-derive it. Pure reads. TEMPORARY — strip with the probe family.
if (AcDream.Core.Physics.PhysicsDiagnostics.ShouldLogRemoteSlide(
update.Guid))
{
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteSlideVector(
guid: update.Guid,
wireVelocity: update.Velocity,
wireOmega: update.Omega,
willMarkAirborne: update.Velocity.Z > 0.5f,
airborneBefore: rm.Airborne,
contact: rm.Body.InContact,
onWalkable: rm.Body.OnWalkable,
gravity: rm.Body.HasGravity,
bodyVelocity: rm.Body.Velocity,
contactPlaneValid: rm.Body.ContactPlaneValid,
contactPlaneNormalZ: rm.Body.ContactPlane.Normal.Z);
}
// Mark airborne when the launch has meaningful +Z. Threshold
// 0.5 m/s rejects noise / horizontal-only updates (server might
// also use VectorUpdate for non-jump events). The per-tick
// remote update reads .Airborne to skip the ground-clamp branch
// and apply gravity instead.
if (update.Velocity.Z > 0.5f)
{
rm.Airborne = true;
// Clear the ground-contact transients so calc_acceleration
// (0x00510950) releases gravity and UpdatePhysicsInternal produces
// the parabolic arc. Retail reaches the same state one frame later
// through check_contact (0x0050F5B0) failing on the ascending
// velocity; clearing them here is the AP-81 head start, and it is
// what keeps the per-tick `set_on_walkable` edge from ALSO firing
// LeaveGround for this same departure.
//
// Bug B (2026-08-04): the `State |= Gravity` that used to follow is
// DELETED. GRAVITY_PS is a persistent object property owned by the
// wire — retail's CPhysicsObj constructor seeds it (state 0x400C08
// @0x00512508) and set_description's set_state (0x00514DD0) assigns
// the description's state wholesale without ever masking it. Now
// that neither landing block clears the bit, manufacturing it here
// would be the only remaining non-retail gravity write, and it
// would mask a server that genuinely sent a gravity-free state.
// ACE agrees: PhysicsGlobals.DefaultState and the player login
// state both carry PhysicsState.Gravity.
rm.Body.TransientState &= ~(AcDream.Core.Physics.TransientStateFlags.Contact
| AcDream.Core.Physics.TransientStateFlags.OnWalkable);
// R3-W4 (J19 — K-fix10/K-fix18 DELETED): the retail mechanism.
// The remote's ground departure fires LeaveGround (0x00528b00):
// strips pending transition links (the RemoveLinkAnimations
// seam) + re-applies movement through DefaultSink, whose
// contact-gated funnel dispatch engages Falling — no forced
// SetCycle, no skip flag. The wire velocity/omega are re-applied
// AFTER so they stay authoritative over LeaveGround's
// state-derived velocity write (adaptation note: retail's
// equivalence comes from the per-tick transition-sweep order —
// R6 scope).
if (_liveEntities.TryGetWorldEntity(update.Guid, out var ent)
&& _animatedEntities.TryGetValue(ent.Id, out var ae)
&& ae.Sequencer is not null)
{
_motionRuntime.EnsureRemoteMotionBindings(rm, ae, update.Guid);
rm.Motion.LeaveGround();
if (!_liveEntities.IsCurrentVectorAuthority(
remoteRecord,
acceptedVectorAuthorityVersion)
|| !_liveEntities.IsCurrentVelocityAuthority(
remoteRecord,
acceptedVectorVelocityAuthorityVersion)
|| !_liveEntities.TryCommitAuthoritativeVector(
remoteRecord,
rm.Body,
update.Velocity,
update.Omega,
_physicsScriptGameTime))
{
return;
}
}
}
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
{
Console.WriteLine(
$"VU guid=0x{update.Guid:X8} vel=({update.Velocity.X:F2},{update.Velocity.Y:F2},{update.Velocity.Z:F2}) airborne={rm.Airborne}");
}
}
///
/// L.2g slice 1: inbound SetState (0xF74B) handler. Propagates the
/// new PhysicsState bits into ShadowObjectRegistry so the
/// existing check honors
/// the flip on the next resolver tick. Chiefly doors:
/// server flips ETHEREAL_PS = 0x4 on Use, the door's
/// cylinder collision stops blocking the threshold.
///
public void OnState(AcDream.Core.Net.Messages.SetState.Parsed parsed)
{
if (!_authorityGate.TryAcceptState(
parsed,
out AcceptedStateNetworkUpdate accepted))
return;
LiveEntityRecord record = accepted.Record;
ulong acceptedStateAuthorityVersion = accepted.StateAuthorityVersion;
// Retail set_state order: Lighting, NoDraw, then Hidden. The live
// runtime already committed the raw/final bits and draw visibility;
// apply the ordered owners before updating motion/collision consumers.
_liveEntityLights?.OnStateChanged(parsed.Guid);
_liveEntityPresentation?.OnStateAccepted(parsed.Guid);
if (!_liveEntities.IsCurrentStateAuthority(
record,
acceptedStateAuthorityVersion))
{
return;
}
_projectileController?.ApplyAuthoritativeState(
record,
acceptedStateAuthorityVersion,
record.FinalPhysicsState,
_physicsScriptGameTime,
_origin.CenterX,
_origin.CenterY);
if (!_liveEntities.IsCurrentStateAuthority(
record,
acceptedStateAuthorityVersion))
{
return;
}
if (parsed.Guid == _playerServerGuid)
{
// C3c-F1 (2026-08-02): route through the owner's
// lifecycle-deciding typed entry. The publication lifecycle —
// not this inbound handler — decides whether the push lands:
// a dormant first-entry controller drops it (the activation
// transaction re-reads the same canonical FinalPhysicsState
// itself; the accepted SetState is queued behind the initial
// residence so this value is unchanged), and a terminal
// controller treats it as a displaced push instead of faulting
// the session (the second connected-gate crash chain,
// logs/connected-world-gate-20260802-125907).
_ = _playerController?.ApplyServerPhysicsState(
record.FinalPhysicsState);
}
if (!_liveEntities.TryGetWorldEntity(parsed.Guid, out var entity)) return;
// L.2g slice 1c (2026-05-13): the server addresses entities by
// ServerGuid (parsed.Guid, e.g. 0x7A9B4015), but
// ShadowObjectRegistry's cell index is keyed by local entity.Id
// (e.g. 0x000F4245). Translate through the canonical Runtime directory before
// mutating the registry — otherwise the lookup misses and the
// state flip silently no-ops, leaving doors blocked even though
// ACE flipped the ETHEREAL bit.
uint registryKey = entity.Id;
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeBuildingEnabled)
Console.WriteLine(System.FormattableString.Invariant(
$"[setstate] guid=0x{parsed.Guid:X8} entityId=0x{registryKey:X8} raw=0x{parsed.PhysicsState:X8} final=0x{(uint)record.FinalPhysicsState:X8} instSeq={parsed.InstanceSequence} stateSeq={parsed.StateSequence}"));
}
public void OnPosition(AcDream.Core.Net.WorldSession.EntityPositionUpdate update)
{
if (_worldDropProjection?.TryRecoverUnknownPosition(update) == true)
{
return;
}
bool payloadIsValid = _projectileController?.CanAcceptPositionPayload(
update.Guid,
update.Position,
update.Velocity) != false;
if (!_authorityGate.TryAcceptPosition(
update,
_playerServerGuid,
update.Guid == _playerServerGuid && _playerController is not null
? _playerController.BodyOrientation
: null,
update.Guid == _playerServerGuid && _playerController is not null
? _playerController.BodyVelocity
: null,
payloadIsValid,
out AcceptedPositionNetworkUpdate accepted))
{
return;
}
var timestampDisposition = accepted.TimestampDisposition;
var acceptedSpawn = accepted.Spawn;
var timestamps = accepted.Timestamps;
RuntimeEntityRecord acceptedPositionCanonical = accepted.Canonical;
ulong acceptedPositionAuthorityVersion =
accepted.PositionAuthorityVersion;
ulong acceptedPositionVelocityAuthorityVersion =
accepted.VelocityAuthorityVersion;
if (!_liveEntities.TryGetProjection(
acceptedPositionCanonical,
out LiveEntityRecord acceptedPositionRecord)
&& !_liveEntityHydration.RecoverCanonicalProjection(
acceptedPositionCanonical,
acceptedPositionAuthorityVersion,
out acceptedPositionRecord))
{
return;
}
bool IsCurrentPositionOwner(
AcDream.Core.World.WorldEntity? expectedEntity = null) =>
_liveEntities.IsCurrentPositionAuthority(
acceptedPositionRecord,
acceptedPositionAuthorityVersion)
&& (expectedEntity is null
|| ReferenceEquals(
acceptedPositionRecord.WorldEntity,
expectedEntity));
if (!IsCurrentPositionOwner())
return;
// A PlayerDescription/CreateObject may establish the live record
// without either Position or enough render data. Bind streaming
// readiness directly to this first accepted canonical Position before
// translating it through the current world origin; projection recovery
// below is a separate concern and may never be needed for UI-only state.
if (_liveEntityHydration?.EnsureWorldOrigin(
acceptedPositionRecord,
acceptedPositionAuthorityVersion,
acceptedSpawn) != true
|| !IsCurrentPositionOwner())
return;
var p = update.Position;
int lbX = (int)((p.LandblockId >> 24) & 0xFFu);
int lbY = (int)((p.LandblockId >> 16) & 0xFFu);
var origin = new System.Numerics.Vector3(
(lbX - _origin.CenterX) * 192f,
(lbY - _origin.CenterY) * 192f,
0f);
var worldPos = new System.Numerics.Vector3(p.PositionX, p.PositionY, p.PositionZ) + origin;
bool forceLocal = timestampDisposition is AcDream.Core.Physics.PositionTimestampDisposition.ForcePosition
&& update.Guid == _playerServerGuid
&& _playerController is not null;
if (forceLocal)
{
if (!IsCurrentPositionOwner())
return;
// C4 route 2 (2026-08-03): the Runtime-owned accepted-Position
// execution seam replaces the deleted LocalForcePositionTransaction.
// Ownership validation is the operation's own currency check,
// the body commit is Runtime's canonical SetPosition transaction
// (retail CPhysicsObj::SetPositionSimple @0x005162B0, called from
// SmartBox::BlipPlayer @0x00453940), and the outbound ack is an
// OUTPUT of that committed route, fired strictly after it.
RuntimeAcceptedPositionExecutionStatus forceStatus =
_acceptedPositionDrive.TryExecuteAcceptedLocalPosition(
acceptedPositionCanonical,
update,
timestampDisposition,
timestamps,
timestamps.PreviousTeleport);
if (forceStatus is RuntimeAcceptedPositionExecutionStatus.Committed
or RuntimeAcceptedPositionExecutionStatus.DeferredCell)
{
// App projects the committed (or parked-toward) result
// through the existing Runtime placement sink
// (RuntimePlacementPresentationSink), which observes the
// SAME Runtime SetPosition FIFO every other placement uses —
// it must not ALSO independently mutate the render-facing
// WorldEntity here (the retired duplicate-write authority:
// the generic tail below). The two local-player side effects
// this neighbourhood still owns independently of WorldEntity
// position (owned-VFX pose-dirty tracking and the
// pre-player-mode streaming landblock tracker) are
// preserved — DeferredCell included, since following the
// destination is what lets its streaming/collision window
// eventually publish the generation the park is waiting on.
_entityEffects?.MarkLiveOwnerPoseDirty(update.Guid);
_authorityGate.ObserveAcceptedLocalPosition(
update.Position.LandblockId);
return;
}
if (forceStatus is not RuntimeAcceptedPositionExecutionStatus.NotApplicable)
{
// R9 review fix (2026-08-03): Rejected/Contention — no
// correction was applied or parked for this exact packet.
// Do NOT move the streaming observer or mark the render pose
// dirty for a landblock this route explicitly declined to
// place into, and do not fall through to the generic tail
// below either (that would resurrect the retired duplicate-
// write authority this whole route exists to remove).
return;
}
// NotApplicable — e.g. an initial-Create residence still owns
// this record (route 1's job). Fall through to the pre-existing
// path unchanged, exactly as every other disposition does.
}
// A leave-world transition deliberately retains WorldEntity as the
// logical/render-resource owner while IsSpatiallyProjected is false.
// A fresh retail Position is the re-entry edge; testing only for the
// retained object reference leaves dropped inventory permanently
// invisible after InventoryPutObjectIn3D.
if (RequiresSpatialProjectionRecovery(acceptedPositionRecord))
{
if (!IsCurrentPositionOwner())
return;
AcDream.App.Rendering.ChildUnparentDisposition unparented =
_equippedChildRenderer?.OnChildBecameUnparented(
update.Guid,
() =>
{
if (!IsCurrentPositionOwner())
return;
_liveEntityHydration!.RecoverProjection(
acceptedPositionRecord,
acceptedPositionAuthorityVersion,
acceptedSpawn);
})
?? AcDream.App.Rendering.ChildUnparentDisposition.NotAttached;
if (unparented is AcDream.App.Rendering.ChildUnparentDisposition.Superseded
or AcDream.App.Rendering.ChildUnparentDisposition.Pending)
return;
if (!IsCurrentPositionOwner())
return;
if (unparented is AcDream.App.Rendering.ChildUnparentDisposition.NotAttached)
{
_liveEntityHydration!.RecoverProjection(
acceptedPositionRecord,
acceptedPositionAuthorityVersion,
acceptedSpawn);
if (!IsCurrentPositionOwner())
return;
}
}
if (!_liveEntities.TryGetWorldEntity(update.Guid, out var entity)) return;
if (!IsCurrentPositionOwner(entity))
return;
_entityEffects?.MarkLiveOwnerPoseDirty(update.Guid);
if (!IsCurrentPositionOwner(entity))
return;
// Phase A.1 / #135: track the PLAYER's last server-known landblock so the
// streaming controller can follow the player in the fly-camera / pre-player-mode
// (login hold) views. Filtered to our OWN character guid — resolving the original
// Phase A.1 TODO. An arbitrary NPC's UpdatePosition from a far outdoor landblock
// must NOT move the streaming observer: during a dungeon-login hold (player not
// yet placed, so _playerController is null and the PortalSpace observer branch
// can't apply) that would drift the observer off the pre-collapsed dungeon
// landblock and trip ExitDungeonExpand, re-streaming the 25×25 neighbor window
// the pre-collapse just suppressed. _playerServerGuid is set from CharacterList
// (~line 1984) before world entry, so it is valid by the time updates arrive.
if (update.Guid == _playerServerGuid)
_authorityGate.ObserveAcceptedLocalPosition(update.Position.LandblockId);
// B.6 slice 1 (2026-05-14): trace inbound UpdatePosition cadence for
// the local player. Combined with [autowalk-mt] this answers
// whether ACE's broadcast frequency during a server-initiated
// auto-walk is dense enough to drive smooth visible motion (the
// Option C viability check from the design spec). Gated on
// ACDREAM_PROBE_AUTOWALK=1; skips remote entities.
if (update.Guid == _playerServerGuid
&& AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
{
string velStr = update.Velocity is { } v
? $"vel=({v.X:F2},{v.Y:F2},{v.Z:F2})"
: "vel=null";
Console.WriteLine(System.FormattableString.Invariant(
$"[autowalk-up] cell=0x{p.LandblockId:X8} pos=({p.PositionX:F2},{p.PositionY:F2},{p.PositionZ:F2}) world=({worldPos.X:F2},{worldPos.Y:F2},{worldPos.Z:F2}) {velStr} grounded={update.IsGrounded}"));
}
var rot = timestampDisposition is AcDream.Core.Physics.PositionTimestampDisposition.ForcePosition
? entity.Rotation
: new System.Numerics.Quaternion(p.RotationX, p.RotationY, p.RotationZ, p.RotationW);
_movementTruthDiagnostics.OnServerEcho(update, worldPos);
bool remoteHardTeleport = update.Guid != _playerServerGuid
&& timestamps.TeleportHookRequired;
bool remotePlacementRequired = update.Guid != _playerServerGuid
&& (remoteHardTeleport
|| _remoteTeleportController?.HasPending(update.Guid) == true);
if (remoteHardTeleport)
{
if (!RunRemoteTeleportHook(
update.Guid,
entity.Id,
() => IsCurrentPositionOwner(entity)))
{
return;
}
}
// Missiles reconcile the same predicted PhysicsBody in place. The
// timestamp gate above already rejected stale corrections; returning
// here prevents the generic remote locomotion path from allocating a
// second body or interpolation owner for the projectile.
if (_projectileController?.ApplyAuthoritativePosition(
acceptedPositionRecord,
acceptedPositionAuthorityVersion,
acceptedPositionVelocityAuthorityVersion,
worldPos,
new System.Numerics.Vector3(
p.PositionX,
p.PositionY,
p.PositionZ),
rot,
acceptedSpawn.Physics?.Velocity
?? System.Numerics.Vector3.Zero,
p.LandblockId,
_physicsScriptGameTime,
_origin.CenterX,
_origin.CenterY) == true)
return;
if (!_liveEntities.TryGetRecord(
update.Guid,
out LiveEntityRecord positionRecord)
|| !ReferenceEquals(positionRecord, acceptedPositionRecord)
|| !ReferenceEquals(positionRecord.WorldEntity, entity)
|| !_liveEntities.IsCurrentPositionAuthority(
positionRecord,
acceptedPositionAuthorityVersion))
{
return;
}
// C4 route 4a: classify BEFORE the generic write below so a remote
// whose accepted Position resolves to NoPositionOperation (retail's
// airborne no-op — writes nothing at all) or Interpolate (retail's
// near InterpolateTo queue — no direct body write here) never
// receives it. remotePlacementRequired guarantees the classifier's
// teleport (SetPosition) disposition never reaches here — that stays
// route 4b-3. The local player never reaches this generic-remote code
// path at all. C4 route 4b-2 additionally routes the >=96 m far snap
// through the canonical Runtime placement owner; a cell-less remote, a
// rejected authority or payload, and "no classification at all" take
// the stated UnroutedCatchUp policy
// (RuntimeRemoteFarSnapPosition.ResolveArm) until 4b-3.
RuntimeAuthoritativePositionRoute? earlyRemoteRoute =
update.Guid != _playerServerGuid && !remotePlacementRequired
? ClassifyRemoteAcceptedPosition(
update,
acceptedPositionCanonical,
timestampDisposition,
timestamps,
worldPos)
: null;
TryApplyGenericRemoteRenderPose(
entity,
earlyRemoteRoute,
worldPos,
p.LandblockId,
rot);
// The spatial bucket transaction runs for EVERY classification,
// including both 4a branches: it is the only site that moves an
// ordinary moving remote's draw bucket, commits its canonical
// FullCellId (which feeds back as the classifier's own
// CommittedCellId and as the ConstraintDistance cell key), and
// recovers a pending bucket promotion.
//
// C4 route 4b-2 review fix — this used to end "Neither 4a branch
// performs a placement, so unlike route 2 there is no committed
// placement receipt that could project this in its stead". The far
// arm DOES perform a placement now, and this call still runs ahead of
// it for every classification. That ordering is what makes the NPC
// arm's post-routing wire-cell suppression a no-op on a non-commit
// outcome (TryAdoptWireCellAfterRouting): the wire full cell is
// already canonical by the time routing starts. A COMMITTED placement
// resolves its own destination cell afterwards, which is the case the
// suppression exists for.
if (!_liveEntities!.RebucketLiveEntity(update.Guid, p.LandblockId)
|| !_liveEntities.TryGetRecord(
update.Guid,
out LiveEntityRecord afterRebucket)
|| !ReferenceEquals(afterRebucket, positionRecord)
|| !ReferenceEquals(afterRebucket.WorldEntity, entity)
|| !_liveEntities.IsCurrentPositionAuthority(
afterRebucket,
acceptedPositionAuthorityVersion))
{
// A projection callback superseded or deleted this incarnation.
// Never let an older UpdatePosition seed the replacement's
// placement, interpolation, or collision state.
return;
}
if (remotePlacementRequired)
{
_remoteTeleportController!.BeginPlacement(
update.Guid,
acceptedSpawn.InstanceSequence);
if (!IsCurrentPositionOwner(entity))
return;
}
// Commit B 2026-04-29 — keep the shadow registry in sync with
// server-authoritative position so the player's collision broadphase
// tests against the up-to-date target body. Skip the local player
// (its body is the simulator, not a target). Retail does the
// equivalent via SetPosition → change_cell → AddShadowObject
// (acclient_2013_pseudo_c.txt:284276 / 281200 / 282862).
// #184 Slice 2b: the former players-only RAW-pos shadow sync is RETIRED.
// It was a Slice-1 stopgap while grounded player remotes (old Path A) skipped
// the sweep and tracked the server position closely. Now that Slice 2b runs
// the SAME per-tick sweep + shadow-follows-resolved for players, writing the
// raw (overlapping) server pos here would re-snap a packed player's shadow
// into overlap once per UP and fight the in-tick de-overlap (research
// finding 9). Player shadows now follow the RESOLVED body — via the DR-tick
// loop (SyncRemoteShadowToBody, pose/cell-gated) and the player UP-branch tail
// below (first-UP / no-Sequencer case), exactly like NPCs. Local-player
// broadphase still tests an up-to-date remote shadow; it is just the resolved
// body now, not the raw wire pos.
// Track remote-entity motion for stop detection. Only record the
// timestamp when position moved MEANINGFULLY (> 0.05m). Updates
// that report the same position keep the old Time, so the
// TickAnimations check can see when motion last changed.
//
// Also populate the dead-reckon state so TickAnimations can
// integrate velocity between server updates and avoid teleport jitter.
// Observed-velocity is computed from the position delta across
// consecutive updates — this is the fallback when the motion table's
// MotionData.Velocity is zero (NPCs without HasVelocity).
if (update.Guid != _playerServerGuid)
{
var now = System.DateTime.UtcNow;
RuntimeEntityKey positionKey = positionRecord.ProjectionKey
?? throw new InvalidOperationException(
$"Position owner 0x{update.Guid:X8}/" +
$"{positionRecord.Generation} has no exact projection key.");
if (_remoteMovementObservations.TryGetValue(positionKey, out var prev))
{
float moveDist = System.Numerics.Vector3.Distance(prev.Pos, worldPos);
if (moveDist > 0.05f)
_remoteMovementObservations[positionKey] = (worldPos, now);
// else: leave old entry so "Time" = last real movement time
}
else
{
_remoteMovementObservations[positionKey] = (worldPos, now);
}
// Retail-faithful hard-snap on UpdatePosition.
// Decompile: FUN_00559030 @ chunk_00550000.c:8232 writes
// pos/rot directly into PhysicsObj+0x80..0xBC with no blending.
// Between UpdatePositions, per-tick velocity integration keeps
// the rendered position close to server truth so each snap is
// small. When HasVelocity is set, we also seed PhysicsBody
// velocity (matches retail's set_velocity call in the same
// dispatcher).
if (!_liveEntities.TryGetRemoteMotionRuntime(
update.Guid,
out IRuntimeRemoteMotion? remoteRuntime)
|| remoteRuntime is not RemoteMotion rmState)
{
rmState =
_liveEntities.GetOrCreateRemoteMotionRuntime(
update.Guid);
// Hard-snap orientation on first spawn so the per-tick
// slerp doesn't visibly rotate from Identity to truth.
rmState.Body.Orientation = rot;
// #184 Slice 2b: PLACE the body at the server position on creation,
// mirroring the UM handler's seed (:5176 `Body.Position =
// entity.Position`). A UP-first RemoteMotion (created here before any
// UM) was left at the default (0,0,0). Path A never swept, so that
// stale origin was harmless — it caught up gradually. Now that Slice
// 2b runs the sweep for grounded PLAYERS too, an unplaced body would
// sweep from (0,0,0) in the server cell that does not contain it →
// garbage resolved pos → the digest's INVISIBLE/misplaced-body bug.
// Seeding here is the root-cause fix (the UP creation path should
// seed exactly like the UM path); worldPos == entity.Position (the
// unconditional snap at the top of this handler already ran).
rmState.Body.Position = worldPos;
// #270: retail spawn placement — establish real ground contact
// for the fresh body (a UP-created remote that then stands
// still would otherwise stay airborne-flagged and
// contact_allows_move would refuse its action animations).
SeedRemoteSpawnPlacement(
rmState,
update.Guid,
entity,
worldPos,
update.Position.LandblockId);
}
// PositionPack::UnPack initializes an absent velocity to zero;
// MoveOrTeleport installs that exact vector with set_velocity.
// The canonical seam wakes the retained ObjectClock and body in
// one operation. Position-delta velocity below remains animation
// diagnostics and is never substituted into physics.
if (!_liveEntities.IsCurrentPositionAuthority(
positionRecord,
acceptedPositionAuthorityVersion))
{
return;
}
if (_liveEntities.IsCurrentVelocityAuthority(
positionRecord,
acceptedPositionVelocityAuthorityVersion)
&& !_liveEntities.TryCommitAuthoritativeVelocity(
positionRecord,
rmState.Body,
acceptedSpawn.Physics?.Velocity
?? System.Numerics.Vector3.Zero,
_physicsScriptGameTime))
{
return;
}
// Retail CPhysicsObj::MoveOrTeleport Branch A (0x00516330): a
// fresh TELEPORT_TS, or the first placement of a cell-less body,
// runs teleport_hook and SetPosition(0x1012) BEFORE the contact
// test. In particular, an airborne UP cannot veto or undo this
// authoritative destination. Do not pre-clear velocity or invent
// grounded flags here: retail SetPosition derives contact from its
// transition, while MoveOrTeleport does not consume arg5/arg6.
if (remotePlacementRequired)
{
double teleportTime =
(System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
bool projectionVisible = _liveEntities.TryGetRecord(
update.Guid,
out LiveEntityRecord teleportRecord)
&& teleportRecord.IsSpatiallyVisible;
var placement = _remoteTeleportController!.TryApply(
positionRecord,
acceptedPositionAuthorityVersion,
acceptedPositionVelocityAuthorityVersion,
rmState,
entity,
worldPos,
p.LandblockId,
new System.Numerics.Vector3(
p.PositionX,
p.PositionY,
p.PositionZ),
rot,
teleportTime,
projectionVisible,
acceptedSpawn.InstanceSequence,
acceptedSpawn.PositionSequence);
if (placement.Superseded
|| !IsCurrentPositionOwner(entity))
{
return;
}
if (!placement.Applied)
{
entity.SetPosition(rmState.Body.Position);
entity.ParentCellId = rmState.CellId;
entity.Rotation = rmState.Body.Orientation;
if (rmState.CellId != 0)
_liveEntities.RebucketLiveEntity(update.Guid, rmState.CellId);
return;
}
if (!IsCurrentPositionOwner(entity))
return;
entity.SetPosition(rmState.Body.Position);
entity.Rotation = rmState.Body.Orientation;
return;
}
// #167 (Campaign P P5): retail SmartBox::HandleReceivedPosition
// (0x00453fd0) arms the ConstraintManager leash for every remote
// MoveOrTeleport call that returns nonzero (did NOT hard-teleport —
// the remotePlacementRequired branch above already handled and
// returned on the hard-teleport case), anchored to the object's OWN
// current position, generically for player AND NPC remotes (the
// disassembly's "this == player" branch loads identical constants
// either way — see ConstraintDistance). ConstraintManager.ConstrainTo
// captures ConstraintPosOffset = distance(anchor, host.Position) at
// call time; since the anchor here IS host.Position (read live,
// matching every other PositionManager/TargetManager consumer's
// notion of "this object's position"), this always (re)starts the
// leash at zero displacement on a fresh accepted Position, matching
// retail's per-packet re-anchor.
// docs/research/2026-07-30-constraint-leash-constants.md §2/§3.2.
//
// C4 route 4a / D2: retail arms this AFTER the operation, only on
// a nonzero MoveOrTeleport return (@0x00454272, inside the
// `if (MoveOrTeleport(...) != 0)` at @0x00454254) — so this
// pre-operation, unconditional arming is now the LEGACY shape and
// runs only for the classifications the post-operation arm does
// not own. C4 route 4b-2 added the far snap to that set, leaving
// this fallback for the cell-less half, the two rejections, and
// "no classification at all"; 4b-3 deletes it. The gate reads the
// SAME predicate TryArmConstraintAfterOperation does, so exactly
// one of the two sites arms any given classification.
if (!RuntimeRemoteFarSnapPosition.OwnsAfterOperationConstraint(
earlyRemoteRoute)
&& rmState.Host is { } remoteConstraintHost)
{
RuntimeRemoteSteadyStatePosition.ArmConstraintAfterOperation(
remoteConstraintHost);
}
// Bug B (2026-08-04) — [remote-slide-up]. This is the ONE point
// both remote arms pass through, and it deliberately sits AHEAD of
// the two IsAirborneNoOperation early returns below: in the
// diagnosis's Shape A (ACE reports IsGrounded == false for the
// whole slide) acdream writes nothing at all, so a line emitted
// after those returns would leave the entire slide window blank
// and NOT ESTABLISHED #1 unanswerable. `wireGrounded` is the raw
// ACE PositionFlags.IsGrounded bit for this packet.
// docs/research/2026-08-04-bug-b-remote-slide-diagnosis.md §2.
// Pure reads. TEMPORARY — strip with the probe family.
if (AcDream.Core.Physics.PhysicsDiagnostics.ShouldLogRemoteSlide(
update.Guid))
{
(int slideQueueDepth, int slideFailCount) =
rmState.Interp.DiagnosticInterpolationState;
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteSlideUp(
guid: update.Guid,
wireGrounded: update.IsGrounded,
wireVelocity: update.Velocity,
disposition: earlyRemoteRoute is { } slideRoute
? slideRoute.Disposition.ToString()
: "unclassified",
playerDistance: _playerController is { } slideController
? System.Numerics.Vector3.Distance(
worldPos,
slideController.Position)
: null,
bodyToTarget: System.Numerics.Vector3.Distance(
rmState.Body.Position,
worldPos),
bodySnapThreshold:
RuntimeRemoteSteadyStatePosition.DiagnosticBodySnapThreshold,
willBeDrTicked: WillAdvanceRemoteMotion(update.Guid, rmState),
firstUp: rmState.LastServerPosTime <= 0.0,
airborne: rmState.Airborne,
contact: rmState.Body.InContact,
onWalkable: rmState.Body.OnWalkable,
gravity: rmState.Body.HasGravity,
bodyVelocity: rmState.Body.Velocity,
contactPlaneValid: rmState.Body.ContactPlaneValid,
contactPlaneNormalZ: rmState.Body.ContactPlane.Normal.Z,
wirePosition: worldPos,
bodyPosition: rmState.Body.Position,
interpQueueDepth: slideQueueDepth,
interpFailCount: slideFailCount);
}
// L.3 M2 (2026-05-05): retail-faithful MoveOrTeleport routing for
// player remotes. Mirrors CPhysicsObj::MoveOrTeleport
// (acclient @ 0x00516330) — airborne no-op, far-snap, near
// InterpolateTo. Gated on IsPlayerGuid so NPCs continue through
// the legacy synth-velocity branch below; their motion comes
// from ServerVelocity / ServerMoveTo which the legacy path
// already handles correctly.
//
if (IsPlayerGuid(update.Guid))
{
// InterpolationManager retains the complete target Position.
// A near correction replaces both origin and orientation via
// Position::subtract2; only placement/far branches snap here.
// Adopt server's cell ID on every UP (airborne or grounded).
// Required by the legacy airborne path's per-tick
// ResolveWithTransition gate (rm.CellId != 0); without this
// an airborne player remote falls through the floor because
// the sphere sweep is skipped. Note: enabling the sweep also
// exposes a pre-existing depenetration bug — see #42. This is
// acdream's OWN free-fall sweep bookkeeping, not a retail
// CPhysicsObj field, so the D1 "writes nothing" rule below
// deliberately does not reach it — register row AP-135, and
// the NPC arm keeps the identical pair for the same reason.
rmState.CellId = p.LandblockId;
// Diagnostic (ACDREAM_REMOTE_VEL_DIAG=1): roll the previous
// server-pos snapshot forward AND print the per-UP comparison
// between the max literal CSequence root-motion speed observed
// since the last UP and the actual server broadcast pace. Both are sampled
// over the same window so the ratio reflects real overshoot.
{
double nowSecDiag = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1"
&& rmState.LastServerPosTime > 0.0)
{
double dtServer = nowSecDiag - rmState.LastServerPosTime;
if (dtServer > 0.001)
{
var serverDelta = worldPos - rmState.LastServerPos;
float serverSpeed = (float)(serverDelta.Length() / dtServer);
float rootMotionSpeed = rmState.MaxRootMotionSpeedSinceLastUP;
if (serverSpeed > 0.1f || rootMotionSpeed > 0.1f)
{
System.Console.WriteLine(
$"[VEL_DIAG] guid={update.Guid:X8} maxRootMotionSpeed={rootMotionSpeed:F3} m/s "
+ $"serverSpeed={serverSpeed:F3} m/s dtServer={dtServer:F3}s "
+ $"ratio={(serverSpeed > 1e-3f ? rootMotionSpeed / serverSpeed : 0f):F3}");
}
}
}
rmState.MaxRootMotionSpeedSinceLastUP = 0f;
rmState.PrevServerPos = rmState.LastServerPos;
rmState.PrevServerPosTime = rmState.LastServerPosTime;
rmState.LastServerPos = worldPos;
rmState.LastServerPosTime = nowSecDiag;
}
// ── AIRBORNE NO-OP (C4 route 4a / D1+D2) ─────────────────────
// Retail CPhysicsObj::MoveOrTeleport (0x00516330): arg4 == 0
// (the wire has_contact bit) falls straight to `return 0`
// @0x0051636D and writes NOTHING — not the body, not the
// interpolation queue, not the render entity, and (because
// ConstrainTo sits inside `if (MoveOrTeleport(...) != 0)` at
// @0x00454254) not the ConstraintManager leash either. The
// classifier read the SAME wire bit this packet carries, so
// there is nothing left to undo: the generic render-pose
// write above was suppressed for this exact classification.
if (RuntimeRemoteSteadyStatePosition.IsAirborneNoOperation(
earlyRemoteRoute))
{
return;
}
if (!update.IsGrounded)
{
// LEGACY airborne no-op, unchanged, for the packets 4a
// does not own — a cell-less remote (route 4b's
// SetPosition), a rejected authority/payload, or no
// classification at all. Those DID take the generic
// render-pose write above, so this still undoes it: the
// body is mid-arc and TickAnimations will write
// entity = body next frame anyway, and setting
// entity = body now prevents a 1-frame
// teleport-to-server-then-yank-back rubber-band.
// 4b deletes this fallback.
entity.SetPosition(rmState.Body.Position);
return;
}
// ── LANDING TRANSITION ────────────────────────────────────────
// First IsGrounded=true UP while the client still considers the
// body airborne (`!Body.OnWalkable`, now derived by the per-tick
// SetPositionInternal commit rather than latched here).
// Hard-snap to the authoritative landing position and clear the
// interpolation queue (an airborne remote's Positions hard-snap
// and never enqueue, so any pre-arc waypoints are stale).
// `rmState.Airborne` is deliberately NOT cleared here: the next
// tick derives it from the sweep, which is the only thing that
// can tell walkable ground from a steep face.
//
// Bug B (2026-08-04) — the twin of the per-tick forge. This
// block used to additionally zero the body velocity, assert
// `Contact | OnWalkable`, invoke MovementManager::HitGround, and
// clear the Gravity STATE bit. All four are deleted:
// • the velocity zero discarded the authoritative vector ACE
// delivered (retail MoveOrTeleport 0x00516330 never reads or
// writes the wire velocity for a remote at all);
// • the transient assert forged the two facts retail derives
// from the contact plane in SetPositionInternal
// (0x00515430 / 0x00515465-0x0051548E) — on a steep roof it
// declared a non-walkable surface walkable;
// • HitGround has exactly ONE retail source,
// `set_on_walkable(1)` @0x00511358, which the per-tick
// SetPositionInternal commit now owns. Firing it from here
// as well would double-dispatch the landing re-apply;
// • retail never toggles GRAVITY_PS on a ground edge — see the
// per-tick commit's comment.
// What remains is AP-87's acdream-only snap, unchanged.
if (rmState.Airborne)
{
rmState.Interp.Clear();
rmState.Body.Position = worldPos;
rmState.Body.Orientation = rot;
// C4 route 4a / D2: a landing packet is a GROUNDED
// correction, so retail's MoveOrTeleport returns nonzero
// and SmartBox::HandleReceivedPosition does arm the leash
// (@0x00454272). This block returns before the grounded
// routing below, so it arms its own — post-move, matching
// the anchor retail reads. Only for a classification 4a
// owns: every other one already armed the legacy
// pre-operation call above.
RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation(
earlyRemoteRoute,
rmState);
// C4 route 4a: a landing packet classifies Interpolate, so
// the generic render-pose write was suppressed for it —
// this block must therefore commit the landing pose to the
// render entity itself, from the RESOLVED body, exactly as
// the two branch tails below do. Before route 4a the
// generic write had already put the entity at worldPos,
// which is the same value the snap above just installed;
// without this the rendered pose lags one frame until
// RemotePhysicsUpdater re-projects it. rmState.CellId is
// the server cell adopted at the top of this arm.
entity.SetPosition(rmState.Body.Position);
entity.ParentCellId = rmState.CellId;
entity.Rotation = rmState.Body.Orientation;
// The motion bindings still have to exist before the next
// per-tick commit can dispatch this remote's ground edge.
// Only the HitGround CALL moved (see the block comment);
// binding is the packet's own responsibility.
if (_animatedEntities.TryGetValue(entity.Id, out var aeForLand)
&& aeForLand.Sequencer is not null)
{
_motionRuntime.EnsureRemoteMotionBindings(rmState, aeForLand, update.Guid);
}
// Bug A investigation (2026-08-04, docs/ISSUES.md #32):
// the packet-side half of the landing capture. Bug B moved
// the HitGround call itself onto the per-tick
// `set_on_walkable` edge, so `hitGroundInvoked` is now
// false here and the "per-tick" pair is the one that
// reports the dispatch. This line still records the exact
// state the authoritative landing snap installed, which is
// what the discriminator table reads it for.
// TEMPORARY — strip once the live-test run has landed.
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeRemoteLandingEnabled)
{
bool gravitySetForProbe = rmState.Body.HasGravity;
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLanding(
site: "controller",
guid: update.Guid,
airborneBefore: true,
gravitySet: gravitySetForProbe,
contact: rmState.Body.InContact,
onWalkable: rmState.Body.OnWalkable,
hasDefaultSink: rmState.Motion.DefaultSink is not null,
resolveIsOnGround: null,
sequencerStyle: aeForLand?.Sequencer?.CurrentStyle ?? 0,
sequencerMotion: aeForLand?.Sequencer?.CurrentMotion ?? 0);
if (!gravitySetForProbe)
{
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLandingGateNoOp(
"controller", update.Guid);
}
// Zero the sink-dispatch latches before reading them
// back. Nothing at THIS site dispatches — the arming
// call lives only next to the per-tick HitGround — so
// without this the line below would print
// sinkApplyCalls/sinkLastMotion/sinkLastResult left
// over from a previous per-tick capture on this
// thread and invite a reader to attribute them to the
// packet. Zeros are the honest report here.
AcDream.Core.Physics.PhysicsDiagnostics
.BeginRemoteLandingDispatchCapture();
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLandingAfter(
site: "controller",
guid: update.Guid,
hitGroundInvoked: false,
sequencerStyle: aeForLand?.Sequencer?.CurrentStyle ?? 0,
sequencerMotion: aeForLand?.Sequencer?.CurrentMotion ?? 0,
forwardCommand: rmState.Motion.InterpretedState.ForwardCommand);
}
return;
}
// ── GROUNDED ROUTING (CPhysicsObj::MoveOrTeleport) ────────────
// C4 routes 4a + 4b-2: the complete near/far/leftover decision
// is the SAME shared entry point the NPC arm below calls —
// retail's disassembly makes no `this == player` distinction
// on any of these branches. The player arm reaches it only
// with Airborne == false (the landing block above returns), so
// the airborne carve-out inside is inert here.
bool willBeDrTicked = WillAdvanceRemoteMotion(update.Guid, rmState);
RemoteContactRouting playerRouting = ApplyRemoteContactRouting(
_remotePlacementDrive,
acceptedPositionCanonical,
rmState,
earlyRemoteRoute,
worldPos,
rot,
willBeDrTicked);
// C4 route 4b-2: the far arm is re-entrant — the canonical
// placement publishes its Place receipt (or, on a non-commit
// outcome, its cancellation receipt) synchronously, and the
// production projection sink can delete or replace this
// incarnation from inside either. Re-validate before ANY
// further write for this packet, exactly as the landing block
// does after MovementManager.HitGround.
//
// R5 review fix: this now sits BEFORE the leash arming, which
// is the same order the NPC arm has always had — the two arms
// were mirror images of each other and one of them had to be
// wrong. Arming is a write (it stamps rmState.Host's
// PositionManager), and this class's own rule is that nothing
// may be written through a superseded owner. The residual
// versus retail's unconditional arm on a nonzero
// MoveOrTeleport return is AP-138.
if (playerRouting.Arm is RemoteContactArm.FarSnapPlacement
&& (!IsCurrentPositionOwner(entity)
|| !ReferenceEquals(
positionRecord.RemoteMotionRuntime,
rmState)))
{
return;
}
// D2: ConstrainTo arms strictly AFTER the operation, anchored
// post-move — retail arms it only once MoveOrTeleport returns
// nonzero (@0x00454254/@0x00454272), which the near AND far
// branches both do (@0x005163BE, @0x005163E8). The far branch
// arms on EVERY placement outcome, including a failed one:
// retail discards SetPositionSimple's SetPositionError and
// returns 1 regardless. Every remaining classification already
// armed through the legacy pre-operation call above.
//
// Delta review N4: retail's arm is unconditional, acdream's is
// not — the currency guard immediately above returns without
// arming when the far arm's synchronous receipt replaced or
// deleted this incarnation. That one-packet gap is the third
// part of AP-138, and this comment must not read as though the
// arm below is reached on every far-snap outcome.
RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation(
earlyRemoteRoute,
rmState);
// Track the UP-derived synth velocity for diagnostics
// ([VEL_DIAG] pace comparison). L.2g S5 (2026-07-02): the
// #39-era cycle-refinement call that used to live here is
// DELETED — retail never adapts a remote's animation from
// observed pace (deviation map DEV-2; premise refuted at
// decomp + ACE source + the S0 live capture, which shows
// explicit 0x0005↔0x0007 UMs on every Shift toggle).
// Player-remote cycles are UM-driven only.
if (rmState.PrevServerPosTime > 0.0)
{
double nowSecVel = rmState.LastServerPosTime;
double dtPos = nowSecVel - rmState.PrevServerPosTime;
if (dtPos > 0.001)
{
var synthVel = (worldPos - rmState.PrevServerPos) / (float)dtPos;
rmState.ServerVelocity = synthVel;
rmState.HasServerVelocity = true;
}
}
// Sync the visible entity to the body — overrides the
// entity.SetPosition(worldPos) write at the top of this
// function (TryApplyGenericRemoteRenderPose, suppressed for
// the two 4a classifications). This prevents a 1-frame
// teleport-then-yank-back rubber-band as TickAnimations
// chases worldPos via the queue.
//
// C4 route 4b-2 review fix — this used to claim "For the
// far-snap branch this is a no-op (body == worldPos)". The
// PROVENANCE claim behind that was always wrong and is what
// matters here: the far arm's body pose comes from the
// canonical accepted destination resolved through Runtime's
// world frame (a committed placement, a park's snap, or
// store_position), never from the caller's separately-derived
// worldPos, and this write is what carries that canonical pose
// to the render entity.
//
// Delta review N4 — the VALUE claim, stated correctly: on the
// store_position path the two happen to be equal, because #283
// proved App's streaming origin and Runtime's world frame
// cannot disagree and both compose the same accepted origin.
// It is genuinely NOT a no-op on the committed path, where the
// body carries the collision-settled spherePath.CurPos. Do not
// "simplify" this write away on the strength of the equal case.
//
// #184 Slice 2b: sync the player-remote shadow to the RESOLVED/placed
// body (mirrors the NPC UP-branch tail). Now that grounded players run
// the DR-tick sweep + shadow-follows-resolved, the retired raw-pos sync
// (top of this handler) would have re-snapped the shadow into overlap
// each UP; this keeps collision == render for the first UP (before any
// DR tick) and for no-Sequencer players. rmState.CellId is the server
// cell adopted above (:5735). The per-tick loop keeps it current
// between UPs (pose-gated). Commit the complete root before the
// publication gate, matching SetPositionInternal ordering.
entity.SetPosition(rmState.Body.Position);
entity.ParentCellId = rmState.CellId;
entity.Rotation = rmState.Body.Orientation;
AcDream.App.Physics.LiveEntityShadowPublisher.TryPublishRemote(
_liveEntities,
positionRecord,
entity,
rmState,
acceptedPositionAuthorityVersion,
() => _remotePhysicsUpdater.SyncRemoteShadowToBody(
entity.Id,
rmState,
_origin.CenterX,
_origin.CenterY));
return;
}
double nowSec = (now - System.DateTime.UnixEpoch).TotalSeconds;
// ── AIRBORNE NO-OP (C4 route 4a / D1) ────────────────────────────
// The exact mirror of the player-remote arm above. Retail's
// MoveOrTeleport makes no `this == player` distinction: arg4 == 0
// returns 0 @0x0051636D and writes nothing, so an NPC remote's
// wire not-in-contact packet must no longer hard-snap the body,
// decide an animation cycle from a synthesized arc velocity, sync
// the render entity, or publish a collision shadow. This branch
// is now driven by the WIRE has_contact bit through the
// classifier, not by the client-tracked rmState.Airborne flag
// (which is what D1 was).
//
// Two acdream-only per-packet bookkeeping writes are deliberately
// KEPT here, exactly as the player arm has always kept them (see
// register row AP-135): the server cell id, which acdream's own
// per-tick free-fall ResolveWithTransition sweep gates on
// (rm.CellId != 0) and without which an airborne remote falls
// through the floor, and the last-server-position sample, without
// which the first grounded packet after the arc would synthesize
// its velocity across the whole jump.
if (RuntimeRemoteSteadyStatePosition.IsAirborneNoOperation(
earlyRemoteRoute))
{
rmState.CellId = p.LandblockId;
rmState.LastServerPos = worldPos;
rmState.LastServerPosTime = nowSec;
return;
}
System.Numerics.Vector3? serverVelocity = update.Velocity;
if (serverVelocity is null
&& !IsPlayerGuid(update.Guid)
&& rmState.LastServerPosTime > 0.0)
{
double elapsed = nowSec - rmState.LastServerPosTime;
if (elapsed > 0.001)
serverVelocity = (worldPos - rmState.LastServerPos) / (float)elapsed;
}
if (serverVelocity is { } authoritativeVelocity)
{
rmState.ServerVelocity = authoritativeVelocity;
rmState.HasServerVelocity = true;
}
else if (!IsPlayerGuid(update.Guid))
{
rmState.ServerVelocity = System.Numerics.Vector3.Zero;
rmState.HasServerVelocity = false;
}
// R5-V3 #171 residual (2026-07-04 gate: "flashing/flapping",
// stale facing, pushed-into-player): while an entity is STUCK,
// the sticky steer owns its frame — retail's UP corrections flow
// through the InterpolationManager into the SAME adjust_offset
// chain where StickyManager OVERWRITES them while armed
// (PositionManager::adjust_offset 0x00555190 order; sticky
// assigns m_fOrigin 0x00555430), so a server correction can
// never fight an armed stick frame-by-frame. This legacy NPC
// path hard-snaps OUTSIDE that chain, producing a visible
// snap-out/steer-back oscillation at UP cadence (position) and
// a stale-facing stomp (orientation). Faithful translation to
// the snap architecture: suppress the position/orientation/
// velocity snaps while stuck. LastServerPos/Time bookkeeping
// still records below — server truth reasserts on the first UP
// after unstick (bounded by the 1 s sticky lease). Register
// row with TS-41/TS-44.
bool snapSuppressedByStick =
(rmState.Host?.PositionManager.GetStickyObjectId() ?? 0u) != 0u;
if (snapSuppressedByStick
&& AcDream.Core.Physics.PhysicsDiagnostics.ProbeStickyEnabled)
{
float snapDist = System.Numerics.Vector3.Distance(
worldPos, rmState.Body.Position);
Console.WriteLine(FormattableString.Invariant(
$"[sticky-snap-skip] guid=0x{update.Guid:X8} d={snapDist:F3} srv=({worldPos.X:F2},{worldPos.Y:F2}) body=({rmState.Body.Position.X:F2},{rmState.Body.Position.Y:F2})"));
}
var npcRouting = new RemoteContactRouting(
RemoteContactArm.UnroutedCatchUp, Placement: null);
if (!snapSuppressedByStick)
{
// C4 routes 4a + 4b-2: the complete near/far/leftover decision
// is the SAME shared entry point the player-remote branch
// above calls; retail's MoveOrTeleport (0x00516330) makes no
// `this == player` distinction, so the two per-kind copies
// became one. TS-44's sticky suppression stays an NPC-only
// CALLER gate (this `if`), which is what its register row
// describes and what the player arm has never had.
//
// #184 (2026-07-07): an AIRBORNE body keeps its authoritative
// hard-snap (the arc integrates locally, K-fix15), and that
// decision is taken FIRST — a landing packet classifies
// Interpolate, so letting route 4a's branch see it before the
// airborne test would enqueue a body that must plant, and a
// creature knocked off a ledge would glide down over a packet
// interval. Physics digest 2026-07-07 banner.
npcRouting = ApplyRemoteContactRouting(
_remotePlacementDrive,
acceptedPositionCanonical,
rmState,
earlyRemoteRoute,
worldPos,
rot,
WillAdvanceRemoteMotion(update.Guid, rmState));
// C4 route 4b-2: the far arm is re-entrant (see
// ApplyRemoteContactRouting's own remarks). Re-validate before
// any further write for this packet — including the leash
// arming below, which is why the player arm now runs this
// check in the SAME position relative to its own arming call
// (R5 review fix).
if (npcRouting.Arm is RemoteContactArm.FarSnapPlacement
&& (!IsCurrentPositionOwner(entity)
|| !ReferenceEquals(
positionRecord.RemoteMotionRuntime,
rmState)))
{
return;
}
}
// D2: ConstrainTo arms strictly AFTER the operation above,
// anchored post-move (@0x00454272, inside the
// `if (MoveOrTeleport(...) != 0)` at @0x00454254), and only for a
// classification the post-operation arm owns — every other one
// already armed the legacy pre-operation call above, exactly as
// before. Retail's ConstraintManager leash is independent of the
// acdream-only TS-44 sticky suppression (which only concerns the
// enqueue/snap/placement above), so this deliberately sits OUTSIDE
// the snapSuppressedByStick gate: a stuck NPC's leash still
// re-arms every accepted Position exactly as it did before this
// route split the single call into a per-branch pair.
RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation(
earlyRemoteRoute,
rmState);
// K-fix15 (2026-04-26): DON'T auto-clear airborne on UP.
// ACE broadcasts UPs during the arc (peak / mid-fall / land)
// at ~5-10 Hz. The previous K-fix9 logic cleared Airborne on
// the FIRST UP after the jump, which:
// * restored Contact + OnWalkable,
// * removed the Gravity flag,
// * caused the next per-tick to stomp Velocity via
// apply_current_movement (reading InterpretedState =
// Ready, so Velocity.Z went to 0),
// …so the body got stuck at the server-broadcast apex Z,
// visibly hovering. The fix: leave Airborne true; the
// per-tick post-resolve logic detects an actual landing
// (resolveResult.IsOnGround && Velocity.Z <= 0) and clears
// it then. Mirrors how PlayerMovementController re-grounds
// the local player at the bottom of its arc.
//
// The position-snap above is still authoritative — if ACE
// says the body is at Z=68 mid-arc, we render Z=68. But we
// continue integrating gravity from there, so the body
// proceeds along the parabolic path between UPs.
// Adopt the server's cell ID as the transition starting cell.
// Retail authoritatively hard-snaps cell membership here too; our
// per-tick ResolveWithTransition sweep then advances CheckCellId
// as the sphere crosses cells and writes the new cell back into
// rmState.CellId so the NEXT frame starts in the correct cell.
//
// C4 route 4b-2: NOT after a far snap — see
// TryAdoptWireCellAfterRouting for the rule and why it applies to
// this arm and not the player one.
TryAdoptWireCellAfterRouting(rmState, npcRouting.Arm, p.LandblockId);
// Near UpdatePosition orientation is carried by the same complete
// interpolation Frame as translation. Placement, airborne, and
// far-correction branches above install the authoritative Frame
// directly. Sticky still receives the shared Frame afterward and
// may replace it while armed.
rmState.LastServerPos = worldPos;
rmState.LastServerPosTime = nowSec;
if (rmState.HasServerVelocity
&& !snapSuppressedByStick
&& _animatedEntities.TryGetValue(entity.Id, out var aeForVelocity))
{
// NPC/monster remotes: PlanFromVelocity cycle selection from
// UP-derived velocity (ACE broadcasts NPC motion patterns the
// UM stream alone doesn't cover). Player remotes return early
// inside — their cycles are UM-driven only per retail (L.2g
// S5; DEV-2 deleted). Unification of NPCs onto the
// CMotionInterp funnel is S6.
//
// D2 (Commit A 2026-05-03): tag whether the velocity feeding
// ApplyServerControlledVelocityCycle is wire-explicit or
// synthesized from position deltas (the common case).
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
{
string velSrc = update.Velocity is null ? "synth" : "wire";
System.Console.WriteLine(
$"[UPCYCLE_SRC] guid={update.Guid:X8} src={velSrc}");
}
RemoteServerControlledVelocityCycle.Apply(
update.Guid,
aeForVelocity,
rmState,
rmState.ServerVelocity);
}
// #184: sync the NPC shadow to the resolved/placed body (NOT the raw
// server pos — the raw-pos sync was RETIRED for players too in Slice 2b)
// so collision == render and the de-overlap isn't snapped away each UP.
// Covers the first UP (before any DR tick) and no-Sequencer NPCs (which
// the per-tick loop skips). The per-tick loop keeps it current between
// UPs, pose-gated. rmState.CellId is the server cell adopted above.
// The root frame is committed before collision publication, as in
// retail SetPositionInternal.
entity.SetPosition(rmState.Body.Position);
entity.ParentCellId = rmState.CellId;
entity.Rotation = rmState.Body.Orientation;
AcDream.App.Physics.LiveEntityShadowPublisher.TryPublishRemote(
_liveEntities,
positionRecord,
entity,
rmState,
acceptedPositionAuthorityVersion,
() => _remotePhysicsUpdater.SyncRemoteShadowToBody(
entity.Id,
rmState,
_origin.CenterX,
_origin.CenterY));
}
// F751 is only a notification gate; the accepted Position may arrive
// before or after it. Canonical physics above always consumes the
// packet first. The presentation coordinator exposes exactly one
// sequence-correlated destination without reordering that state.
if (timestampDisposition is AcDream.Core.Physics.PositionTimestampDisposition.Apply
&& update.Guid == _playerServerGuid)
{
_localPlayerTeleport.OfferDestination(
RuntimeTeleportDestinationAdapter.FromAcceptedPosition(
update),
timestamps.TeleportAdvanced);
}
}
internal static bool RequiresSpatialProjectionRecovery(
LiveEntityRecord record)
{
ArgumentNullException.ThrowIfNull(record);
return record.WorldEntity is null || !record.IsSpatiallyProjected;
}
// Retail teleport transit: the 7-state TAS drives portal/view-plane presentation, holds the
// player in PortalSpace until the destination is resident (TeleportWorldReady), then
// fires Place (materialize) and FireLoginComplete (regain control + ack the server).
// Replaces the old TeleportArrivalController hold/place machine.
}