acdream/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs
Erik e0f96a55bf fix(physics): C4 route 3 — portal placement authority (local player)
Removes a duplicate placement authority for local-player portal arrival.
Portalling worked before this change and works after it — this is not a
bug fix, EXCEPT that it found and fixed one dead-code production bug.

THE PRODUCTION BUG: TryExecuteCanonicalPortalPlacement re-read the
accepted destination at Place time, but TryBeginPortalReveal already
consumes that slot at Aim time — so the arm was 100% dead code and every
real portal Place refused with host-token-unavailable. Found only
because we refused to accept 7 skipped tests instead of chasing the
count to zero.

RETAIL IS THE GENERIC PATH FOR THE THIRD ROUTE RUNNING:
SmartBox::TeleportPlayer @0x00453910 = SetPositionSimple(dest, 1) with
flags 0x1012, followed by PlayerPositionUpdated.

BOTH INVERSIONS, WITH THEIR ANCHORS: unlike route 2, the leash IS armed
here (ConstrainTo @0x0045418A) and velocity is zeroed
(set_velocity @0x004541B4); unlike route 4b-3, the local teleport_hook
runs AFTER placement (@0x004538AE).

THE THREE-ROUND DEFECT CHAIN, HONESTLY:
- Round 1 released the player at the pre-teleport position while the
  anim stream marched on — the contract wrongly assumed Place re-fires
  (process rule 1's third occurrence this campaign).
- Round 2's fix inferred commit from a global PendingCount, which three
  non-committing paths also clear — making the SAME bug complete
  cleanly and silently. Strictly worse than round 1: round 1 at least
  tripped portal-complete-before-materialized.
- Round 3 latches the commit where it actually happens
  (ReconcileAndAcknowledgePortal), keyed on reveal generation and
  teleport sequence, via TryConsumePortalCommit. Two of the three
  required regression tests landed and are sabotage-verified on both
  hosts (ParkedPlace_ForgottenByOrdinaryMergeDoesNotLatchAsCommitted /
  HeadlessPortalPrepareDestinationForgottenByOrdinaryMergeDoesNotLatchAsCommitted).
  The third (force-arm-takes-the-slot) was judged unnecessary on review:
  with the inference gone, PendingCount is only a "don't ask yet" guard
  at both gates, so a force operation occupying or vacating the slot no
  longer changes an input the commit decision reads — the case collapses
  into what the landed test already discriminates.

THE B2/P3 RESOLUTION: both round-2 reviews were right about different
branches of the same synchronous call. RuntimePlacementProjectionSubscription
.OnPlacement acknowledges the FIFO head only when TryApply returns true;
a Place whose portal authority went stale (transit ended/superseded
while parked) used to return false, wedging every later entity's
placement receipt behind it forever. Both sinks
(RuntimePlacementPresentationSink, HeadlessRuntimePlacementProjectionSink)
now acknowledge-and-ignore a stale-authority Place instead of refusing
it. The regression test (RuntimePlacementPresentationSinkTests
.PortalPlace_StaleTransitHostOrSequenceIsAcknowledgedAndIgnored) had
been asserting the old, wrong `false` behaviour; it now asserts and
sabotage-verifies the fix.

Also lands: AP-144 (register discipline — the portal movement-event
send reuses the stricter UsePositionFromServer gate where retail's
SendMovementEvent is the looser autonomy_level != 0 test, diverging
only at level 1, currently unreachable), AP-145 + issue #318 (the
local-player collision-shadow presentation write bypasses its own
publisher's ShadowObjects write via a direct cache .Set(), self-healing
only once dedup diverges — filed, not fixed, pending a composition
test), AD-42 deleted (its last citation retired by the canonical portal
arm), AD-2 updated (the wait-cue's trigger predicate now covers a
second cause), and two documentation corrections: the enter_world
misattribution (both call sites are in SmartBox::HandleCreateObject,
only one in the player branch — portal arrival is TeleportPlayer, not
enter_world) and the stale "local player never reaches this path"
comment on the generic-remote-render-pose write.

Suite: 11,090 passed / 4 skipped / 0 failed. No new skips, nothing
weakened.

STILL OWED: the connected two-client gate, with
ACDREAM_PROBE_LOCAL_TELEPORT=1, scored only if [local-tp] lines
actually appear in the capture — and explicitly NOT scored as covering
issue #318 (no composition test yet asserts PhysicsEngine.ShadowObjects
directly).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 03:57:37 +02:00

