Commit graph

1397 commits

Author SHA1 Message Date
Erik
fca0a13217 feat(phys): ShadowObjectRegistry.RegisterMultiPart
Multi-shape entity registration matching retail's CPhysicsObj model: one
logical entity emits N ShadowEntry rows (one per CylSphere / Sphere /
Part-BSP), all sharing the entity's EntityId. _entityShapes caches the
original shape list per entity for UpdatePosition to recompose part
transforms when the entity moves.

Existing UpdatePhysicsState / Deregister / GetObjectsInCell /
AllEntriesForDebug work unchanged — they iterate by EntityId; multiple
matching entries get handled automatically.

AllEntriesForDebug updated to enumerate all parts per entity (not just
the first) by iterating the first cell that holds entries for each entity.
Single-shape callers that previously relied on deduplicated-by-EntityId
behavior are unaffected since they register exactly one entry per entity.

Six new tests: AllShareEntityId, EmptyShapeList_NoOp,
Deregister_RemovesAllParts, UpdatePhysicsState_PropagatesEtherealToAllParts,
PartsAcrossMultipleCells_AllCellsListed, Register_SingleShapeCompat_Unchanged.

All 24 existing ShadowObjectRegistry tests pass via the unchanged
single-shape Register API. 11/11 CellarUpTrajectoryReplayTests pass.
7/7 ShadowShapeBuilderTests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:19:29 +02:00
Erik
1454eab75a feat(phys): ShadowEntry adds LocalPosition + LocalRotation
Local-to-entity transform fields, default-valued so existing single-shape
callers keep working unchanged. RegisterMultiPart (next commit) populates
them per part so UpdatePosition can rebuild the entry's world Position +
Rotation when the entity moves.

All 24 existing ShadowObjectRegistry tests pass (including the 2 new
slice 1 tests from b49ed90).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:14:20 +02:00
Erik
7f5c28777a feat(phys): ShadowShapeBuilder.FromSetup
Pure function translating Setup -> IReadOnlyList<ShadowShape>. Walks
CylSpheres + Spheres (only when no CylSpheres) + Parts (only when the
GfxObj has a non-null PhysicsBSP), using PlacementFrames in the same
Resting -> Default -> first-available priority as SetupMesh.Flatten.

Six tests pin the behavior: door setup produces 4 shapes (0+1+3), sphere
local offset matches Setup data, parts without BSP are skipped, creature
setups with CylSpheres skip Spheres, scale factor multiplies all radii
and offsets, empty setup returns empty list, null setup throws.

No callers in this commit; RegisterMultiPart + the GameWindow callers
follow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:12:56 +02:00
Erik
ab4278c272 feat(phys): add ShadowShape record (no callers yet)
Standalone record representing one collision-bearing shape attached to
a logical PhysicsObj. Foundation for the per-part BSP collision fix
that closes the M1.5 "doors don't block" bug. Spec at
docs/superpowers/specs/2026-05-24-door-collision-per-part-bsp-design.md.

No callers in this commit; integration follows in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:07:40 +02:00
Erik
8d4f14c173 docs(phys): implementation plan — per-part BSP for server-spawned entities
10-task TDD implementation plan for the design in
docs/superpowers/specs/2026-05-24-door-collision-per-part-bsp-design.md
(commit d71ceab). Each task is bite-sized (write failing test → run
→ implement → run → commit), with complete code in every step per
the writing-plans skill's "no placeholders" rule.

Map: Task 1-2 = ShadowShape + ShadowShapeBuilder; Task 3-6 =
ShadowObjectRegistry multi-part extensions (ShadowEntry fields,
RegisterMultiPart, multi-part UpdatePosition, Deregister cleanup);
Task 7 = RegisterLiveEntityCollision refactor (closes door bug);
Task 8 = landblock-static refactor (unifies paths); Task 9 = live-
capture regression pin; Task 10 = strip investigation diagnostics +
ship docs.

Visual verification gates after Task 7 (door fix surface) and Task 8
(static-collision regression check). 40+ test green-gate at every
commit boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:05:03 +02:00
Erik
d71ceaba9c docs(phys): design spec — per-part BSP collision for server-spawned entities
Captures the brainstorm session 2026-05-24 evening after A6.P4 slice 1
(b49ed90) shipped without closing #99. Investigation surfaced the actual
root cause: doors register as a single 14cm × 20cm bounding-cylinder
approximation derived from Setup.Radius/Height fallback. Their real
collision-bearing geometry lives in per-part GfxObj BSPs (3 parts for
Setup 0x020019FF), including the threshold polygon spanning the doorway.

Retail-faithful design: every server-spawned entity registers N shadow
entries (one per CylSphere + one per Sphere + one per Part-with-BSP),
all sharing the same EntityId. UpdatePhysicsState propagates ETHEREAL
flips to all entries via the existing EntityId-iteration path. Unifies
the live-entity and landblock-static registration code paths under one
ShadowShapeBuilder.

Retail anchor: CObjCell::find_obj_collisions → CPhysicsObj::FindObjCollisions
→ CPartArray::FindObjCollisions → CPhysicsPart::find_obj_collisions →
CGfxObj::find_obj_collisions. One PhysicsObj per entity, parts iterated
internally for collision (acclient_2013_pseudo_c.txt:276776-275055).

Five-commit migration sequence; tests at three layers (builder unit tests,
registry behavior tests, live-capture regression pin). Approach A approved
by user 2026-05-24.

Spec stands on its own as M1.5 work; not formally assigned a phase letter
per CLAUDE.md's "don't invent phase numbers on the fly" rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:21:07 +02:00
Erik
b49ed904c3 feat(phys): A6.P4 slice 1 — portal-reachable cellSet includes outdoor cells
Closes #99 (run-through doors regression from b3ce505).

The b3ce505 stopgap for #98 gates the outdoor 24m radial sweep on indoor
primary cells. Combined with ShadowObjectRegistry.GetNearbyObjects'
"skip outdoor ids" filter on the cellScope-pass loop, this meant doors
registered at outdoor cells (default cellScope=0u for server-spawned
entities at GameWindow.cs:3139) were invisible to spheres on the indoor
side of a doorway threshold — walk-through.

Pre-flight reads found that CellTransit.FindCellSet already adds
outdoor cells to its candidate set when the sphere straddles an
OtherCellId=0xFFFF exit portal (via AddAllOutsideCells triggered by
exitOutside=true inside the indoor-seed BFS). The fix is to stop
filtering those outdoor ids out before iterating, and rename the param
to portalReachableCells to reflect what the set actually contains.

- Q1: Indoor EnvCell.VisibleCellIds is indoor-only in all 16 cottage
  fixtures (low 16 bits ≥ 0x0100). OtherCellId=0xFFFF on portals
  marks "exit to outdoor world" without naming a specific cellId; the
  specific outdoor cell is computed by AddAllOutsideCells from world
  XY when the sphere straddles the exit portal.
- Q2: GameWindow.cs:3139 ShadowObjects.Register for server-spawned
  entities passes no cellScope → default 0u → outdoor 24m grid
  registration. UpdatePosition (line 145) does the same on movement.
  Doors are confirmed outdoor-registered.

Slice 1 makes a smaller change than the spec proposed (no new
parameter; just drop the existing filter), because FindCellSet's
existing exit-portal logic already exposes the needed outdoor cells.
The retail-faithful registration-side BuildShadowCellSet refactor and
the b3ce505 gate removal stay scheduled for slices 2-3.

