Commit graph

2341 commits

Author SHA1 Message Date
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
Erik
96ae274081 fix #182: port CSphere collision family — retail-faithful crowd wiggle (retires TS-45)
Humanoid creatures/players collide as body Spheres (ShadowShapeBuilder emits
Sphere-type shadows for a Setup with Spheres + no CylSpheres), so player-vs-monster
crowd contact ran through Transition.SphereCollision — a hand-rolled 3-D wall-slide
(register TS-45), NOT a port of retail CSphere::intersects_sphere. It shaved no eps,
force-pushed each contact RADIALLY to a fixed combinedR+1cm shell, ignored the head
sphere, and always returned Slid. In a crowd the opposing radial de-penetration
pushes from neighbours fight each other -> the player wedges and can't wiggle free
(the user's live report).

Port the full CSphere family verbatim — dispatcher 0x00537A80 + step_sphere_up /
slide_sphere / land_on_sphere / collide_with_point / step_sphere_down — the direct
analog of the 2026-07-05 CCylSphere port (#172). The grounded slide now routes
through the shared crease SlideSphere (0x00537440, #116-Ghidra-confirmed) ->
tangential shuffle along the contact toward gaps, retail-faithful. isCreature
(target creature/missile) gates OFF the stand-on/land-on branches (2 & 5). ACE
Sphere.cs = readable oracle; pseudocode doc 2026-07-07-csphere-collision-family.

Retail-faithfulness verified: CTransition::validate_transition (0x0050aa70:272593)
reverts curr_pos on any non-clean-OK step, so a deep-mutual-overlap start wedges in
retail too — the realistic crowd-edge graze slides free (SphereCollisionFamilyTests
slide-around trajectory: player grazes a creature's SW, curves around its west side,
continues N).

TS-45 retired, AP-84 added (PerfectClip TOI dead in M1.5). Core 2603/0, App 741/0.
Pending user visual gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 11:25:56 +02:00
Erik
0cbe1102d9 docs(#177): mark stairs pickup-handoff SUPERSEDED (flood theory disproven)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 10:10:53 +02:00
Erik
3120f8ae08 docs(ISSUES): correct #138 status to resolved (residuals #146/#147)
Position-desync fixed (#145), server-object re-hydrate shipped (AP-48), avatar-vanish fixed + user-confirmed (afd5f2a). The stale pending-gate status misrepresented the M1.5 critical path; live follow-ups are #146/#147.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 08:45:58 +02:00
Erik
554af5042d park #177: retail cdb trace DISPROVES the portal-flood theory
Attached cdb to live retail (PDB MATCH), broke on PView::DrawCells (0x005a4840),
dumped cell_draw_num + cell_draw_list cell ids + the eye
(Render::FrameCurrent->viewer.viewpoint) while descending the Facility Hub spiral.
Retail's flood is dynamic and IDENTICAL in character to ours: from the spiral cells
it swings num 3->27 with gaze and collapses to 3 cells at many poses (cam=015d ->
{015d 015e 015f}). Our flood does the same (3->43). So retail does NOT keep the
staircase where we drop it -- the flood is exonerated as the cause.

Session trail (all in ISSUES #177): ruled out lighting, membership, camera coherence,
the collision sweep, the 0178/0182/0183 handoff cells, and edge-on eye-in-opening
(fix#1 shipped -> visual-gate-failed -> reverted, PortalVisibilityBuilder + AP-86 both
restored exactly). Freshest un-chased lead: the steps are STATIC objects (GfxObj
0x010000DE x6/cell) drawn via the viewcone cull, not cell shell.

Adds: Issue177StairDescentCameraFloodTests (real-camera+flood + composition +
flood-depth characterization pins) and a reusable retail-cdb capture toolchain
(tools/cdb/pview-verify.cdb, pview-spiral2.cdb with the correct top-level-qd detach --
qd in a CONDITIONAL bp action does NOT fire and strands cdb attached).

No production code change (fix#1 reverted). PARKED per user; M1.5 critical path next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 08:43:21 +02:00
Erik
c84dc49a63 docs: park #181 with the full elimination ledger; #177 stairs pickup handoff + companion prompt
#181 parked per user decision (the flicker survived the 7-fix ladder; every fix individually verified and standing). ISSUES #181 carries the shipped-commit list, the evidence-backed eliminations, and the four still-open leads (top: capture a clean frame pair at THE USER'S pose - never done). New handoff docs/research/2026-07-06-177-stairs-pickup-handoff.md: the stair-cell admission miss (0x0178/0x0182/0x0183), the existing Issue176177 replay scenarios as the entry point, the 0x0181 sliver adjacency lead, the lighting DO-NOT-REOPEN, and the companion prompt. Render digest re-bannered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:53:23 +02:00
Erik
233b469b01 fix #176/#181 (A7 fix #2): stationary server-weenie fixtures take retail's STATIC light curve - isDynamic decided by motion, not origin
The former #143 flag forced every server-object light onto the D3D dynamic path (1/d attenuation, range x1.5); retail's stationary fixtures use the static calc_point_light curve (0x0059c8b0, Ghidra-verified: f=(1-d/range)*intensity*wrap/d^3 beyond 1m, range=falloff*1.3, per-channel colour clamp) - the dynamic path burned every Facility Hub lamp ~10x hotter than retail at 3m, producing the saturated magenta wash that zebra-striped normal wall geometry into the #176 'stripes/triangles' pattern (VSync-on test + clean captures characterized it as STATIC over-bright content, not flicker/tearing/camera). The shader's static branch already implements the verified curve faithfully (mesh_modern.vert pointContribution - its wrap constants pin LIGHT_POINT_RANGE=0.75), so the whole fix is the registration decision. Site-A lights are all stationary today (AP-44); genuinely moving lights re-earn isDynamic when they exist. Suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:45:50 +02:00
Erik
cace430299 diag #176/#181: artifact CHARACTERIZED - static ~10x-hot magenta wash zebra-striping wall geometry; root = weenie lamps on the dynamic light curve (A7 fix #2); retail static curve Ghidra-verified (calc_point_light 0x0059c8b0)
VSync-on test (30fps): stripes remain => not tearing (windowed + DWM never tears - the tear framing was structurally wrong). Clean captures at the user's spot: the scene is pixel-static except idle anim; the 'stripes/triangles' are the corridor wall's angled braces silhouetted against blown-out saturated magenta. Server-weenie stationary lamps register isDynamic:true (1/d, range x1.5); retail statics use f=(1-d/range)*intensity*wrap/d^3 beyond 1m, range=falloff*1.3, per-channel colour clamp - ~10x dimmer at 3m. Next: A7 fix #2 (static curve; isDynamic decided by motion, not origin) + conformance pin, then the combined #176/#180/#181 gate. a7 pseudocode doc SS1.6 updated with the verified curve; render digest re-bannered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:43:43 +02:00
Erik
378316af15 docs #181: the vsync decision gate - all camera/sweep mechanisms now Ghidra-verified faithful; residual mm limit cycle likely retail-class, visible only via unsynced tearing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:31:36 +02:00
Erik
3f34bca06f port #181: retail viewer step subdivision (calc_num_steps 0x0050a0b0) - radius-anchored steps + remainder final step + viewer-exempt small-offset abort
The user's retail axiom (camera rock steady pressed into walls) vs our measured wall-press wander (~0.5mm/frame limit cycle, headless pin Issue181WallPressEquilibriumTests) sent us back to the decomp. Ghidra (clean, vs the BN x87 mush): retail VIEWERS subdivide the sweep into EXACTLY radius-length steps anchored at the start (offsetPerStep = offset*r/len, numSteps = floor(len/r)+1) with the final step recomputed mid-loop as the exact remainder (find_transitional_position 0x0050bdf0), and the negligible-offset abort is NON-viewer-only. Ours used ceil equal-slices for everything and aborted viewers too. Ported faithfully (pseudocode docs/research/2026-07-06-viewer-step-subdivision-pseudocode.md); non-viewer stepping already matched (TRANSITIONAL_PERCENT_OF_RADIUS=1.0).

Measurement: the wall-press limit cycle is UNCHANGED by the port (537.8um avg; a bit-exact 12-frame cycle: ~130um/frame inward creep x11 then a 2.6mm snap). With adjust_to_plane + adjust_sphere_to_poly now also Ghidra-verified faithful, the residual mm cycle is likely retail-class plateau physics - invisible at retail's 60fps vsync, tear-interleaved into visible stripes at our ~1500fps unsynced. The decisive user test: VSync ON (Settings/F11). Fallback discriminator: cdb-trace retail's viewer at a wall press. Suites green (Core 2600 / App 733 / UI 425 / Net 385).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:30:37 +02:00
Erik
d4ff80cb2b diag #181: retraction + exoneration - the 'scissor rect' was the user's snip-tool marquee; applied light sets FROZEN across all vis=32 frames
The seam-diff run ([seam-cell] applied sets correlated with [flap] vis across 107k parked frames): zero applied-set changes after startup hydration, including all 1,394 vis=32 frames - the vis 31<->32 flap has no lighting consequence. The t0b/still-* pixel-diff evidence was contaminated (the user was dragging the snipping-tool marquee + the chat window overlapped the game). Camera, pool, applied sets, and the sliver cell's own geometry are now ALL probe-exonerated. What stands: the flicker per the user's eyes and their clean screenshots (pink wash patches + a brick-textured stripe across the floor). Next: unobstructed-window captures while the user confirms the flicker is live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:50:08 +02:00
Erik
d366f36557 diag #181 (refined): 0x0181's region is a zero-area sliver; the amplifier is per-drawn-cell state shifting with the cell list, not light-pool scoping
Diagnostic_FlappingCellViewRegion_SliverOrLarge: at the flap pose the admitted region is one degenerate triangle (ndcArea ~0), and ClipPlaneSet.From handles degenerates correctly (area<1e-7 -> Empty) - the cell's own gated geometry costs ~zero pixels either way. The a7 pseudocode CORRECTION-2 re-read kills the flood-scoped-lights framing (the pool is already resident+player-anchored since d8984e87). Remaining suspects: per-cell light-set SSBO slot assignment (SelectForCell) or the seal/punch assembly keyed to the drawn-cell list. Next instrument: parked ACDREAM_PROBE_SEAMDRAW=1 run, diff the washed cell's [seam-blk] applied-set lines between vis=31 and vis=32 frames. ISSUES #181 + render digest updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:46:49 +02:00