Commit graph

1617 commits

Author SHA1 Message Date
Erik
d2212cfaea fix(render): Part 1 — camera boom convergence snap (kills the at-rest viewer-cell flicker trigger)
Port retail CameraManager::UpdateCamera's convergence snap (0x00456fcd):
once the per-frame lerp step is below 0.0004 m AND the rotation within
0.000199999995, freeze the damped eye at an exact fixed point instead of
Vector3.Lerp's endless sub-mm asymptote. The drift was walking the 3rd-person
eye across the vestibule/room portal plane at rest, flipping the per-frame
viewer-cell resolve 0170<->0171 -> the indoor grey/texture flicker. The
collided-eye firewall (separate publishedEye local) is already present.

Adds ApplyConvergenceSnap static (TDD: 3 unit tests + 1 integration freeze
test) + SnapEpsilon/RotCloseEpsilon. App suite 183 -> 187, all green.

Plan: docs/superpowers/plans/2026-06-05-indoor-viewer-cell-flicker-fix.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:56:04 +02:00
Erik
9601ef39c3 docs: indoor flicker/void root cause (decomp + live cdb) + 3-part fix plan handoff
Diagnosis session: the indoor bluish void + grey/texture flicker is visibility metastability at cell boundaries, not a missing flood (R1's per-cell DrawInside is built; the cellar seals). Confirmed by named-retail decomp AND a live cdb capture of retail (viewer_cell rock-stable: clean monotonic transitions, zero oscillation across 4916 samples). Retail stays stable via boom stability + a 0.2mm viewer-cell dead-zone + clip-space portal clipping; acdream diverges on all three. Handoff documents the root cause, the cdb evidence, and the prioritized 3-part retail-faithful fix (boom stability -> dead-zone -> w-space clip) with decomp anchors + a planning/implementation kickoff prompt. Adds the reusable retail viewer-cell cdb capture script and the superseding CLAUDE.md banner.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:31:17 +02:00
Erik
9f95252d20 fix(render): flood the neighbour when the eye stands in an interior portal
When the chase camera roots in a thin doorway cell and the eye stands in an interior portal opening (live capture: vestibule->room portal D=0.16m, proj=0), the 2D projection degenerates and the neighbour was culled (cells=1) -> only the thin cell drew -> bluish void / transparent ceiling. Retail's 3D clip imposes no constraint for a portal the eye is inside, so the neighbour is fully visible. When the clipped region is empty but the eye stands in the opening (EyeInsidePortalOpening: within 0.5m of the portal plane AND point-in-opening), flood the neighbour with the current view. Guarded so an off-screen degenerate portal stays culled (no #95 blowup; over-include is mesh-frustum-culled at draw). Visual-verified: cellar ceiling now solid.

Band-aid for thin-cell-root coverage; likely superseded by the boom-stability + viewer-cell dead-zone + w=0 near-plane clip fix next session (reassess / maybe revert). 2 RED->GREEN tests; cyclic/hub termination guards unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:27:39 +02:00
Erik
5f596f2d25 fix(render): clip portal projection against frustum side planes (clip-space)
ProjectToNdc clipped only the eye half-space (w>MinW, a 2026-06-03 workaround) and left the 4 frustum side planes to the 2D ScreenPolygonClip. When the eye is within a portal's near plane, small-w verts explode under the perspective divide (probe saw NDC (10.2,-67.4)); the 2D clip then collapses to empty -> OutsideView empty -> terrain Skip -> the bluish doorway void. Clip the eye plane + 4 side planes (homogeneous Sutherland-Hodgman) before the divide so NDC is bounded to the screen by construction, matching retail GetClip -> polyClipFinish (clip in clip-space before the divide; pc:432344).

Partial: NOT the full flicker fix. The dominant cause (camera boom drift + viewer-cell flip at boundaries + missing w=0 near-plane clip) is identified and deferred to the next session per the handoff. 2 RED->GREEN tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:27:24 +02:00
Erik
02837ad5dc docs(A): wrap Render Residual A — handoff + roadmap for the core inside render
Residual A (camera collision = verbatim SmartBox::update_viewer) is SHIPPED +
user-kept (0ffc3f5/5177b54/9e70031). Wrap it and hand off to the render session:

- New canonical handoff (docs/research/2026-06-05-render-residual-a-shipped-core-
  inside-render-handoff.md): what A shipped, what A EXPOSED (the render roots at the
  viewer cell — clipRoot=CameraCell, GameWindow.cs:7322 — and A made that cell
  accurate, so the PVS flood from the viewer cell doesn't reach the player's cell →
  cellar floor drops), the reframing (the user's "step C" = the CORE inside render /
  R1 completion, NOT R2 outside-looking-in), the evidence-first job, KEEP/DON'T, the
  kickoff prompt.
- CLAUDE.md banner: A SHIPPED; next = core inside render (R1 completion).
- Render redesign spec: 2026-06-05 sync note (A shipped; R1 is actually incomplete —
  the bleed + cellar-floor drop are the unfinished flood/seal; next is R1, not R2).

The visible problems (bleed + the floor A exposed) are the same family: the inside
path still draws the whole outdoor world instead of retail's "inside → DrawInside
only". A faithful DrawInside seals them by construction (render spec 2026-06-02 §2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 11:49:31 +02:00
Erik
9e70031bc6 feat(A): wire SweepEye to the verbatim update_viewer (start-cell + fallbacks)
Complete Render Residual A's faithful port: PhysicsCameraCollisionProbe.SweepEye
now mirrors SmartBox::update_viewer (acclient_2013_pseudo_c.txt:92761) end-to-end:

- Start cell (pc:92824-92844): indoor (>=0x100) seats the sweep at the head-PIVOT
  via PhysicsEngine.AdjustPosition (the cellar-lip case — feet in the low connector,
  head up at floor level); outdoor keeps the player cell.
- Sweep pivot -> sought-eye from the seated start cell (unchanged 0x5c viewer flags).
- Success (pc:92870): set_viewer(curr_pos), viewer_cell = curr_cell.
- Fallback 1 (pc:92878): AdjustPosition(sought_eye).
- Fallback 2 / no-cell (pc:92775, 92886): snap to player, viewer_cell = null. This
  also makes cellId==0 faithful (was returning the desired eye; retail snaps to
  player_pos) and adds the playerPos arg to ICameraCollisionProbe.SweepEye.

Supporting: ResolveResult.Ok surfaces FindTransitionalPosition's return (retail
find_valid_position != 0, pc:273898) so SweepEye knows when to fall back.