Verification:
- 24/24 ShadowObjectRegistryTests pass (incl. two new slice 1 tests:
  IndoorPrimary_OutdoorCellInPortalSet_DoorReturned closes #99;
  IndoorPrimary_IndoorOnlyPortalSet_OutdoorRadialStillSkipped
  regression-pins #98)
- 11/11 CellarUpTrajectoryReplayTests pass (LiveCompare_FirstCap_
  FixClosesCottageFloorCap stays green)
- dotnet build AcDream.slnx: 0 errors, 0 warnings
- Pre-existing 6-8 static-state-leakage failures in serial physics
  suite verified unchanged by stash+retest baseline check

Visual verification pending: walk Holtburg cottage doorway from both
sides; door blocks both directions; cellar still climbable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 08:10:32 +02:00
Erik
3e3cd77202 docs(handoff): A6.P4 pickup handoff — full session-resume artifact
Self-contained pickup doc for the next session. Combines:
  - State summary (what's done, what's open, where we are in M1.5)
  - Direction (Option B chosen 2026-05-24 — A6.P4 full then #100)
  - Slice 1 pre-flight (Q1 + Q2 to resolve before coding)
  - Slice 1 / 2 / 3 implementation plans with commit shapes
  - #100 follow-up plan
  - Decomp anchors reference card (8 line citations)
  - Apparatus inventory (don't rebuild what's already there)
  - CLAUDE.md rules that apply
  - Copy-paste pickup prompt at the bottom

Cross-references all the canonical artifacts from this saga:
  - docs/superpowers/specs/2026-05-24-phase-a6-p4-retail-shadow-architecture.md
  - docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md
  - docs/ISSUES.md (#98 DONE, #99 OPEN, #100 OPEN)
  - memory: feedback_retail_per_cell_shadow_list.md,
            feedback_apparatus_for_physics_bugs.md
  - commits b3ce505 + b55ae83 (don't redo)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 07:32:58 +02:00
Erik
b55ae831bd docs: A6.P3 #98 resolution + A6.P4 design + #99/#100 filed
Knowledge-preservation pass after the issue #98 cellar-up fix shipped
(`b3ce505`). Closes the saga's documentation loop and plans the next
phase.

Changes:
  - docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md
    Appended "Resolution 2026-05-24" section: v3 hypothesis falsified,
    actual mechanism (head-bump cottage GfxObj floor poly from below)
    confirmed, b3ce505 fix shipped, known door regression flagged.
    Memory artifacts cross-referenced.
  - docs/ISSUES.md
    #98 moved to DONE with full resolution writeup + decomp anchors.
    #99 filed: door regression at building thresholds (caused by
    b3ce505's indoor-primary gate). Closes via A6.P4.
    #100 filed: transparent rectangular patches around houses
    (terrain rendering). Bisect found commit 35b37df introduced the
    hiddenTerrainCells mechanism that collapses 24m outdoor cells
    when buildings sit in them; cottage building only fills part of
    its cell so the rest of the 24m cell shows the sky-bleeding gap.
    Three fix-path options documented.
  - docs/superpowers/specs/2026-05-24-phase-a6-p4-retail-shadow-architecture.md
    Full A6.P4 design doc. Three-slice plan: (1) query-side portal
    expansion to close #99 while preserving #98 fix, (2) port retail's
    BuildShadowCellSet at registration time so per-cell semantics match
    `CObjCell::find_cell_list`, (3) remove b3ce505 stopgap entirely.
    Decomp anchors, file-by-file plan, risk inventory, open questions.

Memory entries written separately (out-of-tree at
~/.claude/projects/.../memory/):
  - feedback_retail_per_cell_shadow_list.md
    The architectural lesson: retail uses per-cell shadow_object_list
    with portal-aware registration; our landblock-wide spatial
    registry diverges at indoor/outdoor seams.
  - feedback_apparatus_for_physics_bugs.md
    The apparatus-first pattern that cracked the saga: live capture +
    fixture dump + replay harness. Template for future physics bugs.
    Quote rule: "when a physics bug is resisting and you catch
    yourself about to ship 'fix attempt N+1 with no new evidence,'
    STOP. Build the apparatus first."
  - MEMORY.md index updated with both new entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 07:23:49 +02:00
Erik
b3ce505ca8 fix(phys): A6.P3 #98 — gate outdoor shadow radial sweep on indoor primary cell
The cellar-up cap was caused by ShadowObjectRegistry.GetNearbyObjects
running its outdoor 24m-grid radial query unconditionally — including
when the moving sphere's primary cell is indoor. The landblock-baked
cottage GfxObj 0x01000A2B (registered with cellScope=0u, i.e.
landblock-wide) was returned for a sphere inside the cellar EnvCell,
and its downward-facing cottage-floor poly at world Z=94 head-bumped
the sphere from below, capping ascent at foot Z=92.74.

Diagnosis this session via the live capture in
a6-issue98-resolve-capture-2.jsonl (92K records, 132 cap events all
with body on the ramp polygon) FALSIFIED the prior "stale ramp
contact plane" hypothesis: the contact plane is correctly the ramp's
plane because the sphere IS on the ramp at the cap. The cap is a
proximate consequence of the cottage GfxObj being queried at all from
an indoor primary cell.

Retail decomp anchor (acclient_2013_pseudo_c.txt):
  - 308751-308769: CObjCell::find_cell_list branches on the moving
    object's m_position.objcell_id — INDOOR adds only that cell +
    portal-visible neighbors via CELLARRAY::add_cell; OUTDOOR adds
    all overlapping outdoor cells via CLandCell::add_all_outside_cells.
    Object-position-driven, not sphere-radius-driven.
  - 309560: CEnvCell::find_collisions calls find_env_collisions
    (own cell BSP only) THEN CObjCell::find_obj_collisions on `this`.
  - 308916: CObjCell::find_obj_collisions iterates this->shadow_object_list
    — strictly per-cell, never landblock-wide.

Combined: a landblock-baked static like the cottage building is added
to outdoor cells' shadow_object_list only (its m_position resolves to
an outdoor cell). An indoor EnvCell's shadow_object_list never
contains the cottage. CEnvCell::find_collisions therefore never tests
the sphere against the cottage. Retail-faithful behavior.

Falsification spike (this session): scoping the cottage to a single
distant outdoor cell instead of landblock-wide caused the harness
LiveCompare_FirstCap test to stop reproducing the cn=(0,0,-1) cap,
confirming the cap is caused by the radial sweep returning the
cottage to an indoor primary.

The fix:
  - Add optional `primaryCellId` parameter to
    ShadowObjectRegistry.GetNearbyObjects. When indoor (>= 0x0100),
    skip the outdoor radial sweep entirely after the indoorCellIds
    branch runs. Default 0u preserves prior behavior for
    cell-unaware callers (existing tests pass unchanged).
  - Transition.FindObjCollisions passes sp.CheckCellId.
  - Harness LiveCompare_FirstCap_* flipped to documents-the-fix form
    (asserts the downward-facing cottage-floor cap does NOT fire).
    Deletes the residual-X-motion test that documented a post-cap
    edge-slide — irrelevant once the cap is gone.

This same gate should close the other "Finding 3 family" indoor/outdoor
collision bugs (#97 phantom collisions, indoor sling-out). Visual
verification by the user is the remaining acceptance check before
closing #98.

Verification:
  - 11/11 CellarUpTrajectoryReplayTests pass in isolation
  - 55 ShadowObjectRegistry + TransitionTypes + PhysicsEngine
    + CellPhysics + CellTransit tests pass
  - 8 pre-existing static-state-leakage failures in serial physics
    suite are unchanged (verified by stash + retest on baseline)
  - dotnet build clean, 0 warnings

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 06:49:46 +02:00
Erik
bf6d97625c docs: A6.P3 #98 — new root-cause hypothesis (stale ramp contact plane)
Today's evening session ran from "harness still doesn't reproduce the
cap" → "harness reproduces it" → "wait, the cap is only a symptom, the
real cause is upstream Z drift from the contact plane never refreshing."

The breakthrough question, from the user: "we know how retail OPENs it
from above, how hard can it be to know how to open it from below?" —
which reframed the investigation away from cap-event mechanics (where
six prior attempts looked) and toward "what about our STATE is wrong
when the player is in the cellar but not on the ramp?"

The math: player at cap is 10 m away from the cellar ramp in cell-local
X, but body.ContactPlane is still the ramp's slope plane. AdjustOffset
projects forward motion along that stale slope every tick, lifting Z
by +0.201 m per tick. After enough ticks of horizontal walking, the
head sphere reaches Z=94 and bumps the cottage floor. If the contact
plane refreshed to the flat cellar floor when the player walked off
the ramp, the drift would be zero, the cap would never be reachable.

Next session's task (per the pickup prompt at the bottom of the
findings doc): (1) verify the hypothesis chronologically against the
live capture, (2) find the walkable-refresh gap in
Transition.FindEnvCollisions / SpherePath.SetWalkable, (3) cross-ref
retail's CObjCell::find_env_collisions for the per-tick contact-plane
refresh logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 06:03:52 +02:00
Erik
7729bdcf98 docs: A6.P3 #98 — record apparatus convergence + residual X-motion
The findings doc gets an evening-v2 follow-on documenting:
  - GfxObj dump infrastructure shipped (cc3afbc)
  - Harness reproduces cap-event collision normal (97fec19)
  - Residual +0.0266m X-motion divergence — the new investigation target
  - Pre-existing test suite flakiness (out of scope, tracked separately)

CLAUDE.md's "Current A6 phase" block points at the residual divergence
as the next concrete move with the test that gives <1s feedback per fix
attempt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:47:17 +02:00
Erik
97fec19dbb test(phys): A6.P3 #98 — comparison harness reproduces cottage-floor cap
Apparatus convergence. With the cottage GfxObj 0x01000A2B registered as
a ShadowEntry in BuildEngineWithCellarFixtures, the harness now reproduces
the live cap-event collision normal (cn=(0,0,-1)) exactly, ending the
"harness doesn't reproduce" divergence the prior session's findings doc
identified.

Concretely:
  * Adds a minimum-stub landblock (TerrainSurface at z=-1000) so
    TryGetLandblockContext succeeds at the cellar XY — production's
    FindObjCollisions early-returns without a landblock and would skip
    the cottage shadow query.
  * Adds RegisterCottageGfxObj that loads the 74-polygon cottage fixture
    via GfxObjDumpSerializer.Hydrate, then registers it at the cottage's
    world transform (translation (130.5, 11.5, 94.0) + 180° around Z,
    derived from the cellar cell's WorldTransform), matching
    GameWindow.cs:5893's landblock-baked-static registration shape.
  * LiveCompare_FirstCap_HarnessMissesCottageFloorBecauseCottageGfxObjNotRegistered
    flips: the cap-normal reproduction is now enforced by
    LiveCompare_FirstCap_HarnessReproducesCottageFloorCapNormal.
  * The full per-field round-trip uncovered ONE residual divergence:
    live preserves +0.0266m of +X motion through the cap event (edge-
    slide along the floor in XY); harness blocks ALL motion at the cap.
    Captured by LiveCompare_FirstCap_ResidualXMotionDivergence_Docs...
    in documents-the-bug form so the next session has a concrete next
    target.

Fixture: tests/AcDream.Core.Tests/Fixtures/issue98/0x01000A2B.gfxobj.json
(74 polygons, 6 downward-facing cottage-floor triangles at object-local
Z=0, BSP radius 13.989m matching the live [resolve-bldg] bspR=13.99).
Captured via launch-a6-issue98-cottage-gfxobj-dump.ps1.

In-isolation: all 12 CellarUpTrajectoryReplayTests + 4 GfxObjDumpRoundTripTests
+ 1 new PhysicsDiagnosticsTests pass.

Note on full-suite baseline: the full xUnit serial run shows 8–19
failures depending on order (pre-existing test interaction with shared
statics across PlayerMovementControllerTests, MotionInterpreterTests,
PositionManagerTests, etc.). The flakiness is independent of this
change — confirmed by stashing the harness changes and observing the
same flaky range. Investigating the static-state isolation problem is
out of scope for issue #98; tracked as a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:44:50 +02:00
Erik
cc3afbcbeb feat(phys): A6.P3 #98 — GfxObj dump infrastructure (ACDREAM_DUMP_GFXOBJS)
Mirror the existing ACDREAM_DUMP_CELLS pattern for GfxObj-owned geometry:
when ACDREAM_DUMP_GFXOBJS lists a hex GfxObj id, the first
PhysicsDataCache.CacheGfxObj for that id writes the full resolved
polygon table to a JSON fixture under
tests/AcDream.Core.Tests/Fixtures/issue98/0x{id:X8}.gfxobj.json (override
dir via ACDREAM_DUMP_GFXOBJS_DIR).

Motivation: the existing [resolve-bldg] probe captures GfxObj-level
metadata (id, BSP root radius, entity origin) but emits
"hitPoly: n/a (BSP path — side-channel not written)" because the
BSPQuery wire site that would populate LastBspHitPoly never landed.
A polygon-level dump at cache time bypasses that gap — one capture run
yields the FULL polygon table, fixture-loadable by the harness's
RegisterCottageGfxObj helper (next commit).

See docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md
for the cottage GfxObj 0x01000A2B context: landblock-baked static at
entity origin (130.5, 11.5, 94.0), responsible for the head-sphere cap
from below at world Z=94.0 that issue #98 is documenting.

Test baseline: 1183 + 8 pre-existing failures (serial run; +5 new tests
all pass; was 1178 + 8 pre-session).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:24:26 +02:00
Erik
4d83ba5620 docs(claude.md): A6.P3 #98 — point at evening-v2 findings doc
Adds the "Evening extension v2" paragraph documenting the apparatus
ship + root cause identification from the 2026-05-23 PM-late session.
The block names:

- The four commits that landed (fb5fba644614ab0f2db62f29c9d5)
- The apparatus: PhysicsResolveCapture + LiveCompare_* tests
- The root cause: cottage GfxObj 0xA9B47900 (landblock-baked static
  building) blocks the head sphere at world Z=94.0 with cn=(0,0,-1)
- User's confirming observation about jumping (rules out step-up
  hypotheses)
- The cap geometry math (foot Z=92.74 + sphereHeight 1.20)
- Documents-the-bug pattern for the first-cap test
- Test baseline (1178 + 8 serial)
- Pointer to the new findings doc as canonical pickup
- Concrete next-session move (extract cottage GfxObj polygons)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:13:24 +02:00
Erik
f29c9d5e61 docs(research): A6.P3 #98 — comparison harness findings + neighborhood fixtures
Session-end documentation for the 2026-05-23 evening session in
which:

1. The PhysicsResolveCapture apparatus shipped (committed earlier
   in fb5fba6).
2. A live capture (41K records) drove the first LiveCompare_* tests
   in CellarUpTrajectoryReplayTests, two of which PASS bit-perfect.
3. The failing third test pinpointed the cap-event divergence.
4. A second capture (70K records + 16 cell dumps + per-poly probes)
   identified the cottage GfxObj 0xA9B47900 as the blocker — a
   landblock-baked static building whose floor polygons live in the
   GfxObj's BSP, NOT in any cottage cell.

The findings doc has:
- TL;DR + chronological commits
- Apparatus inventory (PhysicsResolveCapture, comparison tests,
  fixtures, launch scripts)
- The math: head sphere top at Z=foot+1.68 reaches the cottage floor
  at Z=94.0 when foot Z=92.74, matching the observed cap.
- User's confirming observation (cap fires on pure-vertical jump too,
  ruling out every step-up / AdjustOffset hypothesis)
- What's NOT yet known (why retail doesn't have this cap; full
  cottage GfxObj polygon list)
- Next-session pickup with two ranked options

Adds:
- docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md
- launch-a6-issue98-capture.ps1 (capture-only launch)
- launch-a6-issue98-polydump.ps1 (capture + diagnostic probes + 16-cell dump)
- 13 new cell-dump fixtures (0xA9B40140-0xA9B40142, 0xA9B40144,
  0xA9B40145, 0xA9B40148-0xA9B4014F) at 272 KB total. The harness now
  has the full 0xA9B4014X neighborhood available for any future
  comparison test that needs adjacent cell geometry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:12:43 +02:00
Erik
0f2db62667 test(phys): A6.P3 #98 — convert FirstCap test to documents-the-bug pattern
The previous version of LiveCompare_FirstCap_HeadHitsCottageFloor
asserted the harness matched the live cap by per-field diff, which
correctly FAILED with a clear divergence message. Converted it to the
documents-the-bug pattern matching the existing
Harness_Finding_SphereGoesAirborneAtTick1 style: passes WHILE the
harness lacks the cottage GfxObj, and will start failing when the
cottage GfxObj is added — at which point the test should be flipped
to AssertCallMatchesCapture(engine, captured).

Test name now reads as a finding:
  LiveCompare_FirstCap_HarnessMissesCottageFloorBecauseCottageGfxObjNotRegistered

Second-capture poly-dump finding (committed in the test's xmldoc): the
live cap event attributes the blocking entity as obj=0xA9B47900 — a
landblock-baked static building (the cottage GfxObj). The cottage's
floor lives in this GfxObj's polygon table as a ShadowEntry, NOT in
any of the cottage's cells. The harness's BuildEngineWithCellarFixtures
intentionally skips RegisterStairRampGfxObj today, so the cottage
floor (downward-facing polygon at world Z=94.0) isn't present — and
the harness doesn't reproduce the cn=(0,0,-1) cap.

Next-session move: extract the cottage GfxObj's full polygon list
from a focused live capture (set ACDREAM_PROBE_BUILDING=1 so the
[resolve-bldg] probe fires per-polygon during the cap), add it to
RegisterStairRampGfxObj (rename to RegisterCottageGfxObj), uncomment
the registration call. The harness should then reproduce live's
cn=(0,0,-1) — at which point the documents-the-bug test starts
failing and should be flipped to the assertion form.

Test baseline maintained: 1178 + 8 pre-existing failures (was
1172 + 8 pre-changes; added 6 tests, all pass under serial run).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:10:13 +02:00
Erik
44614ab591 test(phys): A6.P3 #98 — comparison harness + first evidence-driven finding
The capture apparatus pays off on the FIRST iteration. Three records
sampled from a live cottage-cellar session — tick 0 (spawn at Z=92.53),
tick 376 (player on the cellar ramp at Z=91.49), and tick 1183 (first
cap event, foot Z=92.74 with cn=(0,0,-1)) — replayed against the
harness engine reveal:

- LiveCompare_Tick0_Spawn:                  PASSES (full round-trip).
- LiveCompare_Tick376_OnRamp:               PASSES (ramp walkable
                                            polygon hydrates correctly,
                                            engine reproduces live).
- LiveCompare_FirstCap_HeadHitsCottageFloor: FAILS by exactly the
                                            divergence shape that names
                                            the missing fixture.

The cap-record divergence:
  Result.Position:       live=(141.3865,7.2243,92.7390)
                         harness=(141.3599,7.2243,92.7390)  (Y slid; X stuck)
  Result.CollisionNormal:live=(0,0,-1)  ← downward = cottage floor from below
                         harness=(0,0,+1) ← upward = some other floor

Plus the LiveCompare_FirstCap_DiagnosticDump test (always passes; it's
a probe-firing scratch test) prints every cell polygon in world frame:

  Cellar 0xA9B40147 — ceiling polys at world Z=93.80 cover X=133-142,
  Y=-1.0-11.5 but NOT the sphere XY of (141.39, 7.03) — at the right
  edge of Y=7.03 the ceiling quads are at Y<3.90 or Y>8.70.

  Cottage 0xA9B40143 — floor polys at world Z=94.0 cover X=136.7-140.5,
  Y=3.9-13.1 but NOT (141.39, 7.03) either — at X=141.39 we are 0.89m
  east of the floor quad's rightmost vertex.

  Cottage 0xA9B40146 — only 4 walls, no floor.

So both cells we have CAN'T produce the live's cn=(0,0,-1). The actual
blocking polygon must be in a cell or static object we haven't loaded
into the harness yet. The cellar is rectangularly bounded; the cottage
above has a floor that spans the cottage, but the floor polygon RIGHT
ABOVE the ramp top (which is where the freeze fires) is in some OTHER
cell — either a separate cottage-floor sub-cell or a building static
GfxObj.

This is the first evidence-driven step in the saga. Six sessions of
speculation produced ten failed fix shapes; the apparatus produced
this finding in one round trip. Next step: re-capture with
ACDREAM_PROBE_POLY_DUMP + ACDREAM_DUMP_CELLS covering 0xA9B40140-
0xA9B4014F to identify the missing fixture cell.

Adds:
- LiveCompare_Tick0_Spawn / Tick376_OnRamp / FirstCap_HeadHitsCottageFloor
- LiveCompare_FirstCap_DiagnosticDump (always passes; dumps cell polys
  in world frame + enables every relevant probe so the captured stdout
  shows the harness BSP query path)
- tests/AcDream.Core.Tests/Fixtures/issue98/live-capture.jsonl
  (3 representative records from the 41,228-record live capture)
- AssertCallMatchesCapture helper: per-field diff with Vector3 / float
  tolerances, reports every divergence not just the first.

Test baseline maintained at 1172 + 8 baseline + 5 new tests pass +
1 known-failing test that pinpoints the bug = 1178 + 9 (where the
new failure is the desired evidence-driven test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 19:58:51 +02:00
Erik
fb5fba6229 test(phys): A6.P3 #98 — live ResolveWithTransition capture apparatus
Apparatus only — no fix attempt. Per the systematic-debugging skill's
"3+ failures = question architecture" rule, the 6 hypotheses we
tested speculatively on the harness's airborne-at-tick-1 bug all
failed because we kept guessing what state the harness lacks. This
commit ships the evidence-driven path: capture the EXACT player
ResolveWithTransition call (every input + body-before + body-after +
result) into a JSON Lines fixture, then a comparison test loads the
fixture and replays it against the test engine. The first per-field
divergence pinpoints the missing apparatus state — no more guessing.

Adds:
- src/AcDream.Core/Physics/PhysicsResolveCapture.cs — new static module
  with CapturePath (env var ACDREAM_CAPTURE_RESOLVE), PhysicsBodySnapshot
  record, JSON Lines writer (thread-safe, flushes per record), process-
  exit hook for clean shutdown.

- PhysicsEngine.ResolveWithTransition probe wiring: snapshot body at
  method entry, snapshot again before return, refactor the two returns
  into one path so the capture call site is single. Filtered to
  IsPlayer mover flag so NPC/remote DR calls don't pollute.

- CellarUpTrajectoryReplayTests.cs:
  • Capture_WritesJsonLinesRecordsWhenIsPlayerAndEnabled — drives 3
    ticks with capture on, reads file back, verifies round-trip of
    inputs + body-before/after snapshots.
  • Capture_SkipsNonPlayerCalls — drives 3 NPC-style ticks (no
    IsPlayer flag), confirms the file is not created.

Off by default. Set ACDREAM_CAPTURE_RESOLVE=<path> to a writable file
path; capture starts on the next player ResolveWithTransition call.

Test baseline: 1172 + 8 pre-existing failures + 2 new smoke tests
that pass = 1174 + 8. Verified by stashed-baseline comparison.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 19:41:11 +02:00
Erik
ec47159a2e docs(handoff): A6.P3 #98 — full-session handoff doc + CLAUDE.md/ISSUES.md updates
Adds the canonical pickup document
docs/research/2026-05-23-a6-p3-issue98-harness-handoff.md with:
- TL;DR + session arc (10 commits chronological)
- What the trajectory replay harness IS (committed apparatus)
- Bug 1 status: #98 cellar-up freeze (unfixed, 6 fix shapes failed)
- Bug 2 status: airborne-at-tick-1 (new, 6 hypotheses tested, root
  cause not isolated)
- Exclusion list: DO NOT retry any of the 6+6 dead ends
- Apparatus inventory: probes, tests, fixtures, cdb captures
- Recommended next move: side-by-side comparison harness against
  live PlayerMovementController state (evidence-first instead of
  speculation-first)
- Alternative moves: pivot to other M1.5 issues or M2 prep
- Self-contained pickup prompt at the bottom of the handoff doc

Updates CLAUDE.md's "Current A6 phase" block to point at the new
handoff doc as the canonical resume artifact.

Updates ISSUES.md's #98 entry with the late-day extension findings,
the 6-hypothesis exclusion list, and a pointer to the handoff doc.

Test baseline maintained at 1172 + 8 pre-existing failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 19:09:00 +02:00
Erik
5c6bdbe30d test(phys): A6.P3 #98 — harness deep investigation; airborne-at-tick-1 root cause not yet isolated
Multi-step investigation of the airborne-at-tick-1 bug per the
systematic-debugging skill. Several hypotheses tested via the
harness, each producing the same (0,1,0) hit normal at tick 1:

1. WalkablePolygon seeding ADDED to BuildInitialBody (was missing).
   PhysicsEngine.cs:665-673 requires body.WalkablePolygonValid +
   WalkableVertices to call SpherePath.SetWalkable. With seeded
   walkable poly: walkPoly=True survives tick 1 (was False before).
   BUT engine still reports hit=(0,1,0) and body goes airborne.
2. Initial Z lift removed (back to 0): same airborne behavior.
3. Synthetic stair GfxObj DISABLED: same (0,1,0) hit. Hit is not
   from FindObjCollisions.
4. Stub landblock REMOVED: same (0,1,0). FindObjCollisions early-
   returns without landblock context, FindEnvCollisions's outdoor
   terrain returns null. Hit is not from terrain.
5. SYNTHETIC BSP attached to cell fixtures (Hydrate sets BSP=null
   per its xmldoc; without BSP the indoor branch is skipped, falls
   through to outdoor terrain). One-leaf BSP referencing every poly
   in cell.Resolved. Indoor BSP path now runs. Same (0,1,0) hit.

Trace timeline at tick 1:
  find-start: walkPoly=True, CP valid, oi=0x303 (Contact+OnWalkable)
  after-adjust: req=(0,-0.1,0) adj=(0,-0.1,0) — no projection change
  before-insert: check=(141.5, 9.4, 91.43)
  stepdown-enter (Contact-recovery): stepDown=True, height=0.04
  stepdown-after-offset: check=(141.5, 9.4, 91.39) — moved DOWN 0.04
  stepdown-after-insert: state=OK, cp=n/a (no walkable found)
  stepdown-reject
  (second stepdown attempt — same outcome)
  after-insert: state=Collided, hit=n/a, walkPoly=False
  after-validate: state=OK, hit=(0,1,0), slide=(0,1,0)
                   oi=0x300 (Contact+OnWalkable CLEARED)

The (0,1,0) hit is set by ValidateTransition between after-insert
and after-validate. ValidateTransition's default-push-up code path
sets UnitZ=(0,0,1), NOT UnitY=(0,1,0). So something INSIDE
TransitionalInsert sets ci.CollisionNormal=(0,1,0) before
ValidateTransition runs (12 SetCollisionNormal call sites in
TransitionTypes.cs — root cause not isolated to one).

Per systematic-debugging skill: 5+ hypotheses tested without
convergence = "question architecture". The bug is hidden deeper
than a single misconfigured init field.

Next session pickup: build a side-by-side instrumentation harness
that mimics PlayerMovementController's EXACT call sequence
(PhysicsBody field state, ResolveWithTransition args, frame
ordering) and compare per-tick divergence against a live capture.
The harness is missing some piece of state production carries
across ticks — find what piece.

Apparatus progress (committed):
- Harness with synthetic stair GfxObj registration (Issue #98 ramp polygon now constructable programmatically)
- Synthetic cell-BSP attachment (AttachSyntheticBsp) — unlocks indoor
  BSP collision path for hydrated cell fixtures
- WalkablePolygon seeding in BuildInitialBody (PhysicsBody seeding pattern documented)
- Three diagnostic dump tests for tick-by-tick traces

Test baseline: 1167 + 5 (harness) = 1172 + 8 pre-existing failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 19:04:36 +02:00
Erik
227a77522a test(phys): A6.P3 #98 — harness diagnostic + initial Z lift experiment
Adds two diagnostic-only tests:
- Harness_DiagnosticDump_FirstTenTicks: prints trajectory + resolve
  probe lines for the seeded-body path
- Harness_DiagnosticDump_NoBodySeed: same but with body=null, isolating
  whether the CP seed contributes to the airborne-at-tick-1 issue

Also adjusts InitialSphereWorld to lift the sphere by 0.05m above
cellar floor (sphere bottom at Z=91.00, not Z=90.95). The lift
should give the engine a clean step-down on tick 1 instead of an
exact-boundary contact.

Experimental finding: NEITHER the no-body-seed path NOR the 0.05m
lift changes the airborne-at-tick-1 behavior. With sphere center
at world Z=91.48 (0.05m + radius above cellar floor at 90.95):
- Tick 1: in=(141.5, 9.5, 91.48), out=(141.5, 9.5, 91.48) — Y move
  rejected. hit=yes n=(0,0,1) walkable=False.
- Tick 2+: Y advances by 0.1/tick, Z stays put, onGround stays False.

The hit normal (0,0,1) at tick 1 means the engine treats the cellar
floor polygon as a NON-WALKABLE collision target when the sphere is
seeded grounded above it. The walkability classifier returns False
even though Normal.Z=1.0 > FloorZ=0.6642. This is a real engine bug
worth investigating in a future session — independent of the cellar-up
freeze.

The synthetic ramp polygon registered via RegisterStairRampGfxObj is
NOT reached because the sphere is now airborne and floats over the
cellar floor without contacting the ramp.

Next session pickup options:
1. Debug the airborne-at-tick-1 issue (likely in TransitionTypes
   FindEnvCollisions indoor BSP path — why does a flat (0,0,1) hit
   return walkable=False?). Once fixed, the harness should reproduce
   cellar-up freeze.
2. Pivot to a different M1.5 issue with cleaner reproduction.
3. Use the harness mechanics elsewhere — the synthetic-GfxObj +
   ShadowEntry pattern is reusable for any indoor-static-collision
   test (corpse pickup boundaries, door swings, etc.).

Test baseline: 1167 + 5 (harness) = 1172 + 8 pre-existing failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:51:42 +02:00
Erik
3d2d10b331 test(phys): A6.P3 #98 — harness extension: synthetic stair GfxObj + ShadowEntry
Extends the trajectory replay harness with a programmatic mini-stair
piece, reconstructed from the live capture's polydump data
(docs/research/2026-05-21-a6-captures/scen4_cottage_cellar_polydump).

NEW finding: the cellar ramp polygon is NOT in cellStruct.PhysicsPolygons.
It lives in a separate GfxObjPhysics (the cellar's stair-piece static
building) registered via ShadowObjectRegistry, queried via
FindObjCollisions → engine.DataCache.GetGfxObj. CellDumpSerializer is
CORRECT — it captures the cell's physics polygons accurately. The
ramp polygon comes from a different data source entirely.

The polydump probe at BSPQuery.AdjustSphereToPlane:402 reports
"cell=0xA9B40147 polyId=0x0008 sides=Landblock" because the SPHERE
is in that cell at hit time — but the polygon's actual source is the
building's GfxObj. Inside the cellar fixture, polyId=0x0008 happens
to be a wall (Normal=(1,0,0)); inside the building's GfxObj, polyId
=0x0008 is the ramp (Normal=(0,-0.719,0.695) local). Same ID, different
collection.

The new RegisterStairRampGfxObj() in the harness constructs the
building's ramp polygon in WORLD coordinates (translated from
local building frame + 180° yaw), wraps it in a minimal one-leaf
PhysicsBSPTree, registers via cache.RegisterGfxObjForTest, and
attaches a ShadowEntry with cellScope=CellarId so the shadow is only
queried when the sphere is in the cellar cell (matches retail's
per-cell shadow scoping for interior statics — Issue #91 family).

Verified: world plane n=(0,0.719,0.695), d=-69.5035 (matches live
cdb capture exactly to 4 sig figs). Ramp foot at world Y=8.745,
Z=90.955; ramp top at world Y=5.845, Z=93.955. 3.0 m vertical rise.

NEW blocker discovered: the sphere goes airborne at tick 1 (same
issue documented in the prior commit's Finding #2). Sphere FLOATS
at Z=91.43 over the cellar floor, never contacts the synthetic
ramp. The synthetic stair registration mechanics are validated (the
GfxObj is in the cache, the ShadowEntry is in the registry, the
BSP tree is well-formed) — but trajectory replay still blocked on
the seeded-grounded-state bug. Next session needs to diagnose
WHY the engine reports "hit=yes n=(0,0,1) walkable=False" on tick 1
for a sphere correctly seeded as grounded on the cellar floor.

Test baseline maintained: 1167 + 4 (harness) = 1171 + 8 pre-existing
failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:49:34 +02:00
Erik
4c9290c691 test(phys): A6.P3 #98 — trajectory replay harness (mechanics OK; fixtures incomplete)
Adds tests/AcDream.Core.Tests/Physics/CellarUpTrajectoryReplayTests.cs:
a deterministic harness that drives PhysicsEngine.ResolveWithTransition
through N ticks against pre-loaded cell fixtures, capturing per-tick
trajectory points. Pure indoor (no landblock registration needed),
runs 200 ticks in under 100 ms.

The harness MECHANICS work — engine constructs cleanly, DataCache
accepts test fixtures via RegisterCellStructForTest, PhysicsBody
carries ContactPlane state across ticks. 4/4 tests pass, baseline
maintained (1167 + 4 = 1171 + 8 pre-existing failures).

Two real findings surfaced during commissioning, both documented as
passing tests so they don't regress silently:

Finding 1 (Harness_FixtureLimitation_NoRampPolygon): the three
issue-#98 cell fixtures contain ONLY axis-aligned polygons. The
cellar fixture (0xA9B40147) has 37 polys: 8 floor (N=(0,0,1)), 7
ceiling (N=(0,0,-1)), 22 walls. The live capture's CELLAR RAMP
polygon (N ≈ (0, ±0.719, 0.695)) is NOT in any fixture. With no
ramp polygon, the harness can't reproduce the cellar-up climb —
the sphere would walk horizontally across the cellar floor without
ever encountering a slope. Re-capture needed; investigate whether
CellDumpSerializer is skipping polygons or whether the ramp lives
in a cell we didn't dump.

Finding 2 (Harness_Finding_SphereGoesAirborneAtTick1): at the
seeded grounded initial position (sphere center 0.48 m above cellar
floor, ContactPlane = (0,0,1,-90.95), OnWalkable bit set), the
engine reports `hit=yes n=(0,0,1) walkable=False` on tick 1 and
the body's IsOnGround flips to false. Subsequent ticks proceed as
airborne (Y advances, Z stays put — no gravity in the input offset).
Unclear whether this is an engine bug (floor contact classified
as non-walkable collision) or a fixture issue (cellar floor
polygon's containment test mis-firing at the seeded XY). Either
way, the harness now exposes it deterministically.

Net value of this commit: the harness CODE is ready. Once the
fixture issue is solved, fix attempts on #98 (or any trajectory-
dependent bug) iterate in <100 ms instead of 5-minute live-launch
cycles. The "why is this so hard" point #4 from the session-pause
handoff is addressed for everything except the missing-ramp gap.

Test baseline: 1171 (1167 + 4 new) + 8 pre-existing failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:33:17 +02:00
Erik
5f3b64c548 docs(handoff): A6.P3 #98 — session paused after Shape 1 attempted + reverted
Updates ISSUES.md and CLAUDE.md to reflect the actual state of #98
after two days of work:

- The new [step-walk-adjust] probe (8a232a3) + capture + findings
  (8daf7e7) prove AdjustOffset's slope projection is CORRECT. Sphere
  Z climbs monotonically 90.95 -> 92.80 across the ramp at +0.045 m
  mean zGain per call. The earlier "Fix targets 1-4" priority list
  is OBSOLETE — AdjustOffset is not the bug.
- The climb caps at world Z ~= 92.80 because step-up's downward
  step-down probe finds no walkable within stepDownHeight=0.6 m
  below the proposed position. Cottage floor at Z=94 is ABOVE, not
  below. 101 stepdown-reject hits in the capture vs 1 acceptance.
- Shape 1 fix attempted (0cb4c59): gated BSPQuery.AdjustSphereToPlane's
  two SetContactPlane call sites by Normal.Z >= 0.99 to match retail's
  cdb-observed flat-CP-only pattern. Reverted (402ec10) — gate broke
  OnWalkable tracking. 74% of new capture in falling state. User
  report: "can't get up the first step, jumped, stuck in falling
  animation." Either retail synthesizes a flat CP from sloped
  contacts (step_sphere_down:321203 path, unclear from BN decomp)
  or our OnWalkable tracking is over-coupled to ContactPlaneValid.

Apparatus state: probe, findings, replay harness, plan, retail
cdb capture all committed and ready for next session.

Honest next-session moves (in order):
1. Build deterministic trajectory replay harness — 200ms inner loop
   instead of 5-minute live test. Issue98 cell fixtures are half of
   this already.
2. Pivot to less-coupled M1.5 issue while #98 awaits the harness.
3. Deeper named-decomp research on CEnvCell::find_env_collisions ->
   BSPTREE::find_collisions indoor CP-setting chain. Prior passes
   worked on the outdoor CLandCell path; indoor was never traced.

NO further #98 fix attempts until apparatus or research has
converged. Five+ failed attempts in the saga is the signal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:59:43 +02:00
Erik
402ec10ec5 Revert "fix(phys): A6.P3 #98 — gate ContactPlane assignment by Normal.Z (Shape 1)"
This reverts commit 0cb4c59681.
2026-05-23 16:54:19 +02:00
Erik
0cb4c59681 fix(phys): A6.P3 #98 — gate ContactPlane assignment by Normal.Z (Shape 1)
Adds PhysicsGlobals.ContactPlaneFlatThreshold = 0.99f and uses it at
both BSPQuery.AdjustSphereToPlane call sites that previously set CP
unconditionally on any walkable polygon found by FindWalkableInternal.

Backed by the retail cdb capture in cellar_up_capture_1: across 161
set_contact_plane writes during 5 seconds of cellar-up climbing,
EVERY write lands on a flat (Normal.Z = 1.0) plane — cellar floor at
world Z=90.95 or cottage floor at world Z=94. The cellar ramp
(Normal.Z = 0.695, walkable per FloorZ but sloped ~46 degrees) is
never set as CP in retail.

Acdream's prior behavior of setting CP=ramp caused two cascading
issues at the top of the ramp:
1. AdjustOffset's slope-projection produced +Z gain per call (correct
   in isolation) but inflated step-up's responsibility to "find the
   next walkable below the lifted check position".
2. step-up's downward step-down probe found no walkable within 0.6m
   below the proposed check (cottage floor at Z=94 is ABOVE, not
   below), so step-down rejected, sphere rolled back. Infinite freeze
   at world Z ~= 92.80.

With CP only set on flat polygons, sloped surfaces drive collision
detection and walkable-poly tracking (via path.SetWalkable) but
don't override the resting CP. The sphere should now climb the ramp
via step-up over the ramp polygon, with CP staying on the flat
cellar floor until the sphere reaches the flat cottage floor.

Tests: 1167 + 8 baseline maintained. No regression. The Issue98
replay tests still pass — they document the failing-frame geometry
(sphere world Z=92.01 below cottage floor), which doesn't change;
the fix prevents the sphere from getting STUCK at that altitude in
the first place. Live visual verification required next.

If the live test shows new failure modes (sphere stuck somewhere
else, doesn't climb at all, climbs but slides off, etc), the
threshold (0.99) or the gating approach itself may need refining.
This is the conservative empirical version of Shape 1; the named-
decomp research did not conclusively prove the exact retail gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:46:12 +02:00
Erik
8daf7e7e4d research(phys): A6.P3 #98 — [step-walk-adjust] capture + findings
The diagnostic-first capture revealed the failure mode the plan's
four-branch decision tree (A/B/C/D) did not anticipate. AdjustOffset
is CORRECT: 145/146 calls use the into-plane branch, mean zGain
+0.045 m per call, sphere world Z climbs 90.95 -> 92.80 monotonically.

The climb caps at world Z 92.80 (cottage floor at 94.00 is still
1.20 m above). At the cap, the per-step CP reset at TransitionTypes.cs
723-725 clears ContactPlaneValid as designed; TransitionalInsert
should re-establish CP at the proposed position. Step-up logic fires
because the offset has +Z; step-up calls DoStepDown(stepDownHeight=
0.6, runPlacement=true). The downward probe finds NO walkable surface
within 0.6 m below the proposed position (cottage floor is ABOVE,
not below) -- 101 stepdown-reject hits in this capture vs 1 acceptance.

Conclusion: Target E (new). Three candidate fix shapes named in the
findings note. Each one researched against retail named-decomp before
any code lands. Test baseline 1167 + 8 maintained.

Findings:  docs/research/2026-05-23-a6-stepwalkadjust-findings.md
Capture:   docs/research/2026-05-23-a6-captures/stepwalkadjust/acdream.log

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:25:04 +02:00
Erik
8a232a3e6e diag(phys): A6.P3 #98 — [step-walk-adjust] probe inside AdjustOffset
Adds one log line per AdjustOffset call (gated by ACDREAM_PROBE_STEP_WALK)
naming the branch taken (no-cp / no-cp-slide / slide-degenerate /
slide-crease / into-plane / away-plane, optionally +safety-push) plus
zGain = output.Z - input.Z.

No math or control-flow changes — pure observability so the next capture
can disambiguate the three failure-mode hypotheses for the cellar-ramp
climb cap. Re-reading the existing capture (a6-issue98-negpoly-...log)
showed the sphere DOES climb 90.00 -> 92.79 (2.79 m gain), then caps,
contradicting the divergence comparison's "no altitude gain" framing.
The real question is what stops the climb at world Z ~= 92.79 with the
cottage floor still 1.21 m higher. Existing [step-walk] probes wrap
AdjustOffset; this new probe reveals which branch the projection takes.

Fix plan with the four-branch decision tree at
docs/superpowers/plans/2026-05-23-a6-p3-issue98-cellar-up-fix.md.

Test baseline maintained: 1167 + 8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:16:42 +02:00
Erik
67005e21f1 docs(handoff): A6.P3 #98 Step 6 — issue + claude.md handoff
Final step of the apparatus plan. Updates ISSUES.md issue #98 and
CLAUDE.md's M1.5 status to reflect:

- The apparatus completed (Steps 1-5 land in commits 35b37df28c282a).
- The real divergence: retail's sphere is at world Z ≈ 94.48 (resting
  on cottage floor) when find_walkable accepts; acdream's failing-
  frame sphere is 2.47m lower at world Z ≈ 92.01.
- The four fix targets, in priority order. Fix plan is the NEXT plan,
  scoped to Target 1 (step-up + ramp climb Z gain) or Target 2
  (cottage-cell sphere reference).
- The replay harness (Issue98CellarUpReplayTests) is the test loop —
  any fix that doesn't change the failing assertions is not the fix.

Today's commit graph on top of slice 5 (cf3deff):
  35b37df  triage — revert neg-poly + bldg-check experiments
  f62a873  Step 2 — cell-dump probe + roundtrip test
  3f56915  Step 2 capture — 3 real-geometry cell fixtures
  856aa78  Step 3 — deterministic replay harness (7 tests)
  6f666c1  Step 4 — retail cdb find_walkable capture script
  28c282a  Step 5 — replay vs retail divergence comparison
  (this)   Step 6 — ISSUES.md + CLAUDE.md handoff

Test baseline: 1167 + 8 (8 pre-existing failures, +19 new passing
tests across the apparatus). Build green throughout.

A6.P3 #98 is now in evidence-driven mode. Fix plan starts from the
divergence doc at
docs/research/2026-05-23-a6-p3-issue98-replay-comparison.md.

Pickup prompt for the fix-plan session is in §"Pickup prompt for the
fix plan" of that doc.
2026-05-23 15:58:52 +02:00
Erik
28c282a563 docs(phys): A6.P3 #98 Step 5 — replay vs retail divergence comparison
Closes the apparatus loop. Side-by-sides acdream's deterministic replay
(commit 856aa78) against retail's cdb capture taken via Step 4's
runner. The divergence target is named; the fix plan is the next plan.

Retail data (cellar_up_capture_1):
- 35,219 BP hits over ~5 seconds of motion
- BPE (set_contact_plane): 161 writes, ALL to one of two flat planes
  (n=(0,0,1) d=-93.9998 = cottage floor @ Z=94, OR d=-90.95 = cellar
  floor @ Z=90.95). Retail NEVER sets ContactPlane to the cellar ramp.
- BPC (find_crossed_edge): 1 hit in 35K. Retail barely uses this
  predicate during cellar-up.
- BPA (find_walkable) sphere position at each cottage-floor
  acceptance: sphere LOCAL Z = +0.48 to +0.63 (resting on top of the
  floor plane). Sphere world Z ≈ 94.48.

acdream replay (Issue98CellarUpReplayTests):
- At the failing-frame sphere (world (141.7, 8.4, 92.0)), the cottage
  cell 0xA9B40143's poly 0x0004 reports insideEdges=false AND
  overlapsSphere=false. Sphere local Z = -0.69 (below the cottage
  floor plane). 0xA9B40146 has no walkable candidate at all. Step-up
  has nothing to step onto → stuck.

Sphere world Z delta: 2.47m. Retail's sphere is 2.5m higher than ours
at the decision point. The fix targets, in priority order:

1. (HIGHEST CONFIDENCE) Step-up + ramp climb doesn't gain enough Z per
   tick. Retail climbs the ramp GRADUALLY across thousands of ticks;
   ours oscillates at world Z ≈ 92 without altitude gain. Look at
   Transition.AdjustOffset (slope projection) and Transition.DoStepUp
   (does it reset WalkInterp like retail's step_sphere_up?).

2. Cottage-cell candidacy uses wrong sphere reference. Check what
   sphere CheckOtherCells passes to BSPQuery.FindCollisions — is it
   the step-lifted sphere or the pre-step sphere?

3. (SECONDARY) find_crossed_edge over-use. Our walkable test calls
   FindCrossedEdge heavily; retail barely uses it. Possibly a
   code-shape mismatch in step-up vs walkable-acceptance flow.

4. (LOW CONFIDENCE) Ramp polygon normal divergence. Verify via test
   after any fix.

The apparatus that gets us here:
- tests/AcDream.Core.Tests/Fixtures/issue98/*.json (real cell geometry)
- Issue98CellarUpReplayTests (7 tests, <1ms each, deterministic bug
  reproduction)
- tools/cdb/issue98-runner.ps1 (reusable for any future capture)
- docs/research/2026-05-23-a6-captures/cellar_up_capture_1/ (this
  capture, checked in for future analyses)

Next plan: pick Target 1 or 2 from the comparison doc and write the
fix plan against it. The replay harness is the test loop; a fix that
doesn't change the failing assertions in Issue98CellarUpReplayTests is
not the fix.
2026-05-23 15:57:12 +02:00
Erik
6f666c14da tools(cdb): A6.P3 #98 Step 4 — retail find_walkable capture script
Step 4 of the apparatus plan. Adds the cdb script + runner that pairs
with Issue98CellarUpReplayTests to compare retail's walkable-query
behavior against acdream's during the Holtburg cottage cellar ascent.

Breakpoints (all symbols verified against refs/acclient.pdb via grep
docs/research/named-retail/symbols.json):
- BPA: BSPLEAF::find_walkable          — leaf-level walkable query
- BPB: CPolygon::walkable_hits_sphere  — per-polygon overlap test
- BPC: CPolygon::find_crossed_edge     — per-polygon edge containment
- BPD: CTransition::check_other_cells  — outer dispatcher
- BPE: COLLISIONINFO::set_contact_plane — GOLD signal: retail accepted
                                          this plane
- BPF: CPolygon::adjust_sphere_to_plane — per-polygon projection

Output format: 32-bit hex bits for all floats via dwo() + %08X (cdb's
%f handling is broken for dwo reads; see a6-probe.cdb v3→v4 history).
Decoder: tools/cdb/decode_retail_hex.py already handles _h=0x... fields.

Auto-detach threshold: 50000 hits across BPA/B/C/D/F. BPE is unbounded
(contact plane writes are rare, ~18 per ascent per slice 5 capture).

Runner: tools/cdb/issue98-runner.ps1
  .\tools\cdb\issue98-runner.ps1 -ScenarioTag "cellar_up_attempt_1"

Prereqs (per CLAUDE.md retail debugger toolchain section):
- Retail acclient.exe v11.4186 running and in-world
- ACE running on 127.0.0.1:9000
- Character at the BOTTOM of a Holtburg cottage cellar stair
- cdb.exe present at the Windows Kits 10 path

Output:
  docs\research\2026-05-23-a6-captures\<ScenarioTag>\retail.log

Reading the log:
- [BPE] lines tell you which plane retail accepted (the answer we need).
- Cross-reference [BPE]'s normal/d against the cell fixtures in
  tests/AcDream.Core.Tests/Fixtures/issue98/*.json to identify which
  cell + polyId retail picked.
- The divergence between retail's accepted polygon and our replay test's
  "no walkable accepted" result IS the fix target.

The capture itself is a user action (cdb requires a live retail
process); this commit only ships the protocol. Step 5 (comparison doc)
follows after the capture lands.
2026-05-23 15:29:02 +02:00
Erik
856aa78ec1 test(phys): A6.P3 #98 Step 3 — deterministic replay harness
Step 3 of the apparatus plan. Adds Issue98CellarUpReplayTests, a 7-test
harness that loads the three real-geometry cell fixtures captured in
commit 3f56915 and drives the failing-frame sphere through the same
nearest-walkable algorithm the live client uses in
Transition.LogNearestWalkableCandidate.

The tests reproduce the live failure deterministically in under 1ms
each — the issue #98 cellar-up bug is now visible to a unit-test run,
no client launch required.

Tests:
- Fixtures_AllThreeCellsLoadAndShareOrigin — sanity check the cells
  loaded with the expected (130.5, 11.5, 94.0) origin.
- Cellar_HasMostPolygons_CottageNeighborBIsSparse — confirms the
  surprising finding: 0xA9B40146 is too sparse to be a "cottage main
  floor" cell (slice 5 handoff inference was wrong; 0xA9B40143 with 14
  polys is the better candidate).
- FailingFrame_CellarPrimary_HasCellarRampAsNearestWalkable — the
  ramp polygon IS reachable when the player is on top of it
  (sanity: this should always be true).
- FailingFrame_CottageNeighborA_NearestWalkableIsOutsideSphereAndEdges
  — at the failing-frame sphere position, the nearest walkable in
  0xA9B40143 (poly 0x0004, the cottage floor triangle at world Z=94)
  reports BOTH insideEdges=false AND overlapsSphere=false. The sphere
  XY is beyond the triangle edge, and the sphere is too far below the
  plane. THIS IS THE BUG'S SHAPE.
- FailingFrame_CottageNeighborB_HasNoWalkableCandidate — 0xA9B40146
  has NO walkable polygon close enough to the failing-frame sphere.
- FailingFrame_NoCottageNeighbourYieldsAcceptedWalkable — composite:
  across both cottage cells, no walkable passes both edge + sphere
  tests → step-up has nothing to step onto → player stuck.
- FailingFrame_CottageNeighborA_Poly0x0004_HasExpectedShape — pins the
  exact polygon shape so a future fixture re-capture failure is loud.

What this gives us:
1. The bug is now ALWAYS reproducible in test, no live client iteration.
2. Any fix to BSPQuery.FindCrossedEdge / polygon containment / the
   cell transform will instantly show whether it changes the failing-
   frame outcome.
3. Step 4 (retail cdb capture) will tell us what retail finds at the
   same sphere position; Step 5 (comparison doc) will name the
   divergence; the eventual fix is then evidence-driven, not a guess.

The tests document the CURRENT (failing) behavior. They WILL pass
after the fix — at which point they need to flip to assert the
retail-correct behavior. This intentional brittleness is the point:
the test is the bug's gravestone, and a fix that doesn't match retail
should not satisfy the test.

Verification:
- dotnet build: green, 0 errors.
- dotnet test: 1167 passed + 8 pre-existing failed (was 1160+8 before
  this commit; +7 from the replay tests). Same pre-existing failures,
  no new regressions.
- Each Issue98 test runs in under 1ms; loads JSON, calls one internal
  predicate per polygon, asserts.

Next: tools/cdb/issue98-cellar-up-find-walkable.cdb (Step 4).
2026-05-23 15:25:40 +02:00
Erik
3f56915bc6 capture(phys): A6.P3 #98 — cellar/cottage cell fixtures from live capture
Step 2 capture step. Launched the live client with
ACDREAM_DUMP_CELLS=0xA9B40143,0xA9B40146,0xA9B40147 and walked into a
Holtburg cottage cellar. The probe fired on first-cache of each cell
and emitted JSON dumps to tests/AcDream.Core.Tests/Fixtures/issue98/.

Cell contents (resolved polygons + portals):
- 0xA9B40143: 14 polys + 4 portals (~18.7 KB)
- 0xA9B40146:  4 polys + 2 portals (~7.0 KB)
- 0xA9B40147: 37 polys + 2 portals (~45.7 KB) — cellar, biggest
All three share worldOrigin=(130.5, 11.5, 94.0) with 180° yaw rotation
(M11=M22=-1), matching the failing-frame's local-to-world projection.

Reproduction during capture: spawn at (141.6, 8.4, 91.5) @ 0xA9B40147
— almost exactly the slice 7 handoff's failing-frame position. User
tried to walk up the cellar stair and got stuck (issue #98 reproduction
confirmed).

Surprise: 0xA9B40146 with only 4 polys + 2 portals is too sparse to be
the "cottage main floor cell" that the slice 5 handoff inferred — that
designation was a guess, not verified. 0xA9B40143 (14 polys) is the
better candidate. Step 3 (replay harness) will confirm by inspecting
the actual polygon geometry against the failing-frame sphere position.

Cells are real geometry from client_cell_1.dat, not synthetic fixtures.
The replay harness can now drive the leaf-level walkable predicates on
this exact data without launching a window.

Next: Issue98CellarUpReplayTests (Step 3).
2026-05-23 15:21:44 +02:00
Erik
f62a873be3 feat(phys): A6.P3 #98 Step 2 — cell-dump probe + roundtrip test
Step 2 of the apparatus plan at
C:\Users\erikn\.claude\plans\i-did-some-work-sharded-acorn.md. Adds a
one-shot cell-dump probe so the issue #98 replay harness can load real
cellar / cottage geometry as JSON fixtures, eliminating live-client
iteration from every fix attempt.

Probe gate:
  ACDREAM_DUMP_CELLS=0xA9B40143,0xA9B40146,0xA9B40147
  ACDREAM_DUMP_CELLS_DIR=tests/AcDream.Core.Tests/Fixtures/issue98 (default)

When set, the first time PhysicsDataCache.CacheCellStruct sees a matching
envCellId, it serializes the resulting CellPhysics to
<dir>/0x<cellid>.json and prints one [cell-dump] line. Zero cost when
unset (gate is a static-readonly IReadOnlySet<uint>.Count check).

DTOs (CellDump.cs):
- CellDump: top-level record holding cell id, WorldTransform,
  InverseWorldTransform, resolved polygons, portal polygons, portal
  infos, visible cell ids.
- PolygonDump / PortalDump / Vector3Dto / PlaneDto / Matrix4x4Dto:
  System.Text.Json-friendly records with explicit From / To converters.

What is intentionally NOT dumped: the DAT-native PhysicsBSPTree and
CellBSPTree trees. The replay harness drives the leaf-level walkable
predicates (WalkableHitsSphere, FindCrossedEdge, PolygonHitsSpherePrecise)
directly on the resolved polygon list, which is enough to expose the
issue #98 rejection (poly 0x0004 in 0xA9B40143 reports
insideEdges=False / overlapsSphere=False at the failing-frame sphere).
If a future replay needs BSP traversal we can extend the DTO + Hydrate
together without breaking fixtures.

Tests (CellDumpRoundTripTests):
- Capture → Hydrate preserves WorldTransform / InverseWorldTransform /
  every polygon's plane + vertices + NumPoints + SidesType.
- Capture → Hydrate preserves portal list + visible cell ids.
- Write to disk → Read back → Hydrate preserves content.
- Hydrate leaves BSP / CellBSP null by design (replay uses leaf-level
  predicates).

Verification:
- dotnet build: green, 0 errors.
- dotnet test: 1160 passed + 8 pre-existing failed (was 1156 + 8 before
  this commit; +4 from CellDumpRoundTripTests). Same 8 pre-existing
  failures, no new regressions.

Next: capture the three cells from the live client (Step 2 acceptance),
then build the replay harness against the fixtures (Step 3).
2026-05-23 15:16:56 +02:00
Erik
35b37dfb5f chore(phys): A6.P3 #98 triage — revert neg-poly + bldg-check experiments
Triage step from the plan at C:\Users\erikn\.claude\plans\
i-did-some-work-sharded-acorn.md. Four sessions on issue #98 left the
worktree dirty with ~1352 LOC of mixed work. This commit splits the
work into "keep" (defensible + diagnostic) and "drop" (failed
experiments), then commits the keep set with the drops removed.

Plan asked for three commits (diag / fix / revert); consolidated to one
because the diagnostic emits in TransitionTypes.cs are tightly
interleaved with the multi-sphere CellTransit calls and the CellId
switch. Hunk-level splitting in those files for marginal bisect
granularity didn't justify the misclick risk.

Reverted entirely (failed experiments per slice 7 handoff):
- src/AcDream.Core/Physics/PhysicsDataCache.cs — neg-poly storage
  fields (Stippling, PosSurface, NegSurface, HasNegativeSide,
  IsNegativeSide, NegativeSide).
- src/AcDream.Core/Physics/ShadowObjectRegistry.cs — isBuilding flag
  propagation through Register / ShadowEntry.
- tests/AcDream.Core.Tests/Physics/BSPQueryTests.cs — 165 lines of
  PolygonWithNegativeSide_* tests.
- tests/AcDream.Core.Tests/Physics/ShadowObjectRegistryTests.cs —
  isBuilding propagation tests.
- src/AcDream.Core/World/WorldEntity.cs — IsLandblockBuilding field
  (no consumer once ShadowObjectRegistry.isBuilding is gone).
- src/AcDream.Core/World/LandblockLoader.cs — IsLandblockBuilding=true
  setter on building entities (kept BuildBuildingTerrainCells).
- src/AcDream.App/Rendering/GameWindow.cs — isBuilding: arg passed to
  ShadowObjects.Register.
- src/AcDream.Core/Physics/BSPQuery.cs — TryAdjustWalkableSide /
  IsWalkableAt helpers, their callers, the Path 5 / Path 6 neg-poly
  branch split, the BldgCheck-tied clearCell conditional, and the
  neg-poly ResolveCellPolygons writes.
- src/AcDream.Core/Physics/PhysicsDiagnostics.cs — neg-poly fields
  in the poly-dump format.
- src/AcDream.Core/Physics/TransitionTypes.cs — SpherePath.BldgCheck +
  SpherePath.HitsInteriorCell fields and every consumer, the
  savedBldgCheck try/finally around FindCollisions, and the neg-poly
  format additions to the dump-on-error helper.
- src/AcDream.Core/Physics/CellTransit.cs — FindCellSet overloads
  with hitsInteriorCell out-param and the BuildCellSetAndPickContaining
  out-param threading.

Kept (defensible correctness fixes + diagnostic infrastructure):
- src/AcDream.App/Rendering/GameWindow.cs — render-vs-physics cell
  origin split: the 0.02m render lift no longer leaks into physics
  BSP caching. lb.BuildingTerrainCells threaded into LandblockMesh.Build.
- src/AcDream.Core/World/LoadedLandblock.cs — BuildingTerrainCells
  record field.
- src/AcDream.Core/World/LandblockLoader.cs — BuildBuildingTerrainCells
  (cy*8+cx from LandBlockInfo.Buildings).
- src/AcDream.Core/Terrain/LandblockMesh.cs — hiddenTerrainCells
  param that collapses owned-cell triangles to a zero-area degenerate.
- src/AcDream.App/Streaming/{GpuWorldState,LandblockStreamer}.cs —
  mechanical BuildingTerrainCells threading through LoadedLandblock
  reconstructions.
- src/AcDream.Core/Physics/CellTransit.cs — multi-sphere
  FindTransitCellsSphere variant + multi-sphere AddAllOutsideCells +
  FindCellSet(IReadOnlyList<Sphere>, …) overload + the
  BSPQuery.SphereIntersectsCellBsp call for loaded neighbours. Matches
  retail CObjCell::find_cell_list / CEnvCell::find_transit_cells.
- src/AcDream.Core/Physics/TransitionTypes.cs — multi-sphere FindCellSet
  call site, retail-faithful CellId switch after CheckOtherCells, the
  outdoor-landcell terrain-walkable fallback in CheckOtherCells, and
  the full diagnostic suite ([step-walk], [walkable-nearest],
  [issue98-walkable-detail], [cell-set-summary], LastBspHitPoly
  emits).
- src/AcDream.Core/Physics/PhysicsDiagnostics.cs — ProbeStepWalkEnabled
  gate (ACDREAM_PROBE_STEP_WALK=1) + LogStepWalk helper + FormatVector
  / FormatPlane utilities. All emit-gated.
- src/AcDream.Core/Physics/BSPQuery.cs — diagnostic emits to
  LastBspHitPoly at four sites in SphereIntersectsPolyInternal /
  the placement adjustment path.
- Test files for the kept work: CellTransitFindCellSetTests,
  CellTransitFindTransitCellsSphereTests, PhysicsDiagnosticsTests,
  TransitionCheckOtherCellsTests, LandblockMeshTests,
  LandblockLoaderTests.

Verification:
- dotnet build: green, 0 errors, 3 pre-existing warnings.
- dotnet test: 1156 passed + 8 failed (baseline was 1148 + 8 pre-
  existing; the +8 passing are the new tests for the kept defensible
  work). Same 8 pre-existing failures, no new regressions.

Backup of pre-triage worktree state in stash@{0}.

A6.P3 #98 is still open; this is the apparatus-prep step, not a fix.
Next: cell-dump probe (Step 2 of the plan).
2026-05-23 15:11:49 +02:00
Erik
111aa3e59d docs(handoff): A6.P3 issue #98 — slice 6 failed; pivot to terrain-mesh
Tonight's slice 6 session attempted 6 variations of placement-insert
bypass in Transition.FindEnvCollisions + Transition.DoStepUp. None
unstuck the player at the cellar ramp top despite mechanically firing
the bypass up to 72 times per session. Reverted all variants; nothing
shipped tonight beyond this handoff.

The hard finding: the placement-insert path is a SYMPTOM, not the
cause. Bypassing it (in 6 ways) doesn't make the sphere climb the
cellar ramp. The first-order question — why doesn't the sphere
progress UP the ramp via normal slope-walking? — wasn't addressed.

User's most actionable clue (not yet investigated): "outside ground
covers only the open path down into the cellar" → suggests a missing
hole in the outdoor terrain mesh over the cellar entry. That's a
terrain-generation bug, completely separate from BSPQuery.FindCollisions.

Handoff doc captures:
  - The 3-session diagnosis evolution (each previous session's
    confident diagnosis was wrong)
  - All 6 slice-6 bypass variants tried and why each failed
  - What we KNOW (data-confirmed) vs what we DON'T KNOW (open
    questions)
  - Specific next-step investigation order with terrain-mesh as #1
  - Pickup prompt with strong "don't re-attempt placement-insert
    bypass" guard

Test baseline 1148 + 8 unchanged. Slice 5 probe (cf3deff) remains
committed as the durable diagnostic infrastructure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 08:56:43 +02:00
Erik
7e3ab53924 ci(hygiene): allow dotnet ecosystem (nuget+telemetry) so build/test can run
Yesterday's run reported Finding #5 because the sandboxed agent
runner couldn't reach api.nuget.org or
dc.services.visualstudio.com, so 'dotnet restore' failed and the
build/test hygiene check produced no signal. Added the 'dotnet'
ecosystem identifier to network.allowed so nuget restore + telemetry
are reachable inside the sandbox.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 08:48:37 +02:00
Erik
1acb3a525f ci: add GitHub Agentic Workflows scaffolding + daily hygiene assessment
Adds gh-aw (GitHub Agentic Workflows) framework files plus an AI-driven
daily hygiene-assessment workflow that scans main for:
  1. Workaround patterns (CLAUDE.md forbids without approval)
  2. Ungrounded retail-port code (no decomp citation)
  3. Roadmap / phase / CLAUDE.md "currently working toward" drift
  4. Test / build hygiene (warnings, test count regression)
  5. Architecture drift (cross-layer references, WB imports outside Wb/)

Output: one rolling GitHub issue per day, labelled ai+hygiene; the
framework auto-closes the prior day's report. Engine: copilot
(gpt-5.3-codex) — uses your Copilot subscription, no separate API key
needed.

Scaffolding from gh aw init:
  - .gitattributes          (marks .lock.yml as generated)
  - .github/agents/         (dispatcher agent)
  - .github/mcp.json        (MCP server config)
  - .github/workflows/aw.json (ghes:false — we target GitHub.com)
  - .github/workflows/copilot-setup-steps.yml
  - .vscode/settings.json   (editor settings)

Workflow:
  - .github/workflows/hygiene-assessment.md     (human-authored source)
  - .github/workflows/hygiene-assessment.lock.yml (compiled artifact)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:31:13 +02:00
Erik
cf3deff7c2 feat(phys): A6.P3 slice 5 — [place-fail] probe + sharpened #98 diagnosis
Add ACDREAM_PROBE_PLACEMENT_FAIL gate + LogPlacementFail emitter +
side-channel polygon attribution in PhysicsDiagnostics. Wire into
BSPQuery.FindCollisions Path 1 (Placement/Ethereal) on Collided
returns; wire into Transition.DoStepDown after the placement_insert
TransitionalInsert(1) call; wire into Transition.FindObjCollisions
to emit per-static-object [place-fail-obj] lines.

Run scen4 cellar-up with the probe → 168 [place-fail] events. 80 of
81 BSPQuery Path 1 placement rejections cite polygon 0x0020 in
cellar cell 0xA9B40147's BSP: n=(0,0,-1) d=-0.2, world Z=93.82 —
the cellar ceiling (underside of cottage main floor thickness layer).
0 [place-fail-obj] lines, confirming the failure source is the cell
BSP not a static object.

The probe-driven evidence INVALIDATES the 2026-05-22 morning
handoff's "Path 5 vs Path 6 in BSPQuery.FindCollisions" diagnosis.
Retail's BP4 trace shows every find_collisions hit has collide=0 —
retail enters the same Contact branch we do, no outer-dispatcher
divergence. Retail's BP5 fires 17+ times on the cellar ramp polygon,
not "30 hits all on flat planes" as morning claimed.

The actual divergence is downstream in cell-promotion: retail's
check_cell transitions to cottage cell 0xA9B40146 during the ascent
(BP7 sets ContactPlane to the cottage main floor poly, which lives
in cottage cell's BSP not cellar's). Ours stays at cellar 0xA9B40147,
where the ceiling poly 0x0020 correctly rejects the lifted sphere.

No fix attempted this session per CLAUDE.md discipline check
(3+ failed fixes = handoff). Full slice 5 evidence + concrete
next-session pickup steps at docs/research/2026-05-22-a6-p3-slice5-handoff.md.
ISSUES.md #98 updated with the corrected diagnosis.

Test baseline: 1148 + 8 pre-existing fail. Maintained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 20:02:15 +02:00
Erik
c479ea68a3 docs(handoff): A6.P3 2026-05-22 EOS handoff + pickup prompt for #98 fix
Comprehensive handoff doc covering today's full A6.P3 work:
  - 13 commits shipped today (slice 2 + slice 3 + slice 4 probes +
    diagnosis)
  - Issue #98 sharply diagnosed via paired retail+acdream cdb captures:
    BSP path-selection bug (Path 5 vs Path 6) at BSPQuery.FindCollisions
    dispatcher
  - All 4 A6.P2 findings status updated (Findings 1, 3 closed; Finding 2
    partially closed + accepted divergence; Finding 4 = issue #95
    separate scope)
  - Failed fix attempts log so next session doesn't re-attempt dead ends
  - Concrete starting steps + file references for the next session
  - Pasteable pickup prompt at the bottom

CLAUDE.md "Currently working toward" block updated to reflect slice 3
ship + #98 sharp diagnosis + handoff doc pointer.

Test suite: 1148 + 8 pre-existing fail (baseline maintained).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:32:02 +02:00
Erik
efb5f2c3b8 docs(issues): #98 sharpened diagnosis — Path 5 vs Path 6 path-selection bug
Updates issue #98 with the sharp diagnosis from the retail cellar cdb
trace (commit 134c9b8):

The bug isn't cell-resolver, isn't walk_interp, isn't dat-fidelity.
It's BSP path-selection: our dispatcher picks Path 5 (Contact step_up)
for the cellar ramp polygon when retail picks Path 6 (find_walkable
land). The ramp is walkable (N.Z=0.695 > FloorZ=0.6642) so Path 6 is
the correct choice. Investigation continues in next session at
BSPQuery.FindCollisions path-selection logic.

Also documents failed fix attempts this session as informational so
next session doesn't re-attempt the same dead ends.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:26:26 +02:00
Erik
134c9b87f3 capture(research): retail cellar-up trace for issue #98 — SHARP diagnosis
User walked retail UP the same Holtburg cottage cellar that acdream
gets stuck on. cdb captured retail's BSP behavior for paired
comparison against the acdream polydump trace (0b44996).

Retail (successful walk):
  BP1 transitional_insert: 2,651
  BP2 step_up:                29  (incl. 1 hit on the ramp slope, n.z=0.6950)
  BP4 find_collisions:     4,032
  BP5 adjust_sphere:          30  (ALL on FLAT planes; ZERO on the ramp)
  BP6 check_walkable:         25
  BP7 set_contact_plane:      18  (ALL set the SAME flat plane:
                                   (0,0,1) d=-93.9998 = world Z=94 =
                                   cottage main floor)

Acdream (stuck — from scen4_cottage_cellar_polydump):
  cp-write:                  229,300
  push-back:                  ~1000 (270 on the RAMP slope poly 0x0008)
  step_up_slide:                159

THE DIVERGENCE — pinpointed:

Retail's BSP path-selection for the cellar ramp picks Path 6 (find_walkable
land) — the ramp is treated as a walkable floor to LAND ON. Result:
BP7 sets the contact plane to the cottage main floor (Z=94). No push-back
needed on the ramp.

Our BSP picks Path 5 (Contact → step_up → adjust_sphere push-back) for the
SAME ramp polygon. Result: 270 push-backs against the ramp slope; step_up
keeps failing → step_up_slide loop → player stuck.

NEXT STEP (new session): trace why our BSP picks Path 5 instead of Path 6
for the ramp. Likely in BSPQuery.FindCollisions dispatcher's
path-selection logic. The ramp is walkable (N.Z=0.695 > FloorZ=0.6642) so
Path 6 should fire. Maybe a wrong ObjectInfo state flag, or a sub-step
order issue, or the ramp polygon's BSP-side classification is wrong.

This capture + the polydump capture give a complete picture for the next
investigation session. No more guess-fixes today — the data is now sharp.

Test suite: 1148 + 8 (unchanged this commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:25:57 +02:00
Erik
bbd1df46e0 fix(phys): A6.P3 slice 4 — reset WalkInterp before placement_insert (issue #98)
Hypothesis-driven fix for issue #98 (cellar ascent stuck at top step).

Symptom (from polydump trace at scen4_cottage_cellar_polydump):
- Player walks up cellar ramp (real 46-degree slope per dat verification)
- Hits ramp polygon 0x0008 in cellar cell 0xA9B40147 270 times
- Each hit: sphere center lifted 0.75m onto ramp surface, all walk_interp
  consumed (winterp 1.0 -> 0.0)
- Step_up_slide fires 159+ times trying to recover
- Player physically stuck — never advances forward

Bug hypothesis: in DoStepDown, after the primary TransitionalInsert(5)
probe consumes WalkInterp down to 0 (the 0.75m lift), the placement_insert
call runs with WalkInterp=0. AdjustSphereToPlane's interp check
(`interp >= path.WalkInterp` where interp=0) then rejects any push-back
needed for the placement validation -> placement fails -> step_up returns
failure -> step_up_slide loop -> player stuck.

Fix: reset WalkInterp = 1.0 before the placement_insert call (mirrors
retail step_down's walk_interp = 1 reset at function entry, which is what
the placement_insert runs after in retail's flow).

Test suite: 1148 + 8 (baseline maintained).
Visual verification: pending user re-test of cellar-up walk.

If this fixes cellar-up, also likely improves other step-up-onto-slope
scenarios. If it doesn't fix cellar-up, the bug is elsewhere in the
step-up/step-down flow (separate investigation needed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:07:06 +02:00
Erik
8bd311759e fix(phys): A6.P3 slice 3 v3 — REVERT stickiness; hypothesis-test cellar-up
Slice 3 v2 (point-in stickiness) closed the cell-resolver ping-pong
(data confirmed: scen4_cottage_cellar_slice3v2 capture shows 1 cell-
transit vs 20+ pre-fix). BUT user verification revealed: cellar-up
symptom transitioned from "stuck-at-top-ping-pong" (pre-slice-3) to
"never-reach-top-stuck-in-cellar" (post-slice-3). Stickiness was
holding player in cellar cell so aggressively that the legitimate
transition to the cottage main floor cell at the ramp top never
fired.

Reverting the stickiness check entirely. Trade-off:
- Inn doorway ping-pong returns (existed pre-slice-3; lesser evil)
- Player can again reach the top of the cellar ramp (per pre-slice-3
  user observation)
- Issue #98 cellar-up remains open — but with sharper diagnosis: it's
  not the cell resolver at all, it's deeper (BSP step-physics or
  AdjustOffset slope-projection at the cottage main floor boundary,
  per slice 4 polydump trace showing repeated push-back on the
  46-degree ramp polygon)

The slice 3 stickiness premise was correct but the implementation
shape was wrong. A future attempt needs either:
- A "near boundary" gate (only stick when sphere is deep inside cell)
- A retail-faithful per-cell hysteresis matching CObjCell::find_cell_list
  Position-variant (acclient_2013_pseudo_c.txt:308742-308783) more
  exactly than point-in
- OR address the underlying BSP step-physics bug first; then ping-pong
  may not even need a stickiness fix

Test suite: 1148 + 8 (baseline maintained).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:43:58 +02:00
Erik
319847289e diag(phys): A6.P3 slice 4 — extend [cell-cache] probe with portalTargets
Extends the existing [cell-cache] probe (gated by ACDREAM_PROBE_CELL_CACHE=1)
to also dump the list of portal targets per cell: which other cells
each portal connects to, the portal polygon id, and the flags.

Output format (appended to existing [cell-cache] line):
  portalTargets=[(cell=0xNNNN,poly=0xNNNN,flags=0xNNNN),...]

Purpose: investigating issue #98 (cellar-up stuck at top of ramp).
We now know the polygon geometry is correct (per slice 4 polydump
capture: the cellar is a real 46° ramp). Question is whether the
cellar cell has a portal to the cottage main floor cell, and where
that portal is. If portalTargets shows no connection to the expected
upstairs cell, that's the bug.

Test suite: unaffected (probe is gated off by default).
Build: green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:31:34 +02:00
Erik
0b449968a7 diag(phys): A6.P3 slice 4 — add [poly-dump] probe for #98 investigation
Adds a polygon-geometry dump probe that fires alongside [push-back]
whenever AdjustSphereToPlane lands a push-back. Gated by
ACDREAM_PROBE_POLY_DUMP=1.

Output format:
  [poly-dump] cell=0xA9B40147 polyId=0x0042 numPts=4 sides=Single
              n=(0.000,-0.719,0.695) d=-0.1007
              verts=[(x1,y1,z1),(x2,y2,z2),(x3,y3,z3),(x4,y4,z4)]

Purpose: investigate #98 (cellar-up stuck at top step). The push-back
trace shows the player hitting a sloped surface n=(0,-0.719,0.695) at
the cellar stair top. Two possibilities:
  1. The polygon really IS sloped 44° in the dat (genuine geometry).
  2. Our dat-read produces wrong vertices → wrong normal → wrong plane.

The dump lets us:
- Identify which dat polygon was hit (cell + poly ID)
- Compare our extracted vertices against WorldBuilder's straight-from-
  dat read for the same poly
- Or spawn the cell in ACViewer to visually verify the geometry

Changes:
- Added `ushort Id` property to ResolvedPolygon (defaults to 0 for test
  fixtures that don't care; production code in PhysicsDataCache.cs +
  BSPQuery.cs sets it from the dictionary key).
- Added ProbePolyDumpEnabled + LogPolyDump in PhysicsDiagnostics.
- Wired the dump into AdjustSphereToPlane's apply-branch (after the
  existing push-back log; same gating pattern).

Test suite: 1148 pass + 8 pre-existing fail (baseline maintained).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:17:47 +02:00
Erik
ceeb06be7d ship(phys): A6.P3 slice 3 — cell-resolver ping-pong CLOSED + #98 re-diagnosed
Slice 3 v2 (3e140cf) added point-in cell-stickiness in
ResolveCellId's indoor branch. User verification + slice3v2 capture
confirms: cell-resolver ping-pong is FULLY CLOSED.

Data:
- scen2_v2 capture (pre-slice-3): 20+ cell-transit events with
  rampant ping-pong (0xA9B4014B ↔ 0xA9B4014A ↔ 0xA9B4013F at the
  cellar boundary, Z stable ~96.4 — same tick re-classification)
- slice3v2 capture (post-fix): 1 cell-transit event (login teleport
  only) — ping-pong fully eliminated

Findings:
- A6.P2 Finding 3 (cell-resolver sling-out family) CLOSED.
- Issue #90 (sphere-overlap stickiness workaround in same function)
  now redundant; can be removed in A6.P4 after broader visual
  verification.
- Issue #97 (phantom collisions + fall-through on 2nd floor) hypothesis
  pending: same instability family, likely closed as side-effect of
  this fix. Re-test on next happy-test session.
- Issue #98 (cellar-up stuck) PERSISTS but with NEW DIAGNOSIS.
  Originally filed as cell-resolver ping-pong (which was true and now
  fixed), but user verification shows the cellar-up symptom remains
  with a DIFFERENT root cause: BSP step-physics at the cellar stair
  TOP. Push-back trace from slice3v2 capture:
    n=(0, -0.719, 0.695) sloped face (walkable per FloorZ=0.664)
    delta=(0, 0, 0.75) step-down probe lifts sphere by 0.75m
    winterp=1.0->0.0 entire walk-interp consumed per tick
  Player progresses up most of the stairs but blocks at top step
  where the cellar transitions to the cottage main floor. #98 issue
  updated with this re-diagnosis.

Includes:
- scen4_cottage_cellar_slice3 acdream.log (slice 3 v1 evidence;
  ping-pong already closed by v1's sphere-overlap stickiness, but
  v1 over-corrected by holding player in cellar during legitimate
  transitions)
- scen4_cottage_cellar_slice3v2 acdream.log (slice 3 v2 evidence;
  point-in stickiness fixes the over-correction; cellar-up reveals
  the deeper BSP step-physics bug)

Docs updated:
- ISSUES.md — #98 re-diagnosed
- docs/plans/2026-04-11-roadmap.md — A6.P3 slice 3 marked SHIPPED;
  slice 4 (or A6.P4) scoped for #98 step-physics investigation
- CLAUDE.md — Currently-working-toward block updated

Test suite: 1148 pass + 8 pre-existing fail (baseline maintained).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:11:56 +02:00
Erik
3e140cfe71 fix(phys): A6.P3 slice 3 v2 — point-in stickiness (was sphere-overlap)
Slice 3 v1 (8898166) used SphereIntersectsCellBsp for the stickiness
check. User verification showed: ping-pong WAS closed (3 cell-transit
events vs 20+ pre-fix) but user still couldn't walk up out of cellar
because the stickiness was OVER-CORRECTED — the sphere still partially
overlapped the cellar cell at the top of stairs, so stickiness held the
player in the cellar even when the center had transitioned to the
cottage main floor cell.

Fix: switch the stickiness check from SphereIntersectsCellBsp (sphere
overlap) to PointInsideCellBsp (center-in). Matches FindCellList's
own semantics for "which cell are you in." Player stays in fallback
only while center is still inside fallback's BSP volume.

Trade-off:
- More permissive transitions (good — cellar-up works)
- Less aggressive stickiness, so some boundary ping-ponging may return
  IF the sphere center oscillates across the boundary (rare; would
  require sub-mm Z drift across the boundary line)

If the trade-off bites (ping-pong returns somewhere), the fix is a
small geometric margin around the point-in check — but verify before
adding.

Test suite: 1148 pass + 8 pre-existing fail (baseline maintained).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:04:51 +02:00
Erik
88981669fe fix(phys): A6.P3 slice 3 — cell-resolver stickiness for ping-pong fix
Closes A6.P2 Finding 3 (cell-resolver instability) + issue #98 (cellar
ascent stuck at last step) + likely closes #97 (phantom collisions +
fall-through on 2nd floor; same instability family).

Adds a cell-stickiness check at the top of ResolveCellId's indoor
branch: before re-resolving via FindCellList, check if the fallback
(previous-tick) CellId's BSP still validly contains the sphere. If
yes, return fallbackCellId immediately — preserves cell membership
when the sphere is at a boundary where multiple cells overlap.

The bug: at cell boundaries (cellar last step, indoor doorways,
between two adjacent indoor cells), the sphere overlaps multiple
cells geometrically. FindCellList's candidate-iteration order
(HashSet, implementation-defined) determines which cell wins. That
order may shift tick-to-tick → CellId ping-pong → AdjustOffset
operates against a different cell's geometry each tick → player
can't accumulate forward motion → stuck.

Evidence: scen3_inn_2nd_floor_slice2v2 capture shows the ping-pong
chain at the cellar boundary:
  0xA9B4014B → 0xA9B4014A → 0xA9B4013F → 0xA9B4014A → 0xA9B4014B
  (Z stable ~96.4; CellId oscillates every tick; reason=resolver)

Retail oracle: cell-array hysteresis pattern from
CObjCell::find_cell_list Position-variant at
acclient_2013_pseudo_c.txt:308742-308783. Retail preserves cell
membership when sphere is close to (but slightly past) cell
boundaries.

Implementation: 9 lines added (sphere-overlap check against
fallbackCellId's CellBSP before falling through to FindCellList).
Existing #90 workaround at line 299-300 (post-FindCellList sphere-
overlap check) is now redundant in the common case but kept for
safety; deferred to A6.P4 removal after visual verification.

Test suite: 1148 pass + 8 pre-existing fail (baseline maintained).
Visual verification: pending — user happy-test will confirm cellar-
up walk succeeds + no ping-pong in cell-transit log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:01:28 +02:00