probe(physics): ACDREAM_PROBE_STEP_HEIGHTS — three readings along #338's chain
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
The live reading of 0.400 says the controller held its default at that instant, not why. Two very different causes produce it: the Setup-derived prepare/publish path never runs for the local player, or it runs and a later writer clobbers the result. Fixing without knowing which is a coin flip. One reading at each hop — prepare (Setup value computed and scaled), publish (assigned to the controller), resolve (what the resolver is actually handed) — with a decision table on the flag mapping each pattern to its cause, including the 0.000 case that would mean a null Setup took the retail dummy path. Edge-triggered per site, so the per-tick resolve site prints once per distinct pair and cannot drown the two one-shot sites it exists to be compared against. Prepare prints the raw authored pair beside the scaled one, so a surprise separates wrong-Setup from wrong-scale without a second run. Lives in PhysicsDiagnostics per code-structure rule 5 rather than as per-call-site env reads. Zero cost when off. Suite 11,231 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d5dd0b554b
commit
45d7154712
4 changed files with 120 additions and 0 deletions
|
|
@ -2532,6 +2532,99 @@ public static class PhysicsDiagnostics
|
|||
return "?";
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// #338 — step-height provenance
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// #338 (2026-08-06, TEMPORARY). Traces the LOCAL PLAYER's step-up /
|
||||
/// step-down heights along the chain that is supposed to carry them from
|
||||
/// the authored Setup to the resolver, so we can see WHERE the observed
|
||||
/// <c>0.400 / 0.400</c> wins.
|
||||
///
|
||||
/// <para>
|
||||
/// What is already known and does NOT need measuring: retail reads the
|
||||
/// authored field (<c>CTransition::step_up</c> @0x0050b610 substitutes
|
||||
/// <c>object_info.step_up_height</c> when <c>state & 2</c>, i.e.
|
||||
/// OnWalkable, and otherwise uses <c>0.0399999991f</c>), and acdream
|
||||
/// already ports that gate faithfully in <c>Transition.DoStepUp</c>. The
|
||||
/// value is the defect, not the gate.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// What is NOT known, and is exactly what this probe decides: the
|
||||
/// controller's fields initialise to <c>0.4f</c>, while
|
||||
/// <c>RuntimeSetPositionMoverPreparation</c> genuinely computes the
|
||||
/// Setup-derived value and a writer genuinely assigns it. So either the
|
||||
/// prepare/publish path never runs for the local player, or it runs and
|
||||
/// something later overwrites it. A live reading of <c>0.400</c> alone
|
||||
/// cannot tell those apart, and a fix chosen without knowing which is a
|
||||
/// coin flip.
|
||||
/// </para>
|
||||
///
|
||||
/// <para><b>Decision table.</b> Read the <c>[step-h]</c> lines in order:
|
||||
/// <list type="bullet">
|
||||
/// <item>No <c>site=prepare</c> line at all → the Setup-derived path
|
||||
/// never runs for the local player. Fix is to wire it (and the missing
|
||||
/// <c>PlayerModeController.ApplyStepHeights</c> the doc comment names
|
||||
/// was probably it).</item>
|
||||
/// <item><c>site=prepare</c> shows 0.600/1.500 but no
|
||||
/// <c>site=publish</c> → the command is built and never consumed on this
|
||||
/// path.</item>
|
||||
/// <item><c>prepare</c> and <c>publish</c> both show 0.600/1.500 but
|
||||
/// <c>site=resolve</c> shows 0.400 → a later writer clobbers it; find
|
||||
/// that writer, do not re-set the field.</item>
|
||||
/// <item><c>site=prepare</c> itself shows 0.400 → the Setup lookup is
|
||||
/// returning the wrong Setup, or scale is wrong.</item>
|
||||
/// <item><c>site=prepare</c> shows 0.000 → <c>preparation.Setup.Collision</c>
|
||||
/// was null and the retail dummy path was taken.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Initial state from <c>ACDREAM_PROBE_STEP_HEIGHTS=1</c>. Zero cost when
|
||||
/// off (one static bool read per site).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static bool ProbeStepHeightsEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_STEP_HEIGHTS") == "1";
|
||||
|
||||
private static readonly object _stepHeightGate = new();
|
||||
private static readonly Dictionary<string, (float Up, float Down)> _stepHeightSeen = new();
|
||||
|
||||
/// <summary>
|
||||
/// One <c>[step-h]</c> line. Self-guards on
|
||||
/// <see cref="ProbeStepHeightsEnabled"/>, and is edge-triggered per site:
|
||||
/// a site that keeps reporting the same pair prints once, so the
|
||||
/// per-tick resolve site cannot drown the two one-shot sites it has to be
|
||||
/// compared against.
|
||||
/// </summary>
|
||||
/// <param name="site">Where on the chain this reading was taken —
|
||||
/// <c>prepare</c>, <c>publish</c>, or <c>resolve</c>.</param>
|
||||
/// <param name="stepUp">The step-up height at that point, in metres.</param>
|
||||
/// <param name="stepDown">The step-down height at that point.</param>
|
||||
/// <param name="detail">Free-form provenance: the Setup id, the scale, or
|
||||
/// which branch produced the value. This is what makes a surprising
|
||||
/// reading actionable instead of merely surprising.</param>
|
||||
public static void LogStepHeights(
|
||||
string site, float stepUp, float stepDown, string detail)
|
||||
{
|
||||
if (!ProbeStepHeightsEnabled) return;
|
||||
|
||||
lock (_stepHeightGate)
|
||||
{
|
||||
if (_stepHeightSeen.TryGetValue(site, out var prev)
|
||||
&& prev.Up == stepUp && prev.Down == stepDown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_stepHeightSeen[site] = (stepUp, stepDown);
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
$"[step-h] site={site} stepUp={stepUp:F3} stepDown={stepDown:F3} {detail}");
|
||||
}
|
||||
|
||||
private static int ParsePositiveInt(string? value) =>
|
||||
int.TryParse(
|
||||
value,
|
||||
|
|
|
|||
|
|
@ -2625,6 +2625,15 @@ public sealed class PlayerMovementController
|
|||
// or it re-zeros the gravity velocity and the body re-wedges instead of falling off.
|
||||
bool candidateMoved = postIntegratePos != preIntegratePos;
|
||||
|
||||
// #338 (TEMPORARY): third and last reading along the chain — what
|
||||
// the resolver is ACTUALLY handed, per ordinary movement tick.
|
||||
// Edge-triggered inside the probe, so this per-tick site prints
|
||||
// once per distinct pair and cannot drown the two one-shot sites
|
||||
// it exists to be compared against.
|
||||
PhysicsDiagnostics.LogStepHeights(
|
||||
"resolve", StepUpHeight, StepDownHeight,
|
||||
$"onWalkable={_body.OnWalkable} cell=0x{CellId:X8}");
|
||||
|
||||
// ── 3. Collision resolution via CTransition sphere-sweep ─────────────
|
||||
// The Transition system subdivides the movement from pre→post into
|
||||
// sphere-radius steps, testing terrain collision at each step.
|
||||
|
|
|
|||
|
|
@ -214,6 +214,15 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
|||
controller.LocalEntityId = record.Key!.Value.LocalEntityId;
|
||||
controller.StepUpHeight = command.Physics.StepUpHeight;
|
||||
controller.StepDownHeight = command.Physics.StepDownHeight;
|
||||
|
||||
// #338 (TEMPORARY): second of three readings. If `prepare` printed the
|
||||
// authored pair and this line does not appear, the command is built
|
||||
// and never consumed on the local player's path.
|
||||
PhysicsDiagnostics.LogStepHeights(
|
||||
"publish",
|
||||
command.Physics.StepUpHeight,
|
||||
command.Physics.StepDownHeight,
|
||||
$"localEntityId={controller.LocalEntityId} scale={command.Physics.Scale:F3}");
|
||||
controller.SphereList = command.Physics.Spheres;
|
||||
controller.ObjectScale = command.Physics.Scale;
|
||||
controller.PreparePositionForCommit(
|
||||
|
|
|
|||
|
|
@ -180,6 +180,15 @@ internal static class RuntimeSetPositionMoverPreparer
|
|||
float stepUp = setup is not null ? setup.StepUpHeight * scale : 0f;
|
||||
float stepDown = setup is not null ? setup.StepDownHeight * scale : 0f;
|
||||
|
||||
// #338 (TEMPORARY): first of three readings along the chain. Prints
|
||||
// the raw authored pair alongside the scaled one, so a surprise here
|
||||
// separates "wrong Setup" from "wrong scale" without a second run.
|
||||
PhysicsDiagnostics.LogStepHeights(
|
||||
"prepare", stepUp, stepDown,
|
||||
setup is not null
|
||||
? $"authored=({setup.StepUpHeight:F3},{setup.StepDownHeight:F3}) scale={scale:F3}"
|
||||
: "setup=NULL (retail dummy path, exact zero steps)");
|
||||
|
||||
EntityCollisionFlags collisionFlags =
|
||||
EntityCollisionFlagsExt.FromPwdBitfield(
|
||||
record.Snapshot.ObjectDescriptionFlags ?? 0u);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue