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
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue