fix #435 (part 1): delete 17 probes that outlived their closed investigations

Each of these was temporary apparatus added to chase one bug, and each was
supposed to be deleted in the commit that fixed it. Fourteen closed issues
later they were still here: #337's support/wire-mesh trio, #171's sticky
timeline, #119's viewer and entity dumps, #113's phantom probe, and a dozen
more. 3,493 lines removed; the client now reads 144 environment variables
instead of 161, and 47 temporary probes remain instead of 64.

This is not only tidying. Every probe leaves a branch on its hot path when
unset, several re-read the environment per call rather than caching, and
the volume buries the diagnostics that are actually load-bearing. It is
also a headless correctness matter: HeadlessStaticStateAudit reflects over
PhysicsDiagnostics' flags to refuse a multi-session host when any is set,
and cannot see probes that live outside that owner.

Four files went entirely — WalkMissDiagnostic.cs, CollisionMeshWireframe.cs
and two test files whose only subject was a deleted probe.
TransitionTypes.SetContactPlane also sheds its CallerMemberName /
CallerLineNumber parameters, which existed solely for #337's cpSrc=
attribution and carried the instruction to strip them with the probe
family; no call site passed them, so no behavior changes. F2's collision
overlay survives and reverts to its proxy-cylinder form, which is what
removing the ACDREAM_WIRE_MESH upgrade means.

LaunchOptionsDocumentationTests earned its keep here: it refused the
deletion until docs/launch-options.md moved the 17 rows into Retired and
the frozen direct-read counts came down (PhysicsEngine.cs to zero,
TransitionTypes.cs 3 to 2). The documentation could not drift during a
cleanup this wide.

The 14 probes that name no owning issue are deliberately NOT deleted.
Nothing records when they became safe to remove, and guessing is how a
future investigation loses apparatus it needed; #435 stays open for their
attribution.

Full hermetic suite 15,321 passed / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-24 11:41:20 +02:00
parent 05bfe8d162
commit 0c5057c9ff
34 changed files with 57 additions and 3495 deletions

View file

@ -24,9 +24,35 @@ What does NOT go here:
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
## #435 — Probe debt: 17 temporary probes outlived their closed investigations, 14 more name no owner
## #435 PARTLY CLOSED: Probe debt: 17 temporary probes outlived their closed investigations, 14 more name no owner
**Status:** OPEN
**Status:** The 17 orphaned probes are DELETED (2026-08-24) — 3,493 lines
removed, flag count 161 → 144, temporary probes 64 → 47. Build clean; full
hermetic suite 15,321 passed / 0 failed (baseline 15,333 minus the 12 tests
whose only subject was a deleted probe). Four files went entirely:
`WalkMissDiagnostic.cs`, `CollisionMeshWireframe.cs` and two probe-only test
files. `LaunchOptionsDocumentationTests` did its job during the cleanup —
it refused the deletion until the doc's rows moved to Retired and the frozen
direct-read counts were lowered (`PhysicsEngine.cs` to zero,
`TransitionTypes.cs` 3 → 2).
Notable: `TransitionTypes.SetContactPlane` shed its `CallerMemberName` /
`CallerLineNumber` parameters, which existed only for #337's `cpSrc=`
attribution and were explicitly marked "strip with the probe family". No
call site passed them, so no behavior changed. F2's collision overlay
survives and reverts to the proxy-cylinder form, as intended when
`ACDREAM_WIRE_MESH` went.
**STILL OPEN — the 14 unattributed probes.** They name no owning issue, so
nothing records when they are safe to remove. Deleting them on a guess is
how a future investigation loses apparatus it needed. The right next step is
attribution, not deletion: for each, find the commit that introduced it
(`git log -S ACDREAM_PROBE_X`), record the issue in its
`docs/launch-options.md` row, and only then decide. Deliberately deferred.
**Original report follows.**
**Status (original):** OPEN
**Severity:** LOW (no runtime defect; hot-path clutter and measurement noise)
**Filed:** 2026-08-24 (measured during the launch-options audit)
**Component:** diagnostics ownership

View file

@ -260,26 +260,24 @@ Each row names the issue that owns it. **A temporary probe is deleted in
the same commit as its investigation's fix** — if you find one here whose
issue is closed, the strip was missed; delete both.
> **Probe debt, measured 2026-08-24:** 64 temporary probes exist. They cite
> 21 distinct issues, and **14 of those are already closed** — 17 rows below
> are apparatus whose investigation ended without the strip. A further 14
> rows name no owning issue at all, which is worse: nobody can tell when
> they are safe to remove. Tracked as [#435](ISSUES.md); do not add to the
> pile. Every probe here still costs a branch on its hot path even when
> unset, and a handful re-read the environment per frame rather than
> caching (see their side-effects column).
> **Probe debt, measured 2026-08-24:** 64 temporary probes existed, citing 21
> distinct issues with 14 already closed. [#435](ISSUES.md) stripped the 17
> rows whose investigation had ended without the strip — see the Retired
> section below for their removal record — leaving 47. A further 14 rows
> (unchanged by this pass) name no owning issue at all, which is worse:
> nobody can tell when they are safe to remove. Every probe here still costs
> a branch on its hot path even when unset, and a handful re-read the
> environment per frame rather than caching (see their side-effects column).
| Flag | Owning investigation | Value | What it does | Side effects | Read by |
|---|---|---|---|---|---|
| `ACDREAM_A8_DUMP_PV` | (unattributed) | `=1` | Dumps local→NDC→clipped portal geometry (first 2 `Build` calls per distinct camera cell) | print-only (`Console.WriteLine`) | `PortalVisibilityBuilder.cs:270-271` (static field, not a diagnostics-owner class) |
| `ACDREAM_AIRBORNE_DIAG` | #42 | `=1` | prints `[SWEEP]`/`[SWEEP-OBJ]` lines tracing airborne-sweep XY drift, only when `!isOnGround` | print-only; re-reads the env var (`Environment.GetEnvironmentVariable`) on every airborne resolve/candidate instead of caching — minor per-call overhead when the flag is unset too | raw reads in `PhysicsEngine.cs:2300` + `TransitionTypes.cs:3905` (issue #42) |
| `ACDREAM_CLIP_DEBUG` | #176 | `=1` | forces the EnvCell SHELL pass to map every instance to clip slot 0 (no-clip) instead of its cell's portal-slice region | ALTERS RENDERED OUTPUT: shells draw whole/unclipped instead of trimmed — a visual isolation mode, not a log-only probe; no DebugPanel mirror | `RenderingDiagnostics.ClipDebugNoShellTrim` |
| `ACDREAM_DUMP_APPEARANCE` | #5 | `="1"` | Logs every `0xF625` ObjDescEvent + `0xF7DB` UpdateObject with body length, target guid, hex preview — used to debug remote-player appearance asymmetry | print-only (`Console.WriteLine`) | `WorldSession` static field `DumpAppearanceEnabled` (`WorldSession.cs:792-793`), raw scattered read, issue #5 diagnostic |
| `ACDREAM_DUMP_CELLS` | #98 | `=<comma list of hex cell ids>` | one-shot JSON dump of any cached EnvCell whose id matches the list, to `ProbeDumpCellsPath` (issue #98 fixture capture) | file I/O once per matching cell id (no-op on repeat); fixture-generation tool, not a perf-neutral no-op when ids are listed | `PhysicsDiagnostics.ProbeDumpCellIds` (`ParseHexIdList`) |
| `ACDREAM_DUMP_CELLS_DIR` | (unattributed) | `=<dir>` | overrides the output directory for `ACDREAM_DUMP_CELLS` | print/file-path only; no effect unless `ACDREAM_DUMP_CELLS` is also set | `PhysicsDiagnostics.ProbeDumpCellsPath` |
| `ACDREAM_DUMP_CLOTHING` | (unattributed) | `=1` | Print-only: dumps clothing/part-swap diagnostics for a spawned entity when its setup has ≥10 mesh parts (humanoids). Gated additionally on part count even when the flag is on. | `print-only` | `RuntimeOptions.DumpClothing``DatLiveEntityProjectionMaterializer.cs:251-258,1190` |
| `ACDREAM_DUMP_EDGE_SLIDE` | (unattributed) | `=1` | gates five `edge-slide:` trace lines (stepdown-failed, stepdown-branch-enter, phase2, branch, cliffslide) inside the L.4-diag edge-slide/cliff-slide code path | print-only; property re-reads `Environment.GetEnvironmentVariable` on EVERY call (not cached in a field) — repeated env lookups during edge-slide resolution when active; raw read outside any diagnostics-owner class (rule-5 candidate) | `Transition.DumpEdgeSlideEnabled` (private expression-bodied property in `TransitionTypes.cs`, raw read) |
| `ACDREAM_DUMP_ENTITY` | #119 | comma-separated hex ids, optional `0x` prefix, malformed segments ignored | per-entity HYDRATE/DRAW/WALK-REJECT trace for a watchlist of Setup/GfxObj source ids across `LandblockBuildFactory`, `WbDrawDispatcher` | print-only; every call site fast-exits on `Count==0`. The same id set is ALSO reused (undocumented in its own XML doc) as the watchlist for the `ACDREAM_PROBE_OUTSTAGE` `[outstage-own]` per-entity verdict probe — see Notes | `RenderingDiagnostics.DumpEntitySourceIds` |
| `ACDREAM_DUMP_GFXOBJS` | #98 | `=<comma list of hex GfxObj ids>` | one-shot JSON dump of any cached GfxObj's polygon table + BSP root metadata matching the list, to `ProbeDumpGfxObjsPath` (issue #98 fixture capture) | file I/O once per matching id (no-op on repeat) | `PhysicsDiagnostics.ProbeDumpGfxObjIds` (`ParseHexIdList`) |
| `ACDREAM_DUMP_GFXOBJS_DIR` | (unattributed) | `=<dir>` | overrides the output directory for `ACDREAM_DUMP_GFXOBJS` | print/file-path only; no effect unless `ACDREAM_DUMP_GFXOBJS` is also set | `PhysicsDiagnostics.ProbeDumpGfxObjsPath` |
| `ACDREAM_DUMP_LIVE_SPAWNS` | (unattributed) | `=1` | Print-only: logs every live `CreateObject` spawn as it's processed, plus DROP lines when a setup dat id is missing. | `print-only` | `RuntimeOptions.DumpLiveSpawns``DatLiveEntityProjectionMaterializer.cs:161-226`, `SessionPlayerComposition.cs:566,712` |
@ -294,7 +292,6 @@ issue is closed, the strip was missed; delete both.
| `ACDREAM_DUMP_VITALS` | (unattributed) | `="1"` | Logs every `PrivateUpdateVital(Current)` parse, every parsed `PlayerDescription` (vector flags/attr/spell counts), and `PlayerDescriptionParser` trailer/mid-walk `FormatException` failures with position | print-only at every site. `PlayerDescriptionParser.cs:458/473` re-read the env var raw inside `catch` blocks on every parse failure (rare, but scattered/uncached). | Read independently (not shared) at 4 sites: `WorldSession.cs:790-791` (`DumpVitalsEnabled`), `GameEventWiring.cs:1041` (local `dumpPd` at PlayerDescription registration), `PlayerDescriptionParser.cs:458` and `:473` (per-catch-block raw reads). Also mirrored (display-only, non-functional) via `DebugPanel.cs:240`/`DebugVM.cs:225`. |
| `ACDREAM_HIDE_PART` | (unattributed) | `=<int>` | Hides one mesh part by index on entities with ≥10 parts (humanoids) — a debugging aid for equipment/clothing part-visibility issues. | Real (visible) behavior change, not print-only, but scoped to a single diagnostic index and off by default. | `RuntimeOptions.HidePartIndex``LivePresentationComposition.cs:608``LiveEntityAnimationPresenter.cs:21,38,243` |
| `ACDREAM_LIGHT_DEBUG` | #176 | `=<int>` (`int.TryParse`; unset/invalid → 0) | shader isolation mode uploaded as `uLightDebug` by `EnvCellRenderer` + `WbDrawDispatcher`: 0=off, 1=ambient-only vertex lighting, 2=kill dynamic point lights, 3=raw vLit visualization (texture ignored) | ALTERS RENDERED OUTPUT directly every draw pass (changes fragment-shader lighting/texturing) — not a log probe; no DebugPanel mirror | `RenderingDiagnostics.LightDebugMode` |
| `ACDREAM_PROBE_AUTOWALK` | issue #63 | `=1` | gates `[autowalk-out]`/`[autowalk-mt]`/`[autowalk-up]` lines in `LiveEntityNetworkUpdateController` tracing local-player server-initiated auto-walk (`SendUse`/`SendPickUp`, inbound `UpdateMotion`, inbound `UpdatePosition`) | print-only; filtered to local player only, low volume | `PhysicsDiagnostics.ProbeAutoWalkEnabled` |
| `ACDREAM_PROBE_BUILDING` | l.2d slice 1 | `=1` | gates the multi-line `[resolve-bldg]` BSP-shadow-hit trace in `TransitionTypes.FindObjCollisions`, one-time `[entity-source]` registration logs in `GameWindow`, `[door-cycle]` UM dispatch trail, and a one-shot `[setstate-hex]` wire dump of the first `SetState` (0xF74B) packet in `WorldSession` | print-only; also un-gates the `PhysicsDiagnostics.LastBspHitPoly` diagnostic side-channel (a static field write in `BSPQuery`/`FlatBspQuery`, read back by the `[resolve-bldg]` line) — no gameplay effect, but an extra static-field write per BSP hit while on; heavy output (one multi-line entry per BSP hit per physics tick) | `PhysicsDiagnostics.ProbeBuildingEnabled` |
| `ACDREAM_PROBE_CELL` | (unattributed) | `=1` | gates one `[cell-transit]` line per `PlayerMovementController.CellId` change (old→new cell, position, reason tag) | print-only; low volume (only on actual cell crossings) | `PhysicsDiagnostics.ProbeCellEnabled` |
| `ACDREAM_PROBE_CELLSET` | a6.p5 | `=1` | gates `PhysicsDiagnostics.LogCellSetBuild`, one `[cellset-build]` line per `BuildCellSetAndPickContaining` call (seed cell, sphere XY, candidate list) from `CellTransit.cs:1468` | print-only; builds a `StringBuilder` of the candidate id list only when the flag is on | `PhysicsDiagnostics.ProbeCellSetEnabled` |
@ -308,34 +305,19 @@ issue is closed, the strip was missed; delete both.
| `ACDREAM_PROBE_INDOOR_BSP` | indoor walking phase 1 / cellar-lip wedge | `=1` | gates `[indoor-bsp]` (per `BSPQuery.FindCollisions` indoor call), `[neg-poly]` (near-miss polygon detail in `BSPQuery`), and `[stepdown-decide]` (step-down accept/reject inputs in `TransitionTypes`) trace lines | print-only; also un-gates the `LastBspHitPoly` diagnostic side-channel write (same as `ACDREAM_PROBE_BUILDING`) | `PhysicsDiagnostics.ProbeIndoorBspEnabled` |
| `ACDREAM_PROBE_INDOOR_LIGHT` | #176/#177 discriminator, a7.l1 | `=1` | rate-limited (1 Hz) `[indoor-light]` line from `LightManager.BuildPointLightSnapshot`: point-light pool set composition (pool/cellLess/registered/capped/byCell histogram) | print-only, explicitly "inert unless set" per the call-site comment (LightManager.cs:368-370); no DebugPanel mirror | `RenderingDiagnostics.ProbeIndoorLightEnabled` |
| `ACDREAM_PROBE_JUMP` | campaign ch round 2 | `=1` | gates the `[jump]` line in `PlayerMovementController.ReportJumpRefusal`, printed UNCONDITIONALLY (even when `OnInterfaceText` is null) to distinguish "branch never fired" from "branch fired, callback dropped it" | print-only; `Headless/Policies/HeadlessBotPolicy.cs`'s `JumpProbeHeadlessBotPolicy` doc comment references this flag as a companion but does not itself read it — it is a headless bot behavior meant to be run alongside `ACDREAM_PROBE_JUMP=1`, not a second consumer | `PhysicsDiagnostics.ProbeJumpEnabled` |
| `ACDREAM_PROBE_LIGHT` | #133 a7 | `=1` | rate-limited (1 Hz) `[light]` line + up to 3 `[light-detail]` lines: scene ambient/sun/registered/active light counts and nearest active point/spot light detail | print-only ("Output-only, inert when off" per doc) | `RenderingDiagnostics.ProbeLightEnabled` |
| `ACDREAM_PROBE_LOCAL_TELEPORT` | c4 route 3 d-t8 | `=1` | gates one `[local-tp]` line per local-player portal-arrival attempt (committed AND refused) from `LocalPlayerTeleportController` and `RuntimeAcceptedPositionDriveController.LogPortalArrivalAttempt` — the single Runtime chokepoint both graphical and headless hosts share | print-only; dual-host parity evidence (same line shape from both hosts) | `PhysicsDiagnostics.ProbeLocalTeleportEnabled` |
| `ACDREAM_PROBE_OUTSTAGE` | #131 | `=1` | print-on-change `[outstage]` line (outside-stage routing + per-slice cone verdicts) from `RetailPViewRenderer`; plus, when `ACDREAM_DUMP_ENTITY` also names watched ids, `[outstage-own]` per-entity PASS/CULL lines | print-only | `RenderingDiagnostics.ProbeOutStageEnabled` |
| `ACDREAM_PROBE_PARK` | issue #309 | `=1` | gates `[park]`/`[park-restore]` lines when a `RuntimeSetPositionState` placement parks or a cancelled park's withdrawal is rolled back | print-only, low volume (parks are rare); in a MULTI-session headless host, `HeadlessStaticStateAudit.ValidateProcessIsolation` THROWS `HeadlessConfigurationException` at startup if this (or any other process-global `Probe*`/`Dump*` boolean, `CollisionShadowSampleEvery`, or `PhysicsResolveCapture`) is enabled — refusal is waived only when `sessionCount==1` (logs loudly and proceeds instead) | `PhysicsDiagnostics.ProbeParkEnabled` |
| `ACDREAM_PROBE_PHANTOM` | #113, "throwaway apparatus — strip when the phantom closes" | `=1` | print-on-change `[phantom-shell]` / `[phantom-objs]` lines identifying which draw mechanism (shell pass vs. entity list) draws geometry unclipped/un-viewcone'd per cell | print-only | `RenderingDiagnostics.ProbePhantomEnabled` |
| `ACDREAM_PROBE_PLACEMENT_FAIL` | issue #98 | `=1` | gates one `[place-fail]` line per Path-1 (Placement/Ethereal) `Collided` return in `BSPQuery.FindCollisions`, plus one per `Transition.DoStepDown` placement-insert rejection | print-only; low volume (fires only on actual rejection) | `PhysicsDiagnostics.ProbePlacementFailEnabled` |
| `ACDREAM_PROBE_POLY_DUMP` | a6.p3 slice 4, issue #98 | `=1` | gates one `[poly-dump]` line (full polygon geometry: cell, poly index, sides, plane, all vertices) per `AdjustSphereToPlane` push-back call | print-only; HEAVY output (one full-geometry dump per push-back call) — doc explicitly says "use briefly, then turn off" | `PhysicsDiagnostics.ProbePolyDumpEnabled` |
| `ACDREAM_PROBE_PORTAL_CHURN` | "throwaway apparatus — strip once the bound ships" | `=1` | one `[portal-churn]` summary per `PortalVisibilityBuilder.Build` call: per-cell pop/re-pop counts, re-enqueue totals, reciprocal-clip pre→post region growth | print-only | `RenderingDiagnostics.ProbePortalChurnEnabled` |
| `ACDREAM_PROBE_PUSH_BACK` | phase a6.p1 | `=1` | gates `[push-back]` (`BSPQuery.AdjustSphereToPlane`), `[push-back-disp]` (`BSPQuery.FindCollisions` 6-path dispatcher), `[push-back-cell]` (`Transition.CheckOtherCells` multi-cell BSP) lines | print-only; the `DebugVM.cs:380` "runtime mirror" is dead code — `DebugVM`/`DebugPanel` (`AcDream.UI.Abstractions/Panels/Debug/`) are never instantiated anywhere in `src/` (the ImGui frontend they required was removed at Campaign V slice V11); only the startup env var takes effect | `PhysicsDiagnostics.ProbePushBackEnabled` |
| `ACDREAM_PROBE_PVINPUT` | "throwaway apparatus — strip once the jitter source is pinned" | `=1` | one `[pv-input]` line/frame with 6-dp-precision `PortalVisibilityBuilder.Build` inputs (camera eye, player position, VP elements) + resulting flood-cell count; deliberately runs WITHOUT the heavier `[flap]` probe so the log stays diffable | print-only | `RenderingDiagnostics.ProbePvInputEnabled` |
| `ACDREAM_PROBE_REACH` | #334, temporary — strip with the probe family | `=1` | gates `[reach-q]` (per-cell candidate-disposition query summary, emitted even on zero-entry cells) and `[reach-obj]` (per-candidate disposition: exempt/no-shape/bsp-only-skip/tested) lines in `Transition.FindObjCollisionsInCell` | print-only; runs on a HOT path (per cell per transitional insert); de-duplicated via two `Dictionary` caches with a `lock`-protected gate (`_reachGate`) — a real per-call dictionary lookup + occasional lock contention while on, bounded emission (≤2/sec per cell, ≤1/sec per candidate) | `PhysicsDiagnostics.ProbeReachEnabled` |
| `ACDREAM_PROBE_REMOTE_LANDING` | bug a / issue #32, temporary | `=1` | gates `[remote-landing]`/`[remote-landing-gate]`/`[remote-landing-after]` lines around remote ground-contact edges in `LiveEntityNetworkUpdateController` and `RuntimeRemotePhysicsUpdater`; `MotionTableDispatchSink.ApplyMotion` unconditionally forwards its result to `PhysicsDiagnostics.RecordRemoteLandingDispatch` (self-guarded internally, no behavior change) | print-only; uses `[ThreadStatic]` capture latches (`_remoteLandingApplyCalls` etc.) so a headless host ticking several sessions in parallel doesn't cross-contaminate | `PhysicsDiagnostics.ProbeRemoteLandingEnabled` |
| `ACDREAM_PROBE_REMOTE_SLIDE` | bug b, temporary — strip once two-client roof capture lands | `=1` OR `=<comma-separated hex GUID list>` | gates `[remote-slide-up]`/`[remote-slide-vec]`/`[remote-slide-snap]`/`[remote-slide-enq]` lines across `LiveEntityNetworkUpdateController`, `InterpolationManager`, `RuntimeRemotePhysicsUpdater`, `RuntimeRemoteSteadyStatePosition` tracing two candidate remote-slide "blip" producers | print-only; `BeginRemoteSlideAttribution`/GUID-stamping calls are UNCONDITIONAL at several call sites (self-guard is internal), so a `[ThreadStatic]` field write happens on every remote tick regardless of the flag (cheap, non-allocating); a GUID allow-list narrows output to specific entities for a readable two-client capture | `PhysicsDiagnostics.ProbeRemoteSlideEnabled` + `ProbeRemoteSlideGuids` (raw string parsed via `ParseHexIdList` unless it's the literal `"1"`) |
| `ACDREAM_PROBE_REMOTE_TELEPORT` | c4 route 4b-3, temporary | `=1` | gates one `[remote-teleport]` line per routed remote teleport arm in `LiveEntityNetworkUpdateController.ApplyRemoteContactRouting` | print-only; a 2026-08-04 fix moved the enabled-check to the CALL SITE because the probe's internal self-guard did not prevent `teleportStatus.ToString()` from being evaluated/allocated on every teleport regardless of flag state — now properly guarded | `PhysicsDiagnostics.ProbeRemoteTeleportEnabled` |
| `ACDREAM_PROBE_SEAMDRAW` | #176, "throwaway apparatus" | `"1"`/`"true"`/blank → default #176 Facility Hub cell set (7 fixed hex ids); otherwise comma-separated hex cell-id list | change-deduped + 2 s-heartbeat `[seam-cell]`/`[seam-snap]`/`[seam-ent]`/`[seam-mask]` lines from `EnvCellRenderer.Render` and `WbDrawDispatcher` describing per-instance transforms and resolved light-set identities at target cells | print-only | `RenderingDiagnostics.ProbeSeamDrawEnabled` / `SeamDrawTargetCells` |
| `ACDREAM_PROBE_SHELL` | #78, "throwaway apparatus — strip once the indoor-enclosure render is fixed" | `=1` | one `[shell]` line per opaque-pass `EnvCellRenderer.Render` call: per filtered cell — snapshot presence, gfxObj/batch/index/translucent/zero-bindless-handle counts | print-only; allocates a `StringBuilder` and loops every visible cell on every opaque pass while enabled | `RenderingDiagnostics.ProbeShellEnabled` |
| `ACDREAM_PROBE_STEP_HEIGHTS` | issue #338 | `=1` | gates edge-triggered `[step-h]` lines at `prepare`/`publish`/`resolve` sites tracing step-up/step-down height provenance | print-only; `AnnounceStepHeightProbeOnce` prints TWO self-report lines EXACTLY ONCE PER PROCESS regardless of the flag's value (reports the flag's own state + the raw env var text + the running assembly's file path) — this self-report line is NOT gated by the flag itself, only rate-limited to once | `PhysicsDiagnostics.ProbeStepHeightsEnabled` |
| `ACDREAM_PROBE_STEP_WALK` | a6.p3 issue #98 | `=1` | gates `[step-walk]` lines at select points in the transition sub-step loop and step-down probe (requested vs adjusted offset, sphere positions, contact planes, walkable flags) | print-only; no DebugPanel mirror | `PhysicsDiagnostics.ProbeStepWalkEnabled` |
| `ACDREAM_PROBE_STICKY` | r5-v3 issue #171 | `=1` | gates `[sticky]` lifecycle lines (STICK/UNSTICK/LEASE-EXPIRE/TARGET-status teardown) and per-armed-tick steer lines in `AcDream.Core.Physics.Motion.StickyManager`, plus `[sticky-snap-skip]` in `LiveEntityNetworkUpdateController` when a server hard-snap is suppressed for a stuck entity | print-only; heavy while a pack is stuck (~60 Hz × stuck count) | `PhysicsDiagnostics.ProbeStickyEnabled` |
| `ACDREAM_PROBE_SUPPORT` | issue #337, temporary | `=1` | gates `[support]` (per resolve, per body INCLUDING corpses/NPCs — independent terrain sample at the body's out-XY compared against the contact plane's height/provenance) and `[geom]` (once per nearby GfxObj — physics-BSP vertex cloud vs visual mesh AABB coincidence verdict) lines in `PhysicsEngine` | print-only; `[support]` performs an INDEPENDENT terrain height sample every time it fires (real extra computation beyond the resolve itself, throttled to 4 Hz per body plus every 10 cm of vertical movement); pure reads only, never mutates production collision state | `PhysicsDiagnostics.ProbeSupportEnabled` |
| `ACDREAM_PROBE_SWEPT` | phase w stage 0 | `=1` | gates one `[cell-swept]` line per `ResolveWithTransition` call comparing the transition's swept cell vs the legacy static `ResolveCellId` path | print-only | `PhysicsDiagnostics.ProbeSweptEnabled` |
| `ACDREAM_PROBE_TELEPORT` | 2026-06-22, "removable diagnostic" | `=1` | gates `[tp-probe]` lines (`LogTeleport`) at AIM/ENQ/BUILD/APPLY/PLACED teleport-pipeline events across `LocalPlayerTeleportController` and `RuntimeAcceptedPositionDriveController`, with cross-thread monotonic timestamps | print-only | `PhysicsDiagnostics.ProbeTeleportEnabled` |
| `ACDREAM_PROBE_TEXFLUSH` | #105 | `=1` | one `[tex-flush]` line whenever `WbMeshAdapter.Tick`'s staged-texture-update picture changes (pending layer updates before/after the per-frame mipmap flush) | print-only | `RenderingDiagnostics.ProbeTexFlushEnabled` |
| `ACDREAM_PROBE_VIEWER` | #119-residual | `=1` | one `[viewer]` line per CHANGE of (root cell, flood size, OutsideView poly count, player cell), with mm-precision projection eye — capture half of the tower-ascent capture→replay loop (`TowerAscentReplayTests`) | print-only | `RenderingDiagnostics.ProbeViewerEnabled` |
| `ACDREAM_PROBE_WALK_MISS` | issues #83, spike-only | `=1` | gates `[walk-miss]` (per `Transition.TryFindIndoorWalkablePlane` MISS) and `[floor-polys]` (per indoor cell cached, enumerating walkable-eligible polygons) lines | print-only; no DebugPanel mirror | `PhysicsDiagnostics.ProbeWalkMissEnabled` |
| `ACDREAM_WIRE_MESH` | #337, explicitly "temporary" | `=1` | when the separate F2 collision-wireframe overlay is already active, replaces its cheap broadphase-proxy-cylinder drawing with the object's REAL physics-BSP polygon edges (cyan) + visual mesh AABB (magenta) + terrain triangle under the player (yellow), resolved live every frame | ALTERS RENDERED OUTPUT (debug overlay geometry): adds real per-frame physics-BSP polygon extraction + line-drawing cost while F2 is on; only takes effect when the separate F2 toggle (`_state.CollisionWireframesVisible`) is also enabled; also emits a throttled print-on-change stats line | `RenderingDiagnostics.CollisionMeshWireframeEnabled` |
| `ACDREAM_WIRE_RADIUS` | companion knob to #337/`acdream_wire_mesh` | `=<float metres>` (`float.TryParse`, invariant culture; falls back to 30 if unparsable or ≤0) | sets the radius around the player within which `CollisionMeshWireframeEnabled` resolves polygon geometry | larger radius = more physics-BSP polygon extraction/line-drawing cost per frame; only matters while `ACDREAM_WIRE_MESH=1` | `RenderingDiagnostics.CollisionMeshWireframeRadius` |
## Deprecated
| Flag | Value | What it does | Side effects | Default | Read by |
@ -360,3 +342,20 @@ from the "must still exist" check.
| `ACDREAM_RENDER_BACKEND` | Selected the GL-vs-Vulkan backend. Campaign V deleted the OpenGL backend; Vulkan is the only one. Two comments still named it as a live co-requisite until 2026-08-24. | none |
| `ACDREAM_ANIM_SPEED_SCALE` | Animation-speed multiplier from the pre-retail-sequencer era; died with the 1.248x factor. | none |
| `ACDREAM_A8_AUDIT` | Phase A8 EnvCell batch/cull audit dump. Its only caller never existed; `EnvCellRenderer.CollectCellAuditLines` was unreachable and was deleted 2026-08-24. | `ACDREAM_PROBE_ENVCELL` |
| `ACDREAM_AIRBORNE_DIAG` | #42 airborne-sweep `[SWEEP]`/`[SWEEP-OBJ]` XY-drift trace. Investigation closed; stripped 2026-08-24 (#435) along with its 16 siblings below. | none |
| `ACDREAM_DUMP_ENTITY` | #119 tower-staircase HYDRATE/DRAW/WALK-REJECT entity watchlist. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_AUTOWALK` | Issue #63 server-initiated auto-walk trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_LIGHT` | #133 A7 dungeon-lighting `[light]`/`[light-detail]` trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_OUTSTAGE` | #131 outside-stage dynamics routing trace (also the `ACDREAM_DUMP_ENTITY` `[outstage-own]` watchlist consumer). Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_PHANTOM` | #113 phantom-shell/phantom-objs draw-mechanism trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_REACH` | #334 broadphase candidate-disposition trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_REMOTE_LANDING` | Bug A / issue #32 remote ground-contact landing trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_SHELL` | #78 cell-shell opaque-pass render trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_STEP_HEIGHTS` | Issue #338 step-up/step-down height provenance trace (including its unconditional once-per-process `AnnounceStepHeightProbeOnce` self-report). Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_STICKY` | R5-V3 issue #171 sticky-melee lifecycle/steer trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_SUPPORT` | Issue #337 `[support]`/`[geom]` collision-vs-visual classifier trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_TEXFLUSH` | #105 white-indoor-textures staged-upload trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_VIEWER` | #119-residual viewer/flood capture (tower-ascent replay). Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_WALK_MISS` | Issue #83 indoor walkable-plane miss trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_WIRE_MESH` | Issue #337 F2 overlay upgrade to real physics-BSP polygon edges. Investigation closed; stripped 2026-08-24 (#435) — F2 reverted to its proxy-cylinder overlay. | none |
| `ACDREAM_WIRE_RADIUS` | Companion radius knob for `ACDREAM_WIRE_MESH`. Stripped alongside it 2026-08-24 (#435). | none |

View file

@ -332,8 +332,6 @@ internal sealed class PlayerModeController :
{
moveTo.MoveToComplete = error =>
{
if (PhysicsDiagnostics.ProbeAutoWalkEnabled)
Console.WriteLine($"[autowalk-end] reason=complete err={error}");
if (error == WeenieError.None)
approachLifetime.PublishNaturalCompletion();
else

View file

@ -229,14 +229,6 @@ internal sealed class LiveEntityMotionRuntimeController
// CObjectMaint's object table and must still resolve here.
if (liveEntities.IsHidden(id))
{
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
{
Console.WriteLine(
$"[autowalk-host-miss] object=0x{id:X8} "
+ $"materialized={_liveEntities.ContainsWorldEntity(id)} "
+ $"registered={liveEntities.TryGetPhysicsHost(id, out _)} "
+ $"hidden={liveEntities.IsHidden(id)}");
}
return null;
}
if (liveEntities.TryGetPhysicsHost(id, out var existing))
@ -494,26 +486,6 @@ internal sealed class LiveEntityMotionRuntimeController
}
}
movement.PerformMovement(ms);
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
{
string target = turnPath.TargetGuid is { } targetGuid
? $"0x{targetGuid:X8}" : "null";
bool targetVisible = turnPath.TargetGuid is { } visibleGuid
&& _liveEntities.TryGetInteractionEligibleEntity(
visibleGuid,
out _);
bool targetHost = turnPath.TargetGuid is { } hostGuid
&& _liveEntities?.TryGetPhysicsHost(hostGuid, out _) == true;
var moveTo = movement.MoveTo;
Console.WriteLine(
$"[autowalk-turn-route] wire=0x{update.MotionState.MovementType:X2} "
+ $"routed={ms.Type} target={target} visible={targetVisible} "
+ $"host={targetHost} stop={mp.StopCompletelyFlag} "
+ $"initialized={moveTo?.Initialized ?? false} "
+ $"nodes={moveTo?.PendingActions.Count() ?? 0} "
+ $"command=0x{moveTo?.CurrentCommand ?? 0u:X8} "
+ $"pendingMotions={movement.Minterp.MotionsPending()}");
}
return true;
}

View file

@ -619,28 +619,6 @@ internal sealed class LiveEntityNetworkUpdateController
// the exact swing and carries it in Commands[].
if (update.Guid == _playerServerGuid)
{
// B.6 slice 1 (2026-05-14): trace inbound motion for the
// local player. One line per inbound UM, gated on
// ACDREAM_PROBE_AUTOWALK=1 (name kept through R4-V5).
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
{
string cmdHex = command.HasValue ? $"0x{command.Value:X4}" : "null";
string pathStr = update.MotionState.MoveToPath is { } p
? $"path=cell=0x{p.OriginCellId:X8},xyz=({p.OriginX:F2},{p.OriginY:F2},{p.OriginZ:F2}),minDist={p.MinDistance:F2},objDist={p.DistanceToObject:F2}"
: "path=null";
string spd = update.MotionState.ForwardSpeed is { } fs
? $"fwdSpd={fs:F2}"
: "fwdSpd=null";
string mtsSpd = update.MotionState.MoveToSpeed is { } ms
? $"mtSpd={ms:F2}"
: "mtSpd=null";
string mtsRun = update.MotionState.MoveToRunRate is { } mr
? $"mtRun={mr:F2}"
: "mtRun=null";
Console.WriteLine(System.FormattableString.Invariant(
$"[autowalk-mt] stance=0x{stance:X4} cmd={cmdHex} mt=0x{update.MotionState.MovementType:X2} isMoveTo={update.MotionState.IsServerControlledMoveTo} moveTowards={update.MotionState.MoveTowards} {pathStr} {spd} {mtsSpd} {mtsRun}"));
}
// R4-V5: retail unpack_movement dispatch for the local
// player — the SAME shape the remote branch uses below.
// Head (@300566): interrupt + unstick fire for EVERY
@ -700,11 +678,6 @@ internal sealed class LiveEntityNetworkUpdateController
}
if (localDispatch.RoutedMoveTo)
{
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
{
Console.WriteLine(System.FormattableString.Invariant(
$"[autowalk-begin] mt=0x{update.MotionState.MovementType:X2} movingTo={_playerController.Movement.IsMovingTo()} type={_playerController.MoveTo?.MovementTypeState}"));
}
return;
}
if (!localDispatch.AppliedInterpretedState)
@ -2128,21 +2101,6 @@ internal sealed class LiveEntityNetworkUpdateController
if (update.Guid == _playerServerGuid)
_authorityGate.ObserveAcceptedLocalPosition(update.Position.LandblockId);
// B.6 slice 1 (2026-05-14): trace inbound UpdatePosition cadence for
// the local player. Combined with [autowalk-mt] this answers
// whether ACE's broadcast frequency during a server-initiated
// auto-walk is dense enough to drive smooth visible motion (the
// Option C viability check from the design spec). Gated on
// ACDREAM_PROBE_AUTOWALK=1; skips remote entities.
if (update.Guid == _playerServerGuid
&& AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
{
string velStr = update.Velocity is { } v
? $"vel=({v.X:F2},{v.Y:F2},{v.Z:F2})"
: "vel=null";
Console.WriteLine(System.FormattableString.Invariant(
$"[autowalk-up] cell=0x{p.LandblockId:X8} pos=({p.PositionX:F2},{p.PositionY:F2},{p.PositionZ:F2}) world=({worldPos.X:F2},{worldPos.Y:F2},{worldPos.Z:F2}) {velStr} grounded={update.IsGrounded}"));
}
var rot = timestampDisposition is AcDream.Core.Physics.PositionTimestampDisposition.ForcePosition
? entity.Rotation
: new System.Numerics.Quaternion(p.RotationX, p.RotationY, p.RotationZ, p.RotationW);
@ -2685,14 +2643,6 @@ internal sealed class LiveEntityNetworkUpdateController
// first UP after unstick (bounded by the 1 s sticky lease).
bool snapSuppressedByStick = !IsPlayerGuid(update.Guid)
&& (rmState.Host?.PositionManager.GetStickyObjectId() ?? 0u) != 0u;
if (snapSuppressedByStick
&& AcDream.Core.Physics.PhysicsDiagnostics.ProbeStickyEnabled)
{
float snapDist = System.Numerics.Vector3.Distance(
worldPos, rmState.Body.Position);
Console.WriteLine(FormattableString.Invariant(
$"[sticky-snap-skip] guid=0x{update.Guid:X8} d={snapDist:F3} srv=({worldPos.X:F2},{worldPos.Y:F2}) body=({rmState.Body.Position.X:F2},{rmState.Body.Position.Y:F2})"));
}
// C4 routes 4a + 4b-2 + 4b-3 collapse: the complete near/far/
// teleport/leftover decision — including the dissolved LANDING
@ -2796,43 +2746,6 @@ internal sealed class LiveEntityNetworkUpdateController
_motionRuntime.EnsureRemoteMotionBindings(
rmState, aeForLand, update.Guid);
}
// Bug A investigation (2026-08-04, docs/ISSUES.md #32):
// the packet-side half of the landing capture, now fired
// for both guids (diagnostic-only — TEMPORARY, strip with
// the probe family; not behaviour).
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeRemoteLandingEnabled)
{
bool gravitySetForProbe = rmState.Body.HasGravity;
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLanding(
site: "controller",
guid: update.Guid,
airborneBefore: true,
gravitySet: gravitySetForProbe,
contact: rmState.Body.InContact,
onWalkable: rmState.Body.OnWalkable,
hasDefaultSink: rmState.Motion.DefaultSink is not null,
resolveIsOnGround: null,
sequencerStyle: aeForLand?.Sequencer?.CurrentStyle ?? 0,
sequencerMotion: aeForLand?.Sequencer?.CurrentMotion ?? 0);
if (!gravitySetForProbe)
{
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLandingGateNoOp(
"controller", update.Guid);
}
// Zero the sink-dispatch latches before reading them
// back — nothing at THIS site dispatches (the arming
// call lives only next to the per-tick HitGround).
AcDream.Core.Physics.PhysicsDiagnostics
.BeginRemoteLandingDispatchCapture();
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLandingAfter(
site: "controller",
guid: update.Guid,
hitGroundInvoked: false,
sequencerStyle: aeForLand?.Sequencer?.CurrentStyle ?? 0,
sequencerMotion: aeForLand?.Sequencer?.CurrentMotion ?? 0,
forwardCommand: rmState.Motion.InterpretedState.ForwardCommand);
}
}
}

View file

@ -1,372 +0,0 @@
using System.Collections.Immutable;
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Core.Rendering;
namespace AcDream.App.Rendering;
/// <summary>
/// #337 collision-mesh wireframe (2026-08-06 — TEMPORARY, strip with the #337
/// probe family).
///
/// <para>
/// The F2 collision overlay predating this class drew, for a BSP object, a
/// proxy cylinder sized from the object's registered BROADPHASE radius. That
/// answers "where does the collision system think this object roughly is" and
/// nothing more. The open question in Neftet is a different one — whether an
/// object's collision SURFACES are where its visual mesh is drawn — and a
/// proxy sphere cannot answer it in either direction.
/// </para>
///
/// <para>
/// This draws the actual geometry instead, in three colours that are meant to
/// be read against each other:
/// <list type="bullet">
/// <item><b>Cyan</b> — the object's real physics-BSP polygon edges, in world
/// space. These are the surfaces a body can stand on or be stopped by.
/// Where the cyan mesh sits away from the rock you can see, the collision
/// is displaced; where a visible rock has no cyan on it at all, it has no
/// collision geometry.</item>
/// <item><b>Magenta</b> — the same object's VISUAL mesh bounding box, from
/// the same prepared assets the renderer draws from. It is the reference
/// the cyan is judged against, so the comparison does not depend on the
/// eye's guess about where the visual "really" is.</item>
/// <item><b>Yellow</b> — the outdoor terrain surface under the player, as a
/// grid of the physics engine's own sampled heights. If a body is resting
/// on the yellow rather than on cyan, terrain is holding it up and the
/// object's collision is not involved at all.</item>
/// </list>
/// Dim orange keeps the old broadphase proxy visible so nothing the previous
/// overlay showed has been taken away.
/// </para>
///
/// <para>
/// Geometry is resolved through the SAME prepared collision accessors the
/// resolver queries (<c>PhysicsDataCache.GetFlatGfxObj</c> /
/// <c>GetVisualBounds</c>) and placed with the SAME world transform the
/// collision probes use, so this cannot draw a shape the collision system does
/// not actually hold. Reading the geometry by a second route is how AP-156
/// managed to report a sphere the registry never emitted.
/// </para>
///
/// <para>
/// Pure reads. Nothing here mutates physics, registry, or render state; the
/// caller owns the <see cref="DebugLineRenderer"/> frame.
/// </para>
/// </summary>
internal sealed class CollisionMeshWireframe
{
// Colours, in the order the class comment lists them.
private static readonly Vector3 PhysicsColor = new(0f, 1f, 1f);
private static readonly Vector3 VisualColor = new(1f, 0f, 1f);
private static readonly Vector3 TerrainColor = new(1f, 1f, 0f);
private static readonly Vector3 BroadphaseColor = new(0.45f, 0.22f, 0f);
/// <summary>
/// Per-frame line ceiling. Each line is 48 bytes in the debug renderer's
/// ring allocation, so this caps the overlay at ~1.9 MB a frame. Landblock
/// 0x8766 carries the largest single collision owner measured in the game
/// (an 81-cell footprint), and an uncapped walk of it would be the one
/// place this overlay falls over.
/// </summary>
private const int MaxLines = 40_000;
/// <summary>Per-object polygon ceiling, so one enormous formation cannot
/// consume the whole budget and hide every other object near it.</summary>
private const int MaxPolygonsPerObject = 4_000;
/// <summary>Half-width in metres of the terrain grid drawn under the
/// player, and its sample spacing.</summary>
private const float TerrainGridHalfWidth = 12f;
private const float TerrainGridStep = 2f;
private readonly PhysicsEngine _physics;
public CollisionMeshWireframe(PhysicsEngine physics)
=> _physics = physics ?? throw new ArgumentNullException(nameof(physics));
/// <summary>
/// Emit the overlay for everything within
/// <see cref="RenderingDiagnostics.CollisionMeshWireframeRadius"/> of
/// <paramref name="centre"/>. Returns what it drew so the caller can
/// report a capped frame rather than silently showing partial geometry.
/// </summary>
public CollisionMeshWireframeStats Draw(DebugLineRenderer lines, Vector3 centre)
{
ArgumentNullException.ThrowIfNull(lines);
float radius = RenderingDiagnostics.CollisionMeshWireframeRadius;
float radiusSquared = radius * radius;
var budget = new LineBudget(MaxLines);
int objects = 0;
int polygons = 0;
int withoutGeometry = 0;
PhysicsDataCache? cache = _physics.DataCache;
foreach (ShadowEntry shadow in _physics.ShadowObjects.AllEntriesForDebug())
{
// Objects register their part ORIGIN, which for a BSP part is
// routinely nowhere near the geometry itself (376 of the 973
// installed physics-BSP parts sit further from their own bounding
// centre than half their radius). Admitting on origin distance
// ALONE would drop exactly the large displaced-centre formations
// this overlay exists to look at, so the object's own radius is
// added to the window.
float reach = radius + shadow.Radius;
if (Vector3.DistanceSquared(shadow.Position, centre) > reach * reach)
continue;
objects++;
if (shadow.CollisionType != ShadowCollisionType.BSP)
{
DrawBroadphaseProxy(lines, in shadow, budget);
continue;
}
FlatGfxObjCollisionAsset? asset = cache?.GetFlatGfxObj(shadow.GfxObjId);
int drawn = DrawPhysicsPolygons(lines, in shadow, asset, centre, radiusSquared, budget);
polygons += drawn;
if (drawn == 0) withoutGeometry++;
DrawVisualBounds(lines, in shadow, cache?.GetVisualBounds(shadow.GfxObjId), budget);
DrawBroadphaseProxy(lines, in shadow, budget);
}
DrawTerrainGrid(lines, centre, budget);
return new CollisionMeshWireframeStats(
ObjectsConsidered: objects,
PolygonsDrawn: polygons,
ObjectsWithoutPhysicsGeometry: withoutGeometry,
LinesDrawn: budget.Used,
Capped: budget.Capped);
}
/// <summary>
/// Walk the object's physics BSP and emit one closed edge loop per polygon
/// the tree actually indexes. Polygons the tree does not reference are NOT
/// drawn: no query can reach them, so showing them would overstate the
/// collision surface. Returns the polygon count emitted.
/// </summary>
private static int DrawPhysicsPolygons(
DebugLineRenderer lines,
in ShadowEntry shadow,
FlatGfxObjCollisionAsset? asset,
Vector3 centre,
float radiusSquared,
LineBudget budget)
{
FlatPhysicsBsp? bsp = asset?.PhysicsBsp;
if (bsp is not { RootIndex: >= 0 } || bsp.Nodes.Length == 0)
return 0;
FlatPolygonTable table = bsp.PolygonTable;
ImmutableArray<Vector3> vertices = table.Vertices;
int emitted = 0;
foreach (FlatPhysicsBspNode node in bsp.Nodes)
{
FlatIndexRange indices = node.PolygonIndexRange;
for (int i = indices.Start; i < indices.EndExclusive; i++)
{
if (emitted >= MaxPolygonsPerObject || budget.Exhausted)
return emitted;
int polygonIndex = bsp.PolygonIndexStream[i];
if ((uint)polygonIndex >= (uint)table.Polygons.Length) continue;
FlatIndexRange span = table.Polygons[polygonIndex].VertexRange;
if (span.Count < 2) continue;
Vector3 first = ToWorld(vertices[span.Start], in shadow);
// Per-polygon distance rejection, AFTER the world transform:
// a big object admitted by the object-level window still only
// needs the faces near the player drawn.
if (Vector3.DistanceSquared(first, centre) > radiusSquared) continue;
Vector3 previous = first;
for (int v = span.Start + 1; v < span.EndExclusive; v++)
{
Vector3 current = ToWorld(vertices[v], in shadow);
if (!budget.TryAdd()) return emitted;
lines.AddLine(previous, current, PhysicsColor);
previous = current;
}
if (span.Count > 2)
{
if (!budget.TryAdd()) return emitted;
lines.AddLine(previous, first, PhysicsColor);
}
emitted++;
}
}
return emitted;
}
/// <summary>
/// The visual mesh box, placed with the SAME transform as the physics
/// polygons above. It is drawn as the object's own rotated box (eight
/// transformed corners, twelve edges) rather than as a world-axis-aligned
/// box, so a rotated object's magenta lines still bound its actual visual.
/// </summary>
private static void DrawVisualBounds(
DebugLineRenderer lines,
in ShadowEntry shadow,
GfxObjVisualBounds? visual,
LineBudget budget)
{
if (visual is null || budget.Exhausted) return;
Vector3 min = visual.Min;
Vector3 max = visual.Max;
Span<Vector3> corners =
[
ToWorld(new Vector3(min.X, min.Y, min.Z), in shadow),
ToWorld(new Vector3(max.X, min.Y, min.Z), in shadow),
ToWorld(new Vector3(max.X, max.Y, min.Z), in shadow),
ToWorld(new Vector3(min.X, max.Y, min.Z), in shadow),
ToWorld(new Vector3(min.X, min.Y, max.Z), in shadow),
ToWorld(new Vector3(max.X, min.Y, max.Z), in shadow),
ToWorld(new Vector3(max.X, max.Y, max.Z), in shadow),
ToWorld(new Vector3(min.X, max.Y, max.Z), in shadow),
];
ReadOnlySpan<int> edges =
[
0, 1, 1, 2, 2, 3, 3, 0,
4, 5, 5, 6, 6, 7, 7, 4,
0, 4, 1, 5, 2, 6, 3, 7,
];
for (int e = 0; e < edges.Length; e += 2)
{
if (!budget.TryAdd()) return;
lines.AddLine(corners[edges[e]], corners[edges[e + 1]], VisualColor);
}
}
/// <summary>
/// The registered broadphase shape — what the pre-#337 overlay showed, and
/// what the collision system's reach filter measures against. Kept so this
/// overlay is a superset of the one it replaces.
/// </summary>
private static void DrawBroadphaseProxy(
DebugLineRenderer lines,
in ShadowEntry shadow,
LineBudget budget)
{
// AddCylinder emits a fixed 36 lines. Reserve them together so a
// partial ring cannot be drawn.
if (!budget.TryAdd(36)) return;
if (shadow.CollisionType == ShadowCollisionType.Cylinder)
{
float height = shadow.CylHeight > 0f ? shadow.CylHeight : shadow.Radius * 2f;
lines.AddCylinder(shadow.Position, shadow.Radius, height, BroadphaseColor);
return;
}
lines.AddCylinder(
shadow.Position - new Vector3(0f, 0f, shadow.Radius),
shadow.Radius,
shadow.Radius * 2f,
BroadphaseColor);
}
/// <summary>
/// The terrain surface under the player, sampled through the physics
/// engine's own height resolver — the same numbers the resolver grounds
/// against, not a re-derivation. Drawn as a grid rather than as the single
/// containing triangle so the slope around the player reads at a glance.
/// </summary>
private void DrawTerrainGrid(DebugLineRenderer lines, Vector3 centre, LineBudget budget)
{
int steps = (int)(TerrainGridHalfWidth * 2f / TerrainGridStep);
float originX = centre.X - TerrainGridHalfWidth;
float originY = centre.Y - TerrainGridHalfWidth;
for (int ix = 0; ix <= steps; ix++)
{
for (int iy = 0; iy <= steps; iy++)
{
float x = originX + ix * TerrainGridStep;
float y = originY + iy * TerrainGridStep;
float? z = _physics.SampleTerrainZ(x, y);
if (z is null) continue;
var here = new Vector3(x, y, z.Value);
if (ix < steps)
{
float nx = x + TerrainGridStep;
if (_physics.SampleTerrainZ(nx, y) is { } nz)
{
if (!budget.TryAdd()) return;
lines.AddLine(here, new Vector3(nx, y, nz), TerrainColor);
}
}
if (iy < steps)
{
float ny = y + TerrainGridStep;
if (_physics.SampleTerrainZ(x, ny) is { } nz2)
{
if (!budget.TryAdd()) return;
lines.AddLine(here, new Vector3(x, ny, nz2), TerrainColor);
}
}
}
}
}
/// <summary>
/// The one placement formula, matching the <c>[resolve-bldg]</c> probe's
/// world transform for a shadow part
/// (<c>TransitionTypes.FindObjCollisionsInCell</c>): scale in the part's
/// own frame, then rotate, then translate to the registered position.
/// </summary>
private static Vector3 ToWorld(Vector3 local, in ShadowEntry shadow)
=> shadow.Position + Vector3.Transform(local * shadow.Scale, shadow.Rotation);
/// <summary>
/// Mutable line counter shared across the draw. A class rather than a
/// struct so the per-shape helpers can be static and still share it
/// without ref-plumbing through every signature.
/// </summary>
private sealed class LineBudget(int limit)
{
public int Used { get; private set; }
public bool Capped { get; private set; }
public bool Exhausted => Used >= limit;
public bool TryAdd(int count = 1)
{
if (Used + count > limit)
{
Capped = true;
return false;
}
Used += count;
return true;
}
}
}
/// <summary>What one <see cref="CollisionMeshWireframe.Draw"/> emitted.
/// <paramref name="ObjectsWithoutPhysicsGeometry"/> is the interesting one: a
/// non-zero count means objects near the player carry no reachable collision
/// polygons at all.</summary>
internal readonly record struct CollisionMeshWireframeStats(
int ObjectsConsidered,
int PolygonsDrawn,
int ObjectsWithoutPhysicsGeometry,
int LinesDrawn,
bool Capped);

View file

@ -302,37 +302,6 @@ internal sealed class RetailPViewPassExecutor :
slice,
sliceIndex);
public void EmitOutStageOwner(
WorldEntity entity,
Vector3 sphereCenter,
float sphereRadius,
int sliceIndex,
bool passed) =>
_diagnostics.EmitOutStageOwner(
RenderingDiagnostics.ProbeOutStageEnabled,
RenderingDiagnostics.DumpEntitySourceIds,
entity,
sphereCenter,
sphereRadius,
sliceIndex,
passed);
public void EmitOutStageRouting(
int sliceIndex,
IReadOnlyList<WorldEntity> entities,
ViewconeCuller viewcone) =>
_diagnostics.EmitOutStageRouting(
RenderingDiagnostics.ProbeOutStageEnabled,
sliceIndex,
entities,
viewcone);
public void EmitPhantomObjects(uint cellId, int survivorCount) =>
_diagnostics.EmitPhantomObjects(
RenderingDiagnostics.ProbePhantomEnabled,
cellId,
survivorCount);
public void DrawLandscapeSlice(
RetailPViewFrameInput frame,
RetailPViewLandscapeSliceContext context)
@ -449,11 +418,6 @@ internal sealed class RetailPViewPassExecutor :
_particleClassifications.ReplaceOutdoor(context.ParticleOwnerIds);
_diagnostics.EmitOutStageParticles(
RenderingDiagnostics.ProbeOutStageEnabled,
_particles,
_particleClassifications.Outdoor);
if (!frame.RootCell.IsOutdoorNode
&& _particleClassifications.Outdoor.Count > 0
&& _particles is not null
@ -575,7 +539,6 @@ internal sealed class RetailPViewPassExecutor :
RetailPViewFrameInput frame,
RetailPViewFrameResult result) =>
_diagnostics.EmitRetailPViewDiagnostics(
RenderingDiagnostics.ProbeViewerEnabled,
RenderingDiagnostics.ProbeVisibilityEnabled,
RenderingDiagnostics.ProbeFlapEnabled,
result,
@ -584,7 +547,6 @@ internal sealed class RetailPViewPassExecutor :
frame.PlayerCellId,
frame.CameraWorldPosition,
frame.PlayerViewPosition,
frame.CameraView,
frame.CameraCellResolution);
private void DrawPortalDepthWrite(

View file

@ -699,15 +699,6 @@ public sealed class RetailPViewRenderer
r);
if (ownerPass)
_lateParticleOwnerScratch.Add(e.Id);
// #131 owner watchlist (throwaway): ACDREAM_DUMP_ENTITY ids
// double as an ENTITY-id watchlist here — one line per watched
// outdoor-static owner per CHANGE of its cone verdict.
passes.EmitOutStageOwner(
e,
c,
r,
probeSliceIndex,
ownerPass);
}
}
foreach (var e in _outsideStageDynamics)
@ -734,10 +725,6 @@ public sealed class RetailPViewRenderer
probeSliceIndex,
0);
}
passes.EmitOutStageRouting(
probeSliceIndex,
_outsideStageDynamics,
viewcone);
_candidateObserver?.ObservePViewBucket(
CurrentRenderPViewRoute.LandscapeOutsideDynamic,
probeSliceIndex,
@ -1093,9 +1080,6 @@ public sealed class RetailPViewRenderer
int survivors = _allCellStatics.Count - survivorsBefore;
if (survivors > 0)
_cellObjCells.Add(cellId);
// BR-2 phantom-site probe (T3-updated): post-viewcone survivors.
passes.EmitPhantomObjects(cellId, survivors);
}
// ONE batched static-object draw for every visible cell (was N per-cell
@ -1210,8 +1194,6 @@ public sealed class RetailPViewRenderer
private bool LegacyPartitionDiagnosticsEnabled =>
_partitionObserver is not null
|| AcDream.Core.Rendering.RenderingDiagnostics.ProbeOutStageEnabled
|| AcDream.Core.Rendering.RenderingDiagnostics.ProbePhantomEnabled
|| AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled
|| AcDream.App.Streaming.EntityVanishProbe.Enabled;
@ -1380,17 +1362,6 @@ public interface IRetailPViewPassExecutor
ClipFrameAssembly clipAssembly,
ClipViewSlice slice,
int sliceIndex);
void EmitOutStageOwner(
WorldEntity entity,
Vector3 sphereCenter,
float sphereRadius,
int sliceIndex,
bool passed);
void EmitOutStageRouting(
int sliceIndex,
IReadOnlyList<WorldEntity> entities,
ViewconeCuller viewcone);
void EmitPhantomObjects(uint cellId, int survivorCount);
void DrawLandscapeSlice(RetailPViewFrameInput frame, RetailPViewLandscapeSliceContext context);
void DrawLandscapeSliceLate(RetailPViewFrameInput frame, RetailPViewLandscapeLateSliceContext context);
void ClearInteriorDepth();

View file

@ -993,48 +993,6 @@ public sealed partial class EnvCellRenderer :
_lastFrameStats.TrianglesDrawn += (dc.renderData.Batches.Count > 0
? dc.renderData.Batches[0].IndexCount / 3
: 0) * dc.count;
// Issue #78 (2026-05-31) [shell] probe (ACDREAM_PROBE_SHELL) — THROWAWAY.
// Per opaque-pass call: totals + per visible (filtered) cell whether it is
// present in the prepared snapshot, and its geometry/flags. Answers why the
// interior walls/ceiling don't appear: NOSNAP / gfx=0 ⇒ no shell geometry
// prepared for the cell; idx>0 + zh>0 ⇒ prepared but missing bindless texture
// (invisible); idx>0 + zh=0 + tr=0 ⇒ opaque geometry drawn (fault is depth/
// occlusion or the geometry isn't the wall). Opaque pass only (halves noise).
if (renderPass == WbRenderPass.Opaque
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeShellEnabled)
{
var sb = new System.Text.StringBuilder(256);
sb.Append("[shell] filter=").Append(filter?.Count ?? -1)
.Append(" drawCalls=").Append(drawCalls.Count)
.Append(" inst=").Append(allInstances.Count)
.Append(" tris=").Append(_lastFrameStats.TrianglesDrawn);
if (filter != null)
{
foreach (var cellId in filter)
{
if (!snapshot.BatchedByCell.TryGetValue(cellId, out var gfxDict))
{
sb.Append(" [0x").Append(cellId.ToString("X8")).Append(":NOSNAP]");
continue;
}
int gfxN = 0, tf = 0, batch = 0, idx = 0, tr = 0, zh = 0;
foreach (var (gfxObjId, transforms) in gfxDict)
{
gfxN++; tf += transforms.Count;
var rd = _meshManager.TryGetRenderData(gfxObjId);
if (rd != null)
foreach (var b in rd.Batches)
{ batch++; idx += b.IndexCount; if (b.IsTransparent) tr++; if (!b.TextureSlot.IsAssigned) zh++; }
}
sb.Append(" [0x").Append(cellId.ToString("X8"))
.Append(":gfx=").Append(gfxN).Append(" tf=").Append(tf)
.Append(" batch=").Append(batch).Append(" idx=").Append(idx)
.Append(" tr=").Append(tr).Append(" zh=").Append(zh).Append(']');
}
}
System.Console.WriteLine(sb.ToString());
}
}
}

View file

@ -780,21 +780,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
private readonly HashSet<ulong> _missRequested = new();
private readonly HashSet<ulong> _missLogged = new();
// #119 decisive probe (2026-06-11): ACDREAM_DUMP_ENTITY one-shot entity
// dump. Keyed by entity Id; the stored signature re-emits the header line
// whenever (MeshRefs count, cache batch count, zero-translation count,
// culled) changes — e.g. the Tier-1 populate landing one frame after the
// first slow-path draw. The full per-part listing prints only on first
// sight. Inert (one Count==0 check per new entity) when the env var is
// unset. Render-thread only.
private readonly Dictionary<uint, (int MeshRefCount, int CacheBatches, int ZeroT, bool Culled)> _entityDumpSig = new();
// Rate limiter for [dump-entity] WALK-REJECT lines: a rejected entity
// re-tests every frame; emit the first rejection per entity then every
// 300th (~5 s at 60 fps). Static because WalkEntitiesInto is static;
// render-thread only like the walk itself.
private static readonly Dictionary<uint, int> _walkRejectCounts = new();
// CPU + GPU timing for [WB-DIAG] under ACDREAM_WB_DIAG=1. The GPU samples
// are written by the RHI arm's SampleRhiTimers (WbDrawDispatcher.Rhi.cs)
// from the device's own timer pool; the raw-GL query-object ring that used
@ -1200,7 +1185,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
if (!cellInVis)
{
if (shellScoped) result.BuildingShellAnchorReject++;
MaybeEmitWalkRejectDump(entity, "visibleCellIds-miss");
if (isCellEntity && RenderingDiagnostics.ProbeIndoorCullEnabled
&& indoorProbeState!.ShouldEmit(cellProbeId))
{
@ -1226,7 +1210,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
if (!aabbVisible)
{
MaybeEmitWalkRejectDump(entity, "frustum");
if (isCellEntity && RenderingDiagnostics.ProbeIndoorCullEnabled
&& indoorProbeState!.ShouldEmit(cellProbeId))
{
@ -1324,117 +1307,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
}
}
/// <summary>
/// #119 decisive probe: rate-limited <c>[dump-entity] WALK-REJECT</c> line
/// for an <c>ACDREAM_DUMP_ENTITY</c>-targeted entity that the walk filtered
/// out (visibleCellIds gate / per-entity frustum). Absence of any DRAW dump
/// plus presence of these lines attributes "entity exists but never reaches
/// the draw loop" to the specific gate. Inert when the target set is empty.
/// </summary>
private static void MaybeEmitWalkRejectDump(WorldEntity entity, string reason)
{
var targets = RenderingDiagnostics.DumpEntitySourceIds;
if (targets.Count == 0 || !targets.Contains(entity.SourceGfxObjOrSetupId)) return;
_walkRejectCounts.TryGetValue(entity.Id, out int n);
_walkRejectCounts[entity.Id] = n + 1;
if (n % 300 != 0) return;
Console.WriteLine(
$"[dump-entity] WALK-REJECT id=0x{entity.Id:X8} src=0x{entity.SourceGfxObjOrSetupId:X8} " +
$"reason={reason} parentCell=0x{(entity.ParentCellId ?? 0u):X8} " +
$"pos=({entity.Position.X:F2},{entity.Position.Y:F2},{entity.Position.Z:F2}) n={n + 1}");
}
/// <summary>
/// #119 decisive probe: per-entity state dump at draw time for
/// <c>ACDREAM_DUMP_ENTITY</c>-targeted entities. First sight prints a
/// header + every MeshRef's GfxObj id, part-transform translation, and
/// loaded flag; afterwards a compact header re-emits only when the
/// (meshRefs, cacheBatches, zeroTranslations, culled) signature changes.
/// Discriminates H-A (hydration-time MeshRef corruption: translations
/// collapsed to ~zero / missing parts) from H-B (Tier-1 cache holding a
/// partial or stale batch set) from H-C (both healthy ⇒ draw-side compose).
/// </summary>
private void MaybeEmitEntityDump(
in RenderInstanceCandidate entity,
uint landblockId,
bool culled,
IReadOnlyList<RenderInstanceTuple> tuples)
{
var targets = RenderingDiagnostics.DumpEntitySourceIds;
if (targets.Count == 0 || !targets.Contains(entity.SourceId))
return;
int zeroT = 0;
int refsCount = 0;
float tzMin = float.MaxValue, tzMax = float.MinValue;
for (int i = 0; i < tuples.Count; i++)
{
RenderInstanceTuple tuple = tuples[i];
if (tuple.Candidate.LocalEntityId != entity.LocalEntityId
|| tuple.Candidate.TupleLandblockId
!= entity.TupleLandblockId)
{
continue;
}
refsCount++;
Vector3 t = tuple.MeshRef.PartTransform.Translation;
if (t.LengthSquared() < 1e-9f) zeroT++;
if (t.Z < tzMin) tzMin = t.Z;
if (t.Z > tzMax) tzMax = t.Z;
}
int cacheBatches = -1;
int restZero = 0;
float rzMin = float.MaxValue, rzMax = float.MinValue;
if (_cache.TryGet(entity.Id, landblockId, out var cacheEntry))
{
cacheBatches = cacheEntry!.Batches.Length;
foreach (var b in cacheEntry.Batches)
{
var t = b.RestPose.Translation;
if (t.LengthSquared() < 1e-9f) restZero++;
if (t.Z < rzMin) rzMin = t.Z;
if (t.Z > rzMax) rzMax = t.Z;
}
}
var sig = (refsCount, cacheBatches, zeroT, culled);
bool first = !_entityDumpSig.TryGetValue(entity.Id, out var prev);
if (!first && prev == sig) return;
_entityDumpSig[entity.Id] = sig;
string cacheStr = cacheBatches < 0
? (_tier1CacheDisabled ? "disabled" : "miss")
: $"hit:{cacheBatches} restZero={restZero} restZ=[{rzMin:F2}..{rzMax:F2}]";
Console.WriteLine(
$"[dump-entity] DRAW{(first ? "" : "-CHANGED")} id=0x{entity.Id:X8} src=0x{entity.SourceId:X8} " +
$"lb=0x{landblockId:X8} cell=0x{entity.ParentCellId:X8} " +
$"pos=({entity.Position.X:F2},{entity.Position.Y:F2},{entity.Position.Z:F2}) scale={entity.Scale:F2} " +
$"meshRefs={refsCount} tZero={zeroT} tZ=[{tzMin:F2}..{tzMax:F2}] cache={cacheStr} culled={culled}");
if (first)
{
for (int i = 0; i < tuples.Count; i++)
{
RenderInstanceTuple tuple = tuples[i];
if (tuple.Candidate.LocalEntityId
!= entity.LocalEntityId
|| tuple.Candidate.TupleLandblockId
!= entity.TupleLandblockId)
{
continue;
}
MeshRef mr = tuple.MeshRef;
var t = mr.PartTransform.Translation;
bool loaded = _meshAdapter.TryGetRenderData(mr.GfxObjId) is not null;
Console.WriteLine(
$"[dump-entity] part[{tuple.MeshRefIndex:D2}] gfx=0x{mr.GfxObjId:X8} t=({t.X:F3},{t.Y:F3},{t.Z:F3}) loaded={loaded}");
}
}
}
public void Draw(
ICamera camera,
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
@ -1651,15 +1523,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
? new Vector2(lighting.Luminosity, lighting.Diffuse)
: new Vector2(0f, 1f);
// #119 decisive probe: one-shot dump (+ change re-emission) for
// ACDREAM_DUMP_ENTITY-targeted entities. Before the culled-continue
// so a routed-out entity still reports its state.
MaybeEmitEntityDump(
in entity,
cacheLb,
_currentEntityCulled,
_candidateTupleScratch);
// #176 seam-draw probe: any entity parented to a target cell reports
// its position + light set (a floor-coincident static/plate would be
// the z-fight's second draw; the player entity is the positive

View file

@ -589,11 +589,6 @@ public sealed class WbMeshAdapter
meshManager.RequeueStagedMeshData(m);
meshManager.SetArenaBackpressure(arenaBackpressured);
bool texProbe = AcDream.Core.Rendering.RenderingDiagnostics.ProbeTexFlushEnabled;
var pendingBefore = texProbe
? meshManager.GetPendingTextureUpdateStats()
: default;
// #105 root cause (2026-06-10): TextureAtlasManager.AddTexture only STAGES
// immutable decoded payloads in ManagedGLTextureArray._pendingUpdates — the
// actual TexSubImage3D copies + mipmap regeneration happen in
@ -643,9 +638,6 @@ public sealed class WbMeshAdapter
LastBufferAllocationBytes = uploadBudget.BufferAllocationBytes;
LastBufferCopyBytes = uploadBudget.BufferCopyBytes;
LastNewBufferCount = uploadBudget.NewBufferCount;
if (texProbe)
EmitTexFlushProbe(pendingBefore);
}
private static void RejectUnsupportedHead(
@ -663,32 +655,6 @@ public sealed class WbMeshAdapter
meshManager.RejectUnsupportedStagedUpload(rejected, error);
}
// #105 apparatus state — see RenderingDiagnostics.ProbeTexFlushEnabled.
private int _lastTexFlushBefore = -1;
private int _texFlushHeartbeat;
/// <summary>
/// #105 apparatus: one <c>[tex-flush]</c> line on change of the staged-texture
/// pending picture (plus a ~10 s heartbeat while anything is stuck). A healthy
/// frame ends with <c>after=0</c>; <c>before==after&gt;0</c> persisting at
/// standstill is the white-walls mechanism live (staged uploads never applied).
/// </summary>
private void EmitTexFlushProbe((int PendingUpdates, int ArraysWithPending, int TotalArrays) before)
{
var after = _meshManager!.GetPendingTextureUpdateStats();
bool changed = before.PendingUpdates != _lastTexFlushBefore;
bool flushed = after.PendingUpdates != before.PendingUpdates;
bool heartbeat = after.PendingUpdates > 0 && ++_texFlushHeartbeat >= 600;
if (!changed && !flushed && !heartbeat) return;
_texFlushHeartbeat = 0;
_lastTexFlushBefore = before.PendingUpdates;
Console.WriteLine(
$"[tex-flush] before={before.PendingUpdates} after={after.PendingUpdates}" +
$" arrays={after.ArraysWithPending}/{after.TotalArrays}" +
$" (arraysBefore={before.ArraysWithPending})");
}
/// <inheritdoc/>
public void Dispose()
{

View file

@ -51,15 +51,11 @@ internal sealed class WorldRenderDiagnostics
{
private readonly IRenderGlStateReader _gl;
private readonly IRenderFrameDiagnosticLog _log;
private readonly HashSet<uint> _lastViewerFloodCells = [];
private readonly HashSet<uint> _outStageUnmatched = [];
private readonly HashSet<uint> _outStageMatched = [];
private readonly Stopwatch _terrainStopwatch = new();
private readonly RollingTimingSampleWindow _terrainSamples = new(256);
private string? _lastRenderSignature;
private int _renderSignatureFrame;
private int _renderSignatureStableFrames;
private string? _lastViewerSignature;
private string? _lastGlStateSignature;
private long _glStateFrame;
private long _glStateStableFrames;
@ -68,10 +64,6 @@ internal sealed class WorldRenderDiagnostics
private long _postWorldGlStateStableFrames;
private string? _lastScissorSignature;
private long _scissorSequence;
private string? _lastOutStageSignature;
private string? _lastOutStageRoutingSignature;
private readonly Dictionary<uint, bool> _outStageOwnerVerdicts = [];
private readonly Dictionary<uint, int> _phantomObjectSignatures = [];
private string? _lastClipRouteSignature;
private long _clipRouteSequence;
private readonly List<uint> _clipRouteCellKeys = [];
@ -258,75 +250,6 @@ internal sealed class WorldRenderDiagnostics
_log.WriteLine($"[clip-route] n={_clipRouteSequence} {signature}");
}
public void EmitOutStageOwner(
bool enabled,
IReadOnlySet<uint> watchedEntityIds,
WorldEntity entity,
Vector3 sphereCenter,
float sphereRadius,
int sliceIndex,
bool passed)
{
if (!enabled
|| !watchedEntityIds.Contains(entity.Id)
|| (_outStageOwnerVerdicts.TryGetValue(entity.Id, out bool previous)
&& previous == passed))
{
return;
}
_outStageOwnerVerdicts[entity.Id] = passed;
_log.WriteLine(FormattableString.Invariant(
$"[outstage-own] id=0x{entity.Id:X8} src=0x{entity.SourceGfxObjOrSetupId:X8} pos=({entity.Position.X:F1},{entity.Position.Y:F1},{entity.Position.Z:F1}) c=({sphereCenter.X:F1},{sphereCenter.Y:F1},{sphereCenter.Z:F1}) r={sphereRadius:F1} slice={sliceIndex} {(passed ? "PASS" : "CULL")}"));
}
public void EmitOutStageRouting(
bool enabled,
int sliceIndex,
IReadOnlyList<WorldEntity> entities,
ViewconeCuller viewcone)
{
if (!enabled)
return;
var text = new StringBuilder(192);
text.Append("slice=").Append(sliceIndex)
.Append(" outStage=").Append(entities.Count).Append(" [");
for (int i = 0; i < entities.Count; i++)
{
WorldEntity entity = entities[i];
EntitySphere(entity, out Vector3 center, out float radius);
bool passed = viewcone.SphereVisibleInOutsideSlice(
sliceIndex,
center,
radius);
if (i > 0)
text.Append(' ');
text.Append(FormattableString.Invariant(
$"0x{(entity.ServerGuid != 0 ? entity.ServerGuid : entity.Id):X8}(s{entity.SourceGfxObjOrSetupId:X8}):{(passed ? "PASS" : "CULL")}:r={radius:F1}"));
}
text.Append(']');
string signature = text.ToString();
if (signature == _lastOutStageRoutingSignature)
return;
_lastOutStageRoutingSignature = signature;
_log.WriteLine("[outstage] " + signature);
}
public void EmitPhantomObjects(bool enabled, uint cellId, int survivorCount)
{
if (!enabled
|| (_phantomObjectSignatures.TryGetValue(cellId, out int previous)
&& previous == survivorCount))
{
return;
}
_phantomObjectSignatures[cellId] = survivorCount;
_log.WriteLine(
$"[phantom-objs] cell=0x{cellId:X8} entities={survivorCount} (drawn unclipped, no viewcone)");
}
public void EmitSeamMask(
bool enabled,
IReadOnlySet<uint> targetCells,
@ -374,57 +297,7 @@ internal sealed class WorldRenderDiagnostics
$"[pv-input] outRoot={root} flood={portalFrame.OrderedVisibleCells.Count} eye=({eye.X:F6},{eye.Y:F6},{eye.Z:F6}) player=({player.X:F6},{player.Y:F6},{player.Z:F6}) rawPlayer=({rawPlayer.X:F6},{rawPlayer.Y:F6},{rawPlayer.Z:F6}) yaw={yaw:F8} {terrain} vp=[{vp.M11:F6} {vp.M13:F6} {vp.M22:F6} {vp.M31:F6} {vp.M33:F6} {vp.M41:F6} {vp.M42:F6} {vp.M43:F6}]"));
}
public void EmitOutStageParticles(
bool enabled,
ParticleSystem? particles,
IReadOnlySet<uint> ownerIds)
{
if (!enabled || particles is null)
return;
int matched = 0;
int attached = 0;
int unattached = 0;
_outStageUnmatched.Clear();
_outStageMatched.Clear();
foreach (var (emitter, _) in particles.EnumerateLive())
{
if (emitter.AttachedObjectId == 0)
{
unattached++;
continue;
}
attached++;
if (ownerIds.Contains(emitter.AttachedObjectId))
{
matched++;
if (_outStageMatched.Count < 48)
_outStageMatched.Add(emitter.AttachedObjectId);
}
else if (_outStageUnmatched.Count < 12)
{
_outStageUnmatched.Add(emitter.AttachedObjectId);
}
}
var unmatched = new StringBuilder(96);
foreach (uint id in _outStageUnmatched)
unmatched.Append(FormattableString.Invariant($" 0x{id:X8}"));
var matchedIds = new StringBuilder(192);
foreach (uint id in _outStageMatched)
matchedIds.Append(FormattableString.Invariant($" 0x{id:X8}"));
string signature = FormattableString.Invariant(
$"ids={ownerIds.Count} attachedEmitters={attached} matched={matched} unattached={unattached} matchedIds=[{matchedIds}] unmatchedIds=[{unmatched}]");
if (signature == _lastOutStageSignature)
return;
_lastOutStageSignature = signature;
_log.WriteLine("[outstage-pt] " + signature);
}
public void EmitRetailPViewDiagnostics(
bool viewerEnabled,
bool visibilityEnabled,
bool flapEnabled,
RetailPViewFrameResult result,
@ -433,22 +306,8 @@ internal sealed class WorldRenderDiagnostics
uint playerCellId,
Vector3 cameraPosition,
Vector3 playerPosition,
Matrix4x4 cameraView,
CameraCellResolution cameraCellResolution)
{
if (viewerEnabled)
{
string signature = FormattableString.Invariant(
$"root=0x{clipRoot.CellId:X8}{(clipRoot.IsOutdoorNode ? "(OUT)" : string.Empty)} flood={result.PortalFrame.OrderedVisibleCells.Count} outPolys={result.PortalFrame.OutsideView.Polygons.Count} pCell=0x{playerCellId:X8}");
if (signature != _lastViewerSignature)
{
_lastViewerSignature = signature;
_log.WriteLine(FormattableString.Invariant(
$"[viewer] {signature} eye=({cameraPosition.X:F3},{cameraPosition.Y:F3},{cameraPosition.Z:F3}) fwd=({-cameraView.M13:F4},{-cameraView.M23:F4},{-cameraView.M33:F4}) viewerCell=0x{viewerCellId:X8}"));
EmitViewerDiff(result.PortalFrame.OrderedVisibleCells);
}
}
if (visibilityEnabled)
{
AcDream.Core.Rendering.RenderingDiagnostics.EmitVis(
@ -619,49 +478,6 @@ internal sealed class WorldRenderDiagnostics
+ $"stencil={(state.Stencil ? 1 : 0)} "
+ $"clip=0x{state.ClipBits:X2} err=0x{state.Error:X}";
private void EmitViewerDiff(IReadOnlyList<uint> current)
{
var text = new StringBuilder(96);
text.Append("[viewer-diff] added=[");
bool first = true;
foreach (uint cell in current)
{
if (_lastViewerFloodCells.Contains(cell))
continue;
if (!first)
text.Append(',');
text.Append("0x").Append(cell.ToString("X8"));
first = false;
}
text.Append("] removed=[");
first = true;
foreach (uint cell in _lastViewerFloodCells)
{
bool present = false;
for (int index = 0; index < current.Count; index++)
{
if (current[index] == cell)
{
present = true;
break;
}
}
if (present)
continue;
if (!first)
text.Append(',');
text.Append("0x").Append(cell.ToString("X8"));
first = false;
}
text.Append(']');
_log.WriteLine(text.ToString());
_lastViewerFloodCells.Clear();
foreach (uint cell in current)
_lastViewerFloodCells.Add(cell);
}
private static string FormatVector(Vector3 value)
{
static float Quantize(float component) => MathF.Round(component * 20f) / 20f;
@ -748,15 +564,4 @@ internal sealed class WorldRenderDiagnostics
.Append(" live=").Append(partition.Dynamics.Count)
.ToString();
}
private static void EntitySphere(
WorldEntity entity,
out Vector3 center,
out float radius)
{
if (entity.AabbDirty)
entity.RefreshAabb();
center = (entity.AabbMin + entity.AabbMax) * 0.5f;
radius = (entity.AabbMax - entity.AabbMin).Length() * 0.5f;
}
}

View file

@ -513,17 +513,6 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation
// (150..1500 m) fog and hid the fact that the sky pass was being
// fogged at all (see SkyRenderer / sky.frag).
_lightingUbo?.Upload(ubo);
RenderingDiagnostics.EmitLight(
insideCell: roots.PlayerInsideCell,
ambientR: _lighting.CurrentAmbient.AmbientColor.X,
ambientG: _lighting.CurrentAmbient.AmbientColor.Y,
ambientB: _lighting.CurrentAmbient.AmbientColor.Z,
sunIntensity: _lighting.Sun?.Intensity ?? 0f,
registeredLights: _lighting.RegisteredCount,
activeLights: (int)ubo.CellAmbient.W,
playerCellId: roots.PlayerRoot?.CellId ?? 0u,
lights: _lighting);
}
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells)

View file

@ -64,10 +64,6 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
private readonly DebugVmRenderFactsPublisher _debugVm;
private readonly bool _debugVmConsumerActive;
private int _debugDrawLogCount;
// #337 (TEMPORARY): built on first use so the ordinary overlay path and
// every headless/no-window host pay nothing for it.
private CollisionMeshWireframe? _meshWireframe;
private CollisionMeshWireframeStats _lastMeshStats = new(-1, -1, -1, -1, false);
public WorldSceneDiagnosticsController(
WorldRenderDiagnostics diagnostics,
@ -219,18 +215,6 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
if (!_state.CollisionWireframesVisible || _lines is null)
return;
// #337 (2026-08-06 — TEMPORARY): ACDREAM_WIRE_MESH=1 replaces the
// broadphase-proxy overlay below with the objects' real physics-BSP
// polygon edges beside their visual mesh boxes and the terrain surface
// — see CollisionMeshWireframe for why the proxy cannot answer the
// question this mode exists for. Default off; F2 keeps its old
// behaviour otherwise.
if (RenderingDiagnostics.CollisionMeshWireframeEnabled)
{
DrawCollisionMesh(in camera);
return;
}
_lines.Begin();
int drawn = 0;
foreach (ShadowEntry shadow in _physics.ShadowObjects.AllEntriesForDebug())
@ -271,50 +255,6 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
_lines.Flush(camera.Camera.View, camera.Projection);
}
/// <summary>
/// #337 (2026-08-06 — TEMPORARY, strip with the probe family). Centres on
/// the player when there is one, else on the camera, so the fly-camera
/// mode can inspect geometry too.
/// </summary>
private void DrawCollisionMesh(in WorldCameraFrame camera)
{
Vector3 centre = _mode.IsPlayerMode && _player.Controller is { } controller
? controller.Position
: camera.Position;
_meshWireframe ??= new CollisionMeshWireframe(_physics);
_lines!.Begin();
CollisionMeshWireframeStats stats = _meshWireframe.Draw(_lines, centre);
if (_mode.IsPlayerMode && _player.Controller is { } player)
{
_lines.AddCylinder(
player.Position,
DebugVmRenderFactsPublisher.PlayerCollisionRadius,
1.8f,
new Vector3(1f, 0f, 0f));
}
_lines.Flush(camera.Camera.View, camera.Projection);
// A capped frame is showing PARTIAL geometry, which would otherwise be
// indistinguishable from an object that has none — exactly the
// misreading this overlay exists to prevent. Say so, throttled, rather
// than letting the picture lie.
if (stats != _lastMeshStats)
{
_lastMeshStats = stats;
Console.WriteLine(string.Format(
System.Globalization.CultureInfo.InvariantCulture,
"[wire-mesh] centre=({0:F2},{1:F2},{2:F2}) objects={3} polys={4} " +
"noPhysicsGeometry={5} lines={6} capped={7}",
centre.X, centre.Y, centre.Z,
stats.ObjectsConsidered, stats.PolygonsDrawn,
stats.ObjectsWithoutPhysicsGeometry, stats.LinesDrawn, stats.Capped));
}
}
private void LogNearbyCollisionObjects(Vector3 playerPosition, int drawn)
{
if (_debugDrawLogCount >= 5)

View file

@ -557,15 +557,6 @@ public sealed class LandblockBuildFactory
// Phase 2d: static objects inside the EnvCell.
foreach (var stab in envCell.StaticObjects)
{
// #119 decisive probe: HYDRATE-side dump for ACDREAM_DUMP_ENTITY-
// targeted stabs. This is the MOMENT MeshRefs are constructed —
// a degraded dat read here (setup null / placement frames short /
// part GfxObj null) permanently corrupts the entity (H-A), and
// nothing downstream ever rebuilds it. Inert when the set is empty.
bool dumpStab = AcDream.Core.Rendering.RenderingDiagnostics
.DumpEntitySourceIds.Contains(stab.Id);
int dumpSetupParts = -1, dumpPlacementFrames = -1, dumpFlattened = -1, dumpDropped = 0;
// #136: skip an EDITOR-ONLY placement marker. Such a dat object degrades to
// nothing (GfxObj id 0) at any runtime distance, so retail's distance-based
// degrade (CPhysicsPart::UpdateViewerDistance) never draws it — only the
@ -596,10 +587,6 @@ public sealed class LandblockBuildFactory
if (pb is not null) interiorBounds.Add(System.Numerics.Matrix4x4.Identity, pb.Value);
meshRefs.Add(new AcDream.Core.World.MeshRef(stab.Id, System.Numerics.Matrix4x4.Identity));
}
else if (dumpStab)
{
Console.WriteLine($"[dump-entity] HYDRATE src=0x{stab.Id:X8} cell=0x{envCellId:X8} GFXOBJ-NULL -> entity dropped");
}
}
else if ((stab.Id & 0xFF000000u) == 0x02000000u)
{
@ -608,12 +595,6 @@ public sealed class LandblockBuildFactory
{
stabLightCount = setup.Lights.Count;
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
if (dumpStab)
{
dumpSetupParts = setup.Parts.Count;
dumpPlacementFrames = setup.PlacementFrames.Count;
dumpFlattened = flat.Count;
}
foreach (var mr in flat)
{
// #136: skip an editor-only marker PART (retail hides it at runtime
@ -625,9 +606,6 @@ public sealed class LandblockBuildFactory
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
if (gfx is null)
{
dumpDropped++;
if (dumpStab)
Console.WriteLine($"[dump-entity] HYDRATE src=0x{stab.Id:X8} cell=0x{envCellId:X8} part gfx=0x{mr.GfxObjId:X8} GFXOBJ-NULL -> part dropped");
continue;
}
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
@ -635,20 +613,12 @@ public sealed class LandblockBuildFactory
meshRefs.Add(mr);
}
}
else if (dumpStab)
{
Console.WriteLine($"[dump-entity] HYDRATE src=0x{stab.Id:X8} cell=0x{envCellId:X8} SETUP-NULL -> entity dropped");
}
}
if (!AcDream.Core.Meshing.EntityHydrationRules.ShouldKeepEntity(meshRefs.Count, stabLightCount))
{
if (dumpStab)
Console.WriteLine($"[dump-entity] HYDRATE src=0x{stab.Id:X8} cell=0x{envCellId:X8} meshRefs=0 lights=0 -> entity dropped");
continue;
}
if (meshRefs.Count == 0 && dumpStab)
Console.WriteLine($"[dump-entity] HYDRATE src=0x{stab.Id:X8} cell=0x{envCellId:X8} meshRefs=0 lights={stabLightCount} -> KEPT as mesh-less light carrier");
// Stabs inside EnvCells are already in landblock-local coordinates
// (same space as LandBlockInfo.Objects stabs). Adding cellOrigin would
@ -671,20 +641,6 @@ public sealed class LandblockBuildFactory
if (interiorBounds.TryGet(out var ibMin, out var ibMax))
hydrated.SetLocalBounds(ibMin, ibMax);
if (dumpStab)
{
Console.WriteLine(
$"[dump-entity] HYDRATE src=0x{stab.Id:X8} cell=0x{envCellId:X8} entId=0x{hydrated.Id:X8} " +
$"setupParts={dumpSetupParts} placementFrames={dumpPlacementFrames} flattened={dumpFlattened} " +
$"built={meshRefs.Count} dropped={dumpDropped} " +
$"pos=({worldPos.X:F2},{worldPos.Y:F2},{worldPos.Z:F2})");
for (int i = 0; i < meshRefs.Count; i++)
{
var t = meshRefs[i].PartTransform.Translation;
Console.WriteLine($"[dump-entity] hyd-part[{i:D2}] gfx=0x{meshRefs[i].GfxObjId:X8} t=({t.X:F3},{t.Y:F3},{t.Z:F3})");
}
}
result.Add(hydrated);
}
}

View file

@ -34,12 +34,6 @@ 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;
}

View file

@ -82,10 +82,6 @@ public sealed class StickyManager
if (TargetId == 0)
return;
if (PhysicsDiagnostics.ProbeStickyEnabled)
Console.WriteLine(FormattableString.Invariant(
$"[sticky] guid=0x{_host.Id:X8} UNSTICK target=0x{TargetId:X8}"));
TargetId = 0;
Initialized = false;
_host.ClearTarget();
@ -125,10 +121,6 @@ public sealed class StickyManager
Initialized = false;
StickyTimeoutTime = _host.CurTime + StickyTime;
if (PhysicsDiagnostics.ProbeStickyEnabled)
Console.WriteLine(FormattableString.Invariant(
$"[sticky] guid=0x{_host.Id:X8} STICK target=0x{objectId:X8} tgtR={targetRadius:F2} ownR={_host.Radius:F2} lease={StickyTime:F1}s"));
// set_target(context_id=0, objectId, radius=0.5, quantum=0.5).
_host.SetTarget(0, objectId, 0.5f, 0.5);
}
@ -149,10 +141,6 @@ public sealed class StickyManager
// C0|C3 clear = cur_time > timeout; ACE `>` too), not >=.
if (_host.CurTime > StickyTimeoutTime)
{
if (PhysicsDiagnostics.ProbeStickyEnabled)
Console.WriteLine(FormattableString.Invariant(
$"[sticky] guid=0x{_host.Id:X8} LEASE-EXPIRE target=0x{TargetId:X8}"));
TargetId = 0;
Initialized = false;
_host.ClearTarget();
@ -183,10 +171,6 @@ public sealed class StickyManager
if (TargetId != 0)
{
if (PhysicsDiagnostics.ProbeStickyEnabled)
Console.WriteLine(FormattableString.Invariant(
$"[sticky] guid=0x{_host.Id:X8} TARGET-{info.Status} teardown target=0x{TargetId:X8}"));
TargetId = 0;
Initialized = false;
_host.ClearTarget();
@ -268,9 +252,5 @@ public sealed class StickyManager
if (heading < -MoveToMath.Epsilon)
heading += 360f;
offset.SetHeading(heading);
if (PhysicsDiagnostics.ProbeStickyEnabled)
Console.WriteLine(FormattableString.Invariant(
$"[sticky] guid=0x{_host.Id:X8} ADJ dist={dist:F3} delta={delta:F3} speed={speed:F1} hdgDelta={heading:F1} live={(target is not null ? 1 : 0)}"));
}
}

View file

@ -650,23 +650,6 @@ public sealed class PhysicsDataCache
$"[cell-cache] envCellId=0x{envCellId:X8} physicsPolyCount={cellStruct.PhysicsPolygons?.Count ?? 0} resolvedCount={resolved.Count} bspTotalLeafPolys={bspTotalLeafPolys} bspUnmatchedIds={bspUnmatchedIds} {bsStr} portalCount={portals.Count} visibleCells={visibleCellIds.Count} cellBspRoot={(cellStruct.CellBSP?.Root is null ? "null" : "ok")} worldOrigin=({worldOrigin.X:F2},{worldOrigin.Y:F2},{worldOrigin.Z:F2}) {portalTargets}"));
}
if (PhysicsDiagnostics.ProbeWalkMissEnabled)
{
int walkableCount = 0;
foreach (var entry in WalkMissDiagnostic.EnumerateWalkable(
resolved, PhysicsGlobals.FloorZ))
walkableCount++;
Console.Write(System.FormattableString.Invariant(
$"[floor-polys] cellId=0x{envCellId:X8} walkableCount={walkableCount}"));
foreach (var entry in WalkMissDiagnostic.EnumerateWalkable(
resolved, PhysicsGlobals.FloorZ))
{
Console.Write(System.FormattableString.Invariant(
$" [id=0x{entry.PolyId:X4} nz={entry.NormalZ:F3} bbox=({entry.BboxMin.X:F2},{entry.BboxMin.Y:F2})..({entry.BboxMax.X:F2},{entry.BboxMax.Y:F2}) planeZ@center={entry.PlaneZAtBboxCenter:F3}]"));
}
Console.WriteLine();
}
}
/// <summary>

File diff suppressed because it is too large Load diff

View file

