Commit graph

330 commits

Author SHA1 Message Date
Erik
0442eadcec test(p1): production-path membership conformance — divergence CONFIRMED (0/11), not a probe artifact
Replays the golden indoor 0170<->0171 segments through the real
PhysicsEngine.ResolveWithTransition (engine builds the global sphere + sweeps;
cells loaded from dats with real BSP). Result: 0/11 match retail. Every segment
restPos==target (the sweep completes the move) but CellId stays on the SOURCE
cell — acdream moves the body across the doorway yet NEVER advances curr_cell.
So the 'probe artifact' hypothesis is FALSIFIED: production membership genuinely
lags retail.

Refined mechanism: both retail and acdream PICK with center-only point_in_cell
(architect's radius-aware-pick hypothesis falsified, confirmed by reading
CEnvCell::point_in_cell -> BSPTREE::point_inside_cell_bsp). The gap is retail's
curr_cell ADVANCES across the portal mid-sweep (swept crossing / leading sphere
point) while acdream's swept advance keeps the source cell. P1 ports that advance.

ProductionPath_IndoorCrossings_DivergeFromRetail_PendingP1 is the RED gate the P1
fix must turn GREEN. Conformance 60 pass / 1 skip / 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:56:52 +02:00
Erik
bb4dead0ae test(p0): retail-trace golden captured — membership criterion divergence pinned (P0 GATE MET)
P0 Task 6 complete. Captured live retail membership at the 0031<->0170<->0171
doorway via cdb on CPhysicsObj::change_cell (symbol-driven; offsets verified by
discover-types.cdb; PDB MATCH). 22 transitions, clean monotonic sequence, NO
ping-pong (retail is correct-by-construction). Golden:
Conformance/Fixtures/find-cell-list-threshold.log.

ROOT-CAUSE FINDING (the central P1 work): retail transitions membership at the
PORTAL CROSSING (CEnvCell::find_transit_cells @ 0x52c820 pc:309968 — sphere crosses
the doorway polygon plane), while acdream's FindCellList re-picks by POINT-IN-CELL
containment at the foot. Retail commits room 0171 while the foot is STILL inside
vestibule 0170's BSP (in_0171=0); acdream lags. ALL 22 transitions diverge for this
one criterion mismatch — not a per-cell hysteresis or a building-entry-only split.
This is master-plan §0 'hysteresis gap' confirmed against the real client.

FindCellList_DoorwayThreshold_DivergesFromRetail_PendingP1 (documents-the-bug, GREEN)
+ ThresholdDivergenceDiagnosticTests (per-transition containment print) pin it; both
flip when P1 ports the directed portal crossing. Conformance 59 pass / 1 skip / 0 fail;
full Core 1308 pass / 5 fail (baseline) / 1 skip — no new failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:04:51 +02:00
Erik
1662da8731 test(p0): threshold-trace golden wiring + PVS scaffold + P1-entry checklist
P0 Tasks 6 (autonomous half) + 7. FindCellList_DoorwayThreshold_MatchesRetailTrace
asserts acdream's pick == each captured retail pick; skips until the capture
fixture lands. PvsConformanceTests scaffolds the render visible-set golden
(skipped; filled in P4). ConformanceDats.FixturesDir resolves fixtures from the
source tree (issue98 pattern). Notes record: existing retail traces are
collision-only (no membership) so the strict P1 gate needs the one live capture;
plus the P1 re-scope finding (Stage-1 membership already on this branch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:29:30 +02:00
Erik
b35e491f12 test(p0): retail find_cell_list trace parser + cdb value-capture tooling
P0 Task 5. RetailTrace parses the [fcl] golden format (seed/pos/picked,
RetailCellPick); 4 TDD tests green. find-cell-list-capture.cdb targets
CPhysicsObj::change_cell (commit-on-diff) to capture retail's accepted
membership sequence at the doorway; README is the operator runbook
(dt offset verification + decode_retail_hex float decode). The live run
is P0's one user-gated step (Task 6 mines existing traces first).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:26:24 +02:00
Erik
ec78beb843 test(p0): find_cell_list golden conformance — unambiguous interior picks
P0 Task 4. FindCellList resolves a sphere deep inside room 0171 -> 0171,
deep inside vestibule 0170 -> 0170, and re-picks 0170 from a stale 0171
seed (membership re-picks by containment, not the seed). Retail-faithful
by construction (candidate cells loaded from the real dats). The subtle
doorway-threshold pick is the trace-backed golden (Task 6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:21:44 +02:00
Erik
a90f34368f test(p0): dat-backed conformance loader + characterized cottage-doorway topology
P0 (verbatim-spatial-pipeline-port) Tasks 1+2. ConformanceDats loads the
cottage-doorway cells from the real dats with their real ContainmentBsp;
CottageDoorwayCharacterizationTests maps the Holtburg 0140..017F indoor
neighborhood and pins the master-plan threshold building (origin
161.93,7.50,94.00): 0xA9B40170 vestibule (exit portal 0xFFFF + portal to
0171), 0xA9B40171 room. Grid math confirms the outdoor side is landcell
0xA9B40031 -> the 0031<->0170<->0171 ping-pong is verified real. Verified
interior points recorded for the point_in_cell/find_cell_list goldens.

Plan: docs/superpowers/plans/2026-06-03-p0-conformance-apparatus.md
Notes: docs/research/2026-06-03-p0-conformance-apparatus-notes.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:20:17 +02:00
Erik
79fb6e7c23 fix(render): doorway blue-hole — render root clobbered by NPCs (CurrCell per-entity write)
THE doorway flap root cause, found via [flap-cam]/[shell]/[cell-transit] (2026-06-03):
the player spawned + stood still in the room (cell 0171, NO [cell-transit] after teleport),
yet the render rooted at the vestibule (0170) for all 77,951 frames — drawing only 0170's
~8-triangle shell, the rest = GL clear color = the bluish void.

CellGraph.CurrCell IS "the player's cell" (the render root), but it was written by
SetCurrAndReturn inside the PER-ENTITY ResolveWithTransition + ResolveCellId — so EVERY NPC
wrote it. A Holtburg NPC (0x000F4240) jump-looping near the doorway clobbered the player's
render root every tick. Standing still (player makes no resolve calls) the NPC's write wins
→ stuck blue void; moving, player/NPC writes fight → the flap. This is why the membership
pick fix (correct, kept) didn't change the visual — the render root was clobbered regardless.

Fix: CurrCell is now written ONLY by the player. New PhysicsEngine.UpdatePlayerCurrCell is
called from PlayerMovementController.UpdateCellId — the single player-only chokepoint for
CellId (teleport / server snap @ SetPosition + per-frame resolver). Removed the CurrCell
write from SetCurrAndReturn (inlined the 2 resolve call sites to sp.CurCellId) and the 4
ResolveCellId sites. NPCs no longer touch the render root. Teleport→UpdateCellId also covers
spawn/standing-still (CurrCell = the player's spawn cell immediately).

CellGraphMembershipTests rewritten to the new contract (3 tests): UpdatePlayerCurrCell writes
the render root; ResolveCellId does NOT (the blue-hole guard); stale-beats-null preserved.
Full Core suite: 1295 pass / 5 fail = the documented §10 baseline, zero new breakage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:12:38 +02:00
Erik
22a184ca68 fix(physics): Stage 1 — verbatim ordered-CELLARRAY membership pick (the R1 flap)
Port CObjCell::find_cell_list (acclient_2013_pseudo_c.txt:308742) faithfully:
- build candidates into an ordered CellArray with the CURRENT cell at index 0
  (add_cell @308766);
- EXPAND via a single forward walk over the growing array, mirroring retail's
  for(i=0;i<num_cells;i++) cells[i].find_transit_cells loop (308775-308785),
  replacing the order-losing Queue/visited BFS;
- PICK in array order with interior-wins-break (308788-308825): current cell at
  index 0 wins a boundary straddle, so membership no longer ping-pongs.

Deletes the 5ca2f44 current-first pre-check (the ordered array subsumes it for every
seed). Keeps its guard test (TwoOverlappingCells_CurrentCellWinsTheStraddle) + adds
two conformance tests (current-cell-first ordering; interior-wins over outdoor
fallback). Membership net: 45 pass. Decomp finding: retail stability is emergent from
the ordered pick + carried seed, not a separate portal-crossing detector — see
docs/research/2026-06-03-cell-membership-ordered-cellarray-pseudocode.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 09:00:12 +02:00
Erik
b44dd147bc feat(physics): Stage 1 — CellArray ordered/deduped cell collection (retail CELLARRAY)
Ports retail CELLARRAY::add_cell (acclient_2013_pseudo_c.txt:701036): ordered list,
dedup by cell_id, append at end. The order is load-bearing for the verbatim
find_cell_list current-cell-first interior-wins pick (next commits) that fixes the
R1 cottage membership flap. Implements ICollection<uint> (helper-facing) +
IReadOnlyCollection<uint> (consumer-facing). 5 unit tests.

Also lands the membership-port pseudocode (workflow step 3) + the Stage-1 plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:54:45 +02:00
Erik
5ca2f448d4 fix(physics): R1 membership — current-cell-first hysteresis in find_cell_list pick
The flap R1 exposed is a cell-membership ping-pong: the find_cell_list containing-
cell pick (CellTransit.BuildCellSetAndPickContaining) iterated an UNORDERED HashSet
and returned the first interior cell whose BSP contains the sphere center, with no
preference for the current cell. Retail CObjCell::find_cell_list adds the current
cell at index 0 (add_cell, pc:308766) and iterates current-first with interior-wins-
break (pc:308791-308819) — you STAY in your current cell until the center genuinely
leaves it. acdream's HashSet dropped that ordering; once the candidate set churns at
a boundary the enumeration can surface a neighbour before the current cell → the
ping-pong. Restore the explicit, deterministic current-cell-first test (retail's
index-0 hysteresis). + a two-direction regression guard (current cell wins the
straddle).

Diagnosed from the existing [cell-transit] walk log (no new probing): room flips are
the pick non-determinism; stairs flips additionally show the foot Z oscillating
~0.2m/tick (a separate stairs-physics residual, #98 family, to verify after this).

The 2 DoorBugTrajectoryReplay failures are PRE-EXISTING (verified: they fail without
this change too) — 2 of the handoff's '3 door-collision apparatus / A6.P5'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 21:51:25 +02:00
Erik
4bc99fc6fd test(physics): Phase W triage — fix stale Path6/tick-gate/ComputeOffset tests (behavior changed by L.3.2/L.4/L.5)
Four tests were asserting pre-change behavior after intentional production
changes:

#2 BSPStepUpTests.C3_Path6_AirborneMoverHitsSteepSlope_SetsCollide
  b1af56e (L.4, 2026-04-30) added a steep-normal gate in Path 6 that
  fires BEFORE SetCollide. Airborne sphere hitting steep poly now returns
  Slid + Collide=false (slide-tangent interim fix). Updated assertion +
  renamed to ReturnsSlid.

#7 PlayerMovementControllerTests.Update_ForwardInput_MovesInFacingDirection
#8 DispatcherToMovementIntegrationTests.Dispatcher_W_held_produces_forward_motion
  235de33 (L.5, 2026-04-30) added _physicsAccum accumulator gate: a single
  Update(1.0f) only integrates one MaxQuantum (0.1s ~ 0.312m at walk speed),
  not the full 1s. Time is carried in accumulator (not dropped). Fixed both
  tests to loop Update(MaxQuantum) for ~11 ticks to accumulate >2m of real
  forward motion, preserving the original distance-threshold assertion intent.

#9 PositionManagerTests.ComputeOffset_BothActive_Combined
  842dfcd (L.3.2, 2026-05-03) changed ComputeOffset from additive
  (rootMotion + correction) to replace semantics: when AdjustOffset returns
  non-zero, it REPLACES root motion (retail Frame::operator= semantics).
  offset.Y = 0 (not 0.4); root motion is dropped when catch-up engages.
  Updated assertion and renamed to CorrectionReplacesRootMotion.

Suite: 9 failures → 5 (only the 5 known-bug tests remain red).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 16:43:02 +02:00
Erik
21609a7cd7 test(physics): Phase W triage — fix stale GetMaxSpeed tests; file #104 (particle cell-clip deferral)
GetMaxSpeed deliberately does NOT branch on ForwardCommand — it returns RunAnimSpeed x run-rate as the InterpolationManager.AdjustOffset catch-up speed (doc comment + ACE MotionInterp.cs:670-678, retail-verified; the slow catch-up fixed the 1-Hz remote-blip). The 3 failing tests (WalkForward/WalkBackward/Idle) asserted a REMOVED command-branching design. Consolidated into one [Theory] pinning the no-branch contract across commands.

Also files #104 (LOW): scene VFX particles not clipped to the PView visible cell set — deferred out of the Phase W seal (entity bleed already gated by Stage 5; scene particles depth-tested; sky particles scissored). Needs OwnerCellId plumbing (~6-8 files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 16:37:49 +02:00
Erik
573c5559a0 test(render): Stage 3 T3.4 — CellGraphRootTests (6 tests)
New test file: tests/AcDream.Core.Tests/Rendering/CellGraphRootTests.cs.

Tests 1-3: render-branch predicates (rootSeenOutside, playerInsideCell, renderSky):
  RootSelection_OutdoorRoot_NullCurrCell_SeenOutsideDefaultsToTrue
  RootSelection_BuildingInterior_SeenOutside_SkyRenderedAndSunKept
  RootSelection_Dungeon_NoSeenOutside_SkyNotRenderedAndSunZeroed

Tests 4-6: CellGraph.FindVisibleChildCell:
  FindVisibleChildCell_PlayerCellContains_ReturnsPlayerCell
  FindVisibleChildCell_StabListContains_ReturnsNeighbour
  FindVisibleChildCell_NeitherContains_ReturnsNull

All 6 pass. Core suite: 12 pre-existing failures (same baseline), 1276 passing.
App suite: 160/160 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:37:19 +02:00
Erik
fcea816391 test(core): commit doorway-threshold fixture for DoorwayMembershipReplayTests (portable)
T0 test-hygiene pass (2026-06-02): DoorwayMembershipReplayTests previously loaded
doorway-capture.jsonl from the repo root — a 719 MB untracked file that only
exists on the developer's machine after a specific live capture run. On any other
machine (CI, fresh worktree, other developers) the tests would silently SKIP instead
of running.

Fixes:
- Extract the 57 doorway-seam records (Y∈[15.5,17.5], ticks 17392-17448) from the
  large capture into committed fixture
  tests/AcDream.Core.Tests/Fixtures/issue98/doorway-threshold-capture.jsonl (110 KB).
- Update DoorwayMembershipReplayTests to use FixturePath() (same SolutionRoot walk
  pattern as CellarUpTrajectoryReplayTests) instead of FindCapturePath().
- Change from silent-skip-if-absent to Assert.True(File.Exists) with a clear error
  message — the committed fixture must be present.
- Both DoorwaySeam_FindCellSet_StableNoStrobe and
  OutdoorSeamRecords_FindCellSet_ReturnsCorrectOutdoorCell pass against the fixture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:22:57 +02:00
Erik
21ee5e1035 test: fix PhysicsResolveCapture/PhysicsDiagnostics static-leak isolation
xUnit's default parallel execution let diagnostic-harness tests (CellarUp,
DoorBug, DoorCollisionApparatus) mutate PhysicsResolveCapture.CapturePath
and PhysicsDiagnostics probe flags concurrently with victim tests
(MotionInterpreter, PositionManager, PlayerMovementController,
DispatcherToMovement, BSPStepUp), producing a flaky 14-26 failure range.

Fixes:
- Add PhysicsResolveCapture.ResetForTest() + PhysicsDiagnostics.ResetForTest()
  as documented test-only reset APIs (never called from production paths).
- Add IDisposable to CellarUpTrajectoryReplayTests with ctor/Dispose calling
  both ResetForTest() — prevents CapturePath from leaking between the Capture_*
  tests in the same class (the immediate root cause of Capture_SkipsNonPlayerCalls
  finding an unexpected file).
- Add xunit.runner.json (maxParallelThreads=1, parallelizeTestCollections=false)
  to AcDream.Core.Tests — eliminates parallelism-induced probe-flag leaks across
  all test classes without requiring [Collection] boilerplate on every offender.

After: two consecutive runs produce the identical 12-failure set.
Confirmed: LiveCompare_FirstCap_FixClosesCottageFloorCap passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:20:24 +02:00
Erik
59f3a1380d feat(core): Phase W — faithful find_cell_list membership (interior-wins pick + swept determination, drop static :1947)
Change A (TransitionTypes.FindEnvCollisions:~1947): replace the unconditional
static ResolveCellId re-derive with the SWEPT find_cell_list pick via
CellTransit.FindCellSet. When DataCache is available (always in production),
the swept pick runs and resolves the containing cell from the portal-graph
candidate set. When DataCache is null (test engines without a cell registry),
the old ResolveCellId fallback is preserved to keep PhysicsEngineTests green.

Change B (CellTransit.BuildCellSetAndPickContaining): replace the containment
loop that silently skipped all outdoor candidates (CellBSP=null) with the
retail CObjCell::find_cell_list interior-wins pick (pseudo_c:308788-308819):
interior EnvCells win first; if no interior cell contains the center, fall
to the outdoor XY-grid column (CLandCell::point_in_cell equivalent). This is
the missing half of find_cell_list that caused the 0xA9B40170↔0xA9B40031
doorway cell-strobe — the swept pick previously always returned currentCellId
for outdoor candidates, letting the static re-derive at :1947 strobe on every
tick from a different result.

DoorwayMembershipReplayTests: two facts, loads doorway-capture.jsonl (364K records,
strobing live run), filters to Y∈[15.5,17.5] seam zone (57 records), verifies
FindCellSet produces exactly 1 transition (enter indoor → stay outdoors) with
zero A→B→A ping-pong across the full window. Second test verifies outdoor-seed
records round-trip correctly via the XY-grid formula. Both pass.

LiveCompare_FirstCap_FixClosesCottageFloorCap: still passes (issue #98 gate intact).
Full Core suite: 15 failures (within documented flaky baseline of 14–19;
all 15 are pre-existing static-leak/document-the-bug tests, zero new regressions
in cell/transit/BSP/physics classes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 14:53:30 +02:00
Erik
d23d1f40dc Revert "feat(core): UCG W2 Task 3 — stab-list doorway hysteresis in ResolveCellId"
This reverts commit 2acd8f9e1d.
2026-06-02 14:09:42 +02:00
Erik
851cecc757 feat(core): Phase W Stage 0 — [cell-swept] diagnostic (swept vs static cell, no behavior change)
Add ProbeSweptEnabled (ACDREAM_PROBE_SWEPT=1) to PhysicsDiagnostics mirroring
ProbeCellEnabled. Emits one [cell-swept] line per ResolveWithTransition call —
sp.CurCellId and sp.CheckCellId (the transition's swept cells) alongside the
incoming cellId so a doorway capture shows whether the swept cell is stable
where ResolveCellId strobes. No ResolveCellId call in the probe — avoids the
CellGraph.CurrCell side effect. No behavior change.

TDD: ProbeSweptEnabled_DefaultsToFalse RED→GREEN in PhysicsDiagnosticsTests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 14:08:00 +02:00
Erik
2acd8f9e1d feat(core): UCG W2 Task 3 — stab-list doorway hysteresis in ResolveCellId
Port retail CObjCell::find_cell_list do_not_load_cells prune
(acclient_2013_pseudo_c.txt:308829-308867) as indoor->outdoor doorway
hysteresis: hold the previous indoor cell when the outdoor candidate is
not in its stab list AND the foot-sphere still overlaps the cell's
containment BSP expanded by DoorwayHoldMargin. Kills the front-door
0170<->0031 ping-pong (handoff §5) the #98 saga never addressed. Fires
only at the front-door seam; the cellar has no exit portal so it never
falls through here (#98 cellar-up untouched).

Three TDD tests in CellGraphMembershipTests: HOLD (the RED->GREEN case,
Y=3.9 inside the 0.2 m margin), RELEASE when fully outside (Y=4.5
exceeds expanded margin), and stab-list gate (outdoor candidate in stab
list releases even near the boundary).

Adds using System.Linq for IReadOnlyList.Contains at the prune site.
SphereOverlapsEnvCell helper mirrors BSPQuery.SphereIntersectsCellBsp
via EnvCell.InverseWorldTransform + ContainmentBsp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:22:50 +02:00
Erik
0e27a6cc3f feat(core): UCG W2 Task 1 — ResolveCellId writes CellGraph.CurrCell (additive)
Add private `SetCurrAndReturn(uint)` helper in PhysicsEngine that looks up
the resolved id in `DataCache.CellGraph` and writes `CurrCell` when the cell
is present.  Wrap the four RESOLVED-id return sites in ResolveCellId:
  - indoor no-CellBSP return (trust FindCellList)
  - indoor sphere-overlaps-CellBSP return
  - outdoor→indoor building-transit return (foreach candidate)
  - outdoor terrain-grid return
The final no-match `return fallbackCellId;` is intentionally NOT wrapped —
stale beats null (the caller's seed is preserved unchanged).

CurrCell has zero readers in src/ (verified by ripgrep); this is additive
write-only, identical observable behavior to W1.  One new unit test
(CellGraphMembershipTests) proves RED→GREEN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:17:09 +02:00
Erik
8e703bef22 feat(core): UCG Stage 1 — populate CellGraph from CacheCellStruct + AddLandblock (inert)
PhysicsDataCache gains a `CellGraph` property (UCG Stage 1). The env-cell
hook is placed at the very top of CacheCellStruct — before the idempotency
guard and the null-PhysicsBSP early-return — so BSP-less cells are included
in the graph even though they are dropped from the legacy _cellStruct map.
PhysicsEngine.AddLandblock/RemoveLandblock mirror terrain registration into
the graph via a null-guarded DataCache?.CellGraph call. Zero behavior change:
CellGraph has no readers this stage.

A using-alias (UcgEnvCell / UcgCellGraph) resolves the EnvCell name
collision between AcDream.Core.World.Cells and DatReaderWriter.DBObjs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:29:30 +02:00
Erik
1aede3d6aa test(core): UCG Stage 1 — real cottage-cell fixture grounding
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:24:37 +02:00
Erik
cf5d60d8fb feat(core): UCG Stage 1 — CellGraph resolver + registry + inert CurrCell
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:21:12 +02:00
Erik
b4c4318c8b feat(core): UCG Stage 1 — LandCell synthesized from TerrainSurface
Outdoor terrain cell (retail CLandCell) synthesized on demand from a
landblock's TerrainSurface. Factory Synthesize() samples four quad
corners to establish Z bounds; PointInCell() tests the 24 m XY quad
in world-local space. BuildingCellId stub is null (Stage 2).
2/2 tests RED→GREEN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:17:57 +02:00
Erik
03f08f00c1 fix(core): UCG Stage 1 — ResolvePortalPolygon all-or-nothing (match BuildLoadedCell)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:15:37 +02:00
Erik
5bc72d5cd1 feat(core): UCG Stage 1 — EnvCell.FromDat derivation (mirrors BuildLoadedCell)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:07:18 +02:00
Erik
76c9e2f07d feat(core): UCG Stage 1 — EnvCell + PointInCell (AABB/BSP)
Adds `EnvCell` (sealed, extends `ObjCell`) with a primitive constructor
and `PointInCell` that uses the cell-containment BSP when present, else
falls back to an AABB test. Retail anchor: CEnvCell (acclient.h:32072).
BSP branch delegates to `BSPQuery.PointInsideCellBsp` (BSPQuery.cs:1034);
the AABB branch is the genuinely new logic. No `FromDat` factory — that is
a separate later task. Consumed by nobody yet (Stage 1 scaffold).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 08:59:52 +02:00
Erik
9cb15710be feat(core): UCG Stage 1 — ObjCell base + CellPortal
Introduces AcDream.Core.World.Cells namespace with the two foundational
types for the Unified Cell Graph. CellPortal is a readonly struct
unifying the three legacy portal representations; ObjCell is the abstract
base for all traversable cells with the retail id-magnitude IsEnv
discriminator (CObjCell::GetVisible, pseudo_c:308215). Zero consumers;
zero behavior change. 5/5 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 08:51:55 +02:00
Erik
0b125830fe feat(render): Phase U.2d — ACDREAM_PROBE_VIS visibility probe in RenderingDiagnostics
Add the durable per-frame visibility probe apparatus that #103 lacked, so
the Phase U portal-visibility builder can be validated on live frames before
any GL/visual work.

EmitVis(rootCellId, visibleCells, outsidePolyCount, outsidePlaneCount,
perCellPlaneCounts, scissorFallbacks) prints ONE concise [vis] line gated on
root-cell CHANGE (private _lastVisRootCellId tracker; no-op when the root is
unchanged or ProbeVisibilityEnabled is false — one bool compare per frame when
off). Line format:
  [vis] root=0x… cells=N ids=[…] outside(polys=…,planes=…) percell=[0x…:N,…] fallbacks=…

Reuses the existing Phase A8 ProbeVisibilityEnabled flag (env ACDREAM_PROBE_VIS,
already DebugPanel-mirrored via DebugVM.ProbeVisibility) rather than adding a
parallel owner — Code Structure Rule 5 (one diagnostic owner per subsystem).
Property doc repurposed from the abandoned A8 two-pipe stencil semantics to the
Phase U unified pipeline.

Decoupling note: RenderingDiagnostics lives in AcDream.Core, which must not
reference AcDream.App (Code Structure Rule 2). The plan's EmitVis signature took
an App-layer CellView; this lands the equivalent as pre-computed primitives
(outsidePolyCount + outsidePlaneCount) so the owner stays in Core. The U.4a call
site supplies OutsideView.Polygons.Count and the OutsideView ClipPlaneSet.Count.

TDD: 3 new tests in RenderingDiagnosticsVisibilityTests (no-op when disabled,
fires-once-per-new-root + suppressed-on-unchanged, env-default contract), each
self-contained via internal ResetVisibilityProbeForTests + Console.Out capture
to avoid the documented static-leak flakiness. Core suite +3 tests, no new
failures (flaky physics/input static-leak set unchanged at 16, untouched area).

Courtesy: removed the dangling RenderInsideOutAcdream comment reference (deleted
in U.1) + the AcDream.App.Rendering.Wb doc cref (a Core→App layer inversion).

The emit SITE wiring (per-frame call from the render loop) lands in U.4a; this
task lands only the owner members + formatter + test. GameWindow untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 17:11:02 +02:00
Erik
3fc77be5de refactor(render): Phase U.1 — delete two-pipe inside-out machinery
Remove IndoorCellStencilPipeline + portal_stencil shaders, RenderInsideOutAcdream,
RenderOutsideInAcdream, the A8-perf instrumentation, the cameraInsideBuilding /
ACDREAM_A8_INDOOR_BRANCH branch, and the dead EntitySet partition values. Collapse
the render branch to the default Draw(All) path (U.4a replaces it with the gated
unified pass). Keep all audited EnvCellRenderer / BuildingLoader / CellVisibility /
camera-collision fixes.

Also deleted with the partition: the two test-only walk helpers
(WbDrawDispatcher.WalkEntitiesForTest / WalkEntitiesForTestByCellIds) and their
test files (WbDrawDispatcherEntitySetTests, WbDrawDispatcherCellIdsOverloadTests),
which existed solely to exercise the removed IndoorPass/OutdoorScenery/
BuildingShells/LiveDynamic partition. EntityMatchesSet / IsShellScopedSet collapse
to the All-path constants; the set: parameter is retained as a seam for the
unified pass.

Note: the depth-clear-if-inside default-path workaround was removed per the
U.1 task list — any current indoor-wall degradation persists until a later
Phase U task lands the unified pass (expected, not a regression introduced here).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 16:05:19 +02:00
Erik
7a244b3291 fix(physics): Phase A8.F — viewer sweeps bypass the 30-step cap (retail-faithful)
Retail's CTransition::find_transitional_position (:273613) has no step
cap. calc_num_steps (:272149) has a dedicated viewer branch `if ((state
& 4) != 0)` at :272181 for sight/viewer objects (ObjectInfoState.IsViewer
= 0x4). The existing acdream cap correctly had a comment "Sight objects
bypass this" but the bypass was never wired — no IsViewer caller existed
until the A8.F camera spring-arm.

With radius 0.3 m the cap fires at ~9 m. The spring-arm sweeps up to
40 m (≈134 steps), so zoomed-out cameras snapped to the player's head
instead of sweeping through geometry. The fix adds `&& !ObjectInfo.IsViewer`
to the guard; non-viewers keep the 30-step safety net (player spheres
~0.48 m radius never exceed 14 m/tick).

Conformance test: radius=0.3, dist=12 (40 steps > 30 cap) over flat
terrain. Normal mover bails (Assert.False). Viewer proceeds to target
(Assert.True + CurPos.X > from.X). RED → GREEN.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 20:35:26 +02:00
Erik
fcea05f808 fix(render): Phase A8.F — camera sweep uses retail moverFlags 0x5c (PathClipped hard-stop)
Code review found the probe passed ObjectInfoState.None; retail's
SmartBox::update_viewer calls init_object(player, 0x5c) =
IsViewer|PathClipped|FreeRotate|PerfectClip (pseudo-C :92864). PathClipped makes
the sweep hard-stop at first contact (TransitionTypes.cs:811) instead of
edge-sliding around corners (which would re-trigger the A8.F camera-cell
instability); IsViewer lets the eye pass through creatures, colliding only with
world geometry. Resolves the spec's slide-vs-stop open question. Also reset
CollideCamera in the Defaults_AreRetailValues baseline test (review: maintenance
trap). Spec §5.1/§11.1 synced.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 19:11:53 +02:00
Erik
69c7f8db86 feat(render): Phase A8.F — add CameraDiagnostics.CollideCamera flag (default on)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 19:00:11 +02:00
Erik
5dc4140c11 feat(render): Phase A8 — indoor visibility + streaming fixes batch
Lands the working A8 indoor-rendering and streaming fixes accumulated this
session. User has verified these visually to some degree (e.g. lifestone /
translucent meshes confirmed fine under the FrontFace flip; bridge / wall /
collision regressions confirmed fixed after travel); not every path has been
exhaustively gated. The cellar-flap defect remains OPEN and will be solved
the retail-faithful way via a dedicated brainstorm (see handoff docs).

Rendering core (reviewed, high confidence):
- EnvCellRenderer SSBO stride fix: upload packed Matrix4x4[] (64B) instead of
  the 80B CPU InstanceData struct the shader never expected — fixes the
  transform/texture "explosion" for any draw with >1 instance (cells that
  dedupe to a shared cellGeomId). Real root cause.
- WB-style global FrontFace(CW) + per-batch CullMode carried through the MDI
  layout (GroupKey + BuildIndirectArrays + DrawIndirectRange split into
  same-cull runs with absolute uDrawIDOffset per run).
- EntitySet partitioning (IndoorPass / OutdoorScenery / LiveDynamic) +
  WorldEntity.BuildingShellAnchorCellId so building shells scope to their
  dat-derived building cell instead of rendering everywhere.
- RenderOutsideInAcdream (look into buildings from outside) +
  CollectVisiblePortalBuildings frustum cull of portal bounds.
- Sky-when-inside-building + per-cell audit probe + GL-state probe.

Streaming / perf (test-covered; not independently code-reviewed this session):
- Near/far priority queues so near work wins over far; PromoteToNear carries
  full landblock + mesh data; LandblockEntriesWithoutAnimatedIndex avoids
  rebuilding the animated-lookup dict in the hot draw path. Fixes the
  bridge-not-appearing / missing-walls / broken-collision-after-travel
  regressions and improves post-transition FPS.

Tooling + docs:
- tools/A8CellAudit: offline dat cell/portal/building dumper (portals +
  buildings modes) — reproduces the cellar-flap investigation with no launch.
- docs/research cellar-flap root-cause + option-2 handoff (the didInsideStencil
  double-duty finding + the WB-recursive design decision + brainstorm prompt),
  entity-taxonomy, replan, issue-78 visibility investigation.

Diagnostics retained on purpose: ACDREAM_A8_DIAG_* gates, portal_stencil.vert
provisional pos.w clamp, and the probe families are kept (env-var gated, zero
cost when off) because the pending option-2 cellar-flap brainstorm needs them.
Strip in the option-2 ship commit.

Indoor branch stays behind ACDREAM_A8_INDOOR_BRANCH=1 (default off = pre-A8
visual). Build green; App tests + Core (streaming/dispatcher/loader) tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 10:14:50 +02:00
Erik
3361933ce6 feat(render): Phase A8 RR5 — WbDrawDispatcher Draw(cellIds:) overload
Adds a new public overload accepting an explicit IReadOnlyCollection<uint>
cellIds (the camera-buildings' EnvCellIds) instead of a BFS-derived
visibility set. Used by RR7's IndoorPass to scope indoor rendering to the
camera-buildings' cells, not the full portal BFS (which causes Issues A+C).

Pure-data test helper WalkEntitiesForTestByCellIds added alongside the
production overload, mirroring the WalkEntitiesForTest pattern.

The overload internally delegates to the existing visibleCellIds path —
the dispatcher's semantic stays the same; only the caller's intent differs
(explicit cell list vs visibility-derived).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:18:21 +02:00
Erik
55f26f2a9c feat(render): Phase A8 R2 — WbDrawDispatcher.EntitySet taxonomy partition
Reshapes the dormant EntitySet enum from binary IndoorOnly/OutdoorOnly to
a three-way taxonomy-aware partition:

  IndoorPass     — cell mesh + cell statics + building shells
                   (ParentCellId.HasValue OR IsBuildingShell), live-dynamic
                   excluded
  OutdoorScenery — outdoor scenery only (ParentCellId == null AND
                   !IsBuildingShell), live-dynamic excluded
  LiveDynamic    — ServerGuid != 0 (player, NPCs, dropped items)

Centralizes the membership predicate in EntityMatchesSet to keep the three
call sites (two in WalkEntitiesInto, one in WalkEntitiesForTest) DRY.

R1's IsBuildingShell flag is now consumed at render time. Integration into
the render frame ships in R3.

Tests rebuilt from scratch — 7 cases cover the new partition truth table.
Existing dispatcher tests (Tier 1 cache, etc.) continue to pass under the
default EntitySet.All.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:42:09 +02:00
Erik
ed72704f7b feat(world): Phase A8 R1 — tag WorldEntity.IsBuildingShell at LandblockLoader
Adds a bool flag at the WorldEntity data layer set by LandblockLoader from
the source dat array: LandBlockInfo.Buildings → true (cottage walls, inn
walls, smithy walls); LandBlockInfo.Objects → false (trees, lampposts,
rocks, hitching posts).

Retail anchor: CLandBlock::init_buildings reads a separate BuildInfo**
array from objects (acclient.h:31893 num_buildings / buildings field;
acclient_2013_pseudo_c.txt:313854 init_buildings entry). WorldBuilder
preserves the same distinction via SceneryInstance.IsBuilding
(StaticObjectRenderManager.cs:334). Today acdream's loader reads both
arrays into the same WorldEntity pool with no tag, destroying the
distinction (the comment at GameWindow.cs:5175 already acknowledges this
gap for scenery suppression). This commit closes the gap.

Render-time consumption arrives in R2 (EntitySet partition refactor).
Two new LandblockLoader tests lock the tagging behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:31:11 +02:00
Erik
96f8bd2bd7 Revert "fix(render): Phase A8 — animated entities exempt from stencil-gated outdoor pass"
This reverts commit a2ad5c1ac4.
2026-05-26 09:38:37 +02:00
Erik
a2ad5c1ac4 fix(render): Phase A8 — animated entities exempt from stencil-gated outdoor pass
Visual verification of A8 (commit 41c2e67) surfaced a showstopper:
player + NPCs disappeared when the camera entered a building. Root
cause: live server-spawned entities (animated player/NPCs/monsters)
have ParentCellId == null. The EntitySet partition classified them
as "outdoor" and stencil-gated them in the OutdoorOnly pass — so
they only rendered where stencil bit 1 was set (portal silhouettes),
producing partial-body and head-backwards artifacts at doorway
transits and full invisibility everywhere else inside.

Fix: animatedEntityIds overrides the ParentCellId-based partition.
Animated entities always belong in the IndoorOnly pass (stencil OFF),
never in OutdoorOnly. Three changes:
- WalkEntitiesInto full-walk path: compute isAnimated up front, use
  it in both partition checks
- WalkEntitiesInto animated-only path: skip the entire path on
  OutdoorOnly (every iterated entity is animated by definition)
- WalkEntitiesForTest: add optional animatedEntityIds parameter,
  mirror the new partition logic

Two new tests cover:
- EntitySet_IndoorOnly_IncludesAnimatedEntitiesEvenWithNullParentCellId
- EntitySet_OutdoorOnly_ExcludesAnimatedEntities

Known remaining limitation: dropped items / static-but-live objects
have ParentCellId == null AND are NOT in animatedEntityIds, so they
still classify as outdoor scenery and stencil-gate. Addressing this
requires a "live entity" flag on WorldEntity — deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 08:55:45 +02:00
Erik
dcf69a1feb feat(render): Phase A8 — WbDrawDispatcher.EntitySet partition
Adds EntitySet { All, IndoorOnly, OutdoorOnly } and a Draw parameter to
partition the per-entity walk by ParentCellId presence. EntitySet.All
preserves pre-A8 behavior; IndoorOnly drops null-ParentCellId entities;
OutdoorOnly drops ParentCellId.HasValue entities. The visibleCellIds
filter is still applied on top.

Used by Task 7 to split the render frame's single Draw call into two
(indoor stencil-OFF, outdoor stencil-gated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 08:20:45 +02:00
Erik
6577c0a21c feat(render): Phase A8 — RenderingDiagnostics.ProbeVisibilityEnabled
Adds the ACDREAM_PROBE_VIS=1 env-var-toggleable flag for the indoor-cell
visibility culling pipeline (#78). Mirrors the existing ProbeIndoor*
pattern. DebugVM checkbox follows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 07:47:56 +02:00
Erik
a64e6f20da refactor: #100 — remove hiddenTerrainCells / BuildingTerrainCells plumbing
Retired in favour of Task 1's retail-faithful terrain shader Z nudge.
Pure removal — ~50 LOC of dead surface area across:

  - src/AcDream.Core/Terrain/LandblockMesh.cs (drop parameter +
    cell-collapse block)
  - src/AcDream.Core/World/LoadedLandblock.cs (drop field)
  - src/AcDream.Core/World/LandblockLoader.cs (drop method + call)
  - src/AcDream.App/Rendering/GameWindow.cs (3 sites)
  - src/AcDream.App/Streaming/GpuWorldState.cs (6 ctor sites)
  - src/AcDream.App/Streaming/LandblockStreamer.cs (1 ctor site)
  - tests/AcDream.Core.Tests/World/LandblockLoaderTests.cs (drop test)
  - tests/AcDream.Core.Tests/Terrain/LandblockMeshTests.cs (drop test)

No retail anchor — the deleted mechanism never had one; this commit
rolls our code back to the actual retail behaviour established in
the prior commit's shader nudge.

ISSUES.md #100 moved to Recently closed.

Cross-ref:
  docs/research/2026-05-25-issue-100-terrain-cutout-handoff.md
  docs/superpowers/plans/2026-05-25-issue-100-terrain-cutout.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 21:37:53 +02:00
Erik
6ca872feba docs(test): #101 — sync stale GameWindow.cs line ref in test class doc
Task 2's 11-line insertion shifted the synthesis gate from line
6116 to 6127. The implementation XML doc was updated in the same
commit; the parallel test-class-level reference was missed.
Code-review minor finding; one-character fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:56:35 +02:00
Erik
f6305b1e3c feat(physics): #101 — add IsPhantomGfxObjSource predicate
Retail's CPartArray::InitParts emits collision shapes only from
Setup-level CylSpheres/Spheres or per-Part PhysicsBSP — never
from visual mesh AABBs. The predicate captures the retail rule:
a stab whose source is a GfxObj (high byte 0x01) with no cached
GfxObjPhysics is phantom (no collision). Wired into GameWindow's
mesh-aabb-fallback synthesis in the next commit.

Refs docs/research/2026-05-25-a6-stairs-cyl-retail-investigation.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:45:18 +02:00
Erik
888272aad1 fix(phys): A6.P7 — retail-binary cyl-vs-BSP dispatch (HAS_PHYSICS_BSP_PS gate)
Closes the door-cyl phantom slide where a sphere approaching a closed
cottage door at NE/SE headings could be blocked by the cyl's radial
normal contaminating the slide tangent into the slab face (live
evidence in door-a6p6-v2.utf8.log: 12 resolves with
cn=(0.86,0.51,0) attributed to door entity 0x000F4245).

Retail anchor: CPhysicsObj::FindObjCollisions at
acclient_2013_pseudo_c.txt:276861 dispatches BINARILY between
BSP-only and cyl+sphere based on HAS_PHYSICS_BSP_PS (0x10000 in
acclient.h:2833). For non-PvP, non-missile movers — every M1.5
scope walking-vs-static scenario — an entity with the flag set
tests its BSP exclusively; the foot cyl is never tested. ACE
confirms the truth table at PhysicsObj.cs:412-450 (HasPhysicsBSP,
missileIgnore, exemption).

Our dispatcher iterated every ShadowEntry independently and tested
both the cyl AND the BSP for a closed door. Cyl was registered
first (FromSetup walk order), and its diagonal radial slide normal
"won" attribution at the early-return on first non-OK. Result was
out=in for tangential motion along the door face.

Changes (~15 LOC + 7 unit tests):
- PhysicsStateFlags.HasPhysicsBsp = 0x00010000 (PhysicsBody.cs)
- Transition.BspOnlyDispatch(uint state) static predicate
  (TransitionTypes.cs) — mirrors retail's branch with M1.5 scope
  defaults (ebp_1 and eax_12 treated as false; wire PvP / missile
  refinements when those scopes ship)
- Per-entry guard in FindObjCollisions cyl/sphere branch
  (TransitionTypes.cs:2433) — continue when BspOnlyDispatch fires,
  with [cyl-skip-bsp] diagnostic line gated on ProbeBuildingEnabled
- A6P7DispatchRulesTests (7 tests, all GREEN): flag value + 6
  parameterized predicate cases

Verification: 14-test keep-green list from the 2026-05-25 handoff
passes (5 BSPQueryTests.FindCollisions_Path5_*, 2 CellTransitTests.A6P5_*,
2 DoorCollisionApparatusTests.Apparatus_DeadCenter_*,
5 DoorBugTrajectoryReplayTests, 1
CellarUpTrajectoryReplayTests.LiveCompare_FirstCap_FixClosesCottageFloorCap).
Total: 20/20 pass including the new 7-test predicate suite.

The DocumentsBug test (Apparatus_Grounded_50cmOffCenter) fails
post-fix BUT was already failing pre-fix in the worktree baseline
(verified by stashing the fix and re-running — same failure mode:
sphere blocks at start with floor normal (0,0,1)). Not in the
keep-green list, so this is a known pre-existing condition; the
test's own header comment instructs flipping the assertion when
the fix lands.

Investigation:
docs/research/2026-05-25-a6-door-cyl-retail-dispatch-investigation.md

Needs visual verification at Holtburg cottage door (NE/SE approach
should now slide smoothly along the door face — zero [cyl-test]
log lines attributed to door entity, replaced by [cyl-skip-bsp]).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:35:32 +02:00
Erik
2a890e6bde test(phys): A6.P5 RED — BFS from indoor cell doesn't reach door outdoor cell
Adds CellTransitTests with two A6P5_* unit tests:
  A6P5_BuildCellSetFromIndoorStart_ReachesDoorOutdoorCell  (RED — the bug)
  A6P5_BuildCellSetFromAlcove_AlsoReachesDoorOutdoorCell   (passes today)

The RED test reproduces the over-penetration tick's cellSet build:
sphere at (132.594, 16.350) in cell 0xA9B4013F, BFS portal-walks to
0xA9B40150 (alcove) but does NOT add the door's outdoor cell 0xA9B40029.
Pre-fix cellSet: 0xA9B4013F, 0xA9B40150, 0xA9B4014C — no outdoor cells.
Sphere wasn't straddling 0xA9B40150's exit portal so exitOutside stayed
false.

Also removes the 3 A6P5_* replay tests added to
DoorBugTrajectoryReplayTests in the previous commit (3253d84's
follow-up). Those tests didn't reproduce the bug — the harness's
BuildFaithfulDoorEngine has no cell fixtures, so cellSet returned empty
and GetNearbyObjects treated it as "no filter" → door always visible
→ over-penetration test trivially passed for the wrong reason. The
CellTransitTests version pins the bug at the BFS layer directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:51:33 +02:00
Erik
82781c272b test(phys): A6.P5 fixture — 3 ticks from live door-stuck capture
over-penetration-capture.jsonl is 3 records extracted from
door-stuck-capture.jsonl: the cell-crossing over-penetration tick
(0xA9B4013F -> 0xA9B40150, sphere committed 0.27m INTO slab), a
stuck-position hit=yes tick, and a stuck-position hit=no tick. Drives
the A6.P5 replay tests that prove the cellSet gate removal closes both
the over-penetration and the intermittent-visibility bugs.

extract-records.ps1 is a one-shot extractor; reusable if we capture more.
Source captures (door-stuck-*.jsonl, door-stuck-*.launch.log) gitignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:43:51 +02:00
Erik
3253d841ac fix(phys): A6.P4 door — pos_hits_sphere records near-miss polygon
Retail's CPolygon::pos_hits_sphere at
acclient_2013_pseudo_c.txt:322974-322993 records the polygon pointer
(*arg5 = this at line 00539509) on STATIC overlap BEFORE the front-
face cull (dot(N, movement) >= 0 return 0 at line 0053952f). So when
a sphere statically overlaps a wall but is moving parallel/away from
the wall normal, retail returns 0 (no full hit) but the polygon
pointer IS set so Path 5's set_neg_poly_hit dispatch at
acclient_2013_pseudo_c.txt:0053a6ea fires and the outer
transitional_insert loop slides the sphere along the wall.

Pre-fix our PosHitsSphere set hitPoly only when both the static-
overlap AND the front-face cull passed. Near-miss polygons were
dropped → Path 5's `if (hitPoly0 is not null)` branch never fired →
NegPolyHit stayed false → outer loop never slid → inside-out cottage
doors let spheres squeeze through walls they were touching.

The handoff (docs/research/2026-05-25-door-bug-cdb-retail-trace-findings.md)
hypothesized swept-sphere intersection + closest-considered-polygon
tracking. Reading the actual retail decomp of pos_hits_sphere AND
polygon_hits_sphere_slow_but_sure (acclient_2013_pseudo_c.txt:322504-
322635) showed both functions are STATIC tests; the motion vector is
used only for the front-face cull. The fix is a one-line reordering.

Adds 3 unit tests in BSPQueryTests covering:
  - Sphere overlaps wall + moves parallel → NegPolyHit fires (RED→GREEN)
  - Sphere overlaps wall + moves away    → NegPolyHit fires (RED→GREEN)
  - Sphere overlaps wall + moves into    → Slid (regression guard, already
                                            passed)

Verification:
  * 3 new Path 5 tests pass.
  * Full Core suite: 14 failures with-fix vs 17 failures baseline-no-fix.
    The with-fix failure set is a STRICT SUBSET of baseline — zero
    regressions. The 14 remaining failures are pre-existing static-leak
    flakiness between test classes (documented in CLAUDE.md) and 2 stale-
    capture LiveCompare_* document-the-bug tests.
  * All handoff "must-stay-green" tests pass:
    - Directional_OutsideIn_SouthApproach_BlocksAtSlabSouthFace
    - Directional_InsideOut_NorthApproach_BlocksAtSlabNorthFace
    - CornerSlide_AlcoveEastToCottageNorth_ShouldBlock
    - Geometric_DoorSlabAtSphereHeight_OverlapsInZ
    - CellarUpTrajectoryReplayTests.LiveCompare_FirstCap_FixClosesCottageFloorCap
      (issue #98 CRITICAL — no regression).

Per CLAUDE.md: needs visual verification at Holtburg cottage door
inside-out off-center (~50 cm) scenario before the A6.P4 phase is
marked complete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:05:04 +02:00
Erik
a657ca946c test(phys): A6.P4 door — corner-slide hypothesis falsified, bug is state-related
CornerSlide_AlcoveEastToCottageNorth_ShouldBlock test:
- Registers cottage GfxObj 0x01000A2B (contains north exterior walls)
- Registers cell 0xA9B40150 BSP via dat-direct load (alcove walls)
- Places sphere at (132.95, 16.8, 94) inside alcove near east wall
- Walks sphere +Y 50 times at walk speed (0.05 m/tick)

Result: sphere STAYS at (132.95, 16.8) for all 50 ticks with collision
normal cn=(0.71, -0.71, 0) — the average of alcove east wall normal
and cottage north wall normal at their meeting corner. The corner
handling works correctly in the harness.

So production's inside-out walkthrough is NOT a geometric or BSP
collision-detection bug. The geometry exists, the collision detection
fires symmetrically at corners. The discrepancy must be a STATE
difference between harness and production:
- Real walkable polygons with edges (harness uses big quad)
- Real terrain (harness uses Z=-1000 stub)
- Accumulated body state across many prior ticks (harness uses fresh)
- Possibly cell ping-pong between 0x0150 and 0x0029 in production

Cottage GfxObj wall polygons at the doorway area confirmed:
- North exterior wall east of doorway: polys 0x0032, 0x0033
  X=[133.5, 136.3], Y=17.10, Z=[94, 97], normal +Y
- North exterior wall west of doorway: polys 0x0030, 0x0031, 0x0034,
  0x0035 (X<131.6 various ranges)
- Lintel polys above doorway: 0x0037, 0x0038, 0x003A, 0x003B at Z>96.5

Next-session moves (per handoff):
1. Replay captured tick 2586 (where sphere went from cell 0x0150 to
   0x0029 at X=134.022, way past alcove east wall). Inspect engine
   behavior at exactly that tick's body state.
2. cdb attach to retail at Holtburg cottage doorway — verify whether
   retail also lets sphere walk through at off-center, OR blocks
   cleanly. If retail also allows walkthrough, this might be
   retail-faithful behavior we should accept.

Updated handoff: docs/research/2026-05-25-door-bug-inside-out-geometry-gap.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:37:31 +02:00
Erik
fe29db5691 test(phys): A6.P4 door inside-out — locate cottage wall, identify corner-slide hypothesis
Followed up the geometry-gap diagnosis with a wider polygon search.
Result: the cottage's north exterior wall east of doorway DOES exist
in cottage GfxObj 0x01000A2B (polys 0x0032, 0x0033) at
world X=[133.5, 136.3], Y=17.10, Z=[94, 97], normal +Y. Symmetric
polys cover the west side and above the doorway lintel.

The wall SHOULD block sphere at X=133.655 (sphere west edge at 133.175
overlaps wall X range; sphere south edge at 17.11 aligns with wall
at Y=17.10).

New hypothesis: the bug is sphere-vs-corner collision at the meeting
point of cell 0x0150's east wall (X=133.5, Y=[16.5, 17.1]) and the
cottage's north exterior wall (X=[133.5, 136.3], Y=17.10). Cell
transit data shows sphere going from X=132.859 entering alcove to
X=134.022 leaving alcove — sphere reached X=134.022 INSIDE cottage
geometry somehow. The sliding along the slab east face (cn=(+1,0,0)
in captured tick 3254) gradually pushes sphere east. Eventually it
shifts past X=133.5 — the corner where alcove east wall meets cottage
north wall. The corner-handling in our BSP collision may incorrectly
let the sphere slide past, or the alcove cell's east wall and cottage
GfxObj's north wall don't compose correctly at the corner.

Diagnostic apparatus extensions:
- HoltburgLandblockStatics_DatInspection: dumps LandBlockInfo for
  landblock 0xA9B4. Shows 114 stabs + 12 buildings. The cottage IS
  Building[6] with modelId=0x01000A2B (the GfxObj we already loaded).
- Diagnostic_CottagePolys_NearWalkthroughPosition: widened search
  reveals the cottage's full north exterior wall geometry.
- HoltburgCottage_CellPortals_DatInspection: extended with cell
  PhysicsPolygon world-frame dump (already in prior commit).

Full updated handoff: docs/research/2026-05-25-door-bug-inside-out-geometry-gap.md

Next-session move: add a "sphere walks +Y from inside alcove at
X near 133" test. If harness slides past the corner like production,
investigate BSPQuery's sphere-vs-edge case. If harness blocks at
corner, the bug is elsewhere (cell 0x0150 BSP not queried, or
cottage GfxObj BSP traversal misses the wall poly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:34:52 +02:00