2986 lines
159 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// Update-thread owner of retail SmartBox's accepted Movement, Vector, State,
/// and Position presentation transactions. Identity and timestamp authority
/// remain canonical in <see cref="LiveEntityRuntime"/>; this controller only
/// routes an accepted exact incarnation into its App-layer owners.
/// </summary>
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 LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _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;
/// <summary>
/// C4 route 4b-2: the Runtime-owned remote placement seam. Route 4b-1
/// landed it with no production caller; the remote far snap
/// (<c>SetPositionSimple</c>, <c>player_distance &gt;= 96 m</c>) is its
/// first, so its ownership ledger stops being tautologically zero here.
/// </summary>
private readonly RuntimeRemotePlacementDriveController _remotePlacementDrive;
/// <summary>
/// #315 (closed by the OnPosition collapse, 2026-08-04): scratch fields
/// backing <see cref="_remoteArmCallbacks"/> — the two delegates
/// <see cref="RunRemoteArmTail"/> passes into
/// <see cref="ApplyRemoteContactRouting"/> every accepted remote Position
/// (5-10 Hz per remote). Before the collapse there were three duplicated
/// call sites, each allocating a fresh closure per packet regardless of
/// whether the packet was a teleport; the collapse converged them to one,
/// which is what makes caching worthwhile — one cached pair now serves
/// every remote guid. <see cref="RunRemoteArmTail"/> stamps these
/// fields from its own parameters immediately before use; nothing reads
/// them between calls, so last-remote staleness between packets is
/// harmless (mirrors the existing per-instance scratch-field pattern,
/// e.g. <c>RemoteMotion.PositionManagerDeltaScratch</c>).
/// </summary>
private RuntimeEntityRecord? _remoteArmCanonical;
private RemoteMotion? _remoteArmMotion;
private LiveEntityRecord? _remoteArmPositionRecord;
private ulong _remoteArmPositionAuthorityVersion;
private AcDream.Core.World.WorldEntity? _remoteArmExpectedEntity;
/// <summary>
/// B4 fix (C4 route 5 round-2 architecture review): the same #315
/// cached-delegate discipline for the adopted-body missile arm's
/// teleport-hook currency check
/// (<see cref="RemoteArmCallbacks.IsCurrentProjectilePositionOwner"/>).
/// The missile dispatch block stamps these scratch fields immediately
/// before calling <see cref="RunRemoteTeleportHook"/> instead of
/// allocating a fresh <c>Func&lt;bool&gt;</c> closure every accepted
/// missile packet — the same defect class the remote arm's own #315
/// collapse already closed, flagged independently by both round-2
/// reviews for this arm.
/// </summary>
private LiveEntityRecord? _projectileArmPositionRecord;
private ulong _projectileArmPositionAuthorityVersion;
/// <summary>
/// #315: the two per-packet delegates cached ONCE (constructed here,
/// reused for every accepted remote Position) rather than allocated
/// fresh every packet. Deliberately its own small type, not two bare
/// <c>Func&lt;bool&gt;</c> fields directly on this class:
/// <c>tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs</c>'s
/// <c>ProductionFrameAdaptersRetainTypedOwnersWithoutWindowCallbacks</c>
/// asserts every typed production owner (this class included) carries
/// ZERO <c>Delegate</c>-typed fields — the GameWindow decomposition
/// campaign's guard against a callback silently smuggling a window
/// reference back in. Neither delegate here touches a window (both are
/// bound to this controller alone), but the rule is written as a
/// blanket field-type check, not a window-specific one, so the cache
/// lives in its own named type instead of tripping it.
/// </summary>
private sealed class RemoteArmCallbacks
{
internal readonly Func<bool> IsCurrentPositionOwner;
internal readonly Func<bool> RunTeleportHook;
/// <summary>
/// B4 fix: the projectile (missile) arm's currency check, cached
/// the same way as the two remote-arm delegates above rather than
/// allocated fresh per accepted missile packet. Bound to
/// <see cref="IsCurrentProjectileArmPositionOwner"/>, which reads
/// the <c>_projectileArmPosition*</c> scratch fields the missile
/// dispatch block stamps immediately before use.
/// </summary>
internal readonly Func<bool> IsCurrentProjectilePositionOwner;
internal RemoteArmCallbacks(LiveEntityNetworkUpdateController owner)
{
IsCurrentPositionOwner = owner.IsCurrentRemoteArmPositionOwner;
RunTeleportHook = owner.RunCachedRemoteTeleportHook;
IsCurrentProjectilePositionOwner =
owner.IsCurrentProjectileArmPositionOwner;
}
}
private readonly RemoteArmCallbacks _remoteArmCallbacks;
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,
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> 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<uint, AcceptedPhysicsTimestamps> 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));
_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;
// #315: cached once, reused for every accepted remote Position — see
// the field docs above _remoteArmCanonical.
_remoteArmCallbacks = new RemoteArmCallbacks(this);
}
internal void ResetSessionState() => _authorityGate.ResetSessionState();
private static bool IsPlayerGuid(uint guid) =>
(guid & 0xFF000000u) == 0x50000000u;
private static bool IsDoorName(string? name) => name == "Door";
/// <summary>
/// #270 (2026-07-30): retail spawns run the placement transition
/// (<c>CPhysicsObj::SetPosition</c> → <c>SetPositionInternal</c>
/// 0x00515330), which establishes CONTACT/ON_WALKABLE from the floor the
/// creature stands on. A raw position seed leaves the fresh body
/// airborne-flagged, and <c>contact_allows_move</c> (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
/// <c>apply_interpreted_movement</c>). Mirrors the canonical Runtime
/// placement commit (<c>RuntimeSetPositionState</c>) with spawn-shaped
/// inputs (no prior contact).
/// </summary>
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;
}
/// <summary>
/// R6/R2 fix round (2026-08-04). Two independent fixes:
///
/// <para>
/// <b>R6.</b> Previously re-resolved the <c>RemoteMotion</c>/host BY
/// GUID instead of using the <c>rmState</c> the caller already holds and
/// hands to <see cref="ApplyRemoteContactRouting"/>. Every action was
/// null-conditional (<c>remote?.</c>/<c>host?.</c>), so a resolution
/// mismatch silently no-op'd all six actions while
/// <see cref="RemoteTeleportHook.Execute"/> still returned <c>true</c> —
/// and <c>PhysicsDiagnostics.LogRemoteTeleport</c> printed
/// <c>hookRan=True</c> for a hook that did nothing. <paramref name="remote"/>
/// is now the caller's own <c>rmState</c> directly (never null, no
/// lookup, no mismatch risk) and <c>host</c> comes from
/// <see cref="RemoteMotion.Host"/> — the SAME bound reference, not a
/// second independent resolution. A null host is now a genuine "the
/// manager doesn't exist yet" case (retail's own "each guarded on the
/// manager existing"), not a resolution bug.
/// </para>
///
/// <para>
/// <b>R2.</b> <c>ReportCollisionEnd</c> — retail
/// <c>report_collision_end(this, 1)</c> @0x00514F31 → @0x00514620, a
/// force-end-all of the collision TABLE with bidirectional
/// <c>DoCollisionEnd</c> — used to call <c>ShadowObjects.Suspend</c>,
/// which ports a DIFFERENT retail function
/// (<c>remove_shadows_from_cells</c>) that <c>teleport_hook</c> never
/// calls, and which retail does NOT do at this call site (a real, if
/// frames-scale, added divergence — the shadow un-suspends again inside
/// the arm tail's own <c>LiveEntityShadowPublisher.TryPublishRemote</c>,
/// exactly like every other placement, so nothing else needs to
/// compensate for dropping it here). Now routes through
/// <see cref="LiveEntityRuntime.ForceEndCollisionReporting"/>, which
/// forwards to <c>RuntimeCollisionReportingState.LeaveWorld</c> — the
/// existing, unreached-until-now, exact port of this retail call.
/// </para>
///
/// <para>
/// Round-2 architecture review B2 — do NOT record this as a closed
/// observable delta. The plumbing is retail-correct, but
/// <c>IRuntimeCollisionReportObserver</c> has ZERO production
/// implementations, so retail's bidirectional <c>DoCollisionEnd</c> half
/// still reaches no gameplay consumer. What this fix closes is the wrong
/// retail-function binding; what stays open is that nobody listens. That
/// remains true until an observer ships.
/// </para>
/// </summary>
private bool RunRemoteTeleportHook(
RuntimeEntityRecord canonical,
RemoteMotion remote,
Func<bool> isCurrent)
{
EntityPhysicsHost? host = remote.Host;
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: () =>
_liveEntities.ForceEndCollisionReporting(canonical)),
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;
}
/// <summary>
/// Resolves one live remote owner and delegates retail's entire
/// <c>unpack_movement</c> body to the shared animation-optional packet
/// dispatcher. The render PartArray contributes only its optional sink.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <summary>
/// Reports whether the exact remote component currently belongs to the
/// visible ordinary-object workset that consumes interpolation targets.
/// </summary>
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);
}
/// <summary>
/// C4 route 4a: asks Runtime to classify one remote's accepted Position.
/// App contributes only retail's <c>player_distance</c> — the live
/// physics-controller distance, and <see langword="null"/> (never a
/// fabricated <c>Vector3.Zero</c>) when no controller exists yet, which
/// makes Runtime decline. Callers must only invoke this for a genuinely
/// remote (never local-player) entity.
///
/// <para>
/// There is no legacy path: the duplicated App-side near/far blocks were
/// deleted at C4 route 4b-2, and a declined classification takes the
/// stated <c>UnroutedCatchUp</c> policy (AP-137) through
/// <c>ApplyRemoteContactRouting</c>'s default arm. The
/// <see langword="null"/> return is still "Runtime has no opinion", never
/// "rejected"; what the caller does with it is
/// <c>ApplyRemoteContactRouting</c>'s job, not this method's.
/// </para>
/// </summary>
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);
/// <summary>
/// C4 route 4a: the generic top-of-<c>OnPosition</c> 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.
///
/// <para>
/// 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
/// <c>MoveOrTeleport</c> 0x00516330 returns 0 @0x0051636D). Writing the
/// wire pose here first would be the second writer route 2's original
/// defect consisted of.
/// </para>
///
/// <para>
/// The gate is <c>OwnsSteadyState</c> — unchanged by C4 routes 4b-2 or
/// 4b-3. The far snap AND the teleport arm both DO take the wire-pose
/// write here even though each goes on to place canonically. That is
/// deliberate and is not a second writer in the route 2 sense: both
/// arms' tails re-sync the render entity from the RESOLVED body
/// afterwards, so this write only covers the window before the placement
/// commits.
/// </para>
///
/// <para>
/// 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
/// <c>RebucketLiveEntity</c> is the only site that moves an ordinary
/// moving remote's draw bucket, commits its canonical <c>FullCellId</c>,
/// and recovers a pending bucket promotion, and it must keep running for
/// both 4a classifications.
/// </para>
///
/// <para>
/// The <see langword="bool"/> 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.
/// </para>
/// </summary>
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;
}
/// <summary>Which arm of the remote contact routing claimed a packet. The
/// value is the seam's observable outcome, asserted by the acceptance
/// tests; production distinguishes
/// <see cref="RemoteContactArm.FarSnapPlacement"/> and (C4 route 4b-3)
/// <see cref="RemoteContactArm.TeleportPlacement"/> — the two arms that
/// execute a canonical placement and are therefore re-entrant — but the
/// finer result is what makes the PRECEDENCE and the arm selection
/// testable and must not be collapsed to a bool.</summary>
internal enum RemoteContactArm : byte
{
/// <summary>The body was in free flight — NOT in contact with any
/// surface. Hard-snapped. Gated on the body's own
/// <c>Body.InContact</c> ALONE (AP-140, retired 2026-08-04) — 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
/// <c>SetPosition</c>, or rejected), because the
/// <c>IsAirborneNoOperation</c> early return fires only for
/// classifications it does own. That matches pre-4a behaviour.
///
/// <para>The name is retained for continuity with route 4a; read
/// "airborne" here as retail's CONTACT_TS-clear, not as the client
/// <c>RemoteMotion.Airborne</c> flag, which is
/// <c>!Body.OnWalkable</c> — a strictly WIDER set. The two disagree on
/// a body in contact with a non-walkable face (a steep slide), which
/// retail interpolates.</para></summary>
AirborneSnap,
/// <summary>Route 4a's near InterpolateTo branch.</summary>
SteadyStateInterpolate,
/// <summary>C4 route 4b-2: retail's far snap — <c>StopInterpolating</c>
/// @0x005163CB then <c>SetPositionSimple</c> @0x005163D9 — executed
/// through the canonical Runtime placement owner.</summary>
FarSnapPlacement,
/// <summary>
/// C4 route 4b-3: retail's teleport/cell-less branch —
/// <c>teleport_hook</c> @0x00514ED0 then <c>SetPosition</c>
/// @0x00516420 — executed through the canonical Runtime placement
/// owner. Decided AHEAD of the contact test (D5): a teleport packet
/// never takes <see cref="AirborneSnap"/> regardless of wire or body
/// contact.
/// </summary>
TeleportPlacement,
/// <summary>The acdream-only leftover set (null, <c>Rejected*</c>).
/// See <see cref="RuntimeRemoteAcceptedPositionArm.UnroutedCatchUp"/>
/// for the stated policy.</summary>
UnroutedCatchUp,
}
/// <summary>
/// The complete observable outcome of one
/// <see cref="ApplyRemoteContactRouting"/> call: which arm claimed the
/// packet, and — for
/// <see cref="RemoteContactArm.FarSnapPlacement"/> alone — what the
/// canonical Runtime placement actually did.
///
/// <para>
/// C4 route 4b-2 review fix: the placement status used to be discarded at
/// the call site (<c>_ = placementDrive.…</c>), 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 <c>MoveOrTeleport</c> likewise discards
/// <c>SetPositionSimple</c>'s <c>SetPositionError</c> and returns 1
/// @0x005163E8 — but the value is now carried out of the seam so the
/// acceptance tests assert the commit path and the
/// <c>store_position</c> fallback path apart from each other.
/// <see cref="Placement"/> is <see langword="null"/> for every arm that
/// performs no placement.
/// </para>
/// </summary>
internal readonly record struct RemoteContactRouting(
RemoteContactArm Arm,
RuntimeRemotePlacementExecutionStatus? Placement);
/// <summary>
/// The complete remote grounded/contact routing for ONE accepted Position,
/// shared by every remote guid through the single collapsed
/// <c>OnPosition</c> tail (C4 route 4b-3 collapse, 2026-08-04) — retail's
/// <c>CPhysicsObj::MoveOrTeleport</c> (0x00516330) makes no
/// <c>this == player</c> distinction on any of these branches.
///
/// <para>
/// 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 <c>Interpolate</c>, 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. Before the collapse the
/// player-remote caller had its own pre-check (the standalone LANDING
/// TRANSITION block) that reached this method only with
/// <c>Body.InContact == true</c>, making the carve-out inert for that
/// caller specifically. That pre-check is deleted: the single caller now
/// reaches this method with either contact state for either guid, so the
/// carve-out is live for both — its <c>AirborneSnap</c> result is exactly
/// the dissolved landing scenario, for every guid (see
/// <c>ToConstraintArm</c>'s A1 mapping and <c>OnPosition</c>'s own
/// <c>arm is AirborneSnap</c> handling for the two guid-preserved
/// extras — #316's shadow-publish skip and the interp-clear — that ride
/// along with it).
/// </para>
///
/// <para>
/// C4 route 4b-2 added <see cref="RemoteContactArm.FarSnapPlacement"/> and
/// deleted the two duplicated App-side near/far blocks that used to follow
/// this call; C4 route 4b-3 added
/// <see cref="RemoteContactArm.TeleportPlacement"/>, decided AHEAD of
/// everything else (see below). <b>The far and teleport arms are the only
/// re-entrant ones</b> — a canonical placement publishes its
/// <c>Place</c> 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 either arm, on EVERY placement status, before writing
/// anything else for the packet. That includes the <c>ConstrainTo</c>
/// leash: every caller therefore runs the re-validation FIRST and arms
/// second (see AP-138).
/// </para>
/// </summary>
internal static RemoteContactRouting ApplyRemoteContactRouting(
RuntimeRemotePlacementDriveController placementDrive,
RuntimeEntityRecord canonical,
RemoteMotion remote,
RuntimeAuthoritativePositionRoute? route,
System.Numerics.Vector3 worldPos,
System.Numerics.Quaternion rotation,
bool willBeDrTicked,
Func<bool> runTeleportHook)
{
ArgumentNullException.ThrowIfNull(placementDrive);
ArgumentNullException.ThrowIfNull(canonical);
ArgumentNullException.ThrowIfNull(remote);
ArgumentNullException.ThrowIfNull(runTeleportHook);
// C4 route 4b-3 (D5): retail decides the teleport/cell-less branch
// BEFORE reading arg4 (the wire contact bit) —
// `MoveOrTeleport`'s @0x00516375-@0x00516386 test runs before
// @0x0051638E. A teleport-classified packet therefore places
// unconditionally, ahead of the free-flight carve-out below — an
// airborne-body teleport packet places, it does not AirborneSnap.
// teleport_hook @0x00514ED0 (the caller-supplied delegate) runs
// BEFORE the placement, regardless of what the placement then
// yields, exactly like retail's ordering
// (@0x005163EF before @0x00516420).
if (RuntimeRemoteTeleportPosition.OwnsTeleportPlacement(route))
{
// R7 (2026-08-04): hookRan is consumed ONLY by the probe below —
// routing proceeds to the placement regardless of its value.
// This is retail-faithful, not a dropped result: retail has no
// currency concept and runs teleport_hook unconditionally before
// ever knowing the placement outcome (@0x005163EF is called
// regardless of what @0x00516420 later yields).
bool hookRan = runTeleportHook();
RuntimeRemotePlacementExecutionStatus teleportStatus =
placementDrive.ApplyAcceptedRemoteTeleport(
canonical,
remote,
route!.Value);
// Live-execution proof (process rule 5): confirms the arm
// actually ran rather than inferring it from a clean-looking
// session. TEMPORARY — strip with ACDREAM_PROBE_REMOTE_TELEPORT.
//
// A6 fix round (2026-08-04): guarded at the call site now — the
// probe's own self-guard inside LogRemoteTeleport did not stop
// teleportStatus.ToString() from being evaluated (and allocated)
// on every teleport regardless of whether the probe was enabled.
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeRemoteTeleportEnabled)
{
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteTeleport(
canonical.ServerGuid,
cause: route.Value.Authority.TeleportAdvanced
? "teleport-ts"
: "cellless",
hookRan,
teleportStatus.ToString());
}
return new RemoteContactRouting(
RemoteContactArm.TeleportPlacement,
teleportStatus);
}
// 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);
// AP-140 (retired 2026-08-04): retail's predicate for exactly this
// decision is CONTACT, not walkability.
// `InterpolationManager::adjust_offset` @0x00555D30 gates its ENTIRE
// body on `physics_obj->transient_state & 1` @0x00555D52 — and
// TransientState bit 0 is `CONTACT_TS` (acclient.h:3690), NOT
// `ON_WALKABLE_TS` (0x2). A retail body in contact with a non-walkable
// face therefore still walks toward its queued waypoint. This gate used
// to read `remote.Airborne`, which is `!Body.OnWalkable` — WALKABILITY,
// a strictly wider set. The two disagree on precisely one state (in
// contact, not on walkable ground), and Bug B (`204d0ae0`) turned that
// state from unreachable into ordinary by deleting the per-tick
// `TransientState |= Contact | OnWalkable` forge that had made every
// non-airborne remote walkable by construction. A remote sliding on a
// steep roof lives in it, and was being hard-snapped at UpdatePosition
// cadence instead of interpolated.
//
// `Airborne` itself is deliberately NOT re-derived from CONTACT: it has
// four writers (C4 route 4b-3 retired the fifth, the deleted
// `RemoteTeleportPlacement.Apply` — its derivation is now the
// canonical placement commit's, already on the list), all spelling
// `!Body.OnWalkable`, and the per-tick updater's ground-clamp branch
// depends on the walkability reading. Only the two ROUTING gates move
// (this one and `OnPosition`'s player-remote landing block).
if (!remote.Body.InContact)
{
// 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 CLEARS the body's CONTACT bit clears the
// queue: it is dropped by the per-tick sweep's SetPositionInternal
// commit (and by TickHidden's resolve) whenever the contact plane
// stops being valid, and by nothing else — the teleport hook's
// StopInterpolating is the only thing that empties the queue on a
// leave-ground edge, and the 0xF74E VectorUpdate (OnVector, below)
// does not. 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; the single production caller
// (`OnPosition`'s collapsed unified tail, since the 2026-08-04
// OnPosition collapse — cited by name because line numbers
// went stale within one review round) early-returns on
// `IsAirborneNoOperation` before it ever routes. 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);
}
}
/// <summary>
/// C4 route 4b-3 (D2): retail's <c>arg4 != 0</c>/<c>== 0</c> return-0
/// shape, applied to the two acdream-only leftover classifications that
/// remain wire-airborne after <c>IsAirborneNoOperation</c>
/// (<c>NoPositionOperation</c>, handled by each caller's own early
/// return before this is ever reached) and the teleport arm (D5 routes
/// it ahead of every contact carve-out — a teleport-classified route
/// must never be caught here, which is why every caller passes
/// <c>isTeleportRoute</c> rather than re-deriving it) — null during the
/// login window, or <c>RejectedAuthority</c>/<c>RejectedData</c>.
/// AP-135's bookkeeping (the server-cell adopt and the
/// <c>LastServerPos</c>/<c>LastServerPosTime</c> sample) is 4a-owned
/// free-fall-sweep/first-grounded-velocity state, not a retail
/// <c>CPhysicsObj</c> field, so it stays; nothing else does — no body
/// write, no queue write, no render write, no leash arm.
///
/// <para>
/// Fix round (2026-08-04, R1/A7): shared by both remote branches so they
/// could not re-diverge on this shape the way they had before that
/// round — the player arm had it, the NPC arm did not, so a wire-airborne
/// null/<c>Rejected*</c> NPC packet fell through to
/// <see cref="ApplyRemoteContactRouting"/>'s own free-flight carve-out
/// and received a body write, an arm, and a render/shadow publish
/// retail's <c>return 0</c> never produces. The OnPosition collapse
/// (2026-08-04) went further: there is now exactly ONE call site per
/// early-return (the <c>IsAirborneNoOperation</c> return and the D2
/// wire-airborne return), reached by every guid, so <paramref
/// name="wireCellId"/> is load-bearing at both — no caller writes it
/// redundantly beforehand any more (the former player-guid pre-routing
/// write this paragraph used to describe is deleted; proven a true
/// no-op by the round-2 architecture review's call-site check, since
/// <c>RebucketLiveEntity</c> commits the identical value earlier in the
/// SAME packet's processing regardless of guid).
/// </para>
/// </summary>
private static void ApplyWireAirborneLeftoverBookkeeping(
RemoteMotion remote,
uint wireCellId,
System.Numerics.Vector3 worldPos,
double nowSec)
{
ArgumentNullException.ThrowIfNull(remote);
remote.CellId = wireCellId;
remote.LastServerPos = worldPos;
remote.LastServerPosTime = nowSec;
}
/// <summary>
/// C4 route 4b-3 fix round (2026-08-04). The routing-decision-plus-
/// currency sequence every remote arm tail performs identically around
/// <see cref="ApplyRemoteContactRouting"/>: run the routing (which
/// itself decides D5's teleport-before-contact ordering and D3's hook
/// timing), then re-validate ownership before writing anything further
/// for this packet (R5's guard-before-arm shape — the far and teleport
/// arms are re-entrant, because their canonical placement publishes its
/// Place or cancellation receipt synchronously and the production
/// projection sink can delete or replace this incarnation from inside
/// either).
///
/// <para>
/// Extracted after two independent reviews found the three hand-written
/// App-layer copies of this sequence that existed at the time (the
/// player arm's teleport dispatch, the player arm's grounded-routing
/// dispatch, the NPC arm's single dispatch) had begun to drift in ways
/// this class's own duplication made invisible: A2/R3 is precisely the
/// player copy's teleport block returning before its synth-velocity code
/// while the NPC copy had no such boundary at all, and A1 is
/// <c>ToConstraintArm</c> having been written against only the player
/// copy's reachable arm set. The OnPosition collapse (2026-08-04) went
/// further and removed the guid-conditional dispatch shape itself: this
/// method now has exactly ONE call site in production, in the unified
/// remote routing tail, reached by every guid — so the "narrow or widen
/// one copy's guard relative to the others'" failure mode this
/// extraction was built to prevent can no longer arise structurally,
/// not merely by convention.
/// </para>
///
/// <para>
/// Returns <see langword="null"/> when nothing further should be
/// written for this packet — the currency guard tripped — and the
/// caller must return without arming, adopting the wire cell, or
/// publishing the render entity/collision shadow. Does NOT itself arm
/// the leash (<see cref="ToConstraintArm"/>'s corrected mapping is
/// applied by the caller, which also needs it for the sticky-suppressed
/// default-arm case where this method is never called) and does NOT
/// itself gate the synth-velocity install (the caller's own
/// <c>isTeleportRoute</c> guard, unconditional for every guid since the
/// collapse).
/// </para>
///
/// <para>
/// #315 (closed here): <paramref name="positionAuthorityVersion"/> and
/// <paramref name="expectedEntity"/> replace what used to be a
/// caller-constructed <c>Func&lt;bool&gt; isCurrentPositionOwner</c> —
/// this method stamps the shared <c>_remoteArm*</c> scratch fields from
/// its own parameters and passes the two CACHED delegates
/// (<see cref="_remoteArmCallbacks"/>) into
/// <see cref="ApplyRemoteContactRouting"/> instead of allocating a fresh
/// closure over <c>canonical</c>/<c>remote</c>/the currency check every
/// packet. Observably identical: the currency check reads the exact same
/// <c>positionRecord</c>/<c>positionAuthorityVersion</c>/<c>expectedEntity</c>
/// triple either way, just from fields instead of a closure.
/// </para>
/// </summary>
private RemoteContactRouting? RunRemoteArmTail(
RuntimeEntityRecord canonical,
LiveEntityRecord positionRecord,
RemoteMotion remote,
RuntimeAuthoritativePositionRoute? route,
uint guid,
System.Numerics.Vector3 worldPos,
System.Numerics.Quaternion rotation,
ulong positionAuthorityVersion,
AcDream.Core.World.WorldEntity? expectedEntity)
{
_remoteArmCanonical = canonical;
_remoteArmMotion = remote;
_remoteArmPositionRecord = positionRecord;
_remoteArmPositionAuthorityVersion = positionAuthorityVersion;
_remoteArmExpectedEntity = expectedEntity;
RemoteContactRouting routing = ApplyRemoteContactRouting(
_remotePlacementDrive,
canonical,
remote,
route,
worldPos,
rotation,
willBeDrTicked: WillAdvanceRemoteMotion(guid, remote),
runTeleportHook: _remoteArmCallbacks.RunTeleportHook);
if ((routing.Arm is RemoteContactArm.FarSnapPlacement
or RemoteContactArm.TeleportPlacement)
&& (!_remoteArmCallbacks.IsCurrentPositionOwner()
|| !ReferenceEquals(positionRecord.RemoteMotionRuntime, remote)))
{
return null;
}
return routing;
}
/// <summary>
/// #315: the cached backing method for
/// <see cref="RemoteArmCallbacks.IsCurrentPositionOwner"/> — reads the
/// scratch fields <see cref="RunRemoteArmTail"/> just stamped rather than
/// closing over per-packet locals. Identical logic to the local-function
/// <c>IsCurrentPositionOwner</c> pattern used elsewhere in
/// <c>OnPosition</c> for the local-player paths, which this method does
/// not replace (those stay untouched, per contract invariant 10).
/// </summary>
private bool IsCurrentRemoteArmPositionOwner() =>
_remoteArmPositionRecord is { } record
&& _liveEntities.IsCurrentPositionAuthority(
record, _remoteArmPositionAuthorityVersion)
&& (_remoteArmExpectedEntity is null
|| ReferenceEquals(record.WorldEntity, _remoteArmExpectedEntity));
/// <summary>
/// B4 fix: the cached backing method for
/// <see cref="RemoteArmCallbacks.IsCurrentProjectilePositionOwner"/>.
/// Reads the <c>_projectileArmPosition*</c> scratch fields the missile
/// dispatch block in <c>OnPosition</c> stamps immediately before calling
/// <see cref="RunRemoteTeleportHook"/> for the adopted-body case. No
/// expected-entity check here — unlike the remote arm, the missile
/// dispatch block never captured one; parity with the remote arm's
/// extra guard is not required because the caller already re-validates
/// <c>ReferenceEquals(positionRecord.WorldEntity, entity)</c> earlier in
/// the SAME accepted-Position dispatch for the non-missile tail, and
/// the missile tail's own record is the SAME instance stamped here —
/// there is no second entity to disagree with.
/// </summary>
private bool IsCurrentProjectileArmPositionOwner() =>
_projectileArmPositionRecord is { } record
&& _liveEntities.IsCurrentPositionAuthority(
record, _projectileArmPositionAuthorityVersion);
/// <summary>
/// #315: the cached backing method for
/// <see cref="RemoteArmCallbacks.RunTeleportHook"/>. Only ever invoked on
/// the teleport-classified path, inside
/// <see cref="ApplyRemoteContactRouting"/>; reads the scratch fields
/// <see cref="RunRemoteArmTail"/> just stamped for THIS packet.
/// </summary>
private bool RunCachedRemoteTeleportHook() =>
_remoteArmCanonical is { } canonical
&& _remoteArmMotion is { } motion
&& RunRemoteTeleportHook(
canonical, motion, _remoteArmCallbacks.IsCurrentPositionOwner);
/// <summary>
/// C4 route 4b-2: the 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. Since the
/// OnPosition collapse (2026-08-04) this is the ONLY wire-cell adopt site
/// for every guid — the former player-guid pre-routing write (which set
/// the identical value unconditionally, before classification, and was
/// proven a true no-op by the round-2 architecture review's call-site
/// check) is deleted.
///
/// <para>
/// <see cref="RemoteMotion.CellId"/> writes THROUGH to the canonical
/// <c>FullCellId</c> (<c>RuntimePhysicsState.CommitCanonicalCell</c>).
/// After a far snap OR a teleport (C4 route 4b-3, D6) the canonical
/// placement is the cell authority — retail
/// <c>CPhysicsObj::SetPositionInternal</c> (0x00515BD0) resolves the
/// destination cell through <c>AdjustPosition</c>/<c>set_cell</c> and
/// nothing writes the wire cell over it afterwards — so this write is
/// suppressed for both arms. This call sits AFTER routing (row 3 of the
/// collapse contract, resolved to unify on this shape); 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.
/// </para>
///
/// <para>
/// <b>Scope, stated precisely (C4 route 4b-2 review; corrected at the
/// delta review).</b> The suppression bites whenever the canonical
/// placement RESOLVED a cell different from the wire cell. That is the
/// commit, and also the <c>RejectedByPlacement</c> shape where
/// <c>CommitCanonical</c> settled the body (and wrote
/// <c>record.FullCellId</c>) before the projection ownership was
/// displaced — the earlier "only when the placement COMMITTED" wording
/// missed that one. It also bites on <c>Deferred</c>, which round 3
/// (correction m1) adds to this enumeration: <c>ParkDeferred</c> snaps
/// the body to the PARKED result cell and
/// <c>RestoreParkWithdrawal</c> re-commits residency from
/// <c>body.CellPosition.ObjCellId</c>, which for a post-sweep park is the
/// swept/settled cell and need not be the wire cell. The remaining
/// outcomes — <c>Refused</c>, <c>Contention</c>,
/// <c>RejectedPreparation</c>, <c>NotApplicable</c>, and the
/// <c>RejectedByPlacement</c> shape the engine's own sweep refused —
/// resolve no cell, and there the suppression is a no-op: the per-UP
/// <c>RebucketLiveEntity</c> above already committed the wire full cell
/// to canonical, and <c>RemoteMotion.CellId</c> reads through to the same
/// <c>FullCellId</c>, 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 <c>store_position</c> fallback fixes at its source.
/// </para>
///
/// <para>
/// <b>Completion of the "already there" justification for the park case
/// (2026-08-04).</b> The no-op argument above is a claim about the four
/// cell-resolving-nothing outcomes only, and it does NOT extend to
/// <c>Deferred</c>. A park runs <c>WithdrawCanonical</c>, which ZEROES
/// <c>record.FullCellId</c>; <c>CommitCanonicalCell</c> early-returns only
/// on equality, so nothing about "the value is already there" survives a
/// park. <c>Deferred</c> 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
/// <c>RestoreParkWithdrawal</c> re-commits residency from that body cell
/// (or, in this controller's shipped order, leaves in place the full cell
/// the per-UP <c>RebucketLiveEntity</c> above committed before routing).
/// Adopting the wire cell into <c>RemoteMotion.CellId</c> afterwards would
/// contradict whichever of those two the entity actually holds.
/// </para>
///
/// <para>
/// Returns true when the wire cell was adopted.
/// </para>
/// </summary>
internal static bool TryAdoptWireCellAfterRouting(
RemoteMotion remote,
RemoteContactArm arm,
uint wireCellId)
{
ArgumentNullException.ThrowIfNull(remote);
if (arm is RemoteContactArm.FarSnapPlacement
or RemoteContactArm.TeleportPlacement)
return false;
remote.CellId = wireCellId;
return true;
}
/// <summary>
/// C4 route 4b-3 (D4): translates this App-layer routing outcome into the
/// Runtime arm value
/// <see cref="RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation"/>
/// consumes — the single post-operation <c>ConstrainTo</c> site.
///
/// <para>
/// A1 fix round (2026-08-04): this table previously mapped
/// <see cref="RemoteContactArm.AirborneSnap"/> to
/// <see cref="RuntimeRemoteAcceptedPositionArm.AirborneNoOperation"/> —
/// the one value that never arms — on the claim that "production never
/// calls this with that arm". That claim was false on the NPC branch:
/// <see cref="ApplyRemoteContactRouting"/>'s free-flight carve-out
/// (<c>!remote.Body.InContact</c>) returns <c>AirborneSnap</c> for ANY
/// non-teleport classification whenever the body lacks a contact
/// plane — including an ordinary landing packet (wire IS grounded, so
/// retail's <c>arg4 != 0</c>, so retail's <c>MoveOrTeleport</c> returns
/// nonzero and arms @0x00454272 regardless of the body's own contact
/// state, which is an acdream-only concept for snap-vs-interpolate
/// selection, not retail's arming predicate). Mapping it to
/// <c>AirborneNoOperation</c> silently dropped the leash's per-packet
/// re-anchor to zero for a creature knocked off a ledge. It now maps to
/// <see cref="RuntimeRemoteAcceptedPositionArm.NearInterpolate"/> — the
/// SAME arming value the player arm's own LANDING TRANSITION block
/// already uses explicitly for the identical scenario (grounded wire,
/// body not in contact). Retail's true airborne no-op
/// (<c>arg4 == 0</c>, acdream's <see cref="RemoteContactArm"/> has no
/// dedicated case for it because it is never routed through
/// <c>ApplyRemoteContactRouting</c> at all — both callers return before
/// reaching it, via <c>IsAirborneNoOperation</c>/D2) is the only case
/// this table cannot express, which is why the switch stays total via a
/// throwing default rather than a silent zero-arm fallback.
/// </para>
/// </summary>
private static RuntimeRemoteAcceptedPositionArm ToConstraintArm(
RemoteContactArm arm) => arm switch
{
RemoteContactArm.TeleportPlacement =>
RuntimeRemoteAcceptedPositionArm.TeleportPlacement,
RemoteContactArm.FarSnapPlacement =>
RuntimeRemoteAcceptedPositionArm.FarSnapPlacement,
RemoteContactArm.SteadyStateInterpolate =>
RuntimeRemoteAcceptedPositionArm.NearInterpolate,
RemoteContactArm.AirborneSnap =>
RuntimeRemoteAcceptedPositionArm.NearInterpolate,
RemoteContactArm.UnroutedCatchUp =>
RuntimeRemoteAcceptedPositionArm.UnroutedCatchUp,
_ => throw new ArgumentOutOfRangeException(
nameof(arm), arm, "Unhandled RemoteContactArm in ToConstraintArm."),
};
/// <summary>
/// K-fix9 (2026-04-26): handle 0xF74E VectorUpdate from remote jumps.
/// The payload seeds the world-space launch velocity and angular velocity.
/// </summary>
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}");
}
}
/// <summary>
/// L.2g slice 1: inbound SetState (0xF74B) handler. Propagates the
/// new <c>PhysicsState</c> bits into ShadowObjectRegistry so the
/// existing <see cref="CollisionExemption.ShouldSkip"/> check honors
/// the flip on the next resolver tick. Chiefly doors:
/// server flips <c>ETHEREAL_PS = 0x4</c> on Use, the door's
/// cylinder collision stops blocking the threshold.
/// </summary>
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);
// C4 route 5 (D-P1/D-P6, REVISED after the review round — A2/R1,
// A9): classify ONCE, kind-aware. This single call now decides both
// the remote route (unchanged for a non-missile packet — see the
// reuse below) AND whether this packet is a missile packet,
// replacing the former ApplyAuthoritativePosition short-circuit.
// The null-classification arm's test is the SAME conjunctive
// predicate the classifier itself applies
// (RuntimeEntityObjectLifetime.ClassifyRemoteAcceptedPosition,
// D-P1) — Missile bit AND a bound RuntimeProjectile whose Body is
// the canonical PhysicsBody, never the bit alone, so an unbindable
// or not-yet-bound missile takes the ordinary remote tail exactly
// as it did before this route (the deleted method's TryGetCurrent
// fall-through) — and explicitly fenced off the local player (A9):
// update.Guid == _playerServerGuid always takes the null branch
// below, and ACE never sets Missile on a player, but the fence
// makes that structurally true rather than incidentally true.
RuntimeAuthoritativePositionRoute? earlyRemoteRoute =
update.Guid != _playerServerGuid
? ClassifyRemoteAcceptedPosition(
update,
acceptedPositionCanonical,
timestampDisposition,
timestamps,
worldPos)
: null;
bool isMissilePacket = earlyRemoteRoute is { } classifiedRoute
? classifiedRoute.OperationKind
is RuntimeSetPositionOperationKind.ProjectileAuthoritative
: update.Guid != _playerServerGuid
&& (acceptedPositionCanonical.FinalPhysicsState
& AcDream.Core.Physics.PhysicsStateFlags.Missile) != 0
&& acceptedPositionCanonical.Projectile is { } boundProjectile
&& ReferenceEquals(
acceptedPositionCanonical.PhysicsBody,
boundProjectile.Body);
if (isMissilePacket)
{
// The projectile arm over the canonical Runtime placement owner
// (RuntimeRemotePlacementDriveController.ApplyAcceptedProjectilePosition)
// — never the generic remote locomotion path below, which would
// allocate a second body or interpolation owner for the
// projectile. A null classification (login-window shape) or a
// RejectedAuthority/RejectedData disposition is swallowed:
// nothing to route, the shared authority gate above already
// rejected an invalid payload.
if (earlyRemoteRoute is { } route)
{
// A3/R2 fix: retail's teleport_hook @0x00514ED0 runs five
// manager-guarded actions BEFORE the placement, in addition
// to the collision force-end the Runtime seam performs on
// its own (action 6). For a bare arrow/bolt (no RemoteMotion
// adopted) all five are structurally absent no-ops through
// retail's own per-manager guards — matching what the
// Runtime seam already does unaided. For the ADOPTED-BODY
// case (TryBind's shared-body branch: an ordinary remote
// whose Missile bit was set by a later State packet, so it
// still carries a live RemoteMotion with a populated Interp
// queue and possibly an armed ConstrainTo leash) those five
// actions are LIVE and must run — using the SAME ordered
// hook seam and per-packet currency check the remote
// teleport arm already uses (RunRemoteTeleportHook, #315
// pattern), so retail's per-manager guards decide for
// themselves rather than being re-derived here.
if (route.Disposition
is RuntimeAuthoritativePositionDisposition.SetPosition
&& acceptedPositionCanonical.RemoteMotion is RemoteMotion adoptedRemote)
{
// B4 fix (round-2 review): cache the currency-check
// delegate the same way the remote arm's #315 collapse
// already does, instead of allocating a fresh closure
// every accepted missile packet. Scratch fields stamped
// immediately before use; nothing reads them between
// calls, so last-packet staleness is harmless.
_projectileArmPositionRecord = acceptedPositionRecord;
_projectileArmPositionAuthorityVersion =
acceptedPositionAuthorityVersion;
RunRemoteTeleportHook(
acceptedPositionCanonical,
adoptedRemote,
_remoteArmCallbacks.IsCurrentProjectilePositionOwner);
}
RuntimeRemotePlacementExecutionStatus? placementStatus =
_remotePlacementDrive.ApplyAcceptedProjectilePosition(
acceptedPositionCanonical,
route);
// A1/R5 fix: mirror Runtime's OWN presentation gate
// (ApplyAcceptedProjectilePosition/SyncProjectilePresentation
// — every outcome except Deferred/RejectedByPlacement) rather
// than acknowledging unconditionally. Deferred already
// snapped the body to the PARKED result and withdrew the
// entity; RejectedByPlacement leaves the body exactly where
// it was. Acknowledging either would move the render entity
// to (or through) a pose/cell Runtime explicitly declined to
// publish — the concrete Interpolate/RejectedByPlacement
// wrong-cell scenario the review found.
if (placementStatus is not null
and not RuntimeRemotePlacementExecutionStatus.Deferred
and not RuntimeRemotePlacementExecutionStatus.RejectedByPlacement)
{
_projectileController?.SyncPresentationFromResolvedBody(
acceptedPositionRecord,
_physicsScriptGameTime);
}
}
return;
}
if (!_liveEntities.TryGetRecord(
update.Guid,
out LiveEntityRecord positionRecord)
|| !ReferenceEquals(positionRecord, acceptedPositionRecord)
|| !ReferenceEquals(positionRecord.WorldEntity, entity)
|| !_liveEntities.IsCurrentPositionAuthority(
positionRecord,
acceptedPositionAuthorityVersion))
{
return;
}
// C4 route 4a/5: `earlyRemoteRoute` was already classified above
// (D-P6) — this is the SAME value, reused 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
// the generic write below. C4 route 4b-2 routes the >=96 m far snap
// and C4 route 4b-3 routes the teleport/cell-less classification
// through the canonical Runtime placement owner
// (ApplyRemoteContactRouting); a rejected authority or payload, and
// "no classification at all", take the stated UnroutedCatchUp
// policy (RuntimeRemoteFarSnapPosition.ResolveArm).
//
// CORRECTED 2026-08-04 (C4 route 3, process rule 6 — this comment
// used to claim "the local player never reaches this generic-remote
// code path at all", which is FALSE): for the local player,
// `earlyRemoteRoute` is null and `OwnsSteadyState(null)` is false
// (RuntimeRemoteSteadyStatePosition.cs — both pattern matches fail
// on null), so every accepted local Apply — including the portal
// DESTINATION Position itself — reaches and runs the generic write
// + rebucket below, writing the raw wire pose onto the local
// player's WorldEntity while portal space still covers the
// viewport. This is pre-existing, tolerated (AD-2/AP-131/#275
// territory — see the D-T7 discussion in
// docs/research/2026-08-04-c4-route-3-contract.md), and overwritten
// by the committed portal Place receipt's presentation suffix
// (LocalPlayerTeleportPlacement.Place) once the canonical Runtime
// commit lands. Route 3 does not suppress it.
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 (the ConstraintDistance cell key), and recovers a
// pending bucket promotion.
//
// R4 fix round (2026-08-04): the parenthetical used to also claim
// this commit "feeds back as the classifier's own CommittedCellId" —
// false since D1 (this file, above): the remote classifier's
// CommittedCellId is the PRE-merge value threaded on
// AcceptedPhysicsTimestamps, never this post-routing rebucket's
// commit, which runs well after classification has already
// happened for this packet.
//
// 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;
}
// 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);
}
// 4a-family correction (2026-08-04, found and reported while
// pinning C4 route 5's D-P5 no-velocity design): the previous
// comment here claimed "MoveOrTeleport installs that exact vector
// with set_velocity". A byte-level disassembly of the PDB-paired
// binary (0x00516330-0x00516438, every branch) shows
// MoveOrTeleport never reads its velocity argument's stack slot,
// and UnpackPositionEvent performs no set_velocity either — the
// only set_velocity in the whole accepted-Position chain zeroes
// the LOCAL player (@0x004541B4). This call's actual retail
// justification is therefore NOT yet established and needs its
// own audit; what IS still true and unaffected by that finding:
// the canonical seam below wakes the retained ObjectClock and
// body in one operation, and the Position-delta velocity further
// down remains animation diagnostics, 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;
}
// C4 route 4b-3 / D4: retail's single ConstrainTo arming site
// (@0x00454272) is now entirely post-operation
// (TryArmConstraintAfterOperation, called once per arm below,
// after routing). The legacy pre-operation call that used to sit
// here — unconditional arming ahead of the teleport/cell-less
// branch this method now dispatches through
// ApplyRemoteContactRouting — is deleted; retail's own
// teleport_hook (which the placement runs) UnConstrains before
// the post-op site re-arms, matching @0x00514F0C then
// @0x00454272.
// 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);
}
// ── UNIFIED REMOTE ROUTING TAIL (OnPosition collapse, 2026-08-04)
// ────────────────────────────────────────────────────────────
// Retail CPhysicsObj::MoveOrTeleport (0x00516330) makes NO
// this==player distinction on any branch of this decision — see
// ApplyRemoteContactRouting's own doc. This collapses the former
// player-guid and NPC-guid copies of the routing tail (L.3 M2
// onward) into one guid-blind path. Two guid-conditionals
// survive, both named and justified rather than silently kept:
// • TS-44 sticky suppression (below) — an NPC-only steady-state
// gate; its own register row already describes it that way.
// • The AirborneSnap arm's interp-clear and collision-shadow
// publish (further below) — PRESERVED, not unified, because
// unifying either way would be an unauthorized behaviour
// change: #316 (filed 2026-08-04) is a real, UNMEASURED
// pre-existing player-guid defect (no shadow publish on
// landing) that this behaviour-preserving collapse must not
// fix, and the interp-clear's equivalence could not be proven
// for the steep-non-walkable-landing edge case (see the
// comment at that arm).
// nowSec is captured ONCE, shared by both guid ranges (was two
// independent DateTime.UtcNow reads before this collapse — a
// microsecond-scale skew in acdream-only bookkeeping/diagnostics).
double nowSec = (now - System.DateTime.UnixEpoch).TotalSeconds;
// Diagnostic (ACDREAM_REMOTE_VEL_DIAG=1), PLAYER-guid scope
// preserved exactly as before this collapse: 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. Once
// the grounded-tail synth-velocity install below is unified onto
// the single NPC formula, Prev/PrevServerPosTime feed nothing but
// this print — kept as an explicitly-diagnostic step, not
// extended to NPC guids (which never had it and have no reader
// for it either).
if (IsPlayerGuid(update.Guid))
{
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1"
&& rmState.LastServerPosTime > 0.0)
{
double dtServer = nowSec - 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;
}
// ── AIRBORNE NO-OP (C4 route 4a / D1) ────────────────────────────
// 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. AP-135's bookkeeping is
// acdream-only free-fall-sweep/first-grounded-velocity state, not
// a retail CPhysicsObj field, so it stays — now written through
// the shared helper for both guids (previously an inline copy for
// NPC, an implicit pre-write for the player).
if (RuntimeRemoteSteadyStatePosition.IsAirborneNoOperation(
earlyRemoteRoute))
{
ApplyWireAirborneLeftoverBookkeeping(
rmState, p.LandblockId, worldPos, nowSec);
return;
}
// C4 route 4b-3 (D5): the teleport/cell-less classification is
// decided AHEAD of every contact carve-out — retail decides
// @0x00516386 before ever reading arg4 (@0x0051638E). Hoisted
// here so the D2 check and the synth-velocity gate below can both
// use it, and so ApplyRemoteContactRouting's own teleport
// dispatch (inside RunRemoteArmTail, below) is the ONLY teleport
// arm site for both guids — the former player-only pre-dispatch
// block is deleted; row 1's outcome-equivalence was verified
// call-site-by-call-site by the round-2 architecture review.
bool isTeleportRoute = RuntimeRemoteTeleportPosition
.OwnsTeleportPlacement(earlyRemoteRoute);
// C4 route 4b-3 (D2): retail's return-0 shape, applied to the two
// acdream-only leftover classifications that remain wire-airborne
// (null during the login window; RejectedAuthority/RejectedData).
// Never fires for a teleport-classified route (D5 routes it ahead
// of every contact carve-out, including this one).
if (!update.IsGrounded && !isTeleportRoute)
{
ApplyWireAirborneLeftoverBookkeeping(
rmState, p.LandblockId, worldPos, nowSec);
return;
}
// C4 route 4b-3 (R3/A2 fix round 2026-08-04): retail's teleport
// branch writes NO velocity at all (contract invariant 6) — the
// hook's own CancelMoveTo already removes the moveto that would
// otherwise have suppressed a synthesized run cycle, so this
// must positively exclude the teleport route rather than rely on
// an incidental guard. This is now the SINGLE synth-velocity
// install for both guids (the former player-guid grounded-tail
// copy, which derived from the Prev pair and fed nothing but its
// own [VEL_DIAG] print, is deleted): ServerVelocity/
// HasServerVelocity have exactly two production readers —
// RemoteServerControlledVelocityCycle.Apply's internal
// IsPlayerGuid-gated return and RuntimeRemotePhysicsUpdater's
// !IsPlayerGuid-gated stale-velocity watchdog — so this write is
// observably inert for player guids (nothing reads it) and
// load-bearing for NPC guids, exactly as before the collapse.
// Reads the OLD LastServerPos/Time pair — this runs BEFORE the
// post-routing sample further below overwrites them.
if (!isTeleportRoute)
{
System.Numerics.Vector3? serverVelocity = update.Velocity;
if (serverVelocity is null && 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
{
rmState.ServerVelocity = System.Numerics.Vector3.Zero;
rmState.HasServerVelocity = false;
}
}
// R5-V3 #171 residual / TS-44 — THE ONE named surviving branch of
// this collapse (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. NOT vacuous for players:
// LiveEntityMotionRuntimeController.StickToObjectFromWire (retail
// stick_to_object 0x005127e0, the mt-0 wire sticky trailer) can
// arm sticky on ANY remote host, player remotes included — but
// the player arm has never gated on it (only the NPC steady-state
// path did), and this collapse does not widen that: the TS-44
// register row already describes exactly "an NPC-only
// steady-state gate" and stays true. 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).
bool snapSuppressedByStick = !IsPlayerGuid(update.Guid)
&& (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})"));
}
// C4 routes 4a + 4b-2 + 4b-3 collapse: the complete near/far/
// teleport/leftover decision — including the dissolved LANDING
// TRANSITION block, now the AirborneSnap arm — is the ONE shared
// entry point every remote guid calls. `arm` defaults to
// UnroutedCatchUp for the sticky-suppressed case (D4: a stuck
// NPC's leash still re-arms — see the arming site below).
RemoteContactArm arm = RemoteContactArm.UnroutedCatchUp;
if (!snapSuppressedByStick || isTeleportRoute)
{
// C4 route 4b-3 (D5): TS-44's sticky suppression does NOT
// suppress the teleport arm — retail's sticky cannot survive
// a teleport (`UnStick` is the hook's second action,
// @0x00514EEE), so a stuck NPC's teleport packet must still
// run the hook and place.
//
// R5 review fix: the currency guard (inside RunRemoteArmTail)
// sits BEFORE the leash arming — arming is a write (it stamps
// rmState.Host's PositionManager), and nothing may be written
// through a superseded owner.
RemoteContactRouting? routing = RunRemoteArmTail(
acceptedPositionCanonical,
positionRecord,
rmState,
earlyRemoteRoute,
update.Guid,
worldPos,
rot,
acceptedPositionAuthorityVersion,
entity);
if (routing is null)
return;
arm = routing.Value.Arm;
if (arm is RemoteContactArm.AirborneSnap)
{
// PRESERVED (rows 2a/2b of the collapse contract — NOT
// unified either way). This is the dissolved LANDING
// TRANSITION scenario: a grounded wire packet for a body
// with no contact plane. ApplyRemoteContactRouting's
// free-flight carve-out already wrote
// rmState.Body.Position/Orientation to the landing pose;
// ToConstraintArm(AirborneSnap) => NearInterpolate (the
// A1 fix) supplies the same arm value the old landing
// block hard-coded.
//
// #316 (filed 2026-08-04, deliberately NOT fixed here):
// the player-guid copy of this scenario has never
// published the collision shadow (the tail below does,
// for every OTHER arm and for this SAME arm on NPC
// guids) — a real, UNMEASURED pre-existing defect that
// contradicts the file's own #184 Slice 2b design intent
// ("player shadows now follow the resolved body ...
// exactly like NPCs"). Fixing it is a behaviour change
// this collapse may not make; the skip is reproduced
// verbatim at the tail below, keyed on this same `arm`
// value.
//
// The interp-queue clear is preserved alongside it rather
// than unified either way. AdjustOffset's CONTACT_TS gate
// (InterpolationManager.AdjustOffset: `if (!inContact)
// return ...;`, before EVER touching the queue) proves a
// populated queue is inert on an ordinary flat/walkable
// landing regardless of whether it was cleared here — the
// per-tick sweep's own `!previousOnWalkable &&
// finalOnWalkable` edge (AP-139) clears it in the SAME
// tick that first reopens the CONTACT gate. But that
// per-tick clear is keyed to WALKABLE, not CONTACT, while
// AdjustOffset's gate is keyed to CONTACT — so a body
// that gains CONTACT on a non-walkable steep face (Bug
// B's own scenario: a remote landing on a roof) would
// have AdjustOffset's gate reopen one tick before the
// WALKABLE-keyed clear would ever fire, and a populated
// pre-arc queue would then be walked with stale
// waypoints. That edge case's equivalence could not be
// proven from either direction in the time this slice
// budgeted; shipping either "always clear" (a behaviour
// change for NPCs) or "never clear" (a behaviour change
// for players) on the strength of an incomplete proof is
// exactly what the contract forbids (§10 stop condition
// 2). Preserving both guids' exact pre-collapse behaviour
// is the safe outcome.
if (IsPlayerGuid(update.Guid))
{
rmState.Interp.Clear();
}
// Resolved to UNIFY (idempotent + strictly additive):
// EnsureRemoteMotionBindings early-returns once rm.Host/
// rm.Sink are already bound
// (LiveEntityMotionRuntimeController.
// EnsureRemoteMotionBindings:73), so "always ensure" is
// safe for both guids. The motion bindings still have to
// exist before the next per-tick commit can dispatch
// this remote's ground edge; an NPC that reaches this arm
// with no prior UM/OnVector binding had the identical
// latent gap the player copy already worked around.
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, now fired
// for both guids (diagnostic-only — TEMPORARY, strip with
// the probe family; not behaviour).
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).
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);
}
}
}
// D2/D4: ConstrainTo arms strictly AFTER the operation, anchored
// post-move — retail arms it only once MoveOrTeleport returns
// nonzero (@0x00454254/@0x00454272), which the near, far, AND
// teleport branches all do (@0x005163BE, @0x005163E8,
// @0x00516438). This is the ONE arming site for every guid (the
// legacy pre-operation call and the two duplicated post-operation
// copies are deleted — D4); TryArmConstraintAfterOperation's own
// partition decides whether THIS arm arms. Sits OUTSIDE the
// snapSuppressedByStick gate: retail's ConstraintManager leash is
// independent of the acdream-only TS-44 suppression, so a stuck
// NPC's leash still re-arms every accepted Position (AP-138(3)'s
// one-packet unarmed residual on a superseded incarnation still
// applies — the currency guard inside RunRemoteArmTail returns
// above without reaching this call).
RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation(
ToConstraintArm(arm), rmState);
// C4 route 4b-2/4b-3: NOT after a far snap or a teleport — see
// TryAdoptWireCellAfterRouting for the suppression rule. The ONE
// post-routing wire-cell adopt site for every guid (proof
// obligation re-verified for this collapse: the constraint
// anchor reads host.Position, never rm.CellId; ApplyInterpolate,
// the free-flight carve-out, and WillAdvanceRemoteMotion do not
// read it either; RebucketLiveEntity, above, already committed
// the wire cell into RuntimeEntityRecord.FullCellId — which
// RemoteMotion.CellId's getter reads through to — before ANY of
// this method's guid branching runs, so deleting the former
// player-guid copy's redundant pre-routing write is a true
// no-op, exactly as the round-2 architecture review's call-site
// check already found for the extracted-helper case).
TryAdoptWireCellAfterRouting(rmState, 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.
//
// Resolved to UNIFY: ONE post-routing sample for every guid.
// Equivalence walk (ApplyInterpolate's AP-87 backstop reads
// `firstUp = remote.LastServerPosTime <= 0.0`): on a genuine
// first UP the RemoteMotion was created THIS packet with
// Body.Position == worldPos (the creation branch above), so
// bodyToTarget == 0 regardless of which guid's firstUp timing
// applies. The (until now) NPC-timed firstUp=true path takes
// ApplyInterpolate's Snapped branch — a no-op body/orientation
// write at zero distance, an idempotent queue clear. The
// (until now) player-timed firstUp=false-but-zero-distance path
// takes InterpolationManager.Enqueue's "already-close" branch
// (also an idempotent queue clear, and an orientation write
// derived from the SAME wire orientation via
// SetHeading(o, GetHeading(o)) — read verbatim, not assumed,
// and identical for the heading-only quaternions every remote's
// wire Position carries). Sampling once, after routing, is
// therefore unobservable versus the former player-guid
// pre-routing stamp.
rmState.LastServerPos = worldPos;
rmState.LastServerPosTime = nowSec;
if (!isTeleportRoute
&& rmState.HasServerVelocity
&& !snapSuppressedByStick
&& _animatedEntities.TryGetValue(entity.Id, out var aeForVelocity))
{
// The second data-driven-not-branching survivor of this
// collapse: RemoteServerControlledVelocityCycle.Apply's own
// internal IsPlayerGuid early return (AP-80/DEV-2) is now the
// ONLY carrier of the player/NPC animation-cycle distinction
// — this call site is guid-blind. Player remotes' cycles stay
// UM-driven only per retail (L.2g S5; DEV-2 deleted); NPC/
// monster remotes keep PlanFromVelocity cycle selection from
// UP-derived velocity. Excluded for a teleport-classified
// route above (isTeleportRoute) — retail's teleport branch
// writes no velocity at all, so there is nothing here to plan
// a cycle from.
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 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 remotes (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. The ONE entity-sync + shadow-publish tail
// for every guid and every arm — except the #316-preserved
// exception: a player-guid AirborneSnap arm still commits the
// render entity from the resolved body but does NOT publish the
// shadow, matching its pre-collapse behaviour exactly (see the
// comment at that arm, above).
entity.SetPosition(rmState.Body.Position);
entity.ParentCellId = rmState.CellId;
entity.Rotation = rmState.Body.Orientation;
if (arm is not RemoteContactArm.AirborneSnap
|| !IsPlayerGuid(update.Guid))
{
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.
}