using System; using System.Numerics; namespace AcDream.Core.Physics; /// /// 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. /// /// /// L.2d slice 1 (2026-05-13) adds + /// the diagnostic side-channel. Future /// slices may fold the older ACDREAM_DUMP_* env vars into this /// class for unified runtime toggling. Until then, those older flags /// remain sticky-at-startup per their original implementation. /// /// public static class PhysicsDiagnostics { /// /// When true, emits /// one structured [resolve] 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 ACDREAM_PROBE_RESOLVE=1. /// public static bool ProbeResolveEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_RESOLVE") == "1"; /// /// When true, every change to PlayerMovementController.CellId /// emits one [cell-transit] line: old → new cell, current /// world position, reason tag (resolver / teleport). /// Initial state from ACDREAM_PROBE_CELL=1. /// public static bool ProbeCellEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_CELL") == "1"; /// /// L.2d slice 1 (2026-05-13). When true, every BSP-shadow-entry hit /// attributed by TransitionTypes.FindObjCollisions emits a /// multi-line [resolve-bldg] 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. /// /// /// Also gates a one-time [entity-source] log line at every /// ShadowObjects.Register(...) call site in GameWindow /// — makes entityId=0xA9B479 in a probe line greppable to its /// source registration within the same log file. /// /// /// /// Initial state from ACDREAM_PROBE_BUILDING=1. Mirrorable /// via DebugVM.ProbeBuilding when ACDREAM_DEVTOOLS=1. /// /// /// /// Spec: docs/superpowers/specs/2026-05-13-l2d-cbuildingobj-collision-design.md. /// /// public static bool ProbeBuildingEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_BUILDING") == "1"; /// /// L.2d slice 1 (2026-05-13). Diagnostic side-channel: the /// that /// recorded for the most recent collision-normal write. /// clears this to /// before each shadow-entry test and reads it /// back after, so emitting the [resolve-bldg] probe line can /// reference the actual hit poly without plumbing an out-param /// through BSPQuery's recursive private methods. /// /// /// Written by only when /// is true, so this stays /// zero-cost in normal play. Cylinder collisions leave this /// — the probe line emits /// hitPoly: n/a (cylinder) in that case. /// /// /// /// Not threadsafe — physics runs on a single thread. If that /// changes, this needs [ThreadStatic] 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. /// /// public static ResolvedPolygon? LastBspHitPoly { get; set; } /// /// 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 [autowalk-*] logs: /// /// [autowalk-out] on every SendUse /// / SendPickUp the local player issues — these are the /// packets that may trigger ACE's server-side CreateMoveToChain /// when the target is out of WithinUseRadius. /// [autowalk-mt] on every inbound /// UpdateMotion for the local player — captures the /// MovementType + MoveToPath + speed/runRate ACE sends. /// [autowalk-up] on every inbound /// UpdatePosition for the local player — answers "what's /// ACE's broadcast cadence during auto-walk?" /// /// Initial state from ACDREAM_PROBE_AUTOWALK=1. /// /// /// Spec: docs/superpowers/specs/2026-05-14-phase-b6-design.md /// §"Required investigation". /// /// public static bool ProbeAutoWalkEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_AUTOWALK") == "1"; /// /// 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. /// /// /// 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". /// /// /// Toggle via env var ACDREAM_PROBE_USEABILITY_FALLBACK=1 /// or DebugPanel checkbox. /// public static bool ProbeUseabilityFallbackEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_USEABILITY_FALLBACK") == "1"; /// /// L.4-diag (2026-04-30) → promoted into /// 2026-05-16 per CLAUDE.md "Code Structure Rules" §5 (diagnostic owner /// classes, not per-call-site env reads). Gates the [steep-roof] /// trace family that fires from four sites during the rooftop-bounce /// investigation: /// /// PhysicsEngine.ResolveWithTransition — /// [steep-roof] KILL-VELOCITY-APPLIED when retail-faithful /// kill_velocity zeroes the body's velocity on steep-slope /// impact. /// TransitionTypes (FindEnvCollisions /// post-step) — per-frame plane-normal trace on the active /// . /// PlayerMovementController — two sites /// emitting [steep-roof] + the per-frame bounce trace when /// the post-collision velocity disagrees with retail. /// /// Initial state from ACDREAM_DUMP_STEEP_ROOF=1. Runtime-toggleable /// via the property setter; not yet wired to a DebugPanel checkbox (open /// follow-up if a debugging session calls for it). /// public static bool DumpSteepRoofEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_DUMP_STEEP_ROOF") == "1"; /// /// Indoor walking Phase 1 (2026-05-19). When true, emits one /// [indoor-bsp] line per /// call made from 's indoor /// cell-BSP branch. Captures the cell id, sphere local position, /// resulting , and the hit poly's id, /// local-normal, and side-type — pinpoints why indoor collision /// returns spurious collisions (#84) and helps cross-check the /// outdoor-in approach path (#85). /// /// /// While true, this also un-gates the diagnostic /// side-channel inside /// — see the OR'd condition at every poly /// write site. Zero-cost when off. /// /// /// /// Initial state from ACDREAM_PROBE_INDOOR_BSP=1. /// Runtime-toggleable via DebugPanel. /// /// /// /// Spec: docs/superpowers/specs/2026-05-19-indoor-walking-phase1-bsp-cluster-design.md. /// /// public static bool ProbeIndoorBspEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_BSP") == "1"; /// /// Indoor walking Phase D follow-up (2026-05-19). When true, emits one /// [cell-cache] line each time /// caches a new EnvCell. Reports per-cell polygon counts and BSP root /// structure so the caller can cross-reference with [indoor-bsp] /// lines to distinguish between: /// /// Empty data (physicsPolyCount=0 or resolvedCount=0) /// — candidate (a)/(c) in the poly=n/a investigation. /// Non-zero polygon counts but bspRootPolyCount=0 at /// root + tree has children — correct structure for non-leaf root, /// leaves hold the poly refs; not a bug. /// Non-zero polygon counts but bspRootPolyCount=0 at /// root AND root is a leaf (bspRootHasChildren=false) — BSP leaf with /// zero poly refs, candidate (b)/(d). /// /// This diagnostic fires at most once per EnvCell (cache is no-op after /// first population). It does NOT have a DebugPanel mirror yet — this is /// a one-shot capture tool, not a persistent toggle. Promote to full /// infrastructure after the root cause is identified. /// /// Initial state from ACDREAM_PROBE_CELL_CACHE=1. /// public static bool ProbeCellCacheEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_CELL_CACHE") == "1"; /// /// ContactPlane retention spike (2026-05-20). When true, every write to /// CollisionInfo.ContactPlane{,Valid,CellId,IsWater} and /// LastKnownContactPlane{,Valid,CellId,IsWater} emits one /// [cp-write] line: field, old → new value, caller method (walked /// from the stack), and source line. Maps the per-frame lifecycle of the /// contact plane to confirm/refute the hypothesis that /// FindEnvCollisions indoor branch is rewriting CP every frame /// instead of retaining it across frames. /// /// /// Only logs when the value actually changes (suppresses no-op writes to /// reduce log volume). Initial state from /// ACDREAM_PROBE_CONTACT_PLANE=1. Spike-only — remove once the fix /// lands and the diagnostic value is captured. /// /// public static bool ProbeContactPlaneEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_CONTACT_PLANE") == "1"; /// /// Indoor walking ISSUES #83 H-disambiguation spike (2026-05-21). /// When true, two diagnostic emissions activate: /// /// One [walk-miss] line per /// MISS /// event, dumping foot world/local position, the nearest /// walkable polygon in the cell (with XY-containment flag and /// vertical gap), and whether the LandCell terrain at the same /// XY would have grounded the player. /// One [floor-polys] line per indoor /// cell cached, enumerating each walkable-eligible polygon's /// id, normal Z, local-XY bounding box, and plane Z at the /// bbox center. /// /// Together these answer H1 (multi-cell iteration missing) vs H2 /// (probe distance too short) vs H3 (poly absent / /// walkable_hits_sphere rejection) for the ISSUES #83 /// stuck-falling bug. Spike-only — remove once the root cause is /// identified and the fix lands. /// /// /// Initial state from ACDREAM_PROBE_WALK_MISS=1. /// No DebugPanel mirror — one-shot diagnostic. /// /// /// /// Spec: docs/superpowers/specs/2026-05-21-indoor-walk-miss-probe-design.md. /// /// public static bool ProbeWalkMissEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_WALK_MISS") == "1"; /// /// Phase A6.P1 cdb probe spike (2026-05-21). When true, every BSP /// collision response site emits a structured [push-back] line: /// input/output sphere center, plane geometry, push-back delta, walk /// interp, and the dispatcher's selected path. Direct comparison to /// retail's cdb breakpoint set documented at /// tools/cdb/a6-probe.cdb. /// /// /// Three emission sites: BSPQuery.AdjustSphereToPlane /// (the suspected over-correction site), /// (the 6-path dispatcher), and /// (multi-cell BSP iteration outcomes). All three are zero-cost when /// off — checked via early-out at each site. /// /// /// /// Initial state from ACDREAM_PROBE_PUSH_BACK=1. /// Runtime-toggleable via DebugVM mirror. /// /// /// /// Spec: docs/superpowers/specs/2026-05-21-phase-a6-indoor-physics-fidelity-design.md. /// /// public static bool ProbePushBackEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_PUSH_BACK") == "1"; /// /// A6.P1 emission helper for the AdjustSphereToPlane site. /// One line per call: input sphere center, plane geometry, push-back /// delta, walk-interp before/after, and whether the adjust applied. /// Direct paired comparison to retail's cdb breakpoint on /// CPolygon::adjust_sphere_to_plane. /// /// /// Caller MUST guard with if (!ProbePushBackEnabled) return; /// before computing the delta arguments — this method assumes the /// caller paid that price already. /// /// public static void LogPushBackAdjust( Vector3 inputCenter, Vector3 outputCenter, Plane plane, float radius, float walkInterpBefore, float walkInterpAfter, float dpPos, float dpMove, float iDist, bool applied) { var delta = outputCenter - inputCenter; float deltaMag = delta.Length(); var ci = System.Globalization.CultureInfo.InvariantCulture; Console.WriteLine(string.Format(ci, "[push-back] site=adjust_sphere " + "in=({0:F4},{1:F4},{2:F4}) " + "out=({3:F4},{4:F4},{5:F4}) " + "delta=({6:F4},{7:F4},{8:F4}) deltaMag={9:F4} " + "n=({10:F4},{11:F4},{12:F4}) d={13:F4} " + "r={14:F4} winterp={15:F4}->{16:F4} " + "dpPos={17:F4} dpMove={18:F4} iDist={19:F4} applied={20}", inputCenter.X, inputCenter.Y, inputCenter.Z, outputCenter.X, outputCenter.Y, outputCenter.Z, delta.X, delta.Y, delta.Z, deltaMag, plane.Normal.X, plane.Normal.Y, plane.Normal.Z, plane.D, radius, walkInterpBefore, walkInterpAfter, dpPos, dpMove, iDist, applied)); } /// /// A6.P1 emission helper for the FindCollisions dispatcher /// site. One line per call: input sphere center, movement vector, /// path-selection state flags (collide / insertType / objState), /// walk-interp at entry, and the return state. Direct paired /// comparison to retail's cdb breakpoint on /// BSPTREE::find_collisions. /// /// /// Caller MUST guard with if (!ProbePushBackEnabled) return; /// before calling. /// /// public static void LogPushBackDispatch( Vector3 sphereCenter, Vector3 movement, bool collide, int insertType, int objState, float walkInterpEntry, int returnState) { // Output format mirrors LogPushBackAdjust: single string.Format // with CultureInfo.InvariantCulture so the F4 / hex formatting is // locale-independent and the output is greppable. var ci = System.Globalization.CultureInfo.InvariantCulture; Console.WriteLine(string.Format(ci, "[push-back-disp] site=dispatch " + "center=({0:F4},{1:F4},{2:F4}) " + "mvmt=({3:F4},{4:F4},{5:F4}) " + "collide={6} insertType={7} objState=0x{8:X} " + "winterp={9:F4} return={10}", sphereCenter.X, sphereCenter.Y, sphereCenter.Z, movement.X, movement.Y, movement.Z, collide, insertType, objState, walkInterpEntry, returnState)); } /// /// A6.P1 emission helper for the CheckOtherCells multi-cell /// BSP iteration site. One line per off-cell hit: from-cell, to-cell, /// BSP result (Ok / Adjusted / Slid / Collided), and the iteration /// outcome. Direct paired comparison to retail's /// CTransition::check_other_cells loop at decomp line /// 272717. Augments the existing A4 multi-cell BSP instrumentation /// with explicit per-iteration outcome telemetry. /// /// /// Caller MUST guard with if (!ProbePushBackEnabled) return; /// before calling. /// /// public static void LogPushBackCellTransit( uint primaryCellId, uint otherCellId, int bspResult, bool halted) { var ci = System.Globalization.CultureInfo.InvariantCulture; Console.WriteLine(string.Format(ci, "[push-back-cell] site=other_cell " + "primary=0x{0:X8} other=0x{1:X8} " + "bspResult={2} halted={3}", primaryCellId, otherCellId, bspResult, halted)); } public static void LogCpBoolWrite(string field, bool oldValue, bool newValue) { var caller = GetCpCallerName(); Console.WriteLine(System.FormattableString.Invariant( $"[cp-write] {field}: {oldValue} -> {newValue} caller={caller}")); } public static void LogCpPlaneWrite(string field, Plane oldPlane, Plane newPlane) { var caller = GetCpCallerName(); Console.WriteLine(System.FormattableString.Invariant( $"[cp-write] {field}: n=({oldPlane.Normal.X:F3},{oldPlane.Normal.Y:F3},{oldPlane.Normal.Z:F3}) D={oldPlane.D:F3} -> n=({newPlane.Normal.X:F3},{newPlane.Normal.Y:F3},{newPlane.Normal.Z:F3}) D={newPlane.D:F3} caller={caller}")); } public static void LogCpCellIdWrite(string field, uint oldValue, uint newValue) { var caller = GetCpCallerName(); Console.WriteLine(System.FormattableString.Invariant( $"[cp-write] {field}: 0x{oldValue:X8} -> 0x{newValue:X8} caller={caller}")); } /// /// Walks the stack to identify the first frame outside CollisionInfo /// and PhysicsDiagnostics — that's the actual caller writing the /// ContactPlane field. Format: TypeName.MethodName:line when file /// info is available, else just TypeName.MethodName. Walked with /// fileNeeded=true only when the probe flag is on, so zero cost /// when off. /// private static string GetCpCallerName() { // Skip 2: this method + the LogCp*Write helper that called it. var st = new System.Diagnostics.StackTrace(2, fNeedFileInfo: true); for (int i = 0; i < st.FrameCount; i++) { var f = st.GetFrame(i); var m = f?.GetMethod(); if (m is null) continue; var typeName = m.DeclaringType?.Name ?? "?"; if (typeName == "CollisionInfo" || typeName == "PhysicsDiagnostics") continue; int line = f?.GetFileLineNumber() ?? 0; return line > 0 ? $"{typeName}.{m.Name}:{line}" : $"{typeName}.{m.Name}"; } return "?"; } }