@ -1913,15 +1913,6 @@ public sealed class PhysicsEngine
? PhysicsResolveCapture.Snapshot(body)
: null;
// #337 (2026-08-06 — TEMPORARY): arm the [support] probe's
// contact-plane provenance latch for this resolve, ahead of everything
// including the carried-plane seed below. The seed is itself one of
// the ten sites that assert a plane, so it stamps its own name and a
// capture can read `cpSrc=ResolveWithTransition:<line>` as "carried
// from the body, nothing re-derived it this resolve" without needing a
// sentinel value for that case. No-op when the probe is off.
PhysicsDiagnostics.BeginContactPlaneAttribution();
// #345 probe (2026-08-08): reset the per-tick transition-phase trace
// buffer. Scoped to exactly one resolve/tick — see
// PhysicsDiagnostics.BeginTransitFailTrace. No-op when the probe is
@ -1934,38 +1925,6 @@ public sealed class PhysicsEngine
transition.ObjectInfo.StepUpHeight = stepUpHeight;
transition.ObjectInfo.StepDownHeight = stepDownHeight;
// #338 (TEMPORARY): the resolver's own reading, taken where the
// values actually land rather than at one of two candidate call
// sites. The first attempt probed PlayerMovementController and
// printed NOTHING across 11,523 live log lines — the wrong one of
// its two resolve calls. A silent probe proves nothing, so this
// one sits where every caller must pass through. Filtered to the
// player so remotes cannot drown it.
// #338 self-report, once per process, UNCONDITIONAL. The probe has
// now been silent through two placements, and "no output" cannot
// distinguish "this site is never reached" from "the flag is
// false". This line answers both directly instead of a third round
// of inference. One Interlocked per process; strip with the probe.
PhysicsDiagnostics.AnnounceStepHeightProbeOnce(
(moverFlags & ObjectInfoState.IsPlayer) != 0);
// The flag test MUST precede the interpolated string: this site runs
// per resolve, and building the detail eagerly cost 128 B/resolve
// with the probe OFF — caught by Slice I1's zero-allocation gate,
// which is exactly what that gate is for.
if (PhysicsDiagnostics.ProbeStepHeightsEnabled
&& (moverFlags & ObjectInfoState.IsPlayer) != 0)
{
// The mover id is REQUIRED here (feedback_probe_identity_attribution):
// remote players also carry IsPlayer, and the one early
// 0.400 reading this probe caught was nearly misattributed to
// the local player for exactly that reason — it was a remote
// in its Setup-residency window (AD-68).
PhysicsDiagnostics.LogStepHeights(
"resolve", stepUpHeight, stepDownHeight,
$"mover=0x{movingEntityId:X8} onGround={isOnGround} hasBody={body is not null}");
}
transition.ObjectInfo.StepDown = true;
// Fix #42 (2026-05-05): the moving entity's ShadowEntry must be
// skipped in FindObjCollisions or the sweep collides with self.
@ -2286,33 +2245,6 @@ public sealed class PhysicsEngine
bool collisionNormalValid = ci.CollisionNormalValid;
Vector3 collisionNormal = ci.CollisionNormal;
// #42 diagnostic (2026-05-05): trace airborne sweeps to identify the
// source of the ~1m XY drift on retail-observed stationary jumps.
// Gated on ACDREAM_AIRBORNE_DIAG=1 and !isOnGround. One line per
// resolve call. deltaXY = post - target tells us how much the sweep
// diverged from the requested target; for a clean stationary +Z
// jump we expect (0,0). cp=valid with a tilted normal would confirm
// H1 (initial-overlap depenetration → next-step AdjustOffset projects
// the +Z offset along a non-+Z normal). User repros at flat plaza /
// east hillside / north hillside; if drift direction tracks terrain
// orientation, H1 is the cause; if it tracks actor facing, H2 / H3.
if (!isOnGround
&& Environment.GetEnvironmentVariable("ACDREAM_AIRBORNE_DIAG") == "1")
{
var post = sp.CheckPos;
float dx = post.X - targetPos.X;
float dy = post.Y - targetPos.Y;
string cpInfo = ci.ContactPlaneValid
? $"valid cpN=({ci.ContactPlane.Normal.X:F3},{ci.ContactPlane.Normal.Y:F3},{ci.ContactPlane.Normal.Z:F3})"
: "none";
Console.WriteLine(
$"[SWEEP] airborne pre=({currentPos.X:F3},{currentPos.Y:F3},{currentPos.Z:F3}) " +
$"target=({targetPos.X:F3},{targetPos.Y:F3},{targetPos.Z:F3}) " +
$"post=({post.X:F3},{post.Y:F3},{post.Z:F3}) " +
$"cell={cellId:X8}->{sp.CheckCellId:X8} ok={ok} " +
$"deltaXY=({dx:F3},{dy:F3}) cp={cpInfo}");
}
// L.2a slice 1 (2026-05-12): general-purpose resolver probe.
// One line per call when PhysicsDiagnostics.ProbeResolveEnabled
// is set (env var ACDREAM_PROBE_RESOLVE=1 at startup, or the
@ -2355,67 +2287,6 @@ public sealed class PhysicsEngine
$"[resolve] ent=0x{movingEntityId:X8} in=({currentPos.X:F3},{currentPos.Y:F3},{currentPos.Z:F3}) cell=0x{cellId:X8} tgt=({targetPos.X:F3},{targetPos.Y:F3},{targetPos.Z:F3}) out=({probePost.X:F3},{probePost.Y:F3},{probePost.Z:F3}) cell=0x{sp.CheckCellId:X8} ok={ok} groundedIn={isOnGround} cp={probeCp} hit={probeHit} walkable={sp.HasLastWalkablePolygon}"));
}
// #337 [support] probe (2026-08-06 — TEMPORARY, strip with the
// physics-probe family). Runs for EVERY body, not just the player:
// a corpse sinking through geometry is a plain physics body with
// no player-specific logic, so it is the cheapest possible control
// on whether the movement code or the geometry is at fault, and it
// is invisible to any player-filtered probe.
//
// The terrain sample below is INDEPENDENT of whatever the sweep
// decided — it asks the landblock directly what the ground height
// is under the body's own out-XY. Pairing that with the contact
// plane's height at the same XY is what separates "terrain is
// holding this body up" from "some object surface is". Read-only:
// SampleTerrainWalkable takes no locks, mutates nothing, and is
// not on the resolve's committed path.
if (PhysicsDiagnostics.ProbeSupportEnabled)
{
Vector3 outPos = sp.CheckPos;
TerrainWalkableSample? terrain =
SampleTerrainWalkable(outPos.X, outPos.Y);
bool terrainSampled = terrain.HasValue
&& PhysicsDiagnostics.TryPlaneZAt(
terrain.Value.Plane, outPos.X, outPos.Y, out _);
float terrainZ = float.NaN;
if (terrainSampled)
{
PhysicsDiagnostics.TryPlaneZAt(
terrain!.Value.Plane, outPos.X, outPos.Y, out terrainZ);
}
PhysicsDiagnostics.LogSupport(
moverId: movingEntityId,
isPlayer: (moverFlags & ObjectInfoState.IsPlayer) != 0,
inPos: currentPos,
inCell: cellId,
targetPos: targetPos,
outPos: outPos,
outCell: sp.CheckCellId,
ok: ok,
groundedIn: isOnGround,
contact: transition.ObjectInfo.Contact,
onWalkable: transition.ObjectInfo.OnWalkable,
contactPlaneValid: ci.ContactPlaneValid,
contactPlane: ci.ContactPlane,
contactPlaneCellId: ci.ContactPlaneCellId,
contactPlaneIsWater: ci.ContactPlaneIsWater,
contactPlaneSource: PhysicsDiagnostics.ContactPlaneSource,
lastKnownValid: ci.LastKnownContactPlaneValid,
lastKnownPlane: ci.LastKnownContactPlane,
terrainSampled: terrainSampled,
terrainZ: terrainZ,
terrainNormal: terrain?.Plane.Normal ?? Vector3.Zero,
terrainCellId: terrain?.CellId ?? 0u,
terrainIsWater: terrain?.IsWater ?? false,
walkablePolygon: sp.HasWalkablePolygon,
lastWalkablePolygon: sp.HasLastWalkablePolygon,
stepUpHeight: stepUpHeight,
stepDownHeight: stepDownHeight,
velocity: body?.Velocity ?? Vector3.Zero);
}
// Phase W Stage 0 (2026-06-02): [cell-swept] probe — swept cell vs static-derived cell.
// Emits before the ResolveResult is built so it shows what BOTH paths would return.
// No ResolveCellId call here (it has a CellGraph.CurrCell side effect). No behavior change.

View file

@ -446,29 +446,8 @@ public sealed class CollisionInfo
public void SetContactPlane(
Plane plane,
uint cellId,
bool isWater = false,
// #337 [support] attribution — recorded on PhysicsDiagnostics, not on
// this object; see the comment in the body for why. Compiler-supplied
// literals: no call site passes these explicitly and none needs to.
[System.Runtime.CompilerServices.CallerMemberName] string sourceMember = "",
[System.Runtime.CompilerServices.CallerLineNumber] int sourceLine = 0)
bool isWater = false)
{
// #337 attribution (2026-08-06 — TEMPORARY, strip with the probe
// family). Recorded ABOVE the no-op guard on purpose: the meaning is
// "the last site that ASSERTED this plane", not "the site that first
// differed from the previous value" — a sweep that re-derives the
// identical plane it was seeded with has still told you where the
// plane comes from, and that is the fact the capture needs.
//
// It lives on PhysicsDiagnostics, NOT on this object. CollisionInfo's
// stored members are compared member-for-member by the flat/graph
// differential referee and by the scratch-reset poison test; a
// diagnostic field there is state those oracles must then be taught to
// ignore, which is how a referee stops refereeing. The latch is
// [ThreadStatic] and is armed once per resolve — see
// PhysicsDiagnostics.BeginContactPlaneAttribution.
PhysicsDiagnostics.RecordContactPlaneSource(sourceMember, sourceLine);
// A6.P3 slice 2 (2026-05-22): no-op-if-unchanged guard. Closes
// issue #96 (per-tick CP-write blowup) without removing the
// PhysicsEngine.cs L622 seed that step_up depends on. When the
@ -3880,35 +3859,12 @@ public sealed class Transition
var oi = ObjectInfo;
var ci = CollisionInfo;
// #334 candidate-disposition probe (2026-08-06 — TEMPORARY, strip with
// the physics-probe family). Filtered to the player mover so NPC /
// remote dead-reckoning resolves do not pollute the capture, matching
// PhysicsResolveCapture's filter. The zero-entry query below is
// reported EXPLICITLY: "this cell yielded nothing" is outcome (b) —
// a registration gap — and must appear as data, never as silence.
bool reachProbe = PhysicsDiagnostics.ProbeReachEnabled && oi.IsPlayer;
if (objsInCell.Count == 0)
{
if (reachProbe)
PhysicsDiagnostics.LogReachQuery(
oi.SelfEntityId, cellId, sp.StepDown,
inCell: 0, exempt: 0, reached: 0, rejectedReach: 0,
noShape: 0, tested: 0, blocked: 0,
currPos: sp.GlobalCurrCenter[0].Origin);
return TransitionState.OK;
}
// #42 diagnostic (2026-05-05): identify which static object causes
// the airborne first-frame ~1m push.
bool airborneDiag = !oi.Contact
&& Environment.GetEnvironmentVariable("ACDREAM_AIRBORNE_DIAG") == "1";
Vector3 sphereCheckBefore = sp.CheckPos;
Vector3 checkPos = sp.GlobalSphere[0].Origin;
Vector3 currPos = sp.GlobalCurrCenter[0].Origin;
float sphereRadius = sp.GlobalSphere[0].Radius;
Vector3 movement = checkPos - currPos;
// Landblock offsets feed the [resolve-bldg] probe only.
engine.TryGetLandblockContext(checkPos.X, checkPos.Y,
@ -3919,38 +3875,8 @@ public sealed class Transition
// registry mutations through the same reference mid-iteration.
using var nearbyObjs = ShadowEntrySnapshot.Capture(objsInCell);
// #334 probe tallies — see LogReachQuery for what each one decides.
// `rejectedReach` is deliberately still REPORTED and structurally 0
// since #333 deleted the filter: a run of the acceptance capture whose
// [reach-q] lines read rejectedReach=0 where the pre-fix capture read
// 7,225 rejections is the evidence the filter is gone, and dropping the
// column would make the two captures incomparable.
const int rRejected = 0;
int rExempt = 0, rReached = 0, rNoShape = 0,
rTested = 0, rBlocked = 0;
foreach (ShadowEntry obj in nearbyObjs.Entries)
{
// #337 [geom] probe (2026-08-06 — TEMPORARY, strip with the
// physics-probe family). Emitted here, at the TOP of the candidate
// loop, so it covers every object the mover comes near regardless
// of what the exemptions and the reach filter later do with it —
// an object whose collision geometry is absent or displaced must
// be reported even when nothing ever tests it. Once per GfxObj per
// process: the line describes an ASSET, not a moment.
if (obj.CollisionType == ShadowCollisionType.BSP
&& PhysicsDiagnostics.ShouldLogGeometry(obj.GfxObjId))
{
PhysicsDiagnostics.LogGeometryFromAssets(
gfxObjId: obj.GfxObjId,
entityId: obj.EntityId,
flat: engine.DataCache.GetFlatGfxObj(obj.GfxObjId),
visual: engine.DataCache.GetVisualBounds(obj.GfxObjId),
entityWorldPosition: obj.Position,
entityScale: obj.Scale,
registeredRadius: obj.Radius);
}
// Self-skip — fix #42 (2026-05-05). Mirrors retail
// CObjCell::find_obj_collisions at acclient_2013_pseudo_c.txt
// 308931: `physobj != arg2->object_info.object` rejects the
@ -3965,12 +3891,6 @@ public sealed class Transition
// gfxObj=0x02000001 at exactly the entity's own position).
if (oi.SelfEntityId != 0 && obj.EntityId == oi.SelfEntityId)
{
if (reachProbe)
{
rExempt++;
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
"exempt-self", sphereRadius, movement.Length(), currPos);
}
continue;
}
@ -3979,12 +3899,6 @@ public sealed class Transition
// dispatch; a true result reaches neither branch.
if (oi.MissileIgnore(obj.EntityId, obj.State, obj.Flags))
{
if (reachProbe)
{
rExempt++;
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
"exempt-missile", sphereRadius, movement.Length(), currPos);
}
continue;
}
@ -3993,7 +3907,6 @@ public sealed class Transition
// Cell membership IS the broad phase, and the BSP walk's own root
// node bounding-sphere test — correctly centred, unlike the deleted
// filter — is the early-out that made this one unnecessary.
if (reachProbe) rReached++;
// Commit C 2026-04-29 — retail exemption block at the top of
// CPhysicsObj::FindObjCollisions
@ -4005,12 +3918,6 @@ public sealed class Transition
// so this is a cheap fall-through for them.
if (CollisionExemption.ShouldSkip(obj.State, obj.Flags, ObjectInfo.State))
{
if (reachProbe)
{
rExempt++;
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
"exempt-rule", sphereRadius, movement.Length(), currPos);
}
continue;
}
@ -4040,12 +3947,6 @@ public sealed class Transition
if (etherealForTest && sp.StepDown)
{
// retail pc:276799 — ethereal target not tested in step-down
if (reachProbe)
{
rExempt++;
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
"exempt-ethereal-stepdown", sphereRadius, movement.Length(), currPos);
}
continue;
}
sp.ObstructionEthereal = etherealForTest;
@ -4096,15 +3997,6 @@ public sealed class Transition
// clear (pc:276989) fires after shape tests; we clear early here to
// leave the flag clean for the next iteration.
sp.ObstructionEthereal = false;
// #334 outcome (c): the entry IS a candidate, the reach
// filter DID admit it, and it still contributes nothing
// because no usable physics BSP resolved for its GfxObj.
if (reachProbe)
{
rNoShape++;
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
"no-shape", sphereRadius, movement.Length(), currPos);
}
continue;
}
@ -4172,12 +4064,6 @@ public sealed class Transition
Console.WriteLine(System.FormattableString.Invariant(
$"[sph-skip-bsp] obj=0x{obj.EntityId:X8} state=0x{obj.State:X8} — HAS_PHYSICS_BSP_PS dispatches BSP-only"));
}
if (reachProbe)
{
rExempt++;
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
"bsp-only-skip", sphereRadius, movement.Length(), currPos);
}
continue;
}
@ -4221,12 +4107,6 @@ public sealed class Transition
Console.WriteLine(System.FormattableString.Invariant(
$"[cyl-skip-bsp] obj=0x{obj.EntityId:X8} state=0x{obj.State:X8} — HAS_PHYSICS_BSP_PS dispatches BSP-only"));
}
if (reachProbe)
{
rExempt++;
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
"bsp-only-skip", sphereRadius, movement.Length(), currPos);
}
continue;
}
@ -4390,138 +4270,15 @@ public sealed class Transition
// per-object test body before the outer loop continues.
sp.ObstructionEthereal = false;
// #334: this candidate actually reached a shape test. `result` is
// post-Layer-2, i.e. what the query will really act on.
if (reachProbe)
{
rTested++;
if (result != TransitionState.OK) rBlocked++;
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
result switch
{
TransitionState.OK => "tested-ok",
TransitionState.Collided => "tested-collided",
TransitionState.Adjusted => "tested-adjusted",
TransitionState.Slid => "tested-slid",
_ => "tested-invalid",
},
sphereRadius, movement.Length(), currPos);
}
if (result != TransitionState.OK)
{
if (airborneDiag)
{
var sphereCheckAfter = sp.CheckPos;
var d = sphereCheckAfter - sphereCheckBefore;
Console.WriteLine(
$"[SWEEP-OBJ] type={obj.CollisionType} gfxObj=0x{obj.GfxObjId:X8} " +
$"objPos=({obj.Position.X:F3},{obj.Position.Y:F3},{obj.Position.Z:F3}) " +
$"objR={obj.Radius:F3} cylH={obj.CylHeight:F3} " +
$"state={result} pushDelta=({d.X:F3},{d.Y:F3},{d.Z:F3})");
}
// #334 (TEMPORARY): the early exit is a real query outcome, so
// it must be summarised too — otherwise the summary would
// under-report exactly the queries where something DID block,
// and "blocked" is the control that proves the probe can see a
// working collision as well as a missing one.
if (reachProbe)
PhysicsDiagnostics.LogReachQuery(
oi.SelfEntityId, cellId, sp.StepDown,
nearbyObjs.Entries.Length, rExempt, rReached,
rRejected, rNoShape, rTested, rBlocked, currPos);
return result;
}
}
if (reachProbe)
PhysicsDiagnostics.LogReachQuery(
oi.SelfEntityId, cellId, sp.StepDown,
nearbyObjs.Entries.Length, rExempt, rReached,
rRejected, rNoShape, rTested, rBlocked, currPos);
return TransitionState.OK;
}
/// <summary>
/// #334 candidate-disposition probe helper (2026-08-06 — TEMPORARY, strip
/// with the physics-probe family). Static, and takes everything by
/// parameter, so it introduces no closure display class into
/// <see cref="FindObjCollisionsInCell"/> — Slice I1's 0 B/resolve budget
/// must hold with the probe compiled in and switched off.
///
/// <para>
/// The target's physics-BSP ROOT sphere is resolved through the SAME
/// production accessor registration used
/// (<c>GetFlatGfxObj(id).PhysicsBsp</c>'s root node, per
/// <c>LiveEntityCollisionBuilder</c> and <c>ShadowShapeBuilder</c>), so the
/// probe cannot report geometry that differs from what the registry
/// actually emitted. AP-156's lesson was exactly that: one resolver.
/// </para>
/// </summary>
private static void ProbeReachCandidate(
PhysicsEngine engine,
ObjectInfo oi,
SpherePath sp,
uint cellId,
in ShadowEntry obj,
string disposition,
float sphereRadius,
float movementLen,
Vector3 currPos)
{
bool xyOnly = obj.CollisionType == ShadowCollisionType.Cylinder;
Vector3 dOrigin = currPos - obj.Position;
float distOrigin = xyOnly
? MathF.Sqrt(dOrigin.X * dOrigin.X + dOrigin.Y * dOrigin.Y)
: dOrigin.Length();
// World-space offset from the part ORIGIN (what the DELETED filter
// measured against) to the BSP root sphere CENTRE (what it should have
// measured against). Both distances are still emitted after #333: the
// pair is what proved the diagnosis, and it stays comparable across the
// pre-fix and post-fix captures. Zero for non-BSP shapes, whose
// Position already IS their centre.
Vector3 bspCentreOffset = Vector3.Zero;
if (obj.CollisionType == ShadowCollisionType.BSP)
{
var flatBsp = engine.DataCache?.GetFlatGfxObj(obj.GfxObjId)?.PhysicsBsp;
if (flatBsp is { RootIndex: >= 0 })
{
bspCentreOffset = Vector3.Transform(
flatBsp.Nodes[flatBsp.RootIndex].BoundingSphere.Origin * obj.Scale,
obj.Rotation);
}
}
Vector3 dCentre = currPos - (obj.Position + bspCentreOffset);
float distCentre = xyOnly
? MathF.Sqrt(dCentre.X * dCentre.X + dCentre.Y * dCentre.Y)
: dCentre.Length();
float budget = sphereRadius + obj.Radius + movementLen + 2f;
PhysicsDiagnostics.LogReachCandidate(
moverId: oi.SelfEntityId,
entityId: obj.EntityId,
gfxObjId: obj.GfxObjId,
cellId: cellId,
shape: obj.CollisionType,
disposition: disposition,
stepDown: sp.StepDown,
distOrigin: distOrigin,
distCenter: distCentre,
objRadius: obj.Radius,
sphereRadius: sphereRadius,
movementLen: movementLen,
budget: budget,
centerBudget: budget - 2f,
objPos: obj.Position,
bspCentreOffset: bspCentreOffset,
currPos: currPos);
}
/// <summary>
/// BR-7 / A6.P4 (2026-06-11). The retail BUILDING collision channel —
/// <c>CSortCell::find_collisions</c> (Ghidra 0x005340a0): an outdoor

View file

@ -1,173 +0,0 @@
using System.Collections.Generic;
using System.Numerics;
namespace AcDream.Core.Physics;
/// <summary>
/// ISSUES #83 H-disambiguation spike (2026-05-21). Pure-function
/// aggregator over a <see cref="CellPhysics.Resolved"/> dict — picks
/// the nearest walkable-eligible polygon to a given foot position
/// (cell-local space) and reports XY-containment + vertical gap so
/// the <c>[walk-miss]</c> emission site can disambiguate H1/H2/H3
/// without re-walking the dictionary itself.
///
/// <para>
/// Also enumerates walkable polygons for the one-shot
/// <c>[floor-polys]</c> dump at cell-cache time.
/// </para>
///
/// <para>
/// Spec: <c>docs/superpowers/specs/2026-05-21-indoor-walk-miss-probe-design.md</c>.
/// </para>
/// </summary>
public static class WalkMissDiagnostic
{
public readonly struct AggregateResult
{
public bool Found { get; init; }
public ushort PolyId { get; init; }
public bool ContainsFootXY { get; init; }
public float Dz { get; init; }
public float NormalZ { get; init; }
}
public readonly struct WalkableEntry
{
public ushort PolyId { get; init; }
public float NormalZ { get; init; }
public Vector3 BboxMin { get; init; }
public Vector3 BboxMax { get; init; }
public float PlaneZAtBboxCenter { get; init; }
}
/// <summary>
/// Walks <paramref name="resolved"/>, considering only polygons
/// whose plane normal Z is at least <paramref name="floorZ"/>
/// (walkable slope). Selection rule:
/// <list type="number">
/// <item><description>Polygons whose local-XY bounding box contains
/// <paramref name="footLocal"/>'s XY are preferred. Among them,
/// the one with smallest <c>|dz|</c> wins.</description></item>
/// <item><description>If no poly contains the foot XY, the poly
/// with smallest <c>|dz|</c> across all walkable polys wins,
/// and <see cref="AggregateResult.ContainsFootXY"/> is false.</description></item>
/// </list>
/// </summary>
public static AggregateResult AggregateNearestWalkable(
IReadOnlyDictionary<ushort, ResolvedPolygon> resolved,
Vector3 footLocal,
float floorZ)
{
bool bestFound = false;
bool bestContainsFootXY = false;
ushort bestPolyId = 0;
float bestAbsDz = float.MaxValue;
float bestSignedDz = 0f;
float bestNormalZ = 0f;
foreach (var kvp in resolved)
{
var poly = kvp.Value;
if (poly.Plane.Normal.Z < floorZ) continue;
if (poly.Vertices.Length < 3) continue;
// Local-XY bounding box.
float minX = float.MaxValue, minY = float.MaxValue;
float maxX = float.MinValue, maxY = float.MinValue;
for (int i = 0; i < poly.Vertices.Length; i++)
{
var v = poly.Vertices[i];
if (v.X < minX) minX = v.X;
if (v.Y < minY) minY = v.Y;
if (v.X > maxX) maxX = v.X;
if (v.Y > maxY) maxY = v.Y;
}
bool containsFootXY =
footLocal.X >= minX && footLocal.X <= maxX &&
footLocal.Y >= minY && footLocal.Y <= maxY;
// Signed vertical gap from foot to the polygon's plane at
// the foot's XY: plane.D + n.x*X + n.y*Y + n.z*Z = 0
// => planeZ = -(D + n.x*X + n.y*Y) / n.z
// => dz = footZ - planeZ
float planeZ = -(poly.Plane.D
+ poly.Plane.Normal.X * footLocal.X
+ poly.Plane.Normal.Y * footLocal.Y)
/ poly.Plane.Normal.Z;
float signedDz = footLocal.Z - planeZ;
float absDz = MathF.Abs(signedDz);
// Preference: prefer XY-containing polys. Among the
// preferred set, smallest |dz| wins.
bool preferOver = !bestFound
|| (containsFootXY && !bestContainsFootXY)
|| (containsFootXY == bestContainsFootXY && absDz < bestAbsDz);
if (preferOver)
{
bestFound = true;
bestContainsFootXY = containsFootXY;
bestPolyId = kvp.Key;
bestAbsDz = absDz;
bestSignedDz = signedDz;
bestNormalZ = poly.Plane.Normal.Z;
}
}
return new AggregateResult
{
Found = bestFound,
PolyId = bestPolyId,
ContainsFootXY = bestContainsFootXY,
Dz = bestSignedDz,
NormalZ = bestNormalZ,
};
}
/// <summary>
/// Enumerates walkable-eligible polygons (normal Z &gt;= floorZ)
/// with their local-XY bounding boxes and plane Z at the bbox
/// center. Used by the one-shot <c>[floor-polys]</c> cell-load
/// dump.
/// </summary>
public static IEnumerable<WalkableEntry> EnumerateWalkable(
IReadOnlyDictionary<ushort, ResolvedPolygon> resolved,
float floorZ)
{
foreach (var kvp in resolved)
{
var poly = kvp.Value;
if (poly.Plane.Normal.Z < floorZ) continue;
if (poly.Vertices.Length < 3) continue;
float minX = float.MaxValue, minY = float.MaxValue, minZ = float.MaxValue;
float maxX = float.MinValue, maxY = float.MinValue, maxZ = float.MinValue;
for (int i = 0; i < poly.Vertices.Length; i++)
{
var v = poly.Vertices[i];
if (v.X < minX) minX = v.X;
if (v.Y < minY) minY = v.Y;
if (v.Z < minZ) minZ = v.Z;
if (v.X > maxX) maxX = v.X;
if (v.Y > maxY) maxY = v.Y;
if (v.Z > maxZ) maxZ = v.Z;
}
float cx = (minX + maxX) * 0.5f;
float cy = (minY + maxY) * 0.5f;
float planeZAtCenter = -(poly.Plane.D
+ poly.Plane.Normal.X * cx
+ poly.Plane.Normal.Y * cy)
/ poly.Plane.Normal.Z;
yield return new WalkableEntry
{
PolyId = kvp.Key,
NormalZ = poly.Plane.Normal.Z,
BboxMin = new Vector3(minX, minY, minZ),
BboxMax = new Vector3(maxX, maxY, maxZ),
PlaneZAtBboxCenter = planeZAtCenter,
};
}
}
}

View file

@ -99,33 +99,6 @@ public static class RenderingDiagnostics
public static bool ProbeVisibilityEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_VIS") == "1";
/// <summary>
/// #119-residual viewer/flood capture (2026-06-11): one <c>[viewer]</c>
/// line per CHANGE of (root cell, flood size, OutsideView poly count,
/// player cell), with the projection EYE at mm precision on every line —
/// the capture half of the tower-ascent capture→replay loop
/// (TowerAscentReplayTests replays the captured pairs deterministically).
/// Light: silent while the visibility state is stable; a tower climb
/// emits a few dozen lines. Initial state from
/// <c>ACDREAM_PROBE_VIEWER=1</c>.
/// </summary>
public static bool ProbeViewerEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_VIEWER") == "1";
/// <summary>
/// #131 (2026-06-12) outside-stage dynamics probe. When true, the renderer
/// emits one <c>[outstage]</c> line per CHANGE of the outside-stage
/// routing + per-slice cone verdict set under an interior root (which
/// outdoor dynamics were routed to the landscape slice, which survived the
/// slice viewcone), and GameWindow emits one <c>[outstage-pt]</c> line per
/// change of the slice Scene-particle id set + matched-emitter count.
/// Built for the portal-swirl-missing-through-doorway capture. Light:
/// silent while the set is stable. Initial state from
/// <c>ACDREAM_PROBE_OUTSTAGE=1</c>.
/// </summary>
public static bool ProbeOutStageEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_OUTSTAGE") == "1";
/// <summary>
/// Phase U.4c (2026-05-31) flap-convergence probe. When true, the portal
/// visibility pass emits, EVERY frame the camera root is an indoor cell, a
@ -144,21 +117,6 @@ public static class RenderingDiagnostics
public static bool ProbeFlapEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_FLAP") == "1";
/// <summary>
/// Issue #78 (2026-05-31) cell-shell render probe. When true,
/// <c>EnvCellRenderer.Render</c> emits one <c>[shell]</c> line per opaque-pass
/// call: per visible (filtered) cell — is it present in the prepared snapshot,
/// how many gfxObjs + instances, and per-gfxObj batch count / index count /
/// translucent / zero-bindless-handle (missing texture) — plus the pass totals.
/// This directly answers WHY interior walls/ceilings don't appear: no geometry
/// prepared for the cell (cell absent / 0 instances), drawn-but-invisible
/// (zeroHandle / translucent against the clear color), or prepared+drawn (so the
/// fault is elsewhere — depth/occlusion). Throwaway apparatus — strip once the
/// indoor-enclosure render is fixed. Initial state from <c>ACDREAM_PROBE_SHELL=1</c>.
/// </summary>
public static bool ProbeShellEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_SHELL") == "1";
/// <summary>
/// Flap root-cause apparatus (2026-06-07). When true, the indoor render path emits ONE
/// <c>[pv-input]</c> line per frame with the EXACT PortalVisibilityBuilder.Build inputs at HIGH
@ -202,21 +160,6 @@ public static class RenderingDiagnostics
public static bool ProbeClipRouteEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_CLIPROUTE") == "1";
/// <summary>
/// #105 white-indoor-textures apparatus (2026-06-10). When true, <c>WbMeshAdapter.Tick</c>
/// emits one <c>[tex-flush]</c> line whenever the staged-texture-update picture changes:
/// pending layer updates across all shared atlases BEFORE and AFTER the per-frame
/// <c>ObjectMeshManager.GenerateMipmaps()</c> flush, plus arrays-with-pending / total-array
/// counts. The broken contract this pins: <c>TextureAtlasManager.AddTexture</c> only STAGES
/// pixel data (PBO + pending list); without the per-frame flush (WB GameScene.cs:975) the
/// data never reaches the GL texture and the batch samples undefined content behind a valid
/// bindless handle — the classic white walls. A healthy run shows <c>after=0</c> on every
/// line; a stuck <c>before==after&gt;0</c> at standstill is the #105 mechanism live.
/// Initial state from <c>ACDREAM_PROBE_TEXFLUSH=1</c>.
/// </summary>
public static bool ProbeTexFlushEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_TEXFLUSH") == "1";
/// <summary>
/// Bounded-propagation port apparatus (2026-06-08). When true, PortalVisibilityBuilder.Build emits
/// one [portal-churn] summary line per call: per-cell pop count (re-pops = churn), total re-enqueues,
@ -228,52 +171,6 @@ public static class RenderingDiagnostics
public static bool ProbePortalChurnEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_PORTAL_CHURN") == "1";
/// <summary>
/// BR-2 phantom-site probe (2026-06-11; plan
/// <c>docs/plans/2026-06-11-building-render-port-plan.md</c> §BR-2 first
/// task). The BR-1 pre-check proved the #113 phantom residual cannot be
/// GfxObj portal fills (never extracted); the surviving suspects are
/// cell-side. When true, <c>RetailPViewRenderer</c> emits, print-on-change
/// per cell: <c>[phantom-shell]</c> — per shell-pass cell, the clip-enable
/// state and each drawn slice's slot + plane count, flagging the pass-all
/// cases (NoClipSlice fallback for slot-less cells; assembler slot-0
/// scissor fallback) — and <c>[phantom-objs]</c> — per object-list cell,
/// the entity-bucket size drawn unclipped/un-viewcone'd. Reproducing the
/// phantom with this on pins which mechanism draws it (shells → BR-2/BR-3;
/// statics → BR-5). Throwaway apparatus — strip when the phantom closes.
/// Initial state from <c>ACDREAM_PROBE_PHANTOM=1</c>.
/// </summary>
public static bool ProbePhantomEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_PHANTOM") == "1";
/// <summary>
/// #133 A7 (2026-06-13) dungeon-lighting objective probe. When true,
/// the per-frame scene-lighting build emits ONE <c>[light]</c> line
/// roughly every second (wall-clock rate-limited like WB-DIAG) via
/// <see cref="EmitLight"/>:
/// <code>
/// [light] insideCell=&lt;bool&gt; ambient=(r,g,b) sun=&lt;intensity&gt;
/// registeredLights=&lt;N&gt; activeLights=&lt;uCellAmbient.w&gt; playerCell=0x&lt;id&gt;
/// </code>
/// This is the self-verification signal for the dungeon-dim question:
/// <list type="bullet">
/// <item><description><c>insideCell=true ambient=(0.20,0.20,0.20) sun=0</c>
/// confirms the indoor branch fired (retail flat ambient, sun killed).</description></item>
/// <item><description><c>registeredLights</c> is the count of dat-baked
/// point/spot lights (<c>Setup.Lights</c>) registered with the
/// <c>LightManager</c> — if this is 0 in a dungeon, the cell's static
/// objects carry no baked torches (so the only illumination IS the
/// 0.2 ambient → dim).</description></item>
/// <item><description><c>activeLights</c> is <c>uCellAmbient.w</c> — the
/// shader's active-slot count, which INCLUDES the (zeroed) sun slot
/// indoors. So <c>activeLights=1 registeredLights=0</c> = "only the dead
/// sun slot, no torches in range".</description></item>
/// </list>
/// Output-only, inert when off. Initial state from <c>ACDREAM_PROBE_LIGHT=1</c>.
/// </summary>
public static bool ProbeLightEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIGHT") == "1";
/// <summary>
/// A7.L1 (2026-07-06) light-pool SET-COMPOSITION probe — the apparatus the
/// <c>[light]</c> counts could not provide (the #176/#177 discriminator: the bug
@ -496,94 +393,8 @@ public static class RenderingDiagnostics
/// </summary>
internal static void ResetVisibilityProbeForTests() => _lastVisRootCellId = 0;
// Wall-clock rate-limit gate for EmitLight. Ticks (100 ns) is plenty —
// we only need ~1 Hz and avoid a Stopwatch allocation/field. Sentinel 0
// = "never emitted" so the first call always fires.
private static long _lastLightEmitTicks;
private const long LightEmitIntervalTicks = 10_000_000; // 1 s in 100-ns ticks
/// <summary>
/// #133 A7 — emit ONE rate-limited <c>[light]</c> line describing the
/// current scene-lighting state, followed (when <paramref name="lights"/>
/// is supplied) by up to three <c>[light-detail]</c> lines for the nearest
/// ACTIVE point/spot lights. Cheap no-op when
/// <see cref="ProbeLightEnabled"/> is false; otherwise fires at most
/// once per second. Pull the values from the spot where
/// <c>GameWindow.UpdateSunFromSky</c> set <c>Lighting.CurrentAmbient</c>
/// / <c>Lighting.Sun</c> and where <c>SceneLightingUbo.Build</c> computed
/// the active-slot count.
/// <para>
/// The <c>[light-detail]</c> lines are the answer to the "candle-spotlight"
/// question — they expose each torch's REAL dat-derived runtime values
/// (<c>range=</c> Falloff metres, <c>intensity=</c>, <c>cone=</c> radians,
/// <c>color=</c>, <c>distToViewer=</c>) so it is visible in launch.log
/// whether dungeon torches are tiny-range points or wide cones and at what
/// intensity — without a screenshot:
/// <code>
/// [light-detail] kind=Point range=&lt;Falloff m&gt; intensity=&lt;I&gt; cone=&lt;rad&gt; color=(r,g,b) distToViewer=&lt;m&gt;
/// </code>
/// </para>
/// </summary>
/// <param name="insideCell">The <c>playerInsideCell</c> value driving the indoor branch.</param>
/// <param name="ambientR">Cell ambient red (xyz of <c>uCellAmbient</c>).</param>
/// <param name="ambientG">Cell ambient green.</param>
/// <param name="ambientB">Cell ambient blue.</param>
/// <param name="sunIntensity">The sun <c>LightSource.Intensity</c> (0 indoors).</param>
/// <param name="registeredLights">Total point/spot lights registered with the LightManager.</param>
/// <param name="activeLights"><c>uCellAmbient.w</c> — shader active-slot count (includes the zeroed sun slot indoors).</param>
/// <param name="playerCellId">The player's current cell id (0 if unresolved → outside).</param>
/// <param name="lights">The ticked <c>LightManager</c> (its <c>Active</c> list, sorted nearest-first by the
/// just-completed Tick). When non-null, drives the <c>[light-detail]</c> lines. Optional so existing call
/// sites / tests that only want the aggregate line keep compiling.</param>
public static void EmitLight(bool insideCell,
float ambientR, float ambientG, float ambientB,
float sunIntensity,
int registeredLights,
int activeLights,
uint playerCellId,
AcDream.Core.Lighting.LightManager? lights = null)
{
if (!ProbeLightEnabled) return;
long now = DateTime.UtcNow.Ticks;
if (_lastLightEmitTicks != 0 && (now - _lastLightEmitTicks) < LightEmitIntervalTicks)
return;
_lastLightEmitTicks = now;
var ci = System.Globalization.CultureInfo.InvariantCulture;
Console.WriteLine(string.Format(ci,
"[light] insideCell={0} ambient=({1:0.###},{2:0.###},{3:0.###}) sun={4:0.###} registeredLights={5} activeLights={6} playerCell=0x{7:X8}",
insideCell, ambientR, ambientG, ambientB, sunIntensity,
registeredLights, activeLights, playerCellId));
// #133 A7 (2026-06-13) — per-light detail for the "spotlight bubble"
// question. Dump the actual runtime dat-derived values of the nearest
// ~3 ACTIVE point/spot lights so the real Falloff/Intensity/ConeAngle
// are visible in launch.log (are torch ranges 1m or 10m? points or
// spots? what intensity?). The sun (Directional, slot 0) is skipped —
// it carries no Range/cone meaning. DistSq is already cached by
// LightManager.Tick this frame, so the active list is sorted nearest-
// first; we just take the first few non-directional entries.
if (lights is null) return;
var active = lights.Active;
int shown = 0;
const int MaxDetail = 3;
for (int i = 0; i < active.Length && shown < MaxDetail; i++)
{
var ls = active[i];
if (ls is null) continue;
if (ls.Kind == AcDream.Core.Lighting.LightKind.Directional) continue;
float dist = ls.DistSq >= 0f ? MathF.Sqrt(ls.DistSq) : 0f;
Console.WriteLine(string.Format(ci,
"[light-detail] kind={0} range={1:0.###} intensity={2:0.###} cone={3:0.####} color=({4:0.###},{5:0.###},{6:0.###}) distToViewer={7:0.###} owner=0x{8:X8} cell=0x{9:X8} dyn={10}",
ls.Kind, ls.Range, ls.Intensity, ls.ConeAngle,
ls.ColorLinear.X, ls.ColorLinear.Y, ls.ColorLinear.Z, dist,
ls.OwnerId, ls.CellId, ls.IsDynamic ? 1 : 0));
shown++;
}
}
// Wall-clock rate-limit gate for EmitIndoorLight (shares the 1 s interval).
private static long _lastIndoorLightEmitTicks;
@ -694,27 +505,6 @@ public static class RenderingDiagnostics
/// </summary>
public static bool IsEnvCellId(ulong id) => (id & 0xFFFFu) >= 0x0100u;
/// <summary>
/// #119 tower-staircase decisive probe (2026-06-11). Comma-separated
/// Setup / GfxObj source ids (hex, optional 0x prefix) from
/// <c>ACDREAM_DUMP_ENTITY</c>. Any <c>WorldEntity</c> whose
/// <c>SourceGfxObjOrSetupId</c> is in this set emits:
/// (a) a <c>[dump-entity] HYDRATE</c> dump at MeshRef construction time
/// (<c>GameWindow.BuildInteriorEntitiesForStreaming</c>) — per-part
/// placement-frame translations + dropped-part accounting — discriminating
/// hydration-time corruption (H-A: SetupMesh.Flatten identity fallback /
/// silent gfx-null part drops under degraded dat reads);
/// (b) a <c>[dump-entity] DRAW</c> dump in <c>WbDrawDispatcher</c> at first
/// draw — live MeshRefs translations + Tier-1 classification cache state —
/// re-emitted compactly whenever that state changes (H-B: stale/partial
/// cached batch set); and
/// (c) rate-limited <c>[dump-entity] WALK-REJECT</c> lines when the
/// dispatcher's walk filters the entity out (absence-of-draw attribution).
/// Empty set = probe off; every call site early-outs on <c>Count == 0</c>.
/// </summary>
public static IReadOnlySet<uint> DumpEntitySourceIds { get; } =
ParseDumpEntityIds(Environment.GetEnvironmentVariable("ACDREAM_DUMP_ENTITY"));
/// <summary>
/// Parse the <c>ACDREAM_DUMP_ENTITY</c> value: comma-separated hex ids,
/// optional 0x prefix, whitespace tolerated, malformed segments ignored
@ -794,52 +584,4 @@ public static class RenderingDiagnostics
/// </summary>
public static string? FrameHistoryPath { get; } =
Environment.GetEnvironmentVariable("ACDREAM_FRAME_HISTORY");
// ── #337 collision-mesh wireframe (2026-08-06 — TEMPORARY) ──────────────
//
// The F2 collision overlay already existed, but for a BSP object it drew a
// proxy cylinder sized from the REGISTERED BROADPHASE RADIUS. That shows
// where the collision system thinks the object roughly is; it cannot show
// where the collision SURFACES are, which is the only thing that answers
// "is the collision geometry where the visual geometry is". The knobs
// below turn F2 into the real answer: the actual physics-BSP polygon
// edges, in world space, next to the same object's visual mesh box.
//
// Off by default, so F2 keeps its old cheap behaviour for anyone who wants
// it and this costs nothing until asked for.
/// <summary>
/// When true, the F2 collision overlay draws each nearby object's REAL
/// physics-BSP polygon edges (cyan) and, beside them, the same object's
/// visual mesh bounding box (magenta), plus the terrain triangle under the
/// player (yellow). Any separation between the cyan surfaces and the
/// object you can see is the "collision is not where the visual is"
/// defect, read directly off the screen instead of inferred from a log.
/// Initial state from <c>ACDREAM_WIRE_MESH=1</c>.
/// TEMPORARY — strip with the #337 probe family.
/// </summary>
public static bool CollisionMeshWireframeEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_WIRE_MESH") == "1";
/// <summary>
/// Radius in metres around the player within which
/// <see cref="CollisionMeshWireframeEnabled"/> resolves polygon geometry.
/// A whole landblock of rock is far more geometry than a line list wants;
/// 30 m covers everything you can wedge against. Override with
/// <c>ACDREAM_WIRE_RADIUS=&lt;metres&gt;</c>.
/// </summary>
public static float CollisionMeshWireframeRadius { get; set; } =
ParsePositiveFloat(
Environment.GetEnvironmentVariable("ACDREAM_WIRE_RADIUS"),
fallback: 30f);
private static float ParsePositiveFloat(string? raw, float fallback)
=> float.TryParse(
raw,
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out float value)
&& value > 0f
? value
: fallback;
}

View file

@ -215,14 +215,6 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
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(
@ -320,35 +312,16 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
// the PhysicsDiagnostics owner exactly as before.
handleUpdateTarget: info =>
{
if (PhysicsDiagnostics.ProbeAutoWalkEnabled)
{
Console.WriteLine(
$"[autowalk-target] object=0x{info.ObjectId:X8} "
+ $"status={info.Status} context={info.ContextId} "
+ $"target=({info.TargetPosition.Frame.Origin.X:F2},"
+ $"{info.TargetPosition.Frame.Origin.Y:F2},"
+ $"{info.TargetPosition.Frame.Origin.Z:F2})");
}
movement.HandleUpdateTarget(info);
},
interruptCurrentMovement: () =>
{
if (PhysicsDiagnostics.ProbeAutoWalkEnabled
&& movement.IsMovingTo())
{
Console.WriteLine("[autowalk-end] reason=interrupt");
}
movement.CancelMoveTo(WeenieError.ActionCancelled);
});
movement.MakeMoveToManager();
motion.UnstickFromObject = physicsHost.PositionManager.UnStick;
motion.InterruptCurrentMovement = () =>
{
if (PhysicsDiagnostics.ProbeAutoWalkEnabled
&& movement.IsMovingTo())
{
Console.WriteLine("[autowalk-end] reason=interrupt");
}
movement.CancelMoveTo(WeenieError.ActionCancelled);
};
controller.PositionManager = physicsHost.PositionManager;

View file

@ -551,34 +551,6 @@ internal sealed class RuntimeRemotePhysicsUpdater
if (!previousOnWalkable && finalOnWalkable)
{
// Bug A investigation (2026-08-04, docs/ISSUES.md #32):
// capture the exact state HitGround is about to act on —
// see PhysicsDiagnostics.LogRemoteLanding for the field
// list and PhysicsDiagnostics.ProbeRemoteLandingEnabled
// for the discriminator table. TEMPORARY — strip once
// the live-test run has landed.
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeRemoteLandingEnabled)
{
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLanding(
site: "per-tick",
guid: serverGuid,
airborneBefore: true,
gravitySet: rm.Body.HasGravity,
contact: rm.Body.InContact,
onWalkable: rm.Body.OnWalkable,
hasDefaultSink: rm.Motion.DefaultSink is not null,
resolveIsOnGround: resolveResult.IsOnGround,
sequencerStyle: sequencer?.CurrentStyle ?? 0,
sequencerMotion: sequencer?.CurrentMotion ?? 0);
if (!rm.Body.HasGravity)
{
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLandingGateNoOp(
"per-tick", serverGuid);
}
AcDream.Core.Physics.PhysicsDiagnostics
.BeginRemoteLandingDispatchCapture();
}
// #161: HitGround MUST run with the Gravity state bit
// still set — CMotionInterp::HitGround (0x00528AC0)
// gates on state & 0x400. Bug B deleted the clear that
@ -591,22 +563,6 @@ internal sealed class RuntimeRemotePhysicsUpdater
// (MovementManager::HitGround 0x00524300).
rm.Movement.HitGround();
// Bug A investigation (2026-08-04) — the OUTCOME half of
// the probe above, emitted before the ownership re-check
// below can return so the two lines always pair. See
// PhysicsDiagnostics.LogRemoteLandingAfter and
// docs/research/2026-08-04-bug-a-h3-scheduler-diagnosis.md
// §6.1 for the three-way decision table. TEMPORARY.
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeRemoteLandingEnabled)
{
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLandingAfter(
site: "per-tick",
guid: serverGuid,
hitGroundInvoked: true,
sequencerStyle: sequencer?.CurrentStyle ?? 0,
sequencerMotion: sequencer?.CurrentMotion ?? 0,
forwardCommand: rm.Motion.InterpretedState.ForwardCommand);
}
if (!IsCurrentOwner(
record,
rm,

View file

@ -180,15 +180,6 @@ 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);

View file

@ -51,8 +51,7 @@ public sealed class LaunchOptionsDocumentationTests
["src/AcDream.App/Rendering/Sky/SkyRenderer.cs"] = 1,
["src/AcDream.App/Rendering/TextureCache.cs"] = 1,
["src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs"] = 2,
["src/AcDream.Core/Physics/PhysicsEngine.cs"] = 1,
["src/AcDream.Core/Physics/TransitionTypes.cs"] = 3,
["src/AcDream.Core/Physics/TransitionTypes.cs"] = 2,
["src/AcDream.Core/Vfx/PhysicsScriptRunner.cs"] = 1,
["src/AcDream.Core/World/SkyDescLoader.cs"] = 2,
["src/AcDream.Core.Net/GameEventWiring.cs"] = 1,

View file

@ -32,7 +32,6 @@ public sealed class RetailPViewPassExecutorTests
"landscape-early",
"terrain-clip",
"clear-routing",
"outstage-routing",
"landscape-late",
"landscape-alpha",
"indoor-routing",
@ -126,7 +125,6 @@ public sealed class RetailPViewPassExecutorTests
Assert.Contains("entity-bucket", executor.Operations);
Assert.Contains("cell-particles", executor.Operations);
Assert.Contains("dynamics-particles", executor.Operations);
Assert.Contains("phantom-objects", executor.Operations);
}
[Fact]
@ -591,18 +589,6 @@ public sealed class RetailPViewPassExecutorTests
ClipFrameAssembly clipAssembly,
ClipViewSlice slice,
int sliceIndex) => Operations.Add("clip-probe");
public void EmitOutStageOwner(
WorldEntity entity,
Vector3 sphereCenter,
float sphereRadius,
int sliceIndex,
bool passed) => Operations.Add("outstage-owner");
public void EmitOutStageRouting(
int sliceIndex,
IReadOnlyList<WorldEntity> entities,
ViewconeCuller viewcone) => Operations.Add("outstage-routing");
public void EmitPhantomObjects(uint cellId, int survivorCount) =>
Operations.Add("phantom-objects");
public void DrawLandscapeSlice(
RetailPViewFrameInput frame,
RetailPViewLandscapeSliceContext context)

View file

@ -145,23 +145,6 @@ public sealed class WorldRenderDiagnosticsTests
changed => Assert.StartsWith("[render-sig] frame=3 stable=1 branch=pview", changed));
}
[Fact]
public void OutStageParticleProbe_IsDisabledWithoutEnumerationAndSuppressesDuplicates()
{
var log = new RecordingLog();
var diagnostics = new WorldRenderDiagnostics(new RecordingGlStateReader(), log);
var particles = new ParticleSystem(new EmitterDescRegistry());
diagnostics.EmitOutStageParticles(false, particles, new HashSet<uint>());
diagnostics.EmitOutStageParticles(true, particles, new HashSet<uint>());
diagnostics.EmitOutStageParticles(true, particles, new HashSet<uint>());
Assert.Single(log.Messages);
Assert.Equal(
"[outstage-pt] ids=0 attachedEmitters=0 matched=0 unattached=0 matchedIds=[] unmatchedIds=[]",
log.Messages[0]);
}
private static RenderGlStateSnapshot State(int depthFunction) => new(
DepthTest: true,
DepthWrite: true,

View file

@ -311,7 +311,6 @@ public sealed class WorldRenderFrameBuilderTests
typeof(EnvCellRenderer),
typeof(SceneLightingUbo),
typeof(SceneLightingUboBinding),
typeof(RenderingDiagnostics),
];
string[] methodOrder =
[
@ -323,7 +322,6 @@ public sealed class WorldRenderFrameBuilderTests
nameof(EnvCellRenderer.SetPointSnapshot),
nameof(SceneLightingUbo.Build),
nameof(SceneLightingUboBinding.Upload),
nameof(RenderingDiagnostics.EmitLight),
];
AssertCompiledCallOrder(calls, ownerOrder, methodOrder);

View file

@ -1,120 +0,0 @@
using System.Numerics;
using AcDream.Core.Physics;
using Xunit;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// #337 (2026-08-06 — TEMPORARY, delete with the <c>[support]</c> probe).
///
/// <para>
/// The <c>[support]</c> line's whole value is its <c>support=</c> verdict:
/// terrain, an object surface, or nothing. If that classifier is wrong, a
/// capture does not merely fail to answer — it answers CONFIDENTLY WRONG, and
/// this campaign has already spent two diagnoses on confident wrong answers.
/// These cover the decision boundaries directly, so the live capture can be
/// read at face value.
/// </para>
/// </summary>
public sealed class SupportProbeClassifierTests
{
private const float FlatNormalZ = 1f;
[Fact]
public void NoContactPlane_IsUnsupported()
{
Assert.Equal(
"none",
PhysicsDiagnostics.ClassifySupport(
contactPlaneValid: false,
terrainSampled: true,
contactPlaneZAtXY: 100f,
contactPlaneNormalZ: FlatNormalZ,
terrainZ: 100f,
terrainNormalZ: FlatNormalZ));
}
[Fact]
public void PlaneAtTerrainHeightAndTilt_IsTerrain()
{
Assert.Equal(
"terrain",
PhysicsDiagnostics.ClassifySupport(
contactPlaneValid: true,
terrainSampled: true,
contactPlaneZAtXY: 41.25f,
contactPlaneNormalZ: 0.94f,
terrainZ: 41.26f,
terrainNormalZ: 0.94f));
}
[Fact]
public void PlaneWellAboveTerrain_IsObject()
{
// The rock-plateau shape: the body rests six metres above the ground.
Assert.Equal(
"object",
PhysicsDiagnostics.ClassifySupport(
contactPlaneValid: true,
terrainSampled: true,
contactPlaneZAtXY: 47.5f,
contactPlaneNormalZ: FlatNormalZ,
terrainZ: 41.5f,
terrainNormalZ: 0.9f));
}
[Fact]
public void SameHeightDifferentTilt_IsReportedSeparately()
{
// A collision surface lying flat against sloped ground. This must NOT
// collapse into either answer: it is precisely the ambiguous case, and
// guessing between them is what the probe exists to avoid.
Assert.Equal(
"coplanar-tilt-mismatch",
PhysicsDiagnostics.ClassifySupport(
contactPlaneValid: true,
terrainSampled: true,
contactPlaneZAtXY: 41.5f,
contactPlaneNormalZ: 1.0f,
terrainZ: 41.5f,
terrainNormalZ: 0.72f));
}
[Fact]
public void NoTerrainUnderTheBody_SaysSoRatherThanGuessing()
{
Assert.Equal(
"no-terrain",
PhysicsDiagnostics.ClassifySupport(
contactPlaneValid: true,
terrainSampled: false,
contactPlaneZAtXY: 12f,
contactPlaneNormalZ: FlatNormalZ,
terrainZ: float.NaN,
terrainNormalZ: float.NaN));
}
[Fact]
public void PlaneHeightIsEvaluatedAtTheBodysOwnXy()
{
// A 45-degree ramp through the origin: height must track X, or a body
// standing on a slope would read as displaced from its own support.
var slope = new Plane(Vector3.Normalize(new Vector3(-1f, 0f, 1f)), 0f);
Assert.True(PhysicsDiagnostics.TryPlaneZAt(slope, 0f, 0f, out float atOrigin));
Assert.Equal(0f, atOrigin, 3);
Assert.True(PhysicsDiagnostics.TryPlaneZAt(slope, 10f, 0f, out float atTen));
Assert.Equal(10f, atTen, 3);
}
[Fact]
public void VerticalPlaneHasNoHeight()
{
// A wall is never a floor. Reporting a height for one would read as a
// wildly displaced surface and manufacture a false positive.
var wall = new Plane(new Vector3(1f, 0f, 0f), -5f);
Assert.False(PhysicsDiagnostics.TryPlaneZAt(wall, 0f, 0f, out _));
}
}

View file

@ -137,12 +137,8 @@ public sealed class TransitFailProbeTests
string log = sw.ToString();
// The probe's own families must be completely silent on a healthy
// moving tick. (Console.Out may still carry unrelated one-shot
// process diagnostics — e.g. the #338 AnnounceStepHeightProbeOnce
// self-report, which fires unconditionally on the first IsPlayer
// resolve in the process regardless of any flag — so this checks
// the probe's own tag rather than asserting total silence.)
// The probe's own family must be completely silent on a healthy
// moving tick.
Assert.DoesNotContain("[transit-fail", log);
float actualDy = result.Position.Y - 0.00f;

View file

@ -1,119 +0,0 @@
using AcDream.Core.Physics;
using DatReaderWriter.Enums;
using System.Collections.Generic;
using System.Numerics;
using Xunit;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// Tests for the ISSUES #83 H-disambiguation probe spike (spec
/// 2026-05-21-indoor-walk-miss-probe-design.md).
///
/// Covers:
/// 1. PhysicsDiagnostics.ProbeWalkMissEnabled flag get/set roundtrip.
/// 2. WalkMissDiagnostic.AggregateNearestWalkable selects the nearest
/// walkable polygon by |dz| when the foot XY lies inside a poly's
/// local XY bounding box.
/// 3. WalkMissDiagnostic.AggregateNearestWalkable falls back to the
/// nearest poly by |dz| when no walkable poly XY-contains the foot,
/// reporting ContainsFootXY=false.
/// </summary>
public class WalkMissDiagnosticTests
{
[Fact]
public void ProbeWalkMiss_StaticApi_Roundtrip()
{
bool initial = PhysicsDiagnostics.ProbeWalkMissEnabled;
try
{
PhysicsDiagnostics.ProbeWalkMissEnabled = true;
Assert.True(PhysicsDiagnostics.ProbeWalkMissEnabled);
PhysicsDiagnostics.ProbeWalkMissEnabled = false;
Assert.False(PhysicsDiagnostics.ProbeWalkMissEnabled);
}
finally
{
PhysicsDiagnostics.ProbeWalkMissEnabled = initial;
}
}
private static ResolvedPolygon MakeFloorPoly(
Vector3 v00, Vector3 v10, Vector3 v11, Vector3 v01)
{
var verts = new[] { v00, v10, v11, v01 };
var normal = Vector3.Normalize(Vector3.Cross(v10 - v00, v01 - v00));
float d = -Vector3.Dot(normal, v00);
return new ResolvedPolygon
{
Vertices = verts,
Plane = new System.Numerics.Plane(normal, d),
NumPoints = 4,
SidesType = CullMode.None,
};
}
/// <summary>
/// Foot at (0,0,1). Two walkable polys: a low one at Z=0 (foot is
/// 1 m above) and a high one at Z=0.8 (foot is 0.2 m above).
/// Aggregator picks the high one — smaller |dz|.
/// </summary>
[Fact]
public void AggregateNearestWalkable_PicksNearestByDz_WhenFootXYInsideMultiplePolys()
{
var lowFloor = MakeFloorPoly(
new Vector3(-5f, -5f, 0f),
new Vector3( 5f, -5f, 0f),
new Vector3( 5f, 5f, 0f),
new Vector3(-5f, 5f, 0f));
var highFloor = MakeFloorPoly(
new Vector3(-2f, -2f, 0.8f),
new Vector3( 2f, -2f, 0.8f),
new Vector3( 2f, 2f, 0.8f),
new Vector3(-2f, 2f, 0.8f));
var resolved = new Dictionary<ushort, ResolvedPolygon>
{
[1] = lowFloor,
[2] = highFloor,
};
var result = WalkMissDiagnostic.AggregateNearestWalkable(
resolved,
footLocal: new Vector3(0f, 0f, 1f),
floorZ: PhysicsGlobals.FloorZ);
Assert.True(result.Found);
Assert.Equal((ushort)2, result.PolyId);
Assert.True(result.ContainsFootXY);
Assert.Equal(0.2f, result.Dz, precision: 5);
Assert.Equal(1.0f, result.NormalZ, precision: 5);
}
/// <summary>
/// Foot at (10,10,1) — outside both poly XY bboxes. Aggregator
/// returns the poly with smallest |dz| but with ContainsFootXY=false.
/// </summary>
[Fact]
public void AggregateNearestWalkable_FallsBackByDz_WhenFootXYOutsideAllBboxes()
{
var poly = MakeFloorPoly(
new Vector3(-1f, -1f, 0.5f),
new Vector3( 1f, -1f, 0.5f),
new Vector3( 1f, 1f, 0.5f),
new Vector3(-1f, 1f, 0.5f));
var resolved = new Dictionary<ushort, ResolvedPolygon> { [42] = poly };
var result = WalkMissDiagnostic.AggregateNearestWalkable(
resolved,
footLocal: new Vector3(10f, 10f, 1f),
floorZ: PhysicsGlobals.FloorZ);
Assert.True(result.Found);
Assert.Equal((ushort)42, result.PolyId);
Assert.False(result.ContainsFootXY);
Assert.Equal(0.5f, result.Dz, precision: 5);
}
}

View file

@ -176,11 +176,4 @@ public sealed class RenderingDiagnosticsTests
Assert.Single(set);
Assert.Contains(0x020003F2u, set);
}
[Fact]
public void DumpEntitySourceIds_DefaultsEmpty_WhenEnvUnset()
{
// Env var is absent in the test host → probe inert.
Assert.Empty(RenderingDiagnostics.DumpEntitySourceIds);
}
}