fix(physics): remote bodies slide on steep faces instead of freezing (#32)
A remote observed in acdream landed on a sloped roof and froze; the server slid
on, the gap passed AP-87's 4 m threshold, and the body snapped — the visible
blip. Live probe capture, two adjacent ticks 63 ms apart:
t=88420671 rsInContact=True rsOnWalkable=False rsIsOnGround=True
bodyCpNz=0.6097 floorZ=0.6642 steep=True gravity=True
vel=(2.146,2.264,-3.549)
t=88420734 contact=True onWalkable=True <- forced against the sweep
gravity=False <- cleared
velBeforeZero=(2.146,2.264,0.000)
moved=0.0000 <- and every tick after
The roof is 52.4 degrees against a 48.4 degree limit, so acdream's classifier
was CORRECT and was then overruled. Four independent links each froze the body
on their own: a per-tick force of Contact|OnWalkable, a per-tick velocity zero,
a Gravity clear at landing, and a landing edge testing IsOnGround
(= inContact || ...) instead of OnWalkable. The tick called
HandleAllCollisions alone — the tail of SetPositionInternal without its prefix.
Retail simulates remotes locally and derives these bits rather than asserting
them: CPhysics::UseTime @0x00509950 iterates the whole object table;
update_object @0x00515D10 gates only on parent/cell/FROZEN with no
is_player fork; SetPositionInternal @0x00515330 sets CONTACT from
contact_plane_valid @0x00515430 and ON_WALKABLE from contact_plane.N.z vs
floor_z @0x00515465-@0x0051548E before handle_all_collisions @0x005154FE;
set_on_walkable @0x00511310 fires HitGround @0x00511364 / LeaveGround
@0x00511346 edge-triggered with no ownership gate; calc_acceleration
@0x00510950 zeroes only when CONTACT && ON_WALKABLE && !Sledding @0x0051096B;
calc_friction @0x0050EE70 returns at its first line when ON_WALKABLE is clear.
acdream had copied retail's airborne no-op WITHOUT retail's local simulation.
The fix is mostly deletion: stop forging the transients, stop discarding the
authoritative velocity, stop clearing Gravity, and route the remote tick
through the same SetPositionInternal commit TickHidden and the local player
already use, with the landing edge derived from the sweep's own OnWalkable.
AP-87's threshold and conditions and InterpolationManager's node_fail_counter
snap-to-tail are deliberately untouched — this removes the CAUSE of the
divergence rather than weakening the backstop.
Cross-checked against ACE: its only creature-side VectorUpdate emitters are the
jump broadcast and spell projectiles, so integrating the wire velocity cannot
double-move a walking remote; and PhysicsGlobals.DefaultState already carries
Gravity, so deleting the manufactured State |= Gravity is safe.
Register: AP-81 narrowed (its GRAVITY half retired outright), AP-87 annotated,
AP-139 filed (the interpolation-queue clear on the landing edge), AP-140 filed
(the two routing gates select snap-vs-interpolate on walkability where retail
uses CONTACT — adjust_offset @0x00555D30 gates on transient_state & 1
@0x00555D52). AP-140's follow-up is deliberately shaped as "point the two gates
at Body.InContact", NOT "re-derive Airborne", which would perturb five writers
and collide with a pinned RemoteTeleportPlacementTests assertion.
Three gaps recorded in #32 rather than papered over: the new LeaveGround
dispatch is untested for chatter; a persistently !Ok transition can latch a
remote airborne; and — the visual-gate watch item — the deleted forge was a
blanket guarantee of Contact|OnWalkable, and contact_allows_move @0x00528dd0
silently refuses action animations without both, which is the literal root
cause of closed #270. Retail-correct on a steep face, a regression anywhere
else.
10 discriminating tests over a real PhysicsEngine landblock whose contact
normal Z is 0.61 against FloorZ 0.6642 — the live roof's exact relationship.
Suite 11,019 passed / 4 skipped / 0 failed. Includes the temporary
ACDREAM_PROBE_REMOTE_LANDING / ACDREAM_PROBE_REMOTE_SLIDE probe family that
produced the capture above; strip with the family.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f058dfc9f9
commit
204d0ae047
11 changed files with 3103 additions and 245 deletions
|
|
@ -198,13 +198,25 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
// Retail's spawn contact comes from the FIRST GRAVITY FRAME, not the
|
||||
// placement itself: every retail CPhysicsObj simulates, so a freshly
|
||||
// placed creature falls the few centimetres onto the floor and the
|
||||
// transition's touch grants the contact plane. Our stationary remotes
|
||||
// never run a physics frame (the DR tick resolves only movers), so
|
||||
// the settle is compressed here: a short downward sweep from the
|
||||
// server position. Its touch handler produces exactly the state
|
||||
// retail's first frame would (position snapped onto the floor,
|
||||
// contact plane + CONTACT/ON_WALKABLE committed below). A sweep that
|
||||
// finds no floor (true airborne spawn) leaves the body airborne.
|
||||
// transition's touch grants the contact plane. Our remotes reach that
|
||||
// state SLOWLY or not at all — the DR tick only sweeps when the
|
||||
// composed candidate actually moved, so a remote spawned exactly on
|
||||
// its floor never sweeps and a remote spawned above one needs however
|
||||
// many ticks gravity takes to close the gap. The settle is therefore
|
||||
// compressed here: a short downward sweep from the server position.
|
||||
// Its touch handler produces exactly the state retail's first frame
|
||||
// would (position snapped onto the floor, contact plane +
|
||||
// CONTACT/ON_WALKABLE committed below). A sweep that finds no floor
|
||||
// (true airborne spawn) leaves the body airborne.
|
||||
//
|
||||
// Bug B (2026-08-04) weakened — but did not remove — the reason this
|
||||
// exists. The deleted per-tick `Contact | OnWalkable` forge used to
|
||||
// make a stationary remote's transients permanent, so a contact-free
|
||||
// remote could NEVER settle on its own; now gravity survives and one
|
||||
// WILL settle by itself after a few ticks of falling. This compressed
|
||||
// settle is what keeps it from spending those ticks visibly
|
||||
// contact-free, which is the #270 window (`contact_allows_move`
|
||||
// @0x00528dd0 refuses action animations without both transients).
|
||||
if (!AcDream.Core.Physics.SpawnPlacementSettler.TrySettle(
|
||||
_physicsEngine,
|
||||
remote.Body,
|
||||
|
|
@ -1028,21 +1040,38 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
ArgumentNullException.ThrowIfNull(placementDrive);
|
||||
ArgumentNullException.ThrowIfNull(canonical);
|
||||
ArgumentNullException.ThrowIfNull(remote);
|
||||
// Bug B (2026-08-04): stamp the GUID that any [remote-slide-*] line
|
||||
// emitted from inside this synchronous routing window belongs to —
|
||||
// ApplyInterpolate (blip producer Candidate 1) has no GUID of its own.
|
||||
// TEMPORARY — strip with the ACDREAM_PROBE_REMOTE_SLIDE family.
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.BeginRemoteSlideAttribution(
|
||||
canonical.ServerGuid);
|
||||
if (remote.Airborne)
|
||||
{
|
||||
// Verbatim from the pre-4a branch, queue deliberately NOT
|
||||
// cleared: the arc integrates locally (K-fix15), and clearing
|
||||
// stale waypoints is owned by the per-tick LANDING detection
|
||||
// (RuntimeRemotePhysicsUpdater.cs:497-502), not by this snap.
|
||||
// stale waypoints is owned by the per-tick LANDING detection —
|
||||
// the `!previousOnWalkable && finalOnWalkable` arm of
|
||||
// RuntimeRemotePhysicsUpdater.Tick's SetPositionInternal commit,
|
||||
// whose `rm.Interp.Clear()` is register row AP-139 — not by this
|
||||
// snap. Cited by SYMBOL on purpose: the same reference was a line
|
||||
// range twice and went stale both times, once within a single
|
||||
// review round.
|
||||
//
|
||||
// Do NOT restate this as "the queue is already empty here" — it
|
||||
// is not. Nothing that sets Airborne clears the queue except the
|
||||
// teleport hook's StopInterpolating: neither the 0xF74E
|
||||
// VectorUpdate (:1059) nor the three `Airborne = !Body.OnWalkable`
|
||||
// sites do. A walking NPC can enqueue a near waypoint and then
|
||||
// step off a lip, arriving here with a populated queue. That is
|
||||
// exactly why the landing clear exists, and a reader who believes
|
||||
// the queue is empty here could delete it.
|
||||
// VectorUpdate (OnVector, below) nor any of the five
|
||||
// `Airborne = !Body.OnWalkable` sites do. Those five are
|
||||
// SettleSpawnedRemoteContact (this file),
|
||||
// RemoteTeleportPlacement.Apply,
|
||||
// RuntimeSetPositionState's canonical placement commit, and
|
||||
// RuntimeRemotePhysicsUpdater's two — the SetPositionInternal
|
||||
// commit in Tick and the TickHidden resolve. A walking NPC can
|
||||
// enqueue a near waypoint and then step off a lip, arriving here
|
||||
// with a populated queue. That is exactly why the landing clear
|
||||
// exists, and a reader who believes the queue is empty here could
|
||||
// delete it.
|
||||
remote.Body.Position = worldPos;
|
||||
remote.Body.Orientation = rotation;
|
||||
return new RemoteContactRouting(
|
||||
|
|
@ -1257,6 +1286,31 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
return;
|
||||
}
|
||||
|
||||
// Bug B (2026-08-04) — [remote-slide-vec]. NOT ESTABLISHED #4 asks
|
||||
// whether ACE relays a 0xF74E at all while a sender slides; the
|
||||
// ABSENCE of these lines across a captured slide window is the
|
||||
// answer, so this sits on the committed path rather than inside the
|
||||
// +Z airborne branch below (a downhill slide has Velocity.Z < 0 and
|
||||
// would never reach it). willMarkAirborne restates that branch's own
|
||||
// test so the log states the outcome rather than making the reader
|
||||
// re-derive it. Pure reads. TEMPORARY — strip with the probe family.
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ShouldLogRemoteSlide(
|
||||
update.Guid))
|
||||
{
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteSlideVector(
|
||||
guid: update.Guid,
|
||||
wireVelocity: update.Velocity,
|
||||
wireOmega: update.Omega,
|
||||
willMarkAirborne: update.Velocity.Z > 0.5f,
|
||||
airborneBefore: rm.Airborne,
|
||||
contact: rm.Body.InContact,
|
||||
onWalkable: rm.Body.OnWalkable,
|
||||
gravity: rm.Body.HasGravity,
|
||||
bodyVelocity: rm.Body.Velocity,
|
||||
contactPlaneValid: rm.Body.ContactPlaneValid,
|
||||
contactPlaneNormalZ: rm.Body.ContactPlane.Normal.Z);
|
||||
}
|
||||
|
||||
// Mark airborne when the launch has meaningful +Z. Threshold
|
||||
// 0.5 m/s rejects noise / horizontal-only updates (server might
|
||||
// also use VectorUpdate for non-jump events). The per-tick
|
||||
|
|
@ -1265,12 +1319,26 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
if (update.Velocity.Z > 0.5f)
|
||||
{
|
||||
rm.Airborne = true;
|
||||
// Clear ground-contact bits + enable gravity so calc_acceleration
|
||||
// returns (0, 0, -9.8) instead of zero. UpdatePhysicsInternal then
|
||||
// produces the parabolic arc.
|
||||
// Clear the ground-contact transients so calc_acceleration
|
||||
// (0x00510950) releases gravity and UpdatePhysicsInternal produces
|
||||
// the parabolic arc. Retail reaches the same state one frame later
|
||||
// through check_contact (0x0050F5B0) failing on the ascending
|
||||
// velocity; clearing them here is the AP-81 head start, and it is
|
||||
// what keeps the per-tick `set_on_walkable` edge from ALSO firing
|
||||
// LeaveGround for this same departure.
|
||||
//
|
||||
// Bug B (2026-08-04): the `State |= Gravity` that used to follow is
|
||||
// DELETED. GRAVITY_PS is a persistent object property owned by the
|
||||
// wire — retail's CPhysicsObj constructor seeds it (state 0x400C08
|
||||
// @0x00512508) and set_description's set_state (0x00514DD0) assigns
|
||||
// the description's state wholesale without ever masking it. Now
|
||||
// that neither landing block clears the bit, manufacturing it here
|
||||
// would be the only remaining non-retail gravity write, and it
|
||||
// would mask a server that genuinely sent a gravity-free state.
|
||||
// ACE agrees: PhysicsGlobals.DefaultState and the player login
|
||||
// state both carry PhysicsState.Gravity.
|
||||
rm.Body.TransientState &= ~(AcDream.Core.Physics.TransientStateFlags.Contact
|
||||
| AcDream.Core.Physics.TransientStateFlags.OnWalkable);
|
||||
rm.Body.State |= AcDream.Core.Physics.PhysicsStateFlags.Gravity;
|
||||
|
||||
// R3-W4 (J19 — K-fix10/K-fix18 DELETED): the retail mechanism.
|
||||
// The remote's ground departure fires LeaveGround (0x00528b00):
|
||||
|
|
@ -1924,6 +1992,53 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
remoteConstraintHost);
|
||||
}
|
||||
|
||||
// Bug B (2026-08-04) — [remote-slide-up]. This is the ONE point
|
||||
// both remote arms pass through, and it deliberately sits AHEAD of
|
||||
// the two IsAirborneNoOperation early returns below: in the
|
||||
// diagnosis's Shape A (ACE reports IsGrounded == false for the
|
||||
// whole slide) acdream writes nothing at all, so a line emitted
|
||||
// after those returns would leave the entire slide window blank
|
||||
// and NOT ESTABLISHED #1 unanswerable. `wireGrounded` is the raw
|
||||
// ACE PositionFlags.IsGrounded bit for this packet.
|
||||
// docs/research/2026-08-04-bug-b-remote-slide-diagnosis.md §2.
|
||||
// Pure reads. TEMPORARY — strip with the probe family.
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ShouldLogRemoteSlide(
|
||||
update.Guid))
|
||||
{
|
||||
(int slideQueueDepth, int slideFailCount) =
|
||||
rmState.Interp.DiagnosticInterpolationState;
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteSlideUp(
|
||||
guid: update.Guid,
|
||||
wireGrounded: update.IsGrounded,
|
||||
wireVelocity: update.Velocity,
|
||||
disposition: earlyRemoteRoute is { } slideRoute
|
||||
? slideRoute.Disposition.ToString()
|
||||
: "unclassified",
|
||||
playerDistance: _playerController is { } slideController
|
||||
? System.Numerics.Vector3.Distance(
|
||||
worldPos,
|
||||
slideController.Position)
|
||||
: null,
|
||||
bodyToTarget: System.Numerics.Vector3.Distance(
|
||||
rmState.Body.Position,
|
||||
worldPos),
|
||||
bodySnapThreshold:
|
||||
RuntimeRemoteSteadyStatePosition.DiagnosticBodySnapThreshold,
|
||||
willBeDrTicked: WillAdvanceRemoteMotion(update.Guid, rmState),
|
||||
firstUp: rmState.LastServerPosTime <= 0.0,
|
||||
airborne: rmState.Airborne,
|
||||
contact: rmState.Body.InContact,
|
||||
onWalkable: rmState.Body.OnWalkable,
|
||||
gravity: rmState.Body.HasGravity,
|
||||
bodyVelocity: rmState.Body.Velocity,
|
||||
contactPlaneValid: rmState.Body.ContactPlaneValid,
|
||||
contactPlaneNormalZ: rmState.Body.ContactPlane.Normal.Z,
|
||||
wirePosition: worldPos,
|
||||
bodyPosition: rmState.Body.Position,
|
||||
interpQueueDepth: slideQueueDepth,
|
||||
interpFailCount: slideFailCount);
|
||||
}
|
||||
|
||||
// L.3 M2 (2026-05-05): retail-faithful MoveOrTeleport routing for
|
||||
// player remotes. Mirrors CPhysicsObj::MoveOrTeleport
|
||||
// (acclient @ 0x00516330) — airborne no-op, far-snap, near
|
||||
|
|
@ -2013,15 +2128,36 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
}
|
||||
|
||||
// ── LANDING TRANSITION ────────────────────────────────────────
|
||||
// First IsGrounded=true UP after rmState.Airborne signals landed.
|
||||
// Clear airborne flags, hard-snap to authoritative landing position,
|
||||
// clear interpolation queue (any pre-jump waypoints are stale).
|
||||
// First IsGrounded=true UP while the client still considers the
|
||||
// body airborne (`!Body.OnWalkable`, now derived by the per-tick
|
||||
// SetPositionInternal commit rather than latched here).
|
||||
// Hard-snap to the authoritative landing position and clear the
|
||||
// interpolation queue (an airborne remote's Positions hard-snap
|
||||
// and never enqueue, so any pre-arc waypoints are stale).
|
||||
// `rmState.Airborne` is deliberately NOT cleared here: the next
|
||||
// tick derives it from the sweep, which is the only thing that
|
||||
// can tell walkable ground from a steep face.
|
||||
//
|
||||
// Bug B (2026-08-04) — the twin of the per-tick forge. This
|
||||
// block used to additionally zero the body velocity, assert
|
||||
// `Contact | OnWalkable`, invoke MovementManager::HitGround, and
|
||||
// clear the Gravity STATE bit. All four are deleted:
|
||||
// • the velocity zero discarded the authoritative vector ACE
|
||||
// delivered (retail MoveOrTeleport 0x00516330 never reads or
|
||||
// writes the wire velocity for a remote at all);
|
||||
// • the transient assert forged the two facts retail derives
|
||||
// from the contact plane in SetPositionInternal
|
||||
// (0x00515430 / 0x00515465-0x0051548E) — on a steep roof it
|
||||
// declared a non-walkable surface walkable;
|
||||
// • HitGround has exactly ONE retail source,
|
||||
// `set_on_walkable(1)` @0x00511358, which the per-tick
|
||||
// SetPositionInternal commit now owns. Firing it from here
|
||||
// as well would double-dispatch the landing re-apply;
|
||||
// • retail never toggles GRAVITY_PS on a ground edge — see the
|
||||
// per-tick commit's comment.
|
||||
// What remains is AP-87's acdream-only snap, unchanged.
|
||||
if (rmState.Airborne)
|
||||
{
|
||||
rmState.Airborne = false;
|
||||
rmState.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||
rmState.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
|
||||
| AcDream.Core.Physics.TransientStateFlags.OnWalkable;
|
||||
rmState.Interp.Clear();
|
||||
rmState.Body.Position = worldPos;
|
||||
rmState.Body.Orientation = rot;
|
||||
|
|
@ -2052,16 +2188,10 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
entity.ParentCellId = rmState.CellId;
|
||||
entity.Rotation = rmState.Body.Orientation;
|
||||
|
||||
// #161: retail landing = MovementManager::HitGround
|
||||
// (minterp → moveto, 0x00524300 — the R5-V5 facade
|
||||
// relay) with the Gravity state bit STILL SET
|
||||
// (CMotionInterp::HitGround gates on state&0x400). The
|
||||
// re-apply dispatches the PRESERVED pre-fall forward
|
||||
// command → landing link → cycle. This replaces the
|
||||
// forced SetCycle, which read the then-clobbered
|
||||
// ForwardCommand (Falling) and re-set the pose it meant
|
||||
// to clear. See the twin block in TickAnimations
|
||||
// (VU.land).
|
||||
// The motion bindings still have to exist before the next
|
||||
// per-tick commit can dispatch this remote's ground edge.
|
||||
// Only the HitGround CALL moved (see the block comment);
|
||||
// binding is the packet's own responsibility.
|
||||
if (_animatedEntities.TryGetValue(entity.Id, out var aeForLand)
|
||||
&& aeForLand.Sequencer is not null)
|
||||
{
|
||||
|
|
@ -2069,11 +2199,14 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
}
|
||||
|
||||
// Bug A investigation (2026-08-04, docs/ISSUES.md #32):
|
||||
// capture the exact state HitGround is about to act on —
|
||||
// see PhysicsDiagnostics.LogRemoteLanding for the field
|
||||
// list and PhysicsDiagnostics.ProbeRemoteLandingEnabled
|
||||
// for the discriminator table. TEMPORARY — strip once
|
||||
// the live-test run has landed.
|
||||
// the packet-side half of the landing capture. Bug B moved
|
||||
// the HitGround call itself onto the per-tick
|
||||
// `set_on_walkable` edge, so `hitGroundInvoked` is now
|
||||
// false here and the "per-tick" pair is the one that
|
||||
// reports the dispatch. This line still records the exact
|
||||
// state the authoritative landing snap installed, which is
|
||||
// what the discriminator table reads it for.
|
||||
// TEMPORARY — strip once the live-test run has landed.
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeRemoteLandingEnabled)
|
||||
{
|
||||
bool gravitySetForProbe = rmState.Body.HasGravity;
|
||||
|
|
@ -2093,26 +2226,23 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLandingGateNoOp(
|
||||
"controller", update.Guid);
|
||||
}
|
||||
}
|
||||
|
||||
ulong landingStateAuthorityVersion =
|
||||
positionRecord.StateAuthorityVersion;
|
||||
rmState.Movement.HitGround();
|
||||
if (!IsCurrentPositionOwner(entity)
|
||||
|| !ReferenceEquals(
|
||||
positionRecord.RemoteMotionRuntime,
|
||||
rmState))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// DR bookkeeping only (partner of the jump-start
|
||||
// `State |= Gravity`).
|
||||
if (_liveEntities.IsCurrentStateAuthority(
|
||||
positionRecord,
|
||||
landingStateAuthorityVersion))
|
||||
{
|
||||
rmState.Body.State &=
|
||||
~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
|
||||
// Zero the sink-dispatch latches before reading them
|
||||
// back. Nothing at THIS site dispatches — the arming
|
||||
// call lives only next to the per-tick HitGround — so
|
||||
// without this the line below would print
|
||||
// sinkApplyCalls/sinkLastMotion/sinkLastResult left
|
||||
// over from a previous per-tick capture on this
|
||||
// thread and invite a reader to attribute them to the
|
||||
// packet. Zeros are the honest report here.
|
||||
AcDream.Core.Physics.PhysicsDiagnostics
|
||||
.BeginRemoteLandingDispatchCapture();
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLandingAfter(
|
||||
site: "controller",
|
||||
guid: update.Guid,
|
||||
hitGroundInvoked: false,
|
||||
sequencerStyle: aeForLand?.Sequencer?.CurrentStyle ?? 0,
|
||||
sequencerMotion: aeForLand?.Sequencer?.CurrentMotion ?? 0,
|
||||
forwardCommand: rmState.Motion.InterpretedState.ForwardCommand);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,18 @@ public sealed class InterpolationManager
|
|||
/// <summary>Current waypoint count (visible to tests for cap verification).</summary>
|
||||
internal int Count => _queue.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Bug B (2026-08-04) read-only diagnostic view for the
|
||||
/// <c>ACDREAM_PROBE_REMOTE_SLIDE</c> family. The queue depth plus the
|
||||
/// live <c>node_fail_counter</c> is what lets a reader see blip producer
|
||||
/// Candidate 2 ARMING (fail count climbing toward
|
||||
/// <see cref="StallFailCountThreshold"/>) from the per-packet
|
||||
/// <c>[remote-slide-up]</c> line, before it fires. Pure read; no
|
||||
/// production consumer. TEMPORARY — strip with the probe family.
|
||||
/// </summary>
|
||||
public (int Depth, int FailCount) DiagnosticInterpolationState
|
||||
=> (_queue.Count, _failCount);
|
||||
|
||||
/// <summary>
|
||||
/// Stop interpolating: drain queue and reset all stall state to sentinel
|
||||
/// values. Retail StopInterpolating (@ 0x00555950).
|
||||
|
|
@ -459,6 +471,22 @@ public sealed class InterpolationManager
|
|||
{
|
||||
InterpolationNode tail = _queue.Last!.Value;
|
||||
Vector3 tailDelta = tail.TargetPosition - currentBodyPosition;
|
||||
// Bug B (2026-08-04) blip producer CANDIDATE 2 — observation only.
|
||||
// docs/research/2026-08-04-bug-b-remote-slide-diagnosis.md §2
|
||||
// establishes this snap as a FAITHFUL port of retail
|
||||
// InterpolationManager::UseTime @0x00555f20 firing correctly on a
|
||||
// body frozen upstream, and rules it explicitly out of scope for
|
||||
// any fix. The call reads only values already computed on this
|
||||
// line and is self-guarded on ProbeRemoteSlideEnabled, so it
|
||||
// changes neither the branch nor its result. TEMPORARY — strip
|
||||
// with the ACDREAM_PROBE_REMOTE_SLIDE family.
|
||||
PhysicsDiagnostics.LogRemoteSlideStallSnap(
|
||||
failCount: _failCount,
|
||||
threshold: StallFailCountThreshold,
|
||||
queueDepth: _queue.Count,
|
||||
bodyPosition: currentBodyPosition,
|
||||
tailPosition: tail.TargetPosition,
|
||||
distanceToHead: dist);
|
||||
Clear();
|
||||
return new InterpolationStep(
|
||||
true,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ public sealed class MotionTableDispatchSink : IInterpretedMotionSink
|
|||
public bool ApplyMotion(uint motion, float speed)
|
||||
{
|
||||
uint result = _sequencer.PerformMovement(MotionTableMovement.Interpreted(motion, speed));
|
||||
// Bug A probe ([remote-landing-after], ACDREAM_PROBE_REMOTE_LANDING):
|
||||
// the MotionTableManagerError code is discarded by this bool return,
|
||||
// so hand it to the diagnostic latch before it is lost. Self-guarded
|
||||
// — one flag test when the probe is off, no behaviour change either
|
||||
// way. TEMPORARY, strips with the rest of the probe family.
|
||||
PhysicsDiagnostics.RecordRemoteLandingDispatch(motion, result);
|
||||
return result == MotionTableManagerError.Success;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -264,6 +264,498 @@ public static class PhysicsDiagnostics
|
|||
$"[remote-landing-gate] site={site} guid=0x{guid:X8} t={Environment.TickCount64} NOOP gravityAlreadyClear=true"));
|
||||
}
|
||||
|
||||
// ── [remote-landing-after] — the OUTCOME half of the Bug A probe ──────
|
||||
//
|
||||
// docs/research/2026-08-04-bug-a-h3-scheduler-diagnosis.md §6.1: the
|
||||
// [remote-landing] line above reads state immediately BEFORE
|
||||
// MovementManager.HitGround, so it cannot separate (a) the edge never
|
||||
// firing, from (b) HitGround firing and something re-asserting Falling,
|
||||
// from (c) the motion-table sink refusing the cycle. The companion line
|
||||
// below reads the same entity immediately AFTER the call, at the same
|
||||
// two sites, and pairs 1:1 with it (same site + guid, next line for
|
||||
// that guid).
|
||||
//
|
||||
// Dispatch capture: MotionTableDispatchSink.ApplyMotion discards the
|
||||
// MotionTableManagerError code (it returns bool) and HitGround itself
|
||||
// returns void, so nothing at the call site can observe what the sink
|
||||
// did. These [ThreadStatic] latches carry it across the synchronous
|
||||
// HitGround call without changing any signature: the call site calls
|
||||
// BeginRemoteLandingDispatchCapture() right before HitGround, the sink
|
||||
// records each ApplyMotion, and LogRemoteLandingAfter reports the count
|
||||
// plus the LAST ApplyMotion — which for the landing re-apply
|
||||
// (ApplyInterpretedMovement, MotionInterpreter.cs:2842-2903) is the
|
||||
// decisive one: either Falling (:2867) or InterpretedState.ForwardCommand
|
||||
// (:2878). Thread-static because the whole window is synchronous on the
|
||||
// ticking thread, and headless hosts tick several sessions in parallel.
|
||||
//
|
||||
// Every member here is inert unless ProbeRemoteLandingEnabled is true.
|
||||
// TEMPORARY — strip with the rest of the ACDREAM_PROBE_REMOTE_LANDING
|
||||
// family once the discriminating live capture has landed.
|
||||
|
||||
[ThreadStatic] private static int _remoteLandingApplyCalls;
|
||||
[ThreadStatic] private static uint _remoteLandingLastApplyMotion;
|
||||
[ThreadStatic] private static uint _remoteLandingLastApplyResult;
|
||||
|
||||
/// <summary>
|
||||
/// Arm the per-call sink-dispatch capture read back by
|
||||
/// <see cref="LogRemoteLandingAfter"/>. Call immediately before
|
||||
/// <c>MovementManager.HitGround</c>. No-op unless
|
||||
/// <see cref="ProbeRemoteLandingEnabled"/>.
|
||||
/// </summary>
|
||||
public static void BeginRemoteLandingDispatchCapture()
|
||||
{
|
||||
if (!ProbeRemoteLandingEnabled) return;
|
||||
_remoteLandingApplyCalls = 0;
|
||||
_remoteLandingLastApplyMotion = 0;
|
||||
_remoteLandingLastApplyResult = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record one <c>IInterpretedMotionSink.ApplyMotion</c> dispatch and its
|
||||
/// raw <c>MotionTableManagerError</c> code. Called by
|
||||
/// <see cref="Motion.MotionTableDispatchSink"/>; self-guarded, so it is
|
||||
/// a single flag test when the probe is off.
|
||||
/// </summary>
|
||||
public static void RecordRemoteLandingDispatch(uint motion, uint result)
|
||||
{
|
||||
if (!ProbeRemoteLandingEnabled) return;
|
||||
_remoteLandingApplyCalls++;
|
||||
_remoteLandingLastApplyMotion = motion;
|
||||
_remoteLandingLastApplyResult = result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emit one <c>[remote-landing-after]</c> line for the landing edge whose
|
||||
/// <c>[remote-landing]</c> line was just written. Caller MUST guard with
|
||||
/// <c>if (!ProbeRemoteLandingEnabled) return;</c> before calling, and MUST
|
||||
/// emit it before any post-HitGround ownership re-check can return — a
|
||||
/// before-line with no after-line therefore means the call site threw.
|
||||
/// <paramref name="hitGroundInvoked"/> is <see langword="false"/> if a
|
||||
/// gate short-circuited between the two lines (no such gate exists at
|
||||
/// either site today; the field exists so the absence is stated rather
|
||||
/// than inferred from a missing line).
|
||||
/// </summary>
|
||||
public static void LogRemoteLandingAfter(
|
||||
string site,
|
||||
uint guid,
|
||||
bool hitGroundInvoked,
|
||||
uint sequencerStyle,
|
||||
uint sequencerMotion,
|
||||
uint forwardCommand)
|
||||
{
|
||||
var ci = System.Globalization.CultureInfo.InvariantCulture;
|
||||
Console.WriteLine(string.Format(ci,
|
||||
"[remote-landing-after] site={0} guid=0x{1:X8} t={2} " +
|
||||
"hitGroundInvoked={3} seqStyle=0x{4:X8} seqMotion=0x{5:X8} " +
|
||||
"fwdCmd=0x{6:X8} sinkApplyCalls={7} sinkLastMotion=0x{8:X8} " +
|
||||
"sinkLastResult=0x{9:X8}",
|
||||
site, guid, Environment.TickCount64,
|
||||
hitGroundInvoked, sequencerStyle, sequencerMotion,
|
||||
forwardCommand, _remoteLandingApplyCalls,
|
||||
_remoteLandingLastApplyMotion, _remoteLandingLastApplyResult));
|
||||
}
|
||||
|
||||
// ── [remote-slide-*] — Bug B (remote ledge/roof slide) capture ────────
|
||||
//
|
||||
// docs/research/2026-08-04-bug-b-remote-slide-diagnosis.md §2 names TWO
|
||||
// live blip producers and states that NO existing probe distinguishes
|
||||
// them:
|
||||
// • Candidate 1 — AP-87's `bodyToTarget > 4 m` body snap in
|
||||
// RuntimeRemoteSteadyStatePosition.ApplyInterpolate (:129-137).
|
||||
// • Candidate 2 — InterpolationManager's own retail-faithful
|
||||
// `node_fail_counter > 3` snap-to-tail (:458-467; retail
|
||||
// InterpolationManager::UseTime @0x00555f20). That code is CORRECT
|
||||
// and is only reachable because §1 froze the body; this probe
|
||||
// OBSERVES it and must never be read as a reason to change it.
|
||||
// Both emit `[remote-slide-snap]` with a distinct `producer=` tag, so
|
||||
// one grep finds every blip and the tag alone answers "which one".
|
||||
//
|
||||
// The same family also settles the diagnosis's two load-bearing NOT
|
||||
// ESTABLISHED items:
|
||||
// • #1 (Shape A vs Shape B) — `[remote-slide-up] wireGrounded=` is the
|
||||
// raw ACE PositionFlags.IsGrounded bit for the accepted packet,
|
||||
// emitted at the ONE routing point both remote arms pass through,
|
||||
// AHEAD of the NoPositionOperation early returns, so a Shape-A slide
|
||||
// (every packet `wireGrounded=false disp=NoPositionOperation`) is
|
||||
// visible even though acdream writes nothing for it.
|
||||
// • #2 (is the roof steep in OUR collision data) —
|
||||
// `[remote-slide-tick] bodyCpNz=/rsCpNz=` against `floorZ=`.
|
||||
// `[remote-slide-vec]` covers the 0xF74E half of NOT ESTABLISHED #4: an
|
||||
// absence of lines during a slide is itself the answer.
|
||||
//
|
||||
// Pure reads only. Nothing here gates, orders, or mutates production
|
||||
// state; the throttle dictionary and the attribution latch are
|
||||
// probe-owned and [ThreadStatic] because a headless host ticks several
|
||||
// sessions in parallel.
|
||||
//
|
||||
// TEMPORARY — strip the whole ACDREAM_PROBE_REMOTE_SLIDE family once the
|
||||
// two-client roof capture has landed.
|
||||
|
||||
private static readonly string? RemoteSlideProbeRaw =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_REMOTE_SLIDE");
|
||||
|
||||
/// <summary>
|
||||
/// Initial state from <c>ACDREAM_PROBE_REMOTE_SLIDE</c>. <c>1</c> enables
|
||||
/// the family for every remote; a comma-separated hex GUID list (e.g.
|
||||
/// <c>0x50000123,0x8001ABCD</c>) enables it only for those GUIDs, which is
|
||||
/// what keeps a live two-client capture readable. Unset/empty = inert.
|
||||
/// </summary>
|
||||
public static bool ProbeRemoteSlideEnabled { get; set; } =
|
||||
!string.IsNullOrWhiteSpace(RemoteSlideProbeRaw);
|
||||
|
||||
/// <summary>
|
||||
/// Optional GUID allow-list for <see cref="ProbeRemoteSlideEnabled"/>.
|
||||
/// Empty means "every remote".
|
||||
/// </summary>
|
||||
public static IReadOnlySet<uint> ProbeRemoteSlideGuids { get; set; } =
|
||||
RemoteSlideProbeRaw is null || RemoteSlideProbeRaw.Trim() == "1"
|
||||
? new HashSet<uint>()
|
||||
: ParseHexIdList(RemoteSlideProbeRaw);
|
||||
|
||||
/// <summary>
|
||||
/// The single gate every <c>[remote-slide-*]</c> call site checks first.
|
||||
/// One static bool read plus (only when enabled) one set lookup.
|
||||
/// </summary>
|
||||
public static bool ShouldLogRemoteSlide(uint guid) =>
|
||||
ProbeRemoteSlideEnabled
|
||||
&& (ProbeRemoteSlideGuids.Count == 0
|
||||
|| ProbeRemoteSlideGuids.Contains(guid));
|
||||
|
||||
// Neither InterpolationManager nor RuntimeRemoteSteadyStatePosition has
|
||||
// access to a server GUID (RemoteMotion does not carry one), and
|
||||
// claude-memory/feedback_probe_identity_attribution.md makes the GUID
|
||||
// mandatory on a per-entity probe. Rather than widen either production
|
||||
// signature, the two per-remote windows that call into them stamp this
|
||||
// latch first — the same [ThreadStatic] shape the [remote-landing-after]
|
||||
// dispatch capture already uses, and for the same reason (the whole
|
||||
// window is synchronous on the ticking thread).
|
||||
[ThreadStatic] private static uint _remoteSlideAttributionGuid;
|
||||
|
||||
/// <summary>
|
||||
/// Stamp the GUID that any <c>[remote-slide-*]</c> line emitted from
|
||||
/// inside the following synchronous per-remote window belongs to. No-op
|
||||
/// unless <see cref="ProbeRemoteSlideEnabled"/>.
|
||||
/// </summary>
|
||||
public static void BeginRemoteSlideAttribution(uint guid)
|
||||
{
|
||||
if (!ProbeRemoteSlideEnabled) return;
|
||||
_remoteSlideAttributionGuid = guid;
|
||||
}
|
||||
|
||||
/// <summary>The GUID stamped by the innermost
|
||||
/// <see cref="BeginRemoteSlideAttribution"/>; <c>0</c> when unknown.</summary>
|
||||
public static uint RemoteSlideAttributionGuid => _remoteSlideAttributionGuid;
|
||||
|
||||
/// <summary>
|
||||
/// Per-GUID rate limit for the ~30 Hz <c>[remote-slide-tick]</c> line.
|
||||
/// A resting remote emits at most one line per this interval; any change
|
||||
/// in the caller-supplied signature (the contact/walkable/airborne/
|
||||
/// gravity/steep/moved bit pattern) emits immediately, so every
|
||||
/// transition is captured at full fidelity.
|
||||
/// </summary>
|
||||
private const long RemoteSlideTickThrottleMs = 200;
|
||||
|
||||
[ThreadStatic]
|
||||
private static Dictionary<uint, (long Ms, int Signature)>? _remoteSlideTickGate;
|
||||
|
||||
/// <summary>
|
||||
/// Edge-or-throttle admission for <see cref="LogRemoteSlideTick"/>.
|
||||
/// Returns true when the line should be emitted; updates the per-GUID
|
||||
/// gate as a side effect. Probe-owned state only.
|
||||
/// </summary>
|
||||
public static bool ShouldEmitRemoteSlideTick(uint guid, int signature)
|
||||
{
|
||||
if (!ShouldLogRemoteSlide(guid)) return false;
|
||||
_remoteSlideTickGate ??= new Dictionary<uint, (long, int)>();
|
||||
long now = Environment.TickCount64;
|
||||
if (_remoteSlideTickGate.TryGetValue(guid, out var previous)
|
||||
&& previous.Signature == signature
|
||||
&& now - previous.Ms < RemoteSlideTickThrottleMs)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_remoteSlideTickGate[guid] = (now, signature);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One <c>[remote-slide-up]</c> line per accepted remote Position, from
|
||||
/// the single point BOTH remote arms pass through — ahead of the
|
||||
/// <c>NoPositionOperation</c> early returns, so a Shape-A slide (which
|
||||
/// acdream answers by writing nothing) still produces a line.
|
||||
/// Caller MUST guard with <see cref="ShouldLogRemoteSlide"/>.
|
||||
/// </summary>
|
||||
public static void LogRemoteSlideUp(
|
||||
uint guid,
|
||||
bool wireGrounded,
|
||||
Vector3? wireVelocity,
|
||||
string disposition,
|
||||
float? playerDistance,
|
||||
float bodyToTarget,
|
||||
float bodySnapThreshold,
|
||||
bool willBeDrTicked,
|
||||
// Read at packet ENTRY. The player-remote arm stamps
|
||||
// LastServerPosTime between here and the routing call, so the value
|
||||
// ApplyInterpolate actually tests can differ — the
|
||||
// [remote-slide-snap] producer=ap87-4m line reports that one. Hence
|
||||
// the distinct firstUpAtEntry= field name.
|
||||
bool firstUp,
|
||||
bool airborne,
|
||||
bool contact,
|
||||
bool onWalkable,
|
||||
bool gravity,
|
||||
Vector3 bodyVelocity,
|
||||
bool contactPlaneValid,
|
||||
float contactPlaneNormalZ,
|
||||
Vector3 wirePosition,
|
||||
Vector3 bodyPosition,
|
||||
int interpQueueDepth,
|
||||
int interpFailCount)
|
||||
{
|
||||
var ci = System.Globalization.CultureInfo.InvariantCulture;
|
||||
string wireVel = wireVelocity is { } wv
|
||||
? string.Format(ci, "({0:F3},{1:F3},{2:F3})", wv.X, wv.Y, wv.Z)
|
||||
: "null";
|
||||
string playerDist = playerDistance is { } pd
|
||||
? pd.ToString("F2", ci)
|
||||
: "n/a";
|
||||
Console.WriteLine(string.Format(ci,
|
||||
"[remote-slide-up] guid=0x{0:X8} t={1} wireGrounded={2} wireVel={3} " +
|
||||
"disp={4} playerDist={5} bodyToTarget={6:F3} snapThreshold={7:F3} " +
|
||||
"willBeDrTicked={8} firstUpAtEntry={9} airborne={10} contact={11} " +
|
||||
"onWalkable={12} gravity={13} bodyVel=({14:F3},{15:F3},{16:F3}) " +
|
||||
"cpValid={17} cpNz={18:F4} floorZ={19:F4} steep={20} " +
|
||||
"wirePos=({21:F3},{22:F3},{23:F3}) bodyPos=({24:F3},{25:F3},{26:F3}) " +
|
||||
"queueDepth={27} failCount={28}",
|
||||
guid, Environment.TickCount64, wireGrounded, wireVel,
|
||||
disposition, playerDist, bodyToTarget, bodySnapThreshold,
|
||||
willBeDrTicked, firstUp, airborne, contact,
|
||||
onWalkable, gravity,
|
||||
bodyVelocity.X, bodyVelocity.Y, bodyVelocity.Z,
|
||||
contactPlaneValid, contactPlaneNormalZ, PhysicsGlobals.FloorZ,
|
||||
contactPlaneValid && contactPlaneNormalZ < PhysicsGlobals.FloorZ,
|
||||
wirePosition.X, wirePosition.Y, wirePosition.Z,
|
||||
bodyPosition.X, bodyPosition.Y, bodyPosition.Z,
|
||||
interpQueueDepth, interpFailCount));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One <c>[remote-slide-vec]</c> line per accepted remote 0xF74E
|
||||
/// VectorUpdate. NOT ESTABLISHED #4 asks whether ACE relays one at all
|
||||
/// during a slide — the ABSENCE of these lines across a captured slide
|
||||
/// window is the answer, which is why this sits on the committed path
|
||||
/// rather than inside the <c>Velocity.Z > 0.5f</c> airborne branch.
|
||||
/// Caller MUST guard with <see cref="ShouldLogRemoteSlide"/>.
|
||||
/// </summary>
|
||||
public static void LogRemoteSlideVector(
|
||||
uint guid,
|
||||
Vector3 wireVelocity,
|
||||
Vector3 wireOmega,
|
||||
bool willMarkAirborne,
|
||||
bool airborneBefore,
|
||||
bool contact,
|
||||
bool onWalkable,
|
||||
bool gravity,
|
||||
Vector3 bodyVelocity,
|
||||
bool contactPlaneValid,
|
||||
float contactPlaneNormalZ)
|
||||
{
|
||||
var ci = System.Globalization.CultureInfo.InvariantCulture;
|
||||
Console.WriteLine(string.Format(ci,
|
||||
"[remote-slide-vec] guid=0x{0:X8} t={1} " +
|
||||
"wireVel=({2:F3},{3:F3},{4:F3}) wireOmega=({5:F3},{6:F3},{7:F3}) " +
|
||||
"willMarkAirborne={8} airborneBefore={9} contact={10} " +
|
||||
"onWalkable={11} gravity={12} bodyVel=({13:F3},{14:F3},{15:F3}) " +
|
||||
"cpValid={16} cpNz={17:F4} floorZ={18:F4} steep={19}",
|
||||
guid, Environment.TickCount64,
|
||||
wireVelocity.X, wireVelocity.Y, wireVelocity.Z,
|
||||
wireOmega.X, wireOmega.Y, wireOmega.Z,
|
||||
willMarkAirborne, airborneBefore, contact,
|
||||
onWalkable, gravity,
|
||||
bodyVelocity.X, bodyVelocity.Y, bodyVelocity.Z,
|
||||
contactPlaneValid, contactPlaneNormalZ, PhysicsGlobals.FloorZ,
|
||||
contactPlaneValid && contactPlaneNormalZ < PhysicsGlobals.FloorZ));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blip producer <b>Candidate 1</b> — AP-87's <c>bodyToTarget > 4 m</c>
|
||||
/// body snap (<c>RuntimeRemoteSteadyStatePosition.ApplyInterpolate</c>).
|
||||
/// Tagged <c>producer=ap87-4m</c>. Caller MUST guard with
|
||||
/// <see cref="ShouldLogRemoteSlide"/>.
|
||||
/// </summary>
|
||||
public static void LogRemoteSlideBodySnap(
|
||||
uint guid,
|
||||
bool firstUp,
|
||||
bool willBeDrTicked,
|
||||
float bodyToTarget,
|
||||
float threshold,
|
||||
Vector3 bodyPosition,
|
||||
Vector3 targetPosition,
|
||||
int interpQueueDepth,
|
||||
int interpFailCount)
|
||||
{
|
||||
var ci = System.Globalization.CultureInfo.InvariantCulture;
|
||||
Console.WriteLine(string.Format(ci,
|
||||
"[remote-slide-snap] producer=ap87-4m guid=0x{0:X8} t={1} " +
|
||||
"firstUp={2} willBeDrTicked={3} bodyToTarget={4:F3} threshold={5:F3} " +
|
||||
"body=({6:F3},{7:F3},{8:F3}) target=({9:F3},{10:F3},{11:F3}) " +
|
||||
"queueDepth={12} failCount={13}",
|
||||
guid, Environment.TickCount64,
|
||||
firstUp, willBeDrTicked, bodyToTarget, threshold,
|
||||
bodyPosition.X, bodyPosition.Y, bodyPosition.Z,
|
||||
targetPosition.X, targetPosition.Y, targetPosition.Z,
|
||||
interpQueueDepth, interpFailCount));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The non-blip outcome of the same seam: the packet fed the queue. Its
|
||||
/// presence is what tells Shape B (queue fed, so Candidate 2 can arm)
|
||||
/// apart from Shape A (queue never fed, so only Candidate 1 can fire).
|
||||
/// Caller MUST guard with <see cref="ShouldLogRemoteSlide"/>.
|
||||
/// </summary>
|
||||
public static void LogRemoteSlideEnqueue(
|
||||
uint guid,
|
||||
float bodyToTarget,
|
||||
Vector3 targetPosition,
|
||||
int interpQueueDepth,
|
||||
int interpFailCount)
|
||||
{
|
||||
var ci = System.Globalization.CultureInfo.InvariantCulture;
|
||||
Console.WriteLine(string.Format(ci,
|
||||
"[remote-slide-enq] guid=0x{0:X8} t={1} bodyToTarget={2:F3} " +
|
||||
"target=({3:F3},{4:F3},{5:F3}) queueDepth={6} failCount={7}",
|
||||
guid, Environment.TickCount64, bodyToTarget,
|
||||
targetPosition.X, targetPosition.Y, targetPosition.Z,
|
||||
interpQueueDepth, interpFailCount));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blip producer <b>Candidate 2</b> — the retail-faithful
|
||||
/// <c>node_fail_counter > 3</c> snap-to-tail inside
|
||||
/// <see cref="InterpolationManager"/> (retail
|
||||
/// <c>InterpolationManager::UseTime</c> @0x00555f20). Tagged
|
||||
/// <c>producer=interp-stall</c>. This line is OBSERVATION ONLY: the code
|
||||
/// it reports on is a correct port firing correctly on a body frozen
|
||||
/// upstream, and the diagnosis explicitly rules it out of scope for any
|
||||
/// fix. Self-guarded on <see cref="ProbeRemoteSlideEnabled"/> so the
|
||||
/// snap site pays one bool read when off; GUID comes from
|
||||
/// <see cref="RemoteSlideAttributionGuid"/>.
|
||||
/// </summary>
|
||||
public static void LogRemoteSlideStallSnap(
|
||||
int failCount,
|
||||
int threshold,
|
||||
int queueDepth,
|
||||
Vector3 bodyPosition,
|
||||
Vector3 tailPosition,
|
||||
float distanceToHead)
|
||||
{
|
||||
uint guid = _remoteSlideAttributionGuid;
|
||||
if (!ShouldLogRemoteSlide(guid)) return;
|
||||
Vector3 tailDelta = tailPosition - bodyPosition;
|
||||
var ci = System.Globalization.CultureInfo.InvariantCulture;
|
||||
Console.WriteLine(string.Format(ci,
|
||||
"[remote-slide-snap] producer=interp-stall guid=0x{0:X8} t={1} " +
|
||||
"failCount={2} threshold={3} queueDepth={4} " +
|
||||
"body=({5:F3},{6:F3},{7:F3}) tail=({8:F3},{9:F3},{10:F3}) " +
|
||||
"tailDelta=({11:F3},{12:F3},{13:F3}) tailDeltaLen={14:F3} " +
|
||||
"distToHead={15:F3}",
|
||||
guid, Environment.TickCount64,
|
||||
failCount, threshold, queueDepth,
|
||||
bodyPosition.X, bodyPosition.Y, bodyPosition.Z,
|
||||
tailPosition.X, tailPosition.Y, tailPosition.Z,
|
||||
tailDelta.X, tailDelta.Y, tailDelta.Z, tailDelta.Length(),
|
||||
distanceToHead));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One <c>[remote-slide-tick]</c> line per admitted remote physics tick
|
||||
/// (see <see cref="ShouldEmitRemoteSlideTick"/> for the edge-or-throttle
|
||||
/// rule). Confirms LIVE what the diagnosis asserts from source.
|
||||
///
|
||||
/// <para>
|
||||
/// The first three parameters were written against the PRE-FIX code and
|
||||
/// their meaning changed with it; they keep their C# names because the
|
||||
/// diagnosis doc quotes them, but they are emitted under different LOG
|
||||
/// keys (see the format string). <paramref name="forcedContact"/> and
|
||||
/// <paramref name="forcedWalkable"/> once meant "the per-tick
|
||||
/// <c>TransientState |= Contact | OnWalkable</c> force flipped a bit that
|
||||
/// was clear" (Link 1); that force is deleted, and they now report the
|
||||
/// INVERSE fact — the body entered this tick WITHOUT that transient — and
|
||||
/// are logged as <c>entryNoContact=</c>/<c>entryNoWalkable=</c>.
|
||||
/// <paramref name="velocityBeforeZero"/> once named the vector the
|
||||
/// per-tick <c>Body.Velocity = Zero</c> discarded (Link 2); nothing
|
||||
/// discards it now, so it is simply the velocity the tick started with.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The <c>rs*</c> fields are the sweep's own retail classification, which
|
||||
/// pre-fix the visible tick never committed (Link 4) and post-fix must
|
||||
/// agree with the <c>contact=</c>/<c>onWalkable=</c> columns beside them.
|
||||
/// <paramref name="bodyContactPlaneNormalZ"/> vs <c>floorZ</c> settles NOT
|
||||
/// ESTABLISHED #2.
|
||||
/// </para>
|
||||
/// Caller MUST guard with <see cref="ShouldEmitRemoteSlideTick"/>.
|
||||
/// </summary>
|
||||
public static void LogRemoteSlideTick(
|
||||
uint guid,
|
||||
bool airborne,
|
||||
bool forcedContact,
|
||||
bool forcedWalkable,
|
||||
Vector3 velocityBeforeZero,
|
||||
bool resolved,
|
||||
bool resolveInContact,
|
||||
bool resolveOnWalkable,
|
||||
bool resolveIsOnGround,
|
||||
bool resolveContactPlaneValid,
|
||||
float resolveContactPlaneNormalZ,
|
||||
bool bodyContactPlaneValid,
|
||||
float bodyContactPlaneNormalZ,
|
||||
bool contact,
|
||||
bool onWalkable,
|
||||
bool gravity,
|
||||
Vector3 velocity,
|
||||
Vector3 acceleration,
|
||||
Vector3 preIntegratePosition,
|
||||
Vector3 postIntegratePosition,
|
||||
Vector3 resolvedPosition)
|
||||
{
|
||||
var ci = System.Globalization.CultureInfo.InvariantCulture;
|
||||
Console.WriteLine(string.Format(ci,
|
||||
// n1 (2026-08-04): the two keys below USED to read
|
||||
// "forcedContact/forcedWalkable" and meant "the deleted per-tick
|
||||
// force flipped a clear bit". The force is gone; the same
|
||||
// expressions now mean the body ENTERED the tick WITHOUT that
|
||||
// transient — the exact inverse of what the old name implied. The
|
||||
// log keys are renamed so a post-fix capture cannot be misread
|
||||
// against a pre-fix one; the C# parameter names are unchanged
|
||||
// because the diagnosis doc quotes them.
|
||||
"[remote-slide-tick] guid=0x{0:X8} t={1} airborne={2} " +
|
||||
"entryNoContact={3} entryNoWalkable={4} " +
|
||||
"velBeforeZero=({5:F3},{6:F3},{7:F3}) resolved={8} " +
|
||||
"rsInContact={9} rsOnWalkable={10} rsIsOnGround={11} " +
|
||||
"rsCpValid={12} rsCpNz={13:F4} " +
|
||||
"bodyCpValid={14} bodyCpNz={15:F4} floorZ={16:F4} steep={17} " +
|
||||
"contact={18} onWalkable={19} gravity={20} " +
|
||||
"vel=({21:F3},{22:F3},{23:F3}) accel=({24:F3},{25:F3},{26:F3}) " +
|
||||
"pre=({27:F3},{28:F3},{29:F3}) post=({30:F3},{31:F3},{32:F3}) " +
|
||||
"out=({33:F3},{34:F3},{35:F3}) moved={36:F4}",
|
||||
guid, Environment.TickCount64, airborne,
|
||||
forcedContact, forcedWalkable,
|
||||
velocityBeforeZero.X, velocityBeforeZero.Y, velocityBeforeZero.Z,
|
||||
resolved,
|
||||
resolveInContact, resolveOnWalkable, resolveIsOnGround,
|
||||
resolveContactPlaneValid, resolveContactPlaneNormalZ,
|
||||
bodyContactPlaneValid, bodyContactPlaneNormalZ, PhysicsGlobals.FloorZ,
|
||||
bodyContactPlaneValid && bodyContactPlaneNormalZ < PhysicsGlobals.FloorZ,
|
||||
contact, onWalkable, gravity,
|
||||
velocity.X, velocity.Y, velocity.Z,
|
||||
acceleration.X, acceleration.Y, acceleration.Z,
|
||||
preIntegratePosition.X, preIntegratePosition.Y, preIntegratePosition.Z,
|
||||
postIntegratePosition.X, postIntegratePosition.Y, postIntegratePosition.Z,
|
||||
resolvedPosition.X, resolvedPosition.Y, resolvedPosition.Z,
|
||||
Vector3.Distance(preIntegratePosition, resolvedPosition)));
|
||||
}
|
||||
|
||||
public static void LogCellSetBuild(
|
||||
uint seedCellId,
|
||||
System.Numerics.Vector3 sphereCenter,
|
||||
|
|
@ -762,6 +1254,10 @@ public static class PhysicsDiagnostics
|
|||
ProbeStepWalkEnabled = false;
|
||||
ProbeTeleportEnabled = false;
|
||||
ProbeRemoteLandingEnabled = false;
|
||||
ProbeRemoteSlideEnabled = false;
|
||||
ProbeRemoteSlideGuids = new System.Collections.Generic.HashSet<uint>();
|
||||
_remoteSlideAttributionGuid = 0;
|
||||
_remoteSlideTickGate = null;
|
||||
|
||||
// Side-channel fields
|
||||
LastBspHitPoly = null;
|
||||
|
|
|
|||
|
|
@ -104,6 +104,13 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
return false;
|
||||
}
|
||||
uint serverGuid = record.ServerGuid;
|
||||
// Bug B (2026-08-04): stamp the GUID for any [remote-slide-*] line
|
||||
// emitted from inside this remote's synchronous tick — in particular
|
||||
// blip producer Candidate 2, which fires deep inside
|
||||
// InterpolationManager and has no GUID of its own. TEMPORARY — strip
|
||||
// with the ACDREAM_PROBE_REMOTE_SLIDE family.
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.BeginRemoteSlideAttribution(
|
||||
serverGuid);
|
||||
uint localEntityId = record.LocalEntityId
|
||||
?? throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{serverGuid:X8}/{record.Incarnation} has no local identity.");
|
||||
|
|
@ -126,48 +133,58 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
|
||||
// Retail CPhysicsObj::UpdatePositionInternal @ 0x00512C30 scales
|
||||
// the CSequence root displacement by m_scale only while the body
|
||||
// is OnWalkable; otherwise it clears the displacement. Grounded
|
||||
// remotes below are explicitly OnWalkable and airborne remotes use
|
||||
// their authoritative velocity/gravity arc, so this is the same
|
||||
// branch expressed through our retained runtime state.
|
||||
System.Numerics.Vector3 scaledRootMotionLocalOrigin = !rm.Airborne
|
||||
? rootMotionLocalFrame.Origin * objectScale
|
||||
: System.Numerics.Vector3.Zero;
|
||||
// carries ON_WALKABLE_TS (`if ((transient_state & 2) == 0)` at
|
||||
// 0x00512CA1 multiplies the accumulated root frame by 0f, the else
|
||||
// arm by m_scale). Bug B (2026-08-04): read the transient the
|
||||
// sweep committed, never a separately tracked client bool — the
|
||||
// two disagree exactly on a steep contact, which is the surface
|
||||
// this whole fix is about.
|
||||
bool bodyOnWalkableAtTickStart = rm.Body.OnWalkable;
|
||||
System.Numerics.Vector3 scaledRootMotionLocalOrigin =
|
||||
bodyOnWalkableAtTickStart
|
||||
? rootMotionLocalFrame.Origin * objectScale
|
||||
: System.Numerics.Vector3.Zero;
|
||||
|
||||
// Step 1: re-apply current motion commands → body.Velocity.
|
||||
// Forces OnWalkable + Contact so the gate in apply_current_movement
|
||||
// always succeeds (remotes are server-authoritative; we don't
|
||||
// simulate airborne physics for them).
|
||||
// Bug B (2026-08-04) capture for the [remote-slide-tick] line
|
||||
// below. These USED to record whether the deleted per-tick
|
||||
// `TransientState |= Contact | OnWalkable` force actually flipped a
|
||||
// clear bit. The force is gone (see the block comment below), so
|
||||
// they now simply report the transient state the body ENTERED this
|
||||
// tick with — the INVERSE of what "forced" implied. The C# names
|
||||
// are kept because the diagnosis doc quotes them, but the LOG keys
|
||||
// were renamed to `entryNoContact=`/`entryNoWalkable=` so a
|
||||
// post-fix capture cannot be read against a pre-fix one; grep the
|
||||
// new keys.
|
||||
// TEMPORARY — strip with the ACDREAM_PROBE_REMOTE_SLIDE family.
|
||||
bool slideForcedContact = !rm.Body.InContact;
|
||||
bool slideForcedWalkable = !rm.Body.OnWalkable;
|
||||
System.Numerics.Vector3 slideVelocityBeforeZero = rm.Body.Velocity;
|
||||
|
||||
// retail CPhysicsObj::update_object 0x00515D10 -> set_active(1)
|
||||
// @0x00515DC2. ACTIVE is the only transient this tick may assert
|
||||
// on its own; CONTACT and ON_WALKABLE belong to
|
||||
// SetPositionInternal (0x00515330) and are committed from the
|
||||
// sweep's contact plane below.
|
||||
//
|
||||
// K-fix9 (2026-04-26): SKIP this when the remote is airborne.
|
||||
// Otherwise the force-OnWalkable + apply_current_movement
|
||||
// path stomps the +Z velocity we set in OnLiveVectorUpdated,
|
||||
// and gravity never gets to integrate the arc. The airborne
|
||||
// body keeps the launch velocity from the VectorUpdate;
|
||||
// UpdatePhysicsInternal below applies gravity each tick;
|
||||
// the next UpdatePosition snaps to the new ground location
|
||||
// and re-grounds.
|
||||
// Bug B (2026-08-04): the deleted lines were
|
||||
// if (!rm.Airborne)
|
||||
// rm.Body.TransientState |= Contact | OnWalkable | Active;
|
||||
// rm.Body.Velocity = Vector3.Zero;
|
||||
// — a per-tick FORGE of both retail transients plus a discard of
|
||||
// the authoritative velocity ACE delivered. On a 52.4-degree roof
|
||||
// the sweep correctly reported "contact, not walkable" and this
|
||||
// overruled it every tick, so `calc_acceleration` saw
|
||||
// Contact && OnWalkable and returned zero acceleration, friction
|
||||
// never engaged, and the body could not move at all. Retail has no
|
||||
// such write: CONTACT comes from `contact_plane_valid`
|
||||
// (0x00515430) and ON_WALKABLE from `contact_plane.N.z >= floor_z`
|
||||
// (0x00515465-0x0051548E), and `MoveOrTeleport` 0x00516330 never
|
||||
// touches the wire velocity vector for a remote at all.
|
||||
rm.Body.TransientState |=
|
||||
AcDream.Core.Physics.TransientStateFlags.Active;
|
||||
|
||||
if (!rm.Airborne)
|
||||
{
|
||||
rm.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
|
||||
| AcDream.Core.Physics.TransientStateFlags.OnWalkable
|
||||
| AcDream.Core.Physics.TransientStateFlags.Active;
|
||||
|
||||
// #184 (2026-07-07): a grounded remote carries NO translation
|
||||
// velocity. Its per-tick movement is the interp CATCH-UP toward
|
||||
// the MoveOrTeleport-queued server waypoint (computed at the
|
||||
// sticky-compose site below), which the KEPT ResolveWithTransition
|
||||
// sweep de-overlaps against neighbours — and the resolved position
|
||||
// is written back into the SHADOW (below) so the de-overlap
|
||||
// persists and neighbours collide against the resolved body, not
|
||||
// the raw server pos. This REPLACES the old synth-velocity model
|
||||
// (get_state_velocity / SERVERVEL Body.Velocity = ServerVelocity):
|
||||
// retail's UpdateObjectInternal (0x005156b0) has NO synth-velocity
|
||||
// leg — a remote translates by adjust_offset and the UP is a gentle
|
||||
// target. As of #184 Slice 2b this grounded model is the SINGLE
|
||||
// remote path (players + NPCs) — retail has no fork.
|
||||
rm.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||
|
||||
// Stale server-velocity → stop the locomotion CYCLE (the legs).
|
||||
// ANIM ONLY — translation is the catch-up. Kept verbatim (same
|
||||
// !moveToArmed && !stickyArmed gate) from the old SERVERVEL branch
|
||||
|
|
@ -200,12 +217,6 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
// this per-node dispatch + the funnel. The #170-deleted per-frame
|
||||
// apply_current_movement is NOT reintroduced.
|
||||
}
|
||||
else
|
||||
{
|
||||
// Airborne — keep Active flag (so UpdatePhysicsInternal
|
||||
// doesn't early-return) but DON'T set Contact / OnWalkable.
|
||||
rm.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Active;
|
||||
}
|
||||
|
||||
// Step 2: CSequence's complete Frame carries motion-table omega through
|
||||
// the same compose as root translation. PhysicsBody.Omega remains
|
||||
|
|
@ -242,9 +253,15 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
// Origin when armed (0x00555430 ASSIGNS m_fOrigin — the REPLACE
|
||||
// dichotomy), so a stuck monster still steers via #171.
|
||||
// • AIRBORNE: seed an EMPTY frame (no catch-up — the arc integrates
|
||||
// from velocity + gravity, unchanged).
|
||||
// Body.Velocity is 0 when grounded (set above), so UpdatePhysicsInternal
|
||||
// adds no translation on top of the catch-up — no double-move.
|
||||
// from velocity + gravity, unchanged). Retail expresses that
|
||||
// gate as `transient_state & 1` (CONTACT_TS) inside
|
||||
// InterpolationManager::adjust_offset @0x00555D52, which is the
|
||||
// `inContact:` argument below; before Bug B's fix the deleted
|
||||
// per-tick force made that argument permanently true.
|
||||
// Bug B (2026-08-04): the body's own velocity is no longer discarded
|
||||
// each tick, so UpdatePhysicsInternal genuinely integrates whatever
|
||||
// the sweep, gravity, and the authoritative wire vector left on it —
|
||||
// that integration is what a steep-contact slide IS.
|
||||
if (rm.Host is { } npcHost)
|
||||
{
|
||||
AcDream.Core.Physics.Motion.MotionDeltaFrame pmDelta =
|
||||
|
|
@ -252,7 +269,13 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
pmDelta.Origin = scaledRootMotionLocalOrigin;
|
||||
pmDelta.Orientation = rootMotionLocalFrame.Orientation;
|
||||
float maxSpeedNpc = rm.Motion.GetAdjustedMaxSpeed();
|
||||
System.Numerics.Vector3? terrainNormalNpc = !rm.Airborne
|
||||
// AD-10 terrain-only slope projection. Bug B (2026-08-04):
|
||||
// gated on the committed ON_WALKABLE transient, the same fact
|
||||
// retail root-frame scaling reads (0x00512CA1), instead of the
|
||||
// client Airborne bool. A body resting on a NON-walkable steep
|
||||
// contact must not have its root motion projected onto a
|
||||
// terrain plane it is not standing on.
|
||||
System.Numerics.Vector3? terrainNormalNpc = bodyOnWalkableAtTickStart
|
||||
? _physics.Engine.SampleTerrainNormal(
|
||||
rm.Body.Position.X,
|
||||
rm.Body.Position.Y)
|
||||
|
|
@ -289,7 +312,13 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
pmDelta.Origin = scaledRootMotionLocalOrigin;
|
||||
pmDelta.Orientation = rootMotionLocalFrame.Orientation;
|
||||
float maxSpeedNpc = rm.Motion.GetAdjustedMaxSpeed();
|
||||
System.Numerics.Vector3? terrainNormalNpc = !rm.Airborne
|
||||
// AD-10 terrain-only slope projection. Bug B (2026-08-04):
|
||||
// gated on the committed ON_WALKABLE transient, the same fact
|
||||
// retail root-frame scaling reads (0x00512CA1), instead of the
|
||||
// client Airborne bool. A body resting on a NON-walkable steep
|
||||
// contact must not have its root motion projected onto a
|
||||
// terrain plane it is not standing on.
|
||||
System.Numerics.Vector3? terrainNormalNpc = bodyOnWalkableAtTickStart
|
||||
? _physics.Engine.SampleTerrainNormal(
|
||||
rm.Body.Position.X,
|
||||
rm.Body.Position.Y)
|
||||
|
|
@ -378,12 +407,16 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
// deR/deH two-scalar reconstruction above.
|
||||
sphereList: sphereList,
|
||||
sphereScale: sphereScale,
|
||||
// K-fix9 (2026-04-26): mirror the K-fix7 gate —
|
||||
// airborne remotes must NOT pre-seed the
|
||||
// ContactPlane, otherwise AdjustOffset's snap-to-plane
|
||||
// branch zeroes the +Z offset every step (same bug
|
||||
// we hit on the local jump).
|
||||
isOnGround: !rm.Airborne,
|
||||
// With a body present this argument no longer seeds
|
||||
// transition contact at all (retail check_contact
|
||||
// 0x0050F5B0 owns that, see PhysicsEngine); it only decides
|
||||
// whether the retained walkable polygon is handed to the
|
||||
// SpherePath. Bug B (2026-08-04): read the committed
|
||||
// ON_WALKABLE transient, exactly like the local player
|
||||
// (`isOnGround: _body.OnWalkable`) and TickHidden
|
||||
// (`isOnGround: previousOnWalkable`), instead of the client
|
||||
// Airborne bool.
|
||||
isOnGround: previousOnWalkable,
|
||||
body: rm.Body, // persist ContactPlane across frames for slope tracking
|
||||
// Retail default physics state includes EdgeSlide; remote DR
|
||||
// should exercise the same edge/cliff branch as local movement.
|
||||
|
|
@ -446,137 +479,321 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
// to actually-moving remotes — the perf risk the review flagged for
|
||||
// a packed town. (In-place shadow-move + cell-relink-on-change is a
|
||||
// further optimization if profiling still shows churn.)
|
||||
// AD-25 (2026-07-30): retail CPhysicsObj::handle_all_collisions
|
||||
// (0x00514780, pc:282647) runs UNCONDITIONALLY after EVERY
|
||||
// SetPositionInternal — remote objects included; a
|
||||
// VectorUpdate-launched jump arc is ordinary object physics in
|
||||
// retail. #173 (2026-07-05) first mirrored the local player's
|
||||
// reflect math here by hand, but with a narrower gate than
|
||||
// retail's: `shouldReflect = !(prevOnWalkable && nowOnWalkable
|
||||
// && !sledding)` collapses to the two ad-hoc branches this
|
||||
// block used to hand-roll, and got BOTH wrong — the sledding
|
||||
// branch suppressed the bounce exactly when retail's
|
||||
// `!sledding` term forces it UNCONDITIONALLY, and the
|
||||
// non-sledding branch only reflected airborne→airborne where
|
||||
// retail reflects on every transition except grounded→grounded.
|
||||
// PhysicsObjUpdate.HandleAllCollisions is the same verbatim
|
||||
// port the local player and every ordinary body already use
|
||||
// (PhysicsObjUpdate.CommitSetPositionTransition); call it
|
||||
// directly instead of re-deriving the gate. It already
|
||||
// no-ops the reflect step when collisionNormalValid is false,
|
||||
// but — unlike the old wrapper this replaces — still runs the
|
||||
// fsf>1 unconditional velocity-zero "bleed" regardless of
|
||||
// whether this tick found a collision normal, matching
|
||||
// retail's own unconditional call site.
|
||||
AcDream.Core.Physics.PhysicsObjUpdate.HandleAllCollisions(
|
||||
rm.Body,
|
||||
resolveResult.CollisionNormalValid,
|
||||
resolveResult.CollisionNormal,
|
||||
previousContact,
|
||||
previousOnWalkable,
|
||||
resolveResult.IsOnGround);
|
||||
|
||||
// K-fix15 (2026-04-26): post-resolve landing
|
||||
// detection for airborne remotes. Mirrors
|
||||
// PlayerMovementController's local-player landing
|
||||
// path: when the resolver says we're on ground AND
|
||||
// velocity is no longer pointing up, transition
|
||||
// back to grounded — clear Airborne, restore
|
||||
// Contact + OnWalkable, remove Gravity, zero any
|
||||
// residual downward velocity, and trigger
|
||||
// HitGround so the sequencer can swap from
|
||||
// Falling → idle/locomotion. Without this, an
|
||||
// airborne remote falls through the floor (gravity
|
||||
// keeps building Velocity.Z negative until the
|
||||
// sphere-sweep clamps each frame, but Airborne
|
||||
// stays true forever).
|
||||
if (rm.Airborne
|
||||
&& resolveResult.IsOnGround
|
||||
&& rm.Body.Velocity.Z <= 0f)
|
||||
// ── SetPositionInternal commit (Bug B, 2026-08-04) ───────────
|
||||
// This block REPLACES a bare `HandleAllCollisions(...,
|
||||
// resolveResult.IsOnGround)` call. That call was the TAIL of
|
||||
// retail SetPositionInternal (0x00515330) without its PREFIX:
|
||||
// the sweep's own `InContact` / `OnWalkable` — the exact retail
|
||||
// classification the engine already computed — were never
|
||||
// committed to the body, and the ground edge was decided from
|
||||
// `resolveResult.IsOnGround`, which is `inContact || …`
|
||||
// (PhysicsEngine) and is therefore TRUE on a steep contact.
|
||||
// A remote that touched a 52.4-degree roof was consequently
|
||||
// declared landed, forced walkable, and stripped of gravity.
|
||||
//
|
||||
// Retail order (0x00515430 → 0x0051548E → 0x005154FE):
|
||||
// CONTACT_TS <- collision_info.contact_plane_valid
|
||||
// calc_acceleration
|
||||
// ON_WALKABLE_TS <- contact_plane.N.z >= floor_z, via
|
||||
// set_on_walkable @0x00511310, which is
|
||||
// the SOLE source of
|
||||
// MovementManager::HitGround /
|
||||
// ::LeaveGround — no ownership, player, or
|
||||
// creature gate anywhere in it
|
||||
// calc_acceleration
|
||||
// handle_all_collisions
|
||||
//
|
||||
// That is PhysicsObjUpdate.CommitSetPositionTransition's
|
||||
// sequence MINUS its velocity-authority check: the helper also
|
||||
// honours an `isVelocityCurrent` delegate and skips
|
||||
// handle_all_collisions when a newer Vector/Movement packet
|
||||
// installed a velocity from inside the ground-edge callback
|
||||
// (PhysicsObjUpdate.cs:101-102). That check is inert here —
|
||||
// this site would pass it as null (the helper's default), the
|
||||
// same as the TickHidden call below, because a per-quantum
|
||||
// simulation step is not a packet apply and opens no window in
|
||||
// which a competing velocity authority could land: the only
|
||||
// callbacks between the contact prefix and
|
||||
// handle_all_collisions are HitGround/LeaveGround and the
|
||||
// ownership re-check. The packet-driven placement paths are
|
||||
// the ones that need it: `RemoteTeleportPlacement.Apply` is
|
||||
// the only caller that passes the delegate, and
|
||||
// `RuntimeSetPositionState`'s canonical commit makes the same
|
||||
// check inline around its own `HandleAllCollisions`.
|
||||
//
|
||||
// It is spelled out through its own public sub-steps
|
||||
// (CommitSetPositionContactPrefix / the ground edge /
|
||||
// CommitSetPositionPostGround / HandleAllCollisions — the seam
|
||||
// whose doc comment exists for precisely this) for two reasons:
|
||||
// the Bug A landing probes must bracket the exact HitGround
|
||||
// call, and this per-remote per-quantum path must not allocate
|
||||
// an `isCurrent` closure.
|
||||
//
|
||||
// The whole commit is gated on `Ok && candidateMoved`, matching
|
||||
// PlayerMovementController and retail UpdateObjectInternal
|
||||
// (pc:283657): a failed transition is discarded whole and a
|
||||
// zero-move frame never re-derives contact.
|
||||
bool candidateMoved = postIntegratePos != preIntegratePos;
|
||||
if (resolveResult.Ok && candidateMoved)
|
||||
{
|
||||
rm.Airborne = false;
|
||||
// #184 (2026-07-07): clear the interp queue on landing (mirrors
|
||||
// the player-remote landing). Airborne UPs hard-snap and never
|
||||
// Enqueue, so any pre-jump waypoints are stale; without this the
|
||||
// first grounded catch-up after touchdown chases them backward.
|
||||
rm.Interp.Clear();
|
||||
rm.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
|
||||
| AcDream.Core.Physics.TransientStateFlags.OnWalkable;
|
||||
rm.Body.Velocity = new System.Numerics.Vector3(
|
||||
rm.Body.Velocity.X, rm.Body.Velocity.Y, 0f);
|
||||
// #161: HitGround MUST run with the Gravity state
|
||||
// bit still set — CMotionInterp::HitGround
|
||||
// (0x00528ac0) gates on state&0x400 (retail never
|
||||
// clears GRAVITY on landing; it's a persistent
|
||||
// object property). Clearing it first made this
|
||||
// re-apply a silent no-op, which is why the
|
||||
// falling pose never exited. The re-apply
|
||||
// dispatches the PRESERVED pre-fall forward
|
||||
// command through the funnel → the motion table
|
||||
// plays the Falling→X landing link. (The old
|
||||
// K-fix17 forced SetCycle is deleted: it read the
|
||||
// then-clobbered InterpretedState.ForwardCommand
|
||||
// — 0x40000015 — and re-set the very Falling
|
||||
// cycle it meant to clear.)
|
||||
// R4-V5 (closes the V4 wiring-contract gap the
|
||||
// adversarial review caught): retail order —
|
||||
// minterp first, then moveto (MovementManager::
|
||||
// HitGround 0x00524300, §2d — the R5-V5 facade
|
||||
// relay). Re-arms a moveto suspended by the
|
||||
// airborne UseTime contact gate; without it a
|
||||
// chasing NPC that lands stalls until ACE's
|
||||
// ~1 Hz re-emit.
|
||||
ulong landingStateAuthorityVersion =
|
||||
record.StateAuthorityVersion;
|
||||
bool finalOnWalkable = AcDream.Core.Physics.PhysicsObjUpdate
|
||||
.CommitSetPositionContactPrefix(
|
||||
rm.Body,
|
||||
resolveResult.InContact,
|
||||
resolveResult.OnWalkable,
|
||||
previousOnWalkable);
|
||||
|
||||
// Bug A investigation (2026-08-04, docs/ISSUES.md #32):
|
||||
// capture the exact state HitGround is about to act on —
|
||||
// see PhysicsDiagnostics.LogRemoteLanding for the field
|
||||
// list and PhysicsDiagnostics.ProbeRemoteLandingEnabled
|
||||
// for the discriminator table. TEMPORARY — strip once
|
||||
// the live-test run has landed.
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeRemoteLandingEnabled)
|
||||
if (!previousOnWalkable && finalOnWalkable)
|
||||
{
|
||||
bool gravitySetForProbe = rm.Body.HasGravity;
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLanding(
|
||||
site: "per-tick",
|
||||
guid: serverGuid,
|
||||
airborneBefore: true,
|
||||
gravitySet: gravitySetForProbe,
|
||||
contact: rm.Body.InContact,
|
||||
onWalkable: rm.Body.OnWalkable,
|
||||
hasDefaultSink: rm.Motion.DefaultSink is not null,
|
||||
resolveIsOnGround: resolveResult.IsOnGround,
|
||||
sequencerStyle: sequencer?.CurrentStyle ?? 0,
|
||||
sequencerMotion: sequencer?.CurrentMotion ?? 0);
|
||||
if (!gravitySetForProbe)
|
||||
// Bug A investigation (2026-08-04, docs/ISSUES.md #32):
|
||||
// capture the exact state HitGround is about to act on —
|
||||
// see PhysicsDiagnostics.LogRemoteLanding for the field
|
||||
// list and PhysicsDiagnostics.ProbeRemoteLandingEnabled
|
||||
// for the discriminator table. TEMPORARY — strip once
|
||||
// the live-test run has landed.
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeRemoteLandingEnabled)
|
||||
{
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLandingGateNoOp(
|
||||
"per-tick", serverGuid);
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLanding(
|
||||
site: "per-tick",
|
||||
guid: serverGuid,
|
||||
airborneBefore: true,
|
||||
gravitySet: rm.Body.HasGravity,
|
||||
contact: rm.Body.InContact,
|
||||
onWalkable: rm.Body.OnWalkable,
|
||||
hasDefaultSink: rm.Motion.DefaultSink is not null,
|
||||
resolveIsOnGround: resolveResult.IsOnGround,
|
||||
sequencerStyle: sequencer?.CurrentStyle ?? 0,
|
||||
sequencerMotion: sequencer?.CurrentMotion ?? 0);
|
||||
if (!rm.Body.HasGravity)
|
||||
{
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLandingGateNoOp(
|
||||
"per-tick", serverGuid);
|
||||
}
|
||||
AcDream.Core.Physics.PhysicsDiagnostics
|
||||
.BeginRemoteLandingDispatchCapture();
|
||||
}
|
||||
|
||||
// #161: HitGround MUST run with the Gravity state bit
|
||||
// still set — CMotionInterp::HitGround (0x00528AC0)
|
||||
// gates on state & 0x400. Bug B deleted the clear that
|
||||
// used to follow this call: retail NEVER toggles
|
||||
// GRAVITY_PS on a ground edge (`set_state` @0x00514DD0
|
||||
// post-processes only lighting/nodraw/hidden), it gates
|
||||
// gravity ACCELERATION on the CONTACT/ON_WALKABLE
|
||||
// transients inside calc_acceleration @0x00510950.
|
||||
// R4-V5: retail order is minterp then moveto
|
||||
// (MovementManager::HitGround 0x00524300).
|
||||
rm.Movement.HitGround();
|
||||
|
||||
// Bug A investigation (2026-08-04) — the OUTCOME half of
|
||||
// the probe above, emitted before the ownership re-check
|
||||
// below can return so the two lines always pair. See
|
||||
// PhysicsDiagnostics.LogRemoteLandingAfter and
|
||||
// docs/research/2026-08-04-bug-a-h3-scheduler-diagnosis.md
|
||||
// §6.1 for the three-way decision table. TEMPORARY.
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeRemoteLandingEnabled)
|
||||
{
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLandingAfter(
|
||||
site: "per-tick",
|
||||
guid: serverGuid,
|
||||
hitGroundInvoked: true,
|
||||
sequencerStyle: sequencer?.CurrentStyle ?? 0,
|
||||
sequencerMotion: sequencer?.CurrentMotion ?? 0,
|
||||
forwardCommand: rm.Motion.InterpretedState.ForwardCommand);
|
||||
}
|
||||
if (!IsCurrentOwner(
|
||||
record,
|
||||
rm,
|
||||
objectClockEpoch,
|
||||
externalOwnerValid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// #184 (2026-07-07): clear the interp queue on the
|
||||
// LANDING edge. An airborne remote's Positions hard-snap
|
||||
// and never Enqueue, so any pre-arc waypoints are stale;
|
||||
// without this the first grounded catch-up after
|
||||
// touchdown chases them backward. Bug B kept the
|
||||
// behaviour and only re-derived the edge — it now hangs
|
||||
// off the same `set_on_walkable(1)` transition retail
|
||||
// fires HitGround from, instead of the hand-rolled
|
||||
// `IsOnGround && Velocity.Z <= 0` test that fired on a
|
||||
// steep contact too. Register row AP-139.
|
||||
rm.Interp.Clear();
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
|
||||
Console.WriteLine($"VU.land guid=0x{serverGuid:X8} Z={rm.Body.Position.Z:F2}");
|
||||
}
|
||||
else if (previousOnWalkable && !finalOnWalkable)
|
||||
{
|
||||
// set_on_walkable(0) @0x0051133C —
|
||||
// MovementManager::LeaveGround. A remote that walks off
|
||||
// a ledge or slides off a walkable lip onto a steep face
|
||||
// now takes retail's ground-departure edge instead of
|
||||
// staying nominally grounded forever.
|
||||
rm.Motion.LeaveGround();
|
||||
if (!IsCurrentOwner(
|
||||
record,
|
||||
rm,
|
||||
objectClockEpoch,
|
||||
externalOwnerValid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
rm.Movement.HitGround();
|
||||
if (!IsCurrentOwner(
|
||||
record,
|
||||
rm,
|
||||
objectClockEpoch,
|
||||
externalOwnerValid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// DR bookkeeping only (partner of the jump-start
|
||||
// `State |= Gravity`): stops the per-tick gravity
|
||||
// integration for the grounded body.
|
||||
if (record.StateAuthorityVersion
|
||||
== landingStateAuthorityVersion)
|
||||
{
|
||||
rm.Body.State &=
|
||||
~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
|
||||
}
|
||||
AcDream.Core.Physics.PhysicsObjUpdate
|
||||
.CommitSetPositionPostGround(rm.Body);
|
||||
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
|
||||
Console.WriteLine($"VU.land guid=0x{serverGuid:X8} Z={rm.Body.Position.Z:F2}");
|
||||
// retail CPhysicsObj::handle_all_collisions (0x00514780,
|
||||
// pc:282647) @0x005154FE — the same verbatim port the local
|
||||
// player and every ordinary body use. `nowOnWalkable` is the
|
||||
// COMMITTED transient, not the contact-derived
|
||||
// `resolveResult.IsOnGround` the old call passed: on a steep
|
||||
// contact those disagree, and passing IsOnGround suppressed
|
||||
// the landing reflect exactly where retail forces it.
|
||||
AcDream.Core.Physics.PhysicsObjUpdate.HandleAllCollisions(
|
||||
rm.Body,
|
||||
resolveResult.CollisionNormalValid,
|
||||
resolveResult.CollisionNormal,
|
||||
previousContact,
|
||||
previousOnWalkable,
|
||||
rm.Body.OnWalkable);
|
||||
|
||||
// Bug B (2026-08-04): Airborne is DERIVED from the committed
|
||||
// ON_WALKABLE transient, never latched by a landing test.
|
||||
// This is the project's ONE definition of the flag — every
|
||||
// writer spells `!Body.OnWalkable`, and there are FIVE of
|
||||
// them: `SettleSpawnedRemoteContact` (the spawn-settle
|
||||
// tail) and `RemoteTeleportPlacement.Apply` in App,
|
||||
// `RuntimeSetPositionState`'s canonical placement commit,
|
||||
// and this file's two (here and the `TickHidden` resolve).
|
||||
// `PlayerMovementController.IsAirborne` computes the same
|
||||
// predicate for the local player. It stays unchanged here;
|
||||
// only the fact it is derived FROM has moved, from a
|
||||
// hand-rolled `IsOnGround` test to the sweep's own
|
||||
// contact-plane result.
|
||||
//
|
||||
// What this flag then GATES is a separate, still-open
|
||||
// divergence: retail's free-flight predicate for the
|
||||
// interpolate-vs-snap decision is CONTACT, not walkability
|
||||
// (`InterpolationManager::adjust_offset` @0x00555D30 gates
|
||||
// its whole body on `transient_state & 1` @0x00555D52).
|
||||
// Register row AP-140.
|
||||
rm.Airborne = !rm.Body.OnWalkable;
|
||||
}
|
||||
|
||||
// Bug B (2026-08-04) — [remote-slide-tick]. Emitted here, after
|
||||
// the SetPositionInternal commit above, so it reports the
|
||||
// sweep's own retail classification (rsInContact / rsOnWalkable
|
||||
// / rsCpNz) next to what the body now carries. Before the fix
|
||||
// those two columns disagreed on a steep roof — that
|
||||
// disagreement WAS the bug — and they must now agree on every
|
||||
// committed frame. bodyCpNz vs floorZ settles NOT ESTABLISHED
|
||||
// #2 ("is that roof steep in OUR collision data").
|
||||
//
|
||||
// Rate limit: ShouldEmitRemoteSlideTick emits immediately on
|
||||
// any change to the signature below (every contact / walkable /
|
||||
// grounded / airborne / gravity / steep / moved transition is
|
||||
// captured at full 30 Hz fidelity) and otherwise throttles to
|
||||
// one line per GUID per 200 ms, so a RESTING remote — the whole
|
||||
// point of the capture — stays at ~5 lines/s instead of 30.
|
||||
// TEMPORARY — strip with the ACDREAM_PROBE_REMOTE_SLIDE family.
|
||||
{
|
||||
bool slideBodyCpValid = rm.Body.ContactPlaneValid;
|
||||
float slideBodyCpNz = rm.Body.ContactPlane.Normal.Z;
|
||||
int slideSignature =
|
||||
(rm.Airborne ? 1 << 0 : 0)
|
||||
| (slideForcedContact ? 1 << 1 : 0)
|
||||
| (slideForcedWalkable ? 1 << 2 : 0)
|
||||
| (resolveResult.InContact ? 1 << 3 : 0)
|
||||
| (resolveResult.OnWalkable ? 1 << 4 : 0)
|
||||
| (resolveResult.IsOnGround ? 1 << 5 : 0)
|
||||
| (rm.Body.InContact ? 1 << 6 : 0)
|
||||
| (rm.Body.OnWalkable ? 1 << 7 : 0)
|
||||
| (rm.Body.HasGravity ? 1 << 8 : 0)
|
||||
| (slideBodyCpValid ? 1 << 9 : 0)
|
||||
| (slideBodyCpValid
|
||||
&& slideBodyCpNz
|
||||
< AcDream.Core.Physics.PhysicsGlobals.FloorZ
|
||||
? 1 << 10 : 0)
|
||||
| (System.Numerics.Vector3.Distance(
|
||||
preIntegratePos, resolveResult.Position) > 0.01f
|
||||
? 1 << 11 : 0);
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics
|
||||
.ShouldEmitRemoteSlideTick(serverGuid, slideSignature))
|
||||
{
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteSlideTick(
|
||||
guid: serverGuid,
|
||||
airborne: rm.Airborne,
|
||||
forcedContact: slideForcedContact,
|
||||
forcedWalkable: slideForcedWalkable,
|
||||
velocityBeforeZero: slideVelocityBeforeZero,
|
||||
resolved: true,
|
||||
resolveInContact: resolveResult.InContact,
|
||||
resolveOnWalkable: resolveResult.OnWalkable,
|
||||
resolveIsOnGround: resolveResult.IsOnGround,
|
||||
resolveContactPlaneValid: resolveResult.InContact,
|
||||
resolveContactPlaneNormalZ:
|
||||
resolveResult.ContactPlane.Normal.Z,
|
||||
bodyContactPlaneValid: slideBodyCpValid,
|
||||
bodyContactPlaneNormalZ: slideBodyCpNz,
|
||||
contact: rm.Body.InContact,
|
||||
onWalkable: rm.Body.OnWalkable,
|
||||
gravity: rm.Body.HasGravity,
|
||||
velocity: rm.Body.Velocity,
|
||||
acceleration: rm.Body.Acceleration,
|
||||
preIntegratePosition: preIntegratePos,
|
||||
postIntegratePosition: postIntegratePos,
|
||||
resolvedPosition: resolveResult.Position);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// Bug B (2026-08-04): the sweep was SKIPPED this tick (no
|
||||
// starting cell, or no landblocks resident). Reported with
|
||||
// resolved=false and every rs* field default so a silent
|
||||
// stretch in the log cannot be misread as "the probe is not
|
||||
// firing". Same edge-or-throttle admission. TEMPORARY — strip
|
||||
// with the ACDREAM_PROBE_REMOTE_SLIDE family.
|
||||
bool skipBodyCpValid = rm.Body.ContactPlaneValid;
|
||||
float skipBodyCpNz = rm.Body.ContactPlane.Normal.Z;
|
||||
int skipSignature =
|
||||
(rm.Airborne ? 1 << 0 : 0)
|
||||
| (slideForcedContact ? 1 << 1 : 0)
|
||||
| (slideForcedWalkable ? 1 << 2 : 0)
|
||||
| (rm.Body.InContact ? 1 << 6 : 0)
|
||||
| (rm.Body.OnWalkable ? 1 << 7 : 0)
|
||||
| (rm.Body.HasGravity ? 1 << 8 : 0)
|
||||
| (skipBodyCpValid ? 1 << 9 : 0)
|
||||
| (1 << 12);
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics
|
||||
.ShouldEmitRemoteSlideTick(serverGuid, skipSignature))
|
||||
{
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteSlideTick(
|
||||
guid: serverGuid,
|
||||
airborne: rm.Airborne,
|
||||
forcedContact: slideForcedContact,
|
||||
forcedWalkable: slideForcedWalkable,
|
||||
velocityBeforeZero: slideVelocityBeforeZero,
|
||||
resolved: false,
|
||||
resolveInContact: false,
|
||||
resolveOnWalkable: false,
|
||||
resolveIsOnGround: false,
|
||||
resolveContactPlaneValid: false,
|
||||
resolveContactPlaneNormalZ: 0f,
|
||||
bodyContactPlaneValid: skipBodyCpValid,
|
||||
bodyContactPlaneNormalZ: skipBodyCpNz,
|
||||
contact: rm.Body.InContact,
|
||||
onWalkable: rm.Body.OnWalkable,
|
||||
gravity: rm.Body.HasGravity,
|
||||
velocity: rm.Body.Velocity,
|
||||
acceleration: rm.Body.Acceleration,
|
||||
preIntegratePosition: preIntegratePos,
|
||||
postIntegratePosition: postIntegratePos,
|
||||
resolvedPosition: rm.Body.Position);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -696,6 +913,13 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
?? throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} has no local identity.");
|
||||
|
||||
// Bug B (2026-08-04): hidden remotes run the same ComposeOffset chain,
|
||||
// so the InterpolationManager stall snap can fire from here too and
|
||||
// needs the same GUID attribution. TEMPORARY — strip with the
|
||||
// ACDREAM_PROBE_REMOTE_SLIDE family.
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.BeginRemoteSlideAttribution(
|
||||
record.ServerGuid);
|
||||
|
||||
System.Numerics.Vector3 preComposePosition = rm.Body.Position;
|
||||
|
||||
// The part-array contribution is the identity frame while Hidden.
|
||||
|
|
|
|||
|
|
@ -41,6 +41,15 @@ internal static class RuntimeRemoteSteadyStatePosition
|
|||
/// </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
|
||||
|
|
@ -130,6 +139,28 @@ internal static class RuntimeRemoteSteadyStatePosition
|
|||
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;
|
||||
|
|
@ -144,6 +175,23 @@ internal static class RuntimeRemoteSteadyStatePosition
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue