diff --git a/src/AcDream.Core/Physics/PhysicsDiagnostics.cs b/src/AcDream.Core/Physics/PhysicsDiagnostics.cs index 4c470455..385e0639 100644 --- a/src/AcDream.Core/Physics/PhysicsDiagnostics.cs +++ b/src/AcDream.Core/Physics/PhysicsDiagnostics.cs @@ -2532,6 +2532,99 @@ public static class PhysicsDiagnostics return "?"; } + // ----------------------------------------------------------------------- + // #338 — step-height provenance + // ----------------------------------------------------------------------- + + /// + /// #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 + /// 0.400 / 0.400 wins. + /// + /// + /// What is already known and does NOT need measuring: retail reads the + /// authored field (CTransition::step_up @0x0050b610 substitutes + /// object_info.step_up_height when state & 2, i.e. + /// OnWalkable, and otherwise uses 0.0399999991f), and acdream + /// already ports that gate faithfully in Transition.DoStepUp. The + /// value is the defect, not the gate. + /// + /// + /// + /// What is NOT known, and is exactly what this probe decides: the + /// controller's fields initialise to 0.4f, while + /// RuntimeSetPositionMoverPreparation 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 0.400 alone + /// cannot tell those apart, and a fix chosen without knowing which is a + /// coin flip. + /// + /// + /// Decision table. Read the [step-h] lines in order: + /// + /// No site=prepare line at all → the Setup-derived path + /// never runs for the local player. Fix is to wire it (and the missing + /// PlayerModeController.ApplyStepHeights the doc comment names + /// was probably it). + /// site=prepare shows 0.600/1.500 but no + /// site=publish → the command is built and never consumed on this + /// path. + /// prepare and publish both show 0.600/1.500 but + /// site=resolve shows 0.400 → a later writer clobbers it; find + /// that writer, do not re-set the field. + /// site=prepare itself shows 0.400 → the Setup lookup is + /// returning the wrong Setup, or scale is wrong. + /// site=prepare shows 0.000 → preparation.Setup.Collision + /// was null and the retail dummy path was taken. + /// + /// + /// + /// + /// Initial state from ACDREAM_PROBE_STEP_HEIGHTS=1. Zero cost when + /// off (one static bool read per site). + /// + /// + public static bool ProbeStepHeightsEnabled { get; set; } = + Environment.GetEnvironmentVariable("ACDREAM_PROBE_STEP_HEIGHTS") == "1"; + + private static readonly object _stepHeightGate = new(); + private static readonly Dictionary _stepHeightSeen = new(); + + /// + /// One [step-h] line. Self-guards on + /// , 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. + /// + /// Where on the chain this reading was taken — + /// prepare, publish, or resolve. + /// The step-up height at that point, in metres. + /// The step-down height at that point. + /// 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. + 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, diff --git a/src/AcDream.Runtime/Gameplay/PlayerMovementController.cs b/src/AcDream.Runtime/Gameplay/PlayerMovementController.cs index d8968b89..c013fe9b 100644 --- a/src/AcDream.Runtime/Gameplay/PlayerMovementController.cs +++ b/src/AcDream.Runtime/Gameplay/PlayerMovementController.cs @@ -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. diff --git a/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs b/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs index b84b0d37..a5380dbf 100644 --- a/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs +++ b/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs @@ -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( diff --git a/src/AcDream.Runtime/Physics/RuntimeSetPositionMoverPreparation.cs b/src/AcDream.Runtime/Physics/RuntimeSetPositionMoverPreparation.cs index fc6aef6e..648e3d02 100644 --- a/src/AcDream.Runtime/Physics/RuntimeSetPositionMoverPreparation.cs +++ b/src/AcDream.Runtime/Physics/RuntimeSetPositionMoverPreparation.cs @@ -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);