Commit graph

1779 commits

Author SHA1 Message Date
Erik
3e006d372a fix(G.3): register connector cells in the PHYSICS graph too — viewer-cell transit (#133)
After registering portals-only connector cells for VISIBILITY (d90c538), an
angle-dependent residual grey remained when the camera crossed a ramp: the
camera-collision sweep (SmartBox::update_viewer -> sphere_path.curr_cell, pc:92870)
could not transit INTO the connector cell because it had no physics cell to sweep
into — CacheCellStruct was still gated on drawable sub-meshes. So the viewer cell
stalled one cell behind the eye (confirmed live: [flap-sweep] transited every cached
neighbour but NEVER the un-cached connector 0x014D, viewerCell stuck at 0x00070103
while the eye sat 1.32 m past the connector's portal plane), and the side test
correctly culled the on-screen connector portal -> grey.

Fix: move CacheCellStruct out of the `cellSubMeshes.Count > 0` gate, next to
BuildLoadedCell — cache EVERY cell with a valid cellStruct for physics too. Retail
keeps the whole landblock cell array resident for the sweep; a portals-only
connector has an empty collision BSP but its portals drive the transit. User-gated:
"I see no grey background any longer."

Build green; 12 flood-gate tests + 677 physics/cell/transit tests green (no collision
or membership regression). TEMP render probes still retained (strip after).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:21:41 +02:00
Erik
d50023f6d9 docs(D.2b): re-ground spec onto existing AcDream.App/UI scaffold
A direct read of src/AcDream.App/UI/ found a complete (dormant) retained-mode
toolkit the grounding workflow missed: UiRoot (input routing, focus, capture,
drag-drop, tooltip, click detection, world fall-through), UiElement,
UiPanel/UiLabel/UiButton, UiHost (Tick/Draw + WireMouse/WireKeyboard),
UiRenderContext, retail-faithful UiEvent codes. It's never wired into GameWindow,
and UiPanel.cs is the exact file divergence row TS-30 cites.

So the retail UI is this existing UiRoot tree — NOT an IPanelHost/IPanelRenderer
backend. Rewrote the architecture sections: Spec 1 now WIRES the dormant UiHost
and adds only the gaps (DrawSprite + frag uUseTexture=2, UiNineSlicePanel,
UiMeter, MarkupDocument that builds a UiElement subtree, ControlsIni). Input
machinery already exists in UiRoot; deferring it is now about integrating two
input consumers, not a missing contract. Plugin contract becomes a UiElement/
markup subtree added to UiRoot (IUiRegistry on IPluginHost), not IPanel.

Net: strictly less new code, more faithful, retires TS-30 by subclassing the
file it cites. Added §0 documenting the correction + the process lesson
(subsystem-discovery must glob by directory, not by the parent's framing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:13:30 +02:00
Erik
de9229eed5 docs(D.2b): design spec — retail panel frame + live Vitals (Approach C)
Brainstormed design for the D.2b retail-look UI backend: our own KSML-style
markup + controls.ini stylesheet + retained-mode toolkit on Silk.NET (no
embedded browser, zero external deps — Approach C, chosen over Ultralight/CEF
and RmlUi for memory/dep-weight/faithfulness).

Spec 1 scope: an 8-piece dat-sprite window frame + live Vitals bars bound to
the existing VitalsVM, gated behind ACDREAM_RETAIL_UI=1, rendered via a reused
TextRenderer batch. Render-only (input/hit-test, AcFont glyphs, anchor solver,
LayoutDesc importer all deferred).

Grounded by a read-only research workflow (7 readers + gap-critic). The critic
corrected several stale memory/plan-doc facts now baked into the spec's
do-not-trust list: VitalsVM is a sealed class (not the old record); chrome
sprite IDs are unverified (Step-0 dat prove-out resolves them empirically);
controls.ini exists and #FFDBD6A8 is editbox text not a bg; DatCollection reads
are thread-safe; KSML is rich-text not the layout language (we mirror
ElementDesc).

Phase D.2b / Milestone M5 (parallelizable with M3/M4 — opened as a parallel
track while M1.5 stays the active critical-path milestone). Retires divergence
row TS-30 + adds one IA row when the chrome ships.

Also gitignores the /.superpowers/ visual-companion scratch dir.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:00:14 +02:00
Erik
d90c5385d2 fix(G.3): register portals-only connector cells for visibility (#133 ramp grey)
The grey "barrier" at a dungeon ramp was a one-cell registration gap. The ramp's
connector cell (0x0007014D) is a portals-only pass-through — CellMesh.Build yields
0 drawable sub-meshes for it (you walk through it on adjacent floors). But the whole
registration block — including the portal-VISIBILITY registration (BuildLoadedCell ->
_cellVisibility) — was gated behind `if (cellSubMeshes.Count > 0)`. So that cell was
never added to the visibility graph; the flood lookup-missed it (PortalVisibilityBuilder
:369), couldn't traverse it to the room below, and the grey clear color showed through.

Confirmed live via two added probes: [cellreg] registered=204/205 (only 0x014D missing)
+ [pv-trace] p4->0x0007014D skip=lookup-miss. After the fix: registered=205,
hasRamp=True, skip=lookup-miss gone, the room below renders.

Fix: compute the cell transforms and call BuildLoadedCell (visibility) for EVERY cell
with a valid cellStruct, regardless of drawable sub-meshes — matching retail, which
keeps the whole landblock cell array resident before the flood runs. Drawing
(RegisterCell, _pendingCellMeshes) and the physics BSP (CacheCellStruct) stay gated on
drawable geometry (a portals-only connector has nothing to draw and no collision
surface). Not a regression from the FPS-collapse work — a pre-existing gate the
now-navigable dungeon exposed (every ramp/stair/cellar mouth would show it).

TEMP diagnostics retained for the residual angle-grey investigation (strip after):
[cellreg] (GameWindow), the 0x0007 [pv-trace] gate widen + raw-NDC bbox (PortalVisibility-
Builder). Three earlier render-math theories (portal_side, on-screen clip, near-eye
projection) were each refuted by apparatus/probe before shipping — this is the verified one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:49:02 +02:00
Erik
7d8da99f79 fix(G.3): collapse dungeon streaming at the snap, not after landblock finalize (#133)
The dungeon-streaming gate read SeenOutside from the render registry
(_cellVisibility.TryGetCell), which only succeeds AFTER the landblock FINALIZES —
~tens of seconds for a 205-cell dungeon. So the collapse fired late and the full
25x25 neighbor window churned in first ("~30s to stabilize at high FPS").

EnvCell extends ObjCell, which already carries SeenOutside (set from the EnvCell
dat flags at construction), so CurrCell.SeenOutside is available the moment the
player is placed (the snap). Read it directly instead of the registry. Collapse now
engages ~3s in (snap) instead of ~30s (finalize); residual is the ~24 neighbors the
bootstrap loads before the snap, which then unload. Also simplifies the predicate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:06:17 +02:00
Erik
53e22a350d fix(G.3): relocate the player entity to its CELL landblock indoors, not position-derived (#133)
After the dungeon-collapse fix the local player avatar stopped rendering: the
per-frame RelocateEntity moved the player entity to its position-derived landblock
floor(pp/192), which for a dungeon's negative-local-Y cell is the off-by-one (0,6)
— the very landblock the collapse unloads. So the player entity sat in an unloaded
landblock and was never drawn (the dungeon itself, in 0x0007, rendered fine).

Fix: when the player is in an indoor cell (CellId low word >= 0x0100), relocate to
the cell's OWN landblock (CellId >> 16), matching the streaming-collapse pin. The
cell id is authoritative for ocean-placed dungeon geometry. Outdoor entities keep
the position-derived path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 09:52:01 +02:00
Erik
2561918a70 fix(G.3): pin dungeon collapse to the cell's landblock, not the position-derived one (#133)
"The dungeon is broken" — the collapse was unloading the REAL dungeon. A dungeon's
EnvCells sit at arbitrary "ocean" world coords with negative cell-local Y (snap
showed pos=(58.9,-69.6) in cell 0x00070133), so the observer landblock
_liveCenterY + floor(pp.Y/192) = 7 + floor(-69.6/192) = 7 + (-1) = 6 lands one row
off. The collapse pinned to 0x0006 and unloaded 0x0007 — the real dungeon — which
nulled CurrCell (the cell no longer existed) and left the player floating in
outdoor-lit empty space (lb 1/1 @ ~1585 fps, but the wrong landblock). This is the
Bug-A negative-local-coordinate class.

Fix: when inside a dungeon, pin the collapse to the cell's OWN landblock
(CurrCell.Id >> 16), never the position-derived observer landblock — the cell id is
the authoritative landblock for ocean-placed dungeon geometry.

Also hardened the hysteresis so a transient CurrCell flicker can't thrash:
- Re-collapse when insideDungeon at a DIFFERENT landblock (multi-landblock dungeon).
- Expand only on a DISTANT move (Chebyshev > 1) — a real exit teleports far from the
  ocean-grid block; the off-by-one flicker is always an ADJACENT (±1) landblock, so
  it now HOLDS the collapse instead of expanding.
- SweepCollapsed always preserves _collapsedCenter (the true dungeon landblock),
  never the per-frame observer landblock.

Build green; 59 streaming tests green (flicker regression test updated to the
realistic adjacent off-by-one).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:51:50 +02:00
Erik
d9e7dd65e9 fix(G.3): hysteresis on the dungeon streaming gate — stop collapse↔expand thrash (#133)
The first cut of the dungeon gate keyed expand on the per-frame insideDungeon
signal (CurrCell is a sealed EnvCell). Live, CurrCell momentarily resolves to
null mid-frame while the player stays put in the dungeon landblock, so the gate
flipped collapse→expand→collapse every few frames. Each expand re-streamed the
full 25×25 window; the unloads couldn't keep up (MaxCompletionsPerFrame=4), so
registered lights leaked to 212k and FPS spiked to single digits between the
~199 fps collapsed frames.

Fix: once collapsed, key the gate on the STABLE observer landblock, not CurrCell.
Stay collapsed while the player remains in the dungeon landblock (_collapsedCenter);
expand only when the observer actually moves to a different landblock (portal/
teleport out). CurrCell flicker no longer thrashes.

Regression test added (Collapsed_CurrCellFlickersToNull_SameLandblock_DoesNotExpand).
Build green; 60 streaming tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:43:18 +02:00
Erik
56860501b6 fix(G.3): collapse streaming to the single dungeon landblock indoors (#133 FPS)
Dungeon FPS sat at ~30 (frame ~33ms) because the 25x25 streaming window around
the dungeon landblock pulled in ~129 NEIGHBORING landblocks + their thousands of
torch/particle emitters, all drawn though never visible. In AC all dungeons are
packed adjacent in the unused "ocean" map grid, so those neighbors are unrelated
dungeons. The FPS timeline proved it: 247 fps at login (lb 0/0, ~10K entities) →
17 → 30 as landblocks streamed in (lb 0→129) — the cost tracked LANDBLOCK count,
not entities.

Retail-faithful: ACE LandblockManager.GetAdjacentIDs returns ZERO adjacents for a
dungeon (`if (landblock.IsDungeon) return adjacents;`, Landblock.cs:577-582) —
every dungeon is a self-contained landblock you never see out of.

Fix: when the player stands in a sealed indoor cell (CurrCell.IsEnv &&
!SeenOutside — the same predicate that kills the sun/sky), collapse streaming to
just the player's dungeon landblock and unload the neighbors. Building interiors
(cottage/inn) have SeenOutside cells, so they are NOT gated and keep their
surrounding terrain (the frozen building/cellar demo is unaffected). Unloading the
neighbors also tears down their lights (removeTerrain → UnregisterOwner), shrinking
LightManager._all from ~2227 toward retail's ≤40 — which directly helps the A7
lighting bake landing next.

Mechanics (StreamingController):
- Edge IN: ClearPendingLoads() cancels the in-flight 25x25 window (new streamer
  ClearLoads control job — worker drops queued Loads, keeps Unloads), unload every
  resident neighbor, pin a radius-0 StreamingRegion, (re)load the dungeon block if
  needed.
- Stay collapsed: sweep any straggler that finished loading after the edge (a Load
  the worker had already dequeued before ClearLoads).
- Edge OUT (portal/teleport to outdoors): rebuild the full two-tier window at the
  new center, unload anything stale.

AP-36 added to the divergence register (the gate uses the cheap SeenOutside cell
predicate as an approximation of ACE's full landblock IsDungeon classification).
GameWindow also carries a TEMP ACDREAM_LOG_FPS=1 headless FPS line (strip after
the A7 FPS+lighting verification).

Build green; 58 streaming tests green (6 new dungeon-gate tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:32:56 +02:00
Erik
007e287309 fix(A7): port retail calc_point_light (1-dist/falloff) ramp — kill the "spotlight" hard edge (#133)
The dungeon/house/outdoor lights read as hard-edged blown discs ("spotlights")
because our point/spot shader used `atten = 1.0` flat inside a hard `d < range`
cutoff. The mesh.frag comment claimed this was retail-faithful ("no attenuation
inside Range... the bubble-of-light look relies on crisp boundaries", citing
r13 10.2) — that was a misread and the literal cause of the symptom.

Verified against the decomp (not guessed): calc_point_light (0x0059c8b0, the
PER-VERTEX point-light path that lights static walls) scales each light's
contribution by (1 - dist/falloff_eff) — a LINEAR ramp that fades to exactly 0
at the edge, eliminating the hard disc. falloff_eff = Falloff * static_light_factor,
and static_light_factor = 1.3 (0x00820e24), NOT the 1.5 config_hardware_light
rangeAdjust (that 1.5 is the D3D-dynamic path for moving objects, a different
path). The Ghidra port (acclient.c:808639) is more garbled — BN pseudo-C is the
oracle here; the exact normalization factor + a half-Lambert wrap (0.5*dist+N*L)
are x87-obscured (same artifact class as GetPowerBarLevel) and left unported.

Changes:
- mesh_modern.frag + mesh.frag: replace flat atten with clamp(1 - d/range, 0, 1);
  Range now carries falloff_eff so the ramp fades to 0 at the cutoff. Fix the
  false "no attenuation / crisp bubble" comment in mesh.frag.
- LightInfoLoader: Range = Falloff * 1.3 (static_light_factor), was * 1.5.
- LightManager: correct the stale class doc comment (Tick is now nearest-8
  allocation-free partial-select with NO viewer-range slack filter).
- divergence register: AP-16 updated (slack filter removed), AP-35 added
  (per-pixel vs per-vertex Gouraud; dropped half-Lambert wrap + normalization).
- test: LightingHookSinkTests Range 8*1.3 = 10.4.

Build + 20 lighting tests green. Visual gate pending (game-wide lighting change:
dungeon torches, house candles, outdoor braziers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:48:46 +02:00
Erik
5872bcf075 perf(lighting): allocation-free nearest-N light selection (#133 FPS)
Tick built a new List<>(N) and ran an O(N log N) Sort every frame; in a dungeon
N is thousands of torches, so it allocated a large list per frame (GC pressure ->
FPS). Replace with an insertion partial-select that keeps the nearest maxPoint
directly in the _active window — O(N * maxPoint), maxPoint<=8, zero allocation.
Same selection result (nearest 8); lighting suite 20/20 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:26:17 +02:00
Erik
0fe479ba06 docs(A7): pin the GENERAL light over-saturation cause (intensity=100 mis-read) + FPS note
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:19:47 +02:00
Erik
1e70a5a484 fix(G.3 A7): torch range = Falloff x 1.5 (retail rangeAdjust) — wider pools (#133)
Retail PrimD3DRender::config_hardware_light (0x0059ad30) sets the hardware light
Range = Falloff * rangeAdjust (1.5, global 0x00820cc4). We used Range = Falloff, so
torches reached only 2/3 of retail -> tight 'candle/spotlight' bubbles in dungeons.
Match retail's reach. Ambient 0.20 confirmed retail-faithful (the 0.30 was CreatureMode,
not world cells). Lighting suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:58:03 +02:00
Erik
9e809bc661 diag: ACDREAM_PROBE_LIGHT [light-detail] — per-light range/intensity/cone (#133 A7)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:55:14 +02:00
Erik
167f05c4fa docs(G.3 A7): record dungeon light-selection fix (activeLights 2->8) + the 0.30 ambient follow-up
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:45:29 +02:00
Erik
a80061b0c2 fix(G.3 A7): dungeon lighting — select 8 NEAREST lights, not viewer-in-range (#133)
The active-light selection dropped any point light whose range didn't reach the
VIEWER (DistSq > Range^2*slack -> skip). Retail's D3D-style fixed pipeline picks
the 8 NEAREST lights and applies the hard range cutoff PER SURFACE in the shader
(mesh_modern.frag: if (d < range)). The viewer-range candidacy filter suppressed
a torch whenever the player stood outside its range, so a dungeon room with 2227
registered torches lit only the ~1 the player was standing in (activeLights ~= 1,
rest of the room at flat 0.2 ambient = the "lighting off" report). Drop the filter;
take the nearest 8 regardless of viewer range. Removed the now-unused RangeSlack
const; updated the two tests that codified the old filter. Core lighting suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:35:01 +02:00
Erik
d6fb788c96 diag: ACDREAM_PROBE_LIGHT — log dungeon ambient/sun/active-light state (#133 A7)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:43:27 +02:00
Erik
a40c38e8bd milestone(G.3): dungeons RENDER — #95 was a Bug-A symptom, not an unbounded flood
Autonomous /loop verification: a live launch into the 0x0007 dungeon renders with a
sane budget (WB-DIAG instances ~39,000, meshMissing=0; was 9.1M pre-Bug-A), correct
membership (no ACE failed-transition spam), navigable. The chain: G.3a teleport
hold+place + Bug A (2ce5e5c, validated-claim landblock prefix) + login-into-dungeon
recenter (47ae237). A headless diagnostic (Issue95DungeonFloodDiagnosticTests, 95d9dab)
proved the portal flood is already bounded (1-17 cells vs the stab_list's 120-204), so
#95's "port grab_visible_cells stab_list bounding" was the WRONG fix and is NOT pursued.
ISSUES #95 -> RESOLVED, #133 -> renders + login-into-dungeon fixed; CLAUDE.md current
state + render digest updated. Remaining for M1.5: A7 dungeon torch/point-lighting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:36:04 +02:00
Erik
47ae237e7b fix(G.3): recenter streaming onto the spawn landblock at login (#133)
A character saved inside a far dungeon hung at the #107 auto-entry hold because
the streaming center was fixed at the startup default and the login spawn never
recentered it, so the dungeon never streamed. Mirror the teleport-arrival
recenter on the login player-spawn path: when the player's spawn landblock
differs from the current center, recenter before translating the spawn position
(landblock-local -> new-center frame). No-op for a same-landblock (normal
Holtburg) login.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:00:14 +02:00
Erik
95d9dab4bb test(#95): headless dungeon-flood diagnostic — measure visible-cell count on 0x0007
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:52:00 +02:00
Erik
dd7b73a837 docs(G.3): file login-INTO-a-dungeon gap (streaming not recentered at login)
Re-gate of Bug A revealed: logging in with the character saved inside a far
dungeon hangs at the #107 auto-entry hold (frozen, no [snap]). The streaming
center is set once at startup to the default and the login spawn never recenters
it, so the dungeon never streams and IsSpawnCellReady never goes true. The
teleport-arrival path recenters (G.3a); the login path doesn't. Filed under #133
with the fix shape (recenter onto the spawn landblock at login) + the ACE-reset
workaround.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:39:45 +02:00
Erik
c8188e0ed6 docs: correct stale UCG CellGraph comments — the graph is active, not inert
The "consumed by nobody (zero behavior change)" / "INERT in Stage 1 (no writer)"
comments predate the UCG becoming load-bearing. Verified against the call sites:
CellGraph is populated unconditionally in CacheCellStruct (before the idempotency
+ null-BSP guards, so BSP-less cells are included) and consumed for the player
render/lighting root (CurrCell, written at the PhysicsEngine.UpdatePlayerCurrCell
player chokepoint; read by GameWindow:7502/7717), the universal id->cell resolver
(GetVisible), the 3rd-person camera cell (FindVisibleChildCell), and the
block-local terrain origin (TryGetTerrainOrigin, read by CellTransit:484/736).
Comments only — no behavior change. Core suite 1445 passed / 2 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:35:58 +02:00
Erik
70c559c1ba docs(G.3): gate correction — G.3a core landed; #95 CONFIRMED LIVE (not superseded)
The G.3a visual gate ran a real PlayerTeleport into the 0x0007 dungeon. The core
hold+place worked (grounded on the dungeon floor, no ocean) and Bug A (landblock-
prefix mis-stamp) is fixed (2ce5e5c). But the gate proved #95 (portal-graph
visibility blowup, ~9.1M instances/frame) is LIVE under the current pipeline — my
plan's "likely superseded / conditional G.3b" premise was wrong. Spec §2.5/§3.2 +
ISSUES #133/#95 updated: G.3b (grab_visible_cells stab_list bounding) is REQUIRED,
needs its own grounding/brainstorm. Also noted: the render-only hydration decouple
was reverted (e7058ca) for making the player invisible at Holtburg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:30:43 +02:00
Erik
2ce5e5c862 fix(G.3a): validated-claim placement keeps the claim's landblock prefix (#133)
The #111 validated-claim branch returned lbPrefix | (cellId & 0xFFFF), where
lbPrefix is found by searching resident landblocks for one containing the
candidate position. A dungeon EnvCell's local Y can be negative, so the dungeon
landblock fails the [0,192) bounds test and the loop matches a neighbouring
(e.g. Holtburg) resident block -> the validated claim 0x00070143 got re-stamped
0xA9B30143, making the client mis-resolve the player to the wrong landblock and
spam ACE with rejected moves. The validated claim's full id is authoritative;
return it directly. Byte-identical for the login case (position in the claim's
own landblock); fixes the far-teleport dungeon case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:27:45 +02:00
Erik
e7058caa79 Revert "fix(G.3a): hydrate EnvCell physics + visibility independent of render mesh (#133)"
This reverts commit ab050a015f.
2026-06-13 18:05:36 +02:00
Erik
3238f1fde4 docs(G.3a): note CacheCellStruct's unconditional UCG CellGraph add is inert (#133)
Code-review follow-up: the hydration decouple's safety rests not only on
CacheCellStruct self-gating its BSP cache, but on the fact that a geometry-less
cell — though now added to the UCG CellGraph unconditionally — never enters the
_cellStruct BSP dictionary membership/placement resolve through, so the player
can never be rooted in one. Document that load-bearing invariant at the hoist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 17:33:52 +02:00
Erik
ab050a015f fix(G.3a): hydrate EnvCell physics + visibility independent of render mesh (#133)
BuildLoadedCell + CacheCellStruct were gated behind cellSubMeshes.Count > 0, so a
geometry-less collision cell got no collision (fall-through) and no visibility
node. Retail couples neither to visible geometry; CacheCellStruct self-gates on a
null PhysicsBSP, so this is safe. Render registration stays behind the submesh
guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 17:26:34 +02:00
Erik
f22121bd7d feat(G.3a): hold teleport arrival until dungeon hydrates, then place (#133)
Replaces the unconditional OnLivePositionUpdated snap (which resolved against
the resident old landblocks before the destination streamed in -> ocean) with a
recenter + deferred BeginArrival; per-frame Tick places via the unchanged #111
validated-claim Resolve once SampleTerrainZ + IsSpawnCellReady report ready, or
force-snaps loudly on an impossible claim / ~10s timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 17:16:12 +02:00
Erik
aca4b4645a refactor(G.3a): Place flips Idle before delegate; test mid-hold reset (#133)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 17:11:40 +02:00
Erik
7947d7ad0a feat(G.3a): TeleportArrivalController hold-until-hydration state machine (#133)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 17:06:33 +02:00
Erik
c9650bd3bd plan(G.3a): core teleport-into-dungeon implementation plan (#133)
TDD plan for the gated G.3a core: a pure TeleportArrivalController state machine
(hold-until-hydration + force-snap on impossible/timeout) + its GameWindow wiring
(replace the unconditional arrival snap with recenter + deferred BeginArrival;
per-frame Tick; readiness predicate reusing the #107 login triplet) + the EnvCell
physics/visibility hydration decouple + the visual acceptance gate. G.3b/c/d get
their own plans after the gate.

Also syncs the spec: the readiness predicate reuses SampleTerrainZ + IsSpawnCellReady
+ IsSpawnClaimUnhydratable (the validated #107 login gate) rather than a new
IsLandblockApplied query — strictly more faithful, less new surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 17:02:03 +02:00
Erik
6680fd42b2 spec: G.3 dungeon support design (M1.5 exit-gate) — phased, retail-faithful
Brainstorm outcome for #133/G.3. Grounds the corrected root cause (dungeon
landblock = flat terrain + EnvCells, streams via the existing pipeline; the
blocker is the teleport-arrival snap firing BEFORE the dest landblock hydrates)
against the current code (5 verified seams) and lays out Approach C:

  G.3a  core teleport-into-dungeon: hold-until-hydration on the arrival path
        (reuse #107 IsSpawnCellReady + IsSpawnClaimUnhydratable) + #111
        validated-claim EnvCell placement + dest-ready streaming query +
        dest-coord validation + timeout safety + decouple EnvCell
        physics/visibility hydration from the render-mesh guard.  -> VISUAL GATE
  G.3b  #95 stab_list bounding — CONDITIONAL on the gate showing the blowup
        (its repro is stale, from the T4-deleted WB path; the current flood is
        landblock-confined + enqueue-once, so #95 is likely superseded).
  G.3c  faithful TeleportAnimState portal-tunnel FSM (decomp 004d6300 /
        219405-219774); the TAS_TUNNEL hold-exit gates on G.3a's same readiness
        predicate (the tunnel IS the hold's visual form).
  G.3d  recall game-actions (/ls etc.) — same arrival flow; doubles as the test
        lever.

Supersedes the §12 port-plan of r09 (most of it already shipped); r09 stays the
wire/format/recall contract reference. Resolves the handoff's 4 open questions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 16:43:39 +02:00
Erik
90786c19e2 handoff: M1.5 dungeon support (G.3) grounded — design research + the terrain-less-premise refutation
Adds the 2026-06-13 dungeon-G.3 handoff doc + a dat-probe test that
RESOLVED the pivotal design ambiguity. A research agent assumed dungeon
landblocks are terrain-less (LandblockLoader.Load returns null ->
"rewrite the pipeline for terrain-less landblocks", 13 seams). The dat
probe refutes it: dungeon landblock 0x0125 has a flat (all-zero-height)
LandBlock record PLUS 71 EnvCells and no buildings/objects -> it streams
fine via the existing pipeline as a flat-terrain landblock.

The real blocker (#133) is narrow: the teleport-arrival handler
(GameWindow.cs:4928) snaps the player via physics.Resolve BEFORE the
dungeon landblock streams in -> Resolve falls back to the resident
Holtburg landblocks -> places the player at A9B3 ocean. Fix shape:
hold-until-hydration (reuse the #107 IsSpawnCellReady gate for the
teleport-arrival path) + place into the EnvCell + the retail
TeleportAnimState portal-space FSM for the full-G.3 loading screen.
ACE confirms dungeons are single-landblock, so "multi-landblock LOD"
is moot.

The handoff captures: this session's closes (#108-residual/#127/#125
gated, #116 partial), the M1.5 re-open decision, the corrected root
cause, the 5-way reference grounding (holtburger/ACE/retail decomp +
the dat probe), the design direction, and the open brainstorm questions.

Next session: resume the brainstorm at "propose approaches" -> spec ->
writing-plans -> implement.

Suites green: App 264+1skip / Core 1445+2skip / UI 420 / Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 14:53:27 +02:00
Erik
9c2ceb2336 milestone: re-open M1.5 — dungeon support (full G.3) pulled into scope; M2 re-deferred
Correction to 1bf037a. The user reverted the M1.5 landing: the indoor
world isn't done while dungeons are completely broken. Attempting the
dungeon demo revealed it's not a single bug (#133) but a whole-feature
gap — terrain-less indoor-only dungeon landblocks aren't supported
anywhere in the streaming/load/render/physics pipeline:
- LandblockLoader.Load returns null when there's no LandBlock terrain
  record (dungeons have none) -> the dungeon never loads.
- LandblockStreamer fails when the terrain mesh build returns null
  (dungeons have no terrain mesh).
- The teleport-arrival snap Resolves before the dungeon hydrates ->
  places the player in the old Holtburg frame over ocean.

The user chose the FULL Phase G.3 scope (dungeon streaming + portal-space
loading screen + multi-landblock LOD + PlayerTeleport handling) and
pulled it into M1.5. M1.5 lands only when BOTH the building/cellar demo
(done) and the dungeon demo (enter via portal, navigate 3-5 rooms, walls
block, smooth transitions) pass. M2 (CombatMath) re-deferred.

Currently brainstorming the dungeon-support design (spec ->
docs/superpowers/specs/). Docs corrected: milestones (M1.5 ACTIVE +
extended, M2 DEFERRED, currently-working-toward -> M1.5), CLAUDE.md
current-state, ISSUES.md #133 (G.3 pulled into M1.5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 14:45:53 +02:00
Erik
1bf037a1c9 milestone: M1.5 LANDED (building/cellar demo); dungeon demo -> #133/G.3; start M2
M1.5 "Indoor world feels right" lands on its primary building/cellar
demo, user-gated across the 2026-06 sessions: multi-floor inn navigation
without sling-out/wall-clip, cottage cellar descend+ascend without
falling through, walls block everywhere, smooth cell transitions. The
holistic Option-A render port (one DrawInside(viewer_cell), BR-2..BR-7 /
T1..T6) and the A6.P4 per-cell shadow physics shipped and were gated; the
doorway-flap family is closed (#119/#128, #112, #113, #124,
#129/#130/#131/#132, #108-residual, #127) and the #90/synthesis
workarounds removed.

The dungeon half is the one piece NOT landed: attempting the dungeon demo
(meeting-hall portal) surfaced issue #133 - teleport-into-a-dungeon snaps
the player BEFORE the dungeon landblock streams in (GameWindow.cs:4928
Resolve falls back to the resident Holtburg landblocks -> snaps to an
outdoor cell over ocean). That is Phase G.3 (dungeon streaming +
PlayerTeleport handling, M4), not a render bug (#95 died with the Option
A rewrite). Per the milestones doc's pre-flagged choice, the dungeon demo
is promoted to G.3 and M1.5 lands on the building/cellar demo (user
decision 2026-06-13).

Start M2 "Kill a drudge" - first port target CombatMath.ComputeDamage
(port-ready per the combat-math research memo; ACE oracle). Drudges spawn
outdoors for the demo, so M2 does not depend on #133/G.3.

Files: milestones doc (M1.5 -> LANDED, M2 -> ACTIVE, currently-working-
toward flipped), CLAUDE.md current-state -> M2, ISSUES.md #133 filed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 10:45:00 +02:00
Erik
8682a8db70 close #125: bounded upload retry kills the sticky-drop debt (failed GL uploads were never re-staged)
The GL root cause was fixed in fcade06 (the gpu_us query-ring stale
errors). This closes the remaining design debt: a genuinely-failed
UploadMeshData was dropped permanently.

Exact mechanism (traced this session): UploadMeshData's catch returns
null, the staged item is already consumed, and _renderData stays empty -
but the prepared data lingers in _cpuMeshCache, so the #128 EnsureLoaded
re-arm hits PrepareMeshDataAsync's CPU-cache short-circuit
(ObjectMeshManager.cs:448-453) which returns the cached data WITHOUT
re-staging it for upload. The mesh stays invisible until CPU-cache
eviction - session-sticky under low cache pressure (the in-tower
scenario).

Fix: the per-frame Tick drain (WbMeshAdapter) now re-stages a failed
upload for the NEXT frame via ObjectMeshManager.UploadOrRequeue, bounded
by MaxUploadRetries (3). The attempt counter lives on the ObjectMeshData
object so it resets to 0 naturally on re-prepare. Re-stages are
collected and re-enqueued AFTER the drain loop, never inside it, so a
deterministic failure cannot spin the queue within a single frame; past
the cap it gives up with a loud [up-retry] ... giving up line - a
genuine GL defect now surfaces instead of the old silent permanent drop
or an unbounded retry storm. Retail loads content synchronously and has
no such failure mode; this converges the async pipeline toward that
guarantee.

The uncaught GenerateMipmaps path (open-question c) is INTENTIONALLY
left to surface errors - a blanket catch there would mask future real
defects (no-workarounds rule), and its trigger (fcade06) is retired.

No visual gate (robustness). Build green; App.Tests 264 + WbMeshAdapter
tests green. No GL-context test seam exists for the upload path, so the
bounded retry is verified by construction + the regression suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 10:27:26 +02:00
Erik
bf18a54369 fix #116 (partial, Ghidra-confirmed): slide_sphere degenerate guard uses F_EPSILON, not EpsilonSq
The user brought up Ghidra; its decompiler (patchmem.gpr, full PDB)
resolved the Binary-Ninja `test ah,5` x87 branch-sign ambiguity that
blocked the desk read. CSphere::slide_sphere (0x00537440) decompiles
cleanly to:

  fVar3 = |cross(collisionNormal, contactPlane.N)|²;
  if (::F_EPSILON <= fVar3) {                       // crease exists
      ... offset = cross * dot(cross,gDelta)/fVar3;
      if (|offset|² < ::F_EPSILON) return COLLIDED_TS;   // degenerate guard
      ... add_offset_to_check_pos -> SLID_TS
  }

Retail compares the SQUARED magnitudes against F_EPSILON
(0.000199999995 ~= 0.0002 = PhysicsGlobals.EPSILON). Our port compared
against EpsilonSq (0.0002^2 = 4e-8) - a ~5000x too-tight threshold (the
BN pseudo-C rendered the comparison as `test ah,5` after an x87 FCMP,
which is sign-ambiguous; agent reads disagreed). Fixed both comparisons
at TransitionTypes.cs:3098,3105 to EPSILON.

Effect: crease-exists now needs >=0.81 deg between the wall and contact
normals (was 0.011 deg - which routed near-parallel pairs through the
numerically unstable projection); the degenerate guard now hard-stops
slides under ~1.41 cm like retail (was 0.2 mm). Branch POLARITY was
already correct - no change there.

No regression: full physics suite (612) + full Core (1443) green. Not a
register deviation (no row existed; this is an undocumented porting
error corrected to match retail).

This does NOT close #116 - it fixes a tangential constant, not either
reported shape. Ghidra also settled the two shapes' diagnosis (recorded
in ISSUES.md #116 + physics digest):
- Shape-1: our cn=UnitZ default IS retail-faithful (validate_transition
  0x0050aa70 has the identical `if (collision_normal_valid==0)
  set_collision_normal(UnitZ)`). The real divergence is upstream -
  tick-22760 our collision_normal_valid was false where retail's was
  true (it recorded the door-face normal). Needs the instrumented
  tick-22760 replay.
- Shape-2 (D4 stays skipped, note sharpened): slide_sphere slides
  in-frame (SLID_TS) so Z=1.92 is faithful and the D4 Z=2.0 hard-stop
  pin is the suspect half; the threshold fix didn't move D4 (real slide,
  not degenerate). Needs a cdb trace of an airborne wall hit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 10:21:51 +02:00
Erik
35961f2039 #116 oracle desk read: slide-response leads pinned, needs a live cdb to finish
Read both sides quote-for-quote (verified against source): our
CSphere::slide_sphere port (TransitionTypes.cs:3054-3133) vs retail
0x00537440 (decomp 321403-321532). Findings:

1. Shape-1 (tick-22760 lost 3.57cm slide) is NOT the degenerate-offset
   guard - retail's guard only kills slides under ~1.4cm. The real
   divergence is the collision-normal SOURCE: our harness cn=(0,0,1) vs
   live cn=(0,+1,0). Strong lead: TransitionTypes.cs:3701-3702 defaults
   cn=Vector3.UnitZ when no valid normal.
2. Shape-2: retail's slide_sphere applies the slide IN-FRAME
   (add_offset_to_check_pos @0x53777e, return SLID) - our in-frame slide
   to Z=1.92 is likely retail-faithful and the D4 frame-1 hard-stop pin
   is the stale one (pending the first-airborne-frame plane state).
3. Candidate epsilon-squaring divergence: retail compares SQUARED
   quantities against 0.000199999995 (non-squared) while we compare
   against EpsilonSq=0.0002^2 - possibly 1e4x too small. Explains
   neither shape; DO NOT change without cdb (the test ah,5 branch
   polarity is the undecodable BN construct from the PosHitsSphere saga).

No code change (oracle-first; the issue title calls for cdb and the BN
decomp is provably ambiguous on the branch signs). ISSUES.md #116 +
physics digest carry the leads + the scoped cdb plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 10:11:34 +02:00
Erik
fbfecd20ca docs: CLAUDE.md current-state - #108-residual + #127 closed, ledger down to #116 + #125
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 10:05:23 +02:00
Erik
4ad6fb9184 close #127 (user-gated + desk pin): distant-building flood flap died with the W=0 clip port
User re-gate 2026-06-12: ran past distant buildings, 'Seems to have
been fixed' - no flicker/vanish. The per-building flood-admission
bistability (#127, the building-flap mechanism behind the tower roof
flap and #123 'buildings vanish when running past') is gone.

Root: the bistable knife-edge admission died with the W=0
polyClipFinish clip port (987313a - the #119/#120 work that 'kills the
knife-edge class everywhere') plus the #120 containment-rejection
growth fix. The captured-pair evidence (tower-viewer-capture.log,
2026-06-11) PRE-dates all of those - it was that same near-eye knife
edge, not a separate distant mechanism.

Desk confirmation (both green at HEAD):
- CapturedFlipPair_AdmissionIsStable: the original 4 cm flip pair is
  now |A|=|B| with zero diff across all FOVs and both pre-gate states.
- DistantBuildingStrafe_NoAdmissionChurn (new regression pin): 0
  admission churn across all 21 building groups x {10,30,60,120,190} m
  x 100 mm-step run-past strafes, both pre-gate states. A stable flood
  toggles each cell at most once over a monotone eye path; this asserts
  no cell toggles >=2x.

ISSUES.md #127 -> CLOSED with the DO-NOT-RETRY note (no re-opening the
BuildFromExterior seed gates for a flap symptom without a fresh HEAD
repro - the captured-pair lead is dead). Render digest banner updated.

Suites: App 264+1skip / Core 1443+2skip / UI 420 / Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 10:03:33 +02:00
Erik
bf800677f0 close #108-residual (user-gated): terrain backface cull fixed the cellar grass window
User visual gate 2026-06-12 ('Yes it is fixed.') - cellar climb clean,
outdoor terrain intact from above. Flip ISSUES.md #108 to CLOSED.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 09:54:58 +02:00
Erik
96a425a9a5 fix #108-residual (root cause): terrain drew DOUBLE-SIDED - port retail landPolysDraw eye-side gate as terrain backface cull
The cellar-ascent grass window was the UNDERSIDE of the z~94 grade
sheet. Retail terrain is single-sided: ACRender::landPolysDraw
(0x006b7040) draws each land triangle ONLY when the camera is on the
POSITIVE (upper) side of its plane (Plane::which_side2 vs
Render::FrameCurrent, zFightTerrainAdjust bias) - a below-grade eye
gets NO terrain, so retail shows sky through the cellar door.

We inherited WB's frame-global cull DISABLE (WB GameScene.cs:841 - an
editor camera goes underground by design) and TerrainModernRenderer.Draw
set no cull state of its own -> terrain rasterized both sides. From a
below-grade eye every aperture sight-ray RISES, so the only 'terrain'
it can see is the grade sheet's underside - which painted the exit-door
aperture (the landscape slice's 2D NDC clip planes (nx,ny,0,dw) have no
depth axis and cannot exclude between-eye-and-portal geometry) and slid
off the door exactly as the eye crossed grade. Membership/viewer was
exonerated by the harness in the previous commit.

Fix: TerrainModernRenderer.Draw owns its cull state (the 7th
self-contained-GL-state instance): Enable(CullFace) + CullFace(Back) +
FrontFace(Ccw), set -> draw -> restore the frame-global CW + cull-off
baseline. GL backface culling evaluates retail's per-triangle eye-side
predicate at rasterization; no shader change.

Pins:
- LandblockMeshTests.Build_AllTriangles_WindCounterClockwiseInWorldXY:
  every emitted triangle CCW in world XY across both FSplitNESW split
  directions - the winding invariant culling depends on.
- TerrainCullOrientationTests: under the production camera convention
  (LookAt up=+Z, Numerics perspective) an up-facing triangle winds CCW
  in window space from above (kept) and CW from below (culled) - guards
  FrontFace inversion, which would blank terrain from above.

Oracle note: retail's through-portal clip has NO portal-face near plane
(PView::GetClip / Render::set_view install edge planes only); nearer-
than-portal exclusion comes from the eye-side cull + cell-level
admission. No register row: this PORTS the retail mechanism, retiring
an undocumented WB-heritage deviation.

Gate pending: cellar climb (grass window gone) + outdoor sanity glance
(terrain intact from above).

Suites: App 263+1skip / Core 1443+2skip / UI 420 / Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 22:05:31 +02:00
Erik
007af1391c #108-residual apparatus: vertical cellar-ascent viewer harness - membership/viewer layer EXONERATED
The handoff's 'eye-below-grade membership demote' diagnosis is REFUTED.
The harness drives the production stack headlessly per step of the
A9B4 corner-building cellar ascent (0x0174 -> 0x0175 -> 0x0171, path
fitted from the cellar-up live captures): FindCellList on the
foot-sphere center for the player pick + the PhysicsCameraCollisionProbe
SweepEye chain mirrored verbatim (AdjustPosition at pivot ->
ResolveWithTransition IsViewer|PathClipped|FreeRotate|PerfectClip ->
both fallbacks) with per-step branch attribution.

Result: 0 outdoor/null viewer resolutions while the eye is below grade,
0 sweep failures, 0 fallback branches, across boom distance {2.61, 5}
x damping lag {0, 0.3 m}. The viewer enters the main-floor room at eye
z 94.01 - exactly as the head pops above grade (the stairwell portal
sits AT grade), matching the user's report wording. The root is
INTERIOR for the whole grass window; #108-residual is render-side
(fix in the next commit). Tests stay as the healthy-layer
characterization pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 22:05:06 +02:00
Erik
bf965000da handoff: add the M1.5 dungeon-demo exit gate to the next-session work order
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:21:51 +02:00
Erik
5622d56fe8 handoff: night session - 9 user-gated closes; NEXT = #108-residual (queue, the #131 entity-first lesson, apparatus, pickup prompt)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:02:10 +02:00
Erik
49cffe6565 close #131 + #132 (user-gated) + CLAUDE.md current-state ledger refresh
#131: 'Ok now it works' (fix 4, d208002). #132: both sides gated. #124 closed earlier same session. CLAUDE.md open ledger now: #108-residual, #116, #127, #125 sticky-drop debt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:55:13 +02:00
Erik
d208002bf8 fix #131 (root cause 4, structurally forced): look-in cells draw their DYNAMICS - the town portal is a server object in the hall's porch cell
The headless replay of the captured indoor frame proved the look-in flood ADMITS the porch 0x017A (Diagnostic_LookInFlood_AdmitsHallPorchFromCottage: 14 cells). So the portal (a SERVER object - the teleport proves it - with ParentCellId 0xA9B4017A) routes to partition.Dynamics and draws NOWHERE under an interior root: dynamics-last viewcone-culls it (the main cone has no look-in cells) and post-seal it would z-fail beyond the root's door plane (the #118 lesson). This is AP-33's own recorded deferral - 'look-in DYNAMICS are not drawn' - the deferred case was the most-stared-at object in town. Outdoors the merge path puts the porch in the main cone -> drawn -> 'appears when I walk out'.

Fix: DrawBuildingLookIns pass 2 draws look-in-cell dynamics with the statics (whole, AP-33 over-include) and their emitters ride the same DrawCellParticles call. No double-draw: dynamics-last keeps culling them; DrawDynamicsParticles only sees its cone survivors. #124 CLOSED by user gate same session. AP-33 row updated. Suites: App 261+1skip / Core 1439+2skip / UI 420 / Net 294 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:52:34 +02:00
Erik
47f32cd45c fix #131 (root cause): look-in cells draw their emitters - the cell-particles pass was missing from the #124 sub-pass
The teleport capture pinned it: walking into the portal flipped pCell to 0xA9B4017A - the hall's PORCH EnvCell. The swirl emitter is owned by a static inside another building's cell. Outdoors the merge path runs the main per-cell pass incl. DrawCellParticles -> visible; under an interior root the #124 look-in sub-pass drew shells + statics but had no cell-particles call. Retail's nested DrawCells draws objects WITH their emitters (DrawObjCellForDummies pc:432878+). Fix: DrawBuildingLookIns pass 2 invokes DrawCellParticles per look-in cell with its static bucket. The owner-cone verdicts were geometrically correct all along (0xC0A9B462 = a porch torch); fixes 1-2 were real-but-adjacent (the unattached pass plugs an independent hole; the alpha deferral fixed #132).

Suites: App 260+1skip / Core 1439+2skip / UI 420 / Net 294 green. Awaiting the swirl gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:44:24 +02:00
Erik
a07279dfd1 131 probe: print matched emitter owner ids + the setup-dump diagnostic (portal identification capture)
unattached=0 in the last capture refuted the unattached hypothesis (the fix-1 pass is vacuous); the swirl outdoors rides a MATCHED attached emitter, so its owner is an OutdoorStatic keyed by a synthetic id. The matched-ids dump on an inside-vs-outside capture pair names the owner: the id that flips. Issue131SetupProbeTests dumps the outstage candidate setups from the dat.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 19:48:53 +02:00
Erik
87afbc0a42 fix #132 (outdoor sibling): outdoor attached scene emitters move to the post-frame pass; sharpen the #131 probe
User gate on 20d1730: the candle is FIXED indoors ("now the candle
light is visible when I'm in the house when it is in front of the
opening") and the OUTDOOR sibling surfaced exactly as AP-34 recorded
("when I go out it is not showing unless I turn so the angle doesn't
put it in front of the opening"): under an OUTDOOR root the merged
building interiors draw AFTER the landscape stage (DrawEnvCellShells),
so a slice-drawn flame is overpainted by a punched aperture's interior
behind it.

Fix: outdoor roots SKIP the late-slice Scene-particle draw; attached
outdoor-static scene emitters draw in the POST-FRAME pass alongside the
T3 unattached pass, where depth is complete and flames composite
correctly against interiors. The owner-id set carries over from the
late slice (single full-screen slice outdoors); cell-pass and
dynamics-pass emitters keep their own passes (their owners are never in
the outdoor-static id set - no double-draw). Interior roots keep the
late-slice draw (their stage ends with the clear + seal discipline).
AP-34 row updated (the outdoor residual is now covered; the remaining
residual is translucent MESH batches within stage draw calls).

Portal swirl (#131): the user's "same results" on 20d1730 KILLS the
look-in-erasure hypothesis for the portal - the mesh now draws after
the look-ins and is still missing indoors. No further speculative fix;
the [outstage] probe now prints each outside-stage dynamic's
SourceGfxObjOrSetupId (portals have distinctive setups) and
[outstage-pt] lists up to 12 distinct UNMATCHED attached emitter owner
ids - the next capture identifies whether the portal entity reaches the
through-door draw at all, and where its emitters point.

Suites: App 259+1skip / Core 1439+2skip / UI 420 / Net 294 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 19:26:04 +02:00