Commit graph

1368 commits

Author SHA1 Message Date
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
Erik
cf85ea4e17 feat(render): R1 — InteriorEntityPartition (3-bucket per-cell entity split)
Pure helper splitting a frame's entities into live-dynamic / per-cell statics /
outdoor scenery, by the same precedence as WbDrawDispatcher.ResolveEntitySlot
(serverGuid first — live entities have no ParentCellId). Feeds the per-cell
DrawInside loop. 3 unit tests, GL-free.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:53:42 +02:00
Erik
ce7404b92b docs(render): R1 implementation plan — per-cell DrawInside (TDD)
Bite-sized plan for R1 (the per-cell DrawInside core): InteriorEntityPartition
(3-bucket, TDD), InteriorRenderer per-cell loop, the binary render decision in
OnRender (indoor = DrawInside only), and the :1756 bypass repurpose. Particles
deferred to R1b. Grounded in the live code surface (exact file:line + signatures).
Ends at the R1 user visual gate (sealed Holtburg cottage, no bleed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:45:50 +02:00
Erik
7aca79f8eb docs(render): Phase R0 — lock the render-redesign design spec (brainstorm outcome)
Resolves the plan §3 open questions with the user this session:
- object/entity/particle draw = LITERAL PER-CELL LOOP (retail DrawCells),
  not a global MDI batch with per-instance clip. Fidelity > perf > blast-radius.
- sequencing = HOLISTIC: build the per-cell DrawInside directly; no intermediate
  global-pass gate-fix. First visual gate = sealed cottage interior, no bleed.
- terrain in the seal = FAITHFUL: drawn only through the exit-portal clip, never
  as a floor under the interior. Inventory's 'relax Skip' suggestion REJECTED as a
  non-retail workaround; grey-floor = a sealing bug (verify cell mesh in R1).
- WB mesh pipeline KEPT (per-cell draws from the global buffers, batched within a
  cell); two-camera invariant preserved (eye projects, player cell roots visibility).

Phases (holistic): R1 unified per-cell DrawInside (the core) -> R2 outside-looking-in
(DrawPortal) -> R3 dungeons -> R4 polish+cleanup. Each ends GREEN + a user visual gate.
Retail anchors cited throughout (RenderNormalMode 0x453aa0, DrawCells 0x5a4840, etc).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:18:59 +02:00
Erik
21bf97ed35 docs(render): REOPEN the render half — full retail-faithful redesign dossier (handoff + huge plan + 3 research docs)
The Phase W indoor seal did NOT land. The 2026-06-02 visual gate proved the interior render is fundamentally broken (#78: transparent walls, outdoor terrain + scenery entities bleeding in, grey floors, no outside-looking-in). Stage 4 (sky-through-door clip) was real but a top layer on a base that never sealed.

DECISIVE EVIDENCE (committed in the handoff): the PVS computes correctly AND the cell shells render correctly (opaque, textured, complete — the [shell] probe shows zero NOSNAP / zero missing-texture). The failure is the SEAL + three inconsistent gates — concretely the WbDrawDispatcher.cs:1756 ParentCellId==null -> return true bypass draws outdoor scenery indoors, and the indoor path draws the outdoor world then gates it instead of running ONLY DrawInside. Retail, when inside, runs ONE PView flood: visibility IS the cull; the landscape enters only through clipped exit portals + a conditional depth-only clear.

Dossier (per the user's mandate: NO shortcuts/bandaids, port from retail, redesign the whole pipeline if needed, brainstorm first):
- Master handoff (root cause + retail target + reusable-vs-redesign + apparatus + do-not-repeat + copy-paste pickup prompt).
- Huge staged redesign plan R0(brainstorm)->R1(one visibility authority, kill the bleed)->R2(indoor=DrawInside-only)->R3(the seal, DrawCells port)->R4(per-cell object/particle clip)->R5(outside-looking-in)->R6(dungeons)->R7(polish/conformance). Each ends at a user visual gate.
- 3 research docs: full retail render pipeline reference (705 lines, decomp-verified), acdream pipeline inventory + failure map, reference cross-check (WB two-pipe is the wrong model).

#78 promoted to the redesign. The 5 remaining Core test failures are pre-existing physics/collision bugs, none render-related.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 18:28:01 +02:00
Erik
b595cfbb9f fix(render): Phase W Stage 4 — scissor sky/weather mesh in Scissor mode (adversarial-review fix)
Opus adversarial review caught a real gap: the sky/weather MESH bled full-screen indoors in TerrainClipMode.Scissor (a multi-exit interior, or an OutsideView with >8 edges). The assembler only sets the binding=2 clip-plane UBO in Planes mode; in Scissor mode it leaves count==0, so sky.vert's gl_ClipDistance writes all +1 (no clip) and the mesh draws — which had NO scissor wrapper, only the no-op planes — covered the whole screen. The terrain and particle passes were already scissored; the sky/weather mesh was the one unguarded path.

Fix: scissor the WHOLE sky pre-scene + weather post-scene blocks (mesh + particles) to the OutsideView AABB when indoors. In Planes mode the scissor is a harmless over-include (the per-vertex clip planes are tighter and do the exact doorway clip); in Scissor mode it is the sole confinement, mirroring the terrain Scissor path; outdoors it is skipped (full-screen, bit-identical). Also hoisted the scissor-disable out of the particle null-check (cleaner, leak-free on the no-particle path) and corrected a stale 'weather does not write gl_ClipDistance' comment at the world-bracket close.

The single-convex-doorway case (Holtburg cottage) was already correct (Planes mode); this seals the multi-opening case. Build 0/0; App tests 171/171.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 16:57:11 +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
872dd34943 docs(render): Phase W Stage 4 — verify ceiling-sealed / terrain-viewpoint / GL-state + ParentCellId
T4.4: annotate EnvCellRenderer.RegisterCell to document that ceilings are present by
construction. PrepareCellStructMeshData iterates ALL CellStruct.Polygons (floor + walls +
ceiling) with no surface filter; retail PView::DrawCells draws the same closed-box
drawing_bsp. No ceiling filtering confirmed.

T4.2: annotate TerrainModernRenderer.Draw to document that terrain projects from the
passed-in ICamera (uView + uProjection derive from the same camera as all other
renderers). No separate landscape viewpoint exists that could desync from the eye.

T4.5, T5.1, T5.2: pure verification — no code changes (see report).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 16:24:23 +02:00
Erik
a8b831c23b test(render): Phase W Stage 4/5 — assembler OutsideView AABB + PView BFS + entity-clip tests
ClipFrameAssemblerTests (3 new):
- Assemble_OutsideViewWithExitPortal_HasOutsideViewTrue_AabbMatchesBounds
- Assemble_OutsideViewMultiPolygon_ScissorMode_HasOutsideViewTrue_AabbValid
- Assemble_EmptyOutsideView_HasOutsideViewFalse_AabbZero

PortalVisibilityBuilderTests (3 new):
- Build_ExitPortalVisible_OutsideViewNonEmpty
- Build_NoExitPortal_OutsideViewEmpty
- Build_RootCellAlwaysFirstInOrderedVisibleCells

EntityClipTests (new file, 5 tests):
- EntityClip_ParentInVisibleSet_Included
- EntityClip_ParentNotInVisibleSet_Excluded
- EntityClip_NullVisibleSet_IncludesAll
- EntityClip_NullParentCell_NullVisibleSet_Included
- EntityClip_NullParentCell_NonNullVisibleSet_Included

WbDrawDispatcher.EntityPassesVisibleCellGate changed private → internal static
(AcDream.App already has InternalsVisibleTo AcDream.App.Tests; no new seam needed).

160 → 171 tests, all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 16:21:08 +02:00
Erik
ce2edad66a feat(render): Phase W Stage 4 — sky/weather portal-clip seal (LScape through the doorway)
The sky + weather (rain cylinder) are retail's LScape — 'the outside seen through the exit portal.' Retail PView::DrawCells (pseudo_c:432709) draws LScape clipped to the OutsideView when outside_view.view_count>0, then does a conditional Z-buffer-ONLY clear (432731) before the indoor cells. acdream now does the same:

- sky.vert writes gl_ClipDistance against the SAME binding=2 TerrainClip UBO the terrain reads. The OutsideView planes are screen-space (NDC) half-spaces encoded as clip-space planes (nx,ny,0,dw); the test dot(plane,gl_Position)>=0 reduces after perspective divide to nx*ndcX+ny*ndcY+dw>=0 — projection-INDEPENDENT — so the same plane set clips the sky EXACTLY despite its separate dome projection. count==0 (outdoor) → all distances +1 → full-screen, bit-identical. Lighting/fog math untouched.

- GameWindow: relocated the sky pre-scene + weather post-scene draws to their retail LScape positions, each in a local 8-plane clip bracket so sky.vert confines them to the doorway indoors / full-screen outdoors. Added the conditional doorway depth-ONLY Z-clear (no color → no blue hole), scissored to the OutsideView AABB. drawSkyThisFrame = seen_outside policy AND (outdoor OR exit-portal-in-view) — a sealed interior with no exit portal in view draws no sky (kills the full-screen-sky interim regression). Sky pre/post particle passes (particle.vert has no gl_ClipDistance) scissored to the doorway bbox.

- ClipFrameAssembly gains HasOutsideView + OutsideViewNdcAabb (the doorway NDC AABB, computed for BOTH Planes and Scissor terrain modes — unlike TerrainScissorNdcAabb which is Scissor-only).

- The pre-login goto SkipWorldGeometry moved BELOW the sky draw so the live sky still renders during the EnterWorld handshake (clipAssembly is null/no-clip pre-login → full-screen).

Build green; App tests 160/160. Stage 4 tests + verify-annotations follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 16:15:08 +02:00
Erik
55e1b30553 docs(render): Phase W session-2 handoff — membership FIXED + render rewrite (Stage 3 done)
Canonical pickup for the next session. Membership root cause (static :1947 re-derive)
FIXED the retail way (find_cell_list interior-wins pick + swept determination, 59f3a13)
and offline-verified (doorway strobe -> one clean transition). T0 made the suite
deterministic (12 known failures, none Phase-W regressions). Stage 3 (render-root
unification) DONE (6a1fbbd->573c555). Remaining: Stage 4 (the seal: sky/landscape inside
the portal-clip bracket + conditional doorway Z-clear = no blue-hole), Stage 5 (entity/
particle clip), green-tests triage, then the single final visual verification. Render is
wire-and-fill-gaps (PView infra exists). Flags a stash discrepancy (1 of 2 stashes missing
from the shared refs/stash) for the user to check against other worktrees.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:39:41 +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
38a52a7dac feat(core): Stage 3 T3.3 — CellGraph.FindVisibleChildCell (retail find_visible_child_cell)
Port CEnvCell::find_visible_child_cell @ 0x0052dc50 (pseudo_c:311397): walk
rootId's StabList + the root itself, return the first EnvCell whose
PointInCell is true for worldPoint. Used to resolve the camera cell in
3rd-person from the physics cell graph rather than a fresh AABB reclassification.
Root is always the player cell (preserves U.4c flap fix). Returns null when
no stab-list cell or root itself contains the query point.

Confirmed (T3.3 Step 1): no production call site uses FindCameraCell for the
camera projection — the only AABB camera resolver is now deleted (T3.1).
FindVisibleChildCell is wired implicitly via Stage 4 (camera-outside-door scenario);
no GameWindow call site needed in Stage 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:37:10 +02:00
Erik
352086042e feat(render): Stage 3 T3.2 — seen_outside terrain/sky gate per CellManager::ChangePosition
Port retail CellManager::ChangePosition @ 0x004559B0 (pseudo_c:94649) landscape
policy. Three changes in GameWindow.OnRender:
1. Extract rootSeenOutside = physicsRoot?.SeenOutside ?? true after
   ComputeVisibilityFromRoot (outdoor null root → always seen_outside=true).
2. Replace IsInsideAnyCell AABB scan with seen_outside-derived predicate:
   playerInsideCell = cameraInsideCell && !rootSeenOutside.
   Semantics: sun zeroed only in sealed interior (dungeon); building interiors
   with seen_outside keep the sun (sky visible through door).
3. renderSky = !cameraInsideCell || rootSeenOutside (Stage 3 gate, interim:
   sky draws full-screen in building interiors until Stage 4 clips to doorway).
4. Weather gate updated to follow renderSky (seen_outside policy).

Retail anchors: CellManager::ChangePosition 0x004559B0 (landscape/sun policy),
SmartBox::RenderNormalMode 0x00453aa0 (sky gate per seen_outside).

NOTE: Interim regression — sky renders full-screen indoors for seen_outside
cells until Stage 4 wires OutsideView clip. Expected per EXECUTION POLICY.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:37:00 +02:00
Erik
6a1fbbd44e refactor(render): Stage 3 T3.1 — delete FindCameraCell AABB grace-frame fallback
ComputeVisibilityFromRoot(null, …) now returns null (outdoor root) instead of
calling FindCameraCell(fallbackPos). Retail CellManager::ChangePosition
(0x004559B0) reads the transition-owned curr_cell — it does NOT re-derive from
a static position. W2a guarantees CurrCell is set from the first tick, so the
AABB fallback is dead. Deleted: FindCameraCell (389–446), _lastCameraCell,
_cellSwitchGraceFrames, CellSwitchGraceFrameCount. GetVisibleCells retains a
brute-force AABB scan for test-compat; ComputeVisibility stays for the same
reason. Updated 3 null-root tests in CellVisibilityFromRootTests to assert the
new null-returns-null behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:36:47 +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
a06226f9a2 docs(render): Phase W render-rewrite plan (Stages 3-5) — grounded, per-step
Per-step subagent-driven plan for the render half: T0 test-hygiene baseline,
Stage 3 render-root unification (root at CellGraph.CurrCell + seen_outside, drop
the FindCameraCell grace-frame fallback), Stage 4 PView seal (sky/landscape inside
the portal-clip bracket + conditional doorway Z-clear = no blue-hole; EnvCellRenderer
GL_BLEND verify), Stage 5 entity/particle cell-clip. Key reframe from grounding the
plan in the actual code: the PView infra (PortalVisibilityBuilder BFS + OutsideView,
ClipFrame, EnvCellRenderer GL_BLEND fix, WbDrawDispatcher cell gate) ALREADY EXISTS and
the A8 stencil split is already gone — so the render half is wire-and-fill-gaps, not a
from-scratch port. Execution policy: no intermediate user gates, single final visual
verification, full suite green at verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:13:35 +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
ed00719cf4 docs(render): Phase W §1a — Stage-1 gate finding (deeper root: FindEnvCollisions:1947)
Stage 1 (return swept sp.CurCellId, 3e1d502) was gated and the doorway strobe
PERSISTS: [cell-transit] still flips 0170<->0031. Airtight root from code analysis:
Transition.FindEnvCollisions re-derives the cell from the STATIC origin via
engine.ResolveCellId at TransitionTypes.cs:1947 and clobbers sp.CheckCellId (:1949)
at the start of every sweep pass — a second, earlier static re-derive the four
studies missed (they targeted the late return-site). It is the sole path that can
set an indoor swept cell outdoor (the containment pick at :2075 skips outdoor cells).
:1947 is dual-purpose (jitter source AND the only indoor->outdoor exit), so Stage 2
must replace it with a directed exit-portal crossing + do_not_load prune + exitOutside
re-gate — a careful #98-area rework, not a one-line delete. Render residuals at the
gate (no interior outside-looking-in, blue-through-door, particle/NPC bleed) are all
expected Stages 3-5, not Stage-1 regressions. Stage 1 is kept (correct + necessary).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 14:26:00 +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
3e1d502101 feat(core): Phase W Stage 1 — return swept sp.CurCellId from ResolveWithTransition (retail SetPositionInternal)
Replace ResolveCellId(sp.GlobalSphere[0].Origin, ...) with SetCurrAndReturn(sp.CurCellId)
in both the OK and partial paths of ResolveWithTransition. Retail's
SetPositionInternal reads sphere_path.curr_cell which ValidateTransition
advances only on accepted moves and reverts on blocks — so a push-back or
standing-still tick cannot flip the cell. The static re-derive from the
resting origin strobes between outdoor 0031 and indoor 0170 at doorway
boundaries because the origin lands just outside the indoor BSP volume
after push-back; the swept cell doesn't.

SetCurrAndReturn is kept in both paths so the W2a CellGraph.CurrCell write
that the render root consumes still fires. ResolveCellId is NOT deleted —
it still has one caller at TransitionTypes.cs:1947 (AddAllOutsideCells).
partialCellId is kept as the degenerate fallback when sp.CurCellId==0
(teleport / physics reset before any transition has run).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 14:09:35 +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
50b168bc1e docs(render): Phase W chunk-1 plan — transition-owned membership flicker fix
Bite-sized TDD plan for design Stages 0-1 + W2b revert + visual gate: add the
[cell-swept] diagnostic, return the swept sp.CurCellId from ResolveWithTransition
(retail SetPositionInternal), revert the superseded W2b hysteresis, visual-gate the
doorway/cellar strobe, then lock it with a doorway replay regression. Render chunk
(Stages 3-5) gets its own spec+plan after this gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 14:04:45 +02:00
Erik
840c1b6442 docs(render): Phase W (rev) — 4-model research + transition-membership/PView design
Four independent decomp studies (Opus 4.8 x2, Sonnet 4.6, external Codex)
converge: retail carries the cell through the collision sweep (validate_transition
advances curr_cell only on an accepted move, reverts on a block) and commits it in
SetPositionInternal — it never re-derives membership from a static resting position.
acdream already ports the sweep machinery (sp.CurCellId/CheckCellId, ValidateTransition,
CheckOtherCells) but ResolveWithTransition discards the swept cell and re-derives
statically via ResolveCellId (PhysicsEngine.cs:909/928) — the root of the
0170<->0031 doorway/cellar ping-pong. The do_not_load_cells prune is secondary
(static/cross-cell lists), not the anti-flicker; W2b was doubly misplaced and is reverted.

Render: one PView::ConstructView portal traversal over the same cell graph, rooted at
the physics current cell; seen_outside (not a dungeon flag) gates landscape; the outside
draws through exit portals clipped to the doorway (no blue-hole, no stencil split).
Dungeons/interiors share the machinery; "underground" is emergent.

Design doc lays out the staged, evidence-first rewrite (Stage 0 diagnostic ->
Stage 1 transition-owned membership [visual gate] -> Stage 2 CELLARRAY/prune parity ->
Stages 3-5 render root + PView seal + entity clip). Adds the shared research prompt and
all four study reports as the grounding record.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:58:51 +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
3622a658fd docs(render): Phase W — W2a shipped+verified + baseline handoff
W2a (render reads physics CurrCell) visually verified: indoor world-from-below fixed (cellar/stairs seal). Baseline scopes the residuals: W2b (doorway ping-pong 0170<->0031, confirmed) + W3 (one-gate seal: roof, entity bleed, openings) + the EnvCellRenderer GL_BLEND fix (transparent walls). Membership (W2) done.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:55:21 +02:00
Erik
02acac5572 feat(app): UCG W2 Task 2 — render root from physics CurrCell (FindCameraCell fallback)
Wire the BFS visibility root to DataCache.CellGraph.CurrCell (the physics
membership answer written in W2 Task 1) rather than resolving independently
from a position via FindCameraCell.  Closes the render/physics disagreement
that causes the "world from below" spawn-in flicker.

Changes:
- CellVisibility.GetVisibleCells: extracted BFS body into new private
  GetVisibleCellsFromRoot(LoadedCell root, Vector3 cameraPos); existing
  GetVisibleCells delegates to it after FindCameraCell (behavior unchanged).
- CellVisibility.ComputeVisibilityFromRoot(LoadedCell? root, Vector3 fallbackPos):
  new public entry point; when root is null falls through to ComputeVisibility
  (exact today's behavior), otherwise sets _lastCameraCell = root and delegates
  to GetVisibleCellsFromRoot — cannot regress below baseline.
- GameWindow (line 7156): replaced ComputeVisibility(visRootPos) with
  ComputeVisibilityFromRoot(physicsRoot, visRootPos) where physicsRoot is
  resolved from _physicsEngine.DataCache.CellGraph.CurrCell via TryGetCell.
  physicsRoot is null whenever CurrCell is null or its id is not yet in the
  render registry, so the fallback fires until the cell loads.
- 6 new tests in CellVisibilityFromRootTests: null-root fallback equivalence
  (3 cases), registered root → CameraCell == root (3 cases).  All 160 App.Tests
  pass, 0 regressions.

Visual verification PENDING — behavior change; do not claim it works visually.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:24:23 +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
83c452b87f docs: UCG W2 (one membership) spec + plan
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:14:06 +02:00
Erik
07e68e0aff docs(roadmap): register Phase W — Unified Cell Graph (UCG); W1 shipped
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:05:34 +02:00
Erik
f2663b7e4b fix(core): UCG Stage 1 — final-review polish (VisibleCells null-guard, Neighbor doc)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:44:14 +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
bd0244f203 docs(plan): UCG Stage 1 (ObjCell scaffold) implementation plan
8 TDD tasks (RED->GREEN), Core-only, zero behavior change, built alongside the legacy cell systems. Grounded in the retail CObjCell survey + acdream inventory + #98 fixtures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 08:46:27 +02:00
Erik
e8c7164ad9 docs(render): Unified Cell Graph pivot — evidence model + Stage 1 spec
Pixel-grounded investigation concluded the indoor 'world from below' is a cell-MEMBERSHIP disagreement between render-side CellVisibility and physics-side ResolveCellId, not any single draw gate (terrain has one gated draw path; it leaks only on render null-root frames). Decision with user: full migration onto one retail CObjCell graph across physics+collision+render+streaming, staged in 5 verify-each cycles. This lands the evidence model + the Stage 1 (ObjCell scaffold) design. No code yet.

- docs/research/2026-06-02-render-cell-membership-evidence.md (the why, from pixels)

- docs/superpowers/specs/2026-06-02-unified-cell-graph-stage1-design.md (Stage 1)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 08:27:00 +02:00
Erik
1d7d8b1de4 docs(render): session-2 handoff — stencil attempt reverted, evidence-first pickup prompt
Net code change this session = 0 (stencil-occlusion T1-T4 implemented, regressed,
reverted to baseline 9bff2b0). Documents the honest failure + lessons (patchwork via
flag-based gate routing; the interior-writes-mask rule breaks outdoors; coded before
screenshotting), the still-useful evidence (cottage = IsBuildingShell GfxObjs not cell
shells; two redundant traversals; retail DrawCells outside_view gate; working window
screenshot tooling), the open questions to answer with pixels first, and a refined
evidence-first pickup prompt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 13:51:22 +02:00
Erik
9bff2b0462 docs: render-pipeline SSOT section + #78 reset pointer (redo of edits that silently failed)
The architecture and ISSUES edits in the prior commit (0013819) failed silently because
they were anchored on the session-reminder's rendering of the files, not the real text.
Redone against actual content:
- architecture doc: new 'Render Pipeline (SSOT)' section — the 3-gate patchwork vs the
  unified-PView target + the one rule (compute visibility once, enforce it once).
- ISSUES #78: promoted to the render-architecture-reset target; points to the canonical
  handoff + the architecture section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 21:50:07 +02:00
Erik
0013819fa1 docs(render): ARCHITECTURE RESET — indoor render is a 3-gate patchwork; handoff + unified-PView target
A week on the indoor render (Phase U.4 → U.4c → 2026-05-31) fixed the flap but
produced NO shippable progress: walls/ceiling don't seal, outdoor terrain is
visible from inside (#78), the enclosure reads grey/transparent. Root cause is
ARCHITECTURAL, not a bug.

Evidence this session (direct, via the new [shell] probe + screenshots) RULED OUT
every subsystem except the gating architecture: the interior cell shells render
fine (geometry/texture/opaque/depth all correct, zh=0 tr=0); the visibility
traversal computes correct sets + non-empty portal clips; cull mode is fine; the
camera/eye thread was a detour. The residual is that OUTDOOR geometry is not gated
to portal openings when indoors, and acdream enforces visibility THREE inconsistent
ways (TerrainClipMode / per-cell shell clip / entity ParentCellId filter with an
outdoor-stab bypass) instead of retail's ONE PView gate.

This commit is the reset handoff + documentation, not a code fix:
- docs/research/2026-05-31-render-architecture-reset-handoff.md — canonical: honest
  state, evidence ledger (ruled-out / do-not-repeat), the mapped 3-gate patchwork,
  the retail PView target (one traversal → one gate for ALL geometry), the reset
  mission, and a copy-paste pickup prompt.
- docs/architecture/acdream-architecture.md — new "Render Pipeline" SSOT section
  (current divergence + unified-PView target + the one rule: compute visibility
  once, enforce it once). (Doc has pre-existing corruption below this section —
  flagged for separate cleanup.)
- Apparatus: ACDREAM_PROBE_SHELL → [shell] (EnvCellRenderer per-cell prepared/drawn
  geometry + flags) added to RenderingDiagnostics + EnvCellRenderer. Throwaway.
- docs/superpowers/specs/2026-05-31-camera-collision-indoor-engagement-design.md —
  spec for e099b4c (camera collision; now parked as orthogonal to the seam).

Next session: STOP point-fixing; do the architecture reset to a single PView gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 21:35:55 +02:00
Erik
e099b4c4a3 fix(physics): M1.5 — viewer-exempt the #98 indoor shadow gate so the camera eye collides the cottage shell enclosure
Root cause: ShadowObjectRegistry.GetNearbyObjects gated the outdoor radial sweep
whenever primaryCellId is an indoor cell — this was the issue-#98 fix that stops the
cottage-floor GfxObj from capping the player's head sphere from the cellar below.
But the camera probe (ObjectInfoState.IsViewer, 0x004) also sweeps with an indoor
primary cell, and the only geometry that encloses a Holtburg cottage in acdream's data
model is the landblock-baked exterior-shell GfxObj (registered cellScope=0, outdoor).
Result: the camera's spring-arm sweep found nothing and flew to full chase distance
(eye ~3.4 m back, outside the player's cell 90% of frames — root cause of all three
post-flap residuals: transparent outer walls, terrain-through-floor, grey stairs).

Fix (Option A, retail-faithful): add isViewer parameter (default false, all existing
callers keep the gate) to GetNearbyObjects. Thread oi.IsViewer from FindObjCollisions
(TransitionTypes.cs ~line 2307) through to the gate. When isViewer=true the outdoor
sweep runs regardless of indoor primary cell — matching retail's SmartBox::update_viewer
(:92761) which calls find_obj_collisions (:308918) with no indoor-cell restriction.
The #98 gate remains in force for IsPlayer and all other non-viewer sweeps.

Retail anchors:
- SmartBox::update_viewer @ acclient_2013_pseudo_c.txt:92761 — viewer transition
  finds geometry via find_obj_collisions; no indoor gate
- find_obj_collisions @ :308918 — iterates shadow_object_list unconditionally
- CObjCell::find_cell_list @ :308751-308769 — retail's own indoor/outdoor branch
  (the model that makes the #98 gate correct for the player)

Also fixes a test-fixture geometry bug: the original RED test had
gfxLeaf.BoundingSphere.Origin in world space (0, ExteriorWallY, 96) instead of
object-local space (0, 0, 0), causing NodeIntersects to return false even when the
gate was bypassed. Corrected to local space; wall polygon vertices/plane also
expressed in local space relative to the GfxObj origin.

Tests (3 new, 1 renamed):
- SweepEye_IndoorCellExteriorGfxObjWall_StoppedByExteriorShell_AfterViewerGateExemption:
  was RED (_CurrentlyFails); now GREEN — camera sweep stopped by exterior GfxObj wall
- GetNearbyObjects_IndoorPrimaryCell_NonViewer_DoesNotReturnOutdoorGfxObj: #98 guard
  (isViewer=false keeps the gate → GfxObj NOT returned)
- GetNearbyObjects_IndoorPrimaryCell_IsViewer_DoesReturnOutdoorGfxObj: viewer-exempt
  guard (isViewer=true bypasses gate → GfxObj IS returned)

App.Tests: 154 pass / 0 fail (was 151/1). Core.Tests: 15 fail (same pre-existing
static-leak flakiness, unchanged from baseline).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 18:49:39 +02:00
Erik
3066460370 diag(render): camera-collision indoor non-engagement — RED test + diagnosis
Root cause (b): ShadowObjectRegistry.GetNearbyObjects (line 480) returns early
when primaryCellId is an indoor cell, skipping the outdoor radial sweep that
contains the landblock-baked cottage exterior-shell GfxObj. The issue-#98 fix
that prevents the player's head sphere from being capped by the cottage floor
also prevents the IsViewer camera sweep from finding the exterior building shell.
Result: camera passes through exterior walls unimpeded, driving the residual
transparent-walls symptom after the U.4c flap fix.

Evidence: live capture shows eyeInRoot=n ~90% of frames, eye-player distance
3.43m (full chase, no pull-in). RED test deterministically reproduces: synthetic
indoor cell (0xA9B40175) + exterior GfxObj registered at cellScope=0; probe
SweepEye returns pulledIn=0.0000m (full eye distance Y=5.0, wall at Y=4.0).

Fix design: exempt IsViewer from the indoor-primary early-return gate in
GetNearbyObjects — retail's find_obj_collisions (named-retail :308918) has no
indoor/outdoor cell gate; the acdream fix is correct only for IsPlayer.

Apparatus committed:
- tests/AcDream.App.Tests/Rendering/CameraCollisionIndoorTests.cs (RED test)
- docs/research/2026-05-31-camera-collision-indoor-diagnosis.md (findings + design)
- PhysicsCameraCollisionProbe.cs [flap-sweep] diagnostic retained (U.4c spike)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 18:02:37 +02:00
Erik
95b6874c12 docs(render): Phase U.4c — flap fixed + residuals handoff (checkpoint)
Canonical handoff (research note) for the U.4c flap fix + the three residuals the
visual gate revealed (#78 terrain-not-gated-inside, camera-collision need, U.5).
Records the full hypothesis journey (H1/H2 both evidence-disproven) so the next
session doesn't re-walk them. ISSUES.md: flap recorded in Recently-closed; #78
annotated (more visible post-fix). CLAUDE.md: U-phase orientation updated with the
flap-fixed status + the canonical handoff pointer + camera-collision-next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 17:01:23 +02:00
Erik
0ee328a824 fix(render): Phase U.4c — root indoor visibility at the player's cell (the flap)
The visibility root + portal-side test now use the PLAYER position (visRootPos) in
player mode instead of the camera EYE; the eye still drives the per-frame projection
(envCellViewProj). Live ACDREAM_PROBE_FLAP evidence: the flap was the 3rd-person eye
drifting out of the player's cell -> FindCameraCell returning the STALE cell for its
grace frames -> the doorway portal culled as behind-the-eye -> exit cell + terrain +
shells dropped (res=Grace eyeInRoot=n terrain=Skip on every flap frame). Retail's
CellManager::ChangePosition (0x004559B0) tracks curr_cell by the player; acdream
already roots lighting at the player (GameWindow:7152) for the same chase-cam reason
— visibility was the lone holdout on the eye. Removed the earlier synthetic builder
flap test, which modeled a disproven (side-test) hypothesis; the fix is integration-
level, validated by the visual gate + [flap] probe. App tests 151/151.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 14:35:21 +02:00