TDD: 11 new tests (FindVisibleChildCell 4, AdjustPosition 3, ResolveResult.Ok 2,
SweepEye orchestration 2). The seating test's RED proved the sweep does NOT auto-
advance feet->room, so the pivot-seated start cell is genuinely decisive. Core
1326 pass / 4 documented-fail / 1 skip; App 179 pass / 0 fail. No regression.

Per the live-capture finding, the visible payoff is the cellar-corner (point 3);
the cottage-room bluish void stays for residual C. Spec:
docs/superpowers/specs/2026-06-05-residual-a-camera-collision-design.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 11:10:32 +02:00
Erik
5177b54bbe feat(A): port find_visible_child_cell + AdjustPosition (Render Residual A primitives)
The two Core physics primitives retail's SmartBox::update_viewer calls down into,
ported verbatim (TDD, 7 new tests):

- CellTransit.FindVisibleChildCell (CEnvCell::find_visible_child_cell, pc:311397):
  return the cell whose cell-BSP point_in_cell contains a world point — start cell
  first, then (stab-list mode) the start's VisibleCellIds or (portal mode) its
  direct portals. Sibling of FindCellList. Mirrors FindCellList's null-CellBSP skip
  (CellTransit.cs:518) so a cell lacking hydrated CellBSP doesn't spuriously claim
  every point via PointInsideCellBsp's null-node "inside" default.

- PhysicsEngine.AdjustPosition (CPhysicsObj::AdjustPosition, pc:280009): resolve a
  point's cell from a seed. Indoor (>=0x100) → FindVisibleChildCell(stab-list);
  outdoor → landcell snap (same grid lookup as ResolveCellId). The seen_outside
  sub-fallback is deferred (off the cottage/cellar path; spec §6).

Both are unwired into any production path — they land the machinery update_viewer's
start-cell + fallback 1 need (and that residual C also needs). The App SweepEye
orchestration that calls them lands next.

Decomp-faithful per the live-capture finding: A's V1 sweep already contains the eye
(eyeInRoot=Y 99.75%, never void); this completes A as a verbatim port. Spec:
docs/superpowers/specs/2026-06-05-residual-a-camera-collision-design.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 10:56:16 +02:00
Erik
0ffc3f5be9 docs(A): spec — verbatim SmartBox::update_viewer completion (Render Residual A)
Live ACDREAM_PROBE_FLAP capture (Holtburg cottage/cellar) proved the V1 camera
spring-arm already contains the eye (eyeInRoot=Y 99.75%, viewerCell never 0,
indoor collide 97.6% in 0174). The dominant inside-cottage bluish void is the
render-sealing residual C (DrawPortal), NOT the camera.

This spec scopes the FAITHFUL completion of Residual A: port the two missing
update_viewer pieces verbatim — the indoor start-cell seated at the pivot via
CPhysicsObj::AdjustPosition (pc:280009) → CEnvCell::find_visible_child_cell
(pc:311397), plus the two AdjustPosition/snap-to-player fallbacks — and land
FindVisibleChildCell (which residual C also needs).

Faithful layering (mirrors retail SmartBox→CPhysicsObj): primitives in Core
(PhysicsEngine.AdjustPosition + CellTransit.FindVisibleChildCell + ResolveResult.Ok),
orchestration in App PhysicsCameraCollisionProbe.SweepEye. Deterministic crux test
(start-cell resolution) in Core.Tests with the cottage fixtures; SweepEye glue in
App.Tests. Visible payoff is narrow (the cellar-corner, point 3); the cottage-room
void stays for residual C.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 10:44:04 +02:00
Erik
2c7948a9f1 docs: handoff + kickoff for Render Residual A (camera collision verbatim port)
Session wrap: cellar-lip wedge fixed + visual-verified (cc4590f/9fdf6a5/41db027).
Next task per the plan = Render Residual A: keep the chase camera eye inside the
player's cell by porting retail SmartBox::update_viewer verbatim (fixes interior
walls going grey/transparent from inside).

- New canonical handoff with copy-paste fresh-session kickoff prompt, the retail
  update_viewer decode, the V1 current-state map, the gap to pin (faithful
  start-cell + AdjustPosition fallbacks + the no-wall-hit cause), and the
  evidence-first plan ([flap-sweep] capture → deterministic SweepEye test → port).
- Key finding recorded: find_valid_position (pc:273890) just calls
  find_transitional_position — the sweep function is faithful, NOT the divergence.
- CLAUDE.md banner updated to point at the new state + handoff.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 09:46:52 +02:00
Erik
41db027f34 docs(p2): record cellar-lip wedge visual-gate PASS (cellar smooth, door blocks, step-up climbs)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 09:24:54 +02:00
Erik
9fdf6a5d01 chore(p2): strip cellar-lip dispatch-trace probes after visual confirmation
The stale-footCenter fix (cc4590f) is visually confirmed: cellar ascent is
smooth, inn door still blocks, generic step-up still climbs. The residual
9/29 (0,-1,0)-sliding-normal records did NOT manifest in live play —
confirming they were buggy-trajectory artifacts.

Remove the temporary investigation scaffolding added for this trace:
- [fc-dispatch] probe in BSPQuery.FindCollisions
- [step-sphere-down] probe in BSPQuery.StepSphereDown
- CellarLipWedgeTests.Diagnostic_TraceRecordByIndex [Theory]

Kept: the fix, the Fix_StaleFootCenter_* regression guards, and the
DocumentsResidualWedge_* documents-the-bug test. Core suite 1317 pass /
4 fail (documented baseline) / 1 skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 09:24:20 +02:00
Erik
cc4590f9e5 fix(p2): cellar-lip wedge — check_other_cells must use the LIVE sphere position
Root cause of the "blocked at the last cellar step" wedge (the primary,
ramp-climb family — 20/29 captured records). The prior session's pinned
"find_walkable is never called during the step-down" was a probe artifact:
a fresh [fc-dispatch]/[step-sphere-down] trace proves Path-3 StepSphereDown
IS reached for both the carried cell and the iterated other-cell.

The real divergence is in Transition.CheckOtherCells. Retail's
check_other_cells (acclient_2013_pseudo_c.txt:272735 → (*cell+0x88)(this))
re-collides the OTHER cells against the LIVE sphere_path.global_sphere — the
position AFTER the primary insert_into_cell ran. The primary collide can MOVE
the sphere: a Path-5 full-hit dispatches step_sphere_up, and a successful
step-up climbs the foot onto the cottage floor yet still returns OK. acdream
instead reused a footCenter snapshot captured BEFORE the primary collide, so
once the lip-riser step-up climbed the foot onto the floor, check_other_cells
still queried 0171 at the pre-climb (sunk ~0.25 m below the floor) position →
the foot spuriously near-missed the very floor it had climbed onto →
neg_step_up → a doomed second step_up vs the floor normal (0,0,1) whose
step_up_slide unwound the climb → validate_transition reverted → 0% advance.

Fix: re-read footCenter = sp.GlobalSphere[0].Origin at the top of
RunCheckOtherCellsAndAdvance (one line). Pre-fix 0/29 wedge records advanced;
post-fix 20/29 climb onto Z≈94.

No regression: full Core suite 1321 pass / 4 fail (the documented baseline:
Apparatus_Grounded_50cmOffCenter, 2× DoorBugTrajectoryReplay LiveCompare_*,
BSPStepUpTests.D4) / 1 skip. The 2 door LiveCompare divergences are
byte-identical with/without the fix (the door's step_up FAILS → sphere
restored → position unchanged → footCenter == live).

Tests: CellarLipWedgeTests.Fix_StaleFootCenter_RampRecordClimbsCottageFloor +
Fix_StaleFootCenter_MajorityOfWedgeRecordsAdvance (new, GREEN).
DocumentsResidualWedge_LiveFloorCp_SlidingNormalKillsPlusY documents the
remaining 9/29 (0,-1,0)-sliding-normal +Y-kill family (slide territory,
deferred to the visual gate).

Apparatus retained (gated on ACDREAM_PROBE_INDOOR_BSP): [fc-dispatch] in
BSPQuery.FindCollisions + [step-sphere-down] in BSPQuery.StepSphereDown +
CellarLipWedgeTests.Diagnostic_TraceRecordByIndex — strip once the residual
is resolved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 09:15:19 +02:00
Erik
bc1be26907 test(p2): faithful cellar-lip wedge reproduction + investigation apparatus (no fix yet)
P2 / M1.5 "blocked at the last step" cellar-lip wedge. This session built a faithful
deterministic reproduction and peeled the cause through six evidence-disproven framings
to one bounded question. NO fix landed — the last layers were each disproven by evidence,
and guessing at the load-bearing collision code is the saga's failure mode.

Apparatus:
- CellarLipWedgeTests.cs + Fixtures/cellar-lip/ (3 real cell dumps + wedge-records.jsonl =
  29 captured ACDREAM_CAPTURE_RESOLVE wedge calls). Replays the exact calls + body-before
  through the lip-cell engine: all 29 reproduce at 0% advance in <200 ms. Tests are
  documents-the-bug / diagnostics (GREEN while the wedge exists).
- TEMP probes ([path5-wall]/[fw-enter]/[find-walkable] in BSPQuery; [neg-poly]/[stepsphereup]/
  [stepdown-decide]/CheckOtherCells cn/sn/negHit in TransitionTypes), gated on
  ACDREAM_PROBE_INDOOR_BSP, marked STRIP. TransitionTypes neg-poly shortcut has a reverted-fix
  comment (slide attempt didn't clear the wedge).
- tools/cdb/retail-*-trace.cdb (retail cdb traces).

Findings (handoff: docs/research/2026-06-04-p2-cellar-lip-flatfloor-cp-handoff.md, see the
"NEXT-SESSION KICKOFF" at top):
- Flat-floor contact plane is retail-faithful (v1 trace, full-file correlation). NOT the bug.
- PosHitsSphere cull sign is retail-faithful (cdb -z verified; the Binary Ninja `test ah,N; jp`
  parity-jump reads inverted — caught + reverted a wrong fix from that mis-read).
- Sphere radius correct (0.48 player / 0.30 camera probe).
- Retail connector cell 0xA9B40175 never blocks (CEnvCell::find_collisions trace: 0 Collided/Slid).
- PINNED: during the step-up's step-down, BSPQuery.FindWalkableInternal is never called for cell
  0171, so the cottage floor (poly 0x0023, Z=94) is never tested as walkable -> no contact plane
  -> step-up fails -> StepUpSlide=Collided -> wedge.

Next: trace FindEnvCollisions -> FindCollisions path dispatch for 0171 during StepDown=true (why
StepSphereDown/find_walkable is skipped), port retail, validate via CellarLipWedgeTests, regress
DoorBugTrajectoryReplayTests + visual gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 08:30:36 +02:00
Erik
57435e912b docs(p2): fresh-session kickoff prompt — principled P1 membership fix (user-approved)
Appends the copy-paste kickoff prompt for the next session: pursue the principled
P1 fix for the cellar-lip cell-resolver ping-pong (demote ResolveCellId / make the
swept curr_cell the per-frame membership authority), NOT a stickiness band-aid.
Captures the evidence, apparatus, retail anchors, do-not list, and test baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:43:11 +02:00
Erik
664101f08f docs(p2): re-diagnose cellar wedge — cell-resolver ping-pong, not step-up
Instrumented acdream at the cellar lip (ACDREAM_DUMP_STEPUP=1): step-up WORKS
(518 attempts, 220 SUCCESS landing the candidate on the cottage floor Z=94.0,
matching retail's landings), but the committed CurPos never advances -- every
success is reverted, and [cell-transit] shows ResolveCellId ping-ponging every
tick at the 3-cell junction (0xA9B40175<->0174<->0171, reason=resolver). So the
wedge is a MEMBERSHIP cell-resolution instability reverting a working step-up --
NOT a collision/step-up bug, NOT edge-slide.

Notably this contradicts the master-plan P1 claim that ResolveCellId was demoted
out of the per-frame path: it is STILL driving per-frame cell changes here and is
unstable. Fix direction = the parked, approval-gated (a) ResolveCellId
demotion/stickiness (membership), now justified as a real bug by live evidence.
Collision-side fixes (abbd761 B1, 0935a31 slide_sphere) are correct + kept.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:39:21 +02:00
Erik
5ad897b0a5 docs(p2): cellar corner-wedge pinned to step-up-onto-floor (retail cdb) + trace apparatus
Live retail cdb trace (tools/cdb/cellar-corner-escape.cdb) of the Holtburg
cottage cellar-top corner decodes the ground truth: retail escapes by
step_sphere_up->step_up (196x vs 38 near-misses), transitioning the contact
plane from the ramp (N.z=0.78) onto the flat cottage floor (N.z=1.0, 76
landings). acdream slides at the lip and never makes that ramp->floor
transition -> the intermittent cellar wedge.

So the remaining cellar bug is the #98-core step-up-onto-cottage-floor
(DoStepDown / step_sphere_down / find_walkable), which the shipped B1 (abbd761)
+ slide_sphere (0935a31) fixes got close to but didn't finish. Door still
blocks; generic step-up climbs; cellar went always-stuck -> works-mostly.

Next (handoff doc): instrument acdream's OWN corner path (does step_up fire at
the lip and fail to land on the cottage floor?) before porting the lip-climb --
no guessing (#98 saga rule).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:27:44 +02:00
Erik
0935a315bf fix(p2): port retail slide_sphere for head near-miss (cellar wall-slide)
Replace the A6.P4 Collided shortcut in transitional_insert's neg_step_up==0
branch with the faithful CSphere::slide_sphere. Retail
(acclient_2013_pseudo_c.txt:273350-273351, CTransition::transitional_insert):
neg_step_up==0 (HEAD-sphere near-miss) -> slide_sphere -> continue the insert
loop; neg_step_up==1 (foot) -> step_up_slide. The acdream foot branch already
did that; only the head branch took the shortcut (SetCollisionNormal + return
Collided = dead hard-stop). The slide itself is the existing
SlideSphereInternal (Sphere.SlideSphere port): it strips the into-wall
component and keeps the tangential crease (collisionNormal x contactPlane.N).

Surfaced by the B1 near-miss-gate fix (abbd761): once the grounded mover climbs
onto the cottage floor, its head sphere brushes the cellar stairwell walls and
the old hard-stop wedged it (2026-06-04 live capture: 274 (0,-1,0) + 78
(1,0,0) hits, out==current, dead oscillation). Post-fix capture shows 96
hit-and-advanced frames (the body slides along the walls).

Visual-verified 2026-06-04: closed cottage door still BLOCKS (no walkthrough --
drifts sideways along it, retail-faithful); cellar ascent now works (was always
stuck). An intermittent corner-wedge (slide into the -Y/+X wall corner) remains
-- separate finer issue, under investigation.

Core 1310 pass / 4 fail (pre-existing: 3 door documents-the-bug + D4 airborne).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 10:28:59 +02:00
Erik
f984e92e37 docs(p2): correct the handoff — B1 was the Path 5 near-miss gate, not the climb
The prior localization (step-up CLIMB) was disproven by an ITestOutputHelper
capture. Records the real root cause (A6.P4 near-miss missing retail's
num_sphere>1 gate, fixed in abbd761), that the door blocks faithfully with a
real floor, and that the remaining red tests are separate (apparatus
synthetic-floor artifact, LiveCompare buggy-captures, D4 airborne) — not
simple "flip to green" targets. Next is the user visual gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:35:14 +02:00
Erik
abbd7615ee fix(p2): Path 5 near-miss = retail num_sphere>1 gate (fixes B1 step-up wedge)
The 2026-06-03 handoff localized the failing Core tests to the BSP Path 5
step-up CLIMB (find_walkable/step_sphere_down). An ITestOutputHelper capture
of B1 disproved that: the climb code is correct (matches ACE
Polygon.adjust_sphere_to_plane / BSPTree.step_sphere_down exactly). The real
bug is the A6.P4 near-miss dispatch in FindCollisions' Path 5 (Contact
branch), which diverged from retail three ways:

  1. Recorded a near-miss NegPolyHit UNCONDITIONALLY. Retail gates both
     set_neg_poly_hit calls behind `if (num_sphere > 1)`
     (acclient_2013_pseudo_c.txt:323852).
  2. Checked the foot sphere's near-miss before the head's. Retail checks
     the head (sphere1) first.
  3. Mapped foot->neg_step_up=false / head->true. Retail maps head(index 0)
     ->false (slide), foot(index 1)->true (step-up), per
     SPHEREPATH::set_neg_poly_hit (:323279, neg_step_up = arg2).

For B1's single foot sphere, the spurious near-miss -> outer loop
`!NegStepUp -> SetCollisionNormal + Collided` -> revert: the grounded mover
wedged at x=0.1 and never advanced to the wall to step up. With the verbatim
gate, a single-sphere near-miss records nothing, the sphere advances,
full-hits the wall, and step_sphere_up climbs the 0.25 m step (verified via
probe capture: foot ends at (0.6, 0, 0.25)).

The Holtburg cottage door still blocks faithfully (door slab (0,-1,0) normal,
stops in front of the door) when the scenario has a real floor — confirmed
this change does not regress the door.

The two BSPQueryTests Path5 near-miss tests used a single sphere (the very
non-retail assumption that caused this wedge); converted to the production
2-sphere shape where the head sphere records the near-miss, matching retail.

Core 1312 pass / 4 fail (the 4 pre-existing: 3 door documents-the-bug + D4
airborne, none regressed here); App 177 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:32:55 +02:00
Erik
82045805fd docs(p2): session wrap — P1 done, P2 (Path 5 step-up) localized; handoff + plan/CLAUDE.md update
P1 membership is DONE (proven to already match retail; the 0/11 was a cdb capture artifact;
merged + pushed). P2 root cause localized to BSP Path 5 grounded step-up: the Path 5 wrappers
(DoStepUp=retail step_up, DoStepDown=retail step_down) are verified faithful + reached; the
divergence is in the step-up CLIMB (find_walkable/step_sphere_down up-adjust when sp.StepUp=true).

- docs/research/2026-06-03-p2-door-stepup-handoff.md: canonical P2 pickup + fresh-session prompt +
  DO-NOT-RETRY (the wrappers) + the tooling note (xunit swallows Console.WriteLine).
- master-plan §3: P1 marked DONE + the (a)-(d) deletes/unifications re-scoped to approval-gated
  refactors of working code; P2 localization recorded.
- CLAUDE.md M1.5: dated 2026-06-03 pointer (P1 done, P2 active, render seam in P3/P4, pickup doc).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 08:52:56 +02:00
Erik
f0d37d8955 docs(p1): visual-gate result — membership confirmed live; flap+void are render residuals
Some checks failed
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
Standing/pacing the Agent of Arcanum doorway in the acdream client with ACDREAM_PROBE_CELL=1:
the player [cell-transit] sequence is clean + monotonic and crosses at the same Y thresholds as
retail's aligned golden, firing only on character movement (never camera-only). So P1 membership
is correct LIVE, matching the conformance proof.

The visible flap + purple void are the RENDER half, not membership: the flap is the camera-collision
residual (chase eye drifts out of the player cell -> viewer-cell flips; master-plan P3,
SmartBox::update_viewer), the void is the unported PView seal (master-plan P4). The user's intuition
"transition feels tied to the camera" is retail-faithful: retail keys the render on the viewer
(camera) cell, physics+lighting on the player cell.

Per user direction, P2 door collision is next; the render half (P3 camera -> P4 PView) follows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 21:30:02 +02:00
Erik
9017107960 fix(p1): membership already matches retail — the 0/11 was a cdb capture artifact
The P1 "doorway membership lags retail" premise is FALSIFIED. acdream's swept
ResolveWithTransition already matches retail's true per-frame curr_cell: the
production gate ProductionPath_IndoorCrossings reads 9/9 on the indoor 0170<->0171
crossings with NO code change, once fed an aligned retail golden.

Root cause of the false 0/11: CPhysicsObj::SetPositionInternal calls change_cell
(acclient_2013_pseudo_c.txt:283456) BEFORE set_frame writes m_position (:283458),
so the original golden (find-cell-list-capture.cdb, read at the change_cell BP)
paired each frame's NEW cell with the PREVIOUS frame's position — a one-frame skew.
Verified 3 ways: the decomp ordering; golden_picked[i] == geom(golden_position[i+1])
for all 22 rows; acdream's static pick == golden_picked[i-1] for all rows. Both
retail and acdream pick with center-only point_in_cell on global_sphere[0] (no XY
lead; cache_global_sphere @ pc:274196). curr_cell commits via validate_transition
(@ pc:272608, curr_cell = check_cell) = the find_cell_list pick, structurally
identical to acdream's RunCheckOtherCellsAndAdvance -> FindCellSet -> SetCheckPos.
There was nothing to port; a swept advance would make membership LEAD by a frame.

- tools/cdb/find-cell-list-capture-aligned.cdb: re-capture reads the committed
  position from the set_frame that follows change_cell (cell+position same instant).
- Fixtures/find-cell-list-threshold.log: replaced with the aligned capture.
- ThresholdPortalCrossingReplayTests / FindCellListConformanceTests: rewritten from
  documents-the-bug to assert retail truth (per-segment / per-indoor-pick equality).
- handoff + notes + README + memory: banners correcting the disproven premise.

Still open (NOT indoor membership, which is DONE): outdoor->indoor 0031<->0170 entry
conformance (needs landcell + building stab in the gate cache); master-plan cleanups
(delete CheckBuildingTransit, unify find_env_collisions, demote ResolveCellId) refactor
working retail-faithful code -> need explicit user approval.

Conformance 60 pass / 1 skip / 0 fail; full Core 1309 pass / 5 fail (pre-existing
2 BSPStepUp + 3 door-collision = P2) / 1 skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:54:27 +02:00
Erik
db94cb1c90 docs(p1): canonical pickup handoff — swept curr_cell advance is the fix
Single pickup document for the next session: state both altitudes, the 8
session commits, the confirmed finding (production membership diverges 0/11 —
swept move completes but curr_cell never advances across the portal), the
DO-NOT-RETRY list (3 falsified hypotheses), the apparatus inventory + run
commands, the P1 fix scope (two decomp questions + the acdream code map + the
RED gate), the test baseline, and a copy-paste pickup prompt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:00:49 +02:00
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
46a86d282e docs(p0): CORRECTION — retail pick is center-only point_in_cell; the bare-FindCellList divergence is a probe artifact
Read CEnvCell::point_in_cell @ 0x52c300 -> CCellStruct::point_in_cell @ 0x5338f0
-> BSPTREE::point_inside_cell_bsp: the find_cell_list PICK (pc:308810) is
CENTER-ONLY, at global_sphere[0].center (the swept sphere center), NOT
radius-aware and NOT the foot origin. So acdream's PointInsideCellBsp pick
criterion ALREADY matches retail. The architect's 'use SphereIntersectsCellBsp
in the pick' hypothesis is FALSIFIED. The P0 FindCellList_DoorwayThreshold probe
fed the foot origin (captured m_position) through no sweep -> its 'all 22
diverge' is a PROBE ARTIFACT, not a confirmed production divergence (the data's
own tell: retail commits the cell AHEAD of motion while the foot is behind = the
swept sphere center crossing the portal).

P1's decisive first step is the PRODUCTION-PATH trajectory conformance (replay
the golden through ResolveWithTransition, which uses sp.GlobalSphere + the sweep)
BEFORE designing any fix. Do not port a portal-crossing/radius pick on the probe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:25:45 +02:00
Erik
81ea3aa41a docs(p0): P1 design nuances — acdream already has FindTransitCellsSphere; test the production ResolveWithTransition path
From reading CEnvCell::find_transit_cells @ pc:309968: P1 is mostly REWIRING
curr_cell advancement (RunCheckOtherCellsAndAdvance/SetCheckPos) to use the
portal-crossing candidate, not FindCellSet's point-in-cell pick. The P1
conformance must replay the golden positions through ResolveWithTransition (a
trajectory incl. outdoor landcell 0031), not bare FindCellList.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:09:46 +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
a859116d5f docs(spatial): master plan — VERBATIM port of the retail spatial pipeline (no hybrids)
The doorway saga (void -> transparent walls -> flaps) proved patching the hybrid is hopeless:
retail does membership + collision + camera + render as ONE coupled pipeline; acdream
reimplemented pieces with mismatched criteria at the seams. Master plan to port ALL of it
verbatim: A membership (find_cell_list/find_transit_cells/find_building_transit_cells intrinsic,
no bridge), B uniform collision (no indoor/outdoor fork) + door collision, C camera
(update_viewer + find_visible_child_cell), D the full PView render (ConstructView/InitCell/
ClipPortals/GetClip/DrawCells/DrawPortal + the update_count watermark). KEEP/REPLACE/DELETE
lists, decomp anchors per function, P0-P6 sequence (apparatus-first, foundation-up, visual gate
each), and the kickoff prompt. Supersedes the render-only redesign's scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:57:25 +02:00
Erik
0cc561c4d0 fix(render): doorway void — portal near-clip was near-dependent (eye-clip instead)
The cottage doorway 'void' (dark cell + floating entities while the chase camera looks
through the opening): PortalProjection.ProjectToNdc clipped portals on w+z>=0 — the GL
[-1,1] near-plane test — but acdream's camera builds its projection with D3D-convention
Matrix4x4.CreatePerspectiveFieldOfView and a 1.0 m near plane (RetailChaseCamera). Against
that matrix w+z>=0 discards everything within ~0.5 m of the eye, so when the camera orbits
to ~0.1 m from a doorway portal the near edge is clipped, the far edge projects off-screen
([flap] showed p->0171 D=0.10 proj=4 clip=0 ndc Y=-3.5..-6.6), the room behind is culled
(vis=1) and only the tiny vestibule shell draws -> dark void. Rotating away moved the eye
off the portal -> vis=5 -> room rendered.

Fix: clip against the EYE (w > MinW, MinW=0.05 m), near-INDEPENDENT — a portal you're
standing in still projects (covers the screen) so the cell behind stays visible. We only
use the projected x/y for the visibility clip region, so keeping vertices in front of the
near plane is correct. Matches retail PView::GetClip near-clipping the portal before project.
RED->GREEN regression test (doorway 0.1 m from a near=1.0 eye); 177 App tests green; the
existing straddling ±50 bound still holds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:42:46 +02:00
Erik
1e9a9cab8c feat(render): V1 — render keys on the viewer cell+eye; lighting stays on the player
Phase W single-viewpoint V1 (un-split). The render mode decision, indoor root, and portal
side-test now key on the collided-camera viewer cell + eye (RetailChaseCamera.ViewerCellId +
camPos) — retail RenderNormalMode -> DrawInside(viewer_cell) @92675; InitCell side-test vs
viewer.viewpoint @432991. Lighting / seen_outside / playerInsideCell stay on the PLAYER cell
(CurrCell), retail CellManager::ChangePosition @4559B0. The old per-render player-root +
eye-projection split (U.4c) is removed; the flap is avoided by the robust graph-tracked viewer
cell (no AABB, no grace). [flap-cam] probe extended with viewerCell vs playerCell. CurrCell
stays player-only (blue-hole fix intact). App 176 green; Core 1295/5 baseline (no new fails).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:40:10 +02:00
Erik
d03fe84845 feat(render): RetailChaseCamera.ViewerCellId — the swept viewer cell (retail viewer_cell)
Update() now always sets ViewerCellId: the camera-collision sweep's swept cell when collision
is on (retail viewer_cell = sphere_path.curr_cell), else the passed player cell. This is the
robust, per-frame, graph-tracked 'which cell is the camera in?' answer that V1 roots the render
on — no AABB, no grace frames (the U.4c flap source). 176 App tests green (2 new).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:34:07 +02:00
Erik
832001d289 refactor(render): SweepEye returns (Eye, ViewerCellId) — surface the swept viewer cell
The camera spring-arm sweep already resolves the collided eye's cell (ResolveResult.CellId
= sp.CurCellId = retail viewer_cell = sphere_path.curr_cell, update_viewer pc:92871).
Return it from SweepEye so the render can root on the viewer cell (Phase W single-viewpoint
V1, Task 1). Pure plumbing — behavior unchanged; callers extract .Eye. 174 App tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:32:45 +02:00
Erik
b7375c6563 docs(render): V1 implementation plan — single-viewpoint un-split (TDD)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:29:27 +02:00
Erik
b3fe54a5f4 docs(render): spec — single-viewpoint render (retail viewer, no split)
The inside/outside render currently splits viewpoints: the player cell roots
visibility + the portal side-test, the eye only projects. Retail uses ONE
viewpoint — the collided camera (viewer) — for the mode decision, indoor root,
side-test, AND projection (RenderNormalMode -> DrawInside(viewer_cell) @92675;
InitCell side-test vs viewer.viewpoint @432991; viewer_cell = sphere_path.curr_cell
@92871). The split makes the render mode follow the player while the screen comes
from the camera -> doorway-straddle void + see-through transition (user evidence
2026-06-03). Spec unifies on the viewer: V1 un-split (robust viewer cell from the
camera sweep, no AABB/grace -> no U.4c flap; lighting stays on the player cell),
V2 DrawPortal (outside-looking-in), V3 floor seal. Supersedes residual-A; merges
A+C. Keeps the blue-hole fix (CurrCell player-only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:24:42 +02:00
Erik
a1b49f9b24 docs: wrap session — doorway flap FIXED (membership + blue-hole); A/B/C render residuals next
Canonical handoff: docs/research/2026-06-03-membership-and-bluehole-shipped-handoff.md
(what shipped: membership Stage 1 ordered-CELLARRAY port + the blue-hole render-root
clobbering fix; the full remaining-issues list — A camera-collision, B R1b particles,
C R2 outside-looking-in, Stage 2 membership, #7 stairs, the 5-test baseline; KEEP/
DON'T-REDO; key files + decomp anchors; copy-paste pickup prompt for next session).

- ISSUES.md: recorded the cottage doorway flap DONE (both causes) in Recently closed.
- render design spec §7: R1 + flap marked DONE; A/B/C mapped to the next render phases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:09:57 +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
e5457f9552 fix(physics): Stage 1 — collide-then-pick (remove pre-pick fork) — the flap engine
The ordered pick alone did not stop the cottage doorway flap: the live [cell-transit]
log showed the cell faithfully following a position that cleanly oscillated between two
values at constant Z — the signature of a bistable membership<->collision feedback loop
(user's §4.4 #3, the forked collision).

Root cause: FindEnvCollisions RE-PICKED the cell from the TARGET position (old line 1958)
BEFORE running the primary collision, so the collision geometry (which cell BSP / terrain)
swapped the instant the pick flipped -> position shifts -> pick flips back.

Retail does NOT do this. CEnvCell::find_env_collisions (acclient_2013_pseudo_c.txt:309573)
collides against the cell it was called ON (sphere_path.check_cell, the carried seed) and
picks the NEW containing cell AFTER, in CTransition::check_other_cells (272717->272761:
check_cell = var_4c). Collide-then-pick.

This commit ports that order:
- remove the pre-pick (production); collide against the carried cell (indoor BSP block /
  terrain block unchanged);
- new shared RunCheckOtherCellsAndAdvance() runs the ordered FindCellSet pick +
  multi-valued CheckOtherCells + the carried-cell advance AFTER the primary collision, for
  BOTH indoor and outdoor seeds;
- the outdoor-seed post-step replaces the removed pre-pick's outdoor->indoor re-entry
  promotion (CheckBuildingTransit interior cell + its wall collision on the entry frame).

Cache-null unit-test fallback (ResolveCellId) kept. Full Core suite: 1293 pass / 5 fail =
the documented §10 baseline exactly (2 step-up + 3 door-collision), zero new breakage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 09:36:19 +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
bc56545634 refactor(physics): Stage 1 — widen cell-candidate helpers to ICollection<uint>
Non-behavioral: lets BuildCellSetAndPickContaining pass an ordered CellArray (next
commit) while existing HashSet-passing test callers compile unchanged. HashSet<uint>
and CellArray both implement ICollection<uint>. Core builds; 9 helper tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:56:07 +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
1438d73a43 docs(physics): handoff reframe — membership is STATE not recomputation (user analysis)
User's own decomp dig (verified): the flap's deepest root is architectural, not the
find_cell_list pick ordering. Retail membership is persistent object STATE (curr_cell
mutated ONLY by change_cell at a portal crossing); acdream RE-DERIVES CellId from
FindCellSet geometry every tick → ping-pong. Plus multi-valued CELLARRAY (retail) vs
single CellId (acdream), uniform vs forked collision (0x0100), intrinsic vs bridge
building entry. Reframed the handoff + prompt: the pick-ordering port (§4.3) is
SUPERSEDED/symptomatic; the job is STAGE 1 = persistent + multi-valued + portal-
crossing membership (change_cell 281192, find_transit_cells, SetPositionInternal),
drop the 5ca2f44 pre-check; STAGE 2 = uniform collision + intrinsic entry. New §4.4
(the 4-point analysis) + §4.5 (staged fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:20:55 +02:00
Erik
298b3b92b8 docs(physics): handoff — verbatim find_cell_list port (the R1 membership flap fix)
Canonical pickup for a fresh session. R1 (per-cell DrawInside render) shipped + is
correct (cellar seals); it exposed a pre-existing cell-membership ping-pong (the
flap). Root cause: CellTransit.BuildCellSetAndPickContaining picks from an UNORDERED
HashSet, dropping retail find_cell_list's current-cell-first ordering (CELLARRAY
index-0 + interior-wins-break, pc:308742-308825). Next job: verbatim port of that
ordered pick, replacing the HashSet + the 5ca2f44 pre-check approximation. User
authorized breaking any physics to get membership faithful. Full diagnosis, verbatim
retail source, fix plan, KEEP/don't-redo, test baseline, and a copy-paste pickup
prompt in the doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 22:24:22 +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
58822fed96 fix(render): R1 — repurpose the ParentCellId==null cell-gate bypass (#78)
EntityPassesVisibleCellGate no longer returns true unconditionally for outdoor
scenery under a cell filter (was the headline #78 bleed). Outdoor scenery now
draws only via the unfiltered bucket (visibleCellIds: null) + ResolveEntitySlot's
OutsideView routing. The outdoor-root global Draw passes visibleCellIds: null
(no portal-cell scoping outdoors; retires VisibleCellIds as a render gate — peering
into buildings is R5). Updated the EntityClipTests case that pinned the old bypass
(Included -> Excluded). 174/174 App tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:10:26 +02:00
Erik
c4fd71149a feat(render): R1 — binary render decision, indoor = per-cell DrawInside only
GameWindow.OnRender: when clipRoot != null, run only InteriorRenderer.DrawInside
(per-cell shells + per-cell objects + live-dynamics); the global entity pass +
global shell pass are no longer issued indoors. Outdoor scenery drawn clipped to
the doorway (after terrain, before the Z-clear). Outdoor root path unchanged.
pvFrame hoisted so the splice reads OrderedVisibleCells; per-frame 3-bucket
partition built on the indoor root. Retail RenderNormalMode @ 0x453aa0.

InteriorRenderer amended with a DrawableCells membership filter (an IsNothingVisible
cell can be in OrderedVisibleCells but absent from CellIdToSlot — iterate for ORDER,
filter for membership; matches the old envCellShellFilter set exactly).

Build green, 174/174 App tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:01:53 +02:00
Erik
4b75c68ea3 feat(render): R1 — InteriorRenderer per-cell DrawInside loop (retail PView::DrawCells)
Per-cell flood: closest-first over OrderedVisibleCells, per cell draws the closed
shell (EnvCellRenderer.Render(pass,{cellId})) + that cell's objects, then live-
dynamics unclipped, then transparent shells. Reuses the existing dispatcher Draw
per cell (safe to call N x/frame; only diagnostic GPU-timing miscounts). Caller
owns the landscape-through-door + Z-clear. Not yet wired (Task 3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:54:42 +02:00