Flips the last remote classification (SetPosition: teleport-advanced and
cell-less) onto 4b-1's RuntimeRemotePlacementDriveController, runs retail's
teleport_hook before the placement, and deletes the legacy remote-teleport
machinery. Contract: docs/research/2026-08-04-c4-route-4b-3-contract.md.
Retail: MoveOrTeleport @0x00516330's branch @0x00516386 -> teleport_hook
@0x005163EF -> SetFlags(0x1012) @0x00516414 -> SetPosition @0x00516420 ->
return 1 @0x00516438. The hook @0x00514ED0 runs BEFORE the placement and
regardless of its outcome. Retail places this branch unconditionally, at any
distance and any contact state (arg4 is read only @0x0051638E, after the
branch) — which is what retires AP-137's cell-less enqueue-vs-place delta.
D1 — the classifier's cell-less input is now the PRE-merge committed cell.
Retail's predicate is `this_1->cell == 0`, the BODY's own cell at
MoveOrTeleport entry (this_1 is assigned from this @0x00516334). acdream fed
the POST-merge canonical.FullCellId, which RefreshSnapshot ->
RefreshDerivedState -> SetFullCell has already stamped with the accepted wire
cell; a zero wire cell fails validation into RejectedData first. The shipped
remote cell-less predicate was therefore dead code, not merely different from
remotePlacementRequired. Threaded via a builder overload; route 1's overload
is untouched. The graphical !IsSpatiallyVisible arm of
projectionRequiresTeleportHook is deleted — a presentation predicate with no
retail analogue that fired the teleport machinery on a routine hot path.
Deleted: RemoteTeleportController (605), RemoteTeleportPlacement (85),
RemoteShadowPlacementSynchronizer (49), their 1,709 lines of tests, the
remotePlacementRequired predicate, the TeleportHookRequired plumbing, the
legacy pre-operation ConstrainTo fallback, and the player arm's legacy
!IsGrounded fallback. Net -2,030 lines.
Structural fix (two independent Opus reviews, round 1 FAIL/FAIL): three of the
four MAJORs were one defect — OnPosition carried two parallel inline copies of
the routing tail (player-guid, NPC-guid) that had drifted. Extracted
RunRemoteArmTail (3 call sites) and ApplyWireAirborneLeftoverBookkeeping (2),
both branches now share one implementation.
A1 ToConstraintArm mapped AirborneSnap -> AirborneNoOperation, so the NPC
arm armed ConstrainTo ZERO times for an out-of-contact wire-grounded
creature — a regression this slice introduced while closing a
structurally identical hole. Now maps to NearInterpolate; switch made
total with a throwing default proven unreachable.
R1 D2's write-nothing shape existed on the player arm only; NPC packets
fell through and wrote the body. Retail makes no player/NPC distinction.
R2 report_collision_end(this,1) @0x00514F31 was bound to
ShadowObjects.Suspend, a port of a DIFFERENT retail function
(remove_shadows_from_cells) that teleport_hook never calls. Now routes
to RuntimeCollisionReportingState.LeaveWorld, which wraps the private
ForceEnd in an admission-blocking transaction so a DoCollisionEnd
callback cannot recreate the contact table.
R3/A2 A teleported NPC synthesized ServerVelocity from the teleport distance
(~1,000+ m/s) and planned a run cycle from it. Both the install and
RemoteServerControlledVelocityCycle.Apply now gate on !isTeleportRoute.
BISECT HAZARD — A1's fix is correct only BECAUSE R1 landed. AirborneSnap is
reachable wire-airborne on the NPC arm only while D2's shape is missing there.
Reverting R1 alone silently inverts A1 into the opposite divergence: arming
where retail returns 0. Revert both or neither.
Also in the velocity hunk: the NPC block's two !IsPlayerGuid(update.Guid)
guards were dropped when it was wrapped in `if (!isTeleportRoute)`. Safe — all
five exit paths of the enclosing IsPlayerGuid block return, so the predicate is
unconditionally false below it — but it was unremarked by both reviews.
Register: AP-137 REWRITTEN (not deleted) to the surviving acdream-only
divergences — null classification during the login window and Rejected*
through UnroutedCatchUp keep a row. AD-42's RemoteTeleportController citation
retired; AP-136/AP-138 writer lists corrected to the two surviving non-Position
rebucket writers; AP-138 gains the teleport arm as a second producer of the
visible-without-collision residual (retirement path remains #309). AP-135 is
untouched and its two airborne bookkeeping writes are preserved on both arms.
AP-131 does not retire; #276 does not close.
Proof obligation 1: ParkCollisionResidents' overlap throw stays unreachable —
the teleport arm adds packets to the same TryBeginExclusiveAuthoredPlacement
one-operation-per-key machinery the far arm uses, opens no new operation shape,
and every DeferredCell outcome cancels synchronously with
restoreCancelledPark: true. The guarded property remains
HasOldPrefixPlacementDebt's stall, not a throw (4b-1's B2 caveat stands).
Correction to an earlier claim: LiveEntityPresentationController's
_activePlacementOwners was NOT write-never at HEAD —
remotePlacementRequired -> BeginPlacement -> Begin -> BeginAuthoritativePlacement
was a live writer chain. It becomes write-never BECAUSE this slice deletes that
chain, which is why deleting the dead half is behaviour-preserving.
Probe: ACDREAM_PROBE_REMOTE_TELEPORT=1 emits one [remote-teleport] line per
routed arm (guid, cause, hook-ran, placement status). TEMPORARY, strip with the
probe family.
Carried, disclosed not fixed: no dedicated bidirectional collision-partner test
for R2 (the wiring, not LeaveWorld itself, is what lacks coverage); the
stress test's teleport step drives hand-written field assignments rather than
the canonical arm; the per-packet runTeleportHook closure allocation (network
path, not the resolve path Slice I's 0 B discipline governs — file before
route 5 adds a fourth call site). B2: IRuntimeCollisionReportObserver has zero
production implementations, so retail's bidirectional DoCollisionEnd half still
reaches no gameplay consumer — this fix closes the wrong-function binding, not
that nobody listens.
Complete Release suite MEASURED at 11,013 passed / 4 skipped / 0 failed
(baseline 11,027/4/0; net -14 = ~33 deleted test cases against ~19 added).
Neither known flake fired (#302 PortalProjectionTests GC-allocation, #308
NakEmissionTests wall-clock).
STILL OWED: the two-client connected gate, which MUST use an NPC/creature
teleport target. Both round-1 MAJORs lived on the NPC arm and the velocity
cycle early-returns for 0x50xxxxxx guids, so a player target structurally
cannot observe A1, A2, or R3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
277 lines
13 KiB
C#
277 lines
13 KiB
C#
using System;
|
|
using System.Numerics;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Physics.Motion;
|
|
|
|
namespace AcDream.Runtime.Physics;
|
|
|
|
/// <summary>
|
|
/// C4 route 4a (2026-08-03): the Runtime-owned decision for the two
|
|
/// <see cref="RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition"/>
|
|
/// remote branches that perform NO SetPosition — retail
|
|
/// <c>CPhysicsObj::MoveOrTeleport</c> (0x00516330)'s airborne no-op
|
|
/// (<c>arg4==0</c> -> return 0 @0x0051636D, nothing written at all) and its
|
|
/// near <c>InterpolateTo</c> queue (<c>player_distance < 96 m</c>
|
|
/// @0x005163AF). Both classify identically for player-remote and NPC-remote
|
|
/// 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>.
|
|
///
|
|
/// <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"/>) —
|
|
/// 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
|
|
{
|
|
/// <summary>
|
|
/// AP-87 (register row, carried forward — not retired): retail's
|
|
/// InterpolateTo has no concept of "the body isn't already tracking the
|
|
/// target". acdream's catch-up + per-tick sweep needs one — an unplaced
|
|
/// body (a first-UP / spawn-seed origin, or any large correction)
|
|
/// enqueued instead of snapped would let InterpolationManager's 100 m
|
|
/// far-blip fire and the per-tick sweep run from a cell that does not
|
|
/// contain the body, producing the reverted #184 invisible-but-solid
|
|
/// monster.
|
|
/// </summary>
|
|
private const float BodySnapThreshold = 4f;
|
|
|
|
/// <summary>
|
|
/// Bug B (2026-08-04): the same constant, exposed read-only so the
|
|
/// <c>[remote-slide-up]</c> line can print the threshold its
|
|
/// <c>bodyToTarget</c> is about to be compared against instead of the
|
|
/// reader having to remember it. TEMPORARY — strip with the
|
|
/// <c>ACDREAM_PROBE_REMOTE_SLIDE</c> family.
|
|
/// </summary>
|
|
internal const float DiagnosticBodySnapThreshold = BodySnapThreshold;
|
|
|
|
internal enum Action : byte
|
|
{
|
|
/// <summary>AP-87 backstop: the body wasn't already tracking the
|
|
/// target closely enough, has no consumer to walk the queue, or has
|
|
/// never received a server sample.</summary>
|
|
Snapped,
|
|
|
|
/// <summary>The ordinary near catch-up: queued for the per-tick
|
|
/// InterpolationManager/ConstraintManager chain to walk toward.</summary>
|
|
Enqueued,
|
|
}
|
|
|
|
/// <summary>
|
|
/// True when route 4a owns this classification outright, so the legacy App
|
|
/// path must not run for it AT ALL — not partially, not "just the render
|
|
/// write". This is the staged cutover's ONLY discriminator: the
|
|
/// classification itself, never a heuristic or a flag.
|
|
/// </summary>
|
|
internal static bool OwnsSteadyState(RuntimeAuthoritativePositionRoute? route) =>
|
|
IsAirborneNoOperation(route) || IsNearInterpolate(route);
|
|
|
|
/// <summary>
|
|
/// Retail's <c>arg4 == 0</c> return-0 branch: the accepted wire packet
|
|
/// reports no ground contact, so <c>MoveOrTeleport</c> writes nothing and
|
|
/// its caller <c>SmartBox::HandleReceivedPosition</c> (0x00453FD0) skips
|
|
/// <c>ConstrainTo</c> (@0x00454272, inside
|
|
/// <c>if (MoveOrTeleport(...) != 0)</c>) as well.
|
|
/// </summary>
|
|
internal static bool IsAirborneNoOperation(
|
|
RuntimeAuthoritativePositionRoute? route) =>
|
|
route is
|
|
{
|
|
Disposition: RuntimeAuthoritativePositionDisposition.NoPositionOperation,
|
|
};
|
|
|
|
/// <summary>Retail's <c>player_distance < 96f</c> InterpolateTo
|
|
/// branch.</summary>
|
|
internal static bool IsNearInterpolate(
|
|
RuntimeAuthoritativePositionRoute? route) =>
|
|
route is
|
|
{
|
|
Disposition: RuntimeAuthoritativePositionDisposition.Interpolate,
|
|
};
|
|
|
|
/// <summary>
|
|
/// Applies the retail near-InterpolateTo branch for one remote whose
|
|
/// accepted Position has already classified to
|
|
/// <see cref="RuntimeAuthoritativePositionDisposition.Interpolate"/>.
|
|
/// Callers must NOT invoke this for
|
|
/// <see cref="RuntimeAuthoritativePositionDisposition.NoPositionOperation"/>
|
|
/// — that branch writes nothing at all (retail returns 0) and has no
|
|
/// operation for this method to perform.
|
|
///
|
|
/// <para>
|
|
/// The acdream-only TS-44 sticky suppression is deliberately NOT here: it
|
|
/// is an NPC-only caller gate and stays one, so this seam is exactly the
|
|
/// kind-agnostic retail decision plus AP-87.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// The returned <see cref="Action"/> is this seam's observable outcome and
|
|
/// is what the focused tests assert AP-87's snap against its enqueue with.
|
|
/// Production has no use for it and deliberately discards it at both call
|
|
/// sites — do not delete it as dead, because collapsing it to
|
|
/// <see langword="void"/> would make the two AP-87 outcomes
|
|
/// indistinguishable from outside.
|
|
/// </para>
|
|
/// </summary>
|
|
internal static Action ApplyInterpolate(
|
|
RemoteMotion remote,
|
|
Vector3 worldPosition,
|
|
Quaternion orientation,
|
|
bool isMovingTo,
|
|
bool willBeDrTicked)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(remote);
|
|
|
|
// AP-87, all three conditions, verbatim from the NPC copy this
|
|
// replaces. `firstUp` is a belt hint, not the load-bearing guard: it
|
|
// is unreliable because a UM that enters a locomotion cycle can stamp
|
|
// LastServerPosTime before the first UP. It is retained rather than
|
|
// silently dropped, and is exact for BOTH kinds — the player-remote
|
|
// caller stamps LastServerPosTime before it routes (its diagnostic
|
|
// roll-forward block), so `firstUp` is structurally false there and
|
|
// this evaluates to exactly the player copy's own two conditions.
|
|
bool firstUp = remote.LastServerPosTime <= 0.0;
|
|
float bodyToTarget = Vector3.Distance(remote.Body.Position, worldPosition);
|
|
if (firstUp || !willBeDrTicked || bodyToTarget > BodySnapThreshold)
|
|
{
|
|
// Bug B (2026-08-04) blip producer CANDIDATE 1. Emitted BEFORE the
|
|
// snap so body/queue state is the pre-snap truth the reader needs.
|
|
// docs/research/2026-08-04-bug-b-remote-slide-diagnosis.md §2.
|
|
// Pure read; the GUID comes from the attribution latch the routing
|
|
// seam stamps. TEMPORARY — strip with the probe family.
|
|
if (AcDream.Core.Physics.PhysicsDiagnostics.ShouldLogRemoteSlide(
|
|
AcDream.Core.Physics.PhysicsDiagnostics.RemoteSlideAttributionGuid))
|
|
{
|
|
(int depth, int failCount) =
|
|
remote.Interp.DiagnosticInterpolationState;
|
|
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteSlideBodySnap(
|
|
guid: AcDream.Core.Physics.PhysicsDiagnostics
|
|
.RemoteSlideAttributionGuid,
|
|
firstUp: firstUp,
|
|
willBeDrTicked: willBeDrTicked,
|
|
bodyToTarget: bodyToTarget,
|
|
threshold: BodySnapThreshold,
|
|
bodyPosition: remote.Body.Position,
|
|
targetPosition: worldPosition,
|
|
interpQueueDepth: depth,
|
|
interpFailCount: failCount);
|
|
}
|
|
remote.Interp.Clear();
|
|
remote.Body.Position = worldPosition;
|
|
remote.Body.Orientation = orientation;
|
|
return Action.Snapped;
|
|
}
|
|
|
|
Quaternion? immediate = remote.Interp.Enqueue(
|
|
worldPosition,
|
|
orientation,
|
|
isMovingTo,
|
|
remote.Body.Position,
|
|
remote.Body.Orientation);
|
|
if (immediate is { } close)
|
|
remote.Body.Orientation = close;
|
|
// Bug B (2026-08-04): the NON-blip outcome. Its presence across a
|
|
// slide window is what separates Shape B (queue fed, so the
|
|
// InterpolationManager stall snap can arm) from Shape A (queue never
|
|
// fed at all). TEMPORARY — strip with the probe family.
|
|
if (AcDream.Core.Physics.PhysicsDiagnostics.ShouldLogRemoteSlide(
|
|
AcDream.Core.Physics.PhysicsDiagnostics.RemoteSlideAttributionGuid))
|
|
{
|
|
(int depth, int failCount) =
|
|
remote.Interp.DiagnosticInterpolationState;
|
|
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteSlideEnqueue(
|
|
guid: AcDream.Core.Physics.PhysicsDiagnostics
|
|
.RemoteSlideAttributionGuid,
|
|
bodyToTarget: bodyToTarget,
|
|
targetPosition: worldPosition,
|
|
interpQueueDepth: depth,
|
|
interpFailCount: failCount);
|
|
}
|
|
return Action.Enqueued;
|
|
}
|
|
|
|
/// <summary>
|
|
/// D2/D4: retail arms <c>CPhysicsObj::ConstrainTo</c> strictly AFTER
|
|
/// <c>MoveOrTeleport</c> returns nonzero, anchored to the object's own
|
|
/// CURRENT (i.e. post-move) position — <c>SmartBox::HandleReceivedPosition</c>
|
|
/// 0x00453FD0 reads <c>&arg2->m_position</c> at 0x00454272, inside
|
|
/// the <c>if (MoveOrTeleport(...) != 0)</c> at 0x00454254. It therefore
|
|
/// does NOT run on the airborne no-op.
|
|
///
|
|
/// <para>
|
|
/// C4 route 4b-3 (D4): this is now the ONLY arming site — the legacy
|
|
/// pre-operation call is deleted, matching retail's single
|
|
/// @0x00454272. <paramref name="arm"/> is the routing outcome (which arm
|
|
/// actually claimed the packet), NOT the raw classification, because
|
|
/// that is the only input that correctly distinguishes a GROUNDED
|
|
/// <see cref="RuntimeRemoteAcceptedPositionArm.UnroutedCatchUp"/> (arms —
|
|
/// retail has no state here, but the analogue of "MoveOrTeleport returned
|
|
/// nonzero" is true) from the wire-airborne leftover shape (D2's
|
|
/// return-0 replacement, which never reaches this call at all — see the
|
|
/// caller). The complete partition:
|
|
/// </para>
|
|
/// <list type="bullet">
|
|
/// <item><description><see cref="RuntimeRemoteAcceptedPositionArm.TeleportPlacement"/>
|
|
/// — arms on EVERY placement outcome (retail discards
|
|
/// <c>SetPosition</c>'s error and returns 1 unconditionally
|
|
/// @0x00516438).</description></item>
|
|
/// <item><description><see cref="RuntimeRemoteAcceptedPositionArm.FarSnapPlacement"/>
|
|
/// — arms unconditionally, same reason
|
|
/// (@0x005163E8).</description></item>
|
|
/// <item><description><see cref="RuntimeRemoteAcceptedPositionArm.NearInterpolate"/>
|
|
/// — arms; retail's InterpolateTo branch returns 1
|
|
/// (@0x005163BE).</description></item>
|
|
/// <item><description><see cref="RuntimeRemoteAcceptedPositionArm.UnroutedCatchUp"/>
|
|
/// — arms; only reachable here when the body is already known to be in
|
|
/// contact (the caller's free-flight carve-out already
|
|
/// returned).</description></item>
|
|
/// <item><description><see cref="RuntimeRemoteAcceptedPositionArm.AirborneNoOperation"/>
|
|
/// — never arms; retail's <c>arg4 == 0</c> branch returns 0
|
|
/// (@0x0051636D). Both production callers already early-return on this
|
|
/// classification before reaching any arming call, so this case is
|
|
/// defensive.</description></item>
|
|
/// </list>
|
|
/// </summary>
|
|
internal static bool TryArmConstraintAfterOperation(
|
|
RuntimeRemoteAcceptedPositionArm arm,
|
|
RemoteMotion remote)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(remote);
|
|
bool arms = arm switch
|
|
{
|
|
RuntimeRemoteAcceptedPositionArm.TeleportPlacement => true,
|
|
RuntimeRemoteAcceptedPositionArm.FarSnapPlacement => true,
|
|
RuntimeRemoteAcceptedPositionArm.NearInterpolate => true,
|
|
RuntimeRemoteAcceptedPositionArm.UnroutedCatchUp => true,
|
|
RuntimeRemoteAcceptedPositionArm.AirborneNoOperation => false,
|
|
_ => false,
|
|
};
|
|
if (!arms || remote.Host is not { } host)
|
|
return false;
|
|
|
|
ArmConstraintAfterOperation(host);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The leash arming itself: <c>ConstraintPosOffset</c> is captured as
|
|
/// distance(anchor, host.Position) at call time, and the anchor here IS
|
|
/// host.Position read live, so a fresh accepted Position always restarts
|
|
/// the leash at zero displacement — retail's per-packet re-anchor.
|
|
/// docs/research/2026-07-30-constraint-leash-constants.md §2/§3.2.
|
|
/// </summary>
|
|
internal static void ArmConstraintAfterOperation(EntityPhysicsHost host)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(host);
|
|
Position anchor = host.Position;
|
|
host.PositionManager.ConstrainTo(
|
|
anchor,
|
|
ConstraintDistance.GetStartConstraintDistance(anchor.ObjCellId),
|
|
ConstraintDistance.GetMaxConstraintDistance(anchor.ObjCellId));
|
|
}
|
|
}
|