Commit graph

2363 commits

Author SHA1 Message Date
Erik
9bd3a98363 docs: close #193 (OOM — Opacities leak, measurement-verified) + file #194
#193 FIXED (119a2326), verified by before/after dotnet-counters on the same
6-min churny roam: LOH 1.1→6.1 GB climbing (leaked) vs 0.24→0.64 GB then FLAT
(fixed); no crash. #194 filed: WbDrawDispatcher._groups never pruned (LOW,
bounded, not a crash risk) — surfaced during the #193 heap analysis.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 14:24:30 +02:00
Erik
119a2326be fix(#193): clear InstanceGroup.Opacities each frame — stop the ~1 GB/min LOH leak
Root cause (measured, not guessed): the OOM was a MANAGED Large-Object-Heap leak
(~5 GB in 6 min of roaming; working set 1.6 GB -> 7.6 GB), dominated by ~3.8 GB of
live System.Single[]. A heap-retention analysis of a captured gcdump traced 241 of
242 giant float arrays to a single List<float>: WbDrawDispatcher.InstanceGroup.Opacities.

#188 (2026-07-09, fading doors) added Opacities as a 5th per-instance parallel list
alongside Matrices/Slots/LightSets/IndoorFlags, but left it out of the per-frame
clear loop (WbDrawDispatcher.cs:959). So Opacities.Add(1.0f) fired once per drawn
instance per frame and the list never reset — growing forever; List<float> capacity
doubling produced the observed ~128 MB / ~512 MB power-of-two arrays and OOM after
~50 min. Hit both UI builds because it's core render, and started only recently
because Opacities did not exist before #188.

Fix: extract the per-frame reset into InstanceGroup.ClearPerInstanceData() (promoted
InstanceGroup private->internal) that clears ALL FIVE parallel lists in one place, so
a future 6th list can't silently drift out of the frame lifecycle again. Line 959 now
calls it. TDD: InstanceGroupClearTests asserts every parallel list resets (RED with
the old 4-list clear, GREEN after adding Opacities.Clear()).

Static-audit note: this is exactly why we measured before fixing — the audit ranked
GlobalMeshBuffer (GPU) #1 and the entity-dict leak #3; both were wrong about the
dominant cause. The dotnet-counters (managed vs native) + gcdump retention graph
named the real one-line culprit.

Secondary (NOT this commit): _groups dictionary is never pruned (bounded by distinct
group keys, empty groups are cheap) — filed as a follow-up, not the multi-GB driver.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 14:15:10 +02:00
Erik
02487da0ce docs: file #193 — client OOMs after ~50 min of play (two crashes 2026-07-10)
Two reproducible Out-of-memory crashes in one evening (Release build): one after
an extended dev-UI session, one after ~50 min of retail-UI play. Cause
uncharacterized. Filed post-M2 (stability, not M2 critical path).

Explicitly warns against the retracted "entity leak" reading: the smoke plugin's
"saw N entities total" is a cumulative spawn-EVENT counter, not residency.
Investigation must measure actual managed + unmanaged/GPU memory growth over
time, capture-first. May have contributed to tonight's phantom door-regression
(memory pressure -> GC thrash -> dropped inbound motion).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:03:08 +02:00
Erik
9a772d2b2a docs: M1.5 "Indoor world feels right" LANDED 2026-07-10 — advance to M2
User-gated the full dungeon round-trip (enter via portal -> navigate rooms ->
exit to the outdoor world), completing the #138 acceptance. Both M1.5 demo
halves are now confirmed: building/cellar (prior sessions) + full dungeon
round-trip (2026-07-10). Milestone-crossing artifacts per the milestones-doc
discipline:

- Pinned M1.5 landing writeup + flipped header to  LANDED; caveats explicit
  (#145-residual far-town teleport-OUT + #116 slide-response carried as
  post-M1.5 follow-ups, NOT blockers).
- Freeze list applied: all M1.5 phases (indoor render Option A, A6/A6.P4
  physics, A7 lighting, full Phase G.3 dungeon support) now frozen per rule 2.
- M2 "Kill a drudge" flipped to ACTIVE; first ports = CombatMath.ComputeDamage
  (F.3) + inventory panel (F.2) + combat anim (L.1c).
- CLAUDE.md "currently working toward" advanced to M2.
- #138 closed (user gate).

No demo video (per the user's standing preference).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:58:48 +02:00
Erik
113569b482 docs(#138): record 2026-07-10 partial gate — enter+navigate confirmed, exit gate pending
User re-confirmed the ENTER + NAVIGATE half of the dungeon round-trip works
(retail-UI session). The EXIT half (portal-out to outdoor, esp. far-town where
#145-residual lurks) wasn't exercised, so #138 stays open and M1.5 does not land
until the exit gate passes. Marks exactly where next session resumes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:49:19 +02:00
Erik
5e271dd719 docs: reconcile M1.5 tracking with git reality — G.3 dungeons SHIPPED, not "broken"
The milestones doc, roadmap, CLAUDE.md banner, and #138's ISSUES cross-ref had
drifted ~4 weeks behind shipped work — they still claimed "dungeons don't work
AT ALL / terrain-less landblocks unsupported (#133)", which sent this session
chasing a phantom door "regression" for an hour. Git reality (and tonight's live
dungeon session) refute it:

- #133 teleport-into-dungeon DONE (G.3a hold-until-hydrate FSM 7947d7a/2ce5e5c8/47ae237e);
  the terrain-less premise was refuted at design time (90786c19: dungeon LandBlock
  is flat-terrain, streams via the existing pipeline).
- #95 portal-graph visibility blowup RESOLVED (9.1M→~39k instances, a40c38e8).
- #137 dungeon collision CLOSED (0efa7ed2); doors #187/#188; A7 lighting #79/#93/#80/#154 CLOSED.
- #138 teleport-OUT ESSENTIALLY RESOLVED in code — the remaining M1.5 gap is one
  RECORDED end-to-end round-trip user gate, not broken code.

Also fixes a numbering trap: #138's body cited "#146/#147" for its collision
residuals, but those numbers are D.2b inventory-polish issues in this worktree;
the real residuals are #152 (portal-in walls, DONE 49d743f) + #151 (far-town
walls, FIXED 9743537). Sole live residual now surfaced: far-town teleport-OUT
arrival cascade (#145-residual, REOPENED — capture-harness-first).

Historical working-notes annotated [SUPERSEDED] rather than deleted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:45:05 +02:00
Erik
217a4bad69 Merge branch 'main' into claude/peaceful-visvesvaraya-e0a196
# Conflicts:
#	docs/ISSUES.md
#	docs/architecture/retail-divergence-register.md
2026-07-09 23:18:52 +02:00
Erik
0ecb729527 docs: close #192 (login streaming race) — user visual gate passed
"Looks good." Filed directly to Recently closed since the fix already
shipped and gated this session (fa9aedca).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 16:12:31 +02:00
Erik
fa9aedca0a fix(#192): close login streaming race — gate on a known real position, not session state
User-reported: logging in at a non-Holtburg position sometimes showed
stabs/scenery floating in the wrong place. Root-caused via a threading/
lifecycle trace, not inference: WorldSession transitions to InWorld
immediately after the login handshake (WorldSession.cs:608) — well
before the player's own spawn CreateObject (which carries their real
position) has arrived over the network. The streaming gate's old
condition (`!IsLiveModeWaitingForLogin || liveInWorld`) opened the
instant InWorld fired, letting the background streaming worker
(LandblockStreamer's dedicated Thread) build real landblocks using
whatever _liveCenterX/Y held at that moment — the Holtburg startup
placeholder, not a "not known yet" sentinel. Landblocks that started
building in that window bake their world offset from that placeholder
at build time; if their build was still in flight when the real spawn
arrived and recentered the world (ForceReloadWindow, which only
unloads already-RESIDENT landblocks), they finished and got applied
anyway — stale-positioned geometry landing wherever the guess put it
relative to whatever streamed in afterward with the corrected center.

Confirmed with the user this wasn't "wrong placeholder, need a better
one" — any placeholder racing against the real answer reproduces the
same bug. The fix removes the race instead: StreamingReadinessGate
.ShouldStream requires an explicit liveCenterKnown flag (true only once
the player's own spawn has been processed) in addition to liveInWorld.
Nothing streams in live mode until the real position is confirmed — no
placeholder value is ever acted on. Preserves the #106 gate-3 fix this
gate originally existed for (auto-entry waits for terrain under the
spawn; terrain streaming must not wait for chase mode in turn, or the
two deadlock) since liveCenterKnown becomes true independently of chase
mode, driven purely by the spawn packet's arrival.

The stricter render gate (GameWindow.cs:9596, hides ALL world geometry
until chase mode engages, no relaxation) already provided a partial
safety net and is unchanged — this fix stops the stale geometry from
ever being built, rather than relying on the render gate to hide it
until the reveal.

Core 2680+2skip / App 741+2skip / UI 425 / Net 385 green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 15:57:43 +02:00
Erik
3dcd53cab7 docs: bulk issues triage — close 27, note 10 investigated-open
User-driven triage pass over ~60 open issues: 19 closed from the
user's own recollection, 8 more closed after this session's parallel
investigation confirmed the underlying bug/task no longer applies
(#96 accepted-divergence reclassification, #60/#68/#126/#4/#81/#33
fixed by commits that never cross-referenced their issue number, #87
superseded by Phase O + Phase A8). 9 issues investigated and confirmed
still genuinely open got a dated triage note appended in place
(#116/#104/#178/#29/#146/#147/#148/#156/#72); #41 got a user-note only.

Two investigated issues (#148, #156) turned up real completed fixes
sitting on an unmerged branch (claude/peaceful-visvesvaraya-e0a196,
11 commits, diverged from main 168 commits ago) — flagged in their
triage notes and separately to the user, not acted on here.

Verified before committing: header count unchanged (200 before/after,
no blocks lost or duplicated), spot-checked 6+ closures across every
category (user-closed, investigation-closed, stayed-open-with-note,
the #87 duplicate-heading edge case) for accuracy against the
underlying investigation evidence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 13:52:41 +02:00
Erik
c4058203f4 docs: update Current state — #137 + A7 lighting closed, #138 is the sole M1.5 critical-path item left
#137 was already closed in a prior session (0efa7ed2) but this banner
hadn't been updated; A7 dungeon lighting (#79/#93) closed this session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 13:18:01 +02:00
Erik
44cced715c docs: close #93 and #80 (indoor lighting) — user re-verified 2nd-floor fix
"Number 80 is closed, I verified again." Moved #93 (the lighting
umbrella) and #80 (2nd-floor darkness) to Recently closed. #94
(held-item spotlight) is NOT folded into this closure and stays open —
user confirmed acdream doesn't support equipping hand-held items yet,
so it's currently untestable; marked BLOCKED rather than an active A7
target so it doesn't keep resurfacing as unfinished lighting work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 13:17:25 +02:00
Erik
754b299f7a docs: close #189 (fountain water spray) — user visual gate passed
"Fountain is back! We can close it." Root cause was #190 (entity-id
overflow), not the light-carrier hydration fix directly. Moved to
Recently closed with the mechanism summary; the candle-flame
identification sub-thread is noted as an unraised residual rather than
a new open issue, since the user didn't re-flag it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 12:44:13 +02:00
Erik
6e034ac610 docs: A7.L1 session bookkeeping — close #190, update #93/#189, file #191
- #190 (interior entity id overflow) moved to Recently closed with full
  evidence trail — found + fixed same session as #189's investigation.
- #189 (missing particles) updated: root cause was #190's id aliasing,
  not the light-carrier hydration fix itself; fix shipped but the
  fountain's water spray hasn't been re-confirmed visually yet (session
  moved to an FPS question, then a movement bug, before circling back).
  The "candle" identification sub-thread stays open regardless.
- #93 (indoor lighting umbrella) updated earlier this session with the
  two A7.L1 root causes + fixes; #80/#94 still need re-verification
  before it can close.
- #191 filed: tapping W briefly glides forward without playing the step
  animation (retail: single visible step). Different subsystem
  (movement/animation, not rendering) — not investigated this session,
  filed to keep it from interrupting A7 lighting/particle work.
- Roadmap Phase A7 progress note added earlier this session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 12:40:42 +02:00
Erik
e651cb6dd1 fix(#190): interior entity id counter overflowed past its 8-bit budget, aliasing into the next landblock
Found while investigating #189 (missing fountain/candle particles):
reverting the A7.L1 light-carrier hydration fix (9ebb2060) made the
Town Network fountain's water-spray particle work again, which didn't
fit the earlier dat-truth finding that the fountain's own entity was
never touched by that fix. Traced with ACDREAM_DUMP_ENTITY: the
fountain's hydrated entity.Id shifted between reverted (0x400007F8)
and fixed (0x40000815) builds — a 29-id delta matching the extra
mesh-less light carriers the A7.L1 fix now keeps alive earlier in the
same landblock's hydration pass.

Root cause: GameWindow's interior-entity id scheme
(interiorIdBase + localCounter, "0x40XXYY##") reserves only 8 bits
(256 values) for a landblock's ENTIRE interior static population — a
residual explicitly flagged in the #119 fix's own comment ("counter
overflow past 0xFF still bleeds into the lbY byte"). The Town Network
hub (205 cells, one landblock) already sat at 248 before A7.L1; the
light fix pushed it to 277, past the boundary. 0x40000815 decodes as
landblock Y=0x08 — NOT this dungeon's true Y=0x07 — the exact #119
cross-landblock aliasing bug, reincarnated by entity count instead of
a computation bug. EntityScriptActivator keys particle-script
instances by entity.Id directly (no landblock-hint disambiguation
unlike the #119 batch cache), so the aliased id silently broke the
fountain's script tracking.

Fix: AcDream.Core.World.InteriorEntityIdAllocator widens the counter
8->12 bits (256->4096) by shrinking the fixed class prefix from a
full byte (0x40) to its top nibble (0x4_) — verified safe against
every entity.Id classification check in GameWindow (none decode X/Y
back out, they only check thresholds/prefixes). Added a loud
one-time [id-overflow] log if a landblock ever exceeds the new
budget, so this class of bug can never hide silently again.

Core 2675+2skip / App 741+2skip / UI 425 / Net 385 green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 12:21:57 +02:00
Erik
f6b054b7d7 docs(#79/#93): dat-truth check refutes the mesh-gate theory for missing fountain/candle particles
Same-session follow-up: user confirmed the light-carrier hydration fix
worked, then reported missing candle flames + fountain water particles.
Tested whether it's the same root cause (a mesh-empty Setup dropped by
EntityHydrationRules before its Setup.DefaultScript — the ambient
particle script GpuWorldState.cs:221 fires — is ever read). Refuted: the
fountain (0x02000AA3) has a surviving mesh part and a real DefaultScript
(0x33000B21), never dropped by the gate. The guessed "candle" objects
(0x02001967, ring of 16 around the fountain) have real mesh and no
DefaultScript at all — not candles. Separate root cause; filed as its
own issue rather than chased further this session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 11:44:03 +02:00
Erik
9ebb206086 fix(#79/#93): mesh-empty hydration gate was dropping light-only fixtures
Root-caused via dat-truth inspection, not inference: the A7.L1 visible-
cell scoping fix (previous commit) had zero visual effect because the
Town Network fountain room (cell 0x00070144) registers ZERO lights of
its own — confirmed directly against the dat, not assumed. The room's
one dat-authored fixture, Setup 0x02000365 (a ceiling light 5m above the
fountain, warm color, intensity 100), has a single visual part that is a
#136-class runtime-hidden marker. Flattened mesh ref count: 0. GameWindow
's per-stab hydration loop treated meshRefs.Count==0 as "doesn't exist"
and dropped the whole entity before the Setup's Lights were ever read —
so a mesh-less "light attach point" fixture, a normal AC dat authoring
pattern, could never register. Retail's light registration (add_light,
CEnvCell::UnPack) is architecturally independent of a fixture's own mesh
visibility.

Fix: track the stab's Setup.Lights.Count alongside meshRefs during
hydration; keep the entity (with empty MeshRefs — nothing to draw, still
something to light) whenever either is nonzero. Extracted the decision
into EntityHydrationRules.ShouldKeepEntity (pure, unit-tested) since
GameWindow's hydration loop isn't independently testable. Confirmed no
downstream consumer assumes MeshRefs.Count >= 1 (WbDrawDispatcher already
guards on it before any indexing).

Core 2666+2skip / App 741+2skip / UI 425 / Net 385 green. Apparatus:
Issue93TownNetworkFountainRoomLightInspectionTests (dat-truth dump,
reusable for other rooms in this class).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 11:11:16 +02:00
Erik
d275ed554e feat(A7.L1): scope the point-light pool by last frame's visible cells (#79/#93/#176/#177)
The Town Network hub (498 registered fixtures) starved the player's own
room: BuildPointLightSnapshot's player-nearest-128 cap sorts by raw
Euclidean distance, which isn't a reliable proxy for "same room" in a
dense maze — a fixture on the other side of a wall can be geometrically
closer than the room's own torches and win the cap. LightSource.CellId
tagging and the [indoor-light] membership probe already existed from the
c500912b/#176 arc; the missing piece was a candidacy filter.

BuildPointLightSnapshot(playerWorldPos, visibleCells) now narrows
candidates to the frame's actual visible cells before the existing
dynamics-first player-nearest cap runs (cell-less lights, e.g. the viewer
fill, always included). GameWindow feeds LAST FRAME's already-rendered
RetailPViewFrameResult.DrawableCells — one frame of latency instead of
re-threading a mid-DrawInside callback, which was the exact mechanism
(c500912b) that caused the earlier #176 seam-floor flicker regression.
The distance-sort anchor stays the player, unchanged.

AP-85 updated in place (third revision) rather than adding a new row —
same underlying divergence, now with the render-visibility approximation
of retail's true DBObj-load/flush-bounded resident registry documented
alongside its residual risk (one unscoped frame on portal re-entry).

Core 2652+2skip / App 741+2skip / UI 425 / Net 385 green. Pending: user
visual gate at the Town Network fountain, and a #176 corridor-seam
non-regression recheck (Facility Hub, different landblock).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 10:34:15 +02:00
Erik
388f3ed307 docs(A7): Town Network "too dark" root-caused — ambient RULED OUT (live cdb 0.2 white == retail), cause is the 463>128 light cap
Investigation handoff for the deferred A7 per-cell light-scoping fix (#79/#93/#176/#177).
Ambient verified retail-faithful three ways (decomp + dat SeenOutside + live retail
cdb capture: SetWorldAmbientLight level 0x3e4ccccd == 0.2f bit-exact). No fix applied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:18:05 +02:00
Erik
5d013dcb10 docs: close #188 (fading-wall doors fade + hold; door flip-back fix, 3284dd0a)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 09:20:51 +02:00
Erik
3284dd0aed feat(#188): fading-wall + sliding-door translucency; hold open past animation settle
Lands the fading-secret-door feature and fixes the door "flip-back" that
surfaced while testing it.

#188 — fading-wall doors (e.g. "Pedestal Weak Spot") fade their wall part
out via TransparentPartHook instead of swinging:
  - TranslucencyHookSink consumes TransparentPartHook -> TranslucencyFadeManager
    (per-(entity,part) linear translucency ramp; holds at End frame).
  - WbDrawDispatcher: new per-instance alpha SSBO (binding 7); ClassifyBatches
    takes opacityMultiplier (1 - translucency, per CMaterial::SetTranslucencySimple
    0x005396f0) forcing AlphaBlend; fully-invisible parts skipped.
  - mesh_modern.vert/.frag: binding-7 InstanceAlphaBuf -> vOpacityMultiplier ->
    FragColor.a *= vOpacityMultiplier.
  - Register AP-89: the fade multiplies sampled texture alpha, not a separate
    D3D9 material alpha channel (observably identical for texture-alpha==1 surfaces).

Door flip-back fix (affected BOTH #188 fading walls AND #187 sliding doors): a
door/wall that finished opening holds a single unchanging frame, so the
uncommitted IsEntityCurrentlyMoving cache-bypass narrowing dropped it onto the
Tier-1 static cache -- which only remembers the REST pose + opacity 1.0 --
snapping it visually shut/opaque while physics stayed open. Reverted that
narrowing: every Sequencer entity stays on the per-frame path (live pose + live
fade opacity), the known-good pre-optimization behavior. The per-frame CPU cost
that narrowing chased was a Debug-build artifact -- Release is GPU-bound
(~200 fps in Sawato, measured), so the unconditional add is free where it
matters. Left a code comment barring re-introduction.

Tests: full Core suite green (2649 passed, 2 skipped). Live visual gate PASSED --
both fading-wall and sliding doors hold open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 09:17:50 +02:00
Erik
e2e285b855 fix(#187): drop the "Name == Door" special-case — register any entity with a resolvable MotionTableId
The door-swing animation rescue (GameWindow.cs:3897, for entities whose rest pose
is a static single frame but which still carry a reactive MotionTable) was gated on
an exact display-name string match. Sliding doors, gates, portcullises, and disguised
secret-passage props ("Magic Wall") all fail that check because their in-game Name
isn't literally "Door" -- so ACE's UpdateMotion for them was silently dropped forever
by the _animatedEntities.TryGetValue bail-out in OnLiveUpdateMotion.

Retail's own dispatch chain (ACCObjectMaint::CreateObject -> CPhysicsObj::
set_description -> SetMotionTableID -> CPartArray::SetMotionTableID 0x005186e0 ->
MotionTableManager::PerformMovement) is unconditionally data-driven: the only gate
for creating a motion dispatcher anywhere in that chain is "motion table id != 0" --
no CDoor class, no WeenieType switch, no name check. Production weenie data confirms
Sliding Door / Portcullis / Gate / Magic Wall all carry the identical WeenieType=Door
+ non-zero-MotionTableId shape as a plain "Door", differing only in display name.

Fix: the branch's existing `mtableId != 0` check (already computed one line later)
is now the entire gate, matching retail exactly. IsDoorSpawn deleted (dead code);
IsDoorName kept only for an unrelated diagnostic log-label filter.

Live-verified: sliding doors now animate open/closed correctly. Full regression
green (App 741 / Core 2631).

docs(#188): file the fading-wall render gap surfaced during #187's live gate

A "Pedestal Weak Spot" secret-passage door dispatches correctly (proving #187's fix
reaches it) but never visibly changes. Decoded its actual dat MotionTable directly
(0x090000F9): its open cycle carries EtherealHook + TransparentPartHook +
SoundTableHook -- a translucency-fade effect, not part-transform motion. acdream's
IAnimationHookSink documents these hook types as intended for "GfxObjMesh / renderer
state mutations" but no sink anywhere consumes them (only Particle/Lighting/Audio are
wired) -- confirmed via full-repo grep. Collision already works correctly via a
separate server-authoritative SetState wire message, independent of the animation
hook. This is feature-shaped rendering work (a per-part runtime alpha under the
mandatory N.5 bindless pipeline), not a quick fix -- filed for its own design pass.

Kept Issue188FadingDoorMotionTableInspectionTests.cs as a reusable MotionTable/hook
decoder for future "why doesn't this animate" questions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 20:10:45 +02:00
Erik
0efa7ed2a1 docs: close #137 (dungeon collision, user re-gate) + file #187 (door animation dispatch gap)
#137 collision scope is done — user confirmed all dungeon door types can be clicked
and passed through with no phantom blocks (adds to the 2026-07-06 corridor +
window/opening gates). The same check surfaced a SEPARATE visual bug: sliding doors
and "fading wall" gates don't play their open animation, only literal-name "Door"
entities do (GameWindow.cs:3128 IsDoorSpawn gates the reactive-motion-table rescue on
an exact display-name match). Filed as #187 for investigation before any fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 17:14:11 +02:00
Erik
4dea66b633 docs(#186): close — render InsideSide from dat PortalSide bit (fix 8257b9ba); retail trace overturned PICK + FLOOD-epsilon hypotheses
The live retail cdb trace decided it: retail roots at the connector 0118 at the
grey pose (NOT the PICK fork) AND still draws the player room 0116 from that root,
because retail's InitCell side test reads the dat PortalSide bit where acdream's
render path reconstructed the interior side from the cell AABB centroid (mis-sides
a thin connector). ISSUES #186 -> CLOSED; handoff gets a RESOLVED banner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:07:46 +02:00
Erik
8257b9ba10 fix(#186): render side-cull mis-sided thin connectors — use dat PortalSide bit, not AABB centroid
The indoor GREY flap at a top-floor connecting room. The render portal side-cull
reconstructed each doorway's "interior side" (PortalClipPlane.InsideSide) from the
cell's AABB CENTROID. For a THIN connector cell (0xF6820118, 5 render polys), the
bounding-box center falls on the WRONG side of the 0118->0116 doorway, so the eye
read as a back-portal and the forward room 0116 was culled -> the aperture showed
the fog clear color = grey.

Retail's PView::InitCell (0x005a4b70) and acdream's own PHYSICS path
(CellTransit.cs:190) both read the explicit dat PortalSide bit ((Flags&2)==0)
instead of guessing from geometry. Port the render path (GameWindow.BuildLoadedCell)
to the same bit.

Proven by a live retail cdb trace (retail draws 0116 from the 0118 root at the grey
pose; tools/cdb/issue186-connector-decider.cdb) + an offline dat diagnostic
(Issue186...PortalSide_CentroidVsDatBit_AtGreyEye): the dat bit matches the old
centroid on every portal of these cells EXCEPT the one #186 breaks, so the switch is
surgical. Full regression green (App 741 / Core 2631); the CornerFlood + Issue113
dat-loading helpers updated to the same bit confirm every real Holtburg/tower/hall
cell floods identically. Touches neither PortalSideEpsilon nor the deleted
EyeInsidePortalOpening rescue (the two DO-NOT-RETRY traps).

Live-gated: user-confirmed no grey at any camera angle; probe shows 216 root=0118
frames, 0 still grey (0118->0116 now TRV, vis=4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:06:33 +02:00
Erik
9d35a9786f docs(#186): handoff — connector grey flap narrowed to the doorway-flap flood/pick family
Retail SEAMLESS at the same spot (user-confirmed) => real acdream bug: the eye seats
in a sparse 5-poly connector cell 0xF6820118 looking back at the player's room 0116,
and acdream drops 0116 (back-portal side-culled) so the doorway aperture shows the fog
clear color = grey; retail keeps 0116 drawn. RULED OUT: null-root/AD-20/AD-21 (root
valid, eyeInRoot=Y); the color-clear gating (retail's gated DrawCells Clear is
depth/stencil, post-LScape::draw). Next step = retail cdb trace (viewer_cell +
cell_draw_list at the grey pose) to pin viewer-cell PICK vs portal FLOOD, then a careful
frozen-render fix. Full handoff + apparatus + DO-NOT-RETRY + code/decomp sites in the doc.
Keeps the offline cell-geometry inspection test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 14:04:23 +02:00
Erik
cd42369581 docs(#186): file indoor->indoor grey flap at a connecting room (new house type)
Camera-direction-dependent grey (world background) at a top-floor connecting room.
Diagnosed by class (doorway-FLAP family): null viewer-cell root -> AD-21 outdoor
fallback; camera dependence -> AD-20 camera-eye viewer-cell resolution. Report-only
investigation next: ACDREAM_PROBE_FLAP capture at the spot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:26:54 +02:00
Erik
5a9df00aa9 docs(#185): close — REAL root cause = shadow part-id uint32 overflow (07c5b832)
Live gate passed. ISSUES #185 moved to DONE with the corrected root cause
(registration overflow, not the collision response). The handoff's convex-edge
theory and design-v1's grounding-retention theory are both recorded as superseded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:17:08 +02:00
Erik
07c5b832cf fix(#185): landblock collision part-id uint32 overflow dropped stair steps
Root cause (live capture #3 + code): GameWindow's per-part landblock shadow
registration used a synthetic part-id `entity.Id * 256u + partIndex` that
OVERFLOWS uint32 for class-prefixed landblock ids (0x40/0x80/0xC0...). The << 8
drops the prefix byte, so different-class entities sharing the low 24 bits
collide on ONE shadow part-id and Register's deregister-then-insert silently
overwrites one entity's collision geometry. Landblock 0xF682 had 23 such
collisions incl. the stair runs (0xF6822100 <- {0x40F68221, 0xC0F68221}, ...),
so 3 mid-staircase steps rendered but had no collision -> the player floats into
the hole and the (faithful) PrecipiceSlide wedge fires = the 'invisible wall
half-way up the stairs'.

Fix (Option A, retail-faithful): register each multi-part landblock entity via
ShadowObjectRegistry.RegisterMultiPart under its UNIQUE 32-bit entity.Id
(retail CPhysicsObj::add_shadows_to_cells 0x00514ae0 -> CPartArray::AddPartsShadow
- one object, a part array; no synthetic per-part id). New testable builder
ShadowShapeBuilder.FromLandblockBspParts decomposes each MeshRef.PartTransform to
local pos/rot/scale; RegisterMultiPart reconstructs the identical world placement.
Building shells stay excluded (building channel); the Setup cyl/sphere path is
unchanged (runs only when entityBsp==0, retail BSP-xor-cyl dispatch). Despawn is
landblock-scoped (RemoveLandblock by cell prefix), so the id change is safe.

Tests: ShadowRegistrationOverflowTests (overflow arithmetic; old scheme drops one;
RegisterMultiPart keeps both; builder). Issue185OutdoorStairsSeamReplayTests
(dat-free clean-climb pin). Core 2629 / App 741 green, 0 warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:07:50 +02:00
Erik
b62b13ce21 docs(#185): design v2 — REAL root cause = shadow part-id uint32 overflow
Live capture #3 (cp-write + entity-source + full bsp-test object map) disproved
v1's grounding-retention theory and pinned the real bug: GameWindow.cs:7951
partId = entity.Id*256+partIndex OVERFLOWS uint32 for class-prefixed landblock
ids (0x40/0x80/0xC0), dropping the prefix byte so different-class entities sharing
the low 24 bits collide on one shadow part-id; Register deregisters the loser
(last-writer-wins), silently deleting collision geometry while render shows every
step. Landblock 0xF682 has 23 such collisions incl. the stair runs. The player
floats into the collision hole and the PrecipiceSlide wedge fires = the invisible
wall (a faithful symptom). Fix = Option A: RegisterMultiPart per entity (unique
32-bit entity.Id, retail add_shadows_to_cells/AddPartsShadow model), unifying on
the one faithful multi-part path and deleting both synthetic-id schemes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 09:55:24 +02:00
Erik
75e3b76445 docs(#185): implementation plan — red replay -> pin -> retention fix -> regress
Inline-execution plan (frozen collision internals): dat-backed seam replay as the
red pin, empirical pinning of the forward-move contact-plane loss, the localized
retail-faithful grounding-retention fix (candidate tree keyed to the pin), #137
regression net, register/digest/ISSUES bookkeeping, live gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 08:42:36 +02:00
Erik
06c3ecd89d docs(#185): design — Approach A (keep mover grounded on the forward move)
Root cause confirmed (decomp cross-check wf_3c1120c4-a04 + live apparatus): the
outdoor stairs are a continuous coplanar 38.7-degree ramp of stacked step-box
objects; at a seam the grounded forward move loses contact_plane_valid, the
step-down recovery can't reach the coplanar (at-level) continuation, and
EdgeSlide/PrecipiceSlide fabricates a horizontal (0,1,0) sliding normal that
absorbs the up-stairs motion (the #137/TS-4 family). Fabrication math, the
SetSlidingNormal Z-zero, and the multi-object search are all verified faithful;
the divergence is upstream (retail keeps contact_plane_valid, pc 273244).

Approach A: restore retail's grounded forward-move retention so the fragile
step-down recovery isn't needed at seams. Exact retention-loss line pinned by a
dat-backed replay test (the #137 method), not guessed. Alternatives B/C recorded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 08:37:24 +02:00
Erik
4a067e34a9 docs(#185): handoff for the outdoor-stairs phantom fix (synthetic sliding-normal, #137/TS-4 family) 2026-07-08 07:35:11 +02:00
Erik
99c22fad48 docs(#185): root cause = synthetic sliding-normal at a convex tread edge (#137/TS-4 family, artifact not geometry) 2026-07-08 07:28:35 +02:00
Erik
29e01c3829 docs: file #185 - local player jams half-way up outdoor stairs (house on stilts) 2026-07-08 07:14:06 +02:00
Erik
bd632f6f6d docs(#184): CLOSE — Slice 2 gate passed (players walk through, monsters collide)
Slice 2 (2a e1ac56cc extract + 2b ddb5a967 fork-collapse) shipped and the visual
gate PASSED (user: "Looks good"), closing #184. Mark ISSUES #184 DONE and the Slice 2
handoff DONE, both noting the review-driven correction: non-PK players WALK THROUGH
each other (retail PvP), so the player win is monster/terrain/wall collision, not
player-vs-player de-overlap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 06:40:29 +02:00
Erik
ddb5a96799 feat(#184): Slice 2b — unify the remote player/NPC fork (players collide faithfully)
Collapse the two-path fork in RemotePhysicsUpdater.Tick: the former Path A
(grounded PLAYER remotes advanced by the interp catch-up with ResolveWithTransition
deliberately OMITTED, per the now-retired issue-#40 premise) is gone. Every remote --
player and NPC -- now runs the SAME per-tick catch-up + sweep + shadow-follows-resolved.
Retail's UpdateObjectInternal (0x005156b0) has no player/remote fork; this is the
faithful shape. Player remotes now get terrain-Z snap (no slope staircase), wall
collision, and MONSTER collision -- previously (Path A) they skipped ALL collision.

RETAIL PvP (adversarial review caught this): two non-PK players WALK THROUGH each
other in AC (you can stand inside another non-PK player) -- they do NOT de-overlap.
The remote-player mover now carries IsPlayer|EdgeSlide (mirroring the LOCAL player at
PlayerMovementController), so CollisionExemption's PvP block exempts a non-PK pair,
exactly as retail sets IsPlayer on every object's own transition (OBJECTINFO::init
0x0050cf30) and FindObjCollisions (pc:276812) exempts it. The first 2b draft passed
bare EdgeSlide and de-overlapped players (MORE solid than retail); the 3-lens review
flagged it. PK/PKLite/Impenetrable are not plumbed onto the remote mover yet -- the
same M1.5 gap the local player carries (TS-23, extended).

#40 proven dead in code (before the gate): PlayerVsMonster_DeOverlapsAndAbsorbsTheStallBlip
drives the real ComputeOffset -> InterpolationManager catch-up (incl. the fail_count
blip-to-tail) for a player mover converging on a monster and asserts de-overlap +
maxSpike<0.30 -- the sweep absorbs the stall-blip. ConvergingPlayers_WalkThroughEachOther
proves the PvP exemption (non-PK players pass through). Path B already ran the
#40-feared config stably; #40 (May 2026) predates the CSphere/#137/#170/#171 rebuild.

Placement (HIGH, review finding 3): the player UP routing gains the SAME placement-snap
backstop Slice 1 gave NPCs (AP-87). Without it a UM-first player (RemoteMotion seeded
to the spawn pos, then a first UP in a different cell) would sweep from a stale cell ->
garbage -> the digest's invisible/misplaced-player bug. The 4 m bodyToTarget guard +
!willBeDrTicked + dist>96 snap; near placed corrections still enqueue for smooth
catch-up. Also seed Body.Position=worldPos at UP-handler RemoteMotion creation
(mirrors the UM handler :5176) for the UP-first case.

Coupled shadow edits (research finding 9): RETIRED the players-only raw-worldPos shadow
sync -- now that players run the sweep + shadow-follows-resolved, the raw sync would
re-snap a packed player's shadow into overlap each UP. Player shadows follow the
RESOLVED body via the DR-tick loop + a new player UP-branch-tail SyncRemoteShadowToBody.

Surviving player/NPC split (AP-88): the omega -- grounded PLAYERS keep the
ObservedOmega-or-seqOmega world-frame (Concatenate) fallback ("rectangle when running
circles"); NPCs + airborne keep ObservedOmega-only body-frame (Multiply). They commute
for an upright body + yaw omega, so the fork is faithful.

Register: TS-23 extended (remote-player mover PK gap); AP-86 updated (raw sync retired
for players too, Where column fixed); AP-88 added (omega fork + eval-order note).

Tests: Core 2623 / App 741 green, 0 warnings. 3-lens adversarial review + per-finding
verification (10 agents); all 6 confirmed findings addressed (2 substantive: PvP mover
flags + player placement-snap; 4 doc/cosmetic).

VISUAL GATE (acceptance test) owed by the user -- NOTE the corrected expectation:
  (a) a player remote on a hill -- no slope staircase;
  (b) two packed player remotes -- they WALK THROUGH each other (retail PvP), NOT
      de-overlap (this corrects the design's original "players de-overlap" gate);
  (c) a player remote cannot stand inside a MONSTER (new: player-vs-monster collision);
  (d) remote walk/run/jump/land/turn UNCHANGED.

Handoff: docs/research/2026-07-07-184-slice2-unify-extract-handoff.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 00:45:44 +02:00
Erik
e1ac56cce9 refactor(#184): Slice 2a — extract RemotePhysicsUpdater (byte-exact, fork preserved)
Extract the ~690-line per-remote dead-reckoning tick out of the >10k-line
GameWindow.TickAnimations into a testable AcDream.App.Physics.RemotePhysicsUpdater
(Code Structure Rule 1). The guard in TickAnimations now calls
_remotePhysicsUpdater.Tick(rm, ae, dt, _liveCenterX, _liveCenterY); the
animation/render half stays in GameWindow.

Pure, behaviour-neutral refactor — the Path A (grounded player remotes skip the
sweep) / Path B (NPCs + airborne players run it) FORK is preserved verbatim inside
the new class. Slice 2b collapses it.

Mechanics:
- Moved into RemotePhysicsUpdater: the Tick body (verbatim), SyncRemoteShadowToBody
  (now called back from the NPC UP-branch tail), ApplyPositionManagerDelta,
  TickRemoteMoveTo, ServerControlledVelocityStaleSeconds, and the diagnostics.
- Injected as delegates (kept on GameWindow — they have callers outside the DR
  loop): GetSetupCylinder (player cylinder + moveto/sticky radii) and
  ApplyServerControlledVelocityCycle (also called from the UP handler).
- AnimatedEntity: private -> internal (matches RemoteMotion) so the extracted
  class can take it by type.

Verified byte-exact: the extracted Tick body reverse-transforms (re-indent +8, undo
the 4 delegate/id substitutions) to diff-identical against the original block.
Behaviour-neutral: Core 2621 / App 741 green (unchanged), 0 warnings. No visual gate
(2a is structure only).

Handoff: docs/research/2026-07-07-184-slice2-unify-extract-handoff.md section 3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:52:16 +02:00
Erik
4b07b0f541 docs(#184): Slice 2 handoff — extract RemotePhysicsUpdater (2a) + unify the player/NPC fork (2b)
Deliberately deferring the last #184 piece (Slice 2) to a fresh focused session per
the user's call. This is a self-contained handoff: the #40 sweep-blip verdict (DEAD,
high confidence — Path B already runs that config stably + a Core test proves it),
the 2a-first-then-2b plan, the exact code sites (Path A :10194-10429, Path B, the
extraction seam :10152-10873, the shared-helper wrinkle), the 2b merge gotchas
(Path A's player-specific omega/compose/diagnostic bits; the coupled :5699 shadow
sync), the preserve-list, and the test/gate plan.

Corrects the earlier ISSUES characterization: Slice 2 is a GATED behavior change to
the frozen R4/R5 arc (players gain the sweep), not a no-gate refactor. The #184
symptom itself stays RESOLVED + gated (Slices 1+3).

Research: workflow wf_c6a2e2b9-833 (3-agent read-only sweep).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:31:01 +02:00
Erik
f0f6a92a4c docs(#184): mark the remote-creature de-overlap symptom RESOLVED + gated (Slices 1+3)
Both visual gates passed (crowd de-overlap + large-monster spacing). The reported
symptom is fixed end-to-end. Only Slice 2 (internal Path A unification + the
RemotePhysicsUpdater extraction) remains — an internal refactor with no visual
change, not urgent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 22:59:14 +02:00
Erik
f51c1dffa5 feat(#184): Slice 3 — Setup-derived mover sphere for the remote de-overlap sweep
The per-tick remote de-overlap sweep used a hardcoded HUMAN collision sphere
(0.48 m radius / 1.835 m capsule top) for EVERY creature, so large and small
monsters de-overlapped at human spacing (register TS-46). Retail seeds the
transition from the object's OWN Setup sphere list scaled by its wire ObjScale
(CPhysicsObj::transition 0x00512dc0 -> init_sphere(GetNumSphere, GetSphere,
m_scale); ObjScale from set_description 0x00514f40).

Slice 3 (one call site, no signature change): before the Path B ResolveWithTransition
call, read the creature's Setup-derived dims via the existing GetSetupCylinder
helper -- (setup.Radius, setup.Height) x ObjScale, the same source the local player
and the moveto/sticky radii already use, consistent with the spawn-time shadow
registration's entScale -- and pass them as sphereRadius/sphereHeight. Fall back to
the human capsule when GetSetupCylinder returns (0,0) for a shapeless / unresolvable
Setup (a zero radius would degenerate the sweep). The player call site is unchanged
(the player IS the human Setup). stepUp/stepDown stay 0.4 m (retail derives those
from the Setup too -- an adjacent divergence left as-is).

Big monsters now spread wider, small ones tighter -- the de-overlap distance tracks
each creature's true radius.

Test: RemoteDeOverlapMechanismTests.ConvergingLargeCreatures_DeOverlapWiderThanHuman
(an R=0.9 pair settles ~1.8 m -- materially wider than the human 0.96 m contact --
proving the sweep de-overlaps at the radius it is given). Register: narrows TS-46
(remotes no longer human-dimmed; residual = the two-scalar reconstruction vs retail's
sphere list, plus the 0.4 m step heights). Core 2621 / App 741 green.

Research: workflow wf_e8306250-21b (3-agent read-only sweep: acdream data source /
retail init_sphere reference / minimal-edit path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 22:53:21 +02:00
Erik
37a94e1fa4 feat(#184): remote-creature de-overlap — placement-snap + shadow-follows-resolved (visual gate passed)
Packed monster remotes interpenetrate in acdream but barely in retail on the same
ACE. Retail de-overlaps them CLIENT-side: it sweeps every remote creature every
tick against neighbours' LIVE resolved positions (the collision shadow == the
resolved m_position, re-registered every moved transition step), with the server
position a gentle catch-up target (CPhysicsObj::MoveOrTeleport 0x00516330), not a
hard-snap. The collision math was already faithful; the bug was the reconciliation
(hard-snap) + the movement model (synth-velocity) + a stale shadow.

A first attempt (reverted) enqueued EVERYTHING and left the shadow at the raw
server position — it gate-failed with invisible monsters (an unplaced body blipped
over a huge distance into the sweep -> garbage pos) and the player stuck on offset
shadows. This redo fixes both root causes, mechanism-proven in a Core test first:

- NPC UpdatePosition routes through MoveOrTeleport with a PLACEMENT-SNAP: the body
  is snapped to the server pos when it is not already near it (first UP, no
  Sequencer to consume the queue, >96 m, or |Body - worldPos| > 4 m); only near DR
  corrections enqueue. This restores the body's placement authority (no invisible
  monsters). Airborne keeps the authoritative hard-snap.
- Grounded movement drives the body from the interp CATCH-UP (ComputeOffset ->
  InterpolationManager::adjust_offset, REPLACE dichotomy) instead of synth-velocity
  (get_state_velocity / SERVERVEL); MovementManager::UseTime runs unconditionally.
- SHADOW-FOLLOWS-RESOLVED: after each tick's sweep the collision shadow is
  re-registered at the resolved body (SyncRemoteShadowToBody), movement-gated
  (|Body - LastShadowSyncPos| > 1 cm). The per-UP :5669 raw-pos sync is now
  PLAYERS-ONLY, so an NPC's shadow is only ever written to its resolved body ->
  neighbours de-overlap against resolved bodies, the spread PERSISTS, and collision
  == render (no stuck-on-nothing). Landing clears the interp queue.

Preserved: airborne path, sticky #171 (gate + StickyManager overwrite of the seeded
frame), omega, the #173 bounce, landing, the node_fail_counter watchdog, and Path A
(player remotes, untouched -- Slice 2 unifies it).

Tests (RemoteDeOverlapMechanismTests): converging pair settles STABLE at 0.86 m
(barely overlapping = the retail look) WITH the shadow-sync vs <0.40 m (full
overlap) WITHOUT it; a third test drives the REAL InterpolationManager loop and
confirms the sweep absorbs the stall-blip (no pop-into-neighbour). 2-lens Opus
review (CONCERNS) addressed: movement-gated re-flood for the town-FPS risk;
players-only :5669; the blip-absorption test.

Register: retires TS-41 (SERVERVEL synth-velocity -> catch-up), narrows TS-44 (NPC
UP unified onto the interp queue; gate kept for orientation), adds AP-86
(shadow-follows-resolved impl) + AP-87 (MoveOrTeleport 4 m/no-Sequencer placement
snap). Known residual: the de-overlap sweep uses the human sphere for the mover, so
large creatures de-overlap at human radii (TS-46; Slice 3 plumbs Setup dims).

Visual gate PASSED (user: monsters visible + spacing much better). Core 2620 /
App 741 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 22:36:54 +02:00
Erik
7f7a78d3ea docs: remote-creature de-overlap — design spec + fresh-session handoff
The crowd-tightness residual from the #182 gate: monsters overlap (arms) in acdream but
barely in retail on the SAME ACE. Verified root (workflow wf_d2ff782f-9cb + source): retail
runs UpdateObjectInternal+transition on EVERY remote creature (CPhysics::UseTime 0x00509950,
no fork) so they de-overlap client-side, with the server pos a gentle MoveOrTeleport catch-up
target (0x00516330), NOT a hard-snap. acdream (a) hard-snaps NPC remotes to the raw overlapping
server pos (GameWindow.cs:5925) overwriting the swept de-penetration, and (b) forks player-remotes
(skip sweep) from NPCs (sweep at :10558 but driven by get_state_velocity, not the catch-up).

Collision math already exists + is faithful; the fix is the reconciliation (hard-snap→catch-up)
+ the movement model (synth-velocity→interp catch-up) — a delicate rework of the frozen R4/R5
remote-DR arc, staged NPC-first. Design spec + full handoff (verified code sites, retail anchors,
preserve-list, gotchas, slices) written for a fresh session. Implementation NOT started.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 20:29:06 +02:00
Erik
e3c4c59b84 docs(#182): retire TS-3, narrow AD-25 + add AD-39/40/41, update ISSUES for the shipped rebuild
TS-3 (frames_stationary_fall accounting absent) retired — ported in the verbatim
UpdateObjectInternal rebuild. AD-25 narrowed to the remote-DR sweep (player half retired).
AD-39 (fsf ladder placement vs ACE's interleave), AD-40 (fsf bit-encode in the Core
writeback + CachedVelocity computed-not-consumed), AD-41 (candidateMoved gates only
handle_all_collisions) added. ISSUES #182 → rebuild shipped, awaiting the visual gate,
with the fsf-not-cached_velocity correction + the #137 Slice-3 residual note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 14:30:07 +02:00
Erik
54d5622960 feat(#182): route player collision response through the ported UpdateObjectInternal chain (Slice 2b/2c)
Replaces PlayerMovementController's ad-hoc airborne-only reflect + Velocity.Z landing
snap with the verbatim retail chain: cached_velocity = (resolved-old)/dt (separate
reporting value), SetPositionInternal contact determination (kept Velocity.Z<=0 gate for
acdream's always-step-down resolver), then handle_all_collisions (fsf<=1 reflect / fsf>1
zero — the airborne-stuck bleed). Contact is committed BEFORE the reflect so the landing
gate isn't defeated by a reflected +Z; that ordering + the ungated small-velocity-zero
(Slice 1a) retire AD-25's micro-bounce spiral.

Load-bearing: handle_all_collisions + cached_velocity are gated on candidateMoved (retail
UpdateObjectInternal pc:283657 only reaches SetPositionInternal when the integrated
candidate moved off m_position). After fsf>1 zeros a blocked jump, the next frame
integrates zero motion (velMag2==0), so the candidate hasn't moved — skipping the response
that frame lets gravity rebuild the velocity instead of re-zeroing it and re-wedging.

End-to-end Core test (Issue182CrowdJumpTests): a jump blocked by an overhead creature
bleeds its +12 up-velocity to ~0 within a couple frames (fsf>1) and the body grounds on
the manufactured plane instead of hanging with persistent +12. Core 2617 / App 741 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 14:25:47 +02:00
Erik
78df1370b6 feat(#182): port handle_all_collisions as a pure Core unit (Slice 2a)
PhysicsObjUpdate.HandleAllCollisions — retail CPhysicsObj::handle_all_collisions
(0x00514780, pc:282647): the velocity 'bleed on block' decision. fsf<=1 → reflect the
into-surface component (v += -(v·n)(elasticity+1)·n) unless staying-on-walkable (retail's
should_reflect guard, restoring the broader rule AD-25 suppressed); INELASTIC zeros
instead; fsf>1 → v=0 entirely (the airborne-stuck fix). Bit round-trip owned by the Core
resolve writeback (Slice 1), not re-encoded here. 7 conformance tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 14:12:59 +02:00
Erik
6c3cd96b7a feat(#182): frames_stationary_fall round-trip in the kept transition internals (Slice 1b/1c)
Completes the TS-3 stub (the deferred 'full physics port'):
- ValidateTransition: the fsf increment/reset ladder + the fsf>=3 upward-contact-plane
  manufacture (retail validate_transition 0x0050aa70 pc:272625-656; ACE Transition.cs:
  1029-1061). Runs after acdream's fused contact block (structural adaptation preserving
  the L.2.3c/L.2.4/A6.P3 contact-retention divergences), deriving retail's _redo as
  cleanAdvance || OnWalkable — a grounded wall-slide is not a stuck-fall.
- The sweep-loop fsf early-out (retail find_valid_position pc:273745 / ACE :587): stop
  as soon as fsf != 0, so a later advancing sub-step can't reset it in the same frame
  (load-bearing — without it the counter never escalates across frames).
- ObjectInfo.MoverHasGravity gate (pc:272625).
- PhysicsEngine: seed ci.fsf from the body's Stationary* bits AFTER InitPath
  (retail transition() pc:280940-947); writeback publishes body.FramesStationaryFall +
  encodes the Stationary* bits (co-located with the fsf compute so the round-trip is
  self-contained in Core; retail encodes in handle_all_collisions — register note).

Tests: FramesStationaryFallTests — an airborne jump wedged under an overhead creature
escalates fsf 0->1->2->3 and at fsf 3 manufactures the UP plane (grounded 'glide onto
the crowd top'); a grounded wall-slide never accumulates fsf. Core 2609/0.
Behaviour dormant in ordinary locomotion (fsf stays 0 unless a gravity mover is blocked).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 14:11:35 +02:00
Erik
6e8117741b feat(#182): PhysicsBody fsf state + CachedVelocity; ungate small-velocity-zero (Slice 1a)
Verbatim UpdatePhysicsInternal (0x00510700): the small-velocity-zero
(<0.25 m/s) fires unconditionally, not gated on OnWalkable (the old acdream
divergence) — gravity re-accelerates the same frame via the unconditional
v += a*dt. Adds TransientStateFlags.Stationary{Fall,Stop,Stuck} (0x10/0x20/0x40)
for the fsf round-trip, plus PhysicsBody.FramesStationaryFall and the separate
CachedVelocity field (retail's two-velocity model — reporting/DR only, never fed
to the integrator). All 1536 physics tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:39:28 +02:00
Erik
8bb8b20411 tools+plan(#182): resolve-capture histogram classifier + verbatim player-physics rebuild plan
Slice 0 of the #182 verbatim rebuild. The classifier reproduces the design
baseline off acdream-crowd-resolve.jsonl (2883 move-intent resolves:
52.8% OK / 25.1% partial / 22.1% stuck / 107 airborne-stuck) — the A/B
'before' the rebuild measures against (retail target ~78% OK, 0 airborne-stuck).

The plan refines the design spec's §7: the airborne-stuck bleed is the
frames_stationary_fall counter (validate_transition increments; handle_all_collisions
zeros velocity at fsf>1), NOT the cached_velocity field (a separate reporting value).
Slices reorder accordingly; calc_friction (retail 0.25 vs acdream 0.0) is an
orthogonal L.3c divergence kept out of scope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:37:15 +02:00
Erik
80881ed6f1 docs(#182): crowd-collision investigation outcome + velocity-model rebuild design
The #182 CSphere port (96ae2740) failed its visual gate and introduced an
airborne "stuck in the falling animation" regression. A player-attributed retail
cdb trace (tools/cdb/retail-crowd-jump3.cdb) proved retail's LOCAL client fully
runs player-vs-creature collision (76 land_on_sphere, 188 COLLIDED, 130 SLID,
~78% OK, glides across) -- NOT server-authoritative (an earlier unfiltered
land_on_sphere=0 read was a false lead the attributed trace refuted).

acdream's same-repro capture: 50.9% OK, 22.4% stuck, 115 airborne-stuck. Root
divergence: retail CPhysicsObj::UpdateObjectInternal (0x005156b0, pc:283688) sets
cached_velocity = (resolved - old)/dt -- velocity from ACTUAL movement, so a
blocked jump collapses to ~0 -> gravity -> the player falls/glides. acdream
integrates velocity + reflects on collision (PlayerMovementController ~:1008-1069),
so the jump velocity (~18) persists against the creature -> hang.

Fix = verbatim rebuild of the per-frame player-physics loop (UpdateObjectInternal
chain), velocity model first, transition internals kept. Full design +
retail function inventory + the capture apparatus + retail target numbers:
docs/superpowers/specs/2026-07-07-player-physics-update-verbatim-rebuild-design.md.
Implementation deferred to a fresh session (user decision). Also files #183
(floating distant scenery, observed during testing). #182 stays as the base.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:07:56 +02:00