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
|
|
@ -588,6 +588,18 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
() => d.PlayerController.Controller,
|
||||
() => d.Character.UsePositionFromServer,
|
||||
() => liveSessionSource.CurrentSession);
|
||||
// C4 route 4b-2 (2026-08-04): the graphical remote-placement drive
|
||||
// controller — route 4b-1's dormant owner, now driven by the remote
|
||||
// far snap. Its service window is the graphical host's near-tier
|
||||
// residency (GraphicalRemotePlacementServiceWindow's own remarks
|
||||
// justify IsNearTier over IsNearTierOrPending). Shares the SAME entity
|
||||
// lifetime, clock, and prepared-collision source as the two drive
|
||||
// controllers above.
|
||||
var remotePlacementDrive = new RuntimeRemotePlacementDriveController(
|
||||
d.EntityObjects,
|
||||
d.Runtime.Clock,
|
||||
firstEntryCollision,
|
||||
new GraphicalRemotePlacementServiceWindow(live.WorldState));
|
||||
var hydration = new LiveEntityHydrationController(
|
||||
live.LiveEntities,
|
||||
d.EntityObjects,
|
||||
|
|
@ -654,6 +666,7 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
localPhysicsTimestamps.Publish,
|
||||
d.MovementDiagnostics,
|
||||
acceptedPositionDrive,
|
||||
remotePlacementDrive,
|
||||
worldDropProjection);
|
||||
var liveness = new LiveEntityLivenessController(
|
||||
live.LiveEntities,
|
||||
|
|
@ -977,7 +990,8 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
live.PlacementProjection,
|
||||
placementProjectionRetry,
|
||||
firstEntryDrive,
|
||||
acceptedPositionDrive),
|
||||
acceptedPositionDrive,
|
||||
remotePlacementDrive),
|
||||
liveSessionCommands,
|
||||
d.Log);
|
||||
LiveSessionHost sessionHost = sessionRuntimeFactory.Create(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
|
|||
private readonly RuntimePlacementProjectionRetrySlot _retries;
|
||||
private readonly RuntimeFirstEntryDriveController? _firstEntry;
|
||||
private readonly RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
|
||||
private readonly RuntimeRemotePlacementDriveController? _remotePlacementDrive;
|
||||
private readonly Action<RuntimeEntityRecord>? _localPlayerCompleted;
|
||||
private RuntimePlacementProjectionSubscription? _subscription;
|
||||
private IDisposable? _retryLease;
|
||||
|
|
@ -32,7 +33,8 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
|
|||
RuntimePlacementProjectionRetrySlot retries,
|
||||
RuntimeFirstEntryDriveController? firstEntry = null,
|
||||
Action<RuntimeEntityRecord>? localPlayerCompleted = null,
|
||||
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null)
|
||||
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null,
|
||||
RuntimeRemotePlacementDriveController? remotePlacementDrive = null)
|
||||
: this(
|
||||
events,
|
||||
() => new RuntimePlacementProjectionSubscription(
|
||||
|
|
@ -43,7 +45,8 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
|
|||
retries,
|
||||
firstEntry,
|
||||
localPlayerCompleted,
|
||||
acceptedPositionDrive)
|
||||
acceptedPositionDrive,
|
||||
remotePlacementDrive)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
ArgumentNullException.ThrowIfNull(placements);
|
||||
|
|
@ -56,7 +59,8 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
|
|||
RuntimePlacementProjectionRetrySlot retries,
|
||||
RuntimeFirstEntryDriveController? firstEntry = null,
|
||||
Action<RuntimeEntityRecord>? localPlayerCompleted = null,
|
||||
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null)
|
||||
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null,
|
||||
RuntimeRemotePlacementDriveController? remotePlacementDrive = null)
|
||||
{
|
||||
_events = events ?? throw new ArgumentNullException(nameof(events));
|
||||
_createSubscription = createSubscription
|
||||
|
|
@ -67,6 +71,7 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
|
|||
_firstEntry = firstEntry;
|
||||
_localPlayerCompleted = localPlayerCompleted;
|
||||
_acceptedPositionDrive = acceptedPositionDrive;
|
||||
_remotePlacementDrive = remotePlacementDrive;
|
||||
}
|
||||
|
||||
public void Attach()
|
||||
|
|
@ -84,6 +89,10 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
|
|||
// Position drive controller (RuntimeAcceptedPositionDriveController
|
||||
// .AttachRoute's doc comment).
|
||||
_acceptedPositionDrive?.AttachRoute(this);
|
||||
// C4 route 4b-2: route 4b-1's controller gained its first production
|
||||
// caller, so its one-route-at-a-time latch and its route-scoped
|
||||
// cancellation on detach are load-bearing rather than dormant.
|
||||
_remotePlacementDrive?.AttachRoute(this);
|
||||
_events.Attach();
|
||||
|
||||
RuntimePlacementProjectionSubscription? subscription = null;
|
||||
|
|
@ -106,6 +115,10 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
|
|||
{
|
||||
_firstEntry?.DriveAll();
|
||||
_acceptedPositionDrive?.Advance();
|
||||
// C4 route 4b-2: resolves a retained remote preparation
|
||||
// retry (Setup or world frame not resolved yet) on the
|
||||
// same cadence, before the FIFO head is republished.
|
||||
_remotePlacementDrive?.Advance();
|
||||
return boundSubscription.RetryPending();
|
||||
});
|
||||
_subscription = subscription;
|
||||
|
|
@ -136,6 +149,7 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
|
|||
// route's entries.
|
||||
_firstEntry?.DetachRoute(this);
|
||||
_acceptedPositionDrive?.DetachRoute(this);
|
||||
_remotePlacementDrive?.DetachRoute(this);
|
||||
if (!_eventsDisposed)
|
||||
{
|
||||
_events.Dispose();
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@ internal sealed record LiveSessionWorldRuntime(
|
|||
RuntimePlacementPresentationSink PlacementProjection,
|
||||
RuntimePlacementProjectionRetrySlot PlacementRetries,
|
||||
RuntimeFirstEntryDriveController FirstEntryDrive,
|
||||
RuntimeAcceptedPositionDriveController AcceptedPositionDrive);
|
||||
RuntimeAcceptedPositionDriveController AcceptedPositionDrive,
|
||||
RuntimeRemotePlacementDriveController RemotePlacementDrive);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the exact per-generation route/reset graph for the canonical live
|
||||
|
|
@ -266,7 +267,8 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
_world.PlacementRetries,
|
||||
_world.FirstEntryDrive,
|
||||
_ => session.SendGameAction(GameActionLoginComplete.Build()),
|
||||
_world.AcceptedPositionDrive);
|
||||
_world.AcceptedPositionDrive,
|
||||
_world.RemotePlacementDrive);
|
||||
}
|
||||
|
||||
private LiveInventorySessionBindings CreateInventoryBindings() => new(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -61,6 +61,29 @@ public static class PhysicsDiagnostics
|
|||
public static bool ProbeCellEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_CELL") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Issue #309's connected-gate confirmation signal (2026-08-04, C4 route
|
||||
/// 4b-2). The gate's quiescence steps need a ForcePosition to land INSIDE
|
||||
/// a transient collision-prefix quiescence window, which a tester cannot
|
||||
/// synchronise with a teleport or portal arrival — so without a signal a
|
||||
/// clean teleport and a correctly-parked one look identical and the gate
|
||||
/// passes while broken.
|
||||
///
|
||||
/// <para>When true, <c>RuntimeSetPositionState</c> emits one
|
||||
/// <c>[park]</c> line when a placement parks (entity, park cause, the
|
||||
/// blocking landblock prefix if any, the caller's pre-snap cell, the
|
||||
/// POST-snap cell the rollback would restore into, and whether the
|
||||
/// rollback was captured or declined), and one <c>[park-restore]</c> line
|
||||
/// when a cancelled park's withdrawal is rolled back or its residency arm
|
||||
/// is declined at restore time. Low volume: parks are rare, and nothing
|
||||
/// is emitted for an ordinary committing placement. Zero cost when off
|
||||
/// (one static-bool read per park).</para>
|
||||
///
|
||||
/// <para>Initial state from <c>ACDREAM_PROBE_PARK=1</c>.</para>
|
||||
/// </summary>
|
||||
public static bool ProbeParkEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_PARK") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// #283 reachability probe (2026-08-03). Runtime rebases its world frame
|
||||
/// the instant an accepted Position carries TeleportAdvanced, while App's
|
||||
|
|
@ -721,6 +744,7 @@ public static class PhysicsDiagnostics
|
|||
{
|
||||
ProbeResolveEnabled = false;
|
||||
ProbeCellEnabled = false;
|
||||
ProbeParkEnabled = false;
|
||||
ProbeBuildingEnabled = false;
|
||||
ProbeCellSetEnabled = false;
|
||||
ProbeStickyEnabled = false;
|
||||
|
|
|
|||
|
|
@ -600,9 +600,18 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
/// 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
|
||||
/// to derive retail's <c>player_distance</c> from. In every one of those
|
||||
/// cases the caller's pre-existing legacy path runs completely unchanged
|
||||
/// — a null here is "route 4a has no opinion", never "rejected".
|
||||
/// to derive retail's <c>player_distance</c> from. A null here is
|
||||
/// "Runtime has no opinion", never "rejected".
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// C4 route 4b-2 review fix — this used to add "In every one of those
|
||||
/// cases the caller's pre-existing legacy path runs completely
|
||||
/// unchanged". That path no longer exists: 4b-2 deleted the graphical
|
||||
/// caller's duplicated near/far blocks, and a null now takes the stated
|
||||
/// <c>UnroutedCatchUp</c> policy (AP-137;
|
||||
/// <c>RuntimeRemoteFarSnapPosition.ResolveArm</c>), which is AP-87's
|
||||
/// shared catch-up rather than a re-derived 96 m test.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal RuntimeAuthoritativePositionRoute? ClassifyRemoteAcceptedPosition(
|
||||
|
|
|
|||
208
src/AcDream.Runtime/Physics/RuntimeRemoteFarSnapPosition.cs
Normal file
208
src/AcDream.Runtime/Physics/RuntimeRemoteFarSnapPosition.cs
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.Runtime.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Which arm of retail <c>CPhysicsObj::MoveOrTeleport</c> (0x00516330) an
|
||||
/// already-classified REMOTE accepted Position takes. Total over every
|
||||
/// classification a remote caller can hold, including the two acdream-only
|
||||
/// ones retail cannot produce (see
|
||||
/// <see cref="RuntimeRemoteAcceptedPositionArm.UnroutedCatchUp"/>).
|
||||
/// </summary>
|
||||
internal enum RuntimeRemoteAcceptedPositionArm : byte
|
||||
{
|
||||
/// <summary>Retail's <c>arg4 == 0</c> branch: <c>return 0</c>
|
||||
/// @0x0051636D, nothing written. Route 4a.</summary>
|
||||
AirborneNoOperation,
|
||||
|
||||
/// <summary>Retail's <c>player_distance < 96f</c>
|
||||
/// <c>InterpolateTo</c> @0x005163AF. Route 4a.</summary>
|
||||
NearInterpolate,
|
||||
|
||||
/// <summary>Retail's <c>player_distance >= 96f</c>
|
||||
/// <c>StopInterpolating</c> @0x005163CB + <c>SetPositionSimple</c>
|
||||
/// @0x005163D9. Route 4b-2 — this slice.</summary>
|
||||
FarSnapPlacement,
|
||||
|
||||
/// <summary>
|
||||
/// <b>acdream-only.</b> No retail arm corresponds to it, because retail
|
||||
/// cannot reach the states that produce it: a classification of
|
||||
/// <c>RejectedAuthority</c>/<c>RejectedData</c> (retail validates no wire
|
||||
/// frame this way), <see langword="null"/> (retail always has a player, so
|
||||
/// <c>player_distance</c> always exists — acdream returns null for every
|
||||
/// remote packet during the login window before the local movement
|
||||
/// controller exists), or the cell-less <c>SetPosition</c> half that route
|
||||
/// 4b-3 will own. See <see cref="ResolveArm"/>'s remarks for the stated
|
||||
/// policy.
|
||||
/// </summary>
|
||||
UnroutedCatchUp,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C4 route 4b-2 (2026-08-04): the remaining <c>MoveOrTeleport</c> arm
|
||||
/// selection for a REMOTE accepted Position, beside route 4a's
|
||||
/// <see cref="RuntimeRemoteSteadyStatePosition"/>. Route 4a owns the two
|
||||
/// dispositions that perform no SetPosition; this owns the far snap
|
||||
/// (<c>player_distance >= 96 m</c> with wire contact, classified
|
||||
/// <see cref="RuntimeAuthoritativePositionDisposition.SetPositionSimple"/>)
|
||||
/// and states what happens to everything left over.
|
||||
/// </summary>
|
||||
internal static class RuntimeRemoteFarSnapPosition
|
||||
{
|
||||
/// <summary>
|
||||
/// True when route 4b-2 owns this classification: retail's far branch,
|
||||
/// @0x005163C1-@0x005163E8.
|
||||
///
|
||||
/// <para>
|
||||
/// The disposition alone is NOT sufficient — the classifier emits
|
||||
/// <c>SetPositionSimple</c> for the LOCAL PLAYER's FORCE_POSITION
|
||||
/// (<c>RuntimeAuthoritativePositionRouteClassifier.cs:332</c>) and
|
||||
/// teleport-advanced (<c>:355</c>) branches too, both carrying
|
||||
/// <c>RuntimeSetPositionOperationKind.LocalAuthoritative</c>. The
|
||||
/// <c>Teleport</c> flag bit is the same discriminator
|
||||
/// <see cref="AcDream.Runtime.Session.RuntimeRemotePlacementDriveController.OwnsPlacement"/>
|
||||
/// uses (a remote top-level Create carries <c>Placement|Slide</c>, an
|
||||
/// accepted remote Position carries retail's
|
||||
/// <c>Teleport|Slide|SendPositionEvent</c> = <c>0x1012</c>, exactly the
|
||||
/// flags <c>CPhysicsObj::SetPositionSimple</c> @0x005162B0 builds for
|
||||
/// <c>arg3 != 0</c>), so this predicate is a strict narrowing of
|
||||
/// <c>OwnsPlacement</c> to its far half.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static bool OwnsFarSnap(RuntimeAuthoritativePositionRoute? route) =>
|
||||
route is
|
||||
{
|
||||
Disposition:
|
||||
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
|
||||
OperationKind:
|
||||
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
||||
}
|
||||
&& (route.Value.SetPositionFlags & PhysicsSetPositionFlags.Teleport)
|
||||
!= 0;
|
||||
|
||||
/// <summary>
|
||||
/// The ONE predicate deciding whether retail's single
|
||||
/// <c>SmartBox::HandleReceivedPosition</c> <c>ConstrainTo</c> site
|
||||
/// (@0x00454272, inside <c>if (MoveOrTeleport(...) != 0)</c> @0x00454254)
|
||||
/// is armed AFTER the position operation for this classification, rather
|
||||
/// than by the legacy pre-operation App call.
|
||||
///
|
||||
/// <para>
|
||||
/// Route 4a moved its two dispositions onto the post-operation arm; this
|
||||
/// slice adds the far snap. Every remaining classification still arms
|
||||
/// through the legacy pre-operation call site, unchanged. Both the legacy
|
||||
/// gate and
|
||||
/// <see cref="RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation"/>
|
||||
/// read THIS predicate, so a classification can never be armed twice or
|
||||
/// not at all.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static bool OwnsAfterOperationConstraint(
|
||||
RuntimeAuthoritativePositionRoute? route) =>
|
||||
RuntimeRemoteSteadyStatePosition.OwnsSteadyState(route)
|
||||
|| OwnsFarSnap(route);
|
||||
|
||||
/// <summary>
|
||||
/// Selects the arm for one already-classified remote accepted Position.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>The <see cref="RuntimeRemoteAcceptedPositionArm.UnroutedCatchUp"/>
|
||||
/// policy, stated.</b> Before this slice, every classification route 4a
|
||||
/// did not own fell into a duplicated App-side near/far block that
|
||||
/// re-derived retail's <c>player_distance >= 96 m</c> test from
|
||||
/// <c>_playerController?.Position ?? Vector3.Zero</c>. That test is
|
||||
/// deleted here rather than preserved, because in every residual case it
|
||||
/// was already meaningless or wrong:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see langword="null"/> — the classifier returned no
|
||||
/// route precisely BECAUSE there is no local-player position
|
||||
/// (<c>RuntimeAcceptedPositionRouteRequests.TryBuild</c> refuses to
|
||||
/// fabricate one; <c>GameRuntime</c> states the rule). The legacy block
|
||||
/// then measured the distance from a fabricated <c>Vector3.Zero</c>.
|
||||
/// <b>Review fix — what that actually computed:</b> <c>worldPos</c> is
|
||||
/// streaming-origin-relative
|
||||
/// (<c>LiveEntityNetworkUpdateController.cs</c>: local position plus
|
||||
/// <c>(landblock − _origin.Center) * 192 m</c>), and the streaming origin
|
||||
/// recentres on the local player's landblock, so the fabricated distance
|
||||
/// was the remote's distance from the ORIGIN LANDBLOCK'S CORNER — a
|
||||
/// biased but genuinely correlated proxy for <c>player_distance</c>,
|
||||
/// its error bounded by the player's own offset inside that landblock
|
||||
/// (0-192 m per axis). The earlier claim that it had "no relationship to
|
||||
/// <c>player_distance</c>" was false. It is deleted anyway because a
|
||||
/// silently-biased proxy for retail's exact 96 m threshold is not a
|
||||
/// threshold: the bias is up to ~2.8x the threshold itself, so the arm it
|
||||
/// selects is not the arm retail would select, and there is no honest way
|
||||
/// to correct for it without the player position the classifier already
|
||||
/// declined to fabricate.</description></item>
|
||||
/// <item><description>Cell-less <c>SetPosition</c> — the classifier
|
||||
/// returns at <c>:399-417</c>, BEFORE it ever evaluates the distance.
|
||||
/// Retail's cell-less body takes the <c>this_1->cell == 0</c> branch
|
||||
/// @0x00516386 (<c>SetPosition</c>, flags <c>0x1012</c>), never the far
|
||||
/// branch, so applying a far test to it was a divergence in its own
|
||||
/// right. <b>Review fix — the residual delta, stated:</b> retail's
|
||||
/// cell-less arm is an UNCONDITIONAL placement sitting ahead of the
|
||||
/// contact test — <c>teleport_hook</c> @0x005163EF then <c>SetPosition</c>
|
||||
/// @0x00516420, <c>return 1</c> @0x00516438 — while acdream now routes
|
||||
/// that classification here, where <see cref="ApplyInterpolate"/> ENQUEUES
|
||||
/// whenever <c>!firstUp && willBeDrTicked &&
|
||||
/// bodyToTarget <= 4 m</c>, at any distance. <b>It is deliberately not
|
||||
/// changed to place here.</b> Retail's arm is not a pose write: it is
|
||||
/// <c>teleport_hook</c> @0x00514ED0 — the COMPLETE call list, read from
|
||||
/// the decomp (delta review N6 added the last of these, which the earlier
|
||||
/// enumeration dropped): <c>MovementManager::CancelMoveTo</c> @0x00514EDF,
|
||||
/// <c>PositionManager::UnStick</c> @0x00514EEE,
|
||||
/// <c>PositionManager::StopInterpolating</c> @0x00514EFD,
|
||||
/// <c>PositionManager::UnConstrain</c> @0x00514F0C,
|
||||
/// <c>TargetManager::ClearTarget</c> @0x00514F1B +
|
||||
/// <c>NotifyVoyeurOfEvent(Teleported_TargetStatus)</c> @0x00514F28, and
|
||||
/// <c>CPhysicsObj::report_collision_end(this, 1)</c> @0x00514F31 — followed
|
||||
/// by the canonical flags-<c>0x1012</c>
|
||||
/// <c>SetPosition</c>. Writing only the pose here would leave a live
|
||||
/// moveto, a live stick, a live constraint leash pointing at the old
|
||||
/// cell, and every collision partner still believing it is in contact,
|
||||
/// which is strictly worse than the recorded queue. Porting the
|
||||
/// whole arm is route 4b-3's entire scope; claiming half of it here is
|
||||
/// route 4a's "'not Interpolate' is not 'far'" mistake one level up.
|
||||
/// Recorded as AP-137.</description></item>
|
||||
/// <item><description><c>RejectedAuthority</c>/<c>RejectedData</c> — the
|
||||
/// packet's authority or payload was refused; retail, which validates
|
||||
/// neither, has no state here at all. <b>Review fix — stated plainly:</b>
|
||||
/// this arm therefore APPLIES a payload the classifier just marked
|
||||
/// invalid. <c>ClassifyAcceptedPosition</c> emits <c>RejectedData</c> from
|
||||
/// two sites a remote can reach — an accepted wire position that fails
|
||||
/// <c>ValidPosition</c> (non-finite origin/orientation or a frame
|
||||
/// <c>PositionFrameValidation</c> refuses), and a non-finite or negative
|
||||
/// derived <c>player_distance</c> — and both then take this arm. That is
|
||||
/// not a regression — the deleted legacy block fed the same payload to the
|
||||
/// same snap/enqueue pair — but this slice's stated purpose was an
|
||||
/// EXPLICIT handler, so it is named rather than left implied. Retiring it
|
||||
/// means giving the two rejections their own no-operation arm, which is a
|
||||
/// behaviour change route 4b-3 must decide with the cell-less half, not a
|
||||
/// tidy-up. Recorded as AP-137.</description></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// What remains is exactly AP-87 — the shared
|
||||
/// <see cref="RuntimeRemoteSteadyStatePosition.ApplyInterpolate"/> catch-up
|
||||
/// the near branch already uses, whose unconditional snap on
|
||||
/// <c>firstUp || !willBeDrTicked || bodyToTarget > 4 m</c> still PLACES
|
||||
/// an unplaced or badly-lagging body. So a remote keeps tracking the
|
||||
/// server through the login window and through a rejected packet; it is
|
||||
/// never silently frozen, which is what deleting the legacy block without
|
||||
/// a replacement would have done. The single 4 m constant now lives only
|
||||
/// in <c>RuntimeRemoteSteadyStatePosition</c>; the App's two duplicated
|
||||
/// copies are gone.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static RuntimeRemoteAcceptedPositionArm ResolveArm(
|
||||
RuntimeAuthoritativePositionRoute? route)
|
||||
{
|
||||
if (RuntimeRemoteSteadyStatePosition.IsAirborneNoOperation(route))
|
||||
return RuntimeRemoteAcceptedPositionArm.AirborneNoOperation;
|
||||
if (RuntimeRemoteSteadyStatePosition.IsNearInterpolate(route))
|
||||
return RuntimeRemoteAcceptedPositionArm.NearInterpolate;
|
||||
return OwnsFarSnap(route)
|
||||
? RuntimeRemoteAcceptedPositionArm.FarSnapPlacement
|
||||
: RuntimeRemoteAcceptedPositionArm.UnroutedCatchUp;
|
||||
}
|
||||
}
|
||||
|
|
@ -16,16 +16,15 @@ namespace AcDream.Runtime.Physics;
|
|||
/// incarnations — retail's disassembly makes no <c>this==player</c>
|
||||
/// distinction on this path (see <c>ConstraintDistance</c>) — so one Runtime
|
||||
/// owner decides and applies both, replacing the two independent per-kind
|
||||
/// copies that used to live in <c>LiveEntityNetworkUpdateController</c>. The
|
||||
/// far (>=96 m) and teleport/cell-less branches remain the legacy App path
|
||||
/// until C4 route 4b.
|
||||
/// copies that used to live in <c>LiveEntityNetworkUpdateController</c>.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Exactly two dispositions are owned here.</b> Everything else —
|
||||
/// <c>SetPositionSimple</c>, <c>SetPosition</c>, <c>RejectedAuthority</c>,
|
||||
/// <c>RejectedData</c>, and "not classified at all" (<see langword="null"/>) —
|
||||
/// falls through to the untouched legacy App routing. "Not Interpolate" must
|
||||
/// never be read as "far".
|
||||
/// belongs to <see cref="RuntimeRemoteFarSnapPosition"/>, which owns the far
|
||||
/// (>=96 m) snap from C4 route 4b-2 and states the policy for the
|
||||
/// leftovers. "Not Interpolate" must never be read as "far".
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static class RuntimeRemoteSteadyStatePosition
|
||||
|
|
@ -157,9 +156,23 @@ internal static class RuntimeRemoteSteadyStatePosition
|
|||
/// does NOT run on the airborne no-op.
|
||||
///
|
||||
/// <para>
|
||||
/// Route-gated so it can only fire for a classification route 4a owns:
|
||||
/// every other classification still arms the leash through the legacy
|
||||
/// pre-operation call site, unchanged, until 4b moves them too.
|
||||
/// Route-gated by
|
||||
/// <see cref="RuntimeRemoteFarSnapPosition.OwnsAfterOperationConstraint"/>
|
||||
/// — route 4a's two dispositions plus (C4 route 4b-2) the far snap. Every
|
||||
/// other classification still arms the leash through the legacy
|
||||
/// pre-operation call site, unchanged, until 4b-3 moves them too. The
|
||||
/// legacy site reads the SAME predicate, so no classification can be
|
||||
/// armed twice or left unarmed.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// This deliberately does not consult the outcome of the operation it
|
||||
/// follows. Retail's far branch returns 1 @0x005163E8 unconditionally —
|
||||
/// <c>MoveOrTeleport</c> discards <c>SetPositionSimple</c>'s
|
||||
/// <c>enum SetPositionError</c> return entirely — so
|
||||
/// <c>HandleReceivedPosition</c> arms the leash even when the placement
|
||||
/// FAILED. "Arm only on commit" is the natural misreading and is the same
|
||||
/// shape as the already-recorded unarmed-leash bug.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static bool TryArmConstraintAfterOperation(
|
||||
|
|
@ -168,7 +181,8 @@ internal static class RuntimeRemoteSteadyStatePosition
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(remote);
|
||||
if (route is not { } selected
|
||||
|| !OwnsSteadyState(selected)
|
||||
|| !RuntimeRemoteFarSnapPosition.OwnsAfterOperationConstraint(
|
||||
selected)
|
||||
|| !selected.ConstrainAfterRouting
|
||||
|| remote.Host is not { } host)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using AcDream.Content;
|
||||
|
|
@ -1346,6 +1346,14 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
// B2 dependency (C4 route 4b-2): the `_operations.ContainsKey`
|
||||
// refusal is what makes an INHERITED park impossible for any
|
||||
// operation begun here, which is in turn what lets
|
||||
// RuntimeRemotePlacementDriveController document a pre-engine
|
||||
// Rejected as unreachable on its far-snap path (the
|
||||
// WakeableLostCell-only `with` rewrite in
|
||||
// SubmitPreparedPlacementCore cannot apply). Loosening it to admit a
|
||||
// second concurrent operation invalidates that argument.
|
||||
if (record.Key is not { } key
|
||||
|| _operations.ContainsKey(key)
|
||||
|| HasRetainedCompletion(key))
|
||||
|
|
@ -1377,6 +1385,12 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
|| operation.Stage is not RuntimeEntityPlacementStage
|
||||
.AwaitingPreparation
|
||||
|| !ReferenceEquals(operation.Record, record)
|
||||
// B2 dependency (C4 route 4b-2): this `is not null` is what makes
|
||||
// DormantLocalActivation and "the record has a body" mutually
|
||||
// exclusive, which is the step
|
||||
// RuntimeRemotePlacementDriveController's pre-engine-Rejected
|
||||
// argument uses to dismiss the AwaitingCell stage divergence
|
||||
// between PrepareMover and SubmitPreparedPlacementCore.
|
||||
|| record.PhysicsBody is not null
|
||||
|| !IsCurrent(operation)
|
||||
|| body.InWorld
|
||||
|
|
@ -1560,6 +1574,15 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
command = default;
|
||||
// Round 3 audit: safe - fresh lookup + Token check earlier in this
|
||||
// SAME guard clause precede IsCurrent, nothing reentrant in between.
|
||||
//
|
||||
// B2 dependency (C4 route 4b-2): this guard clause SHADOWS
|
||||
// SubmitPreparedPlacementCore's entry validation one call later, and
|
||||
// RuntimeRemotePlacementDriveController's far-snap default arm
|
||||
// documents a pre-engine `Rejected` as unreachable BECAUSE of that
|
||||
// shadowing - every term dropped from here re-surfaces there as a
|
||||
// non-storing Rejected instead of a storing RejectedAuthority. The
|
||||
// one term it deliberately does NOT shadow is the null-body test;
|
||||
// that caller's own entry check covers it, and says so.
|
||||
if (!token.IsValid
|
||||
|| !_operations.TryGetValue(token.Entity, out Operation? operation)
|
||||
|| operation.Token != token
|
||||
|
|
@ -2977,7 +3000,12 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
collisionGenerationOverride:
|
||||
quiescence!.Token.CollisionGeneration,
|
||||
collisionPrefixOverride:
|
||||
quiescence.Token.LandblockPrefix);
|
||||
quiescence.Token.LandblockPrefix,
|
||||
// A quiescence park is a momentarily-unavailable destination,
|
||||
// not a world going away, so it is eligible for the rollback.
|
||||
// ParkDeferred is what decides whether the cell it would
|
||||
// restore into is itself quiescing.
|
||||
restorableOnCancel: true);
|
||||
}
|
||||
|
||||
PhysicsSetPositionResult result;
|
||||
|
|
@ -3028,7 +3056,11 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
collisionGenerationOverride:
|
||||
queriedQuiescence!.Token.CollisionGeneration,
|
||||
collisionPrefixOverride:
|
||||
queriedQuiescence.Token.LandblockPrefix);
|
||||
queriedQuiescence.Token.LandblockPrefix,
|
||||
// Same reason as the pre-sweep park above, plus the swept
|
||||
// footprint: ResultTouchesPrefix scans every QueriedCellIds
|
||||
// entry, and that set spans NEIGHBOUR landblocks.
|
||||
restorableOnCancel: true);
|
||||
}
|
||||
operation.Result = result;
|
||||
if (!result.IsSuccessful)
|
||||
|
|
@ -3048,9 +3080,9 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
_preparedMovers[operation.Key] = canonicalRequest;
|
||||
|
||||
if (result.IsDeferred)
|
||||
// The one restorable park: the destination is not placeable right
|
||||
// now. Every other ParkDeferred caller is a quiescence/retirement
|
||||
// withdrawal - see ParkDeferred's restorableOnCancel doc.
|
||||
// The plain restorable park: the destination is not placeable
|
||||
// right now. The only NON-restorable caller is the retirement
|
||||
// park - see ParkDeferred's restorableOnCancel doc.
|
||||
return ParkDeferred(operation, result, restorableOnCancel: true);
|
||||
|
||||
if (!CommitCanonical(operation, result))
|
||||
|
|
@ -3418,6 +3450,13 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
/// cancelled park cannot leave the entity invisible and intangible with
|
||||
/// nothing able to wake it.
|
||||
///
|
||||
/// <para>Residency specifically is re-tested against the live quiescence
|
||||
/// map HERE as well as at park time, because this runs on a later packet
|
||||
/// for a retained park - see the inline comment for the window. The
|
||||
/// <c>InWorld</c>/transient/clock half is restored unconditionally: it is
|
||||
/// per-entity simulation state, not a claim on any landblock's collision
|
||||
/// generation, so it cannot block a retirement.</para>
|
||||
///
|
||||
/// <para><b>The committed pose is deliberately left alone.</b>
|
||||
/// <see cref="ParkDeferred"/> snaps the body to the destination before
|
||||
/// withdrawing, which is precisely retail's lost-cell behaviour:
|
||||
|
|
@ -3476,13 +3515,39 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
}
|
||||
if (withdrawal.ClockActive)
|
||||
_entities.ResumeObjectClock(record);
|
||||
if (residentCellId != 0u && record.FullCellId == 0u)
|
||||
// Round 4 (D6): the park-time decision in ParkDeferred is a SNAPSHOT,
|
||||
// and for a RETAINED park it can be stale by the time this runs. The
|
||||
// far snap's park is cancelled synchronously (SubmitAndResolve's
|
||||
// DeferredCell arm), so its window is nanoseconds - but route 2's
|
||||
// park is retained (AwaitingCommitWake) and this restore lands on the
|
||||
// NEXT packet's merge-time Forget, ~150 ms later at ACE's 5-10 Hz.
|
||||
// Streaming opens a quiescence per landblock mutation, so a prefix
|
||||
// that was clean when the park was taken can be quiescing now.
|
||||
// Re-testing here is what makes AP-136's claim - "the rollback never
|
||||
// re-admits a spatial root into a retiring prefix" - exact at the
|
||||
// moment residency is actually written, instead of true only as of
|
||||
// when the park was taken. The `!= 0u` test ahead of it is also the
|
||||
// C3c-F3 sentinel guard ParkDeferred applies: AdjustToOutside zeroes
|
||||
// the cell id on map-edge failure, and prefix 0x00000000 is the
|
||||
// legitimate corner landblock.
|
||||
bool residencyRestored = false;
|
||||
if (residentCellId != 0u
|
||||
&& !IsCollisionPrefixQuiescing(residentCellId)
|
||||
&& record.FullCellId == 0u)
|
||||
{
|
||||
_entities.SetFullCell(
|
||||
record,
|
||||
residentCellId,
|
||||
(residentCellId & 0xFFFF0000u) | 0xFFFFu);
|
||||
_physics.AcknowledgeSpatialProjection(record, spatial: true);
|
||||
residencyRestored = true;
|
||||
}
|
||||
// Issue #309's connected-gate confirmation signal; see the [park]
|
||||
// line's own comment in ParkDeferred.
|
||||
if (PhysicsDiagnostics.ProbeParkEnabled)
|
||||
{
|
||||
Console.WriteLine(FormattableString.Invariant(
|
||||
$"[park-restore] guid=0x{record.ServerGuid:X8} restoreCell=0x{residentCellId:X8} inWorld={withdrawal.InWorld} residency={residencyRestored}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3687,7 +3752,13 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
collisionGenerationOverride:
|
||||
quiescence?.Token.CollisionGeneration,
|
||||
collisionPrefixOverride:
|
||||
quiescence?.Token.LandblockPrefix);
|
||||
quiescence?.Token.LandblockPrefix,
|
||||
// Stated, not inherited from the parameter default: a
|
||||
// RETIREMENT park withdraws the entity because its own world
|
||||
// is going away, which is the one shape AP-136's reason
|
||||
// covers exactly. Round 3 (m5) - the blanket doc that used to
|
||||
// guard a forgetful caller here is gone.
|
||||
restorableOnCancel: false);
|
||||
if (_pendingProjection.TryGetValue(
|
||||
parked.Projection.Sequence,
|
||||
out RuntimePlacementProjectionSnapshot staged))
|
||||
|
|
@ -4252,20 +4323,97 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
/// 0x00000000: a corner-landblock quiescence override read as "absent",
|
||||
/// so <see cref="Operation.CollisionQuiescenceHeld"/> derived false and
|
||||
/// the parked operation skipped the QuiescenceHeld stage entirely.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// <paramref name="restorableOnCancel"/> opts this park into
|
||||
/// <see cref="Forget"/>'s rollback. It is true for exactly one caller:
|
||||
/// the plain "destination is not placeable right now" park, which is the
|
||||
/// one that strands an entity when a later packet cancels it.
|
||||
///
|
||||
/// <para>It is deliberately FALSE for every quiescence/retirement park.
|
||||
/// Those withdraw the entity precisely because its own landblock prefix
|
||||
/// is being retired or held, so restoring canonical residency there would
|
||||
/// re-admit a spatial root into the prefix that is trying to quiesce and
|
||||
/// block the retirement outright - a streaming stall traded for a
|
||||
/// stranded entity. Those entities are withdrawn because their world is
|
||||
/// going away, not because a destination was momentarily unavailable.</para>
|
||||
/// <para>
|
||||
/// <paramref name="restorableOnCancel"/> is the caller's HALF of the
|
||||
/// opt-in into <see cref="Forget"/>'s rollback: "this park is a
|
||||
/// momentarily-unavailable destination, not a world going away". It is
|
||||
/// true for the plain "destination is not placeable right now" park and
|
||||
/// for <see cref="SubmitPreparedPlacementCore"/>'s two collision-prefix
|
||||
/// QUIESCENCE parks; it is false for a retirement park
|
||||
/// (<see cref="ParkCollisionResidents"/>), which withdraws the entity
|
||||
/// precisely because its own world is being retired.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The OTHER half is decided here, after <see cref="PhysicsBody.SnapToCell"/>
|
||||
/// has committed the parked pose, and it is the only test AP-136's stated
|
||||
/// reason actually needs: the rollback restores canonical residency at
|
||||
/// <c>body.CellPosition.ObjCellId</c>
|
||||
/// (<see cref="RestoreParkWithdrawal"/>), so it is safe exactly when THAT
|
||||
/// cell's prefix is not itself quiescing
|
||||
/// (<see cref="IsCollisionPrefixQuiescing"/> — the pre-flight's own
|
||||
/// predicate). Deciding it here rather than at the call sites is what
|
||||
/// makes the claim true rather than approximately true, for two
|
||||
/// independent reasons (C4 route 4b-2, round-3 correction A1):
|
||||
/// </para>
|
||||
/// <list type="number">
|
||||
/// <item><description>A blocking-quiescence token is the MINIMUM-
|
||||
/// <c>OperationId</c> match, not the only live one —
|
||||
/// <c>_collisionPrefixQuiescence</c> is a per-prefix dictionary and a
|
||||
/// streaming recentre opens one per landblock mutation. Comparing the
|
||||
/// restore cell against that single token proves nothing about the
|
||||
/// others, so it could answer "safe" for a destination that is itself
|
||||
/// retiring. (Round 4 bounded the reach rather than asserting it. On a
|
||||
/// FIRST submit it is unreachable, and provably so: the pre-engine check
|
||||
/// can match at most the request's own prefix and its
|
||||
/// <c>CurrentCellId</c>, which are the same prefix there, so its token IS
|
||||
/// the restore cell's; and the post-sweep check runs only once the
|
||||
/// pre-engine check has proven the destination prefix is NOT quiescing,
|
||||
/// so its token can never be. What is NOT ruled out is a RETAINED
|
||||
/// operation whose <c>CurrentCellId</c> has since been moved to another
|
||||
/// landblock by a non-Position rebucket — see the arm's own scoping
|
||||
/// below — with concurrent quiescences on both prefixes. That shape is
|
||||
/// unmeasured, and it is exactly the shape the far arm's pre-flight
|
||||
/// refuses on the only route that has one. Testing the live map here
|
||||
/// makes the decision free of the question rather than dependent on
|
||||
/// it.)</description></item>
|
||||
/// <item><description>The restore cell is not the caller's
|
||||
/// <c>result.CellId</c>. <see cref="PhysicsBody.StageDormantCellFrame"/>
|
||||
/// runs <see cref="AcDream.Core.Physics.LandDefs.AdjustToOutside"/> for
|
||||
/// any outdoor cell, whose own contract says the re-derived id may belong
|
||||
/// to a NEIGHBOUR landblock. Only the post-snap value is the value the
|
||||
/// rollback will use. This is the reachable half, and it is pinned by
|
||||
/// <c>QuiescingOwnPrefix_SeamCrossingParkIsRestoredAtTheReDerivedNeighbourCell</c>:
|
||||
/// a wire (cell, position) pair whose position lies past its own named
|
||||
/// block's seam is exactly the pair #107's re-derivation exists to
|
||||
/// distrust, and the entity is stranded without this.</description></item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>
|
||||
/// AP-136's blanket "no quiescence park is restorable" was over-broad
|
||||
/// because <see cref="TryGetBlockingQuiescence"/> also fires on prefixes a
|
||||
/// placement merely TOUCHES — chiefly any <c>QueriedCellIds</c> entry
|
||||
/// (a NEIGHBOUR the sweep reached across a seam;
|
||||
/// <c>CellTransit.AddOutsideCell</c> has no same-block filter), and the
|
||||
/// request's <c>CurrentCellId</c>.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>What <c>CurrentCellId</c> names, scoped to the submit (round 4,
|
||||
/// D5).</b> On the FIRST submit of an accepted Position it names the
|
||||
/// DESTINATION, not a departed source: both accepted-Position callers
|
||||
/// commit the accepted wire cell to <c>record.FullCellId</c> ahead of the
|
||||
/// drive (route 2 through the merge, the graphical remote path through
|
||||
/// <c>LiveEntityRuntime.RebucketLiveEntity</c>), and this request field is
|
||||
/// read from that same record field. It is NOT invariant for a RETAINED
|
||||
/// operation: both drives re-submit from their own cadence pump with no
|
||||
/// fresh merge in between (<c>SubmitAndResolve</c> re-reads the record as
|
||||
/// it then stands), and <c>RemoteTeleportController</c>'s rollback is a
|
||||
/// shipped writer that rebuckets <c>FullCellId</c> back to the
|
||||
/// PRE-teleport landblock. <c>RuntimeRemotePlacementDriveController</c>'s
|
||||
/// <c>CanAttemptDestination</c> doc states this correctly; treat the arm
|
||||
/// as live, not as dead code.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// In those touched-prefix
|
||||
/// shapes the retiring prefix is not the one residency is restored into,
|
||||
/// so the rollback pins nothing, while leaving the park non-restorable
|
||||
/// stranded the entity (<c>InWorld = false</c>, clock suspended,
|
||||
/// <c>FullCellId = 0</c>) with the only operation able to wake it
|
||||
/// destroyed by its own next accepted Position.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private RuntimeSetPositionOutcome ParkDeferred(
|
||||
Operation operation,
|
||||
|
|
@ -4276,21 +4424,55 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
bool restorableOnCancel = false)
|
||||
{
|
||||
PhysicsBody body = operation.Body!;
|
||||
// Captured BEFORE the withdrawal below clears it. Only the
|
||||
// WITHDRAWAL is captured, never the pose: the SnapToCell on the next
|
||||
// line deliberately commits the destination pose and that commit
|
||||
// stands, exactly as retail's store_position @0x00515CE2 commits it
|
||||
// on the lost-cell branch and never un-commits it.
|
||||
operation.ParkWithdrawal = new ParkWithdrawal(
|
||||
Captured: restorableOnCancel,
|
||||
InWorld: body.InWorld,
|
||||
TransientState: body.TransientState,
|
||||
ClockActive: operation.Record.ObjectClock.IsActive);
|
||||
// Read BEFORE the withdrawal below clears them - and before the
|
||||
// SnapToCell, which sets InWorld = true itself. Only the WITHDRAWAL
|
||||
// is captured, never the pose: the SnapToCell deliberately commits
|
||||
// the destination pose and that commit stands, exactly as retail's
|
||||
// store_position @0x00515CE2 commits it on the lost-cell branch and
|
||||
// never un-commits it.
|
||||
bool priorInWorld = body.InWorld;
|
||||
TransientStateFlags priorTransientState = body.TransientState;
|
||||
bool priorClockActive = operation.Record.ObjectClock.IsActive;
|
||||
body.Orientation = result.Orientation;
|
||||
body.SnapToCell(
|
||||
result.CellId,
|
||||
result.Position,
|
||||
result.CellLocalPosition);
|
||||
// The restorable decision reads the POST-snap cell, because that is
|
||||
// the cell RestoreParkWithdrawal will restore residency into - see
|
||||
// this method's own doc for why the caller's result.CellId and a
|
||||
// single blocking-quiescence token are both the wrong inputs.
|
||||
//
|
||||
// C3c-F3 sentinel guard (round 4, N1): cell id 0 is
|
||||
// AdjustToOutside's map-edge FAILURE sentinel
|
||||
// (LandDefs.AdjustToOutside's `cellId = 0u; return false` tail), and
|
||||
// prefix 0x00000000 is simultaneously the LEGITIMATE corner landblock
|
||||
// (0,0) - which is exactly why BeginCollisionPrefixQuiescence rejects
|
||||
// landblock id 0 rather than treating it as "absent". So the
|
||||
// quiescence map must never be consulted with the sentinel: a corner
|
||||
// landblock quiescing would otherwise make every map-edge failure
|
||||
// read as "restoring into a retiring prefix". A sentinel cell has no
|
||||
// residency to re-admit anywhere - RestoreParkWithdrawal applies the
|
||||
// same `!= 0u` test before it restores residency at all - so the
|
||||
// decision falls back to the caller's half alone.
|
||||
uint parkedCellId = body.CellPosition.ObjCellId;
|
||||
operation.ParkWithdrawal = new ParkWithdrawal(
|
||||
Captured: restorableOnCancel
|
||||
&& (parkedCellId == 0u
|
||||
|| !IsCollisionPrefixQuiescing(parkedCellId)),
|
||||
InWorld: priorInWorld,
|
||||
TransientState: priorTransientState,
|
||||
ClockActive: priorClockActive);
|
||||
// Issue #309's connected-gate confirmation signal. Off by default;
|
||||
// one line per park, nothing at all for a committing placement.
|
||||
if (PhysicsDiagnostics.ProbeParkEnabled)
|
||||
{
|
||||
string parkCause = collisionPrefixOverride is uint blockedPrefix
|
||||
? FormattableString.Invariant($"quiescence:0x{blockedPrefix:X8}")
|
||||
: "unplaceable";
|
||||
Console.WriteLine(FormattableString.Invariant(
|
||||
$"[park] guid=0x{operation.Record.ServerGuid:X8} cause={parkCause} resultCell=0x{result.CellId:X8} restoreCell=0x{parkedCellId:X8} eligible={restorableOnCancel} captured={operation.ParkWithdrawal.Captured}"));
|
||||
}
|
||||
body.InWorld = false;
|
||||
body.TransientState &= ~TransientStateFlags.Active;
|
||||
if (operation.Record.RemoteMotion is IRuntimeRemotePlacement remote)
|
||||
|
|
@ -5231,6 +5413,17 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
orientation.Y,
|
||||
orientation.Z);
|
||||
|
||||
/// <summary>
|
||||
/// B2 dependency (C4 route 4b-2): all SEVEN version dimensions below are
|
||||
/// load-bearing for <c>RuntimeRemotePlacementDriveController</c>'s
|
||||
/// documented invariant that a PRE-engine <c>Rejected</c> can never reach
|
||||
/// its far-snap default arm — it relies on <see cref="PrepareMover"/>
|
||||
/// catching every authority divergence one call earlier, as
|
||||
/// <c>RejectedAuthority</c> (which STORES), rather than
|
||||
/// <see cref="SubmitPreparedPlacementCore"/> catching it as
|
||||
/// <c>Rejected</c> (which does not). Dropping a dimension here silently
|
||||
/// moves that shape to the non-storing arm.
|
||||
/// </summary>
|
||||
private static bool IsPreparationAuthorityCurrent(
|
||||
Operation operation,
|
||||
in MoverPreparationAuthority authority) =>
|
||||
|
|
|
|||
|
|
@ -64,10 +64,20 @@ internal enum RuntimeAcceptedPositionExecutionStatus : byte
|
|||
Contention,
|
||||
|
||||
/// <summary>
|
||||
/// The destination landblock's collision generation was not ready; the
|
||||
/// operation parked. The ack fires later, once
|
||||
/// The operation parked instead of committing. The ack fires later, once
|
||||
/// <see cref="RuntimeAcceptedPositionDriveController.Advance"/> observes
|
||||
/// the deferred commit.
|
||||
///
|
||||
/// Round 4 (2026-08-04, N3): the previous text named only one cause —
|
||||
/// "the destination landblock's collision generation was not ready" —
|
||||
/// which is <c>SubmitPreparedPlacementCore</c>'s plain
|
||||
/// <c>result.IsDeferred</c> park. Two other causes reach this same
|
||||
/// status and are asserted by this route's own tests: the PRE-engine
|
||||
/// collision-prefix quiescence park (the destination prefix is
|
||||
/// mid-retirement) and the POST-sweep one (a landblock the sweep merely
|
||||
/// TOUCHED is mid-retirement, which can rewrite an otherwise
|
||||
/// about-to-commit placement). All three are "parked, not committed";
|
||||
/// none of them means the placement failed.
|
||||
/// </summary>
|
||||
DeferredCell,
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
|
|
@ -19,16 +20,41 @@ namespace AcDream.Runtime.Session;
|
|||
/// <see cref="RuntimeRemotePlacementDriveController"/> can
|
||||
/// ask, BEFORE attempting any canonical SetPosition, whether the accepted
|
||||
/// destination is one this host can actually place a remote into right now.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Headless is not a live consumer, stated plainly (C4 route 4b-2, review
|
||||
/// fix).</b> The headless implementation above exists and is exact, but
|
||||
/// NOTHING in <c>AcDream.Headless</c> constructs a
|
||||
/// <see cref="RuntimeRemotePlacementDriveController"/>: the graphical
|
||||
/// composition root (<c>SessionPlayerComposition</c>) is the only
|
||||
/// construction site, and
|
||||
/// <c>RuntimeLiveEntitySessionController.OnPositionUpdated</c> returns early
|
||||
/// for every non-local GUID before any remote routing could occur. Route
|
||||
/// 4b-2's contract item 6 ("both hosts drive the identical Runtime entry
|
||||
/// point, or the divergence is stated plainly rather than satisfied
|
||||
/// vacuously") is therefore satisfied by THIS statement, not by a shared
|
||||
/// caller. The far snap is a graphical-host-only path until a headless
|
||||
/// remote-motion consumer exists.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public interface IRuntimeRemotePlacementServiceWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// True when <paramref name="landblockId"/>'s collision is currently
|
||||
/// published by this host, so a canonical SetPosition into it can be
|
||||
/// attempted without risking an un-wakeable <c>DeferredCell</c> park (see
|
||||
/// published by this host, so a canonical SetPosition into it is worth
|
||||
/// attempting rather than certain to park (see
|
||||
/// docs/research/2026-08-04-c4-route-4b-1-contract.md's "central
|
||||
/// decision"). <paramref name="landblockId"/> may be a full cell id — the
|
||||
/// implementation canonicalizes to the containing landblock.
|
||||
///
|
||||
/// <para>
|
||||
/// C4 route 4b-2 delta review: this answer is an OPTIMISATION only. It
|
||||
/// cannot be a correctness gate — see
|
||||
/// <see cref="RuntimeRemotePlacementDriveController"/>'s
|
||||
/// <c>CanAttemptDestination</c> for the two Core predicates that are
|
||||
/// strictly broader than any pre-flight, one of which reads a sweep
|
||||
/// footprint that does not exist until the sweep has run.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
bool IsWithinServiceWindow(uint landblockId);
|
||||
}
|
||||
|
|
@ -36,36 +62,208 @@ public interface IRuntimeRemotePlacementServiceWindow
|
|||
/// <summary>
|
||||
/// Typed yields for
|
||||
/// <see cref="RuntimeRemotePlacementDriveController.TryExecuteAcceptedRemotePosition"/>.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>The partition this enum exists to express (C4 route 4b-2, delta review
|
||||
/// MAJOR A).</b> Retail's <c>CPhysicsObj::SetPositionInternal</c> @0x00515BD0
|
||||
/// has exactly two shapes, and the earlier three-value enum could not tell
|
||||
/// them apart:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description><b>Stores.</b> <c>AdjustPosition</c> yielded no
|
||||
/// transition @0x00515C1D, so the resolve never ran:
|
||||
/// <c>prepare_to_leave_visibility</c> @0x00515CDA,
|
||||
/// <c>store_position(this, arg2)</c> @0x00515CE2,
|
||||
/// <c>CObjectMaint::GotoLostCell</c> @0x00515CF2, transient-state clear
|
||||
/// @0x00515CF7, <c>return 0</c> @0x00515D07. The object MOVES to the accepted
|
||||
/// destination.</description></item>
|
||||
/// <item><description><b>Does not store.</b> The resolve DID run and refused:
|
||||
/// <c>CheckPositionInternal == 0</c> @0x00515C85 →
|
||||
/// <c>handle_all_collisions</c> @0x00515CC2 →
|
||||
/// <c>return ((eax_14 - eax_14) & 2) + 2</c> @0x00515CD5 (2 or 4), and
|
||||
/// <c>sphere_path.curr_cell == 0</c> @0x00515C8F → <c>return 3</c>
|
||||
/// @0x00515CB2. Both leave the object where it was.</description></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// <see cref="RuntimeRemotePlacementExecutionStatusExtensions.StoresAcceptedDestination"/>
|
||||
/// is the single predicate that reads this partition; every value below
|
||||
/// states which side it falls on and WHY, so a new value cannot be added
|
||||
/// without deciding.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal enum RuntimeRemotePlacementExecutionStatus : byte
|
||||
{
|
||||
/// <summary>Out of this route's scope: not a disposition this
|
||||
/// controller owns (<see cref="RuntimeRemotePlacementDriveController.OwnsPlacement"/>),
|
||||
/// no canonical body, or no incarnation key.</summary>
|
||||
/// <summary>
|
||||
/// Out of this route's scope: not a disposition this controller owns
|
||||
/// (<see cref="RuntimeRemotePlacementDriveController.OwnsPlacement"/>),
|
||||
/// no canonical body, or no incarnation key. <b>Stores</b> — nothing ran.
|
||||
///
|
||||
/// <para>
|
||||
/// The three sub-cases are not symmetric, and round 3 (correction m4)
|
||||
/// states each. <i>Not owned</i> stores the accepted destination like any
|
||||
/// other never-ran outcome. <i>No canonical body</i> stores nothing:
|
||||
/// there is nowhere to write, which
|
||||
/// <see cref="RuntimeRemotePlacementDriveController.StoreAcceptedDestinationPose"/>
|
||||
/// tests directly rather than inferring from this value. <i>No
|
||||
/// incarnation key, with a body</i> DOES write, and that is correct on
|
||||
/// retail's partition — nothing ran, so the pose must advance — but it is
|
||||
/// unreachable in production regardless: the far arm requires an
|
||||
/// already-classified route, and
|
||||
/// <c>RuntimeAcceptedPositionRouteRequests.TryBuild</c> refuses to build
|
||||
/// one for a record whose <c>Key</c> is null, so no caller can hold a
|
||||
/// route for a keyless record.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
NotApplicable,
|
||||
|
||||
/// <summary>
|
||||
/// The central decision (see the class doc on
|
||||
/// <see cref="RuntimeRemotePlacementDriveController"/>): the accepted
|
||||
/// destination is not one this host can currently collision-publish, or
|
||||
/// the canonical SetPosition attempted anyway and Core still deferred it.
|
||||
/// No operation is retained either way — the entity keeps its last
|
||||
/// committed pose and waits for the next accepted Position, which for a
|
||||
/// remote is a 5-10 Hz stream away.
|
||||
/// <see cref="RuntimeRemotePlacementDriveController"/>): the pre-flight
|
||||
/// declined to attempt this destination at all, so no operation was ever
|
||||
/// begun and the canonical resolve never ran.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Stores.</b> This is exactly retail's no-transition branch — "no cell
|
||||
/// for it here", never "do not move". See
|
||||
/// <see cref="RuntimeRemotePlacementDriveController.ApplyAcceptedRemoteFarSnap"/>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
Refused,
|
||||
|
||||
/// <summary>The entity already holds an active operation — a concurrent
|
||||
/// placement authority (portal/teleport/another host route) or this
|
||||
/// controller's own still-outstanding preparation retry.</summary>
|
||||
/// <summary>
|
||||
/// <see cref="RuntimeSetPositionState.TryBeginExclusiveAuthoredPlacement"/>
|
||||
/// refused, or this controller retained a preparation retry.
|
||||
///
|
||||
/// <para>
|
||||
/// Round 3 (correction m2) — the producers this route can actually reach,
|
||||
/// not the two the earlier text named. The Begin refuses when the entity
|
||||
/// already holds an active operation (a concurrent placement authority —
|
||||
/// portal, teleport, another host route), when it holds a RETAINED
|
||||
/// COMPLETION that has not been acknowledged yet
|
||||
/// (<c>HasRetainedCompletion</c> — ordinary right after a
|
||||
/// <see cref="Committed"/> far snap whose projection sink declined), or
|
||||
/// when the record is no longer the current incarnation
|
||||
/// (<c>!IsCurrent</c>). Its remaining refusals cannot fire from here:
|
||||
/// the expected position-authority version is read off the same record
|
||||
/// one statement earlier, and the accepted destination was already
|
||||
/// required non-null by the pre-flight. This controller adds the fourth
|
||||
/// producer: a retryable preparation status
|
||||
/// (<c>RetrySetupUnavailable</c>/<c>RetryWorldFrameUnavailable</c>).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Stores</b> in every one of those shapes: all four are decided
|
||||
/// before <c>_physics.Engine.SetPosition</c> is called for THIS packet,
|
||||
/// and retail never waits for an asset before committing the pose.
|
||||
/// (<see cref="RuntimeRemotePlacementDriveController.StoreAcceptedDestinationPose"/>
|
||||
/// re-validates currency itself, so the <c>!IsCurrent</c> producer cannot
|
||||
/// write through a superseded record.)
|
||||
/// </para>
|
||||
/// </summary>
|
||||
Contention,
|
||||
|
||||
/// <summary>The canonical SetPosition committed synchronously.</summary>
|
||||
/// <summary>
|
||||
/// Preparation refused TERMINALLY, before anything was submitted:
|
||||
/// <c>RuntimeSetPositionMoverPreparationStatus.RejectedAuthority</c> (the
|
||||
/// operation/authority pair went stale between Begin and prepare) or
|
||||
/// <c>InvalidData</c> (the accepted frame fails
|
||||
/// <c>PositionFrameValidation</c>, or the prepared mover could not be
|
||||
/// built).
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Stores.</b> The engine was never called — this is the same
|
||||
/// "placement never executed" state as <see cref="Refused"/>, reached one
|
||||
/// stage later. Kept distinct from <see cref="RejectedByPlacement"/>
|
||||
/// because that one is the opposite side of the partition.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
RejectedPreparation,
|
||||
|
||||
/// <summary>The canonical SetPosition committed synchronously.
|
||||
/// <b>Does not store</b>: the committed pose IS the settled pose, and it
|
||||
/// is already on the canonical body.</summary>
|
||||
Committed,
|
||||
|
||||
/// <summary>Rejected/cancelled by Core (invalid prepared data, authority
|
||||
/// displaced mid-submit).</summary>
|
||||
Rejected,
|
||||
/// <summary>
|
||||
/// Core parked the placement (<c>RuntimeSetPositionStatus.DeferredCell</c>)
|
||||
/// and this controller cancelled-and-restored it immediately rather than
|
||||
/// retaining a watch — no park survives this controller.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Does not store.</b> <c>ParkDeferred</c> has ALREADY performed
|
||||
/// retail's <c>store_position</c>: it snaps the body to the parked result
|
||||
/// before withdrawing (and <c>RestoreParkWithdrawal</c> deliberately
|
||||
/// leaves that pose alone). For the pre-sweep quiescence park that result
|
||||
/// is the accepted destination; for the post-sweep one it is the
|
||||
/// COLLISION-SETTLED <c>spherePath.CurPos</c>, and writing the raw
|
||||
/// destination over it would undo the settle (delta review N2). The two
|
||||
/// <c>DeferredCell</c> returns that park nothing
|
||||
/// (<c>InheritedLostDeadline</c>, <c>WakeableLostCell</c>) are
|
||||
/// unreachable here: <c>TryBeginExclusiveAuthoredPlacement</c> refuses
|
||||
/// whenever an operation already exists for the key, so neither flag can
|
||||
/// be inherited, and this controller's own retained entries are
|
||||
/// preparation retries that have not parked.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
Deferred,
|
||||
|
||||
/// <summary>
|
||||
/// The canonical placement EXECUTED and did not commit:
|
||||
/// <c>RuntimeSetPositionStatus.Rejected</c> (the engine's own sweep
|
||||
/// refused the destination — <c>PhysicsEngine.SetPosition</c> returned a
|
||||
/// non-Ok error, acdream's port of @0x00515CD5/@0x00515CB2) or
|
||||
/// <c>Cancelled</c> (authority was displaced after the engine ran, which
|
||||
/// includes the case where <c>CommitCanonical</c> had already SETTLED the
|
||||
/// body).
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Does not store.</b> Retail's matching returns (2/3/4) leave the
|
||||
/// object where it was, and the <c>Cancelled</c>-after-commit shape has a
|
||||
/// freshly settled pose (contact plane, step-down) that a raw destination
|
||||
/// write would destroy.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Round 3 (correction B2): "EXECUTED" is a reachability property of the
|
||||
/// producers this route can reach, not a structural guarantee Core
|
||||
/// enforces. <c>RuntimeSetPositionStatus.Rejected</c> has two other,
|
||||
/// PRE-engine producers in <c>SubmitPreparedPlacementCore</c>, both
|
||||
/// unreachable through this controller; the <c>default:</c> arm of
|
||||
/// <see cref="RuntimeRemotePlacementDriveController.SubmitAndResolve"/>
|
||||
/// enumerates exactly what each depends on, and why the failure direction
|
||||
/// if one is ever broken is the safe one.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
RejectedByPlacement,
|
||||
}
|
||||
|
||||
internal static class RuntimeRemotePlacementExecutionStatusExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// True when retail's <c>store_position</c> @0x00515CE2 is what this
|
||||
/// outcome corresponds to: the canonical placement never reached the
|
||||
/// engine, so nothing has committed a pose to the body and the remote
|
||||
/// would otherwise be frozen at a superseded one. See the enum's own doc
|
||||
/// for the retail partition and for why each value falls where it does.
|
||||
///
|
||||
/// <para>
|
||||
/// Written as an exhaustive switch rather than a set test so adding a
|
||||
/// value is a compile-time decision, not a silent default.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static bool StoresAcceptedDestination(
|
||||
this RuntimeRemotePlacementExecutionStatus status) =>
|
||||
status switch
|
||||
{
|
||||
RuntimeRemotePlacementExecutionStatus.NotApplicable => true,
|
||||
RuntimeRemotePlacementExecutionStatus.Refused => true,
|
||||
RuntimeRemotePlacementExecutionStatus.Contention => true,
|
||||
RuntimeRemotePlacementExecutionStatus.RejectedPreparation => true,
|
||||
RuntimeRemotePlacementExecutionStatus.Committed => false,
|
||||
RuntimeRemotePlacementExecutionStatus.Deferred => false,
|
||||
RuntimeRemotePlacementExecutionStatus.RejectedByPlacement => false,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status)),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -85,7 +283,21 @@ internal enum RuntimeRemotePlacementExecutionStatus : byte
|
|||
/// <c>SendPositionEvent</c> — <c>HandleReceivedPosition</c> @0x00453FD0 calls
|
||||
/// it only on the local-player FORCE_POSITION branch. There is nothing here
|
||||
/// resembling route 2's <c>PositionEventOwed</c>/<c>SendPositionEvent</c>
|
||||
/// pair.</description></item>
|
||||
/// pair. <b>The separate question 4b-1's review left open is now settled</b>
|
||||
/// (C4 route 4b-2 review fix): the <c>SetPositionStruct</c> flag
|
||||
/// <c>SEND_POSITION_EVENT_SPF = 0x1000</c> — which
|
||||
/// <c>CPhysicsObj::SetPositionSimple</c> @0x005162B0 sets as part of
|
||||
/// <c>0x1012</c> @0x005162C4 and which therefore reaches
|
||||
/// <c>TryPrepareAndSubmitAuthoredPlacement</c> here — is <b>never read</b>.
|
||||
/// The commit path <c>CPhysicsObj::SetPositionInternal</c>
|
||||
/// @0x00515330-@0x00515593 takes only <c>this</c> and the <c>CTransition</c>
|
||||
/// and never sees the struct; the flag-reading overload @0x00515BD0 tests
|
||||
/// only bit 5 (<c>DONOTCREATECELLS_SPF = 0x20</c>) @0x00515BF8/@0x00515C6C;
|
||||
/// and no site anywhere in the decomp tests bit 12 of a
|
||||
/// <c>SetPositionStruct</c>. It is a carried, inert flag, entirely distinct
|
||||
/// from <c>CommandInterpreter::SendPositionEvent</c> @0x006B4770 (dispatched
|
||||
/// from @0x00454091, the local-player branch 4b-1 already
|
||||
/// cleared).</description></item>
|
||||
/// <item><description><b>No re-issue funnel.</b> Route 2 re-issues a dead
|
||||
/// operation because a ForcePosition is a one-shot correction ACE never
|
||||
/// repeats. A remote Position is a REPEATED stream — re-issuing packet N
|
||||
|
|
@ -104,42 +316,66 @@ internal enum RuntimeRemotePlacementExecutionStatus : byte
|
|||
/// </list>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>The central decision — refuse, do not park.</b>
|
||||
/// <b>The central decision — avoid the PARK where it is cheap to, never the
|
||||
/// tracking.</b>
|
||||
/// <see cref="RuntimeSetPositionState"/>'s <c>DeferredCell</c> park
|
||||
/// withdraws the entity from the world (<c>ParkDeferred</c> sets
|
||||
/// <c>body.InWorld = false</c>, suspends the object clock, and publishes a
|
||||
/// <c>Withdraw</c>) — and <c>RuntimeEntityObjectLifetime.TryApplyPosition</c>
|
||||
/// calls <c>RuntimeSetPositionState.Forget</c> on EVERY subsequent accepted
|
||||
/// Position for that same entity, regardless of disposition. <c>Forget</c>'s
|
||||
/// <c>CancelCoreDeferred</c> removes the operation and rewrites the
|
||||
/// <c>Withdraw</c> into a <c>Discard</c> WITHOUT restoring <c>InWorld</c>,
|
||||
/// resuming the clock, or re-entering residency. Because ACE broadcasts a
|
||||
/// remote's Position every 100-150 ms — almost always faster than the
|
||||
/// collision-generation wake this park would need to resolve on its own —
|
||||
/// any DeferredCell park opened here would be cancelled by the entity's own
|
||||
/// next packet long before it could wake, leaving the entity invisible AND
|
||||
/// intangible for the rest of the session. <see cref="_serviceWindow"/>
|
||||
/// exists to prevent this controller from ever attempting a SetPosition
|
||||
/// whose destination cannot be placed right now: <see cref="TryExecuteAcceptedRemotePosition"/>
|
||||
/// Position for that same entity, regardless of disposition. Because ACE
|
||||
/// broadcasts a remote's Position every 100-150 ms — almost always faster
|
||||
/// than the collision-generation wake this park would need to resolve on its
|
||||
/// own — any DeferredCell park opened here would be cancelled by the
|
||||
/// entity's own next packet long before it could wake.
|
||||
/// <b>Correcting this paragraph's earlier claim</b> (4b-1's review ordered it
|
||||
/// and it shipped uncorrected): that cancellation no longer strands the
|
||||
/// entity "invisible AND intangible for the rest of the session".
|
||||
/// <see cref="CancelToken"/> passes <c>restoreCancelledPark: true</c>, and
|
||||
/// <c>RuntimeSetPositionState.RestoreParkWithdrawal</c> rolls
|
||||
/// <c>InWorld</c>, the object clock, and canonical residency back — the
|
||||
/// AP-136 behaviour <c>634bc551</c> landed. The park is still worth avoiding
|
||||
/// (a restored park re-shows the entity where retail keeps it hidden until
|
||||
/// cell load, which is precisely what AP-136 records), so
|
||||
/// <see cref="_serviceWindow"/> stays — but it is an OPTIMISATION, not a
|
||||
/// correctness mechanism, and it is never the reason a remote stops
|
||||
/// tracking the server (see
|
||||
/// <see cref="ApplyAcceptedRemoteFarSnap"/>'s <c>store_position</c>
|
||||
/// fallback). <see cref="TryExecuteAcceptedRemotePosition"/>
|
||||
/// checks it BEFORE calling
|
||||
/// <see cref="RuntimeSetPositionState.TryBeginExclusiveAuthoredPlacement"/>,
|
||||
/// and if Core still returns <c>DeferredCell</c> despite the guard passing,
|
||||
/// the operation is cancelled immediately rather than retained, so the
|
||||
/// ledger still converges even though the guard's invariant did not hold for
|
||||
/// that one packet. <b>Why the guard can still miss (the co-extensivity
|
||||
/// finding and its residual):</b> a tier/residency-backed service window
|
||||
/// (the graphical host's <c>GpuWorldState.IsNearTier</c>, headless's
|
||||
/// that one packet.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Why a pre-flight structurally cannot be the correctness mechanism (C4
|
||||
/// route 4b-2, delta review MAJOR B/C).</b> A tier/residency-backed service
|
||||
/// window (the graphical host's <c>GpuWorldState.IsNearTier</c>, headless's
|
||||
/// collision-published check) is co-extensive with collision PUBLICATION in
|
||||
/// both directions — verified by reading both tier-writer call sites
|
||||
/// (promotion cannot read Near before collision commits) and the retirement
|
||||
/// call site (the tier flips to Far as the FIRST, synchronous step of
|
||||
/// retirement, strictly before collision-side withdrawal). It is NOT
|
||||
/// co-extensive with a live in-place collision-prefix MUTATION that leaves
|
||||
/// the tier/residency reading unchanged while the prefix quiesces —
|
||||
/// <see cref="RuntimeSetPositionState"/>'s private <c>TryGetBlockingQuiescence</c>
|
||||
/// is Core's own check for exactly that case, and it is what a placement can
|
||||
/// still hit even after this guard passes. That one narrow window is the
|
||||
/// residual this guard cannot close from outside Core.
|
||||
/// co-extensive with a live in-place collision-prefix MUTATION, which is why
|
||||
/// <see cref="CanAttemptDestination"/> also reads Core's own
|
||||
/// <see cref="RuntimeSetPositionState.IsCollisionPrefixQuiescing"/>. But even
|
||||
/// that pair only tests ONE prefix — the DESTINATION's — while Core's park
|
||||
/// predicates are broader: <c>ResultTouchesPrefix</c> scans every
|
||||
/// <c>QueriedCellIds</c> entry, a sweep footprint that provably spans
|
||||
/// NEIGHBOUR landblocks (<c>CellTransit.AddOutsideCell</c> re-derives the
|
||||
/// block id from the global lcoord and has no same-block filter). The sweep
|
||||
/// footprint does not exist until the sweep has run, so no pre-flight can
|
||||
/// close that half. <b>The correctness mechanism is therefore at the source:</b>
|
||||
/// <c>SubmitPreparedPlacementCore</c>'s two quiescence parks are RESTORABLE,
|
||||
/// and <c>ParkDeferred</c> decides whether the rollback is safe by testing
|
||||
/// the cell it will actually restore into — after the snap, against EVERY
|
||||
/// live quiescence — so the rollback re-admits nothing into ANY quiescing
|
||||
/// prefix, which is the only thing AP-136's blanket non-restorable scoping
|
||||
/// was protecting. This guard merely shrinks the reachable set. Recorded as
|
||||
/// AP-138(2).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
|
|
@ -198,6 +434,13 @@ internal sealed class RuntimeRemotePlacementDriveController
|
|||
/// is mid-iteration can never corrupt its scratch buffer.
|
||||
/// </summary>
|
||||
private readonly List<RuntimeEntityKey> _awaitingAcknowledgementScratch = [];
|
||||
/// <summary>
|
||||
/// R9 review fix: <see cref="CountLivePending"/>'s own removal buffer,
|
||||
/// kept separate from <see cref="_driveScratch"/> (owned by
|
||||
/// <see cref="Advance"/>) for the same reason
|
||||
/// <see cref="_awaitingAcknowledgementScratch"/> is.
|
||||
/// </summary>
|
||||
private readonly List<RuntimeEntityKey> _pendingScratch = [];
|
||||
private bool _driving;
|
||||
private object? _routeOwner;
|
||||
|
||||
|
|
@ -215,7 +458,7 @@ internal sealed class RuntimeRemotePlacementDriveController
|
|||
_serviceWindow = serviceWindow
|
||||
?? throw new ArgumentNullException(nameof(serviceWindow));
|
||||
_entityObjects.RegisterRemotePlacementDriveOwnership(
|
||||
() => _pending.Count);
|
||||
CountLivePending);
|
||||
// B4 review fix: a second, independent registration — multiple
|
||||
// registrations sum (RegisterRemotePlacementDriveOwnership's own doc
|
||||
// comment) — so the awaiting-acknowledgement dimension is visible in
|
||||
|
|
@ -230,6 +473,9 @@ internal sealed class RuntimeRemotePlacementDriveController
|
|||
/// that dimension is reported to the lifetime's ownership ledger via the
|
||||
/// constructor's second <c>RegisterRemotePlacementDriveOwnership</c> call
|
||||
/// (B4 review fix) and has no separate test-visible counter of its own.
|
||||
/// Raw, unhealed: this is the map's literal size. The ownership ledger
|
||||
/// reads <see cref="CountLivePending"/> instead, which prunes entries Core
|
||||
/// has already retired (R9 review fix).
|
||||
/// </summary>
|
||||
internal int PendingCount => _pending.Count;
|
||||
|
||||
|
|
@ -398,10 +644,14 @@ internal sealed class RuntimeRemotePlacementDriveController
|
|||
CreateObject.ServerPosition? destination =
|
||||
record.Snapshot.Physics?.Position ?? record.Snapshot.Position;
|
||||
if (destination is not { } accepted
|
||||
|| !_serviceWindow.IsWithinServiceWindow(accepted.LandblockId))
|
||||
|| !CanAttemptDestination(setPosition, accepted.LandblockId))
|
||||
{
|
||||
// The central decision: refuse rather than open a park this
|
||||
// host's own service window could never wake.
|
||||
// The central decision: skip a placement this host already knows
|
||||
// will only park and be cancelled again. An OPTIMISATION — see
|
||||
// CanAttemptDestination for the two Core predicates it cannot
|
||||
// reproduce and where correctness actually lives — and the body
|
||||
// still advances, because Refused is on the storing side of
|
||||
// retail's partition.
|
||||
return RuntimeRemotePlacementExecutionStatus.Refused;
|
||||
}
|
||||
|
||||
|
|
@ -416,6 +666,218 @@ internal sealed class RuntimeRemotePlacementDriveController
|
|||
return SubmitAndResolve(record, token, route);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C4 route 4b-2: retail's far branch, end to end, for one remote whose
|
||||
/// accepted Position already classified to
|
||||
/// <see cref="RuntimeRemoteFarSnapPosition.OwnsFarSnap"/>. This is the
|
||||
/// route's FIRST production caller of
|
||||
/// <see cref="TryExecuteAcceptedRemotePosition"/>.
|
||||
///
|
||||
/// <code>
|
||||
/// 005163c1 position_manager = this_1->position_manager;
|
||||
/// 005163c9 if (position_manager != 0)
|
||||
/// 005163cb PositionManager::StopInterpolating(position_manager);
|
||||
/// 005163d9 CPhysicsObj::SetPositionSimple(this_1, arg2, 1);
|
||||
/// 005163e8 return 1;
|
||||
/// </code>
|
||||
///
|
||||
/// <para>
|
||||
/// The ORDER is load-bearing and is why this lives here rather than being
|
||||
/// assembled by a caller: <c>StopInterpolating</c> runs strictly BEFORE
|
||||
/// the placement (@0x005163CB before @0x005163D9). acdream's
|
||||
/// <c>InterpolationManager</c> queue is the analogue — a stale near
|
||||
/// waypoint surviving a far snap would immediately drag the freshly
|
||||
/// placed body back toward it on the next per-tick catch-up. The
|
||||
/// classifier's own <c>StopInterpolating</c> flag
|
||||
/// (<c>RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition</c>'s
|
||||
/// remote tail, <c>StopInterpolating: !nearby</c>) is what gates it, so
|
||||
/// the retail condition is read from the route rather than restated.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The destination is NOT a parameter: it is read from the canonical
|
||||
/// merged snapshot by <see cref="TryExecuteAcceptedRemotePosition"/> and
|
||||
/// resolved through Runtime's own accepted world frame
|
||||
/// (<c>resolveWorldOffsetFromRuntimeFrame: true</c>). A caller's
|
||||
/// separately-derived world position is not a second input — #283 proved
|
||||
/// by measurement that App's streaming origin and Runtime's world frame
|
||||
/// cannot disagree, and <c>LiveWorldOriginState.EnsureAgreesWithRuntimeFrame</c>
|
||||
/// is terminal if they ever do.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <c>ConstrainTo</c> is deliberately NOT armed here.
|
||||
/// <c>SmartBox::HandleReceivedPosition</c> owns retail's single arming
|
||||
/// site (@0x00454272) for all three nonzero-returning branches; acdream's
|
||||
/// analogue is
|
||||
/// <see cref="RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation"/>,
|
||||
/// called once by the caller after the routing block. Arming inside this
|
||||
/// method would be the second site the route 4b scoping forbids.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>The <c>store_position</c> fallback (C4 route 4b-2 review fix — the
|
||||
/// one root defect).</b> Every outcome for which the canonical placement
|
||||
/// never reached the engine
|
||||
/// (<see cref="RuntimeRemotePlacementExecutionStatusExtensions.StoresAcceptedDestination"/>)
|
||||
/// still advances the body to the accepted destination pose, through
|
||||
/// <see cref="StoreAcceptedDestinationPose"/>. Without it a refused,
|
||||
/// contended, or preparation-rejected far snap left the body at its stale
|
||||
/// pose with the interpolation queue ALREADY cleared by the
|
||||
/// <c>StopInterpolating</c> above — a frozen remote that the next 5-10 Hz
|
||||
/// packet simply reproduces, because nothing about the refusal reason
|
||||
/// changes at packet cadence.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This is retail, not a symptom patch.
|
||||
/// <c>CPhysicsObj::SetPositionInternal</c> @0x00515BD0 takes the
|
||||
/// no-transition branch @0x00515C1D whenever <c>AdjustPosition</c>
|
||||
/// resolves no cell, and that branch commits the destination pose —
|
||||
/// <c>prepare_to_leave_visibility</c> @0x00515CDA,
|
||||
/// <c>store_position(this, arg2)</c> @0x00515CE2,
|
||||
/// <c>CObjectMaint::GotoLostCell</c> @0x00515CF2 — and returns 0
|
||||
/// (success) @0x00515D07. "This host cannot resolve a cell for the
|
||||
/// destination right now" is exactly that state; retail moves the object
|
||||
/// and hides it pending cell load, and never leaves it standing at a
|
||||
/// superseded pose. The deleted legacy far block also always tracked.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The other half of retail's partition is honoured too (delta review
|
||||
/// MAJOR A — correcting this comment's earlier claim that no non-commit
|
||||
/// outcome could represent it).</b> Retail's failure returns that do NOT
|
||||
/// store — <c>curr_cell == 0</c> @0x00515C8F/@0x00515CB2 and the blocked
|
||||
/// <c>CheckPositionInternal</c> @0x00515C85/@0x00515CD5 — are reached
|
||||
/// AFTER the resolve ran, and acdream CAN represent that:
|
||||
/// <see cref="RuntimeRemotePlacementExecutionStatus.RejectedByPlacement"/>
|
||||
/// is returned when <c>PhysicsEngine.SetPosition</c> itself refused, or
|
||||
/// when authority was displaced after it ran (including after
|
||||
/// <c>CommitCanonical</c> had already settled the body). Those do not
|
||||
/// store, so a destination the engine's own sweep refuses no longer
|
||||
/// teleports the canonical body into it, and a just-settled pose is never
|
||||
/// overwritten. The residual is recorded as AP-138.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal RuntimeRemotePlacementExecutionStatus ApplyAcceptedRemoteFarSnap(
|
||||
RuntimeEntityRecord record,
|
||||
RemoteMotion remote,
|
||||
in RuntimeAuthoritativePositionRoute route)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(remote);
|
||||
if (!RuntimeRemoteFarSnapPosition.OwnsFarSnap(route))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Only a remote far-snap classification (SetPositionSimple, "
|
||||
+ "RemoteAuthoritative, Teleport-flagged) may be applied "
|
||||
+ "through the far-snap arm; the caller must select the arm "
|
||||
+ "with RuntimeRemoteFarSnapPosition.ResolveArm.",
|
||||
nameof(route));
|
||||
}
|
||||
|
||||
if (route.StopInterpolating)
|
||||
remote.Interp.Clear();
|
||||
RuntimeRemotePlacementExecutionStatus status =
|
||||
TryExecuteAcceptedRemotePosition(record, route);
|
||||
if (status.StoresAcceptedDestination())
|
||||
StoreAcceptedDestinationPose(record);
|
||||
return status;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CPhysicsObj::store_position</c> @0x00515CE2, reached from
|
||||
/// <c>SetPositionInternal</c>'s no-resolvable-cell branch @0x00515C1D.
|
||||
/// Commits the SAME accepted destination
|
||||
/// <see cref="TryExecuteAcceptedRemotePosition"/> reads — the canonical
|
||||
/// merged snapshot, resolved through Runtime's own accepted world frame,
|
||||
/// which is the exact pair
|
||||
/// <c>RuntimeSetPositionMoverPreparer.TryBuild</c> composes for a
|
||||
/// committed placement (<c>cellLocal + ShadowWorldOffset</c> in X/Y, the
|
||||
/// authored Z, the authored quaternion). A caller's separately-derived
|
||||
/// wire pose is deliberately not an input here either, for the same
|
||||
/// reason the committed path does not take one.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Currency is re-validated first (delta review MAJOR D).</b>
|
||||
/// <see cref="CancelToken"/> publishes its cancellation receipt
|
||||
/// SYNCHRONOUSLY, and the production placement-projection sink can delete
|
||||
/// or replace this incarnation from inside that publish — so by the time
|
||||
/// control returns to <see cref="ApplyAcceptedRemoteFarSnap"/> the record
|
||||
/// may no longer be the canonical one for its GUID. Writing a pose
|
||||
/// through a superseded incarnation is the exact class the R5 fix
|
||||
/// introduced the rule for, and
|
||||
/// <c>RuntimeSetPositionState.RestoreParkWithdrawal</c> already guards the
|
||||
/// same way. The App-side re-validation happens only after this seam
|
||||
/// returns, which is too late.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>The write target is the CANONICAL body (delta review N5).</b>
|
||||
/// Retail's <c>store_position(this, arg2)</c> writes the CPhysicsObj's own
|
||||
/// position, and acdream's canonical CPhysicsObj is
|
||||
/// <see cref="RuntimeEntityRecord.PhysicsBody"/>.
|
||||
/// <c>RemoteMotion.Body</c> IS that instance in production, by two
|
||||
/// independent routes: <c>RuntimePhysicsState.GetOrCreateRemoteMotion</c>
|
||||
/// passes <c>record.PhysicsBody</c> into the constructor, and when the
|
||||
/// record has no body yet <c>SetRemoteMotion</c> ADOPTS the component's
|
||||
/// private one as canonical (<c>InitializeNewPhysicsBody</c> +
|
||||
/// <c>SetPhysicsBody</c>). So the divergent pair is not constructible
|
||||
/// through the production seam and this change moves no production
|
||||
/// behaviour — it removes the ability to write a pose into a body the
|
||||
/// canonical record does not own, which is what
|
||||
/// <see cref="RuntimeRemotePlacementExecutionStatus.NotApplicable"/>'s
|
||||
/// no-body producer would otherwise permit for a caller-supplied
|
||||
/// component. Reading the record directly makes "there is no canonical
|
||||
/// body" and "there is nothing to store into" the same test.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Returns false only when there is nothing to write, or nowhere to write
|
||||
/// it: no canonical body, no accepted destination on the snapshot, a
|
||||
/// superseded incarnation, or no Runtime world frame yet. The last case
|
||||
/// reuses Core's own #284 policy —
|
||||
/// <see cref="RuntimePhysicsState.ThrowIfWorldFrameUnreachable"/> makes a
|
||||
/// frame that can never arrive terminal rather than silent — so the only
|
||||
/// surviving false there is the genuine pre-local-player-Create window,
|
||||
/// which the far arm cannot reach anyway: the classifier needs a live
|
||||
/// <c>player_distance</c> to emit <c>SetPositionSimple</c> at all, and
|
||||
/// that requires the local movement controller to exist.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private bool StoreAcceptedDestinationPose(RuntimeEntityRecord record)
|
||||
{
|
||||
if (!_entityObjects.Entities.IsCurrent(record)
|
||||
|| record.PhysicsBody is not { } body)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CreateObject.ServerPosition? destination =
|
||||
record.Snapshot.Physics?.Position ?? record.Snapshot.Position;
|
||||
if (destination is not { } accepted)
|
||||
return false;
|
||||
|
||||
if (!_entityObjects.Physics.TryGetWorldFrameOffset(
|
||||
accepted.LandblockId,
|
||||
out float worldOffsetX,
|
||||
out float worldOffsetY))
|
||||
{
|
||||
_entityObjects.Physics.ThrowIfWorldFrameUnreachable(
|
||||
accepted.LandblockId);
|
||||
return false;
|
||||
}
|
||||
|
||||
body.Position = new Vector3(
|
||||
accepted.PositionX + worldOffsetX,
|
||||
accepted.PositionY + worldOffsetY,
|
||||
accepted.PositionZ);
|
||||
body.Orientation = new Quaternion(
|
||||
accepted.RotationX,
|
||||
accepted.RotationY,
|
||||
accepted.RotationZ,
|
||||
accepted.RotationW);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Host cadence pump: retries a preparation-only retry status
|
||||
/// (<c>RetrySetupUnavailable</c>/<c>RetryWorldFrameUnavailable</c>) by
|
||||
|
|
@ -457,24 +919,35 @@ internal sealed class RuntimeRemotePlacementDriveController
|
|||
// 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
|
||||
// retires out from under it. Re-check the SAME service-window
|
||||
// guard the entry point uses BEFORE resubmitting — dropping
|
||||
// (and cancelling the already-begun token) rather than
|
||||
// resubmitting matches the entry point's own Refused
|
||||
// semantics: no operation survives, the entity keeps its last
|
||||
// committed pose, and the next packet supplies current truth
|
||||
// on its own. Without this re-check a destination that fell
|
||||
// out of the window would just keep coming back Contention
|
||||
// forever (the asset source has not changed), never
|
||||
// retires out from under it. Re-check the SAME pre-flight the
|
||||
// entry point uses BEFORE resubmitting — dropping (and
|
||||
// cancelling the already-begun token) rather than resubmitting
|
||||
// matches the entry point's own Refused semantics: no
|
||||
// operation survives and the next packet supplies current
|
||||
// truth on its own. Without this re-check a destination that
|
||||
// fell out of the window would just keep coming back
|
||||
// Contention forever (the asset source has not changed), never
|
||||
// converging even though the window already knows better.
|
||||
//
|
||||
// Delta review N1: this path is the entry point's Refused
|
||||
// semantics, so it must ALSO be the entry point's
|
||||
// store_position — correcting this comment's earlier claim
|
||||
// that "the entity keeps its last committed pose". A retained
|
||||
// retry whose destination leaves the window is exactly the
|
||||
// freeze the fallback exists to prevent, one cadence pump
|
||||
// later; the pre-flight is an optimisation here for the same
|
||||
// reason it is one at the entry point, and it may never be the
|
||||
// reason a remote stops tracking.
|
||||
CreateObject.ServerPosition? destination =
|
||||
pending.Record.Snapshot.Physics?.Position
|
||||
?? pending.Record.Snapshot.Position;
|
||||
if (destination is not { } accepted
|
||||
|| !_serviceWindow.IsWithinServiceWindow(
|
||||
|| !CanAttemptDestination(
|
||||
setPosition,
|
||||
accepted.LandblockId))
|
||||
{
|
||||
CancelToken(setPosition, pending.Token);
|
||||
StoreAcceptedDestinationPose(pending.Record);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -517,8 +990,12 @@ internal sealed class RuntimeRemotePlacementDriveController
|
|||
return RuntimeRemotePlacementExecutionStatus.Contention;
|
||||
}
|
||||
|
||||
// Preparation refused TERMINALLY (RejectedAuthority/InvalidData).
|
||||
// Nothing was submitted, so the engine never ran — this is the
|
||||
// STORING side of retail's partition, one stage later than the
|
||||
// pre-flight Refused. Delta review MAJOR A.
|
||||
CancelToken(setPosition, token);
|
||||
return RuntimeRemotePlacementExecutionStatus.Rejected;
|
||||
return RuntimeRemotePlacementExecutionStatus.RejectedPreparation;
|
||||
}
|
||||
|
||||
switch (outcome.Status)
|
||||
|
|
@ -541,25 +1018,141 @@ internal sealed class RuntimeRemotePlacementDriveController
|
|||
return RuntimeRemotePlacementExecutionStatus.Committed;
|
||||
|
||||
case RuntimeSetPositionStatus.DeferredCell:
|
||||
// Central decision: this branch means the service-window
|
||||
// guard passed but Core still deferred the destination — the
|
||||
// narrow residual the class doc's central-decision paragraph
|
||||
// explains (a live in-place collision-prefix quiescence the
|
||||
// tier/residency guard cannot see from outside Core, per
|
||||
// RuntimeSetPositionState's private TryGetBlockingQuiescence).
|
||||
// Cancel immediately rather than retain a watch: no
|
||||
// re-issue, no park survives this controller.
|
||||
// Central decision: the pre-flight passed but Core still
|
||||
// parked the destination — the residual the class doc's
|
||||
// central-decision paragraph explains, now that the pre-flight
|
||||
// is explicitly only an optimisation (it tests the DESTINATION
|
||||
// prefix; Core's own post-sweep predicate also matches the
|
||||
// whole swept footprint, which spans neighbouring landblocks
|
||||
// and does not exist until the sweep has run). Cancel
|
||||
// immediately rather than
|
||||
// retain a watch: no re-issue, no park survives this
|
||||
// controller, and CancelToken's restoreCancelledPark rolls the
|
||||
// withdrawal back at the destination cell.
|
||||
//
|
||||
// ParkDeferred has already performed retail's store_position —
|
||||
// it snaps the body to the parked result before withdrawing,
|
||||
// and RestoreParkWithdrawal deliberately leaves that pose
|
||||
// alone — so the fallback must NOT fire here. For the
|
||||
// post-sweep park that result is the collision-SETTLED
|
||||
// position and a raw destination write would undo the settle
|
||||
// (delta review N2).
|
||||
CancelToken(setPosition, token);
|
||||
return RuntimeRemotePlacementExecutionStatus.Refused;
|
||||
return RuntimeRemotePlacementExecutionStatus.Deferred;
|
||||
|
||||
default:
|
||||
// Rejected/Cancelled — authority moved out from under this
|
||||
// operation, so the body never moved.
|
||||
// Rejected/Cancelled. Correcting this comment's earlier claim
|
||||
// that "the body never moved" (delta review MAJOR A — it was
|
||||
// the claim that made the fallback fire here): the statuses
|
||||
// that reach this arm are returned AFTER
|
||||
// _physics.Engine.SetPosition ran. Rejected is the engine's
|
||||
// own sweep refusing the destination (retail @0x00515CD5 /
|
||||
// @0x00515CB2, neither of which stores), and Cancelled
|
||||
// includes the shape where CommitCanonical already SETTLED
|
||||
// the body and only the projection ownership was displaced.
|
||||
// Neither may be overwritten with the raw accepted
|
||||
// destination.
|
||||
//
|
||||
// Round 3 (correction B2) — that is a REACHABILITY claim, not
|
||||
// a structural one, so state what it rests on. Core's
|
||||
// SubmitPreparedPlacementCore has three Rejected producers
|
||||
// and only the post-engine one (`!result.IsSuccessful` after
|
||||
// the SetPosition call) can reach here. Its two PRE-engine
|
||||
// producers are unreachable through this controller because
|
||||
// each is shadowed by a check PrepareMover already made, with
|
||||
// nothing reentrant in between:
|
||||
// * the entry validation's identity/authority terms (token
|
||||
// ownership, stage, preparation authority) re-test what
|
||||
// PrepareMover's own guard clause tested one call
|
||||
// earlier, and IsPreparationAuthorityCurrent catches
|
||||
// all seven of its version dimensions there first — as
|
||||
// RejectedAuthority, i.e. RejectedPreparation, which
|
||||
// DOES store. The one stage divergence PrepareMover
|
||||
// admits and SubmitPreparedPlacementCore refuses is
|
||||
// AwaitingCell, which additionally requires
|
||||
// DormantLocalActivation — forbidden for a record that
|
||||
// has a body, and this route requires one. Its remaining
|
||||
// terms (finite GameTime, matching Kind/Portal, the exact
|
||||
// PreparedCommand, the velocity-version echo) are
|
||||
// satisfied by construction: TryPrepareAndSubmitAuthored-
|
||||
// Placement submits the very command PrepareMover just
|
||||
// stored, built from the same operation, with nothing in
|
||||
// between.
|
||||
// * the NULL-BODY term is the one entry-validation test
|
||||
// PrepareMover does NOT shadow (round 4, D3 — its guard
|
||||
// clause has no body test at all). This route's own
|
||||
// entry check does: TryExecuteAcceptedRemotePosition's
|
||||
// `record.PhysicsBody is null → NotApplicable` above.
|
||||
// The only two writers that CLEAR it are entity teardown
|
||||
// (RuntimeEntityObjectLifetime's canonical leave-world
|
||||
// tail) and local-player activation retirement, and each
|
||||
// retires the operation as part of the same transaction —
|
||||
// so a body lost in between surfaces as Cancelled, not
|
||||
// as a pre-engine Rejected.
|
||||
// * !IsStructurallyValid(canonicalRequest) re-tests the
|
||||
// command PrepareMover just validated. The `with` between
|
||||
// them only replaces fields IsStructurallyValid reads
|
||||
// when WakeableLostCell is set, which requires an
|
||||
// inherited park — and TryBeginExclusiveAuthoredPlacement
|
||||
// refuses whenever an operation already exists for the
|
||||
// key, so nothing can be inherited here.
|
||||
// If a future change breaks one of those, the failure lands
|
||||
// on the SAFE side: a pre-engine Rejected filed here does not
|
||||
// store, so the body holds a stale pose for ONE packet (no
|
||||
// operation is retained — CancelToken runs below — so the
|
||||
// next 5-10 Hz packet starts clean). The opposite mistake,
|
||||
// storing after the engine refused, teleports the canonical
|
||||
// body into a destination the sweep just declined. That
|
||||
// asymmetry is why this stays a documented invariant rather
|
||||
// than a stage flag threaded out of Core: the flag would
|
||||
// change the shared placement contract three shipped routes
|
||||
// depend on, to make the already-safe direction slightly
|
||||
// safer.
|
||||
CancelToken(setPosition, token);
|
||||
return RuntimeRemotePlacementExecutionStatus.Rejected;
|
||||
return RuntimeRemotePlacementExecutionStatus.RejectedByPlacement;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R9 review fix — the same self-healing read
|
||||
/// <see cref="CountLiveAwaitingAcknowledgement"/> already performed, for
|
||||
/// <see cref="_pending"/>. Both maps are keyed by
|
||||
/// <see cref="RuntimeEntityKey"/> (guid + incarnation), so a reused GUID
|
||||
/// produces a NEW key and cannot displace a dead incarnation's entry by
|
||||
/// itself. <see cref="Advance"/> drops such entries, but it early-returns
|
||||
/// whenever nothing is pending and is a host cadence call the ledger has
|
||||
/// no right to assume ran; without this the count converged only at
|
||||
/// <see cref="DetachRoute"/>, i.e. never in-session — contract item 7's
|
||||
/// GUID-reuse/incarnation dimension.
|
||||
///
|
||||
/// <para>
|
||||
/// Removal is gated on <c>!IsPlacementCurrent</c>, so this can only ever
|
||||
/// drop entries Core has ALREADY retired. It never cancels anything and
|
||||
/// therefore cannot hide the live-operation leak
|
||||
/// <see cref="DetachRoute"/>'s own review fix exists to prevent. The
|
||||
/// disposal guard matches its sibling's, for the same
|
||||
/// post-<c>Dispose()</c> <c>CaptureOwnership()</c> contract.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private int CountLivePending()
|
||||
{
|
||||
if (_pending.Count == 0)
|
||||
return 0;
|
||||
if (_entityObjects.Physics.IsDisposed)
|
||||
return _pending.Count;
|
||||
|
||||
RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
|
||||
_pendingScratch.Clear();
|
||||
foreach ((RuntimeEntityKey key, Pending entry) in _pending)
|
||||
{
|
||||
if (!setPosition.IsPlacementCurrent(entry.Token))
|
||||
_pendingScratch.Add(key);
|
||||
}
|
||||
foreach (RuntimeEntityKey key in _pendingScratch)
|
||||
_pending.Remove(key);
|
||||
return _pending.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Self-healing read: prunes every <see cref="_awaitingAcknowledgement"/>
|
||||
/// entry whose token Core no longer considers current (the ack already
|
||||
|
|
@ -616,6 +1209,96 @@ internal sealed class RuntimeRemotePlacementDriveController
|
|||
return _awaitingAcknowledgement.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An OPTIMISATION, never the correctness mechanism: can this host place a
|
||||
/// remote into <paramref name="landblockId"/> RIGHT NOW without paying for
|
||||
/// a park that will only be cancelled again?
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Deliberately NOT complete, and it cannot be (C4 route 4b-2, delta
|
||||
/// review MAJOR B/C — correcting this comment's earlier "the complete
|
||||
/// pre-flight" framing).</b> This tests ONE prefix, the destination's.
|
||||
/// Core's own park predicates are broader in two independent ways that no
|
||||
/// pre-flight can reproduce:
|
||||
/// </para>
|
||||
/// <list type="number">
|
||||
/// <item><description><c>PlacementTouchesPrefix</c> also matches the
|
||||
/// request's <c>CurrentCellId</c>. <b>Round 3 — the earlier text called
|
||||
/// that "the SOURCE landblock the remote is snapping OUT of" and "the
|
||||
/// likelier shape"; that is not what production supplies.</b>
|
||||
/// <c>CurrentCellId</c> is read from <c>record.FullCellId</c>, and both
|
||||
/// accepted-Position callers commit the accepted WIRE cell to that field
|
||||
/// before this seam is reached — the graphical remote path through
|
||||
/// <c>LiveEntityRuntime.RebucketLiveEntity</c> →
|
||||
/// <c>RuntimeEntityObjectLifetime.CommitRebucket</c> in its shared
|
||||
/// prologue, route 2 through the merge itself. So on the FIRST submit
|
||||
/// this arm names the DESTINATION, and it is the destination prefix — the
|
||||
/// one this pre-flight DOES see — that it matches. Measured, not argued.
|
||||
/// Scoped deliberately (round 4, D5): that is a property of the first
|
||||
/// submit only. A RETAINED operation re-submits from a cadence pump with
|
||||
/// no fresh merge, so see the <see cref="Advance"/> paragraph below for
|
||||
/// what the arm can name by then — the arm is live, not dead
|
||||
/// code.</description></item>
|
||||
/// <item><description><c>ResultTouchesPrefix</c> scans every
|
||||
/// <c>QueriedCellIds</c> entry, and that sweep footprint provably spans
|
||||
/// NEIGHBOUR landblocks (<c>CellTransit.AddOutsideCell</c> re-derives the
|
||||
/// block id from the global lcoord and states outright that there is no
|
||||
/// same-block filter). The footprint does not EXIST until the sweep has
|
||||
/// run.</description></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// <see cref="Advance"/> re-reads this predicate and is subject to the
|
||||
/// same two gaps, plus a third: a non-Position rebucket
|
||||
/// (<c>RemoteTeleportController</c>, the projection materializer, the
|
||||
/// equipped-child renderer) can move <c>record.FullCellId</c> to a THIRD
|
||||
/// landblock between the retained submit and the retry. All three are
|
||||
/// harmless for the same reason (delta review N3). That reason is the
|
||||
/// paragraph below — NOT, as the round-2 text claimed, that re-reading
|
||||
/// <c>record.CurrentCellId</c> here would "re-derive a private Core
|
||||
/// predicate outside Core": this method already calls Core's own
|
||||
/// <c>internal</c> <see cref="RuntimeSetPositionState.IsCollisionPrefixQuiescing"/>,
|
||||
/// so that objection was simply false (round-3 correction A5).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Correctness lives at the source instead.</b>
|
||||
/// <c>SubmitPreparedPlacementCore</c>'s two quiescence parks are opted
|
||||
/// into <c>ParkDeferred</c>'s rollback, and <c>ParkDeferred</c> then
|
||||
/// restores only when the cell it will actually restore into —
|
||||
/// <c>body.CellPosition.ObjCellId</c>, read AFTER the snap and tested
|
||||
/// against EVERY live quiescence — is not itself quiescing.
|
||||
/// <b>Corrected round 4 (D5):</b> the earlier text said "every shape this
|
||||
/// predicate cannot see is a merely-swept NEIGHBOUR", which contradicts
|
||||
/// the third-landblock rebucket shape named ten lines above. The right
|
||||
/// statement is weaker and does not need the enumeration: whatever the
|
||||
/// blocking prefix turns out to be, the rollback is decided against the
|
||||
/// RESTORE cell rather than against that prefix, so it re-admits nothing
|
||||
/// into any quiescing prefix and AP-136's reason for the blanket
|
||||
/// non-restorable scoping (a spatial root re-entering the prefix that is
|
||||
/// trying to quiesce) cannot apply. This predicate therefore only
|
||||
/// shrinks the reachable set, and the <c>DeferredCell</c> case in
|
||||
/// <see cref="SubmitAndResolve"/> handles whatever still reaches Core.
|
||||
/// Recorded as AP-138(2).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Two other properties DEPEND on this pre-flight staying here
|
||||
/// (round-3 correction A5). Do not delete it as redundant.</b> First,
|
||||
/// N3's "a retained retry can no longer strand" argument: a retry whose
|
||||
/// destination has begun quiescing since the submit is refused HERE and
|
||||
/// takes the <see cref="Advance"/> window-drop <c>store_position</c>,
|
||||
/// rather than reaching Core and opening a park into a prefix whose own
|
||||
/// restore is (correctly) declined. Second, the destination-quiescing
|
||||
/// shape is the one case where <c>ParkDeferred</c> would refuse to
|
||||
/// restore, so refusing ahead of it is what keeps that shape off the
|
||||
/// remote path entirely; without this call it becomes reachable and the
|
||||
/// remote is left withdrawn until a later packet commits.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private bool CanAttemptDestination(
|
||||
RuntimeSetPositionState setPosition,
|
||||
uint landblockId) =>
|
||||
_serviceWindow.IsWithinServiceWindow(landblockId)
|
||||
&& !setPosition.IsCollisionPrefixQuiescing(landblockId);
|
||||
|
||||
private static void CancelToken(
|
||||
RuntimeSetPositionState setPosition,
|
||||
in RuntimeEntityPlacementToken token)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue