feat(physics): C4 route 4b-2 — remote far snap through the canonical placement
Flips the SetPositionSimple classification (contact, PlayerDistance >= 96 m) for remotes onto 4b-1's drive controller and deletes both legacy far blocks, both duplicated 96f/4f constant pairs, and both `?? Vector3.Zero` fabrications. The 4 m constant now exists exactly once. Teleport and cell-less stay legacy for 4b-3. Retail: MoveOrTeleport @0x00516330's far branch runs StopInterpolating @0x005163CB before SetPositionSimple @0x005163D9 and returns 1 @0x005163E8 regardless — the SetPositionError is discarded — so HandleReceivedPosition arms ConstrainTo @0x00454272 post-move on commit AND on failure. The x87 parity decode at @0x00516393-@0x0051639E puts exactly 96.0 on the far branch. SetPositionSimple @0x005162B0 builds flags 0x1012 at @0x005162C4. Non-commit outcomes still advance the body, because retail's SetPositionInternal @0x00515BD0 commits the destination via store_position @0x00515CE2 when no cell resolves. The partition is by STAGE, not heuristic, enforced by an exhaustive switch: Refused/Contention/NotApplicable/RejectedPreparation store (the placement never executed); Committed/Deferred/RejectedByPlacement do not (the engine ran and refused, matching retail's non-storing returns @0x00515CB2 and @0x00515CD5). Without this a refused far snap froze the remote with an emptied queue. Also fixes a shipped defect this route made live: ParkDeferred's quiescence parks withdrew the entity (InWorld=false, clock suspended, residency removed) and were never restorable, while Forget(restoreCancelledPark: true) runs for every accepted Position on every entity. The restorable decision now lives inside ParkDeferred AFTER SnapToCell, reading body.CellPosition.ObjCellId — the value RestoreParkWithdrawal actually restores at — against every live quiescence rather than one minimum-OperationId token. The three pre-snap fields are hoisted into locals because SnapToCell ends with InWorld = true. ParkCollisionResidents passes restorableOnCancel: false explicitly; the plain unplaceable park is provably unchanged. RestoreParkWithdrawal re-tests the prefix at restore time so a retained route-2 park cannot re-admit into a prefix that began quiescing during the park. CanAttemptDestination is retained as an OPTIMISATION only, with the two Core predicates it cannot reproduce written down at the pre-flight, plus the two properties that depend on it staying there. Four fix rounds and eight Opus reviews. The slice was fully green at 10,990, 10,997 and 11,004 while containing real defects — a frozen remote pinned as correct by its own test, a fallback that over-wrote on the exact retail paths that decline to store, and a park guard incomplete on two independent axes. Register: AP-137 (leftover classifications take AP-87's catch-up; states the cell-less enqueue-vs-place delta deferred to 4b-3, that RejectedData is applied anyway, and the headless divergence), AP-138 (the refusable far placement), AP-136 narrowed to match the relocation. #309's acceptance steps rewritten — step 5 previously asserted a recovery the code does not perform — and gated on a new ACDREAM_PROBE_PARK=1 signal so the check cannot pass while broken. Suite 11,009 passed / 4 skipped / 0 failed against a measured 10,968 baseline. The 10,973 figure recorded earlier was wrong and is corrected here. Connected gate outstanding: the two-client far-snap walk and #309. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
1b631f127d
commit
7f1c1f5aa6
24 changed files with 5234 additions and 375 deletions
|
|
@ -65,6 +65,13 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
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 >= 96 m</c>) is its
|
||||
/// first, so its ownership ledger stops being tautologically zero here.
|
||||
/// </summary>
|
||||
private readonly RuntimeRemotePlacementDriveController _remotePlacementDrive;
|
||||
|
||||
private PlayerMovementController? _playerController => _playerControllerSource.Controller;
|
||||
private EntityPhysicsHost? _playerHost => _playerHostSource.Host;
|
||||
|
|
@ -106,6 +113,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
Action<uint, AcceptedPhysicsTimestamps> publishTimestamps,
|
||||
IMovementTruthDiagnosticSink movementTruthDiagnostics,
|
||||
RuntimeAcceptedPositionDriveController acceptedPositionDrive,
|
||||
RuntimeRemotePlacementDriveController remotePlacementDrive,
|
||||
InventoryWorldDropProjectionController? worldDropProjection = null)
|
||||
{
|
||||
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
||||
|
|
@ -143,6 +151,8 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
?? throw new ArgumentNullException(nameof(movementTruthDiagnostics));
|
||||
_acceptedPositionDrive = acceptedPositionDrive
|
||||
?? throw new ArgumentNullException(nameof(acceptedPositionDrive));
|
||||
_remotePlacementDrive = remotePlacementDrive
|
||||
?? throw new ArgumentNullException(nameof(remotePlacementDrive));
|
||||
_worldDropProjection = worldDropProjection;
|
||||
}
|
||||
|
||||
|
|
@ -822,9 +832,19 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
/// 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 and leaves the legacy path untouched. Callers
|
||||
/// must only invoke this for a genuinely remote (never local-player)
|
||||
/// entity whose <c>remotePlacementRequired</c> gate is already false.
|
||||
/// makes Runtime decline. Callers must only invoke this for a genuinely
|
||||
/// remote (never local-player) entity whose
|
||||
/// <c>remotePlacementRequired</c> gate is already false.
|
||||
///
|
||||
/// <para>
|
||||
/// C4 route 4b-2 review fix — this comment used to end "and leaves the
|
||||
/// legacy path untouched". There is no legacy path left: the duplicated
|
||||
/// App-side near/far blocks were deleted with this slice, and a declined
|
||||
/// classification now takes the stated <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 changed is what the caller does with it.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private RuntimeAuthoritativePositionRoute? ClassifyRemoteAcceptedPosition(
|
||||
AcDream.Core.Net.WorldSession.EntityPositionUpdate update,
|
||||
|
|
@ -854,8 +874,20 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
/// 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. Every other classification keeps the pre-existing
|
||||
/// write, unchanged, until route 4b.
|
||||
/// defect consisted of.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// C4 route 4b-2 review fix — this comment used to end "Every other
|
||||
/// classification keeps the pre-existing write, unchanged, until route
|
||||
/// 4b", which is now false in both halves. The gate is still
|
||||
/// <c>OwnsSteadyState</c>, so the FAR snap (4b-2's own arm) DOES take the
|
||||
/// wire-pose write here even though it goes on to place canonically. That
|
||||
/// is deliberate and is not a second writer in the route 2 sense: the far
|
||||
/// arm's tail re-syncs the render entity from the RESOLVED body
|
||||
/// afterwards, so this write only covers the window before the placement
|
||||
/// commits, exactly as it did before the slice. Route 4b-3 revisits the
|
||||
/// gate when it takes the cell-less half.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
|
|
@ -893,12 +925,12 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Which arm of the non-player-remote contact routing claimed a
|
||||
/// packet. The value is the seam's observable outcome, asserted by the
|
||||
/// acceptance tests; production only distinguishes
|
||||
/// <see cref="RemoteContactArm.Legacy"/>, but the finer result is what
|
||||
/// makes the PRECEDENCE testable and must not be collapsed to a
|
||||
/// bool.</summary>
|
||||
/// <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 only distinguishes
|
||||
/// <see cref="RemoteContactArm.FarSnapPlacement"/> (which alone can be
|
||||
/// re-entrant), but the finer result is what makes the PRECEDENCE and the
|
||||
/// arm selection testable and must not be collapsed to a bool.</summary>
|
||||
internal enum RemoteContactArm : byte
|
||||
{
|
||||
/// <summary>The body was airborne. Hard-snapped, exactly as before
|
||||
|
|
@ -914,33 +946,87 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
/// <summary>Route 4a's near InterpolateTo branch.</summary>
|
||||
SteadyStateInterpolate,
|
||||
|
||||
/// <summary>Neither: the caller runs its own untouched legacy
|
||||
/// near/far routing.</summary>
|
||||
Legacy,
|
||||
/// <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>The acdream-only leftover set (null, <c>Rejected*</c>, the
|
||||
/// cell-less <c>SetPosition</c> half 4b-3 will own). See
|
||||
/// <see cref="RuntimeRemoteAcceptedPositionArm.UnroutedCatchUp"/> for
|
||||
/// the stated policy.</summary>
|
||||
UnroutedCatchUp,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C4 route 4a: the ORDERING carve-out for the non-player-remote arm. An
|
||||
/// airborne body's contact packet keeps its pre-existing authoritative
|
||||
/// hard-snap and is decided BEFORE route 4a's near-Interpolate branch can
|
||||
/// claim it — the player-remote arm gets the same precedence structurally,
|
||||
/// because its landing block sits ahead of its own routing and returns.
|
||||
/// 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>
|
||||
/// This exists as one entry point precisely so the precedence is
|
||||
/// observable: 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.
|
||||
/// Landing is not a behaviour route 4a is scoped to change.
|
||||
/// 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 static RemoteContactArm ApplyRemoteContactRouting(
|
||||
internal readonly record struct RemoteContactRouting(
|
||||
RemoteContactArm Arm,
|
||||
RuntimeRemotePlacementExecutionStatus? Placement);
|
||||
|
||||
/// <summary>
|
||||
/// The complete remote grounded/contact routing for ONE accepted Position,
|
||||
/// shared by the player-remote and NPC-remote arms — 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. The player-remote caller
|
||||
/// reaches this method only with <c>Airborne == false</c> (its landing
|
||||
/// block sits ahead of its routing and returns), so the carve-out is inert
|
||||
/// there and the two callers stay one decision.
|
||||
/// </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. <b>The far arm is the only re-entrant one</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
|
||||
/// <see cref="RemoteContactArm.FarSnapPlacement"/>, on EVERY placement
|
||||
/// status, before writing anything else for the packet. That includes the
|
||||
/// <c>ConstrainTo</c> leash: both arms therefore run the re-validation
|
||||
/// FIRST and arm 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)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(placementDrive);
|
||||
ArgumentNullException.ThrowIfNull(canonical);
|
||||
ArgumentNullException.ThrowIfNull(remote);
|
||||
if (remote.Airborne)
|
||||
{
|
||||
|
|
@ -959,19 +1045,126 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
// the queue is empty here could delete it.
|
||||
remote.Body.Position = worldPos;
|
||||
remote.Body.Orientation = rotation;
|
||||
return RemoteContactArm.AirborneSnap;
|
||||
return new RemoteContactRouting(
|
||||
RemoteContactArm.AirborneSnap, Placement: null);
|
||||
}
|
||||
|
||||
if (!RuntimeRemoteSteadyStatePosition.IsNearInterpolate(route))
|
||||
return RemoteContactArm.Legacy;
|
||||
switch (RuntimeRemoteFarSnapPosition.ResolveArm(route))
|
||||
{
|
||||
case RuntimeRemoteAcceptedPositionArm.FarSnapPlacement:
|
||||
return new RemoteContactRouting(
|
||||
RemoteContactArm.FarSnapPlacement,
|
||||
placementDrive.ApplyAcceptedRemoteFarSnap(
|
||||
canonical,
|
||||
remote,
|
||||
route!.Value));
|
||||
|
||||
RuntimeRemoteSteadyStatePosition.ApplyInterpolate(
|
||||
remote,
|
||||
worldPos,
|
||||
rotation,
|
||||
isMovingTo: remote.Movement.IsMovingTo(),
|
||||
willBeDrTicked);
|
||||
return RemoteContactArm.SteadyStateInterpolate;
|
||||
case RuntimeRemoteAcceptedPositionArm.NearInterpolate:
|
||||
RuntimeRemoteSteadyStatePosition.ApplyInterpolate(
|
||||
remote,
|
||||
worldPos,
|
||||
rotation,
|
||||
isMovingTo: remote.Movement.IsMovingTo(),
|
||||
willBeDrTicked);
|
||||
return new RemoteContactRouting(
|
||||
RemoteContactArm.SteadyStateInterpolate, Placement: null);
|
||||
|
||||
case RuntimeRemoteAcceptedPositionArm.AirborneNoOperation:
|
||||
// R10 review fix: explicit rather than folded into `default`,
|
||||
// where the comment ASSERTED unreachability that no code
|
||||
// enforced. Retail's arg4 == 0 branch writes NOTHING at all
|
||||
// (@0x0051636D returns 0), so there is no operation this
|
||||
// method could perform; both production callers early-return
|
||||
// on IsAirborneNoOperation before they route (the player arm's
|
||||
// AIRBORNE NO-OP block, the NPC arm's mirror of it — the two
|
||||
// `IsAirborneNoOperation` call sites in this file, cited here
|
||||
// by name because line numbers went stale within one review
|
||||
// round). Reaching here means a caller
|
||||
// skipped that gate, and the only faithful answer is to say
|
||||
// so — ApplyInterpolate's own doc likewise forbids being
|
||||
// called for this disposition.
|
||||
throw new InvalidOperationException(
|
||||
"A NoPositionOperation (airborne no-op) classification "
|
||||
+ "must be handled by the caller's own early return "
|
||||
+ "before routing; retail's MoveOrTeleport writes nothing "
|
||||
+ "at all on that branch (@0x0051636D).");
|
||||
|
||||
default:
|
||||
// UnroutedCatchUp takes the SAME AP-87 catch-up the near
|
||||
// branch uses — the stated policy (AP-137), and the reason
|
||||
// the App's two duplicated 96 m / 4 m constant pairs and both
|
||||
// fabricated Vector3.Zero player positions are gone.
|
||||
RuntimeRemoteSteadyStatePosition.ApplyInterpolate(
|
||||
remote,
|
||||
worldPos,
|
||||
rotation,
|
||||
isMovingTo: remote.Movement.IsMovingTo(),
|
||||
willBeDrTicked);
|
||||
return new RemoteContactRouting(
|
||||
RemoteContactArm.UnroutedCatchUp, Placement: null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C4 route 4b-2: the NPC-remote arm's post-routing wire-cell adoption,
|
||||
/// extracted so its ONE suppression rule is exercised by production and by
|
||||
/// test through the same entry point rather than restated in a test body.
|
||||
///
|
||||
/// <para>
|
||||
/// <see cref="RemoteMotion.CellId"/> writes THROUGH to the canonical
|
||||
/// <c>FullCellId</c> (<c>RuntimePhysicsState.CommitCanonicalCell</c>).
|
||||
/// After a far snap 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 that arm alone. Unlike the player arm, whose identical
|
||||
/// write sits BEFORE its routing, the NPC one sits after; leaving it
|
||||
/// unguarded would discard a resolved cell that differs from the wire
|
||||
/// cell. Every other arm performs no placement, so the wire cell is still
|
||||
/// the newest truth there.
|
||||
/// </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>
|
||||
/// 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)
|
||||
return false;
|
||||
remote.CellId = wireCellId;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1465,11 +1658,12 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
// near InterpolateTo queue — no direct body write here) never
|
||||
// receives it. remotePlacementRequired guarantees the classifier's
|
||||
// teleport (SetPosition) disposition never reaches here — that stays
|
||||
// route 4b. The local player never reaches this generic-remote code
|
||||
// path at all. Every classification 4a does NOT own — the >=96 m
|
||||
// far snap, a cell-less remote, a rejected authority or payload, and
|
||||
// "no classification at all" — falls through to the pre-existing
|
||||
// legacy routing completely unchanged; 4b deletes that fallback.
|
||||
// route 4b-3. The local player never reaches this generic-remote code
|
||||
// path at all. C4 route 4b-2 additionally routes the >=96 m far snap
|
||||
// through the canonical Runtime placement owner; a cell-less remote, a
|
||||
// rejected authority or payload, and "no classification at all" take
|
||||
// the stated UnroutedCatchUp policy
|
||||
// (RuntimeRemoteFarSnapPosition.ResolveArm) until 4b-3.
|
||||
RuntimeAuthoritativePositionRoute? earlyRemoteRoute =
|
||||
update.Guid != _playerServerGuid && !remotePlacementRequired
|
||||
? ClassifyRemoteAcceptedPosition(
|
||||
|
|
@ -1491,9 +1685,18 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
// ordinary moving remote's draw bucket, commits its canonical
|
||||
// FullCellId (which feeds back as the classifier's own
|
||||
// CommittedCellId and as the ConstraintDistance cell key), and
|
||||
// recovers a pending bucket promotion. Neither 4a branch performs a
|
||||
// placement, so unlike route 2 there is no committed placement
|
||||
// receipt that could project this in its stead.
|
||||
// recovers a pending bucket promotion.
|
||||
//
|
||||
// C4 route 4b-2 review fix — this used to end "Neither 4a branch
|
||||
// performs a placement, so unlike route 2 there is no committed
|
||||
// placement receipt that could project this in its stead". The far
|
||||
// arm DOES perform a placement now, and this call still runs ahead of
|
||||
// it for every classification. That ordering is what makes the NPC
|
||||
// arm's post-routing wire-cell suppression a no-op on a non-commit
|
||||
// outcome (TryAdoptWireCellAfterRouting): the wire full cell is
|
||||
// already canonical by the time routing starts. A COMMITTED placement
|
||||
// resolves its own destination cell afterwards, which is the case the
|
||||
// suppression exists for.
|
||||
if (!_liveEntities!.RebucketLiveEntity(update.Guid, p.LandblockId)
|
||||
|| !_liveEntities.TryGetRecord(
|
||||
update.Guid,
|
||||
|
|
@ -1707,10 +1910,14 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
// a nonzero MoveOrTeleport return (@0x00454272, inside the
|
||||
// `if (MoveOrTeleport(...) != 0)` at @0x00454254) — so this
|
||||
// pre-operation, unconditional arming is now the LEGACY shape and
|
||||
// runs only for the classifications 4a does not own. The two 4a
|
||||
// classifications arm it after their own operation instead, or
|
||||
// (the airborne no-op) not at all. 4b deletes this fallback.
|
||||
if (!RuntimeRemoteSteadyStatePosition.OwnsSteadyState(earlyRemoteRoute)
|
||||
// runs only for the classifications the post-operation arm does
|
||||
// not own. C4 route 4b-2 added the far snap to that set, leaving
|
||||
// this fallback for the cell-less half, the two rejections, and
|
||||
// "no classification at all"; 4b-3 deletes it. The gate reads the
|
||||
// SAME predicate TryArmConstraintAfterOperation does, so exactly
|
||||
// one of the two sites arms any given classification.
|
||||
if (!RuntimeRemoteFarSnapPosition.OwnsAfterOperationConstraint(
|
||||
earlyRemoteRoute)
|
||||
&& rmState.Host is { } remoteConstraintHost)
|
||||
{
|
||||
RuntimeRemoteSteadyStatePosition.ArmConstraintAfterOperation(
|
||||
|
|
@ -1911,72 +2118,66 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
}
|
||||
|
||||
// ── GROUNDED ROUTING (CPhysicsObj::MoveOrTeleport) ────────────
|
||||
// C4 route 4a: the near (Interpolate) decision — including the
|
||||
// AP-87 placement-snap backstop — now lives in
|
||||
// RuntimeRemoteSteadyStatePosition.ApplyInterpolate, shared
|
||||
// with the NPC branch below. Every OTHER classification (the
|
||||
// >=96 m far snap, a cell-less remote, a rejection, or none at
|
||||
// all) keeps the pre-existing legacy near/far routing below,
|
||||
// unchanged, until route 4b — "not Interpolate" is NOT "far".
|
||||
// C4 routes 4a + 4b-2: the complete near/far/leftover decision
|
||||
// is the SAME shared entry point the NPC arm below calls —
|
||||
// retail's disassembly makes no `this == player` distinction
|
||||
// on any of these branches. The player arm reaches it only
|
||||
// with Airborne == false (the landing block above returns), so
|
||||
// the airborne carve-out inside is inert here.
|
||||
bool willBeDrTicked = WillAdvanceRemoteMotion(update.Guid, rmState);
|
||||
if (RuntimeRemoteSteadyStatePosition.IsNearInterpolate(
|
||||
earlyRemoteRoute))
|
||||
{
|
||||
RuntimeRemoteSteadyStatePosition.ApplyInterpolate(
|
||||
rmState,
|
||||
worldPos,
|
||||
rot,
|
||||
isMovingTo: rmState.Movement.IsMovingTo(),
|
||||
willBeDrTicked);
|
||||
RemoteContactRouting playerRouting = ApplyRemoteContactRouting(
|
||||
_remotePlacementDrive,
|
||||
acceptedPositionCanonical,
|
||||
rmState,
|
||||
earlyRemoteRoute,
|
||||
worldPos,
|
||||
rot,
|
||||
willBeDrTicked);
|
||||
|
||||
// D2: ConstrainTo arms strictly AFTER the operation,
|
||||
// anchored post-move — retail arms it only once
|
||||
// MoveOrTeleport returns nonzero (@0x00454254/@0x00454272).
|
||||
RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation(
|
||||
earlyRemoteRoute,
|
||||
rmState);
|
||||
}
|
||||
else
|
||||
// C4 route 4b-2: the far arm is re-entrant — the canonical
|
||||
// placement publishes its Place receipt (or, on a non-commit
|
||||
// outcome, its cancellation receipt) synchronously, and the
|
||||
// production projection sink can delete or replace this
|
||||
// incarnation from inside either. Re-validate before ANY
|
||||
// further write for this packet, exactly as the landing block
|
||||
// does after MovementManager.HitGround.
|
||||
//
|
||||
// R5 review fix: this now sits BEFORE the leash arming, which
|
||||
// is the same order the NPC arm has always had — the two arms
|
||||
// were mirror images of each other and one of them had to be
|
||||
// wrong. Arming is a write (it stamps rmState.Host's
|
||||
// PositionManager), and this class's own rule is that nothing
|
||||
// may be written through a superseded owner. The residual
|
||||
// versus retail's unconditional arm on a nonzero
|
||||
// MoveOrTeleport return is AP-138.
|
||||
if (playerRouting.Arm is RemoteContactArm.FarSnapPlacement
|
||||
&& (!IsCurrentPositionOwner(entity)
|
||||
|| !ReferenceEquals(
|
||||
positionRecord.RemoteMotionRuntime,
|
||||
rmState)))
|
||||
{
|
||||
// LEGACY near/far routing, unchanged. Its leash was
|
||||
// already armed by the legacy pre-operation call above.
|
||||
const float MaxPhysicsDistance = 96f; // retail player_distance far-snap
|
||||
const float BodySnapThreshold = 4f; // large correction / teleport / unplaced -> snap
|
||||
var localPlayerPos = _playerController?.Position ?? System.Numerics.Vector3.Zero;
|
||||
float dist = System.Numerics.Vector3.Distance(worldPos, localPlayerPos);
|
||||
// #184 Slice 2b: the player UP routing gains the SAME placement-snap
|
||||
// backstop the NPC routing got in Slice 1 (AP-87). The 4 m
|
||||
// bodyToTarget guard is the LOAD-BEARING backstop;
|
||||
// !willBeDrTicked snaps a no-Sequencer player whose queue nothing
|
||||
// would consume; dist>96 is retail's far-snap.
|
||||
float bodyToTarget = System.Numerics.Vector3.Distance(
|
||||
rmState.Body.Position, worldPos);
|
||||
|
||||
if (dist > MaxPhysicsDistance || !willBeDrTicked
|
||||
|| bodyToTarget > BodySnapThreshold)
|
||||
{
|
||||
// Beyond view bubble / large correction / unplaced body:
|
||||
// SetPositionSimple slide-snap. Clear queue.
|
||||
rmState.Interp.Clear();
|
||||
rmState.Body.Position = worldPos;
|
||||
rmState.Body.Orientation = rot;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Within view bubble, placed + near: enqueue waypoint for
|
||||
// adjust_offset to walk to.
|
||||
System.Numerics.Quaternion? immediateOrientation =
|
||||
rmState.Interp.Enqueue(
|
||||
worldPos,
|
||||
rot,
|
||||
isMovingTo: rmState.Movement.IsMovingTo(),
|
||||
currentBodyPosition: rmState.Body.Position,
|
||||
currentBodyOrientation: rmState.Body.Orientation);
|
||||
if (immediateOrientation is { } closeOrientation)
|
||||
rmState.Body.Orientation = closeOrientation;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// D2: ConstrainTo arms strictly AFTER the operation, anchored
|
||||
// post-move — retail arms it only once MoveOrTeleport returns
|
||||
// nonzero (@0x00454254/@0x00454272), which the near AND far
|
||||
// branches both do (@0x005163BE, @0x005163E8). The far branch
|
||||
// arms on EVERY placement outcome, including a failed one:
|
||||
// retail discards SetPositionSimple's SetPositionError and
|
||||
// returns 1 regardless. Every remaining classification already
|
||||
// armed through the legacy pre-operation call above.
|
||||
//
|
||||
// Delta review N4: retail's arm is unconditional, acdream's is
|
||||
// not — the currency guard immediately above returns without
|
||||
// arming when the far arm's synchronous receipt replaced or
|
||||
// deleted this incarnation. That one-packet gap is the third
|
||||
// part of AP-138, and this comment must not read as though the
|
||||
// arm below is reached on every far-snap outcome.
|
||||
RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation(
|
||||
earlyRemoteRoute,
|
||||
rmState);
|
||||
|
||||
// Track the UP-derived synth velocity for diagnostics
|
||||
// ([VEL_DIAG] pace comparison). L.2g S5 (2026-07-02): the
|
||||
// #39-era cycle-refinement call that used to live here is
|
||||
|
|
@ -1997,12 +2198,30 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
}
|
||||
}
|
||||
|
||||
// Sync the visible entity to the body — overrides the unconditional
|
||||
// entity.SetPosition(worldPos) snap at the top of this function.
|
||||
// For the far-snap branch this is a no-op (body == worldPos); for
|
||||
// the near-enqueue branch this prevents a 1-frame teleport-then-
|
||||
// yank-back rubber-band as TickAnimations chases worldPos via the
|
||||
// queue.
|
||||
// Sync the visible entity to the body — overrides the
|
||||
// entity.SetPosition(worldPos) write at the top of this
|
||||
// function (TryApplyGenericRemoteRenderPose, suppressed for
|
||||
// the two 4a classifications). This prevents a 1-frame
|
||||
// teleport-then-yank-back rubber-band as TickAnimations
|
||||
// chases worldPos via the queue.
|
||||
//
|
||||
// C4 route 4b-2 review fix — this used to claim "For the
|
||||
// far-snap branch this is a no-op (body == worldPos)". The
|
||||
// PROVENANCE claim behind that was always wrong and is what
|
||||
// matters here: the far arm's body pose comes from the
|
||||
// canonical accepted destination resolved through Runtime's
|
||||
// world frame (a committed placement, a park's snap, or
|
||||
// store_position), never from the caller's separately-derived
|
||||
// worldPos, and this write is what carries that canonical pose
|
||||
// to the render entity.
|
||||
//
|
||||
// Delta review N4 — the VALUE claim, stated correctly: on the
|
||||
// store_position path the two happen to be equal, because #283
|
||||
// proved App's streaming origin and Runtime's world frame
|
||||
// cannot disagree and both compose the same accepted origin.
|
||||
// It is genuinely NOT a no-op on the committed path, where the
|
||||
// body carries the collision-settled spherePath.CurPos. Do not
|
||||
// "simplify" this write away on the strength of the equal case.
|
||||
//
|
||||
// #184 Slice 2b: sync the player-remote shadow to the RESOLVED/placed
|
||||
// body (mirrors the NPC UP-branch tail). Now that grounded players run
|
||||
|
|
@ -2105,21 +2324,18 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
Console.WriteLine(FormattableString.Invariant(
|
||||
$"[sticky-snap-skip] guid=0x{update.Guid:X8} d={snapDist:F3} srv=({worldPos.X:F2},{worldPos.Y:F2}) body=({rmState.Body.Position.X:F2},{rmState.Body.Position.Y:F2})"));
|
||||
}
|
||||
var npcRouting = new RemoteContactRouting(
|
||||
RemoteContactArm.UnroutedCatchUp, Placement: null);
|
||||
if (!snapSuppressedByStick)
|
||||
{
|
||||
// C4 route 4a: the near (Interpolate) decision — including the
|
||||
// AP-87 placement-snap backstop — is now the SAME Runtime
|
||||
// owner the player-remote branch above calls; retail's
|
||||
// MoveOrTeleport (0x00516330) makes no `this == player`
|
||||
// distinction, so the two per-kind copies became one. TS-44's
|
||||
// sticky suppression stays an NPC-only CALLER gate (this
|
||||
// `if`), which is what its register row describes and what
|
||||
// the player arm has never had.
|
||||
// C4 routes 4a + 4b-2: the complete near/far/leftover decision
|
||||
// is the SAME shared entry point the player-remote branch
|
||||
// above calls; retail's MoveOrTeleport (0x00516330) makes no
|
||||
// `this == player` distinction, so the two per-kind copies
|
||||
// became one. TS-44's sticky suppression stays an NPC-only
|
||||
// CALLER gate (this `if`), which is what its register row
|
||||
// describes and what the player arm has never had.
|
||||
//
|
||||
// Every OTHER classification (the >=96 m far snap, a
|
||||
// cell-less remote, a rejection, or none at all) keeps the
|
||||
// pre-existing legacy routing below, unchanged, until route
|
||||
// 4b — "not Interpolate" is NOT "far".
|
||||
// #184 (2026-07-07): an AIRBORNE body keeps its authoritative
|
||||
// hard-snap (the arc integrates locally, K-fix15), and that
|
||||
// decision is taken FIRST — a landing packet classifies
|
||||
|
|
@ -2127,73 +2343,42 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
// airborne test would enqueue a body that must plant, and a
|
||||
// creature knocked off a ledge would glide down over a packet
|
||||
// interval. Physics digest 2026-07-07 banner.
|
||||
if (ApplyRemoteContactRouting(
|
||||
rmState,
|
||||
earlyRemoteRoute,
|
||||
worldPos,
|
||||
rot,
|
||||
WillAdvanceRemoteMotion(update.Guid, rmState))
|
||||
is RemoteContactArm.Legacy)
|
||||
{
|
||||
// LEGACY NPC near/far routing, unchanged. Its leash was
|
||||
// already armed by the legacy pre-operation call above.
|
||||
// The body is PLACED (hard-snapped) whenever it is not
|
||||
// already tracking NEAR the server position — the first
|
||||
// UP, a large correction / teleport, an out-of-view
|
||||
// creature (>96 m from the local player), or an entity the
|
||||
// DR loop won't tick — otherwise the server point is a
|
||||
// GENTLE dead-reckoning TARGET the per-tick interp
|
||||
// catch-up walks to, and the KEPT sweep de-overlaps that
|
||||
// movement.
|
||||
const float MaxPhysicsDistanceNpc = 96f; // retail player_distance far-snap
|
||||
const float BodySnapThresholdNpc = 4f; // large correction / teleport -> snap
|
||||
var localPlayerPosNpc = _playerController?.Position
|
||||
?? System.Numerics.Vector3.Zero;
|
||||
float distNpc = System.Numerics.Vector3.Distance(worldPos, localPlayerPosNpc);
|
||||
float bodyToTargetNpc = System.Numerics.Vector3.Distance(
|
||||
rmState.Body.Position, worldPos);
|
||||
bool firstUpNpc = rmState.LastServerPosTime <= 0.0;
|
||||
bool willBeDrTickedNpc = WillAdvanceRemoteMotion(
|
||||
update.Guid,
|
||||
rmState);
|
||||
npcRouting = ApplyRemoteContactRouting(
|
||||
_remotePlacementDrive,
|
||||
acceptedPositionCanonical,
|
||||
rmState,
|
||||
earlyRemoteRoute,
|
||||
worldPos,
|
||||
rot,
|
||||
WillAdvanceRemoteMotion(update.Guid, rmState));
|
||||
|
||||
if (firstUpNpc || !willBeDrTickedNpc
|
||||
|| distNpc > MaxPhysicsDistanceNpc
|
||||
|| bodyToTargetNpc > BodySnapThresholdNpc)
|
||||
{
|
||||
// Placement / far / large-correction: SNAP + clear queue.
|
||||
rmState.Interp.Clear();
|
||||
rmState.Body.Position = worldPos;
|
||||
rmState.Body.Orientation = rot;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Near DR correction: enqueue the waypoint for the per-tick
|
||||
// catch-up (Path B consumes it via ComputeOffset).
|
||||
System.Numerics.Quaternion? immediateOrientation =
|
||||
rmState.Interp.Enqueue(
|
||||
worldPos,
|
||||
rot,
|
||||
isMovingTo: rmState.Movement.IsMovingTo(),
|
||||
currentBodyPosition: rmState.Body.Position,
|
||||
currentBodyOrientation: rmState.Body.Orientation);
|
||||
if (immediateOrientation is { } closeOrientation)
|
||||
rmState.Body.Orientation = closeOrientation;
|
||||
}
|
||||
// C4 route 4b-2: the far arm is re-entrant (see
|
||||
// ApplyRemoteContactRouting's own remarks). Re-validate before
|
||||
// any further write for this packet — including the leash
|
||||
// arming below, which is why the player arm now runs this
|
||||
// check in the SAME position relative to its own arming call
|
||||
// (R5 review fix).
|
||||
if (npcRouting.Arm is RemoteContactArm.FarSnapPlacement
|
||||
&& (!IsCurrentPositionOwner(entity)
|
||||
|| !ReferenceEquals(
|
||||
positionRecord.RemoteMotionRuntime,
|
||||
rmState)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// D2: ConstrainTo arms strictly AFTER the operation above,
|
||||
// anchored post-move (@0x00454272, inside the
|
||||
// `if (MoveOrTeleport(...) != 0)` at @0x00454254), and only for a
|
||||
// classification route 4a owns — every other one already armed
|
||||
// the legacy pre-operation call above, exactly as before. Retail's
|
||||
// ConstraintManager leash is independent of the acdream-only TS-44
|
||||
// sticky suppression (which only concerns the enqueue/snap above),
|
||||
// so this deliberately sits OUTSIDE the snapSuppressedByStick
|
||||
// gate: a stuck NPC's leash still re-arms every accepted Position
|
||||
// exactly as it did before this route split the single call into a
|
||||
// per-branch pair.
|
||||
// classification the post-operation arm owns — every other one
|
||||
// already armed the legacy pre-operation call above, exactly as
|
||||
// before. Retail's ConstraintManager leash is independent of the
|
||||
// acdream-only TS-44 sticky suppression (which only concerns the
|
||||
// enqueue/snap/placement above), so this deliberately sits OUTSIDE
|
||||
// the snapSuppressedByStick gate: a stuck NPC's leash still
|
||||
// re-arms every accepted Position exactly as it did before this
|
||||
// route split the single call into a per-branch pair.
|
||||
RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation(
|
||||
earlyRemoteRoute,
|
||||
rmState);
|
||||
|
|
@ -2222,7 +2407,11 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
// per-tick ResolveWithTransition sweep then advances CheckCellId
|
||||
// as the sphere crosses cells and writes the new cell back into
|
||||
// rmState.CellId so the NEXT frame starts in the correct cell.
|
||||
rmState.CellId = p.LandblockId;
|
||||
//
|
||||
// C4 route 4b-2: NOT after a far snap — see
|
||||
// TryAdoptWireCellAfterRouting for the rule and why it applies to
|
||||
// this arm and not the player one.
|
||||
TryAdoptWireCellAfterRouting(rmState, npcRouting.Arm, p.LandblockId);
|
||||
|
||||
// Near UpdatePosition orientation is carried by the same complete
|
||||
// interpolation Frame as translation. Placement, airborne, and
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue