Commit graph

434 commits

Author SHA1 Message Date
Erik
8a42066192 feat(D.5.1): parse item IconOverlay/IconUnderlay from CreateObject -> faithful icon overlay layer
The CreateObject optional-tail walker previously stopped at UseRadius (~20 fields
before IconOverlay). This left ItemInstance.IconOverlayId/IconUnderlayId always 0,
so IconComposer's underlay/overlay layers were never drawn on toolbar icons.

Exact field order verified against ACE WorldObject_Networking.cs:87-219 (the
serializer is the authority; acdream connects to a local ACE server):
  UseRadius → TargetType(u32) → UiEffects(u32) → CombatUse(sbyte) →
  Structure(u16) → MaxStructure(u16) → StackSize(u16) → MaxStackSize(u16) →
  Container(u32) → Wielder(u32) → ValidLocations(u32) →
  CurrentlyWieldedLocation(u32) → Priority(u32) → RadarBlipColor(u8) →
  RadarBehavior(u8) → PScript(u16) → Workmanship(f32) → Burden(u16) →
  Spell(u16) → HouseOwner(u32) → HouseRestrictions(variable RestrictionDB) →
  HookItemTypes(u32) → Monarch(u32) → HookType(u16) →
  IconOverlay(PackedDwordKnownType) ← CAPTURE →
  IconUnderlay from weenieFlags2 bit 0x01 ← CAPTURE

RestrictionDB handled correctly: Version(u32) + OpenStatus(u32) + MonarchId(u32)
+ count(u16) + numBuckets(u16) + count×8 bytes entries. Length-aware skip, not a
fixed constant.

weenieFlags2 is now CAPTURED (not skipped) when IncludesSecondHeader
(objDescFlags bit 0x04000000) is set, so the IconUnderlay bit can be tested.

The entire extended walk is inside try/catch: truncated packets degrade to
IconOverlayId=0 / IconUnderlayId=0 (no overlay drawn), never corrupting.

Threading: CreateObject.Parsed → WorldSession.EntitySpawn → GameWindow
OnLiveEntitySpawned → Items.EnrichItem — both ids thread through all three
seams. EnrichItem extended with optional iconOverlayId + iconUnderlayId params
(defaulted 0, backward-compatible).

No change to IconComposer or ToolbarController (they already consume the ids).

Tests: 4 new CreateObject tests (IconOverlay only, overlay+underlay, no-overlay
regression, intermediate-fields cursor arithmetic). Full suite: 0 failures,
2636 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:34:47 +02:00
Erik
998a0bd408 docs(D.5.1): clarify EnrichItem fires-on-found (review nit) 2026-06-16 21:52:15 +02:00
Erik
f8da98b67f feat(D.5.1): ItemRepository.EnrichItem (icon/name/type from CreateObject)
Adds EnrichItem(objectId, iconId, name, type) — enriches an existing
stub created from PlayerDescription with the fuller data carried by its
CreateObject message. Returns false when the item isn't tracked yet
(phase 1: enrich-existing only). Raises ItemPropertiesUpdated on success
so bound widgets (the toolbar) re-render.

Two xUnit tests: enrich-existing updates IconId/Name/raises event (true),
unknown-id returns false.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:48:44 +02:00
Erik
5ac9d8c19c merge: bring main into claude/hopeful-maxwell-214a12 (LayoutDesc importer branch)
main was 65 commits ahead of this branch's fork point. Only conflict was the
divergence register: both sides appended an 'AP-32' row. Resolved by keeping
main's AP-32..AP-36 (cell-shell lift, look-in cells, alpha deferral, dungeon
streaming, point lights) and renumbering the importer's row to AP-37; AP header
count -> 37. GameWindow.cs auto-merged cleanly. Verified: AcDream.App builds
0/0; AcDream.App.Tests 354 passed / 1 skipped / 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:19:15 +02:00
Erik
6f81e2c91d fix(render): hide editor-only placement markers in dungeons — port retail's degrade-to-nothing (#136)
The "red cone" (+ green floor petals) in the 0x0007 Town Network dungeon is a dat
EnvCell static object (Setup 0x02000C39 / GfxObj 0x010028CA) using pure red/green
MARKER textures (0x08000109 / 0x0800010A). It is an EDITOR-ONLY placement marker:
its DIDDegrade table 0x11000118 is {slot0 Id=mesh MaxDist=0, slot1 Id=0 MaxDist=FLT_MAX},
i.e. visible ONLY at distance 0 (the WorldBuilder editor origin) and degraded to
GfxObj id 0 (nothing) at any real distance. retail's distance-based degrade
(CPhysicsPart::UpdateViewerDistance 0x0050E030 -> Draw 0x0050D7A0) therefore never
draws it in the live client.

acdream's render pipeline is extracted from WorldBuilder, which (being an editor)
renders every cell static's base mesh directly and has NO degrade handling at all
(zero DIDDegrade references in references/WorldBuilder) — so acdream inherited the
"show the marker" behavior and drew it forever. It only became visible now because
the #135 login-into-dungeon fix drops the player at the exact saved spawn next to it.

Fix: GfxObjDegradeResolver.IsRuntimeHiddenMarker() detects the editor-marker pattern
(HasDIDDegrade + Degrades[0].MaxDist==0 + a degrade entry with Id==0). The EnvCell
static-object hydration (GameWindow ~5793) skips such GfxObjs — whole-stab for bare
GfxObj stabs, per-part for Setup stabs (an all-marker Setup then drops via
meshRefs.Count==0). This is the faithful equivalent of retail's runtime degrade for
static geometry (always viewed at distance > 0); real LOD objects (slot0.MaxDist>0)
and degrade-to-real-mesh objects are untouched.

Diagnosis was extensive (geometry-not-VFX via particle-off; texture-not-lighting via
flat-ambient frame dumps; per-surface runtime decode pinned the red/green marker
surfaces; a draw-time probe pinned the dat-static entity id; a dat dump of the Setup +
degrade table confirmed the editor-marker pattern). Verified live via a frame dump:
the red cone + green petals are gone, all real dungeon decorations still render.
4 new GfxObjDegradeResolver unit tests cover the marker / normal-LOD / no-table /
degrades-to-real-mesh cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 19:03:08 +02:00
Erik
66888d2c8e fix(textures): DecodeSolidColor null-safe against null ColorValue
A Base1Solid (or OrigTextureId==0) Surface can carry a null ColorValue;
DecodeSolidColor dereferenced it (color.Alpha) and threw NullReferenceException.
It is called directly from TextureCache.DecodeFromDats, OUTSIDE
DecodeRenderSurface's try/catch, so the NRE crashed the whole client. Surfaced
by the D.2b chrome prove-out feeding UI surface ids. Guard null -> Magenta
(the decoder's existing "undecodable" sentinel). Test added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:36:07 +02:00
Erik
3b93f91ebe feat(A7): LightBake Core — verified per-vertex static-light burn-in (foundation, not wired)
The faithful fix for the spotty dungeon/house/outdoor lighting is retail's per-vertex
static-light bake (D3DPolyRender::SetStaticLightingVertexColors 0x0059cfe0), NOT a
per-pixel ramp. This lands the GL-free Core: LightBake.PointContribution /
ComputeVertexColor port calc_point_light (0x0059c8b0) VERBATIM — verified against a
clean Ghidra decompile (the BN pseudo-C is x87-mangled): half-Lambert wrap with
LIGHT_POINT_RANGE=0.75 (0x007e5430), the distsq>1 norm branch, the per-channel
min-to-color clamp, and the final [0,1] clamp. static_light_factor=1.3 (0x00820e24)
is already folded into LightSource.Range by LightInfoLoader.

7 conformance tests (hand-derived golden values) green. NOT wired yet — the
integration (a per-vertex colour attribute on the cell mesh + the bake driver keyed
on envCellId + the shader consumption) is the remaining A7 work; see ISSUES.md A7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:27:45 +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
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
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
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
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
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
eeb1c59ded file #131 (portal swirl gone through doorways) + #132 (candle flame vs aperture background) + the [outstage] capture probe
Two user reports from the #124 gate session, both axioms:
- #131: "the portal swirl is missing, when I look out from inside a
  house. Appears when I walk out again." Mechanism frame: under an
  interior root an outdoor dynamic's particles draw ONLY via the
  landscape slice's Scene pass (#118 outside-stage routing; #121
  excludes them from the last-pass particle callback) - if any link
  fails, the swirl draws nowhere exactly when indoors. Desk-exonerated
  already: filter key conventions uniform, the routing predicate
  correct, sphere from vertex bounds.
- #132: "I have a candle ... when a wall is behind it it shows, but if
  I turn a bit and the opening through a house is behind it candle
  light disappears." Background-dependent => per-pixel depth/blend at
  the aperture region, not owner culling. Possible overlap with the
  #124 look-in sub-pass (new pre-clear content in those pixels) - the
  pre-77cef4c check is in the issue.

Apparatus (env-gated, zero cost off): ACDREAM_PROBE_OUTSTAGE=1 ->
[outstage] per-slice outside-stage routing + cone verdict per dynamic
(print-on-change, RetailPViewRenderer) + [outstage-pt] slice
Scene-particle id set + live attached-emitter match count (GameWindow).
One capture standing inside looking at the portal pins which link
breaks.

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:54:56 +02:00
Erik
0cb97aa594 UN-2 RESOLVED: GetMaxSpeed x4 is byte-verified retail; doc-comment was the misread
The register's UN-2 row recorded a contradiction: the GetMaxSpeed XML doc
claimed the bare run rate was retail-correct (~5.9 m/s catch-up, calling
the xRunAnimSpeed multiply a misread), while the implementation multiplied
by RunAnimSpeed citing ACE. Settled against the binary, not the pseudo-C:

- BN pseudo-C (acclient_2013_pseudo_c.txt:305127) renders get_max_speed as
  void with a bare `this->my_run_rate;` because it DROPS x87 instructions.
- Disassembling the PDB-matched v11.4186 binary at VA 0x00527cb0: all THREE
  return paths end `fld <rate>; fmul dword ptr [0x007C8918]; ret`, and the
  .rdata dword at 0x007C8918 is 4.0f. Sibling get_adjusted_max_speed
  (0x00527d00) carries the same trailing fmul. Verifier committed at
  tools/verify_un2_fmul.py (PE parse + byte decode, rerunnable).
- Retail paths: weenie null -> 1.0 x4; InqRunRate ok -> queried x4;
  InqRunRate failed -> my_run_rate x4. ACE MotionInterp.cs:665-676 matches.

Changes:
- Doc-comment rewritten: the implementation is retail-correct; the catch-up
  speed 2 x get_max_speed ~= 23.5 m/s at run 200 IS retail. The 1-Hz
  remote-blip symptom the old comment attributed to this multiply is
  therefore UNEXPLAINED by it (if it recurs: #41 family, not this).
- Weenie-null path aligned to retail's LITERAL 1.0 default (was MyRunRate).
- Tests re-pinned to the three retail paths (the old NoWeenie test pinned
  the non-retail fallback).
- Register: UN-2 row deleted per the retire rule (6 -> 5 UN rows);
  shortlist renumbered.

This is the 2nd confirmed instance of the BN x87-dropout artifact class
(memory: feedback_bn_decomp_field_names) deciding a register row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:17:50 +02:00
Erik
be03146e30 #112 ROOT CAUSE: outdoor-seed pick lacked retail's growing-array walk - threshold tick-skip became absorbing
The instrumented capture (cottage-112-capture1.log) + dat replay pinned
the transparent-cottage mechanism end to end:

1. The A9B3 cottage's entry cell 0x104 is a 0.22 m-wide THRESHOLD band
   (x 184.68->184.46 at y~82). A running player (~13-16 cm/tick at
   30 Hz) can cross it BETWEEN two physics ticks - the tick where the
   centre is inside 0x104 never happens.
2. Our outdoor-seed branch ran CheckBuildingTransit over a landcell
   snapshot and STOPPED - building-admitted entry cells were never
   expanded. The tick after the skip (centre in 0x100, a deep room not
   building-portal-adjacent) found no containing candidate -> the pick
   kept the outdoor landcell FOREVER (absorbing): the user walked the
   whole interior classified outdoor (render faithfully drew an outdoor
   frame = transparent walls), promoting only on touching
   portal-adjacent 0x102's own volume minutes later (captured:
   0xA9B3003C -> 0xA9B30102 with no transitions in between).
3. Retail cannot strand: CObjCell::find_cell_list (0x0052b4e0) runs ONE
   growing-array walk for EVERY seed (0052b576-0052b5ab,
   cells[i]->find_transit_cells vtable dispatch over the GROWING array)
   - the landcell's building bridge admits 0x104 (the foot sphere still
   overlaps the band one tick after the skip) and the walk expands
   0x104's portals to 0x100 where containment wins. Recovery fires one
   tick after any skip.

Fix: BuildCellSetAndPickContaining now runs retail's single growing
walk for both seeds with per-cell-type dispatch (landcells ->
CLandCell::find_transit_cells 0x00533800 -> CSortCell 0x00534060 ->
check_building_transit 0x0052c5d0; envcells -> FindTransitCellsSphere
with the straddle gate + once-per-walk outside add). The old indoor
branch behavior is preserved (seed at index 0, hysteresis, straddle-
gated outdoor pick); the outdoor branch gains the expansion + the
indoor branch gains the retail landcell bridge dispatch for
straddle-admitted landcells.

Pins (dat-backed, Issue112MembershipTests): tick-skip recovery one tick
past the threshold (RED pre-fix); run-speed entry replay across tick
phases never strands outdoor; threshold-gap outdoor-seed keeps outdoor
(over-fix guard); entry-walk replay diagnostic prints the full
promotion chain (0x3C -> 0x104 -> 0x100 -> 0x103 -> 0x100 -> 0x102).

Suites: App 246+1skip / Core 1438+2skip / UI 420 / Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:35:52 +02:00
Erik
6a9b529113 #119: entity bounds from dat vertex data - works for every case, not just multi-part
The 1ca412d part-offset expansion fixed the staircase but still rested
on the 5 m promise one level down: a SINGLE part whose mesh extends
more than 5 m from its own origin (offset 0 -> box +-5 m) keeps the
gaze-dependent vanish. Per the user's mandate ("it must work for every
case"), the bound now derives from the dat VERTEX data - the same
vertices that get drawn - so no synthetic containment promise remains.

Oracle context (read this session): retail has NO whole-entity
visibility volume - CPhysicsPart::Draw (0x0050d7a0) viewcone-checks
each part's dat-authored CGfxObj.drawing_sphere at the part's own
world position (RenderDeviceD3D::DrawMesh 0x005a0860). Retail's bound
IS data; ours was a promise. Our per-ENTITY granularity stays (a
deliberate batching-era choice, WB-owned per the inventory) but the
volume is now data-derived and conservative: visually identical by
construction, never culls what retail would draw.

- GfxObjBounds: per-GfxObj vertex AABB, cached by id (parts repeat
  heavily); LocalBoundsAccumulator: union of part-transformed AABB
  corners (conservative-correct under any affine transform).
- WorldEntity.SetLocalBounds + RefreshAabb preferred path: rotate the
  root-local bounds' 8 corners into world axes + DefaultAabbRadius
  margin (absorbs animated-pose drift vs the rest-pose bounds; keeps
  small objects at their historical box size). Offset heuristic stays
  as the fallback for boundless fixtures.
- All four hydration sites wired (outdoor stabs, scenery incl. baked
  scale, interior cell statics, server live spawns).

Tests: tall-single-part coverage (the case 1ca412d could not see),
rotation-following, accumulator union. Suites: App 246+1skip / Core
1434+2skip / UI 420 / Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 22:39:05 +02:00
Erik
1ca412d07b #119: entity bounds must cover the parts - the gaze-dependent staircase vanish
User re-gate after 2163308/987313a: run-from-town stairs FIXED, barrel
GONE - but the stairs still vanish by VIEWING ANGLE (visible climbing
down, gone climbing up; same at the tower top). The gate3 probe data
exonerates everything downstream: the entity always draws with correct
batches when it reaches the dispatcher (cache hit:119, restZ correct,
zero WALK-REJECTs, never clip-culled) - so the vanish lives in the one
gaze-dependent gate the probe cannot see: the bounds-based cullers.

WorldEntity.RefreshAabb was a fixed +-5 m box around the entity ANCHOR.
The staircase's 43 parts spiral 15 m ABOVE the anchor, and BOTH
visibility gates derive from the box: the dispatcher's per-entity
frustum cull AND RetailPViewRenderer.EntitySphere (the viewcone sphere
= this box's bounding sphere). Looking up the spiral put the anchor's
neighborhood out of view -> the whole entity culled while 15 m of it
stood in front of the camera; looking down kept the anchor in view ->
visible. Exactly the reported asymmetry.

Fix: expand the box by the largest MeshRef part-translation magnitude
(rotation-invariant, so entity.Rotation needs no handling; identity-
part entities get offset 0 - behavior unchanged; scenery scale is
already baked into the part transforms).

Suites: App 246+1skip / Core 1431+2skip / UI 420 / Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:58:17 +02:00
Erik
3cf6bcc219 #119 decisive probe: ACDREAM_DUMP_ENTITY one-shot entity dump (H-A/H-B/H-C discriminator)
The broken-state log (user-session-capture2.log) shows meshMissing=0 /
entSeen==entDrawn WHILE broken stairs are on screen - the staircase is
DRAWN WRONG, not missing. This probe discriminates the three live
hypotheses in ONE launch (handoff 2026-06-11 s4):

- HYDRATE dump (GameWindow.BuildInteriorEntitiesForStreaming): per-part
  placement-frame translations + dropped-part accounting at the MOMENT
  MeshRefs are constructed. H-A (SetupMesh.Flatten identity fallback /
  silent gfx-null part drops under degraded dat reads) shows here as
  zero translations or built<43.
- DRAW dump (WbDrawDispatcher, first tuple per entity): live MeshRefs
  translation summary + per-part loaded flags + Tier-1 classification
  cache state (batch count + RestPose translation summary), re-emitted
  compactly on signature change. H-B (partial/stale cached batch set)
  shows as correct translations + odd batch count.
- WALK-REJECT lines (rate-limited): attributes 'entity never reaches
  the draw loop' to the specific gate (visibleCellIds/frustum).
- Correct everything -> H-C (draw-side compose), instrument next.

Targets: ACDREAM_DUMP_ENTITY=0x020003F2,0x020005D8 (the 43-part spiral
staircase Setup + the wall barrels; H-A predicts the user's 'barrel' IS
the collapsed staircase). Probe is inert when the env var is unset.
Parser in RenderingDiagnostics (diagnostic-owner pattern) + 5 unit tests.

Suites: App 242+1skip / Core 1427+2skip / UI 420 / Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:01:08 +02:00
Erik
120aeff720 #126 RETAIL-CORRECTED: restores commit the server Z - retail never re-derives position from surfaces
The user caught the process failure: two snap fixes were written without
reading retails restore code. The named decomp settles it -
CPhysicsObj::SetPositionInternal (0x00515bd0, pc:283892-283945) treats
the supplied Position as INPUT: AdjustPosition resolves which cell
CONTAINS it, CheckPositionInternal/find_valid_position VALIDATES it
through the collision transition, and the no-cell case goes
store_position + GotoLostCell. There is NO terrain or surface
re-grounding anywhere in the restore path. Trust + validate.

Both prior shapes diverged: grounding outdoor claims to terrainZ warped
a roof-deck logout (ACEs authoritative z=127.2 on the AAB3 tower)
through the roof into the building volume -> the transparent-interior
spawn on every login; the cell-walkable scan that replaced it missed
shell-geometry decks entirely (no EnvCell owns the deck surface) and
failed silently - the user logged in transparent at the tower bottom
again.

Fix: a zero-delta outdoor restore above terrain commits the claims Z
verbatim ([snap] line says so); the first physics tick validates and
settles against the REAL collision world (the BR-7 building channel
covers the deck). max(terrain, z) stays as the under-terrain sanity
bound - our recoverable stand-in for retails lost-cell machinery
(documented divergence, same class as the #107 demote).

Suites: App 242+1skip, Core 1422+2skip, UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:22:17 +02:00
Erik
b94a7e8017 #126: outdoor restore grounds onto elevated walkables, not through them
A zero-delta RESTORE of an outdoor claim standing far above terrain
(logged out on a building roof deck - the AAB3 tower 0x010A slab at
z=127.2 over terrain 112) was grounded to TERRAIN unconditionally,
warping the player through the roof into the building interior,
outdoor-classified -> the transparent-interior spawn the user hit on
every login while the save sat on the roof. Retail restores settle via
AdjustPosition onto real surfaces, not the heightmap.

Fix: the snap outdoor branch, zero-delta shape only, when the claim z
exceeds terrain by more than step height: ground to the nearest CELL
WALKABLE at/below the claim z (the #111 WalkableFloorZNearest query -
real floors only, never the ceiling soup), keeping the outdoor cell id
(honest: a deck-stander center sits above the slab cell BSP - the same
state the user played in all afternoon). GfxObj-shell roofs without
cells not covered - file if a real case shows.

Suites: App 242+1skip, Core 1422+2skip, UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:02:48 +02:00
Erik
a974504e6e #119-residual: [viewer] capture probe - the capture half of the tower capture-replay loop
One line per change of (root cell, flood size, OutsideView polys, player
cell), with the projection eye at mm precision on every line
(ACDREAM_PROBE_VIEWER=1, print-on-change, silent while stable). The
tower-ascent harness replays the captured production (eye, root) pairs
deterministically - replacing the synthetic helix that proved unphysical
in the roof-lip band (the real collided camera may never reach it).

Suites: App 238+1skip, Core 1422+2skip, UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 18:41:05 +02:00
Erik
ca4b482f8b T6 (BR-7) C4: straddle-only outside-add (A6.P5 widening DELETED) + #90 stickiness removed
The two remaining flagged workarounds retired, per the BR-7 plan +
the WF1 [MEDIUM] correction (re-gate, do NOT delete the outside-add):

1. A6.P5 hasExitPortal topology widening DELETED. Outdoor cells enter the
   collision cell array ONLY on the retail straddle gate - |dist| <
   radius + F_EPSILON against an exterior portal plane
   (CEnvCell::find_transit_cells Ghidra 0x0052c820, gate 0052c9d6,
   live-binary verified) - the same flag that already gated the
   membership pick (#112 rider). The widening existed so outdoor-
   registered doors stayed findable from indoor cells under the old flat
   registry query; with per-cell shadow lists the door is found in the
   straddle-admitted outdoor cell's own list (tick-13558 pin holds).
   The hasExitPortal out-param + plumbing deleted from
   FindTransitCellsSphere; the AddAllOutsideCells call in
   BuildCellSetAndPickContaining re-gated on exitOutsideStraddle
   (once-per-walk = retail CELLARRAY.added_outside).

2. #90 ResolveCellId sphere-overlap stickiness REMOVED (the 4ca3596
   workaround, deferred-to-A6.P4 in the physics digest). It was dead
   code: the method's only caller is FindEnvCollisions' cache-null TEST
   fallback, and the indoor branch (where the stickiness lived) required
   a non-null DataCache. Production membership flows exclusively through
   the collide-then-pick advance whose ordered-array hysteresis (current
   cell at index 0, interior-wins-break) is the retail mechanism the
   workaround approximated. ResolveCellId reduced to the bare
   prefix-preserving outdoor re-derive, documented test-only.

Test updates (pins of the deleted behaviors inverted to retail):
- A6P5_BuildCellSetFromIndoorStart_ReachesDoorOutdoorCell (asserted the
  topology widening verbatim) -> DeepInteriorSphere_NoStraddle_
  AddsNoOutdoorCells: a deep-interior sphere admits NO outdoor cells.
- A6P5_BuildCellSetFromAlcove... -> AlcoveSphere_StraddlesExitPortal_
  ReachesDoorOutdoorCell (the captured alcove position genuinely
  straddles - the retail-positive half).
- Issue112MembershipTests straddle pin + the second-sphere straddle test
  updated to the single-flag signature.

Suites: Core 1416/0/2, App 225, UI 420, Net 294 - green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 14:44:49 +02:00
Erik
dbfbf8506c T6 (BR-7) C3: per-cell shadow architecture - flood registration, building channel, per-cell query; b3ce505 stopgap DELETED (closes #99)
The A6.P4 port, fused into one installment per the BR-2 half-port lesson
(registration and query are co-dependent: flood-registering shells under
the old radial query would re-open #98 through the vestibule).

REGISTRATION (ShadowObjectRegistry rewritten):
- Register/RegisterMultiPart/UpdatePosition compute the cell set via
  CellTransit.BuildShadowCellSet (the C2 find_cell_list flood) seeded by
  the entity's m_position cell id; the private 24m XY-grid rectangle and
  its single-landblock clamp are deleted. Flood spheres follow retail's
  CylSphere rule (base point + cyl radius, cap 10; BSP bounding-sphere
  fallback - Ghidra 0x0052b9f0). Statics flood with the do_not_load
  prune; dynamics (server spawns, isStatic:false) without.
- Keep-when-empty (SetPositionInternal num_cells gate, pc:283540): a
  failed flood leaves the previous registration in place.
- RefloodLandblock: streaming-race hook re-runs the flood when a
  landblock's cells hydrate (retail init_objects -> recalc_cross_cells,
  Ghidra 0x0052b420/0x00515a30); wired at GameWindow's hydration tail.
- GameWindow sites pass the server position's full cell id as the seed
  (spawn + UpdatePosition); the five static sites pass ParentCellId.

BUILDING CHANNEL (CSortCell.building shape):
- Building SHELLS are not shadow objects in retail (only caller of
  find_building_collisions is CSortCell::find_collisions 0x005340aa;
  one building per origin landcell, init_buildings 0x0052fd80 verified
  verbatim + ACE cross-ref). IsBuildingShell entities skip the registry;
  Transition.FindBuildingCollisions runs the shell part-0 BSP off
  cache.GetBuilding(cellId) with bldg_check set around it
  (find_building_collisions 0x006b5300), CollidedWithEnvironment on
  non-Contact non-OK. BuildingPhysics.ModelId = pre-resolved part-0
  GfxObj (0x02 Setups resolved at the CacheBuilding site).
- Placement/ethereal weakening: BSPQuery Path 1 passes center_solid=0
  when BldgCheck && HitsInteriorCell (BSPTREE::find_collisions 0x0053a82e
  + placement_insert 0x005399d8) so doorway crossings don't hard-fail
  against shell solids. SpherePath gains both retail fields;
  HitsInteriorCell is rebuilt at every cell-array build
  (build_cell_array reset 0x00509ef2 + find_cell_list/check_building_
  transit set sites).

QUERY (retail per-cell order, transitional_insert 0x0050b6f0):
- TransitionalInsert per attempt: env -> building (LandCell only) ->
  objects on the PRIMARY cell, then on OK the check_other_cells pass
  (env -> building -> objects per OTHER overlapped cell) + the
  carried-cell advance - the advance now happens AFTER all per-cell
  object passes (the WF1 ordering divergence), with Adjusted/Slid
  feeding the retry exactly like retail's OK_TS case.
- FindObjCollisionsInCell = CObjCell::find_obj_collisions (0x0052b750):
  iterate ONLY the asked cell's list. DELETED: the radial 9-landblock
  sweep, the +5m query pad, the b3ce505 indoor-primary gate, and the
  isViewer exemption (the camera is bounded by interior cell-BSP env
  collision - retail's own channel; CameraCornerSealReplayTests pins it
  against real dat, and the new building-channel camera test pins the
  outdoor stop).

TESTS: Core 1416/0/2 (was 1398 + 4 pre-existing #99-era fails + 1 skip),
App 225, UI 420, Net 294 - all green.
- 3 of the 4 #99-era reds flipped green as designed: the door apparatus
  (Apparatus_Grounded_50cmOffCenter_FrontApproach_Blocks) and tick-13558
  (indoor walkthrough) now assert the door BLOCKS; tick-22760 pins the
  outdoor blocking invariant.
- The 4th (BSPStepUp D4) + 22760's lateral-slide delta are NOT cell-set
  problems (probes prove the door is found + BSP-only dispatched;
  BR-7 left both byte-identical) - filed as issue #116 (slide-response
  family), D4 skipped with the issue reference.
- FindEnvCollisionsMultiCellTests migrated to the public entry (the A4
  multi-cell halt now lives at the retail call site).
- New registry pins: per-cell query surface, outdoor-footprint-never-
  indoor (#98 architectural), door-outdoor-cell membership, reflood.
- CameraCollisionIndoorTests rewritten against the building channel
  (the isViewer-exemption pins died with the exemption).

Closes #99 (doors block both ways via registration-time cell membership
+ the straddle-spanning player cell array). #97 likely closed (the +5m
radial pad that produced phantom-collision candidates is gone) - verify
at T5. #98 stays closed ARCHITECTURALLY (outdoor footprints structurally
cannot reach interior cells; the cellar harness stays green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 14:37:50 +02:00
Erik
abf36e2743 T6 (BR-7) C2: BuildShadowCellSet - the registration-side portal flood
Verbatim port of CObjCell::find_cell_list (Ghidra 0x0052b4e0, pc:308742)
as invoked by calc_cross_cells / calc_cross_cells_static (0x00515230 /
0x00515160) - the flood retail runs at SHADOW REGISTRATION time, minus
the containing-cell pick (registration passes a null out-cell):

- Seed: indoor -> exactly that one cell (added even when unloaded, like
  retail's null-pointer add_cell; the walk is then skipped per the
  0052b576 seed-pointer gate); outdoor -> block-crossing
  AddAllOutsideCells.
- Growing-array walk: indoor cells via FindTransitCellsSphere
  (sphere-vs-neighbor-BSP admission; exterior straddle -> outside cells
  once per walk, retail CELLARRAY.added_outside); outdoor cells via the
  CLandCell leg (0x00533800) = add_all_outside_cells (same once-guard)
  + the building bridge CSortCell -> CBuildingObj ->
  check_building_transit (0x00534060/0x006b5230/0x0052c5d0) - how an
  outdoor-positioned door reaches the vestibule's shadow list at
  registration. No XY grid, no visibility lists (the spec's
  VisibleCellIds rule was REFUTED by the WF1 verification).
- Static prune (do_not_load_cells, 0052b66e): indoor-seeded statics keep
  only {seed} + seed.stab_list (VisibleCellIds) - also strips outdoor
  cells, matching retail (interior statics never shadow into landcells;
  outdoor spheres reach them through their own array's building bridge).

11 unit tests: seeds (indoor/outdoor/unloaded), neighbor-BSP admission,
building bridge incl. the C1 negative-portal-id gate, exterior straddle
on/off, static prune drop/keep, zero-sphere no-op.

Consumed by C3 (ShadowObjectRegistry rewrite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 14:02:08 +02:00
Erik
6ec4cde9a4 T6 (BR-7) C1: signed OtherPortalId + the >=0 building-transit gate
Retail CEnvCell::check_building_transit (Ghidra 0x0052c5d0) opens with
`if (other_portal_id >= 0)` on the SIGNED sign-extended portal id
(CBldPortal.other_portal_id is int, acclient.h:32098). Our BldPortalInfo
carried the dat reader's raw ushort and CheckBuildingTransit had no gate
at all, so a portal whose dat value is 0xFFFF (-1, "no reciprocal
portal") could admit its interior cell. BN's pseudo-C renders the
comparison unsigned - the sign-extension is Ghidra-proven (BR-7 verified
corrections, wf1-interior-collision.md).

- BldPortalInfo.OtherPortalId: ushort -> short; GameWindow construction
  reinterprets the dat ushort via unchecked((short)).
- CheckBuildingTransit: negative-id portals rejected before any sphere
  test; new multi-sphere overload matching retail's per-sphere loop
  (0052c5fe, first-hit admits) with the hits_interior_cell output
  (0052c650) the BR-7 building channel consumes next.
- Tests: negative-id skip vs positive-id admit on a leaf-root CellBSP;
  multi-sphere plumbing + zero-sphere no-op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 13:56:16 +02:00
Erik
6cba95047c BR-2 task 1: phantom-site probe (ACDREAM_PROBE_PHANTOM)
The BR-1 pre-check left the #113 phantom residual with two surviving
suspects, both cell-side: (a) flood-admitted cells whose shell draws with
a pass-all slice (NoClipSlice fallback when the assembler handed no slot,
or an assembler slot-0 scissor-fallback slice), and (b) cell entity
buckets drawn unclipped + un-viewcone'd by design.

[phantom-shell]: per shell-pass cell, print-on-change - clip-enable
state, slot presence, every drawn slice's slot + plane count with
PASS-ALL flagged. [phantom-objs]: per object-list cell, print-on-change
- entity bucket size. Env-gated ACDREAM_PROBE_PHANTOM=1, zero cost off,
throwaway (strip when the phantom closes).

Repro protocol: launch with the probe on, stand at the hall bisect spot
(world ~216,-108 looking at the AAB3 meeting hall west face) where the
phantom is visible, read which mechanism fires for stair cells
0xAAB30100..0x106. Shells pass-all -> BR-2/BR-3 close it; statics ->
BR-5 closes it.

Build green; App suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 06:27:44 +02:00
Erik
414c3deaf4 fix(phys): #112 residual - retail straddle gate for outdoor-cell admission (live-binary verified)
The oracle read the #112 residual was waiting on, settled against the
LIVE 2013 client (cdb attach, CEnvCell::find_transit_cells @ 0052c820;
BN pseudo-C was ambiguous and partly wrong per
feedback_bn_decomp_field_names - it invented portal_side tests in this
branch): retail admits outdoor transit cells from an indoor cell IFF a
path sphere STRADDLES an exterior portal polygon plane,
|dist| < radius + F_EPSILON(0.000199999995, @ 007c8c70). The flag at
[esp+18h] (set 0052c925, x87 decode fcompp/test ah,41h +
fcomp/test ah,5/jp) gates the add_all_outside_cells call (0052c9d6 je).
Graph reachability alone NEVER admits outdoor cells in retail.

Port (CellTransit):
- FindTransitCellsSphere: exitOutside now carries the retail straddle
  semantics; new hasExitPortal out carries the old topology-only flag.
- BuildCellSetAndPickContaining: the collision cell SET keeps the A6.P5
  topology widening on hasExitPortal (outdoor-registered doors must stay
  findable from indoor cells until #99/A6.P4 ships per-cell shadow
  lists - the 2026-05-25 door capture scenario), but the membership
  PICK's outdoor branch is gated on the retail flag. Membership is now
  retail-identical in both regimes: straddle -> outdoor candidates valid;
  no straddle -> outdoor ignored -> retail keep-curr. This is what stops
  deep-interior containment gaps in ANY house from demoting to outdoor
  (the #112 transparent-interior shape) - the systemic protection the
  user asked for, without house-by-house verification.

The at-doorway A9B3 gap demote is RETAIL-FAITHFUL (gap point is 0.23m
from 0x104s door plane < 0.48 foot radius -> retail straddles + demotes
+ self-heals inward): DocumentsResidual renamed to
...DemotesRetailFaithfully, expectation unchanged. New conformance pins:
deep-gap keep-curr (A9B3Cottage_GapBeyondStraddleDistance_KeepsCurrCell)
+ function-level gate semantics on real dat geometry
(FindTransitCellsSphere_ExitPortalStraddleGate_MatchesRetail).

Tests: Core 1391 green (+2) / App 224 / UI 420 / Net 294; pre-existing
4 #99-era failures unchanged; P1 membership goldens + A6.P5 door-set
tests explicitly green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 16:52:24 +02:00
Erik
2d6954ee44 fix(phys): #112 - remove the non-retail escape-hatch demote from the pick; lateral stab-graph recovery + retail keep-curr
Root cause (oracle: CLandCell::point_in_cell :316941 = terrain-poly only;
find_cell_list null-result keep-curr pc:308788-308825; CEnvCell::
check_building_transit :309827 = sphere_intersects_cell per portal-adjacent
cell): retail KEEPS curr_cell when nothing contains the centre — including
inside a house's containment gaps. Our 6dbbf95 escape hatch instead demoted
any hydrated indoor claim the sphere no longer overlaps to the outdoor
column; at the A9B3 hill cottage's real interior gap this stranded the
player outdoor-classified deep indoors, where re-promotion is portal-
adjacent-only (retail-identical) -> the outdoor flood rendered the interior
transparent (the user's "sometimes transparent" walk).

The hatch's actual target - poisoned (cell, position) SAVES - has been
handled at the SNAP by PhysicsEngine.Resolve's AdjustPosition validation
since #107/#111, so the per-tick pick reverts to retail semantics:
1. lateral recovery first - when the sphere no longer overlaps the claim,
   search the claim's stab list for a containing cell (retail
   find_visible_child_cell :311444, the same recovery AdjustPosition uses);
   the #111 adjacent-claim shape now self-heals laterally (dat-backed test:
   pick(seed 0x172) at a 0x171-interior point -> 0x171);
2. else KEEP curr_cell (retail null-result).

Two old tests asserting the hatch demote rewritten to the retail semantics
(tests-can-codify-bugs); P1 retail-golden conformance gates explicitly green
(FindCellListConformance + ThresholdPortalCrossing + CottageDoorway +
CameraCornerSeal = 11/11). New Issue112MembershipTests: the lateral-recovery
fact + a DocumentsResidual fact pinning the remaining at-doorway gap demote
(via the NORMAL outdoor-candidate path; open oracle read = retail's
add_all_outside_cells gate in CEnvCell::find_transit_cells pc:317499 -
sphere-proximity vs graph-reachability). Core 1383 + 4 pre-existing #99
failures + 1 skip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 15:03:49 +02:00
Erik
5706e0e10a fix(phys): #111 - ground the validated claim via PHYSICS walkable polygons, not the CellSurface triangle soup
Two grounding selection rules failed against live ACE restores before the
right one: (1) first-hit SampleFloorZ returned 0x171's 99.475 ceiling TOP face
over its 94.0 floor (issue111-verify2.log) - the player committed onto the
roof level, and the session's heartbeats poisoned ACE's save with z=99.475;
(2) nearest-to-reference self-confirmed that poison (the reference SAT on the
ceiling face, issue111-verify3.log). Root insight: ceiling/roof top faces are
upward-facing and XY-projectable - geometrically indistinguishable from
floors in the render-ish CellSurface soup. The PHYSICS walkable set (plane
normal.Z >= PhysicsGlobals.FloorZ over the claim's Resolved cell-local
polygons - retail BSPTREE::find_walkable's filter) contains only real floors:
PhysicsEngine.WalkableFloorZNearest transforms into the cell frame, drops on
each walkable plane under the XY, picks nearest the reference.

Verified live (issue111-verify4.log): ACE restored the roof-poisoned
(0xA9B40171, z=99.475); the snap validated the claim and grounded to
z=94.000 - the first fully clean indoor login of the arc:
[snap] claim=0xA9B40171 VALIDATED -> grounded to its walkable floor z=94.000
[cell-transit] 0x00000000 -> 0xA9B40171 pos=(155.525,12.416,94.000)

Baseline: Core 1381 + 4 pre-existing #99 failures + 1 skip; App/UI/Net green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 14:21:59 +02:00
Erik
5f1eb7c4b1 fix(phys): #111 - a validated indoor claim is authoritative at the snap; stop the whole-landblock bestCell floor-pick from clobbering it
The [snap] apparatus (issue111-snap1.log) caught the mechanism live: ACE
restored a CLEAN pair (0xA9B40171, on-floor) which AdjustPosition validated -
and the legacy Resolve then committed 0xA9B4013F instead: its bestCell floor-
pick scans EVERY CellSurface in the landblock (123 at Holtburg) for "any floor
under this XY nearest currentZ" and breaks same-height ties by iteration
order. The wrong cell then fails containment on the first movement -> outdoor
demote inside the building -> the #111 transparent interior. This free-pick
also explains the earlier "committed verbatim" mystery (the winning tie
happened to echo the input pair) AND seeded the ACE poison loop: our outbound
heartbeats reported the clobbered cell, ACE persisted it, and the NEXT login
inherited it (this run's [spawn-adjust] rejecting 0xA9B4013F is exactly that
echo coming back).

Fix (retail SetPositionInternal shape): when AdjustPosition VALIDATED an
indoor claim, the cell choice is settled - the snap grounds Z onto the
validated cell's own floor (find_valid_position's settle role, :283426) and
returns; it never re-picks the cell from floor geometry. Claims whose cell has
no own floor surface under the XY (thresholds, stair lips) fall through to
the legacy path unchanged; mover-shaped calls (delta != 0, tests) untouched.

[snap] diagnostic kept (snaps only - one line per login/teleport).
Baseline: Core 1381+4 pre-existing #99 failures+1 skip; App/UI/Net green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 14:12:21 +02:00
Erik
e4f6750e09 fix(phys): #107 gate-run regression — auto-entry hold opened on a mid-population cache read; wait for the claimed cell itself
Gate-run finding (wedge-107-gate-indoor-login.log + dat scan): ACE saved the
cell one room off (claim 0xA9B40172, position inside 0xA9B40171 — adjacent
rooms). FindVisibleChildCell corrects this fine when hydrated (proven by the
new AdjacentRoomClaim regression test), but the live entry committed the raw
claim: IsSpawnCellReady's "any cell struct in the landblock => claim is bogus,
proceed" disambiguator observed the MID-POPULATION state (interiors hydrate in
id order on the background worker; the render-thread predicate read the cache
mid-loop) and opened the gate before the claim and its stab neighbors were
cached. AdjustPosition then saw a null cell struct and silently passed the
claim through; the first movement demoted the player to outdoor inside the
house — the user-visible "transparent interior, see straight through walls"
(render is downstream of membership: an outdoor-classified viewer only sees
the interior through the doorway flood).

Fix: the hold now waits for THE CLAIMED CELL's struct, full stop
(IsSpawnCellReady simplification; HasAnyCellStructInLandblock removed).
Claims that can never hydrate are filtered by GameWindow against the dat's
LandBlockInfo.NumCells range (memoized IsSpawnClaimUnhydratable), and
PhysicsEngine.Resolve carries a loud lost-cell-equivalent safety net: an
indoor claim with NO cell struct AND NO CellSurface floor data demotes to the
outdoor landcell with a [spawn-adjust] line instead of committing raw
(retail GotoLostCell :283418; documented divergence). Partial hydration
(CellSurface present, struct pending) keeps the legacy floor-snap behavior —
HasCellSurface uses the file's masked-low-word norm so bare-id fixtures and
full-id production both resolve.

Baseline restored: Core 1381 (+4 new #107 conformance tests) + 4 pre-existing
#99-era failures + 1 skip; App 223 / UI 420 / Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:10:01 +02:00
Erik
1090189d39 fix(phys): #107 indoor-login spawn wedge — validate the server (cell,pos) pair at the player snap (retail AdjustPosition) + unfreeze same-landblock teleport arrivals + self-consistent wire pairs
Root cause (capture resolve-107-login1.jsonl + dat conformance scan): ACE
restored the player with a POISONED (cell, position) pair — cell 0xA9B40162
(one building) with a position inside 0xA9B40171 (a different building 55 m
away). Our entry snap trusted the claim verbatim: the player stood
fake-grounded for minutes (isOnGround passthrough, no contact plane, no
walkable polygon — zero-move resolves short-circuit), the FIRST movement input
ran a real transition, the pick demoted the indoor claim to outdoor
mid-building, and the player fell 2.4 m through the cottage floor onto the
terrain underneath — wedged inside the building shell. The second wedge shape
(flood-fix-gate2.log) was the PortalSpace freeze: the teleport-arrival
detection gated on `differentLandblock || farAway>100m`, an invented
heuristic — ACE's same-landblock short-hop corrections matched neither, so
PortalSpace never exited and movement input stayed frozen all session.

Four legs, all retail-anchored:

1. PhysicsEngine.Resolve (the player snap path: login entry + teleport
   arrival) now runs AdjustPosition first — retail SetPositionInternal step 1
   (acclient :283892, AdjustPosition :280009): validate/correct the claimed
   cell from the foot-sphere center BEFORE any physics. Corrections log one
   [spawn-adjust] line.
2. AdjustPosition's previously-deferred indoor seen_outside →
   adjust_to_outside sub-fallback (:280037-280046) is completed; CellPhysics
   gains the SeenOutside flag (dat EnvCellFlags.SeenOutside) cached in
   CacheCellStruct. The camera path does not reach this sub-branch in the
   gated scenarios (CameraCornerSealReplayTests green).
3. PortalSpace arrival = ANY player position update (holtburger PlayerTeleport
   handler conformant; recenter still only on landblock change). Verified
   live: ACE sent a same-lb dist=69.8 correction that the old gate would have
   frozen on — it now completes.
4. Outbound wire (cell, position) pairs are now SELF-CONSISTENT: derive the
   landblock frame from the resolver's full cell id instead of welding a
   position-derived landblock onto its low word — the old composition could
   write exactly the poisoned pair shape into ACE's character save. Plus the
   #106-gate-2 hold extension: an indoor spawn claim waits for the claimed
   cell's hydration (IsSpawnCellReady) so the validation can act — the async
   equivalent of retail's synchronous cell load.

Live verification (wedge-107-verify1.log): entry clean; ACE's same-lb teleport
correction completed (old code: permanent freeze); the teleport destination
itself carried ANOTHER poisoned claim (0xA9B40150) which [spawn-adjust]
corrected to 0xA9B40019; player fully controllable, walking across landcells.
3 new dat-backed conformance tests pin the poisoned-pair facts
(Issue107SpawnDiagnosticTests). Baseline: 1380+4 pre-existing #99-era
failures+1 skip / 223 / 420 / 294.

Pending user gate: park indoors, log out gracefully, relaunch — expect a clean
indoor spawn standing on the interior floor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:52:47 +02:00
Erik
c78720127a fix(render): #105 white indoor walls — restore WB's per-frame staged-texture flush dropped in the N.4/O-T4 extraction
Root cause: TextureAtlasManager.AddTexture only STAGES texture content (PBO
write + ManagedGLTextureArray._pendingUpdates); the actual TexSubImage3D
copies + mipmap regeneration happen in ProcessDirtyUpdates, which WB drives
once per frame via ObjectMeshManager.GenerateMipmaps() from its render loop
(WB GameScene.cs:975, just before the opaque pass). GameScene is the file we
replaced with GameWindow, so the call site was silently dropped — staged
updates only reached the GPU as a side effect of PBO growth (UpdateLayerInternal
flushes pending updates before orphaning the PBO). Every layer staged after an
array's LAST growth kept undefined TexStorage3D content behind a valid,
resident bindless sampler handle: white/garbage walls, zh==0, dat tripwires
silent — exactly the #105 signature. Only ObjectRenderBatch.BindlessTextureHandle
consumers are affected (EnvCellRenderer cell shells = indoor walls); entities
resolve via TextureCache (immediate TexImage2D) and terrain via TerrainAtlas
(immediate GenerateMipmap), which is why only indoor walls ever struck.

Fix: WbMeshAdapter.Tick() now calls _meshManager.GenerateMipmaps() after the
staged-upload drain — Tick runs before all draw passes (GameWindow OnRender),
the exact WB-equivalent position.

Evidence (ACDREAM_PROBE_TEXFLUSH=1 apparatus, kept env-gated):
- pre-fix (texflush-prefix.log): pending updates climb 0->48->...->142 and
  park at 126 across 34/34 atlas arrays at standstill, forever (19 heartbeats);
  brief dips only at PBO-growth crossings — the broken contract live.
- post-fix (texflush-postfix.log): every line after=0 — staged updates drain
  the same frame, all 34 arrays clean.

Intermittency explained: background decode-completion order shuffles which
textures land in the never-flushed tail; whether a visible wall samples one is
per-run luck. Also explains the #110 correlation: znear=0.1 makes close-up
geometry newly visible -> more prepare/upload pressure indoors -> bigger tail
-> higher strike probability. The near plane is mechanism-innocent (re-land
follows as its own commit).

Baseline maintained: App 223 / UI 420 / Net 294 / Core 1377 green + 4
pre-existing #99-era failures + 1 skip; CornerFloodReplayTests (5) and
CameraCornerSealReplayTests (2) gates green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:10:00 +02:00
Erik
682cba36f1 diag(render): §4 flap [clip-route] probe — slot routing + clip-buffer content + landscape scissor
The decisive probe between the two surviving suspects from the 2026-06-09
building-flood-merge handoff (docs/research/2026-06-09-flap-outdoor-fullworld-
building-flood-merge-handoff.md section 1), gated by ACDREAM_PROBE_CLIPROUTE=1,
all print-on-change:

- [clip-route] (RetailPViewRenderer.DrawLandscapeThroughOutsideView): the
  outside slice slot + NDC AABB + planes, the CellIdToSlot routing table, the
  region-SSBO bytes DECODED at the routed slot, and the terrain-UBO head —
  captured after SetTerrainClip + UploadClipFrame + SetClipRouting, i.e.
  exactly what the landscape draws consume. Pins/refutes suspect (b) and the
  slot-repack half of suspect (a).
- [clip-route-disp] (WbDrawDispatcher.Draw, routed draws only): per-slot
  instance histogram exactly as staged for binding=3 plus the count of
  entities dropped by ResolveSlotForFrame CULL. Pins/refutes the
  instance-routing half of suspect (a).
- [clip-route-scis] (GameWindow.DrawRetailPViewLandscapeSlice): the ACTUAL GL
  scissor enable + box read back right after BeginDoorwayScissor — the whole
  landscape pass (sky + terrain + outdoor entities + player) draws inside this
  box, so a doorway-sized box here IS the full-world kill by construction.

Code-reading findings recorded while building the probe: the landscape pass is
scissored to slice.NdcAabb end-to-end (GameWindow.cs DrawRetailPViewLandscapeSlice),
and ResolveEntitySlot CULLs server entities with null ParentCellId while routing
is active — both now directly observable under the probe.

Throwaway apparatus — strip once §4 ships.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 08:37:09 +02:00
Erik
fafe5d66e8 diag(render): §4 flap apparatus — eye-vs-terrain field + GL-state tripwire
Two probe additions from the §4 outdoor full-world flap investigation
(world turns to fog-tinted clear color at specific outdoor spots;
strobes then holds; camera rotation recovers; forward-walk triggers):

- [pv-input] gains terrZ=/eyeAbove= (terrain height under the camera
  eye) — used to REFUTE the buried-eye hypothesis (eyeAbove stayed
  +1.6..2.1 m through every held-flap block).
- [gl-state] (ACDREAM_PROBE_GLSTATE=1): snapshots the GL fixed-function
  state entering the world passes (depth, blend, cull, scissor + box,
  viewport, FBO, color/clip masks, glGetError), printing on change.
  Delivered the frame-exact flap-onset marker: the leftover scissor box
  flips from full-screen to a drifting 9x21 px doorway footprint at the
  exact frame the nearby building flood merges in (pv-input flood 1->5).

Both probes are gated and zero-cost when off. Strip with the rest of
the §4 apparatus when the flap ships.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 08:18:26 +02:00
Erik
6dbbf953c6 fix(phys): #106 gate-2 — bogus-indoor-claim recovery + spawn-ground entry hold
Gate-2 fallout chain: session 1's bare-id wedge poisoned ACE's save (the
client reported garbage cells while walking through walls), so session 2
logged in with (cell=0xA9B4013F inn interior, pos=(91.4,32.7)) — a
position that cell does not contain. Probe evidence: exactly one
[cell-transit] line all session; the player free-fell into an empty
world. Two holes, both fixed at the root:

1. CellTransit pick escape hatch — restores the #83/A1.7 + #90
   verification that lived in PhysicsEngine.ResolveCellId before the
   collide-then-pick rewrite moved membership into
   BuildCellSetAndPickContaining: an indoor current cell that IS
   hydrated but whose CellBSP no longer overlaps ANY part of the foot
   sphere is a bogus claim (corrupt save, or walked out through an
   unblocked gap). The portal BFS can never reach an exit portal from a
   cell the sphere isn't in, so no candidates exist and the claim held
   forever — wedging collision (ShadowObjectRegistry's #98 gate reads
   "indoor primary" -> outdoor object sweep skipped), wall BSP, terrain,
   and the render root. The pick now demotes to the outdoor column under
   the sphere centre (the LandDefs.AdjustToOutside result already
   computed for the pick — cross-block safe). Sphere-overlap
   (BSPQuery.SphereIntersectsCellBsp, pseudo_c:317666 -> :323267), NOT
   point-in: doorway push-back leaves the centre a few cm outside while
   the sphere still overlaps — no demotion, #90's ping-pong stays dead.
   An unhydrated cell cannot be verified — stale beats null while
   streaming hydrates (retail-equivalent: stale curr_cell kept when the
   pick finds nothing).

2. PlayerModeAutoEntry spawn-ground hold — player-mode entry now waits
   for the terrain under the spawn position to stream in
   (isSpawnGroundReady predicate, K.2 pattern). Entering earlier
   integrates gravity against an empty world: indoor-claimed spawns got
   no floor from any source and free-fell into the void; outdoor spawns
   raced hydration by ~1s every login. Retail never has this state (it
   loads cells synchronously) — the hold is the async-streaming
   equivalent of that invariant. With the hold, the entry snap
   (Resolve, stepUp=100) runs against hydrated cell floors + terrain
   and re-seats a corrupt save's claim immediately.

Tests: IndoorSeed_SphereFullyOutsideHydratedCell_DemotesToOutdoorColumn
(the gate-2 wedge shape, red pre-fix), straddle + no-BSP guards (the #90
hysteresis and stale-beats-null), TryEnter_Armed_SpawnGroundNotReady_
DoesNotFire. Full suite: 294+218+420 green; Core 1375 green + the same
4 pre-existing door/#99-era failures + 1 skip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 23:47:11 +02:00
Erik
23adc9c9df fix(phys): #106 follow-up — legacy Resolve returns full prefixed cell ids (teleport bare-id wedge)
The #106 live gate run was sabotaged by a pre-existing bug the corrective
ACE teleport exposed: PhysicsEngine.Resolve (the legacy Phase-D resolve,
still used by the teleport-arrival snap at GameWindow.cs:4869 and the
player-mode-entry snap at :11295) returned BARE low-word cell ids on
every computed exit (ComputeOutdoorCellId, bestCell.CellId & 0xFFFF,
nextCellIndex, enterCellIndex). The teleport committed 0x0000013F into
PlayerMovementController.CellId, and a bare indoor id wedges the entire
membership chain:

- GetCellStruct(0x0000013F) misses (cells are keyed full-prefix) -> no
  indoor wall BSP -> walk through walls;
- the b3ce505 #98 gate reads "indoor primary" -> outdoor object radial
  sweep skipped -> NO object collision anywhere in the world;
- BuildCellSetAndPickContaining early-returns an unresolvable id forever
  (block 0x0000 is a real far-NW map block) -> membership frozen;
- render root never resolves -> interiors draw empty when stepping in.

Probe evidence: probe-cell-106-gate.log has exactly 2 [cell-transit]
lines for the whole session, both reason=teleport — the second one
(0xA9B3003C -> 0x0000013F) is the wedge. This is the L.2e "player CellId
tracked as bare low byte" finding (2026-05-12) finally biting; prefix
survival until now was a race artifact — Resolve only preserved the full
id when the landblock had not streamed in yet (passthrough exit), which
is why login snaps usually came out prefixed.

Fix: capture the matched landblock's key in Resolve's containment loop
and return lbPrefix | (targetCellId & 0xFFFF) on the computed exit —
the same full-32-bit convention Resolve's own doc comment states for
its inputs, and what both production callers (player snaps) require.
The passthrough exits (no landblock / step-reject) still return the
caller's id unchanged.

Tests: Resolve_IndoorStay_ReturnsFullPrefixedCellId (the teleport
shape, red pre-fix) + Resolve_OutdoorStay_ReturnsFullPrefixedCellId;
Resolve_LeaveIndoorCell_TransitionsToOutdoor's unmasked
`CellId < 0x100` assertion codified the bare behavior — now masked +
asserts the prefix. Full suite: 294+218+420 green; Core 1371 green +
the same 4 pre-existing door/#99-era failures + 1 skip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 23:34:30 +02:00
Erik
7078264291 fix(phys): #106 — outdoor membership crosses landblock boundaries (LandDefs global-lcoord port)
The player's outdoor cell froze at the last in-block cell the moment they
walked over a landblock boundary (10,449-frame playerCell freeze in the
2026-06-09 capture; whole neighbouring-block interiors unenterable, plus
the running-distortion from the stale render anchor). Root cause: the
add_all_outside_cells port clamped BOTH the candidate proposal and the
find_cell_list containing-cell pick to the current landblock's 8x8 grid,
in a frame that silently assumed the current block sits at world origin.
One step over the line -> zero candidates -> FindCellSet returns
currentCellId forever.

Retail has no such clamp. Its cell math runs in a GLOBAL landcell grid
(lcoord 0..2039 spanning the map): get_outside_lcoord = blockid_to_lcoord
+ floor(blockLocalPos/24) with no bounds besides the map edge, and
lcoord_to_gid re-derives the landblock id from the lcoord's upper bits —
crossings are inherent, never special-cased.

The fix, decomp-cited throughout:
- New AcDream.Core.Physics.LandDefs: in_bounds (pc:68509),
  blockid_to_lcoord (pc:68520), inbound_valid_cellid (pc:163438),
  gid_to_lcoord (pc:163500), lcoord_to_gid (pc:171859),
  get_outside_lcoord (pc:438690), adjust_to_outside (pc:438719).
  Cross-checked against ACE LandDefs.cs; three artifacts documented and
  avoided: BN's int8_t mis-render of block_y, BN's dropped 192f
  BlockLength constant, and ACE add_cell_block's "FIXME!" same-block
  guard (an ACE divergence, not retail).
- CellTransit.AddAllOutsideCells rewritten as the faithful sphere
  variant (pc:317499 @0x00533630): adjust_to_outside re-seats the
  (cell, position) pair cross-block, check_add_cell_boundary (pc:317229)
  adds up to 3 neighbours by global lcoord, add_outside_cell (pc:317056)
  has no same-block filter. adjust_to_outside failure breaks the sphere
  loop (pc:533699 verbatim).
- BuildCellSetAndPickContaining: the outdoor containing-cell pick is now
  the global XY-column under the sphere centre (AdjustToOutside), not
  the [0,8)-clamped current-prefix reconstruction. Interior-wins order
  and current-cell-first hysteresis unchanged.
- World->block-local frame conversion via the landblock origin already
  registered in CellGraph (new TryGetTerrainOrigin); Zero fallback
  preserves the legacy anchor-block assumption for unregistered terrain.
- Cross-landblock building entry comes free: the candidate snapshot now
  contains neighbour-block landcells, so GetBuilding/CheckBuildingTransit
  fire for cottages across the line (the capture's one failing entry).

Investigated FIRST per the pickup brief: the b3ce505 #98 stopgap gate is
definitively exonerated — it is a collision-object query gate that fires
only for indoor primary cells; no membership path touches
ShadowObjectRegistry.

Tests: 31 new (25 LandDefs conformance incl. capture-geometry goldens
0xA9B40031 -> 0xA9B30038/0xA9B30034 and the northbound return; 4
AddAllOutsideCells cross-block; 3 FindCellSet membership goldens incl.
the non-anchor-frame origin conversion). Full suite: 294+218+420 green;
Core 1369 green + the 4 pre-existing door/#99-era failures + 1 skip
(unchanged from baseline).

Pseudocode + artifact notes:
docs/research/2026-06-09-landdefs-outside-cells-pseudocode.md.
Remaining acceptance: live boundary walk with ACDREAM_PROBE_CELL=1
(ISSUES.md #106).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 23:10:59 +02:00
Erik
687040ba52 feat(render-diag): add ACDREAM_PROBE_PORTAL_CHURN flag for the bounded-propagation pin
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:49:17 +02:00
Erik
d6aa526dd3 diag(render/physics): flap root-caused to physics rest µm-jitter; refute prior diagnoses
Apparatus + handoff for the indoor flap. Confirmed (primary evidence): the flap is the
portal-flood clip being µm-sensitive at the threshold, driven by a ~1-8µm jitter in the
player RenderPosition (physics resting position not bit-stable; Lerp surfaces it). REFUTES
the 2026-06-07 see-through/EnvCell/outdoor-node diagnosis (ModelId GfxObj 0x01000A2B IS the
solid exterior) AND an enqueue-once attempt (retail propagates late slices via AddToCell;
the existing PropagatesNewSlicesToExit test caught it; reverted). Adds: Build determinism
test, A8CellAudit gfxobj dump, [pv-input] 6dp probe + [render-sig] outRoot/bshell fields.
No functional fix shipped. Next: higher-precision physics rest trace -> port retail
kill_velocity/contact rest-stability. Canonical: docs/research/2026-06-08-flap-rootcause-physics-rest-handoff.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:16:12 +02:00
Erik
1405dd8e90 feat(render): indoor render WORKS — terminating portal flood + every-cell seal + look-in FPS
Checkpoint of the unified retail-faithful indoor render. The two-week HANG/grey is fixed and the
interior seals (live-verified by the user). Commits the session render-rewrite foundation together
with the fixes that made it functional.

- HANG fix: PortalVisibilityBuilder.Build portal flood did not terminate (the faithful ProjectToClip
  near-side clip drifts per round, defeating the CellView dedup; the BFS had no bound after U.2a removed
  MaxReprocessPerCell). Fix = drift-tolerant snapped/canonical CellView.Add dedup (PortalView.cs) plus
  restored MaxReprocessPerCell=16 bounded re-enqueue (PortalVisibilityBuilder.cs). Re-enqueue is kept
  (load-bearing for late-slice propagation, Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit);
  only its count is capped. CellViewDedupTests added.
- Seal (DrawCells Task 2): RetailPViewRenderer.DrawEnvCellShells draws EVERY visible cell via
  IndoorDrawPlan.ShellPass (was gated on the ClipFrameAssembler slot filter, leaving slot-less cells grey).
- Look-in FPS: GameWindow exterior look-in candidates limited to the player landblock +-1 (was all ~81
  loaded LBs iterated every outdoor frame). No behaviour change (far cells were >48m, already culled).

Remaining dominant issue = the FLAP at transitions: viewer-cell metastability (render roots at the
camera-eye cell, which oscillates outdoor-indoor as the 3rd-person boom drifts across the doorway,
confirmed in render-sig). SEPARATE fix, NOT the DrawCells port. Full handoff + flap fix plan + tracked
follow-ups (#78 terrain, look-in-from-inside, look-in FPS, L-spotlight):
docs/research/2026-06-07-indoor-render-session-handoff.md.

Baselines: build 0 err; App.Tests 210/210; Core.Tests 1331 pass / 4 fail (pre-existing) / 1 skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 10:14:43 +02:00
Erik
2b7f5a16c6 fix(render): branch inside/outside on is_player_outside, not the camera cell (PARTIAL)
Retail SmartBox::RenderNormalMode (0x453aa0:92665) branches DrawInside vs the
outdoor LScape::draw on is_player_outside (the PLAYER's cell, 0x451e80), then
roots DrawInside at the VIEWER cell. acdream keyed the whole branch off the
camera cell (clipRoot = visibility.CameraCell), so a 3rd-person chase camera
lagging in a doorway AFTER the player stepped outside took the DrawInside path
rooted at the threshold cell, where the exit-portal flood degenerates: grey
world + entities-through-walls. Now ShouldRenderIndoor(playerCellId,
viewerCellResolved) gates the branch on the player; the DrawInside root stays
the viewer cell (handoff invariant preserved).

SCOPE / HONESTY: this REDUCES the player-OUTSIDE doorway grey (visual-confirmed
reduced a lot) but does NOT fix the deeper symptom: when the player is in one
interior cell (cellar 0174) and the camera is in another (room 0171), the flood
roots at the camera cell and does NOT seal the player's cell, so the cellar
floor / interior walls drop to grey. That is the KNOWN R1-completion problem
(2026-06-05 Residual A handoff + 2026-06-02 design doc section 3: a
SHELL-SEALING / wrong-flood-root bug), not this branch.

Tests: Core 1331p / 4f (documented) / 1s, App 187p, build green.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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