fix(physics): C4 route 5 — projectile authoritative placement (#276 partial)

Ports retail's missile Position handling into the canonical Runtime
placement owner instead of the deleted ApplyAuthoritativePosition
short-circuit. The Create/residence-window halves of the projectile
pipeline (RuntimeProjectile binding, TryBind's adopted-body branch,
the collision/shadow registration) were already canonical from prior
slices; this closes the remaining gap — how an ACCEPTED Position for
an in-flight missile is classified, placed, and presented.

Byte-decode (Step 1 hard gate, before any code was written):
CPhysicsObj::MoveOrTeleport @0x00516330-0x00516438 disassembled from
the PDB-paired binary (Capstone, x86 32-bit thiscall). `ret 0x10`
establishes four stack args; [esp+0x7c] (arg5, the velocity pointer)
is never referenced in any of the three branches (teleport/near/far).
The retail reviewer independently reproduced this by searching the
whole function body for the `24 7c` mod/rm+disp8 encoding a
`[esp+0x7c]` read would require and found zero occurrences. This
retired a fabricated `?? Vector3.Zero` fallback in the deleted method
— retail's PositionPack::UnPack initializes an absent velocity to
zero and MoveOrTeleport never installs it; the projectile's Vector
channel (RuntimeProjectilePhysicsUpdater.ApplyAuthoritativeVector)
remains the sole velocity authority for a missile. D-P5 in the
contract; the Runtime seam commits no velocity from the Position
packet at all.

The unbound-missile fix: RuntimeEntityObjectLifetime's
ClassifyRemoteAcceptedPosition now derives ProjectileAuthoritative
from a CONJUNCTIVE predicate — the Missile bit AND a bound
RuntimeProjectile whose Body is the canonical PhysicsBody — never the
bit alone. Retail places every non-player CPhysicsObj unconditionally
(there is no missile-specific placement gate in MoveOrTeleport or its
callers), so an unbindable or not-yet-bound missile taking the
ordinary remote tail is retail-faithful, not a fallback: the earlier
bit-only discriminator would have silently frozen it instead.

AP-141 records this as a deliberate, recorded divergence, not
fidelity. Retail mechanically WOULD arm a missile's ConstrainTo leash
on any nonzero MoveOrTeleport return: HandleReceivedPosition
@0x00453FD0's only kind test is player-vs-not, ConstrainTo
@0x00454272 has no kind test of its own, and CPhysicsObj::ConstrainTo
@0x00510520 creates a PositionManager on demand via
MakePositionManager @0x00510523 if one doesn't exist. acdream
deliberately does not construct that EntityPhysicsHost/
PositionManager/InterpolationManager chain for a ballistic body — the
route-5b split the C4 route 5 contract rejected — so a live missile
never shows an armed leash and never catches up via the near/
UnroutedCatchUp policy. This divergence is safe specifically because
ACE never sends UpdatePosition for a missile
(references/ACE/Source/ACE.Server/WorldObjects/WorldObject_Tick.cs:
333-334, SendUpdatePosition() commented out inside the
PhysicsState.Missile branch at :265) — every half of this row is
deterministic-test-gated only, never exercised against a real server.

AP-141 also records the surviving ConstrainTo re-anchor divergence
under clause (b): for the adopted-body case (TryBind's shared-body
branch — an ordinary remote whose Missile bit is set by a later
State packet, so it still carries a live RemoteMotion), acdream now
ports retail's teleport-branch and far-branch StopInterpolating
action (Interp.Clear()), but never re-arms or re-anchors the
inherited ConstrainTo leash the way retail's HandleReceivedPosition
@0x00454254/@0x00454272 does on every nonzero return. The risk
column's earlier wording — that a stale leash "would drag the body
toward a stale anchor" — was wrong and is retracted in this same
commit: ConstraintManager.ConstraintPos is write-only in both retail
and the port (never read by AdjustOffset), and
ConstraintManager::adjust_offset @0x00556180 only tapers or zeroes an
already-composed per-tick offset while InContact — a leash brakes
motion the interp/sticky chain already produced, it cannot pull
anything toward the anchor. The real residual is one tick of un-reset
brake accumulator, contact-gated, and it cannot move an airborne
far-snapped missile at all (the clamp branch does not run while
airborne).

NO CONNECTED GATE EXISTS for this route, by design: ACE never sends a
missile UpdatePosition (see above), so retail's own server never
exercises this code path in play. Every proof obligation here is
test-gated only — Runtime and App-level fixtures constructing the
packet directly — never a live client/server capture.

Three review rounds closed 8 MAJOR findings before this landed:
round 1 (A1 App discarded the seam's status; A2/R1 silent swallow on
an unbound missile; A3/R2 the adopted-body teleport_hook never
wired; A4/A5 zero Runtime/App test coverage); round 2 (a
ParentCellId regression introduced by round 1's own R6 finding,
which the retail reviewer retracted the following round as factually
wrong — the fix here is the REVERT to record.FullCellId, not the
relocation round 1 shipped; B2 the far-branch StopInterpolating skip
never extended to the adopted-body case; residual App/Runtime store-
path coverage; a per-packet closure contradicting the file's own
#315 cached-delegate pattern). Round 3 closed on coverage alone (no
defect): the Advance() retry arm's projectile branch — added at
round 2, semantically reordered at round 2's B5 fix (skip prediction
invalidation on a re-parked Contention, since it writes nothing) —
had never been executed by any test; two new tests drive it directly
and are sabotage-verified against both the reordering and the
retry-arm's own SyncProjectilePresentation call site. The one
recorded defect this campaign produced (the ParentCellId regression)
was caused by complying with a review finding that its own author
later retracted — the standing lesson recorded for future rounds is
that review findings are evidence to re-verify against the code, not
commands to obey unconditionally.

Complete Release suite: 11,063 passed / 4 skipped / 0 failed
(baseline 11,036 at 30d3d114, +27 new tests across this campaign).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-04 21:03:41 +02:00
parent 30d3d114b0
commit 36255af0f6
19 changed files with 5390 additions and 393 deletions

View file

@ -93,6 +93,21 @@ internal sealed class LiveEntityNetworkUpdateController
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
@ -113,10 +128,22 @@ internal sealed class LiveEntityNetworkUpdateController
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;
}
}
@ -1478,6 +1505,25 @@ internal sealed class LiveEntityNetworkUpdateController
&& (_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
@ -2102,27 +2148,113 @@ internal sealed class LiveEntityNetworkUpdateController
: new System.Numerics.Quaternion(p.RotationX, p.RotationY, p.RotationZ, p.RotationW);
_movementTruthDiagnostics.OnServerEcho(update, worldPos);
// Missiles reconcile the same predicted PhysicsBody in place. The
// timestamp gate above already rejected stale corrections; returning
// here prevents the generic remote locomotion path from allocating a
// second body or interpolation owner for the projectile.
if (_projectileController?.ApplyAuthoritativePosition(
acceptedPositionRecord,
acceptedPositionAuthorityVersion,
acceptedPositionVelocityAuthorityVersion,
worldPos,
new System.Numerics.Vector3(
p.PositionX,
p.PositionY,
p.PositionZ),
rot,
acceptedSpawn.Physics?.Velocity
?? System.Numerics.Vector3.Zero,
p.LandblockId,
_physicsScriptGameTime,
_origin.CenterX,
_origin.CenterY) == true)
// 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,
@ -2136,26 +2268,18 @@ internal sealed class LiveEntityNetworkUpdateController
return;
}
// C4 route 4a: classify BEFORE the generic write below so a remote
// whose accepted Position resolves to NoPositionOperation (retail's
// airborne no-op — writes nothing at all) or Interpolate (retail's
// near InterpolateTo queue — no direct body write here) never
// receives it. The local player never reaches this generic-remote
// code path at all. 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).
RuntimeAuthoritativePositionRoute? earlyRemoteRoute =
update.Guid != _playerServerGuid
? ClassifyRemoteAcceptedPosition(
update,
acceptedPositionCanonical,
timestampDisposition,
timestamps,
worldPos)
: null;
// 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. The local player never reaches this
// generic-remote code path at all. 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).
TryApplyGenericRemoteRenderPose(
entity,

View file

@ -490,103 +490,84 @@ internal sealed class ProjectileController
}
/// <summary>
/// Applies a timestamp-gated server correction to the same predicted body.
/// The caller supplies both render-world and wire cell-local coordinates so
/// streaming origin changes never leak into the canonical cell frame.
/// C4 route 5 (D-P2 closing paragraph): the presentation acknowledgement
/// App still owns after
/// <c>RuntimeRemotePlacementDriveController.ApplyAcceptedProjectilePosition</c>
/// has committed or stored an accepted Position for
/// <paramref name="expectedRecord"/>. Projects the Runtime-resolved
/// canonical body into the render entity and root pose — the same ack
/// contract the deleted <c>ApplyAuthoritativePosition</c> performed via
/// its <c>acknowledgeProjection</c> closure, now read directly off the
/// canonical body rather than an intermediate snapshot, since the
/// placement already committed (or the store fallback already wrote)
/// before this is called.
///
/// <para>
/// <b>B1/B2 fix (round-2 review — reverting round 1's own R6 "fix",
/// which the retail reviewer retracted as factually wrong).</b>
/// <c>ParentCellId</c> is read from <c>record</c>'s (the local resolved
/// by <c>TryGetCurrent</c> below, equal to
/// <paramref name="expectedRecord"/> once the currency check passes)
/// <c>FullCellId</c> — the WIRE cell — never the body's own
/// <c>CellPosition.ObjCellId</c>. On a committed outcome the two agree
/// (<c>CommitCanonical</c> writes both together), so the choice is a
/// no-op there. On a STORED outcome
/// (<c>Refused</c>/<c>Contention</c>/<c>RejectedPreparation</c>),
/// <c>StoreAcceptedDestinationPose</c> composes <c>body.Position</c>
/// from <c>accepted.PositionX + worldOffset(accepted.LandblockId)</c> —
/// the WIRE cell's world frame — while <c>record.FullCellId</c> is that
/// SAME wire cell (stamped by the merge's
/// <c>RefreshDerivedState</c>/<c>SetFullCell</c>, before classification
/// ever runs). The body's OWN <c>CellPosition.ObjCellId</c>, in
/// contrast, is never touched by the store fallback and is therefore
/// the SOURCE cell the body left — reading it here would pair the new
/// destination position with the cell the body is no longer in. Three
/// independent checks confirm <c>record.FullCellId</c> is the correct
/// source: (1) the sibling remote arm's
/// <c>TryApplyGenericRemoteRenderPose</c> pairs the wire world position
/// with the wire cell the same way; (2) Runtime's own
/// <c>SyncProjectilePresentation</c>, forty lines away in the SAME
/// packet, publishes the shadow row at <c>record.FullCellId</c> — so
/// reading the body's cell here would disagree with the shadow for the
/// SAME body in the SAME call; (3) retail's <c>store_position</c>
/// @0x00515CE2 writes the object's whole <c>Position</c> INCLUDING
/// <c>objcell_id</c>, so after a retail store the object's cell IS the
/// destination (the wire cell) — never the stale one. The body's own
/// stale cell after a store is the acdream-side residual (AP-138,
/// shared with the remote arms), not the truth to project.
/// </para>
///
/// <para>
/// Rebases <paramref name="currentTime"/> into the SAME
/// <c>_lastFiniteGameTime</c> field the deleted method's <c>currentTime</c>
/// parameter used to (unconditionally, when finite) — restoring the
/// controller's own tick-elapsed-time basis instead of leaving it
/// pinned at the last Vector/State/Tick call.
/// </para>
/// </summary>
internal bool ApplyAuthoritativePosition(
internal bool SyncPresentationFromResolvedBody(
LiveEntityRecord expectedRecord,
Vector3 worldPosition,
Vector3 cellLocalPosition,
Quaternion orientation,
Vector3 velocity,
uint fullCellId,
double currentTime,
int liveCenterX,
int liveCenterY) =>
ApplyAuthoritativePosition(
expectedRecord,
expectedRecord.PositionAuthorityVersion,
expectedRecord.VelocityAuthorityVersion,
worldPosition,
cellLocalPosition,
orientation,
velocity,
fullCellId,
currentTime,
liveCenterX,
liveCenterY);
internal bool ApplyAuthoritativePosition(
LiveEntityRecord expectedRecord,
ulong expectedPositionAuthorityVersion,
ulong expectedVelocityAuthorityVersion,
Vector3 worldPosition,
Vector3 cellLocalPosition,
Quaternion orientation,
Vector3 velocity,
uint fullCellId,
double currentTime,
int liveCenterX,
int liveCenterY)
double currentTime)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
uint serverGuid = expectedRecord.ServerGuid;
if (!TryGetCurrent(serverGuid, out LiveEntityRecord record, out RuntimeProjectile runtime)
if (!TryGetCurrent(
expectedRecord.ServerGuid,
out LiveEntityRecord record,
out RuntimeProjectile runtime)
|| !ReferenceEquals(record, expectedRecord)
|| record.PositionAuthorityVersion != expectedPositionAuthorityVersion
|| (record.FinalPhysicsState & PhysicsStateFlags.Missile) == 0
|| record.WorldEntity is not { } entity)
return false;
if (!double.IsFinite(currentTime)
|| !IsFinite(worldPosition)
|| !IsFinite(cellLocalPosition)
|| !IsFinite(velocity)
|| !PositionFrameValidation.IsValid(
fullCellId,
cellLocalPosition,
orientation))
{
DiagnosticSink?.Invoke(
$"Rejected invalid PositionUpdate for missile 0x{serverGuid:X8}.");
return true;
return false;
}
_lastFiniteGameTime = currentTime;
bool ExternalOwnerValid() =>
TryGetCurrent(
serverGuid,
out LiveEntityRecord current,
out RuntimeProjectile currentRuntime)
&& ReferenceEquals(current, record)
&& ReferenceEquals(currentRuntime, runtime)
&& ReferenceEquals(current.WorldEntity, entity)
&& current.PositionAuthorityVersion
== expectedPositionAuthorityVersion;
return _runtimeUpdater.ApplyAuthoritativePosition(
record.Canonical,
expectedPositionAuthorityVersion,
expectedVelocityAuthorityVersion,
worldPosition,
cellLocalPosition,
orientation,
velocity,
fullCellId,
currentTime,
liveCenterX,
liveCenterY,
snapshot =>
{
if (!ExternalOwnerValid())
return false;
entity.SetPosition(snapshot.Position);
entity.Rotation = snapshot.Orientation;
entity.ParentCellId = snapshot.FullCellId;
_rootPoses?.UpdateRoot(entity);
return ExternalOwnerValid();
},
ExternalOwnerValid);
if (double.IsFinite(currentTime))
_lastFiniteGameTime = currentTime;
entity.SetPosition(runtime.Body.Position);
entity.Rotation = runtime.Body.Orientation;
entity.ParentCellId = record.FullCellId;
_rootPoses?.UpdateRoot(entity);
return true;
}
/// <summary>

View file

@ -589,14 +589,38 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
}
/// <summary>
/// C4 route 4a: classifies one REMOTE incarnation's accepted Position
/// through <see cref="RuntimeAuthoritativePositionRouteClassifier"/>, so
/// the graphical host and any future no-window remote-motion host make
/// C4 route 4a: classifies one non-local-player incarnation's accepted
/// Position through <see cref="RuntimeAuthoritativePositionRouteClassifier"/>,
/// so the graphical host and any future no-window remote-motion host make
/// the SAME airborne-no-op / near-interpolate decision from the same
/// generation, the same authority shape, and the same request builder the
/// deferred initial-create continuation uses.
///
/// <para>
/// C4 route 5 (D-P1, REVISED after the retail/architecture review round —
/// A2/R1): the caller's incarnation may be an ordinary remote OR a live
/// missile — this method derives <c>RuntimePositionEntityKind</c> from
/// <c>canonical.FinalPhysicsState &amp; PhysicsStateFlags.Missile</c>
/// **conjoined with a bound, body-agreeing <see cref="RuntimeProjectile"/>**,
/// never the Missile bit alone. Retail's <c>MoveOrTeleport</c> places
/// EVERY non-player object unconditionally — it has no concept of
/// "client-side machinery not yet bound". A Missile-flagged record whose
/// <c>ProjectileController.TryBind</c> permanently refused (an
/// unsupported multi-sphere Setup) or has not yet run (the pre-bind
/// window) still classifies <c>Remote</c> here, so it takes the SAME
/// generic remote placement path retail's client would drive for it and
/// keeps tracking the server — exactly the pre-route-5 behaviour, which
/// fell through the deleted <c>ApplyAuthoritativePosition</c>'s
/// <c>TryGetCurrent</c> failure to the remote tail. The classifier's own
/// disposition/flag shape is unchanged either way (Projectile and Remote
/// are disposition-identical); only the returned route's
/// <c>OperationKind</c> differs, which is what lets the App dispatch
/// (route 5's <c>OnPosition</c> arm) and
/// <see cref="AcDream.Runtime.Session.RuntimeRemotePlacementDriveController.OwnsPlacement"/>
/// tell the two apart.
/// </para>
///
/// <para>
/// Returns <see langword="null"/> when no classification can honestly be
/// made: the lifetime has no bound generation yet, the canonical record
/// has not claimed a local id, or there is no live local-player position
@ -635,11 +659,32 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
{
return null;
}
// C4 route 5 (D-P1, revised — A2/R1): the Missile bit alone is
// data-driven but not sufficient — retail places every non-player
// object regardless of what client-side machinery exists for it, so
// this packet must classify Projectile ONLY when the arm can
// actually own the placement (a bound RuntimeProjectile whose Body
// is the exact canonical PhysicsBody). Anything else — no
// component bound (TryBind refused or has not run yet), or a
// component bound to a stale/displaced body — classifies Remote,
// matching retail's unconditional placement and the pre-route-5
// fall-through the deleted ApplyAuthoritativePosition performed via
// its own TryGetCurrent failure. A mid-life Missile flip (ACE clears
// it on impact; a State packet installs it, and TryBind subsequently
// binds the component) is handled by construction: whichever
// FinalPhysicsState AND binding state the canonical record carries
// for THIS packet decides the packet's kind.
RuntimePositionEntityKind kind =
(canonical.FinalPhysicsState & PhysicsStateFlags.Missile) != 0
&& canonical.Projectile is RuntimeProjectile boundProjectile
&& ReferenceEquals(canonical.PhysicsBody, boundProjectile.Body)
? RuntimePositionEntityKind.Projectile
: RuntimePositionEntityKind.Remote;
if (!RuntimeAcceptedPositionRouteRequests.TryBuild(
generation(),
canonical,
update,
RuntimePositionEntityKind.Remote,
kind,
RuntimeAcceptedPositionSource.PositionEvent,
disposition,
timestamps.PreviousTeleport,

View file

@ -298,130 +298,15 @@ internal sealed class RuntimeProjectilePhysicsUpdater
return true;
}
internal bool ApplyAuthoritativePosition(
RuntimeEntityRecord record,
ulong expectedPositionAuthorityVersion,
ulong expectedVelocityAuthorityVersion,
Vector3 worldPosition,
Vector3 cellLocalPosition,
Quaternion orientation,
Vector3 velocity,
uint fullCellId,
double currentTime,
int liveCenterX,
int liveCenterY,
Func<RuntimePhysicsFrameSnapshot, bool> acknowledgeProjection,
Func<bool>? externalOwnerValid = null)
{
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(acknowledgeProjection);
if (!TryGetCurrent(
record,
externalOwnerValid,
out RuntimeProjectile projectile)
|| record.PositionAuthorityVersion
!= expectedPositionAuthorityVersion
|| (record.FinalPhysicsState
& PhysicsStateFlags.Missile) == 0)
{
return false;
}
if (!double.IsFinite(currentTime)
|| !IsFinite(worldPosition)
|| !IsFinite(cellLocalPosition)
|| !IsFinite(velocity)
|| !PositionFrameValidation.IsValid(
fullCellId,
cellLocalPosition,
orientation))
{
return true;
}
PhysicsBody body = projectile.Body;
projectile.InvalidatePrediction();
ulong predictionVersion = projectile.PredictionAuthorityVersion;
bool wasInWorld = body.InWorld;
body.Orientation = orientation;
body.SnapToCell(fullCellId, worldPosition, cellLocalPosition);
body.State = record.FinalPhysicsState;
if (record.VelocityAuthorityVersion
== expectedVelocityAuthorityVersion)
{
_ = _physics.TryCommitAuthoritativeVector(
record,
body,
velocity,
angularVelocity: null,
currentTime,
externalOwnerValid);
}
bool IsExactOwner() =>
TryGetCurrent(
record,
externalOwnerValid,
out RuntimeProjectile current)
&& ReferenceEquals(current, projectile)
&& current.PredictionAuthorityVersion == predictionVersion
&& record.PositionAuthorityVersion
== expectedPositionAuthorityVersion;
if (!_physics.CommitProjectileCell(
record,
projectile,
predictionVersion,
body.CellPosition.ObjCellId,
IsExactOwner)
|| !IsExactOwner())
{
// This packet was accepted for the old incarnation. A re-entrant
// observer displaced it, so the replacement owns another body.
return true;
}
var snapshot = new RuntimePhysicsFrameSnapshot(
body.Position,
body.Orientation,
body.CellPosition.ObjCellId);
if (!acknowledgeProjection(snapshot) || !IsExactOwner())
return true;
bool spatial = _physics.IsSpatialProjectile(record, projectile);
bool hidden =
(record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
uint localId = record.LocalEntityId ?? 0u;
if (spatial && !hidden)
{
if (!wasInWorld)
{
body.LastUpdateTime = currentTime;
Activate(body, currentTime);
}
body.InWorld = true;
ShadowPositionSynchronizer.Sync(
_physics.Engine.ShadowObjects,
localId,
body.Position,
body.Orientation,
record.FullCellId,
liveCenterX,
liveCenterY);
}
else if (spatial)
{
body.InWorld = true;
body.LastUpdateTime = currentTime;
_physics.Engine.ShadowObjects.Suspend(localId);
}
else
{
body.InWorld = false;
Deactivate(body);
_physics.Engine.ShadowObjects.Suspend(localId);
}
return true;
}
// C4 route 5 (2026-08-04): the position-packet authority that used to
// live here — ApplyAuthoritativePosition — is deleted. A live missile's
// accepted Position now routes through the canonical
// RuntimeRemotePlacementDriveController.ApplyAcceptedProjectilePosition,
// the same shared placement pipeline (TryExecuteAcceptedRemotePosition /
// StoreAcceptedDestinationPose / CommitCanonical) the remote teleport/far
// arms use, instead of this class's bespoke SnapToCell + CommitProjectileCell
// + manual shadow-sync tail. CommitProjectileCell remains the per-quantum
// path's own cell commit (TryBegin/Complete below) — untouched.
private bool IsSpatialCurrent(
RuntimeEntityRecord record,
@ -481,18 +366,6 @@ internal sealed class RuntimeProjectilePhysicsUpdater
0f);
}
private static void Activate(PhysicsBody body, double currentTime)
{
if ((body.State & PhysicsStateFlags.Static) != 0)
return;
if ((body.TransientState & TransientStateFlags.Active) == 0)
body.LastUpdateTime = currentTime;
body.TransientState |= TransientStateFlags.Active;
}
private static void Deactivate(PhysicsBody body) =>
body.TransientState &= ~TransientStateFlags.Active;
private static bool IsFinite(Vector3 value) =>
float.IsFinite(value.X)
&& float.IsFinite(value.Y)

View file

@ -78,6 +78,20 @@ internal static class RuntimeRemoteFarSnapPosition
/// <c>arg3 != 0</c>), so this predicate is a strict narrowing of
/// <c>OwnsPlacement</c> to its far half.
/// </para>
///
/// <para>
/// C4 route 5 (D-P3/A10 fix): after the widening,
/// <c>OwnsPlacement</c> ALSO admits
/// <c>RuntimeSetPositionOperationKind.ProjectileAuthoritative</c> — this
/// predicate's <c>OperationKind: RemoteAuthoritative</c> gate below is
/// therefore a strict narrowing of <c>OwnsPlacement</c>'s REMOTE far
/// half only, not the whole predicate. A projectile far route never
/// satisfies this method (it takes the sibling seam,
/// <c>RuntimeRemotePlacementDriveController.ApplyAcceptedProjectilePosition</c>,
/// which does not call this method and does not go through
/// <see cref="AcDream.Runtime.Session.RuntimeRemotePlacementDriveController.ApplyAcceptedRemoteFarSnap"/>/<see cref="ResolveArm"/> —
/// both require a <c>RemoteMotion</c> a projectile does not have).
/// </para>
/// </summary>
internal static bool OwnsFarSnap(RuntimeAuthoritativePositionRoute? route) =>
route is

View file

@ -516,9 +516,25 @@ internal sealed class RuntimeRemotePlacementDriveController
/// POSITION-only route; the first-entry conductor owns every Create)
/// without excluding either remote Position shape.
/// </para>
/// <para>
/// C4 route 5 (D-P3): widened to admit
/// <c>ProjectileAuthoritative</c> alongside <c>RemoteAuthoritative</c> —
/// a live missile's teleport/cell-less and far accepted-Position
/// dispositions are disposition-identical to a remote's (the classifier
/// never branches on kind past <c>LocalPlayer</c>), and route 5's
/// <see cref="ApplyAcceptedProjectilePosition"/> is the sibling seam over
/// this SAME shared core (<see cref="TryExecuteAcceptedRemotePosition"/> +
/// <see cref="StoreAcceptedDestinationPose"/>) — never a second pending
/// map, never a sibling controller. <c>OwnsFarSnap</c> and
/// <c>OwnsTeleportPlacement</c> are deliberately NOT widened: both remote
/// arm methods (<see cref="ApplyAcceptedRemoteFarSnap"/>/
/// <see cref="ApplyAcceptedRemoteTeleport"/>) require and throw without a
/// <c>RemoteMotion</c>, which a projectile never has.
/// </para>
/// </summary>
internal static bool OwnsPlacement(RuntimeAuthoritativePositionRoute route) =>
route.OperationKind is RuntimeSetPositionOperationKind.RemoteAuthoritative
or RuntimeSetPositionOperationKind.ProjectileAuthoritative
&& route.Disposition is RuntimeAuthoritativePositionDisposition.SetPosition
or RuntimeAuthoritativePositionDisposition.SetPositionSimple
&& (route.SetPositionFlags & PhysicsSetPositionFlags.Teleport) != 0;
@ -842,6 +858,234 @@ internal sealed class RuntimeRemotePlacementDriveController
return status;
}
/// <summary>
/// C4 route 5 (D-P2): the projectile arm over this SAME shared core. A
/// live missile carries no <see cref="RemoteMotion"/>, so
/// <see cref="ApplyAcceptedRemoteFarSnap"/>/<see cref="ApplyAcceptedRemoteTeleport"/>
/// are not reusable — both require one and throw without it. This is the
/// sibling seam the D-P2 design pins: same
/// <see cref="TryExecuteAcceptedRemotePosition"/> +
/// <see cref="StoreAcceptedDestinationPose"/> core, same
/// <see cref="_pending"/>/<see cref="_awaitingAcknowledgement"/> ledgers,
/// no second pending map, no sibling controller (trap T9).
///
/// <para>
/// Returns <see langword="null"/> for every disposition this route does
/// not own: <c>Interpolate</c> (near, in contact) and
/// <c>NoPositionOperation</c> (airborne) are pinned NO-OPS — retail would
/// lazily build interpolation/leash machinery for a manager-less missile
/// (@0x005163AF / @0x00454272-@0x00510523), which acdream deliberately
/// does not construct for a ballistic body (the register row this route
/// adds); <c>RejectedAuthority</c>/<c>RejectedData</c> and an ownership
/// mismatch are SWALLOWED — write nothing, never fall through to the
/// remote tail (trap T5). A caller must not fall back to any remote arm
/// when this returns <see langword="null"/>.
/// </para>
///
/// <para>
/// <b>No velocity write (D-P5).</b> Retail's <c>MoveOrTeleport</c>
/// @0x00516330 never references its velocity argument in the decompiled
/// body, and a byte-level disassembly of the PDB-paired binary
/// (@0x00516330-@0x00516438) confirms no instruction anywhere in the
/// function reads that argument's stack slot. This seam commits no
/// velocity from the Position packet at all — the Vector channel
/// (<see cref="RuntimeProjectilePhysicsUpdater.ApplyAuthoritativeVector"/>)
/// remains the sole velocity authority for a missile.
/// </para>
///
/// <para>
/// <b>No constraint leash armed (D-P4).</b> Unlike the remote arms, this
/// method never calls <c>TryArmConstraintAfterOperation</c> — the
/// classifier's projectile routes still carry
/// <c>ConstrainPhase.AfterPositionOperation</c> (kind-blind), but this arm
/// deliberately does not consume it, matching the pinned divergence.
/// </para>
///
/// <para>
/// <b>Teleport hook reduction (D-P4).</b> Of retail's six
/// <c>teleport_hook</c> @0x00514ED0 actions, five are structurally absent
/// for a manager-less missile (no <c>MovementManager</c>/
/// <c>PositionManager</c>/<c>TargetManager</c>). The sixth,
/// <c>report_collision_end(this, 1)</c> @0x00514F31-@0x00514620, applies
/// to any object with a collision table and runs BEFORE the placement —
/// ported here as <c>RuntimeCollisionReportingState.LeaveWorld</c> (the
/// exact force-end seam the 4b-3 round-2 review validated against the
/// same retail address).
/// </para>
///
/// <para>
/// Prediction is invalidated once per packet, before any body write on
/// this route (placement or the store fallback) — mirroring
/// <c>RuntimeProjectilePhysicsUpdater</c>'s existing invalidate-before-
/// write ordering — so an in-flight split quantum straddling this packet
/// aborts at <c>Complete</c> rather than clobbering a canonical
/// placement. The no-op dispositions invalidate nothing: the body is
/// untouched, so a straddling quantum completing over them is correct.
/// </para>
/// </summary>
internal RuntimeRemotePlacementExecutionStatus? ApplyAcceptedProjectilePosition(
RuntimeEntityRecord record,
in RuntimeAuthoritativePositionRoute route)
{
ArgumentNullException.ThrowIfNull(record);
if (route.OperationKind
is not RuntimeSetPositionOperationKind.ProjectileAuthoritative
|| record.Projectile is not RuntimeProjectile projectile
|| record.PhysicsBody is not { } body
|| !ReferenceEquals(body, projectile.Body))
{
return null;
}
// A6 fix (review round): captured BEFORE the placement dispatch,
// mirroring the deleted tail's `bool wasInWorld = body.InWorld;`
// ordering — TryExecuteAcceptedRemotePosition's canonical commit
// calls body.SnapToCell, which sets InWorld = true, so reading this
// AFTER the dispatch (as the first cut of this seam did) makes the
// re-activation branch below permanently dead on every committed
// outcome.
bool wasInWorld = body.InWorld;
RuntimeRemotePlacementExecutionStatus status;
switch (route.Disposition)
{
case RuntimeAuthoritativePositionDisposition.SetPosition:
_entityObjects.Physics.CollisionReports.LeaveWorld(record);
projectile.InvalidatePrediction();
status = TryExecuteAcceptedRemotePosition(record, route);
break;
case RuntimeAuthoritativePositionDisposition.SetPositionSimple:
// B1/B2 fix (round-2 review): retail's far branch runs
// `StopInterpolating` @0x005163C9-@0x005163CB whenever
// `position_manager != 0` — the SAME guard the remote far
// arm ports as `ApplyAcceptedRemoteFarSnap`'s
// `if (route.StopInterpolating) remote.Interp.Clear();`. A
// bare missile has no RemoteMotion so this is structurally
// inert, but the ADOPTED-BODY case (TryBind's shared-body
// branch: an ordinary remote whose Missile bit was set by a
// later State packet) carries a live Interp queue the far
// branch must clear too — the teleport hook only covers the
// SetPosition disposition.
if (route.StopInterpolating
&& record.RemoteMotion is RemoteMotion adoptedFar)
{
adoptedFar.Interp.Clear();
}
projectile.InvalidatePrediction();
status = TryExecuteAcceptedRemotePosition(record, route);
break;
default:
// Interpolate / NoPositionOperation: pinned no-op (D-P4).
// RejectedAuthority / RejectedData: swallow (T5) — the
// shared authority gate already rejected an invalid payload
// upstream; there is nothing left to route.
return null;
}
if (status.StoresAcceptedDestination())
StoreAcceptedDestinationPose(record);
if (status is not RuntimeRemotePlacementExecutionStatus.Deferred
and not RuntimeRemotePlacementExecutionStatus.RejectedByPlacement)
{
// Invariant 2: presentation advances on every committed/stored
// outcome only — Deferred/RejectedByPlacement leave the body at
// its prior (already-synced) pose.
SyncProjectilePresentation(record, projectile, body, wasInWorld);
}
return status;
}
/// <summary>
/// C4 route 5 (REVISED after the review round — A6/A7/A8): the
/// J5.6-owned post-commit lifecycle tail (InWorld/Activate/shadow-sync
/// on spatial+visible, suspend on spatial+hidden, deactivate+suspend on
/// non-spatial), reduced from the deleted
/// <c>RuntimeProjectilePhysicsUpdater.ApplyAuthoritativePosition</c>
/// tail (former <c>:390-422</c>) to this controller's own seam —
/// <paramref name="wasInWorld"/> is the caller's pre-dispatch capture
/// (A6: reading <c>body.InWorld</c> here, after the canonical commit's
/// own <c>SnapToCell</c> already forced it true, made the re-activation
/// branch permanently dead); <see cref="_clock"/> supplies the same
/// clock source the deleted method took as an explicit
/// <c>currentTime</c> parameter; the world-frame offset comes from
/// <see cref="RuntimePhysicsState.TryGetWorldFrameOffset"/> (the same
/// source <see cref="StoreAcceptedDestinationPose"/> uses) instead of an
/// App-supplied live-center pair.
/// </summary>
private void SyncProjectilePresentation(
RuntimeEntityRecord record,
RuntimeProjectile projectile,
PhysicsBody body,
bool wasInWorld)
{
if (!_entityObjects.Entities.IsCurrent(record)
|| !ReferenceEquals(record.Projectile, projectile)
|| !ReferenceEquals(record.PhysicsBody, body))
{
return;
}
RuntimePhysicsState physics = _entityObjects.Physics;
bool spatial = physics.IsSpatialProjectile(record, projectile);
bool hidden =
(record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
uint localId = record.LocalEntityId ?? 0u;
if (spatial && !hidden)
{
if (!wasInWorld)
{
body.LastUpdateTime = _clock.SimulationTimeSeconds;
if ((body.State & PhysicsStateFlags.Static) == 0)
body.TransientState |= TransientStateFlags.Active;
}
body.InWorld = true;
// A8 fix: #284's policy ("a frame that can never arrive is
// terminal, never silent") applies here exactly as it does to
// StoreAcceptedDestinationPose. A false result during the
// legitimate pre-local-player-Create window silently skips the
// publish (self-heals once the frame arrives); a false result
// AFTER that window is a genuinely stuck frame, and
// ThrowIfWorldFrameUnreachable escalates it instead of leaving
// the shadow silently stale forever.
if (physics.TryGetWorldFrameOffset(
record.FullCellId,
out float offsetX,
out float offsetY))
{
physics.Engine.ShadowObjects.UpdatePosition(
localId,
body.Position,
body.Orientation,
offsetX,
offsetY,
record.FullCellId,
seedCellId: record.FullCellId);
}
else
{
physics.ThrowIfWorldFrameUnreachable(record.FullCellId);
}
}
else if (spatial)
{
body.InWorld = true;
// A7 fix: retail's hidden-branch clock consumption — restored,
// matching ProjectileController.TryBind's equivalent branch
// ("consume the hidden clock so UnHide cannot replay a time
// backlog").
body.LastUpdateTime = _clock.SimulationTimeSeconds;
physics.Engine.ShadowObjects.Suspend(localId);
}
else
{
body.InWorld = false;
body.TransientState &= ~TransientStateFlags.Active;
physics.Engine.ShadowObjects.Suspend(localId);
}
}
/// <summary>
/// Retail <c>CPhysicsObj::store_position</c> @0x00515CE2, reached from
/// <c>SetPositionInternal</c>'s no-resolvable-cell branch @0x00515C1D.
@ -975,6 +1219,29 @@ internal sealed class RuntimeRemotePlacementDriveController
}
_pending.Remove(key);
// R3 fix (review round): a retained retry can belong to a
// projectile operation exactly as it can belong to a
// remote's — this is the SAME shared _pending map (trap T9:
// no second map), and pending.Route carries the OperationKind
// that was classified when the retry was first parked.
// Neither invariant this route pins (prediction invalidated
// before every body write; presentation advances on every
// committed/stored outcome) may hold on the direct arm only.
// Real nullable locals (not a stored bool) so the compiler
// can track definite assignment through the branches below.
RuntimeProjectile? pendingProjectile = null;
PhysicsBody? pendingBody = null;
if (pending.Route.OperationKind
is RuntimeSetPositionOperationKind.ProjectileAuthoritative
&& pending.Record.Projectile is RuntimeProjectile candidateProjectile
&& pending.Record.PhysicsBody is { } candidateBody
&& ReferenceEquals(candidateBody, candidateProjectile.Body))
{
pendingProjectile = candidateProjectile;
pendingBody = candidateBody;
}
bool pendingWasInWorld = pendingBody?.InWorld ?? false;
// B3 review fix: a retry can sit retained across many host
// cadence pumps (bounded only by how long the asset stayed
// unavailable) while its destination's collision publication
@ -1000,17 +1267,55 @@ internal sealed class RuntimeRemotePlacementDriveController
CreateObject.ServerPosition? destination =
pending.Record.Snapshot.Physics?.Position
?? pending.Record.Snapshot.Position;
RuntimeRemotePlacementExecutionStatus retryStatus;
if (destination is not { } accepted
|| !CanAttemptDestination(
setPosition,
accepted.LandblockId))
{
CancelToken(setPosition, pending.Token);
pendingProjectile?.InvalidatePrediction();
StoreAcceptedDestinationPose(pending.Record);
continue;
retryStatus = RuntimeRemotePlacementExecutionStatus.Refused;
}
else
{
// B5 fix (round-2 review): invalidating BEFORE this call
// unconditionally was wrong when SubmitAndResolve itself
// re-parks (returns Contention) — that outcome writes
// NOTHING (no store, no commit), so invalidating for it
// violates invariant 4's "the no-op dispositions
// invalidate nothing" on this arm specifically (unlike
// the entry point, where StoresAcceptedDestination()
// treats Contention as a storing outcome via the
// caller's own StoreAcceptedDestinationPose — this retry
// arm does not store on a re-parked Contention, matching
// the pre-existing residual A3/round-1 already named).
// Invalidating AFTER the call instead of before is safe
// here: this method is single-threaded and synchronous,
// so a write performed inside SubmitAndResolve and the
// very next statement's invalidate are never observably
// separated by a quantum's Complete call.
retryStatus = SubmitAndResolve(
pending.Record, pending.Token, pending.Route);
if (retryStatus
is not RuntimeRemotePlacementExecutionStatus.Contention)
{
pendingProjectile?.InvalidatePrediction();
}
}
_ = SubmitAndResolve(pending.Record, pending.Token, pending.Route);
if (pendingProjectile is { } confirmedProjectile
&& pendingBody is { } confirmedBody
&& retryStatus is not RuntimeRemotePlacementExecutionStatus.Deferred
and not RuntimeRemotePlacementExecutionStatus.RejectedByPlacement)
{
SyncProjectilePresentation(
pending.Record,
confirmedProjectile,
confirmedBody,
pendingWasInWorld);
}
}
}
finally