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:
parent
30d3d114b0
commit
36255af0f6
19 changed files with 5390 additions and 393 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue