acdream/src/AcDream.Core/Physics/PhysicsDiagnostics.cs
Erik e0d5d271f3 fix(retail): rotation rate, useability gate, retail toast strings
Two retail divergences fixed from the 2026-05-16 faithfulness audit
(Commit A of the plan at docs/superpowers/plans/2026-05-16-retail-faithfulness-fixes.md).

1. Rotation rate ignored HoldKey.Run. Retail's CMotionInterp::
   apply_run_to_command (decomp 0x00527be0 line 305098) multiplies
   turn_speed by run_turn_factor (1.5, PDB-named symbol at 0x007c8914)
   when input is TurnRight/TurnLeft under HoldKey.Run. Effective
   running rotation is 50% faster (~135°/s vs walking ~90°/s).
   Our keyboard A/D and ApplyAutoWalkOverlay used a fixed walking
   rate.

   New: RemoteMoveToDriver.TurnRateFor(running) helper. Keyboard
   path passes input.Run; auto-walk overlay passes
   _autoWalkInitiallyRunning. The walking-rate base
   (BaseTurnRateRadPerSec = π/2) is unchanged; TurnRateRadPerSec
   constant is preserved as the walking-rate alias for callers
   that don't have run/walk state (NPC remotes).

2. IsUseableTarget gated on `useability & USEABLE_REMOTE (0x20)`,
   which was stricter than retail. Per ItemUses::IsUseable
   (acclient_2013_pseudo_c.txt:256455) cross-referenced with 4
   call sites, retail's IsUseable() semantic is `_useability != 0`.
   But visually retail's USEABLE_NO (1) entities don't approach
   either, because ACE never broadcasts MovementType=6 for them.
   Our client installs a speculative auto-walk BEFORE the server
   responds, so we'd visibly approach + face signs before the
   wire packet was rejected.

   Pragmatic fix: block USEABLE_UNDEF (0) AND USEABLE_NO (1) in
   IsUseableTarget — slightly stricter than retail's
   IsUseable but matches retail's user-visible behaviour
   ("R on sign does nothing"). Documented in the doc-comment so
   a future implementer knows the gap.

3. New IsPickupableTarget gate for F-key path — requires
   USEABLE_REMOTE (0x20) bit. Null-useability fallback for
   BF_CORPSE + small-item ItemTypes (preserves M1 ground-item
   pickup flow when ACE seed DB doesn't publish useability).

4. R-key (UseCurrentSelection) upfront gate now ALWAYS uses
   IsUseableTarget. R is conceptually "use" with smart-routing
   to pickup as a downstream optimization. F-key (SendPickUp)
   uses IsPickupableTarget directly.

5. Retail toast strings on block, centralised in new
   src/AcDream.Core/Ui/RetailMessages.cs:
   - "The X cannot be used" (data 0x007e2a70, sprintf 0x00588ea4)
     fires on UseCurrentSelection / SendUse gate block.
   - "The X can't be picked up!" (sprintf 0x00587353) fires on
     SendPickUp non-pickupable block.
   - "You cannot pick up creatures!" (data 0x007e22b4) fires on
     SendPickUp creature block (was previously silent).
   - Plus 4 inactive retail strings ready for future call sites:
     CannotBeUsedWith (two-target Use), CannotBePickedUp (formal
     pickup variant), CannotBeUsedWhileOnHook_HooksOff +
     CannotBeUsedWhileOnHook_NotOwner (housing). All cite their
     retail data addresses + runtime sprintf addresses.

6. ProbeUseabilityFallbackEnabled diagnostic (env var
   ACDREAM_PROBE_USEABILITY_FALLBACK=1) logs every time the
   null-useability fallback fires. Settles whether the
   fallback for creature + BF_DOOR/LIFESTONE/PORTAL/CORPSE
   entries in ACE's seed DB without useability is hot code
   or theoretical defense.

Test coverage:
- +3 RemoteMoveToDriverTests cover TurnRateFor walking/running/back-compat.
- +7 RetailMessagesTests cover each retail string with retail anchor.
- +1 CreateObjectTests TryParse_WeenieFlagsUsable_ReadsUseableNoValue
  pins parser correctness for USEABLE_NO=1.
- 294/294 Core.Net pass; 24/24 new+touched Core tests pass.
- Pre-existing baseline of 8 Physics test failures unchanged
  (BSPStepUp + MotionInterpreter regression noise from prior
  sessions; out of scope here).

Deferred to a separate session per user direction:
- Click area = indicator-rect retail fidelity. Retail's picker
  uses per-part CGfxObj.drawing_sphere + polygon refine
  (0x0054c740); ours uses single Setup.SelectionSphere ray-
  intersect. The rect corners are dead zones today. Three fix
  options analyzed: screen-space rectangle hit-test, sqrt(2)
  sphere inflation, polygon refine Stage B.

Plan: docs/superpowers/plans/2026-05-16-retail-faithfulness-fixes.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 12:17:54 +02:00

144 lines
6.7 KiB
C#

using System;
namespace AcDream.Core.Physics;
/// <summary>
/// L.2a slice 1 (2026-05-12) — runtime-toggleable physics probe flags.
/// Initialized from env vars at process start; flippable at runtime via
/// the DebugPanel mirror (or by direct assignment). Log call sites read
/// these statics so a checkbox toggle takes effect on the next resolve
/// without relaunching.
///
/// <para>
/// L.2d slice 1 (2026-05-13) adds <see cref="ProbeBuildingEnabled"/> +
/// the <see cref="LastBspHitPoly"/> diagnostic side-channel. Future
/// slices may fold the older <c>ACDREAM_DUMP_*</c> env vars into this
/// class for unified runtime toggling. Until then, those older flags
/// remain sticky-at-startup per their original implementation.
/// </para>
/// </summary>
public static class PhysicsDiagnostics
{
/// <summary>
/// When true, <see cref="PhysicsEngine.ResolveWithTransition"/> emits
/// one structured <c>[resolve]</c> line per call: input + target +
/// output position/cell, grounded state, contact-plane status,
/// collision-normal validity, walkable polygon status, moving entity
/// id. Initial state from <c>ACDREAM_PROBE_RESOLVE=1</c>.
/// </summary>
public static bool ProbeResolveEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_RESOLVE") == "1";
/// <summary>
/// When true, every change to <c>PlayerMovementController.CellId</c>
/// emits one <c>[cell-transit]</c> line: old → new cell, current
/// world position, reason tag (<c>resolver</c> / <c>teleport</c>).
/// Initial state from <c>ACDREAM_PROBE_CELL=1</c>.
/// </summary>
public static bool ProbeCellEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_CELL") == "1";
/// <summary>
/// L.2d slice 1 (2026-05-13). When true, every BSP-shadow-entry hit
/// attributed by <c>TransitionTypes.FindObjCollisions</c> emits a
/// multi-line <c>[resolve-bldg]</c> entry: which part (partIdx vs 0),
/// physics-BSP root radius vs visual AABB radius, world-space entity
/// origin, and the specific hit polygon's vertices in both
/// object-local and world space. Designed to distinguish the three
/// L.2d hypotheses (wrong BSP loaded / over-registered parts /
/// BSPQuery flaw) from a single Holtburg-doorway capture.
///
/// <para>
/// Also gates a one-time <c>[entity-source]</c> log line at every
/// <c>ShadowObjects.Register(...)</c> call site in <c>GameWindow</c>
/// — makes <c>entityId=0xA9B479</c> in a probe line greppable to its
/// source registration within the same log file.
/// </para>
///
/// <para>
/// Initial state from <c>ACDREAM_PROBE_BUILDING=1</c>. Mirrorable
/// via <c>DebugVM.ProbeBuilding</c> when <c>ACDREAM_DEVTOOLS=1</c>.
/// </para>
///
/// <para>
/// Spec: <c>docs/superpowers/specs/2026-05-13-l2d-cbuildingobj-collision-design.md</c>.
/// </para>
/// </summary>
public static bool ProbeBuildingEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_BUILDING") == "1";
/// <summary>
/// L.2d slice 1 (2026-05-13). Diagnostic side-channel: the
/// <see cref="ResolvedPolygon"/> that <see cref="BSPQuery"/>
/// recorded for the most recent collision-normal write.
/// <see cref="TransitionTypes.FindObjCollisions"/> clears this to
/// <see langword="null"/> before each shadow-entry test and reads it
/// back after, so emitting the <c>[resolve-bldg]</c> probe line can
/// reference the actual hit poly without plumbing an out-param
/// through BSPQuery's recursive private methods.
///
/// <para>
/// Written by <see cref="BSPQuery"/> only when
/// <see cref="ProbeBuildingEnabled"/> is true, so this stays
/// zero-cost in normal play. Cylinder collisions leave this
/// <see langword="null"/> — the probe line emits
/// <c>hitPoly: n/a (cylinder)</c> in that case.
/// </para>
///
/// <para>
/// Not threadsafe — physics runs on a single thread. If that
/// changes, this needs <c>[ThreadStatic]</c> or rethink. Deviation
/// from spec component 4 (which described an out-param); the
/// side-channel keeps BSPQuery's signature stable and the diagnostic
/// path off the production code surface.
/// </para>
/// </summary>
public static ResolvedPolygon? LastBspHitPoly { get; set; }
/// <summary>
/// B.6 slice 1 (2026-05-14) — baseline trace for the local-player
/// server-initiated auto-walk path (issue #63). When true, the
/// following events emit one-line <c>[autowalk-*]</c> logs:
/// <list type="bullet">
/// <item><description><c>[autowalk-out]</c> on every <c>SendUse</c>
/// / <c>SendPickUp</c> the local player issues — these are the
/// packets that may trigger ACE's server-side <c>CreateMoveToChain</c>
/// when the target is out of <c>WithinUseRadius</c>.</description></item>
/// <item><description><c>[autowalk-mt]</c> on every inbound
/// <c>UpdateMotion</c> for the local player — captures the
/// <c>MovementType + MoveToPath + speed/runRate</c> ACE sends.</description></item>
/// <item><description><c>[autowalk-up]</c> on every inbound
/// <c>UpdatePosition</c> for the local player — answers "what's
/// ACE's broadcast cadence during auto-walk?"</description></item>
/// </list>
/// Initial state from <c>ACDREAM_PROBE_AUTOWALK=1</c>.
///
/// <para>
/// Spec: <c>docs/superpowers/specs/2026-05-14-phase-b6-design.md</c>
/// §"Required investigation".
/// </para>
/// </summary>
public static bool ProbeAutoWalkEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_AUTOWALK") == "1";
/// <summary>
/// 2026-05-16. Logs one line per `IsUseableTarget` call that takes
/// the null-useability fallback path (creature pass / BF_DOOR pass /
/// BF_LIFESTONE pass / etc.). Used to measure how often ACE's seed
/// DB ships entities without `_useability` set — settles whether
/// the fallback is live code or theoretical defense.
///
/// <para>
/// Retail has NO fallback; null/zero useability blocks Use entirely
/// (acclient_2013_pseudo_c.txt:402923 ItemHolder::UseObject —
/// IsUseable==0 falls through to "cannot be used" branch). Our
/// fallback exists because ACE genuinely sends null for many seed
/// weenies. The probe quantifies "many".
/// </para>
///
/// <para>Toggle via env var <c>ACDREAM_PROBE_USEABILITY_FALLBACK=1</c>
/// or DebugPanel checkbox.</para>
/// </summary>
public static bool ProbeUseabilityFallbackEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_USEABILITY_FALLBACK") == "1";
}