Resolves the divergence-register conflict: kept the accurate per-VERTEX AP-35
(Fix A shipped per-vertex; main's row was the stale pre-Fix-A per-pixel text),
kept main's UI rows AP-37..AP-42, and renumbered this branch's torch-gate row
AP-37 -> AP-43 (AP-37 was taken by main's LayoutDesc row). AP count 41 -> 42.
Retargeted the AP-37 references in WbDrawDispatcher + the CHECKPOINT to AP-43.
Marked ISSUES #140 RESOLVED (b7d655b) with the corrected root cause.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6179 lines
367 KiB
Markdown
6179 lines
367 KiB
Markdown
# acdream — known issues + small deferred features
|
||
|
||
Rolling tactical list. What goes here:
|
||
|
||
- **Bugs**: user-visible defects we've observed but haven't fixed yet.
|
||
- **Small deferred features**: work that fits in one or two commits.
|
||
Anything larger should be a named Phase in the [roadmap](plans/2026-04-11-roadmap.md).
|
||
|
||
What does NOT go here:
|
||
|
||
- Large multi-commit work → add a Phase to the roadmap instead.
|
||
- Ideas / wishlist → `docs/plans/`.
|
||
- Design questions → open a `docs/research/*.md` note.
|
||
|
||
## Conventions
|
||
|
||
- Sequential integer IDs (`#1`, `#2`, …). Commits that close an issue reference the ID in the message (e.g. `fix #3: periodic TimeSync parsing`).
|
||
- `Status` is `OPEN`, `IN-PROGRESS`, or `DONE`. DONE items move to the **Recently closed** section at the bottom with closed-date + commit SHA.
|
||
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
|
||
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
|
||
|
||
## Template
|
||
|
||
Copy this block when adding a new issue:
|
||
|
||
```
|
||
## #NN — Short title
|
||
|
||
**Status:** OPEN
|
||
**Severity:** HIGH | MEDIUM | LOW
|
||
**Filed:** YYYY-MM-DD
|
||
**Component:** e.g. sky, physics, net, ui
|
||
|
||
**Description:** One paragraph — what's wrong or what's missing.
|
||
|
||
**Root cause / status:** What we know so far. Empty if unknown.
|
||
|
||
**Files:** Path references with approximate line numbers.
|
||
|
||
**Research:** Links to `docs/research/*.md` if applicable.
|
||
|
||
**Acceptance:** How we'll know it's fixed.
|
||
```
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## #140 — A7 "Fix D": outdoor objects too bright near torches
|
||
|
||
**Status:** RESOLVED (`b7d655b`, 2026-06-19 — user-confirmed side-by-side at the Holtburg meeting hall)
|
||
**Severity:** MEDIUM (visible — buildings blow out warm near torches vs retail; ambient/sun itself is correct after Fix C)
|
||
**Filed:** 2026-06-18
|
||
**Component:** render — point lighting on outdoor objects
|
||
|
||
**RESOLUTION (2026-06-19, round 2):** The "bake vs D3D-FF" framing below was the WRONG question — neither lights the building exterior. Retail's per-object torch binder `minimize_object_lighting` (0x0054d480) runs ONLY `if (Render::useSunlight == 0)` (`DrawMeshInternal` 0x0059f398), and the OUTDOOR landscape stage runs `useSunlightSet(1)` (`PView::DrawCells` 0x005a485a before `LScape::draw`). So retail lights outdoor objects (building exterior shells, scenery, outdoor creatures) with the **sun + ambient ONLY — never wall torches**. acdream was torch-lighting them. Fix: `WbDrawDispatcher.ComputeEntityLightSet` now gates torch selection on the object being indoor (`ParentCellId` is an EnvCell) via `IndoorObjectReceivesTorches`; outdoor objects get the sun only. acdream reads the dat falloffs faithfully (the orange torch is genuinely `Falloff 6`; the "reach too long" theory was a red herring). Register **AP-43**; the indoor-vs-outdoor *sun* half uses a per-frame player-inside global (residual logged in AP-43). Full handoff: `docs/research/2026-06-19-lighting-a7-fixD-round2-torch-reach-CHECKPOINT.md` (RESOLVED banner). Indoor-lighting follow-ups the user raised at the gate (windowed-building interior regime; portal swirl as a dynamic light) are SEPARATE M1.5 work, not part of this issue.
|
||
|
||
**Description (user):** Outdoor buildings (e.g. the Holtburg meeting hall) read much brighter near torches in acdream than in retail — the walls blow out warm where retail stays dim. The general ambient/sun is correct after Fix C (`57c1135`); this is specifically the per-object point-light *contribution*.
|
||
|
||
**Root cause / status:** GROUNDED but BLOCKED on one capture. Retail's object point-light path (`config_hardware_light` 0x0059ad30): `Diffuse=color×intensity`, `Attenuation=(0,1,0)`⇒1/d, `Range=falloff×rangeAdjust` (`rangeAdjust=1.5`⇒9 m), `material.diffuse=(1,1,1)`. CONTRADICTION: by that math a torch 3 m away = color×33 ⇒ retail walls should blow to WHITE — but they're DIM. Material/range/intensity all captured + ruled out. So the scaling is in the building's RENDER PATH (unknown). Leading hypothesis: static buildings DON'T use D3D hardware lighting — they use the `SetStaticLightingVertexColors` BAKE (`calc_point_light`, like cells), and the captured `intensity=100` light was a different object (player/portal). **DO NOT port the D3D-FF model — the math says it would make objects brighter, not dimmer.**
|
||
|
||
**Files:** `src/AcDream.App/Rendering/Shaders/mesh_modern.vert` (`pointContribution`/`accumulateLights`); `src/AcDream.Core/Lighting/LightManager.cs` (`SelectForObject`); `LightBake.cs` (verbatim calc_point_light, unwired).
|
||
|
||
**Research:** `docs/research/2026-06-18-lighting-a7-fixABC-shipped-fixD-handoff.md` (full grounding + cdb cheat-sheet + the next capture); `claude-memory/reference_retail_ambient_values.md`.
|
||
|
||
**Acceptance:** Determine the building's actual render path (bake vs D3D-FF; is `SetStaticLightingVertexColors` 0x0059cfe0 called for it / is `D3DRS_LIGHTING` on), then make the object torch contribution match retail — user side-by-side sign-off (meeting hall stays dim near torches).
|
||
|
||
---
|
||
|
||
## #139 — D.2b retail UI polish: chat text colors + buttons
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (cosmetic fit-and-finish — the widget generalization works and matches the prior hand-made build; this is polish vs a side-by-side retail client)
|
||
**Filed:** 2026-06-16
|
||
**Component:** ui — D.2b retail UI (chat window + buttons)
|
||
|
||
**Description (user):** After the widget-generalization pass landed (2026-06-16), two areas want a polish pass against retail:
|
||
1. **Chat text colors** — the per-`ChatKind` transcript text colors need tuning to match retail more precisely. Current values come from a live cdb dump of the named `RGBAColor` constants (colorWhite / BrightPurple / LightBlue / Green / LightRed / Grey) mapped per `ChatKind` in `ChatWindowController.RetailChatColor`. The four common kinds (speech/tell/channel/system) are confirmed; the rarer kinds (emote, soul-emote, combat, popup) map to the nearest named color and may be off — verify each against a side-by-side retail client.
|
||
2. **Buttons** — the chat buttons (Send, Max/Min, and the channel "Chat ▸" menu button) want visual polish: **pressed / hover state feedback** (`UiButton` currently draws only its default-state sprite; the dat carries `Normal`/`Pressed`/`Highlight` states it does not yet switch on), plus a check that the face 3-slice + autosize read cleanly at all widths.
|
||
|
||
**Root cause / status:** Deferred polish, NOT a regression — the generalized chat matches the prior hand-made build (user-confirmed 2026-06-16). `UiButton` intentionally mirrors `UiDatElement`'s single-state render (pressed-state was out of the generalization's scope); chat colors are best-effort from the cdb dump.
|
||
|
||
**Files:**
|
||
- `src/AcDream.App/UI/Layout/ChatWindowController.cs` — `RetailChatColor(ChatKind)` per-kind color map.
|
||
- `src/AcDream.App/UI/UiButton.cs` — `ActiveFile()` / `OnEvent` (no pressed-state swap yet; dat has Normal/Pressed/Highlight).
|
||
- `src/AcDream.App/UI/UiMenu.cs` — `DrawButtonFace` (Normal vs Pressed sprite) for the channel button.
|
||
|
||
**Research:** `claude-memory/reference_retail_chat_colors.md` (the cdb chat-color dump + recipe).
|
||
|
||
**Acceptance:** Chat text colors and button (pressed/hover) states match a side-by-side retail client — user's visual sign-off.
|
||
|
||
---
|
||
|
||
## #138 — Teleport OUT of a dungeon loads the outdoor world incompletely + position desync
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM (breaks the dungeon→outdoor transition; collision + visuals wrong after exit)
|
||
**Filed:** 2026-06-14
|
||
**Component:** streaming — dungeon collapse↔expand (the #133/#135 collapse) + teleport-arrival
|
||
|
||
**Description (user):** taking a portal OUT of a dungeon to the outdoor world often loads
|
||
the world incompletely — **fewer objects than expected (e.g. missing trees/scenery)**, and
|
||
**collision doesn't work properly**. There's also a **position desync**: "it's like I'm not
|
||
moving while my character is moving" (the avatar animates/advances but the player's
|
||
actual position / camera doesn't track, or vice-versa).
|
||
|
||
**Root cause / status (hypothesis — needs investigation):** very likely a gap in the
|
||
dungeon-streaming **collapse→expand** introduced for #133/#135. Inside a dungeon, streaming
|
||
is COLLAPSED to the single dungeon landblock (radius-0). On teleport OUT,
|
||
`StreamingController.ExitDungeonExpand` must rebuild the full 25×25 outdoor window at the new
|
||
center. Suspects: (a) the expand doesn't fully re-enqueue / re-hydrate the outdoor landblocks
|
||
(→ missing trees/scenery + no collision because shadow-object registration never ran for the
|
||
un-hydrated blocks); (b) the teleport-arrival recenter (`OnLivePositionUpdated`) +
|
||
`PreCollapseToDungeon`/observer interaction leaves the streaming observer pinned wrong after
|
||
exit; (c) the position desync = the player controller / streaming observer disagree on the
|
||
post-exit world position (the avatar moves in one frame, the streaming/camera in another).
|
||
Pairs with #135 (`712f17f`/`2c92375`) — same collapse machinery; the EXIT path is the gap.
|
||
|
||
**Files:** `src/AcDream.App/Streaming/StreamingController.cs` (`ExitDungeonExpand`, the
|
||
collapse/expand hysteresis), `src/AcDream.App/Rendering/GameWindow.cs` (`OnLivePositionUpdated`
|
||
teleport recenter ~4912, the streaming Tick gate ~6890, the PortalSpace observer branch),
|
||
`TeleportArrivalController`. Cross-check the post-exit shadow-object/collision registration.
|
||
|
||
**Acceptance:** portal out of the 0x0007 dungeon → full outdoor world streams (trees/scenery
|
||
present), collision works, and the player position tracks correctly (no avatar-vs-camera desync).
|
||
|
||
---
|
||
|
||
## #137 — Dungeon collision incorrect at doors and wall openings
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM (movement/collision correctness in dungeons)
|
||
**Filed:** 2026-06-14
|
||
**Component:** physics — EnvCell collision (doors, portal openings, cell geometry)
|
||
|
||
**Description (user):** collision is still wrong in dungeons — **doors** and **openings in
|
||
walls** in particular. (Symptoms not fully characterized yet: likely walking through
|
||
openings that should block / blocking at openings that should pass, and door collision not
|
||
matching the door's open/closed state.)
|
||
|
||
**Root cause / status (to investigate):** dungeon collision is EnvCell-based — the cell's
|
||
collision BSP + portal openings + per-cell static objects (doors). Candidates: door
|
||
apparatus collision in EnvCells (open/closed BSP swap) not fully ported; portal-opening
|
||
(wall gap) collision geometry handled differently from buildings; the per-cell
|
||
shadow-object registration (A6.P4, see the physics digest) for dungeon EnvCell statics.
|
||
Related families: #32 (edge-slide), #116 (slide-response), the door-collision saga
|
||
(see `feedback_dedup_keys_after_cardinality_change`, `feedback_retail_per_cell_shadow_list`).
|
||
Needs a targeted repro (which door / which opening, expected vs actual) before fixing —
|
||
oracle-first per the physics digest.
|
||
|
||
**Files:** `src/AcDream.Core/Physics/` (EnvCell collision, CellTransit, the door apparatus),
|
||
`src/AcDream.Core/Physics/ShadowObjectRegistry.cs` (per-cell registration). See
|
||
`claude-memory/project_physics_collision_digest.md` (the collision SSOT + DO-NOT-RETRY table).
|
||
|
||
**Acceptance:** doors block/pass per their open/closed state; wall openings pass; solid walls
|
||
block — matching retail, in the 0x0007 dungeon.
|
||
|
||
---
|
||
|
||
## #136 — DONE — "red cone" in the 0x0007 dungeon was an editor-only placement marker acdream drew (retail hides it)
|
||
|
||
**Status:** FIXED `6f81e2c` (2026-06-14) — verified live via frame dump: the red cone +
|
||
green floor "petals" are gone, all real dungeon decorations still render. User-approved
|
||
frozen-phase fix.
|
||
**Severity:** LOW (cosmetic; one marker in one dungeon)
|
||
**Filed/Fixed:** 2026-06-14
|
||
**Component:** rendering — EnvCell static-object hydration (WB-derived path) vs retail degrade
|
||
|
||
**Description:** In the `0x0007` Town Network dungeon a bright-RED downward cone (+ a
|
||
green/red shape on the floor) rendered ~6 m from the login spawn; the user's side-by-side
|
||
retail client showed NOTHING there. Became visible only after the #135 login-into-dungeon
|
||
fix placed the player at the exact saved spawn next to it.
|
||
|
||
**Root cause (definitive):** the cone is ONE dat-hydrated EnvCell static object (`guid=0`,
|
||
`id=0x40000835`, Setup `0x02000C39` / GfxObj `0x010028CA`) baked into cell `0x00070145`,
|
||
using pure red+green MARKER surfaces (`0x08000109` red, `0x0800010A` green). It is an
|
||
**editor-only placement marker**: its `DIDDegrade` table `0x11000118` =
|
||
`{slot0 Id=mesh MaxDist=0, slot1 Id=0 MaxDist=FLT_MAX}` — visible ONLY at distance 0 (the
|
||
WorldBuilder editor origin) and degraded to GfxObj **id 0 (= nothing)** at any real distance.
|
||
Retail's distance-based degrade (`CPhysicsPart::UpdateViewerDistance` 0x0050E030 → `Draw`
|
||
0x0050D7A0 draws `gfxobj[deg_level]`) therefore never draws it in the live client. acdream's
|
||
render path is extracted from **WorldBuilder**, which — being an editor — renders every cell
|
||
static's base mesh directly and has **no degrade handling at all** (zero `DIDDegrade` refs in
|
||
`references/WorldBuilder`), so acdream inherited "show the marker" and drew it forever. (NOT
|
||
a texture/lighting bug — the cone's *own* object 0x70007055 decodes tan and was a red
|
||
herring; the marker is a separate `guid=0` dat static.)
|
||
|
||
**Fix (`6f81e2c`):** `GfxObjDegradeResolver.IsRuntimeHiddenMarker()` detects the editor-marker
|
||
pattern (`HasDIDDegrade` + `Degrades[0].MaxDist==0` + a degrade entry with `Id==0`). EnvCell
|
||
static-object hydration (`GameWindow.cs` ~5793) skips such GfxObjs — whole-stab for bare
|
||
GfxObj stabs, per-part for Setup stabs (an all-marker Setup then drops via `meshRefs.Count==0`).
|
||
Faithful equivalent of retail's runtime degrade for static geometry (always viewed at
|
||
distance > 0); real LOD objects (`slot0.MaxDist>0`) and degrade-to-real-mesh objects are
|
||
untouched. 4 new `GfxObjDegradeResolver` unit tests.
|
||
|
||
**Follow-up (not done):** outdoor `LandBlockInfo.Objects` stabs could carry the same markers;
|
||
apply `IsRuntimeHiddenMarker` there too if any surface. Also revealed (separate): the per-
|
||
pixel point-light shader overblows close torches (no per-channel `min(scale·color,color)` cap
|
||
vs retail `calc_point_light`) — the bright-red dungeon WALL under normal lighting; tracked
|
||
under the #79/#93 A7 lighting umbrella.
|
||
|
||
---
|
||
|
||
## #135 — ~30 s low-FPS ramp at login (≈10 fps → high) before streaming settles
|
||
|
||
**Status:** DONE `712f17f`+`2c92375` (2026-06-14) — user-verified: login into the 0x0007 dungeon is FPS-steady from the start; dungeon loads + places the player. (NOTE: the teleport-OUT path has a separate streaming gap — see #138.)
|
||
**Severity:** LOW (startup-only; self-corrects)
|
||
**Filed:** 2026-06-14
|
||
**Component:** streaming — first-frame bootstrap vs the dungeon collapse
|
||
|
||
**FIX (2026-06-14):** pre-collapse streaming the instant we recenter onto a SEALED
|
||
dungeon cell at login/teleport, before the first `NormalTick` bootstraps the window.
|
||
- `StreamingController.PreCollapseToDungeon(cx,cy)` — fires the existing `EnterDungeonCollapse`
|
||
early (idempotent), so the expensive ocean-grid neighbour window is never enqueued
|
||
(teleport) / is enqueued-then-immediately-cleared for a cheap Holtburg frame (login).
|
||
- `GameWindow.IsSealedDungeonCell(cellId)` — reads the `EnvCell` dat `SeenOutside` flag
|
||
(the same flag the hydrated `ObjCell.SeenOutside` + the per-frame gate use) so a cottage/inn
|
||
interior keeps its outdoor surround; excludes the 0xFFFE/0xFFFF shell ids.
|
||
- Hooks in `OnLiveEntitySpawnedLocked` (login) + `OnLivePositionUpdated` (teleport).
|
||
- Observer robustness: during a teleport `PortalSpace` hold the observer follows the
|
||
recentered destination (not the frozen position); `_lastLivePlayerLandblockId` is now
|
||
filtered to the player guid (resolving a Phase A.1 TODO) so a stray NPC update can't drift
|
||
the login-hold observer off the dungeon and trip `ExitDungeonExpand`.
|
||
Adversarially reviewed (3 lenses); register row AP-36 amended. Tests in
|
||
`StreamingControllerDungeonGateTests` (5 new, incl. the real Tick-then-PreCollapse ordering).
|
||
|
||
**Description:** On login into a dungeon, FPS starts ~10 and climbs over ~30 s before
|
||
settling (then 1000+ fps). User: "we still have about 30ish seconds before FPS is ramped
|
||
up; when logging in I get like 10 then it slowly increases."
|
||
|
||
**Root cause / status:** The #133 streaming collapse (`5686050`/`d9e7dd6`/`7d8da99`) only
|
||
engages once CurrCell resolves to a sealed cell (the snap, a few s in). Before that the
|
||
first Tick bootstraps the full 25×25 window, so ~24 neighbour ocean-grid dungeons (+ their
|
||
~19k entities) load, then unload when the collapse fires. The collapse-at-snap change moved
|
||
the trigger from finalize-time (~30 s) toward snap-time but the bootstrap churn remains.
|
||
Clean fix = pre-collapse at login when the spawn cell is a sealed dungeon cell so the full
|
||
window never enqueues (touches the sensitive login spawn path — do carefully; no band-aid).
|
||
|
||
**Files:** `GameWindow.cs:6885` (streaming Tick gate); `StreamingController.cs` (collapse);
|
||
login recenter `OnLiveEntitySpawnedLocked` ~2470.
|
||
|
||
**Acceptance:** Login into a dungeon reaches steady-state FPS within ~1–2 s (no full-window
|
||
neighbour load/unload churn).
|
||
|
||
---
|
||
|
||
## #134 — Player "lags downward" instead of gliding along a dungeon ramp edge
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW-MEDIUM (movement feel; not a hard traversal block)
|
||
**Filed:** 2026-06-14
|
||
**Component:** physics — slope-walk / edge-slide response
|
||
|
||
**Description:** Running up or down against a dungeon ramp's edge, the player "sort of lags
|
||
downwards" instead of gliding/sliding ALONG the ramp surface (up when running up, down when
|
||
running down). Reported in the 0x0007 Town Network dungeon ramp after #133.
|
||
|
||
**Root cause / status:** Surfaced (not caused) by the #133 connector-cell physics
|
||
registration (`3e006d3`): the ramp connector cell's collision is now fully resident in the
|
||
physics graph, so the slope-walk / edge-slide response on it is exercised for the first time.
|
||
"Lag down" suggests the slide velocity is projected toward gravity rather than along the
|
||
contact plane (the slope tangent). Likely the retail edge-slide / slope-slide response is
|
||
incomplete — see #32 (retail edge-slide/cliff-slide/precipice-slide incomplete) and the
|
||
AP-6 / TS-1 / TS-4 slide rows in the divergence register. NO band-aid — port the retail
|
||
slide-response.
|
||
|
||
**Files:** `src/AcDream.Core/Physics/` (slide-response in TransitionTypes / BSPQuery); ramp
|
||
cell 0x0007014D + neighbours.
|
||
|
||
**Acceptance:** Running up a walkable ramp climbs it smoothly; running into the edge slides
|
||
along the slope (up/down per input direction), matching retail feel.
|
||
|
||
---
|
||
|
||
## #133 — Teleport into a dungeon snaps the player BEFORE the dungeon landblock streams in → lands at the old landblock's frame (ocean), not the dungeon
|
||
|
||
**Status:** OPEN — promoted to **Phase G.3** (Dungeon streaming + portal
|
||
space + `PlayerTeleport` handling), **PULLED INTO M1.5** (user decision
|
||
2026-06-13: the indoor world isn't done while dungeons are broken; full
|
||
G.3 scope chosen). Spec: `docs/superpowers/specs/2026-06-13-dungeon-support-design.md`;
|
||
G.3a plan: `docs/superpowers/plans/2026-06-13-dungeon-support-g3a.md`.
|
||
This is now an M1.5 exit-gate blocker, not deferred.
|
||
|
||
**PROGRESS (2026-06-13 PM — G.3a core LANDED + Bug A fixed; gate exposed #95):**
|
||
the teleport-timing root cause IS fixed. G.3a shipped the `TeleportArrivalController`
|
||
hold-until-hydration (`7947d7a`/`aca4b46`/`f22121b`) + the validated-claim
|
||
landblock-prefix fix (`2ce5e5c`, "Bug A"). Live gate proof: a real `PlayerTeleport`
|
||
into the `0x0007` dungeon held through the 46 km jump and grounded the player on the
|
||
dungeon's walkable floor (`[snap] claim=0x00070143 VALIDATED -> z=0.000`) — **no
|
||
ocean.** The "terrain-less landblock" framing was refuted earlier (dat probe: dungeon
|
||
= flat-terrain LandBlock + EnvCells). REMAINING blockers, both exposed at the gate:
|
||
(1) **#95 CONFIRMED LIVE** — the dungeon renders as "thin air" because WB-DIAG blows
|
||
up to ~9.1M instances/frame at `0x0007` (see #95); (2) **possible Bug C** — per-tick
|
||
membership may still drift in the dungeon's negative-local-Y frame (ACE `movement
|
||
pre-validation failed` spam) — re-gate after Bug A to confirm. NOTE: a render-only
|
||
EnvCell hydration decouple was tried in G.3a and REVERTED (`e7058ca`) — it made the
|
||
player character invisible at Holtburg (it touched the shared building hydration
|
||
path); re-approach separately if a geometry-less collision cell ever needs it.
|
||
|
||
**NEW GAP (2026-06-13 PM — login-INTO-a-dungeon):** logging in while the saved
|
||
character is inside a far dungeon hangs at the auto-entry hold (player frozen,
|
||
no `[snap]`/`auto-entered player mode`, movement input ignored). Root: the
|
||
streaming center is set ONCE at startup to the default (`_liveCenterX/Y = centerX/
|
||
centerY`, `GameWindow.cs:1942` → "centered on 0xA9B4FFFF") and the login spawn never
|
||
recenters it; a dungeon spawn 46 km away never streams, so `IsSpawnCellReady(spawn
|
||
cell)` stays false and the #107 hold waits forever. The TELEPORT-arrival path
|
||
recenters (G.3a `TeleportArrivalController`); the LOGIN path does not. Fix shape =
|
||
recenter streaming onto the spawn landblock when the login spawn first arrives
|
||
(mind the #107 auto-entry hold's `SampleTerrainZ(pe.Position)` frame after the
|
||
recenter). Pre-existing; only surfaces now that the test character can be saved in
|
||
a dungeon. Workaround to unblock testing: move `+Acdream` out of the dungeon
|
||
server-side (ACE) before logging in. **FIXED 2026-06-13 (`47ae237`)** — the login
|
||
player-spawn path now recenters `_liveCenterX/Y` onto the spawn landblock (mirrors
|
||
the teleport-arrival recenter; no-op for a same-landblock Holtburg login). Verified
|
||
live: `live: login spawn — recentering streaming from (169,180) to (0,7)` → dungeon
|
||
streams → `auto-entered player mode` in the dungeon.
|
||
|
||
**✅ DUNGEON RENDERS — M1.5 milestone (2026-06-13 PM, autonomous /loop, objectively
|
||
verified).** With Bug A (`2ce5e5c`) + login-into-dungeon (`47ae237`), a live launch
|
||
into the `0x0007` dungeon: player grounded on the dungeon floor (`[snap] claim=0x00070143
|
||
VALIDATED z=0.000`), correct membership (cell stays `0x0007…`, ZERO ACE `failed
|
||
transition` spam), and the render budget is sane — **WB-DIAG instances ~39,000
|
||
(meshMissing=0)** vs the 9.1M pre-Bug-A blowup (#95, now RESOLVED as a Bug-A symptom).
|
||
User-confirmed: "no errors from ACE this time."
|
||
|
||
**✅ DUNGEON FPS FIXED + GREY BARRIER FIXED (2026-06-14, user-confirmed).** Two
|
||
separate causes, both resolved:
|
||
|
||
- **FPS (was 14–30, now ~1000+):** AC dungeons sit adjacent in the "ocean" landblock
|
||
grid, so the 25×25 (farRadius=12) streaming window pulled ~129 neighbour dungeons +
|
||
their ~19k particle emitters / entities each frame. Fix = **collapse streaming to the
|
||
player's single dungeon landblock** when CurrCell is a sealed EnvCell (`!SeenOutside`),
|
||
with landblock-level hysteresis to stop collapse↔expand thrash. Confirmed against ACE
|
||
(`landblock.IsDungeon → return adjacents` with no neighbours): dungeons have no neighbour
|
||
landblocks, so collapsing to the one block is retail-faithful. Commits `5686050` (collapse)
|
||
+ `d9e7dd6` (hysteresis) + `2561918` (pin to CurrCell's landblock, not the position-derived
|
||
one — the negative cell-local-Y made `floor(pp.Y/192)` land one block off and unload the
|
||
REAL dungeon). Divergence register: AP-36.
|
||
|
||
- **GREY BARRIER (the "barrier above the ramp" / cellar-mouth grey):** portals-only
|
||
connector cells (ramp mouths, stair landings, cellar throats) build **0 drawable
|
||
sub-meshes**, and BOTH cell-registration gates (`BuildLoadedCell` → visibility
|
||
`_cellVisibility`, and `CacheCellStruct` → the physics cell graph) were gated on
|
||
`cellSubMeshes.Count > 0`. So a connector cell never registered → the portal flood
|
||
hit a **lookup-miss** at its opening (the un-flooded opening shows the clear/grey
|
||
colour) AND the camera eye-sweep couldn't transit through it. Fix = register EVERY
|
||
cell with a valid cellStruct for visibility + physics; only the *drawing* registration
|
||
stays gated on having sub-meshes. Commits `d90c538` (visibility) + `3e006d3` (physics
|
||
graph). The physics-graph half EXPOSED the ramp slide-response feel (now **#134**).
|
||
Three render-MATH theories (portal_side centroid, on-screen clip, near-eye projection)
|
||
were instrumented and REFUTED before the real lookup-miss cause was found — apparatus
|
||
discipline held. Render-pipeline digest updated.
|
||
|
||
Residual (filed separately): login FPS ramp **#135**; ramp slide-response **#134**; the
|
||
A7 per-vertex lighting bake (below) is the remaining "lighting off" work.
|
||
|
||
**✅ A7 dungeon lighting — selection fix LANDED + objectively verified (`a80061b`).** The
|
||
"lighting off" report was NOT missing torches — the `ACDREAM_PROBE_LIGHT` diagnostic
|
||
(`d6fb788`) showed the dungeon correctly gets retail's flat 0.2 indoor ambient + sun zeroed
|
||
(`UpdateSunFromSky`, `playerInsideCell` true) AND **2227 torch/point-lights register**. The
|
||
bug was the active-light SELECTION: `LightManager.Tick` dropped any light whose range didn't
|
||
reach the VIEWER (`DistSq > Range²·slack² → skip`), so a room with 2227 torches lit only the
|
||
~1 the player stood inside (`activeLights≈1`, rest at flat 0.2). Retail's D3D model picks the
|
||
8 NEAREST lights and applies the hard range-cutoff PER SURFACE in the shader
|
||
(`mesh_modern.frag: if (d < range)`). Fix = drop the viewer-range candidacy filter, take the
|
||
nearest 8. Probe after: **`activeLights` 2→8** in the dungeon (the room's 8 nearest torches now
|
||
light it). Core lighting suite green. Then `Range = Falloff × 1.5` (retail `rangeAdjust`,
|
||
`config_hardware_light` 0x0059adc, `a80061b`+) widened the pools. Ambient 0.20 is
|
||
retail-faithful (`SmartBox::SetWorldAmbientLight(0.2f)`); the 0.30 was a red herring
|
||
(`CreatureMode` paperdoll renderer, not world cells).
|
||
|
||
**⚠️ REAL remaining cause — REVISED 2026-06-14 (the earlier "mis-read intensity" theory is
|
||
REFUTED).** `intensity=100` is the **REAL dat value** (raw-byte verified `00 00 C8 42` = 100.0f;
|
||
DatReaderWriter 2.1.7 parses it correctly; the garbage `cone` is MSVC `CD CD CD CD`
|
||
uninitialized fill Turbine baked into the dat — point lights never read it). **DO NOT `÷100`.**
|
||
The actual divergence is the **[HIGH] `no-static-light-burnin`**: retail bakes ALL of a cell's
|
||
reaching static lights **PER-VERTEX once** (`D3DPolyRender::SetStaticLightingVertexColors`
|
||
0x0059cfe0 → `calc_point_light` 0x0059c8b0, Gouraud-interpolated → uniform, never blown out via
|
||
the per-channel min-to-colour clamp), while we light **per-PIXEL with only the 8 nearest-to-
|
||
CAMERA lights** → bright pools near torches, dark between, and a crescent that slides as the
|
||
camera re-ranks the 8-slot list. Diagnosed via a 5-agent investigation + a clean Ghidra
|
||
decompile (the BN pseudo-C is x87-mangled). **LANDED:** the per-pixel `(1-dist/falloff_eff)`
|
||
shader ramp (`007e287`, necessary but NOT sufficient — it can't fix the per-vertex-vs-per-pixel
|
||
structure) + the GL-free `LightBake` Core (`3b93f91`: the verbatim `calc_point_light` port +
|
||
7 conformance tests). **REMAINING — the A7 integration:** add a per-vertex linear-RGB colour
|
||
attribute to the cell mesh + a bake driver keyed on `envCellId` (NOT the dedup `cellGeomId` —
|
||
adjacent rooms share a geom but not their torches) + consume it in `mesh_modern.frag` for cell
|
||
draws; bound the bake's light set to the player dungeon (#133's FPS collapse already does this).
|
||
Belongs to the #79/#93 indoor-lighting umbrella; outdoor static objects + building shells still
|
||
use the per-pixel-8 path (the same spottiness — separate follow-up). **NOTE — dungeon FPS is
|
||
FIXED** (was 14–30 from streaming ~129 neighbour ocean-grid dungeons; now ~1000+ fps after the
|
||
#133 streaming collapse + the allocation-free 8-light partial-select, `5872bcf`/`5686050`).
|
||
**Severity:** HIGH (any far/dungeon teleport is unusable)
|
||
**Filed:** 2026-06-13 (M1.5 dungeon-demo gate attempt — meeting-hall portal)
|
||
**Component:** physics/streaming — teleport-arrival snap vs async landblock hydration
|
||
|
||
**Symptom (user):** used the meeting-hall portal to a dungeon; "no
|
||
dungeon, just ocean (where the dungeon is placed)." ACE spams `failed
|
||
transition for +Acdream from 0x01250126 [30 -60 6.0] to 0xA9B0000E
|
||
[-32227 -26748 5.9]` … marching south through `0xA993/0xA97F/…/0xA969`
|
||
at Z≈−0.9 (underwater) — the server keeps rejecting the client's bogus
|
||
outdoor movement.
|
||
|
||
**Root cause (confirmed against code + the diagnostic log
|
||
`launch-dungeon-diag.log`):** ACE correctly placed the player in the
|
||
meeting-hall dungeon cell `0x01250126` (landblock `0x0125` = (1,37)). The
|
||
acdream teleport-arrival handler (`GameWindow.cs:4877-4960`) DOES recenter
|
||
the streaming origin to (1,37) (`_liveCenterX/Y`, :4910-4912), but then
|
||
**immediately** calls `_physicsEngine.Resolve(pos=(30,-60,6.005),
|
||
cell=0x01250126)` to snap the player (:4928-4931) — BEFORE the dungeon
|
||
landblock has streamed in. The physics engine still has only the OLD
|
||
Holtburg landblocks resident (A9B4 + neighbours), so `Resolve` can't find
|
||
the dungeon cell and falls back to an OUTDOOR scan against the resident
|
||
landblocks: local (30,−60) maps into A9B3 (the loaded block south of the
|
||
A9B4 spawn) → snaps to `0xA9B3000E`, terrainZ=94, indoor=False (the
|
||
`[snap]` line). The player is now at Holtburg's south edge; streaming then
|
||
shifts the frame out from under them and they slide south into ocean
|
||
(the `[cell-transit] A9B3→A9B2→…` chain mirrors ACE's failed-transition
|
||
sequence exactly).
|
||
|
||
**Fix shape (G.3):** on a far/different-landblock teleport, recenter +
|
||
HOLD the snap until the destination dungeon landblock/cell hydrates (reuse
|
||
the #107 `IsSpawnCellReady` spawn-ready gate, applied to the teleport-
|
||
arrival path instead of only login), then place into the indoor cell via
|
||
the validated-claim path (#107/#111 `SetPositionInternal` shape). Also
|
||
audit the streaming controller actually LOADS the far dungeon landblock on
|
||
recenter (the 5×5 Chebyshev window around the new center), and that the
|
||
old landblocks unload without stranding the player mid-frame-shift.
|
||
|
||
**Files:** `GameWindow.cs:4877-4960` (teleport arrival),
|
||
`PhysicsEngine.Resolve` (the outdoor fallback), the #107 `IsSpawnCellReady`
|
||
gate, `StreamingController` recenter.
|
||
|
||
**Acceptance:** teleport into the meeting-hall dungeon → the player stands
|
||
in the dungeon cell, the dungeon renders (3-5 rooms), walls block, no
|
||
ocean / no ACE `failed transition` spam.
|
||
|
||
**Apparatus:** `ACDREAM_PROBE_CELL=1` ([cell-transit]) + `ACDREAM_PROBE_VIEWER=1`
|
||
([viewer]) + `ACDREAM_WB_DIAG=1` + the always-on `[snap]`/`live: teleport`
|
||
lines capture the whole chain (`launch-dungeon-diag.log`, this session).
|
||
|
||
---
|
||
|
||
## #104 — Scene VFX particles not clipped to the PView visible cell set
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW
|
||
**Filed:** 2026-06-02
|
||
**Component:** render, vfx
|
||
|
||
**Description:** Scene-pass VFX particles (spell effects, smoke) are drawn from their world-space
|
||
position only; they are not gated by the PView visible cell set, so a particle emitter in a
|
||
sealed (non-visible) cell can bleed past a wall edge. In practice this is mostly masked: scene
|
||
particles ARE depth-tested (walls occlude most of their geometry), the dominant indoor entity
|
||
bleed is already gated by the Phase W Stage 5 entity gate
|
||
(`WbDrawDispatcher.EntityPassesVisibleCellGate`), and Stage 4 already scissors the SKY particle
|
||
passes to the doorway. The residual is the occasional additive particle visible past a wall edge.
|
||
|
||
**Root cause / status:** Particles carry no cell id. `ParticleEmitter` (`Vfx/VfxModel.cs`) has
|
||
`AnchorPos` + `AttachedObjectId` but no owning-cell id; `Particle` has a world `Position` only. A
|
||
clean fix adds an `OwnerCellId` to `ParticleEmitter` (set at spawn from the owning entity's
|
||
`ParentCellId`), threads a `HashSet<uint>? visibleCellIds` into `ParticleRenderer.BuildDrawList`,
|
||
and skips emitters whose `OwnerCellId` ∉ the visible set. That touches `IParticleSystem.SpawnEmitter`,
|
||
`ParticleSystem`, `ParticleHookSink`, and the `SpawnEmitter` call sites (~6–8 files) — a plumbing
|
||
pass, deliberately deferred out of the Phase W seal (which covers sky/terrain/walls/entities).
|
||
|
||
**Files:** `src/AcDream.App/Rendering/ParticleRenderer.cs` (BuildDrawList), `src/AcDream.Core/Vfx/`
|
||
(ParticleSystem, VfxModel), `src/AcDream.App/Rendering/Vfx/ParticleHookSink.cs`.
|
||
|
||
**Acceptance:** A scene-particle emitter in a non-visible cell does not draw; outdoor particles
|
||
(null `visibleCellIds`) unaffected; no regression on fireplace/spell VFX in the visible cell.
|
||
|
||
---
|
||
|
||
## #103 — Phase A8.F portal-frame indoor rendering broken at runtime (visual-gate failure)
|
||
|
||
**Status:** SUPERSEDED 2026-05-30 by **Phase U (Unified Render Pipeline)**. The
|
||
two-pipe (inside/outside) approach this bug lives in is being abandoned wholesale —
|
||
the broken `RenderInsideOut` two-pipe path is deleted as Task 1 of Phase U and
|
||
replaced by a single unified retail `PView` portal-visibility pipeline. #103 will
|
||
not be fixed in place. See
|
||
[docs/research/2026-05-30-unified-render-pipeline-decision-and-handoff.md](research/2026-05-30-unified-render-pipeline-decision-and-handoff.md).
|
||
**Severity:** MEDIUM (opt-in branch only — default game unaffected)
|
||
**Filed:** 2026-05-29
|
||
**Component:** render (indoor visibility)
|
||
|
||
**Description:** With `ACDREAM_A8_INDOOR_BRANCH=1`, the A8.F retail portal-frame port
|
||
renders indoor/outside-in broadly wrong: cottage/cellar interiors covered in outdoor
|
||
terrain with transparent walls; invisible walls in other houses from inside and outside.
|
||
Default game (env var off) is unaffected — `cameraInsideBuilding = a8IndoorBranchEnabled
|
||
&& inside` (GameWindow.cs:7343). The old cellar flap remains in the default path.
|
||
|
||
**Root cause / status:** Two compounding causes (evidence in the handoff): (1) the
|
||
`OutsideView` builder under-produces — `OUTSIDEVIEW polys=0` most frames, and when
|
||
non-empty it doesn't recursively narrow (cellar shows ~full window). (2) The Task-6
|
||
Job-A/B decoupling draws terrain UNGATED when `OutsideView` is empty (`else` branch),
|
||
flooding the cell interior over the (correctly-rendered) walls. Cell walls DO render
|
||
(`[opaque]` tris=50-108). Projection math is correct; the builder integration is fragile.
|
||
|
||
**Files:** `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs` (builder under-produces);
|
||
`src/AcDream.App/Rendering/GameWindow.cs` `RenderInsideOutAcdream` Step-4 `else` ungated-terrain (~11142).
|
||
|
||
**Research:** [docs/research/2026-05-29-a8f-visual-gate-failure-handoff.md](research/2026-05-29-a8f-visual-gate-failure-handoff.md) (root-cause analysis, apparatus, first-fix hypothesis, pickup prompt).
|
||
|
||
**Acceptance:** Holtburg cottage cellar renders with solid walls and no terrain flood;
|
||
terrain shows only through correctly-clipped portal openings; no invisible walls.
|
||
Related: #102 (builder dungeon-scaling fixpoint).
|
||
|
||
# Active issues
|
||
|
||
---
|
||
|
||
## #102 — A8.F PortalVisibilityBuilder — port retail update_count fixpoint (replace MaxReprocessPerCell cap)
|
||
|
||
**Status:** PARTIALLY RESOLVED (Phase U.2a, 2026-05-30, commit `d880775`)
|
||
**Severity:** MEDIUM → LOW (residual is diamond-topology clip-completeness only)
|
||
**Filed:** 2026-05-29
|
||
**Component:** rendering, visibility, EnvCell portal traversal
|
||
|
||
**U.2a resolution (2026-05-30):** Reading the decomp showed retail does NOT
|
||
re-enqueue on view-growth: `AddViewToPortals` (433446) enqueues a cell via
|
||
`InsCellTodoList` ONLY in the first-discovery branch (`ecx_5 == 0`); later
|
||
growth goes through `AddToCell` (433050) in place and never re-enqueues. U.2a
|
||
replaced the `MaxReprocessPerCell` cap with an **enqueue-once gate** (a `seen`
|
||
set = retail `cell_view_done`, 433784) + a distance-priority work list (retail
|
||
`InsCellTodoList`). This **closes I-1 and I-2**: the clip-region union into a
|
||
neighbour now runs UNCONDITIONALLY before the enqueue gate, so >4-portal cells
|
||
no longer under-count (I-1 gone), and each cell processes its exit portals
|
||
exactly once, so cyclic graphs no longer accumulate duplicate polygons (I-2
|
||
gone). The new `Build_CyclicHub_TerminatesAndBounds` test enforces the
|
||
acceptance (4-room ring ⇒ ≤5 cells, no dups). **Residual scope:** retail's
|
||
`AddToCell` ONWARD re-propagation of late growth (a cell reached via a longer
|
||
path AFTER it was drawn gets its own `CellView` unioned but does not
|
||
re-propagate that growth to ITS children) is NOT ported — this affects only
|
||
clip-region completeness on **diamond** topologies, never the visible cell set
|
||
or draw order. Track under U.6 (dungeon-scale validation). (The M-4
|
||
`OtherPortalClip` stub noted below is now CLOSED by Phase U.2b — a separate
|
||
concern from this onward-re-propagation gap.) A naive count-watermark
|
||
re-enqueue is NOT a valid fix (it never terminates, because `CellView.Add`
|
||
appends without merging) — the faithful fix is the in-place slice
|
||
re-propagation.
|
||
|
||
**Description:** A8.F Task 4 shipped a bounded-BFS port of retail's
|
||
`PView::ConstructView` → `ClipPortals` → `AddViewToPortals` in
|
||
[`src/AcDream.App/Rendering/PortalVisibilityBuilder.cs`](../src/AcDream.App/Rendering/PortalVisibilityBuilder.cs).
|
||
Code review found NO correctness bugs (the cellar-flap fix works and the
|
||
BFS terminates), but two scaling issues that bite only on CYCLIC /
|
||
high-fan-in portal graphs (dungeons, network hubs), NOT on the cottage
|
||
cellar (a 2-3 cell chain) which is the current M1.5 goal:
|
||
|
||
- **I-1 — the cap is load-bearing, not a safety net.** `MaxReprocessPerCell = 4`
|
||
is the *actual* termination mechanism for cyclic graphs. The
|
||
`if (nview.Polygons.Count > before)` re-enqueue-on-growth guard is a
|
||
near-no-op because `CellView.Add` (PortalView.cs) appends
|
||
unconditionally and never dedupes, so a cell almost always "grows" and
|
||
is re-enqueued — convergence relies entirely on the count hitting 4.
|
||
A cell reachable through **>4 contributing portals under-counts**
|
||
(drops legitimately-visible contributions).
|
||
- **I-2 — duplicate polygons accumulate on cyclic/multi-path graphs.**
|
||
Measured on a synthetic 4-room ring: 34 `OutsideView` polygons and
|
||
216-poly `CellView`s where retail converges to a small fixed set.
|
||
Correctness survives (overlapping stencil marks are idempotent) but
|
||
it's per-frame cost feeding the stencil pipeline.
|
||
|
||
**Root cause / status:** We approximate retail's monotone-fixpoint
|
||
convergence with a fixed re-process cap. Retail instead converges via an
|
||
`update_count` / `set_view(...,i)` slice watermark — each cell records a
|
||
timestamp/watermark of how much of its view has been propagated, so a
|
||
re-visit only re-propagates the *new* slice and the graph reaches a true
|
||
fixpoint with no duplicate accumulation and no arbitrary cap.
|
||
|
||
Retail anchors (`docs/research/named-retail/acclient_2013_pseudo_c.txt`):
|
||
- `AddToCell` 433050 — `esi[0x11]` update-count/slice watermark on the cell
|
||
- `InitCell` — per-cell timestamp init
|
||
- `AddViewToPortals` 433446 — change-detection that drives the fixpoint
|
||
|
||
**Related M-4 stub — CLOSED (Phase U.2b, 2026-05-30; reciprocal-resolution
|
||
fix 2026-05-30):** the neighbour-side `OtherPortalClip` (decomp:433524) is
|
||
ported. After a portal's near-side opening is clipped against the current
|
||
cell's view, `PortalVisibilityBuilder.ApplyReciprocalClip` resolves the
|
||
neighbour's matching back-portal **by direct index via the dat's
|
||
`CellPortal.OtherPortalId` back-link** (retail `arg2->other_portal_id`,
|
||
005a54b2), projects it through the neighbour's `WorldTransform`, and
|
||
intersects it into the propagated region before the union — so a cell's
|
||
clip region is the intersection of the opening seen from BOTH sides. The
|
||
reciprocal is `neighbour.PortalPolygons[portal.OtherPortalId]`, NOT a scan
|
||
for the first `OtherCellId` match. The direct index is load-bearing: a cell
|
||
with TWO portals to the same neighbour (real on the Holtburg cellar —
|
||
`0x148` has two portals to `0x149`, polys 40/41, and `0x149` has two
|
||
reciprocals back to `0x148`) clips each opening against its OWN reciprocal.
|
||
The earlier scan-by-first-match resolved both near-side openings to the
|
||
FIRST reciprocal, and disjoint apertures then intersected to empty —
|
||
HIDING the geometry through the second opening (under-inclusion). The fix
|
||
plumbs `OtherPortalId` through `CellPortalInfo` + `BuildLoadedCell`. Guards
|
||
degrade to over-include (never clip against a guessed polygon) when the
|
||
index is out of range, the polygon is missing/degenerate, or it projects
|
||
behind the camera. Can only TIGHTEN. Covered by
|
||
`PortalVisibilityBuilderTests.Build_AppliesReciprocalOtherPortalClip`
|
||
(reciprocal tightening) + `…_DegradesGracefully_WhenNoBackPortal`
|
||
(over-include degrade) + `…_MultiplePortalsToSameNeighbour_EachResolvesOwnReciprocal`
|
||
(the disjoint two-back-portal regression). (The diamond-topology onward
|
||
re-propagation of late growth remains out of scope here — tracked under
|
||
U.6.)
|
||
|
||
**Files:**
|
||
- `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs` — replace the
|
||
`MaxReprocessPerCell` cap + re-enqueue-on-growth guard with a
|
||
per-cell slice watermark; honest-limitation comment lives at the
|
||
`MaxReprocessPerCell` declaration.
|
||
- `src/AcDream.App/Rendering/PortalView.cs` — `CellView.Add` currently
|
||
never dedupes; the fixpoint port either dedupes here or tracks a
|
||
propagated-slice index per cell.
|
||
|
||
**Acceptance:** On a cyclic/hub portal graph (synthetic 4-room ring +
|
||
the Town Network dungeon hub), `OutsideView` / `CellView` polygon counts
|
||
converge to a small fixed set (no duplicate accumulation), every cell
|
||
reachable through any number of contributing portals is included, and
|
||
the BFS still terminates. Existing cottage-cellar tests stay green.
|
||
**MUST land before A8.F is relied on for dungeons** (dungeons are
|
||
currently blocked on #95 regardless).
|
||
|
||
---
|
||
|
||
## #87 — Drop WB fork patch by switching to PrepareEnvCellGeomMeshDataAsync
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM (band-aid removal; not user-visible)
|
||
**Filed:** 2026-05-19
|
||
**Component:** rendering, WB integration
|
||
|
||
**Description:** Phase 2 (2026-05-19) shipped a one-line patch in our
|
||
WB fork at `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs:1230`
|
||
(branch `acdream` on the fork, SHA `34460c4`) to guard a blind
|
||
`TryGet<Setup>(stab.Id, ...)` call against GfxObj-prefixed ids. That
|
||
patch fixes the symptom (missing floors) but is structurally a
|
||
band-aid — per CLAUDE.md's no-workarounds rule we should retire it.
|
||
|
||
The proper fix: switch our EnvCell rendering from
|
||
`PrepareMeshDataAsync(envCellId, ...)` (general-purpose entry that
|
||
also iterates static-object parts + emitters we don't need) to WB's
|
||
narrower `PrepareEnvCellGeomMeshDataAsync(geomId, environmentId, cellStructure, surfaces)`
|
||
at [`ObjectMeshManager.cs:386`](../references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs:386).
|
||
That function only builds the cell room mesh (floor / walls / ceiling),
|
||
which is the only piece we actually use from WB for cells — we already
|
||
hydrate static objects as separate `WorldEntity` instances in
|
||
`BuildInteriorEntitiesForStreaming`, and we run particle scripts via
|
||
our own `EntityScriptActivator` (Phase C.1.5b).
|
||
|
||
**Root cause / status:** Misuse of WB's general-purpose API for a
|
||
geometry-only need. The general-purpose path triggers static-object
|
||
iteration that has a bug (TryGet<Setup> without type check) AND that
|
||
does work we throw away. Both problems disappear if we use the
|
||
geometry-only entry point WB already exposes for exactly this purpose
|
||
(it's what WB's own `EnvCellRenderManager` uses internally).
|
||
|
||
**Trade-offs:**
|
||
|
||
| | Current (patched WB) | Switch to geom-only API |
|
||
|---|---|---|
|
||
| WB fork divergence | One-line patch | Zero |
|
||
| Future WB upstream merges | Conflicts | Clean |
|
||
| Performance | Slightly worse (wasted iteration) | Slightly better |
|
||
| Risk to other functionality | None (working today) | Needs re-verification |
|
||
|
||
**Files (the change):**
|
||
|
||
- `src/AcDream.App/Rendering/GameWindow.cs` around line 5367-5378
|
||
(cell-entity hydration — change `MeshRefs[0].GfxObjId` from `envCellId`
|
||
to `envCellId | 0x100000000UL`, the synthetic geom id with bit 32 set).
|
||
- `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs` — add a new method
|
||
`PrepareEnvCellGeomMesh(ulong geomId, uint environmentId, ushort cellStructure, List<ushort> surfaces)`
|
||
that forwards to `_meshManager.PrepareEnvCellGeomMeshDataAsync(...)`,
|
||
and call it from the streaming path instead of the bare
|
||
`IncrementRefCount(envCellId)`.
|
||
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs:1230`
|
||
— revert the type-check guard we added. The function returns to
|
||
pristine WB state.
|
||
|
||
**Acceptance:**
|
||
|
||
- Floors still render in Holtburg Inn (regression check vs Phase 2).
|
||
- `references/WorldBuilder` submodule pointer returns to upstream-clean
|
||
(no acdream-specific commits in the fork's `acdream` branch — or
|
||
rather, the `acdream` branch fast-forwards back to match upstream's
|
||
state for this file).
|
||
- Probe re-capture at Holtburg confirms `[indoor-upload] completed` for
|
||
all cells previously failing.
|
||
- No `[wb-error]` lines.
|
||
|
||
**Research:** [`docs/research/2026-05-19-indoor-cell-rendering-cause.md`](research/2026-05-19-indoor-cell-rendering-cause.md)
|
||
documents the underlying WB bug.
|
||
|
||
---
|
||
|
||
## Indoor walking issue cluster (2026-05-19)
|
||
|
||
The Phase 2 indoor cell rendering fix (floor now renders inside buildings)
|
||
surfaced nine pre-existing indoor bugs the user observed at Holtburg Inn
|
||
the moment they could walk indoors. None caused by the floor fix — all
|
||
existed before but were unobservable because there was no floor to stand
|
||
on. Filed individually below; #78 + #84 + #85 + #86 likely share a root
|
||
cause (cell BSP / portal-cull plumbing), and #79 + #80 + #81 + #82 share
|
||
the indoor-lighting plumbing.
|
||
|
||
---
|
||
|
||
## #78 — Outdoor geometry (stabs + terrain mesh) visible inside EnvCells
|
||
|
||
**Status:** OPEN — **PROMOTED 2026-06-02 to the full render-pipeline redesign** (this IS the
|
||
core interior-seal bug; root cause now PROVEN). See
|
||
[docs/research/2026-06-02-render-pipeline-redesign-handoff.md](research/2026-06-02-render-pipeline-redesign-handoff.md)
|
||
+ [the redesign plan](superpowers/plans/2026-06-02-render-pipeline-redesign-plan.md). Decisive evidence
|
||
(2026-06-02 [shell]/[vis] probes): the PVS + cell shells render correctly; the failure is the SEAL +
|
||
three inconsistent gates — concretely the `WbDrawDispatcher.cs:1756` `ParentCellId==null → return true`
|
||
bypass draws outdoor scenery indoors, and the indoor render draws the outdoor world then gates it
|
||
instead of running ONLY `DrawInside` (retail: visibility IS the cull). Fix = redesign Phase R1→R3.
|
||
**Severity:** HIGH (immediate visual jank; broadened scope per 2026-05-25 PM finding)
|
||
**Filed:** 2026-05-19 (broadened 2026-05-25; promoted to redesign 2026-06-02)
|
||
**Component:** rendering, visibility
|
||
|
||
**Description:** Standing inside Holtburg Inn looking at the floor or
|
||
walls, the user sees other buildings in the distance at their correct
|
||
world position + scale — but visible THROUGH the floor and walls. As if
|
||
the cell mesh is rendered but doesn't occlude or stencil-cull what's
|
||
behind it.
|
||
|
||
**Additional evidence (2026-05-25 PM, post-#100 visual verification):**
|
||
After issue #100 shipped (commits `f48c74a`, `a64e6f2`, `84e3b72`) and
|
||
removed the `hiddenTerrainCells` cell-collapse mechanism, the OUTDOOR
|
||
TERRAIN MESH is now (correctly per retail) rendered everywhere on the
|
||
landblock — including in 3D regions occupied by indoor EnvCell volumes.
|
||
Visual verification at a Holtburg cottage cellar showed a sharp-edged
|
||
rectangular grass patch (outdoor terrain at Z≈93.99) rendering over the
|
||
cellar stair geometry at certain camera angles. Clears when camera
|
||
moves closer (cottage walls + stair treads geometrically occlude the
|
||
terrain from new vantage points). Gameplay unaffected. **This is the
|
||
same root cause as the existing #78 hypothesis #2** ("outdoor stabs not
|
||
culled when player in EnvCell"), just with outdoor terrain mesh
|
||
affected in addition to outdoor stab entities. Per user direction,
|
||
NOT filed as a new issue — additional evidence reinforces #78's
|
||
hypothesis #2, broadens scope of the fix to include terrain culling.
|
||
|
||
**Root cause / status:** Two plausible causes:
|
||
1. The `+0.02f` Z bump applied to cell origin at `GameWindow.cs:5362`
|
||
pushes the floor mesh 2 cm above terrain, so depth test correctly
|
||
occludes terrain. But OUTDOOR STABS (landblock-baked building geometry)
|
||
at the same X,Y may have Z values comparable to or higher than the
|
||
cell-mesh floor, producing z-fighting / see-through.
|
||
2. **(High confidence as of 2026-05-25)** Outdoor geometry (stabs AND
|
||
terrain mesh) isn't being culled when the player is inside an
|
||
EnvCell — this is the Phase 1 Task 3 deferred work
|
||
("Cull outdoor stabs when indoors via VisibleCellIds"). WB has a
|
||
`RenderInsideOut` stencil pipeline (`references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs`)
|
||
that acdream never invokes. Retail anchor:
|
||
`docs/research/named-retail/acclient_2013_pseudo_c.txt:311397`
|
||
(`CEnvCell::find_visible_child_cell` at address `0x0052dc50`,
|
||
called from `acclient_2013_pseudo_c.txt:280028`).
|
||
|
||
**Files:**
|
||
- `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (per-entity walk —
|
||
the dispatcher already filters by `entity.ParentCellId ∈
|
||
visibleCellIds` but outdoor stabs have `ParentCellId == null` so they
|
||
always pass; needs an explicit indoor-camera gate).
|
||
- `src/AcDream.App/Rendering/TerrainModernRenderer.cs` (currently
|
||
renders all loaded landblock terrain unconditionally; needs
|
||
visibility gating when camera resolves to an indoor cell).
|
||
- `src/AcDream.App/Rendering/CellVisibility.cs:222+` (`ComputeVisibility`
|
||
returns `VisibleCellIds`; existing portal-LOS infrastructure to build on).
|
||
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs`
|
||
(`RenderInsideOut` pipeline — reference implementation, never invoked).
|
||
|
||
**Acceptance:** Standing inside a sealed-interior cell, no outdoor
|
||
geometry is visible through floor/walls. Standing where a cell has a
|
||
real outdoor portal (door open, window) outdoor geometry is correctly
|
||
visible through the portal. Cellar-stairs case (2026-05-25 finding):
|
||
standing in a Holtburg cottage cellar at any camera angle, no outdoor
|
||
terrain mesh visible over the stair geometry.
|
||
|
||
**Research:**
|
||
[`docs/research/2026-05-25-issue-100-shipped-and-culling-handoff.md`](research/2026-05-25-issue-100-shipped-and-culling-handoff.md)
|
||
— full session handoff with cellar-stairs evidence, family map (#78 +
|
||
#95 + cellar-stairs), root-cause hypothesis, retail anchors, WB
|
||
references, do-not-retry list, and pickup prompt for the
|
||
investigation session.
|
||
|
||
**2026-05-31 update (post-U.4c-flap-fix):** the U.4c flap fix (`0ee328a`, root
|
||
indoor visibility at the player's cell) made this MORE visible — terrain now
|
||
draws inside again (it was Skipped during the flap), so the "floor shows outdoor
|
||
ground / cellar floor transparent / see the world from below" symptom is now
|
||
prominent. Confirmed at visual gate. Fix direction unchanged: gate outdoor
|
||
terrain by indoor-cell visibility (port retail `CEnvCell::find_visible_child_cell`
|
||
`acclient_2013_pseudo_c.txt:311397` + `seen_outside` landscape-keep). See
|
||
[`docs/research/2026-05-31-u4c-flap-fixed-and-residuals-handoff.md`](research/2026-05-31-u4c-flap-fixed-and-residuals-handoff.md)
|
||
(residual 1).
|
||
|
||
**2026-05-31 (PM) — promoted to the RENDER ARCHITECTURE RESET target.** A week of
|
||
point-fixing produced no shippable indoor render. #78 is now understood as the visible
|
||
symptom of an architectural gap, NOT a standalone bug: acdream enforces visibility via
|
||
THREE inconsistent gates (terrain `TerrainClipMode` / shell per-cell clip / entity
|
||
`ParentCellId` filter with a `ParentCellId==null` outdoor-stab bypass) instead of retail's
|
||
ONE PView gate. Direct evidence (`[shell]` probe, `ACDREAM_PROBE_SHELL`) RULED OUT every
|
||
other subsystem: the interior cell shells render fine (geometry/texture/opaque/depth
|
||
correct); the residual is purely that outdoor geometry isn't gated to portal openings
|
||
when indoors. The fix is the unified PView gate (one traversal → one gate for ALL
|
||
geometry), which closes #78 + transparent walls + grey enclosure together. **Canonical
|
||
(read first):**
|
||
[`docs/research/2026-05-31-render-architecture-reset-handoff.md`](research/2026-05-31-render-architecture-reset-handoff.md)
|
||
+ the "Render Pipeline" section of `docs/architecture/acdream-architecture.md`.
|
||
|
||
---
|
||
|
||
## #79 — Indoor lighting: spurious spot lights on walls
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM
|
||
**Filed:** 2026-05-19
|
||
**Component:** lighting
|
||
|
||
**Description:** Walking around inside Holtburg Inn, the user sometimes
|
||
sees spot-light-like patches on the interior walls that don't correspond
|
||
to retail's lighting.
|
||
|
||
**Root cause / status:** Point lights from cell static objects (torch
|
||
entities) are being registered via `LightInfoLoader.Load` + `LightingHookSink`
|
||
(Phase 1 verified). Their per-light parameters (position, range, intensity,
|
||
cone) may be wrong — wrong falloff treatment, wrong world-space transform,
|
||
or wrong direction for spot lights. Spec at
|
||
`docs/research/deepdives/r13-dynamic-lighting.md` documents the retail
|
||
LightInfo→LightSource mapping but the live behavior hasn't been verified
|
||
against retail.
|
||
|
||
**Files:**
|
||
- `src/AcDream.Core/Lighting/LightInfoLoader.cs`
|
||
- `src/AcDream.App/Rendering/Shaders/mesh_modern.frag` — `accumulateLights`
|
||
spot-cone logic.
|
||
|
||
**Acceptance:** Side-by-side comparison with retail at the inn shows
|
||
matching torch-light pools.
|
||
|
||
---
|
||
|
||
## #80 — Camera on 2nd floor goes very dark
|
||
|
||
**Status:** OPEN — **M1.5 scope (A7 lighting fidelity)**
|
||
**Severity:** MEDIUM
|
||
**Filed:** 2026-05-19
|
||
**Component:** lighting
|
||
|
||
**Description:** Walking up to the second floor of a building, the
|
||
lighting suddenly goes much darker than retail.
|
||
|
||
**Root cause / status:** Possible causes:
|
||
1. The `playerInsideCell` lighting trigger (Phase 1 / commit `1024ba3`)
|
||
uses `CellVisibility.IsInsideAnyCell(playerPos)` which is a brute-force
|
||
PointInCell scan. The 2nd floor cell may not be in the loaded set OR
|
||
may have wrong bounds.
|
||
2. The per-cell ambient is currently a flat `(0.20, 0.20, 0.20)` for
|
||
any indoor cell. Retail has per-cell ambient overrides; ours doesn't
|
||
read them. A 2nd-floor cell with stairwell shadowing may need a
|
||
different value.
|
||
|
||
**Files:**
|
||
- `src/AcDream.App/Rendering/GameWindow.cs:8330+` (`UpdateSunFromSky`,
|
||
indoor branch).
|
||
|
||
**Acceptance:** 2nd-floor cells render with similar brightness to
|
||
ground floor; transition is not abrupt.
|
||
|
||
---
|
||
|
||
## #81 — Static building stabs don't react to atmospheric lighting changes
|
||
|
||
**Status:** OPEN — **M1.5 scope (A7 lighting fidelity)**
|
||
**Severity:** MEDIUM
|
||
**Filed:** 2026-05-19
|
||
**Component:** lighting, rendering
|
||
|
||
**Description:** Outside, time-of-day changes (sunrise/sunset/lightning)
|
||
don't visibly affect static building stabs (the inn / cottages). The
|
||
buildings stay statically lit while terrain and scenery shift colors.
|
||
|
||
**Root cause / status:** Stabs are rendered through `WbDrawDispatcher`
|
||
with `mesh_modern.frag` which DOES consume the `SceneLightingUbo`
|
||
(sun + ambient + fog). Verify the shader is being used for stabs and
|
||
that the UBO is bound at the right binding slot per draw call.
|
||
Possibly a shader-path divergence — terrain uses `terrain_modern.frag`,
|
||
entities use `mesh_modern.frag`, but stabs/scenery may be on a
|
||
different path.
|
||
|
||
**Files:**
|
||
- `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`
|
||
- `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs`
|
||
|
||
**Acceptance:** Stabs darken/brighten in sync with terrain + scenery
|
||
across the day/night cycle.
|
||
|
||
---
|
||
|
||
## #82 — Some slope terrain lit incorrectly
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (cosmetic)
|
||
**Filed:** 2026-05-19
|
||
**Component:** rendering, terrain
|
||
|
||
**Description:** Specific terrain slopes appear lit "wrong" compared to
|
||
retail.
|
||
|
||
**Root cause / status:** Likely terrain normal calculation or the
|
||
landblock-edge normal-blending divergence between WB and retail (per
|
||
`feedback_wb_migration_formulas.md` — WB's terrain split formula
|
||
differs from retail's `FSplitNESW`).
|
||
|
||
**Files:**
|
||
- `src/AcDream.App/Rendering/TerrainModernRenderer.cs`
|
||
- `src/AcDream.App/Rendering/Shaders/terrain_modern.frag`
|
||
|
||
**Acceptance:** Side-by-side comparison with retail at the same Holtburg
|
||
slopes shows matching shading.
|
||
|
||
---
|
||
|
||
## #83 — Indoor multi-Z walking broken (cellars, 2nd floors, intermittent falling-stuck)
|
||
|
||
**Status:** OPEN — **M1.5 scope (A6 physics fidelity, primary umbrella issue)**. Foundation work landed 2026-05-19; root-cause fix scoped to A6.P1-P3 cdb-driven investigation.
|
||
**Severity:** HIGH (blocks vertical indoor traversal + degrades single-floor cases). M1.5 acceptance depends on this closing.
|
||
**Filed:** 2026-05-19
|
||
**Component:** physics, movement, resolver
|
||
|
||
**Description:** Walking UP stairs in single-floor houses works
|
||
(grounded step-up routes through retail-faithful `BSPQuery.FindWalkableInternal`
|
||
via `StepSphereDown`). Walking DOWN into cellars fails ("ground blocking" —
|
||
can't descend). Walking on 2nd floors works partially but intermittently
|
||
gets stuck in the falling animation. "Phantom collisions" / invisible
|
||
obstacles in rooms persist. The original title "Walking up stairs broken"
|
||
was misleading per user's clarification 2026-05-19.
|
||
|
||
**Partial fix landed 2026-05-19 (6 commits `ff548b9` → `f845b22`).**
|
||
Foundation work: extended `BSPQuery.FindWalkableInternal` to expose the
|
||
hit polygon's dictionary key id; added thin public wrapper
|
||
`BSPQuery.FindWalkableSphere` over the existing retail-faithful BSP
|
||
walkable-finder (acclient_2013_pseudo_c.txt:326211 / :326793); refactored
|
||
`Transition.TryFindIndoorWalkablePlane` to route through that wrapper
|
||
instead of its Phase-2 linear first-match XY scan; added `[indoor-walkable]`
|
||
runtime-toggleable probe line for diagnostic visibility. 5 new unit tests
|
||
+ 1 integration test, 9 pre-existing IndoorWalkablePlane tests updated
|
||
to the new signature.
|
||
|
||
**Foundation work did NOT fix the user-reported bugs.** Visual verification
|
||
2026-05-19: cellar descent FAIL, 2nd-floor walking FAIL (intermittent
|
||
falling-stuck), single-floor cottage REGRESSED to intermittent falling-stuck
|
||
(was stable before), phantom collisions PERSIST. The probe captured 1443
|
||
MISS / 2 HIT over 1445 indoor-walkable calls — the BSP walker correctly
|
||
rejects the foot-sphere-tangent-to-floor case (sphere center is exactly
|
||
at `floorZ + radius` when grounded, so `PolygonHitsSpherePrecise` fails
|
||
the `|dist| > radius - epsilon` check by ~0.0002).
|
||
|
||
**Root cause (deeper than originally diagnosed):** `Transition.TryFindIndoorWalkablePlane`
|
||
fundamentally exists as a Phase 2 commit `eb0f772` stop-gap to synthesize
|
||
a ContactPlane every frame when the indoor BSP returns OK. Retail doesn't
|
||
do this — retail RETAINS the previous frame's `ContactPlane` when the
|
||
collision dispatcher says "no collision." There is no retail analog of
|
||
`find_walkable` being called as a standing-still query — retail's
|
||
`find_walkable` only runs inside a downward sphere sweep
|
||
(`step_sphere_down`), where the sphere is moving and the overlap test
|
||
is meaningful. In our `TryFindIndoorWalkablePlane` flow, the sphere is
|
||
tangent (grounded), not moving — the algorithm correctly returns "no
|
||
overlap." The single-floor cottage worked previously because the OLD
|
||
linear scan ignored Z and falsely returned HIT for any XY-overlapping
|
||
walkable; the new BSP-walker correctly identifies "no overlap" and
|
||
falls through to the outdoor terrain backstop, which only happens to
|
||
produce sensible Z for single-floor outdoor-adjacent cases.
|
||
|
||
**Files in the foundation work:**
|
||
- `src/AcDream.Core/Physics/BSPQuery.cs` — `FindWalkableInternal` signature extension, new `FindWalkableSphere` public wrapper
|
||
- `src/AcDream.Core/Physics/TransitionTypes.cs` — `TryFindIndoorWalkablePlane` refactor, `PointInPolygonXY` deletion, `[indoor-walkable]` probe
|
||
- `tests/AcDream.Core.Tests/Physics/BSPQueryTests.cs` — 4 new unit tests
|
||
- `tests/AcDream.Core.Tests/Physics/TransitionTypesTests.cs` — new integration test
|
||
- `tests/AcDream.Core.Tests/Physics/IndoorWalkablePlaneTests.cs` — 9 tests updated to new signature
|
||
|
||
**Next investigation phase (deferred):** Port retail's `ContactPlane` retention
|
||
mechanism so the resolver retains the previous frame's contact plane when
|
||
the BSP says "no collision," instead of re-synthesizing it per frame. The
|
||
proper fix likely eliminates `TryFindIndoorWalkablePlane` entirely. Needs
|
||
deep investigation of retail's `CTransition::transitional_insert` /
|
||
`CPhysicsObj::transition` / `LastKnownContactPlane` interactions. Foundation
|
||
work (BSP walker + probe + tests) remains useful regardless of approach.
|
||
|
||
**Acceptance:** Walk down stairs into a cellar without getting stuck.
|
||
Walk on a 2nd floor without intermittent falling-stuck. Single-floor
|
||
cottage walking remains stable (no regression).
|
||
|
||
**Handoff:** [`docs/research/2026-05-19-indoor-walkable-plane-bsp-port-shipped-handoff.md`](research/2026-05-19-indoor-walkable-plane-bsp-port-shipped-handoff.md).
|
||
|
||
---
|
||
|
||
## #84 — [DONE 2026-05-19] Blocked by air indoors
|
||
|
||
**Status:** DONE
|
||
**Closed:** 2026-05-19
|
||
**Severity:** HIGH (blocks indoor navigation)
|
||
**Filed:** 2026-05-19
|
||
**Component:** physics, collision
|
||
|
||
**Description:** While walking inside buildings, the player sometimes
|
||
collides with invisible obstacles in mid-floor where there's nothing
|
||
visible.
|
||
|
||
**Root cause / status:** Cell BSP geometry doesn't align with the
|
||
visible cell mesh. Possibilities:
|
||
1. The `cellTransform` applied to physics in
|
||
`_physicsDataCache.CacheCellStruct(envCellId, cellStruct, cellTransform)`
|
||
at `GameWindow.cs:5384` includes the `+0.02f` Z bump, but the BSP
|
||
geometry may not be lifted with it — physics geometry sits 2cm BELOW
|
||
render geometry, so invisible "ceilings" at floor-level cause
|
||
blockage.
|
||
2. CellStruct BSP contains polygons that the cell mesh doesn't include
|
||
(or vice versa) — the two are derived from different fields.
|
||
|
||
**Files:**
|
||
- `src/AcDream.App/Rendering/GameWindow.cs:5362-5384` (cellOrigin Z bump
|
||
+ physics cache call).
|
||
|
||
**Acceptance:** Walking through interior cell space hits collisions
|
||
only where visible walls/furniture exist.
|
||
|
||
**Resolution (2026-05-19 partial · `c19d6fb`):** Phase D of Cluster A
|
||
extended `ResolveOutdoorCellId` in `PhysicsEngine.cs` with an indoor
|
||
cell-containment scan: when the player's world position falls inside any
|
||
cached EnvCell's AABB, `CellId` is promoted to that indoor cell, which
|
||
enables the `FindEnvCollisions` indoor-BSP branch. This resolved the
|
||
"spawn in building and be stuck above the floor" variant of #84 —
|
||
player's CellId now promotes to the interior cell on spawn-in, the floor
|
||
is walkable, and the player can move freely. The "invisible air obstacle"
|
||
symptom for rooms the player walks INTO from outside was tracked under #87
|
||
and required portal-based cell tracking.
|
||
|
||
**Resolution (2026-05-19 full · `1969c55, aad6976, 069534a, 702b30a, 3ffe1e4, eb0f772`):**
|
||
Indoor walking Phase 2 replaced AABB containment with portal-graph cell traversal
|
||
(`CellTransit.FindCellList` + `CheckBuildingTransit`). CellId now promotes to indoor
|
||
cells via portals and remains promoted during normal walking through doorways. Indoor
|
||
cell-BSP collision fires consistently. Indoor walkable plane synthesized from floor
|
||
poly (`TryFindIndoorWalkablePlane`) so the resolver tracks walkability correctly when
|
||
the player is standing on an indoor floor. User visually verified at Holtburg cottage:
|
||
walls block from inside, multi-room navigation works, walking outdoors through a door
|
||
works. Issue fully closed.
|
||
|
||
---
|
||
|
||
## #85 — [DONE 2026-05-19 · 1969c55, aad6976, 069534a, 702b30a, 3ffe1e4, eb0f772] Pass through walls from outside→in
|
||
|
||
**Status:** DONE
|
||
**Closed:** 2026-05-19
|
||
**Commits:** `1969c55, aad6976, 069534a, 702b30a, 3ffe1e4, eb0f772`
|
||
**Filed:** 2026-05-19
|
||
**Component:** physics, collision
|
||
|
||
**Resolution (2026-05-19 · Indoor walking Phase 2):** The root cause (CellId never promoted
|
||
to the indoor cell during outdoor→indoor walking) was resolved by portal-graph cell
|
||
traversal in `CellTransit.CheckBuildingTransit`. Once `CellId` promotes to the indoor
|
||
cell, the indoor-BSP collision branch in `FindEnvCollisions` fires for approaches from
|
||
both inside and outside. User visually verified walls block from outside (player must
|
||
use the door portal to enter). See #87 and handoff:
|
||
[`docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md`](2026-05-19-indoor-walking-phase2-shipped-handoff.md).
|
||
|
||
**Original description:** Approaching a building from the outside, the player
|
||
can walk THROUGH walls into the interior — one-directional wall
|
||
collision. From the inside trying to exit, the wall does block.
|
||
|
||
The root cause was pinned (Cluster A 2026-05-19) as the same failure as
|
||
#84's remaining symptom — `CellId` wasn't promoted to the indoor cell
|
||
during normal outdoor→indoor walking because AABB containment was too
|
||
tight for threshold/doorway cells. Without CellId in the indoor cell,
|
||
the indoor-BSP collision branch in `FindEnvCollisions` never fired
|
||
regardless of approach direction.
|
||
|
||
---
|
||
|
||
## #87 — [DONE 2026-05-19 · 1969c55, aad6976, 069534a, 702b30a, 3ffe1e4, eb0f772] Indoor cell tracking uses AABB containment instead of portal traversal
|
||
|
||
**Status:** DONE
|
||
**Closed:** 2026-05-19
|
||
**Commits:** `1969c55, aad6976, 069534a, 702b30a, 3ffe1e4, eb0f772`
|
||
**Filed:** 2026-05-19
|
||
**Component:** physics
|
||
|
||
**Resolution (2026-05-19 · Indoor walking Phase 2):** Portal-graph cell traversal
|
||
(`CellTransit.FindCellList` + `CheckBuildingTransit`) replaced the AABB containment
|
||
shortcut. Player CellId now correctly promotes to indoor cells via portals;
|
||
indoor cell-BSP collision branch fires consistently; walls block from inside.
|
||
Outdoor→indoor entry via `BuildingPhysics` + `BldPortalInfo` (`CheckBuildingTransit`)
|
||
wires the building-shell portal graph. Indoor walkable plane synthesized from the
|
||
cell's floor poly so the resolver tracks walkability during indoor movement (`TryFindIndoorWalkablePlane`).
|
||
See handoff: [`docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md`](2026-05-19-indoor-walking-phase2-shipped-handoff.md).
|
||
|
||
**Original description:** `PhysicsDataCache.TryFindContainingCell` promotes the
|
||
player's `CellId` to an indoor EnvCell when their world position falls
|
||
inside any cached cell's local AABB. This is too tight to keep `CellId`
|
||
promoted to an indoor cell during normal walking. Threshold/doorway cells
|
||
(the polys that sit at a room boundary) have AABB Z ranges of only ~0.2 m;
|
||
a standing player at local Z=0.46 m is OUTSIDE the AABB and containment
|
||
fails. Because `CellId` drifts back to the outdoor cell, the indoor-BSP
|
||
collision branch in `TransitionTypes.FindEnvCollisions` is gated out for
|
||
most movement, so walls don't block from inside the house and the floor
|
||
physics is unreliable. The retail fix is portal-based cell traversal —
|
||
when the player crosses a cell portal boundary, the cell ownership
|
||
propagates through portal connectivity data in `CEnvCell`.
|
||
|
||
---
|
||
|
||
## #88 — Indoor static objects vibrate (bookshelves, open furnaces)
|
||
|
||
**Status:** OPEN — **M1.5 scope (A6 physics — suspected sub-step state corruption family)**
|
||
**Severity:** MEDIUM (visual jitter; doesn't block gameplay)
|
||
**Filed:** 2026-05-19
|
||
**Component:** rendering, animation
|
||
|
||
**Description:** Static objects inside cells (bookshelves, open furnaces, possibly other interior props) show per-frame transform jitter / vibration. Pre-existing (user noticed before Phase 2 shipped). Likely candidates:
|
||
|
||
1. `EntityScriptActivator.OnCreate/OnRemove` firing repeatedly as the player's CellId promotes/demotes near cell boundaries (less likely after Phase 2's portal-based tracking — but worth investigating).
|
||
2. Per-part transforms for cell-static `WorldEntity` instances getting recomputed each frame with floating-point drift.
|
||
3. Particle-emitter offsets accumulating instead of resetting.
|
||
|
||
**Files to investigate:**
|
||
- `src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs` — OnCreate/OnRemove call patterns
|
||
- `src/AcDream.App/Rendering/GpuWorldState.cs` — entity transform updates per frame
|
||
- `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` — per-batch transform composition
|
||
|
||
**Acceptance:** Indoor static objects render stable (no per-frame jitter).
|
||
|
||
---
|
||
|
||
## #89 — Port BSPQuery.SphereIntersectsCellBsp for retail-faithful CheckBuildingTransit
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (Phase 2 ships with a documented approximation)
|
||
**Filed:** 2026-05-19
|
||
**Component:** physics
|
||
|
||
**Description:** Retail's `CEnvCell::check_building_transit` uses `CCellStruct::sphere_intersects_cell` — a radius-aware sphere-vs-BSP test that returns Inside/Crossing/Outside. Phase 2's `CellTransit.CheckBuildingTransit` uses `BSPQuery.PointInsideCellBsp` (radius-less, tests only the sphere CENTER). Practical effect: outdoor→indoor entry fires ~sphereRadius (~0.48m) deeper into the doorway than retail. The sphereRadius parameter is plumbed through but currently unused.
|
||
|
||
**Files:**
|
||
- `src/AcDream.Core/Physics/CellTransit.cs::CheckBuildingTransit` (line ~162)
|
||
- `src/AcDream.Core/Physics/BSPQuery.cs::PointInsideCellBsp` (line ~940) — existing point test to model the new sphere variant after
|
||
|
||
**Acceptance:** `CellTransit.CheckBuildingTransit` calls a new `BSPQuery.SphereIntersectsCellBsp(node, sphereCenter, sphereRadius)` that returns `Inside`/`Crossing`/`Outside`. Entry timing matches retail visually at the Holtburg cottage door.
|
||
|
||
---
|
||
|
||
## #90 — Cell-id ping-pong at indoor doorway threshold — [DONE 2026-06-11 · ca4b482, T6/BR-7]
|
||
|
||
**Status:** DONE — the `4ca3596` sphere-overlap stickiness workaround was
|
||
REMOVED in T6/BR-7: it had become dead code (its only caller path was the
|
||
cache-null test fallback in `ResolveCellId`), and the retail mechanism that
|
||
owns doorway hysteresis is the ordered-pick (current cell at CELLARRAY
|
||
index 0, interior-wins-break — `BuildCellSetAndPickContaining`, the
|
||
collide-then-pick advance), which production has used since the membership
|
||
rewrite. The ping-pong's original harm (outdoor ticks bypassing indoor BSP)
|
||
is structurally gone under the per-cell query: both classifications collide
|
||
the same per-cell lists at the threshold.
|
||
**Severity:** HIGH (workaround unblocks indoor visibility for M1.5 baseline; M1.5 acceptance requires the proper fix)
|
||
**Filed:** 2026-05-20
|
||
**Component:** physics — cell tracking
|
||
|
||
**Description:** Walking into the Holtburg inn through its doorway causes the player's CellId to ping-pong between outdoor cell `0xA9B40022` and indoor vestibule cell `0xA9B40164` every few ticks. Indoor BSP DOES detect walls (Collided/Adjusted/Slid all fire on push-back), but the push-back exits the indoor CellBSP's bounding volume → `PhysicsEngine.ResolveCellId` reclassifies the player as outdoor → next tick bypasses indoor BSP entirely → player advances freely → re-enters → repeats. Net aggregate behaviour: walls APPEAR to walk through even though indoor wall hits ARE firing on the indoor frames.
|
||
|
||
**Root cause / status:** Cell-id stickiness missing. When the indoor BSP pushes the foot-sphere back during wall collision, the resulting world position lies just outside the indoor cell's CellBSP volume (the BSP's volume is tightly bounded to the room's interior). The cell resolver then re-evaluates and prefers the outdoor cell. Retail likely has hysteresis or a "keep previous cell unless clearly outside" rule.
|
||
|
||
**Files:**
|
||
- `src/AcDream.Core/Physics/PhysicsEngine.cs:259-329` — `ResolveCellId` outdoor-then-indoor branch logic
|
||
- `src/AcDream.Core/Physics/CellTransit.cs:235-325` — `FindCellList` / `BuildCellSetAndPickContaining` containment test
|
||
- `src/AcDream.Core/Physics/BSPQuery.cs:950-963` — `PointInsideCellBsp` (radius-less)
|
||
- `src/AcDream.Core/Physics/CellTransit.cs::CheckBuildingTransit` (line ~162) — outdoor→indoor entry test
|
||
|
||
**Research:** [`docs/research/2026-05-20-phase-a4-shipped-cell-pingpong-finding.md`](research/2026-05-20-phase-a4-shipped-cell-pingpong-finding.md) — full ping-pong analysis with launch-revert2.log evidence (61 indoor-bsp queries firing, 11 inside=True building-transit events, 18 cell-id flips between `0xA9B40022` ↔ `0xA9B40164`).
|
||
|
||
Retail oracle for cell-id hysteresis: `acclient_2013_pseudo_c.txt:308742-308783` (`CObjCell::find_cell_list` Position-variant). Not yet decompiled in detail. Bug-A cousin (see [`docs/research/2026-05-20-indoor-walking-bug-a-handoff.md`](research/2026-05-20-indoor-walking-bug-a-handoff.md)) — different symptom (free-fall vs walk-through), same family (doorway-edge geometry mismatch).
|
||
|
||
**Acceptance:** Walking into the Holtburg inn, the player's CellId promotes to `0xA9B40164` and STAYS there while the user is spatially inside the inn (not flipping back to outdoor on each wall push-back). Walls visibly block. Indoor BSP results dominate the per-tick collision evaluation while user is inside the inn. A4's `[other-cells]` probe starts firing for indoor cells adjacent to the primary.
|
||
|
||
---
|
||
|
||
## #93 — Indoor lighting broken (M1.5 lighting umbrella)
|
||
|
||
**Status:** OPEN — **M1.5 scope (A7 lighting fidelity, primary lighting issue)**
|
||
**Severity:** HIGH (degrades indoor experience; M1.5 acceptance depends on it closing)
|
||
**Filed:** 2026-05-20
|
||
**Component:** lighting, rendering
|
||
|
||
**Description:** Interior cells (inn, cottages, dungeons — anywhere with `cellLow >= 0x0100`) render with lighting that doesn't match retail. Specific symptoms include #80 (2nd floor goes dark), wrong per-cell ambient, missing cell-internal light sources (torches/lanterns), and outdoor day-cycle bleeding into indoor cells. Umbrella issue covering the family; sub-issues to be filed during A7.L1 probe spike.
|
||
|
||
**Root cause / status:** Suspected family of bugs in (a) per-cell environment-light tag parsing from the dat (we may not parse `cell.envLightInfo` correctly), (b) cell-light association (which lights belong to which cell), (c) indoor visibility culling for lights, (d) the indoor branch of `GameWindow.UpdateSunFromSky` which uses a flat ambient. Investigation deferred to A7.L1.
|
||
|
||
**Files:**
|
||
- `src/AcDream.App/Rendering/GameWindow.cs:8330+` (`UpdateSunFromSky`, indoor branch with flat ambient)
|
||
- `src/AcDream.App/Rendering/Shaders/mesh_modern.frag` (per-pixel light evaluation)
|
||
- `references/WorldBuilder/...` (any WB lighting helpers we inherit)
|
||
- Retail oracle: grep `Render::lighting_*` in `acclient_2013_pseudo_c.txt`
|
||
|
||
**Acceptance:** Holtburg inn interior lighting matches retail at the same character position. Holtburg Sewer dungeon torchlight reads correctly per-room. 2nd-floor cells brightness matches ground floor.
|
||
|
||
---
|
||
|
||
## #94 — Held items project spotlight on walls
|
||
|
||
**Status:** OPEN — **M1.5 scope (A7 lighting fidelity)**
|
||
**Severity:** MEDIUM (visual fidelity; doesn't block gameplay)
|
||
**Filed:** 2026-05-20
|
||
**Component:** lighting, rendering
|
||
|
||
**Description:** Items the player is holding (torches, light-source items) project a spotlight effect onto nearby walls. The spotlight direction is wrong — should be omnidirectional from the item, but appears to project specifically toward wall surfaces.
|
||
|
||
**Root cause / status:** Per-entity light direction transform. `LightingHookSink` owner-tracking applies an entity-rotation transform that's probably wrong for held-light items — likely passing the entity's facing-direction as the spotlight cone direction when retail's behavior is omnidirectional point-light.
|
||
|
||
**Files:**
|
||
- `src/AcDream.App/Rendering/Vfx/LightingHookSink.cs` (suspected — verify during A7.L1)
|
||
- `src/AcDream.App/Rendering/Shaders/mesh_modern.frag` (point-light eval branch)
|
||
|
||
**Acceptance:** Held-item lighting illuminates nearby surfaces uniformly without directional cone artifacts. Matches retail's behavior at the same item in same scene.
|
||
|
||
---
|
||
|
||
## #95 — Dungeon portal-graph visibility blowup (see-through-walls / other dungeons rendered)
|
||
|
||
**Status:** RESOLVED 2026-06-13 — **the 9.1M-instance blowup was a SYMPTOM of Bug A
|
||
(wrong dungeon membership), NOT an unbounded portal flood.** Chain of evidence: (1) a
|
||
headless diagnostic on the real `0x0007` dungeon (`Issue95DungeonFloodDiagnosticTests`,
|
||
`95d9dab`) measured `PortalVisibilityBuilder` visiting only **1–17 cells** per root —
|
||
already tightly bounded and a strict *subset* of the stab_list (`VisibleCells`, which is
|
||
the BIG set: avg 120, max 204 of 205 cells). So porting `grab_visible_cells` stab_list
|
||
bounding would have made it WORSE — **DO NOT do that.** (2) The 9.1M blowup was captured at
|
||
the G.3a gate *before* Bug A's fix (`2ce5e5c`), when the player's membership wrongly
|
||
resolved to `0xA9B3` (Holtburg) → the render rooted at the wrong place. (3) With Bug A +
|
||
login-into-dungeon (`47ae237`) fixed, a live launch into `0x0007` measured
|
||
**instances=~39,000 (down from 9.1M, ~230×), meshMissing=0**, dungeon renders, no ACE
|
||
errors. The flood was never the bug. **Originally** also: explained user-observed
|
||
"dungeons are broken"
|
||
**Severity:** HIGH (blocks all dungeon navigation visually)
|
||
**Filed:** 2026-05-21
|
||
**Component:** rendering, visibility, EnvCell portal traversal
|
||
|
||
**Description:** When +Acdream enters a dungeon via portal (verified at Town Network hub in A6.P1 scen5), the `visibleCells` count per cell explodes from a normal ~4-7 to **135-145**, and cells from **multiple disconnected landblocks** are loaded simultaneously. Observed result: the player can see through walls, sees geometry from other dungeons rendering inside the current dungeon, and rendering is generally garbled. This single bug is responsible for "dungeons are broken" as a whole — every portal-accessed dungeon hits this on entry.
|
||
|
||
**Root cause / status:** Suspected: portal-graph traversal in the EnvCell visibility computation walks outbound portals recursively without proper termination, so a network hub (which has many outbound portals to different dungeons) marks 100+ cells from disconnected dungeons as visible. The visibility computation likely needs to (a) cap traversal depth, (b) terminate at portal boundaries to OTHER landblocks, or (c) only include cells that share line-of-sight through a chain of portals from the camera's current cell.
|
||
|
||
**Evidence (committed):**
|
||
- `docs/research/2026-05-21-a6-captures/scen5_sewer_entry/acdream.log` — full trace of the rendering breakdown after portal teleport.
|
||
- Pre-teleport: `visibleCells=4` per cell (normal outdoor).
|
||
- Post-teleport: `visibleCells=135-145` per cell at landblock 0x0007 + spurious cells from 0x020A and 0x0408 (different worldOrigins, i.e. different dungeons entirely).
|
||
- Cell-transit chain: `0xA9B40003 -> 0x00070143 reason=teleport` is the portal entry; everything after the teleport is corrupted.
|
||
|
||
**Files:**
|
||
- `src/AcDream.App/Streaming/` — cell streaming + visibility logic (suspect: cell-cache visibility computation)
|
||
- WB-extracted visibility: `src/AcDream.App/Rendering/Wb/` (whichever file owns `visibleCells`)
|
||
- Check `EnvCellRenderManager` + `VisibilityManager` in `references/WorldBuilder/` for the WB-original algorithm and where our extraction may have diverged
|
||
|
||
**Research:** scen5 acdream.log is the primary evidence. Compare against WorldBuilder's original portal-traversal termination logic.
|
||
|
||
**Acceptance:** After portal entry to any dungeon, `visibleCells` per cell stays in the normal ~4-15 range, cells from non-adjacent landblocks do NOT appear in the cell-cache, and visually no other-dungeon geometry renders through walls.
|
||
|
||
---
|
||
|
||
## #96 — Per-tick PhysicsEngine.ResolveWithTransition CP seed (retail divergence)
|
||
|
||
**Status:** PARTIALLY ADDRESSED — accepted as documented retail divergence
|
||
**Severity:** LOW (cosmetic — CP-write counter inflates but behavior is correct)
|
||
**Filed:** 2026-05-21
|
||
**Component:** physics, ContactPlane retention
|
||
|
||
**Description:** After A6.P3 slice 1 (commits `5aba071` + `5f7722a` + `39fc037`) stripped the `TryFindIndoorWalkablePlane` synthesis path from `Transition.FindEnvCollisions` indoor branch, scen3 post-fix re-capture showed acdream still writes ContactPlane fields 25,082 times during a flat-floor walk — 24,906 of those (99.3%) come from `PhysicsEngine.ResolveWithTransition` line 622, which seeds `ci.ContactPlane` from `body.ContactPlane` at every transition start when the body is grounded. Retail's equivalent code path fires `set_contact_plane` zero times during the same flat-floor walk (scen3 retail BP7 = 0).
|
||
|
||
**Slice 2 attempt + outcome (2026-05-22, commits `892019b` + `f8d669b`):**
|
||
|
||
- **v1 attempt (`892019b`):** Removed the L622 seed entirely to match retail's `CTransition::init` clear-at-start behavior. Verified per-rebuild that the change deployed. CP-write count dropped 91% (30,420 → 2,690). **But broke BSP step_up at the last step of stairs** — sub-step 1's `AdjustOffset` had no ContactPlane to compute the lift direction, BSP step_up thrashed (12,489 push-back-disp + 2,226 push-back-cell signal). User confirmed: "I can't pass the last step of the stairs."
|
||
- **v2 fix (`f8d669b`):** Reverted the seed removal + added no-op-if-unchanged guard inside `CollisionInfo.SetContactPlane`. The guard early-returns when called with values identical to current state. **The guard doesn't trigger for the L622 seed** because each tick gets a fresh `Transition` (so `ci.ContactPlaneValid=false` on entry → guard fails → write fires). So slice 2 v2 didn't actually reduce CP-write count for the seed case. It does dedupe within-tick redundant writes (e.g. Mechanism B restoring LKCP that equals current ci.CP), which is a small benign improvement.
|
||
|
||
**Root cause / status (updated 2026-05-22):** The L622 seed IS load-bearing for `AdjustOffset` slope projection on sub-step 1, which BSP step_up depends on. Retail uses a different architecture (no seed; first sub-step has no CP and BSP path-6 establishes it). Matching retail would require a deeper refactor — making `AdjustOffset` fall back to `body.ContactPlane` when `ci.ContactPlane` is invalid, OR re-architecting the sub-step loop to not require CP for the first iteration. Both are non-trivial.
|
||
|
||
**Accepting the divergence:** the per-tick seed call is functionally correct — it propagates the player's current contact plane to the transition. The cost is a noisy CP-write counter (cosmetic) but the BEHAVIOR matches retail (player stays grounded on the correct plane, slope-snap works, step_up works). Closing #96 fully is deferred to a future refactor or accepted as is.
|
||
|
||
**Lessons learned:**
|
||
- A counter-based metric (CP-write count) is not always a direct proxy for "behavior matches retail." Retail's set_contact_plane firing rate differs from ours because the call-site structure differs, not because the behavior differs.
|
||
- The slice 1 hypothesis "Finding 1 (dispatcher entry frequency) may close as side-effect of Finding 2 (CP-write)" was confirmed by stairs+cellar working post-slice-1. But the slice 2 follow-up assumption "remaining 99.3% of CP writes are also a problem" was partially wrong — those writes are correct state propagation.
|
||
|
||
**Files:**
|
||
- `src/AcDream.Core/Physics/PhysicsEngine.cs:620-626` (the seed call site, retained with updated comment)
|
||
- `src/AcDream.Core/Physics/TransitionTypes.cs:259-279` (`CollisionInfo.SetContactPlane` no-op guard, retained as small improvement)
|
||
|
||
**If revisited:** investigate `AdjustOffset` fallback to `body.ContactPlane` when `ci.ContactPlane` is invalid — that would let us safely remove the seed. Or investigate retail's exact first-sub-step behavior to see if there's a different missing piece in our BSP step_up that would let it work without a seeded CP.
|
||
|
||
---
|
||
|
||
## #97 — Phantom collisions + occasional fall-through on indoor 2nd floor (post-slice-1 happy-testing) — [DONE 2026-06-11 · T6/BR-7 + T5 gate]
|
||
|
||
**Status:** DONE — closed by T6/BR-7 (the +5 m radial query pad that made
|
||
spheres test objects in cells they never overlapped — the structural
|
||
producer of this phantom class per the WF1 verification — was deleted with
|
||
the per-cell query) and **user-confirmed at the T5 gate** ("5. Check" —
|
||
clean inn 2nd-floor walk, no invisible barriers).
|
||
**Severity:** MEDIUM (intermittent; doesn't block stair-walking which works post-slice-1)
|
||
**Filed:** 2026-05-21
|
||
**Component:** physics, ContactPlane stability
|
||
|
||
**Description:** During user happy-testing post-A6.P3 slice 1 (2026-05-21), walking on the inn 2nd floor in acdream produced:
|
||
- Intermittent "phantom collisions" — hitting invisible barriers in open floor space.
|
||
- One observed "fall-through the floor" — character dropped through the 2nd floor at a specific spot.
|
||
|
||
These are NOT the indoor stair-climb or cellar-descent symptoms (those WORK post-slice-1). They appear during normal flat-floor walking.
|
||
|
||
**Root cause / status:** Hypothesis: caused by issue #96 (L622 per-tick CP seed). The seed writes `ci.ContactPlane` every tick from `body.ContactPlane`, which may carry stale values across cell transitions or after the BSP didn't land a fresh plane. If a transient `ci.ContactPlane` value points to a plane that doesn't match the actual current floor geometry, `ValidateWalkable` (called from the outdoor terrain fallback) or downstream physics may briefly believe the player is below the floor → fall-through; OR may believe a wall is present where there isn't one → phantom collision.
|
||
|
||
Falsifiable: if #96 fix closes #97 as a side-effect, the hypothesis is confirmed. If #97 persists post-#96, deeper investigation needed (possibly cell-resolver stickiness — Finding 3 family).
|
||
|
||
**Reproduction (informal — needs sharpening):**
|
||
- Launch acdream, teleport to inn 2nd floor.
|
||
- Walk back and forth across the floor for ~30 seconds in various patterns.
|
||
- Phantom collisions appear intermittently — exact reproduction location unknown.
|
||
- Fall-through happened at one specific spot; location not recorded.
|
||
|
||
**Files:**
|
||
- `src/AcDream.Core/Physics/PhysicsEngine.cs` (CP seed + body persist)
|
||
- `src/AcDream.Core/Physics/TransitionTypes.cs` (`Transition.FindEnvCollisions` indoor branch + `Transition.ValidateTransition`)
|
||
- `src/AcDream.Core/Physics/BSPQuery.cs` (Path-6 land write site)
|
||
|
||
**Acceptance:** Walking on inn 2nd floor for ≥60 seconds in varied patterns produces zero phantom collisions and zero fall-through events.
|
||
|
||
---
|
||
|
||
## #98 — [DONE 2026-05-24 · `b3ce505`] Cellar ascent stuck at top (NOT BSP step; per-cell-list architectural divergence)
|
||
|
||
**Closed:** 2026-05-24
|
||
**Commit:** `b3ce505 fix(phys): A6.P3 #98 — gate outdoor shadow radial sweep on indoor primary cell`
|
||
|
||
**Resolution:** The proximate fix is the indoor-primary radial-sweep
|
||
gate in `ShadowObjectRegistry.GetNearbyObjects`. Architectural root
|
||
cause: our landblock-wide spatial shadow registry diverges from
|
||
retail's per-cell `shadow_object_list` with portal-aware registration —
|
||
the cottage GfxObj (registered landblock-wide via cellScope=0) was
|
||
returned to sphere queries inside the cellar EnvCell, and its
|
||
downward-facing floor poly at world Z=94 head-bumped the climbing
|
||
sphere from below.
|
||
|
||
After ~10 failed speculative fix attempts across four sessions, the
|
||
fix landed cleanly once the apparatus converged. The "v3 stale ramp
|
||
contact plane" hypothesis was falsified by chronological replay against
|
||
`a6-issue98-resolve-capture-2.jsonl` — the player IS on the ramp at the
|
||
cap event; the contact plane is correctly the ramp's plane; the head
|
||
sphere bumps the cottage GfxObj's floor poly from below (the
|
||
evening-v2 finding was correct all along).
|
||
|
||
Decomp anchors (`docs/research/named-retail/acclient_2013_pseudo_c.txt`):
|
||
- 308742+ : `CObjCell::find_cell_list` — indoor/outdoor branch
|
||
- 308751-308769 : the branch — indoor adds 1 cell; outdoor calls `add_all_outside_cells`
|
||
- 308773-308825 : portal-visible neighbor recursion
|
||
- 308916 : `CObjCell::find_obj_collisions(this, ...)` — strict per-cell iteration
|
||
|
||
**Visual verification 2026-05-24:** user confirmed "Finally I can go up!"
|
||
|
||
**Knowledge artifacts:**
|
||
- Findings doc resolution section: [`docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md`](research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md) (bottom)
|
||
- Memory: `feedback_retail_per_cell_shadow_list.md`, `feedback_apparatus_for_physics_bugs.md`
|
||
- A6.P4 phase planned to do the full retail-faithful per-cell port and obviate the b3ce505 stopgap
|
||
|
||
**Known regression introduced:** doors at doorway thresholds — see #99 below.
|
||
|
||
---
|
||
|
||
## #99 — Run-through doors at building thresholds (regression from b3ce505) — [DONE 2026-06-11 · dbfbf85 + ca4b482, T6/BR-7]
|
||
|
||
**Status:** DONE — closed ARCHITECTURALLY by the A6.P4 per-cell shadow port
|
||
(T6/BR-7): registration computes cell membership via the retail
|
||
sphere-overlap portal flood (`CellTransit.BuildShadowCellSet` =
|
||
`CObjCell::find_cell_list`, Ghidra 0x0052b4e0), the query iterates strictly
|
||
per cell (`FindObjCollisionsInCell` = `find_obj_collisions` 0x0052b750,
|
||
primary + `CheckOtherCells` per retail order), building shells dispatch via
|
||
the per-LandCell building channel (`FindBuildingCollisions` =
|
||
`CSortCell::find_collisions` 0x005340a0), and the b3ce505 indoor gate +
|
||
radial sweep + 5 m pad + isViewer exemption are DELETED. The door is
|
||
covered twice like retail: registered into every cell its spheres overlap,
|
||
and reached from the indoor side via the straddle-admitted outdoor cells in
|
||
the player's own array. Pins: tick-13558 (indoor approach BLOCKS),
|
||
tick-22760 (outdoor block invariant), the flipped door apparatus, and the
|
||
registry membership tests. Residual: the lateral-slide delta at
|
||
near-perpendicular approach is #116 (slide response, pre-existing).
|
||
Visual confirmation rides the T5 comprehensive gate.
|
||
**Severity:** HIGH (M1 demo regression — opening doors was previously a working demo target)
|
||
**Filed:** 2026-05-24
|
||
**Component:** physics, shadow-object collision query
|
||
|
||
**Description:** With the issue #98 fix (commit `b3ce505`), the
|
||
indoor-primary radial-sweep gate causes our engine to miss outdoor-
|
||
registered door entities when a sphere has crossed the threshold and
|
||
the primary cell resolves to the indoor side. Players can walk through
|
||
doors that previously blocked them.
|
||
|
||
User report 2026-05-24: "I can also run through doors."
|
||
|
||
**Root cause / status:** This is the doorway edge case explicitly
|
||
flagged in the b3ce505 commit message. Doors are server-spawned
|
||
entities with their own cylinder collision, registered via
|
||
`UpdatePosition` to whichever cell their position resolves to. Doors
|
||
at building thresholds typically resolve to **outdoor** cells. The
|
||
b3ce505 gate skips the outdoor radial sweep when the sphere's primary
|
||
cell is indoor → outdoor-registered doors are not returned → no
|
||
collision → walk-through.
|
||
|
||
Retail handles this case via the portal-visible recursion in
|
||
`find_cell_list` (lines 308773-308825 of the named-retail decomp): at
|
||
registration time, an object is added to its position's cell PLUS all
|
||
portal-visible neighbor cells. So a door at a doorway portal ends up in
|
||
both the outdoor cell's shadow list AND the indoor cell's list — a
|
||
sphere on either side sees it.
|
||
|
||
**Fix path:** Closes naturally as part of A6.P4 (per-cell shadow
|
||
architecture refactor — see design spec at
|
||
`docs/superpowers/specs/2026-05-24-phase-a6-p4-retail-shadow-architecture.md`).
|
||
A6.P4 ports retail's `find_cell_list` indoor branch + portal recursion
|
||
into `ShadowObjectRegistry.Register`, eliminates the cellScope=0
|
||
landblock-wide approximation, and removes the b3ce505 stopgap.
|
||
|
||
If A6.P4 takes longer than expected, an intermediate "portal-aware
|
||
indoor query" patch (~20 lines: walk indoor cells' `VisibleCellIds`,
|
||
collect portal-reachable outdoor cells, include in `GetNearbyObjects`
|
||
indoor branch) would close #99 without touching registration. Tagged
|
||
as fallback option B in the A6.P4 spec.
|
||
|
||
**Files:**
|
||
- `src/AcDream.Core/Physics/ShadowObjectRegistry.cs` — `GetNearbyObjects` indoor branch
|
||
- `src/AcDream.Core/Physics/TransitionTypes.cs:2180+` — `FindObjCollisions` caller
|
||
|
||
**Acceptance:** Doors at Holtburg cottage/inn doorways block the player
|
||
from both sides (outside walking in, inside walking out). Issue #98's
|
||
cellar-up fix remains intact.
|
||
|
||
**Related:** #98 (sibling — same architectural cause), #97 (phantom
|
||
collisions on 2nd floor — also likely closed by A6.P4), Finding 3
|
||
family (sling-out — also likely).
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## #98-old-context-preserved-for-reference
|
||
|
||
(retained from the OPEN form for historical context — superseded by the
|
||
DONE resolution above. Skip to next active issue if you've read enough.)
|
||
|
||
**Status:** OPEN — **NEW diagnosis after A6.P3 slice 3 (2026-05-22)**
|
||
**Severity:** HIGH (blocks M1.5 demo cellar half — user can descend but cannot return)
|
||
**Filed:** 2026-05-22
|
||
**Component:** physics, BSP step_up / step_down at cellar stair geometry
|
||
|
||
**Diagnosis update 2026-05-22 (post A6.P3 slice 3):** The cell-resolver ping-pong (the original hypothesis when this issue was filed) WAS confirmed and is now FIXED by slice 3 (commits `8898166` v1 + `3e140cf` v2 — point-in stickiness check in `ResolveCellId`). Data confirms: scen4_cottage_cellar_slice3v2 capture shows only 1 cell-transit event (login teleport) vs 20+ pre-fix.
|
||
|
||
BUT the cellar-up symptom PERSISTS even with the cell-resolver fix. The remaining cause is a BSP step physics issue at the cellar stair geometry. User report: "I'm running up the stairs, at the top it looks like I'm running into something. Still running animation but not going up." Player can climb most of the stair flight but gets blocked at the TOP step where the cellar transitions to the cottage main floor.
|
||
|
||
**Evidence from slice3v2 capture:**
|
||
```
|
||
[push-back] site=adjust_sphere in=(*, -0.0752, 0.0077) out=(*, -0.0752, 0.7577)
|
||
delta=(0, 0, 0.7500) n=(0, -0.7190, 0.6950) d=-0.1007
|
||
r=0.4800 winterp=1.0000->0.0000 applied=True
|
||
```
|
||
- Surface normal `(0, -0.719, 0.695)` — sloped 44° (walkable per FloorZ=0.664)
|
||
- Push-back lifts sphere by 0.75m (step_down probe distance) repeatedly
|
||
- `winterp 1.0→0.0` — entire walk interpolation consumed by the lift each tick
|
||
- Player Z stays stuck around 0.0077 (relative to cell) → not progressing
|
||
|
||
**Hypothesis:** the step_down probe at the top of the cellar stair is hitting the sloped TOP step face (or possibly a wall poly), and consuming all walk interp pushing back. No remaining interp to actually walk forward over the top.
|
||
|
||
**Diagnosis sharpened 2026-05-22 (commit `134c9b8`)** — paired retail+acdream cdb capture confirmed cellar ascent ends with retail's BP7 setting ContactPlane to the cottage main floor (flat plane at world Z=94, 18 BP7 hits all the same plane).
|
||
|
||
**Diagnosis CORRECTED 2026-05-22 evening (slice 5 `[place-fail]` probe)** — the morning handoff's "Path 5 vs Path 6 in `BSPQuery.FindCollisions`" diagnosis is **WRONG**. The slice-5 probe-driven evidence shows:
|
||
- Retail's BP4 trace has every find_collisions hit with `collide=0`. Retail enters the same `(state & 1) Contact` branch our acdream does. There is NO outer-dispatcher path-selection divergence.
|
||
- Retail's BP5 fires on the ramp poly 17+ times during the ascent, NOT "30 hits all on flat planes" as the morning claim said. We misread the retail data.
|
||
- The actual blocker is polygon **0x0020** in the cellar cell's BSP (`n=(0,0,-1) d=-0.2` in cell-local, world Z=93.82 — the cellar's ceiling). When step-up's step-down probe lifts the sphere onto a 45° walkable surface, the sphere top extends past the ceiling polygon and `SphereIntersectsSolidInternal` correctly rejects.
|
||
- Retail succeeds because its `check_cell` transitions to cottage main floor cell 0xA9B40146 during the ascent, where the cellar's ceiling polygon is absent. Our `check_cell` stays at cellar 0xA9B40147.
|
||
|
||
Full slice 5 evidence + sharpened next-step pickup at [`docs/research/2026-05-22-a6-p3-slice5-handoff.md`](docs/research/2026-05-22-a6-p3-slice5-handoff.md). Capture data at `docs/research/2026-05-21-a6-captures/scen4_cottage_cellar_place_fail/`.
|
||
|
||
**Diagnosis FINALIZED 2026-05-23 evening** (commit `28c282a`, divergence doc at [`docs/research/2026-05-23-a6-p3-issue98-replay-comparison.md`](docs/research/2026-05-23-a6-p3-issue98-replay-comparison.md)). After 4 sessions of speculative fixes (10+ variants, none worked), apparatus shipped to turn evidence-driven analysis into a 200ms test loop:
|
||
|
||
- Deterministic replay harness: [`tests/AcDream.Core.Tests/Physics/Issue98CellarUpReplayTests.cs`](tests/AcDream.Core.Tests/Physics/Issue98CellarUpReplayTests.cs) loads the three cottage/cellar cell fixtures (captured live via the new `ACDREAM_DUMP_CELLS` probe) and drives the failing-frame sphere through our walkable predicates. 7 tests, all pass, all reproduce the live failure without a client launch.
|
||
- Retail comparison: [`docs/research/2026-05-23-a6-captures/cellar_up_capture_1/retail.decoded.log`](docs/research/2026-05-23-a6-captures/cellar_up_capture_1/retail.decoded.log) — 35K cdb BP hits during the equivalent retail cellar-up.
|
||
|
||
**REAL divergence**: NOT cell-resolver. NOT path-selection. NOT polygon 0x0020 the cellar ceiling.
|
||
- Retail's sphere is at world Z ≈ **94.48** (resting on cottage floor) when `find_walkable` accepts the cottage main floor plane.
|
||
- Our failing-frame sphere is at world Z ≈ **92.01** (2.47m lower) when our walkable query rejects the cottage main floor.
|
||
- Retail's `ContactPlane` writes during cellar-up are ONLY flat horizontal planes (cellar floor Z=90.95 OR cottage floor Z=94.00). Never the ramp.
|
||
- Retail's `find_crossed_edge` fires ONCE in 35K BPs. Acdream uses it heavily.
|
||
|
||
**Fix targets** (priority order, from the comparison doc):
|
||
1. (HIGHEST) Step-up + ramp climb doesn't gain enough Z per tick. Retail climbs gradually across thousands of ticks; ours oscillates at Z≈92. Look at `Transition.AdjustOffset` slope projection + `Transition.DoStepUp` WalkInterp handling.
|
||
2. Cottage-cell candidacy uses wrong sphere reference (pre-step-up vs step-lifted center).
|
||
3. `find_crossed_edge` over-use in our walkable acceptance path.
|
||
4. (LOW) Ramp polygon normal divergence.
|
||
|
||
**Failed fix attempts (informational):**
|
||
- WalkInterp reset before placement_insert (commit `bbd1df4`) — logical retail-faithful improvement but doesn't fix the cellar-up symptom. Keep.
|
||
- Slice 3 v1/v2/v3 cell-resolver stickiness — closed ping-pong but didn't help cellar-up. v3 reverted (`8bd3117`).
|
||
- Slice 5: `[place-fail]` probe + diagnosis correction. Useful infrastructure; not a fix.
|
||
- Slice 6 (2026-05-22 PM): 6 placement-insert bypass variants. None unstuck the player.
|
||
- Slice 7 (2026-05-23 AM): terrain hole cutout, multi-sphere CellTransit, building bldg-check, negative-side polygon support, render-vs-physics origin split. Triaged in commit `35b37df`: kept render-physics split + multi-sphere CellTransit + diagnostic probes; reverted neg-poly + bldg-check (didn't fix #98).
|
||
|
||
**Related:**
|
||
- Inn stairs UP works (different geometry, doesn't trigger this specific failure mode)
|
||
- Cellar descent works (only ascent fails — direction matters)
|
||
- Issue #90 (cell-id ping-pong workaround in `ResolveCellId`) is now superseded by slice 3 v2's stickiness check; can be removed in A6.P4 after broader visual verification
|
||
|
||
**Description:** Walking UP from a Holtburg cottage cellar in acdream gets stuck "just almost at the last step up." Stairs going UP elsewhere (inn 2nd floor) work fine post-A6.P3 slice 1. Cellar DESCENT works. Only the cellar ASCENT from the bottom back to ground level fails — specifically at the last step where the player should transition from the indoor cellar cell to the cottage ground-floor cell.
|
||
|
||
**Evidence:** captured in slice 2 v2 verification at `docs/research/2026-05-21-a6-captures/scen3_inn_2nd_floor_slice2v2/acdream.log`. Cell-transit chain shows the resolver ping-ponging between three adjacent cells:
|
||
|
||
```
|
||
0xA9B4014B → 0xA9B4014A → 0xA9B4013F → 0xA9B4014A → 0xA9B4014B → ...
|
||
(Z stays ~96.4 throughout the ping-pong — vertical position stable but cell classification oscillating)
|
||
```
|
||
|
||
Eventually the player gives up and returns down: `0xA9B4013F → 0xA9B40143 (Z drops to 94.020) → 0xA9B40146 (Z 93.426) → ...`
|
||
|
||
Each cell-transit event has `reason=resolver`, meaning `PhysicsEngine.ResolveCellId` is making the decision. The resolver classifies the position into a different cell each tick → `AdjustOffset` operates against a different cell's geometry each tick → can't accumulate forward motion → stuck.
|
||
|
||
**Root cause / status:** Same family as scen4 sling-out (A6.P2 Finding 3) and issue #90 cell-id ping-pong (which has a workaround). The retail oracle is `CObjCell::find_cell_list` Position-variant at `acclient_2013_pseudo_c.txt:308742-308783`. Retail uses cell-array hysteresis / stickiness to prevent flipping CellId on adjacent-cell boundaries when the sphere is on the boundary.
|
||
|
||
Our `ResolveCellId` + `CheckBuildingTransit` lack this stickiness — every tick they re-classify based on current position, ignoring "we were already in cell X last tick; if the new position is still close to X, stay in X."
|
||
|
||
**Fix sketch (slice 3):**
|
||
1. Port retail's cell-array hysteresis from `CObjCell::find_cell_list`.
|
||
2. Modify `ResolveCellId` to prefer the previous tick's CellId when the sphere is close to (but slightly outside) the previous cell's CellBSP volume.
|
||
3. Modify `CheckBuildingTransit` similarly for building-shell transitions.
|
||
4. May obsolete issue #90's workaround (the same stickiness mechanism would handle the doorway ping-pong too).
|
||
|
||
**Related issues:**
|
||
- Issue #90 — Cell-id ping-pong at indoor doorway threshold (existing workaround; should be removed if Finding 3 fix lands cleanly)
|
||
- Issue #97 — Phantom collisions + fall-through on 2nd floor (may also be the same cell-resolver instability)
|
||
- A6.P2 Finding 3 — Indoor cell-resolver sling-out (scen4)
|
||
|
||
**Files:**
|
||
- `src/AcDream.Core/Physics/PhysicsEngine.cs` (`ResolveCellId`)
|
||
- `src/AcDream.Core/Physics/CellPhysics.cs` (`CheckBuildingTransit`)
|
||
- `src/AcDream.Core/Physics/CellTransit.cs` (cell list iteration; may need stickiness here)
|
||
|
||
**Acceptance:** User can walk up out of a Holtburg cottage cellar without getting stuck at the last step. Cell-transit log shows no ping-pong on the cellar boundary. Issue #90 workaround can be removed (verified by ping-pong staying absent at the inn doorway too).
|
||
|
||
**2026-05-23 evening session update — Shape 1 attempted + reverted:**
|
||
|
||
- New apparatus committed:
|
||
- `8a232a3` — `[step-walk-adjust]` probe inside `Transition.AdjustOffset` (PhysicsDiagnostics.LogStepWalkAdjust + four branch tokens). Reveals which projection branch fires per call.
|
||
- `8daf7e7` — captured findings note at [`docs/research/2026-05-23-a6-stepwalkadjust-findings.md`](docs/research/2026-05-23-a6-stepwalkadjust-findings.md) + log snapshot at `docs/research/2026-05-23-a6-captures/stepwalkadjust/acdream.log`.
|
||
- **Refined diagnosis (corrects the 2026-05-23 evening "fix targets" priority above):** AdjustOffset is CORRECT — 145/146 calls take the `into-plane` branch with consistent +0.045 m mean zGain per call when offset points into the ramp normal. Sphere world Z climbs monotonically 90.95 → 92.80 across the ramp. **The climb caps at world Z ≈ 92.80** (cottage floor at 94.00 still 1.20 m above) because at the ramp top, the proposed check (Z=92.85) gets rejected by step-up's downward step-down probe — no walkable surface exists below the proposed position within stepDownHeight=0.6 m (cottage floor is ABOVE, not below). 101 `stepdown-reject` hits in the capture vs 1 acceptance.
|
||
- **Shape 1 fix attempted (`0cb4c59`, reverted in `402ec10`):** Added `PhysicsGlobals.ContactPlaneFlatThreshold = 0.99f` and gated `BSPQuery.AdjustSphereToPlane`'s two `SetContactPlane` call sites by `worldNormal.Z >= threshold`. The intent: match retail's cdb-observed pattern where CP is ONLY ever set on flat polygons (cellar floor or cottage floor — Normal.Z = 1.0 in all 161 BPE writes). Live test confirmed the fix breaks OnWalkable tracking: 18,916 / 25,671 step-walk lines (74%) ended in `contact=False onWalkable=False cp=n/a walkPoly=False` (the falling state). User report: "can't get up the first step. Jumped, stuck in falling animation." The gate was too aggressive — sloped walkable polygons (stair tops, ramp faces) NEED ContactPlane set for the sphere to register as on a surface.
|
||
- **What we learned about Shape 1:** simply skipping `set_contact_plane` on sloped polygons doesn't match retail behavior. Either retail synthesizes a flat CP from a sloped contact (the `step_sphere_down:321203` `Plane::Plane(&plane, esi, &point)` codepath — `esi` may be a synthesized direction, not the polygon's normal), OR retail's gate is upstream of `set_contact_plane` (the polygon never reaches CP-setting in the first place), OR our `OnWalkable` tracking is over-coupled to `ContactPlaneValid` in a way retail's isn't. The named-decomp research did not converge on a definitive answer.
|
||
|
||
**Session paused 2026-05-23 evening after two days of work.** Apparatus + probe + findings + plan + first failed fix + revert all committed. M1.5 demo's cellar half remains blocked. The honest next-session moves, in order:
|
||
|
||
1. **Build a deterministic trajectory replay harness** (drives the physics engine through N ticks with mocked input + snapshotted starting state, runs in <500ms). The Issue98 replay tests are half of this — they have the cell fixtures. The missing half is the per-tick driver. With a 200ms inner loop instead of 5-minute live-test iteration, evidence-driven fix attempts become tractable.
|
||
2. **OR pivot to another M1.5 issue** with less cross-subsystem coupling. The cellar-up bug lives at the seam of AdjustOffset + ContactPlane + WalkInterp + step-up + walkable tracking + OnWalkable + cell-set membership — fixing one piece breaks another. Less-coupled issues (chronic open #2/#4/#28/#29/#37/#41, or #90 workaround removal) would yield faster forward progress.
|
||
3. **OR a deeper named-decomp research pass** focused specifically on `CEnvCell::find_env_collisions` → `BSPTREE::find_collisions` → indoor CP-setting chain. This path was never fully traced; the first two research passes worked on the outdoor (`CLandCell`) path. The indoor path is where the cellar lives.
|
||
|
||
**Replay tests at [`tests/AcDream.Core.Tests/Physics/Issue98CellarUpReplayTests.cs`](tests/AcDream.Core.Tests/Physics/Issue98CellarUpReplayTests.cs)** document the failing-frame geometry and will be the regression oracle when a real fix lands. They do not currently simulate trajectory.
|
||
|
||
**2026-05-23 PM extension — trajectory replay harness shipped, blocked on a SECOND bug:**
|
||
|
||
Commits `4c9290c` → `5c6bdbe` ship a deterministic N-tick trajectory replay at [`tests/AcDream.Core.Tests/Physics/CellarUpTrajectoryReplayTests.cs`](tests/AcDream.Core.Tests/Physics/CellarUpTrajectoryReplayTests.cs). 200-tick runs complete in <100 ms. 5 tests pass.
|
||
|
||
- **Finding:** the cellar ramp polygon is NOT in `cellStruct.PhysicsPolygons`. It lives in a separate GfxObj (a static building piece, registered as a ShadowEntry on the landblock). `CellDumpSerializer` correctly captures cell polygons; the ramp comes from a different data source entirely. The harness reconstructs the ramp polygon programmatically from the live capture's polydump data via `RegisterStairRampGfxObj`.
|
||
- **Finding:** `CellDumpSerializer.Hydrate` sets `BSP=null` per its xmldoc — so the indoor BSP collision path is skipped for hydrated fixtures. Harness wraps cells with a synthetic one-leaf BSP via `AttachSyntheticBsp` to fire the indoor path.
|
||
- **Finding:** `PhysicsBody` seeding requires BOTH `ContactPlane*` AND `WalkablePolygon*` fields. The engine at `PhysicsEngine.cs:665-673` only calls `SpherePath.SetWalkable(...)` if `body.WalkablePolygonValid && body.WalkableVertices.Length >= 3`. Without this the engine treats the sphere as "grounded but anchorless" — a contradictory state.
|
||
|
||
**NEW BLOCKER (open finding):** Even with the full apparatus (CP + WalkablePolygon seeded body, synthetic BSP, synthetic stair GfxObj registered, stub landblock), the sphere goes airborne at tick 1 with `hit=(0,1,0)` — a +Y wall normal matching no registered geometry. The hit is set by `ValidateTransition` between the `after-insert` and `after-validate` probe sites, but the inner `TransitionalInsert` call sets `ci.CollisionNormal=(0,1,0)` before ValidateTransition runs. 12 different `SetCollisionNormal` call sites in `TransitionTypes.cs` — root cause not yet isolated.
|
||
|
||
6 hypotheses tested via the harness, all failed to isolate root cause: WalkablePolygon seeding, initial Z lift (0 vs 0.05m), stair GfxObj presence, stub landblock terrain, cell BSP null vs synthetic, body=null vs seeded. Per systematic-debugging skill's "3+ failures = question architecture" rule, stop speculation; next session needs a side-by-side comparison harness against live `PlayerMovementController` state.
|
||
|
||
**Pickup document:** [`docs/research/2026-05-23-a6-p3-issue98-harness-handoff.md`](docs/research/2026-05-23-a6-p3-issue98-harness-handoff.md) is the canonical resume artifact — has the chronological commit list, apparatus inventory, exclusion list, and three concrete next-session options ranked by recommendation.
|
||
|
||
---
|
||
|
||
|
||
|
||
**Status:** DONE
|
||
**Severity:** MEDIUM (refactor blocker; doesn't affect main branch which is unchanged)
|
||
**Filed:** 2026-05-16
|
||
|
||
**Resolution (2026-05-16 · `0b25df5`):** Step 2 re-attempted with
|
||
`[step2-diag]` traces at every hypothesized fault point. The traces
|
||
showed all four hypotheses were wrong — `session.hashcode` was identical
|
||
through `_liveSession`, `_liveSessionController.Session`, and the
|
||
captured `liveSession` local in the chat-bus lambda, ruling out
|
||
identity mismatches and closure-capture bugs. Doors verified via
|
||
inbound `OnLiveMotionUpdated` round-trip (cmd=0x000B open, cmd=0x000C
|
||
close). Pickup verified via 4 successful `[B.5] pickup` calls. The
|
||
previous broken run was almost certainly a stale ACE session (no other
|
||
code-level explanation survives the diag trace). One small material
|
||
diff: the chat-bus lambda's `var liveSession = _liveSession;` capture
|
||
became `var liveSession = session;` (the non-null parameter) so the
|
||
compiler can statically prove non-null inside the lambda — both pointed
|
||
to the same `WorldSession` instance, only the static analysis changed.
|
||
|
||
Traces stripped before commit. Walking-range auto-walk bug observed
|
||
during the second verification run is pre-existing (filed as #77, not
|
||
caused by this refactor).
|
||
|
||
**Description:** A first attempt at Step 2 — extracting `LiveSessionController`
|
||
|
||
**Description:** A first attempt at Step 2 — extracting `LiveSessionController`
|
||
out of `GameWindow.cs` — was implemented and reverted in the same session
|
||
on the `claude/hungry-tharp-b4a27b` worktree. Visual verification at
|
||
Holtburg revealed:
|
||
|
||
- Chat input field accepts text + Enter but nothing is sent (no echo, no
|
||
ACE response).
|
||
- Double-click on doors / NPCs fires `[B.4b] use guid=... seq=N` outbound
|
||
(verified in `launch.log`) but no visible client-side effect (door doesn't
|
||
swing, NPC doesn't dialogue).
|
||
- R + click-target produces `[B.4b] use-deferred guid=... seq=N`, the
|
||
player auto-walks to the target, but the deferred Use does NOT fire on
|
||
arrival (regresses the Phase B.6 / issue #63 / #75 work).
|
||
|
||
The Step 1 (`eda936d` RuntimeOptions) and Rule 5 follow-up
|
||
(`32423c2` DumpSteepRoof → PhysicsDiagnostics) commits are NOT affected
|
||
and stay clean.
|
||
|
||
**Root cause / status:** Unknown. The refactor preserved every event
|
||
subscription line-for-line (verified by `git diff` — only one `_liveSession.X +=`
|
||
line moved, all others present). The new shape:
|
||
|
||
```
|
||
TryStartLiveSession()
|
||
→ _liveSessionController.CreateAndWire(_options, WireLiveSessionEvents)
|
||
→ new WorldSession(endpoint)
|
||
→ wireEvents(session) // i.e. WireLiveSessionEvents(session)
|
||
→ Chat.OnSystemMessage("connecting...")
|
||
→ _liveSession.Connect(user, pass)
|
||
→ ...character validation + EnterWorld + post-setup...
|
||
```
|
||
|
||
Looks identical to the original control flow. Hypotheses to test on a
|
||
clean re-attempt:
|
||
|
||
1. **Timing of `_liveSession` field assignment.** The new code assigns
|
||
`_liveSession` inside `WireLiveSessionEvents` before subscriptions
|
||
run, and again after CreateAndWire returns. The original code set
|
||
`_liveSession` once at the inline `new WorldSession(...)` site. A
|
||
subtle ordering bug between subscriptions and `_liveSession`'s
|
||
externally visible state may matter.
|
||
2. **LiveCommandBus closure capture.** The `var liveSession = _liveSession;`
|
||
capture inside the chat handler block may have been getting a
|
||
different value than before — though the field IS set by the time
|
||
the capture happens (line 1 of `WireLiveSessionEvents`).
|
||
3. **Inbound packet ordering.** ACE may be sending the first
|
||
StateUpdate / spawn stream BEFORE the EnterWorld dance completes in
|
||
the new flow; if subscriptions are wired but `_liveSession` field
|
||
is briefly inconsistent, an early handler call could see a partial
|
||
state. The `_liveSession?.Tick()` route now goes through
|
||
`_liveSessionController?.Tick()`; verify that's not the difference.
|
||
4. **Some non-subscription side effect** in `WireLiveSessionEvents`
|
||
that wasn't carried over correctly — over-indentation suggests a
|
||
diff-friendly intermediate state; full re-indentation may surface
|
||
the bug.
|
||
|
||
**Files (in the reverted state — recover from worktree git reflog or
|
||
re-write):**
|
||
- `src/AcDream.App/Net/LiveSessionController.cs` (new, ~115 LOC)
|
||
- `src/AcDream.App/Rendering/GameWindow.cs` — `TryStartLiveSession` split
|
||
+ new `WireLiveSessionEvents` method
|
||
|
||
**Research:** No memory entry yet. If the re-attempt succeeds, add a
|
||
`feedback_step2_extraction_pitfalls.md` capturing whichever hypothesis
|
||
turned out to be the bug.
|
||
|
||
**Acceptance:** Step 2 lands when the full M1 demo loop (walk Holtburg,
|
||
double-click inn door + door swings, double-click NPC + NPC dialogues,
|
||
F-key pickup on a ground item) works identically to the pre-refactor
|
||
behavior, AND chat input echoes back through the panel.
|
||
|
||
---
|
||
|
||
## #75 — [DONE 2026-05-16 · `f035ea3`] Auto-walk should drive body directly, not synthesize player-input
|
||
|
||
**Status:** DONE
|
||
**Severity:** LOW (functionally correct via grace-period band-aid; architectural cleanup only)
|
||
**Filed:** 2026-05-16
|
||
**Component:** physics / auto-walk
|
||
|
||
**Resolution (2026-05-16 · `f035ea3`):** Refactored `ApplyAutoWalkOverlay` → `DriveServerAutoWalk`. Auto-walk now steps Yaw, sets `_body.set_local_velocity` from runRate, and calls `_motion.DoMotion(WalkForward, speed)` directly — NO `MovementInput` synthesis. `Update` gates the user-input motion + velocity section on `!autoWalkConsumedMotion` to prevent overwrite. The 500ms arrival grace period (band-aid) deleted. The wire-layer `!IsServerAutoWalking` guard at `GameWindow.cs:6419` retained as a semantic statement (user-MoveToState is for user-driven intent only), not as a band-aid for the synthesis leak that no longer exists. Animation cycle plumbed through via `localAnimCmd` / `localAnimSpeed` for both moving-forward and turn-first phases (issue #69 folded in). Walk/run threshold corrected to 1.0m (overrides ACE's wire-supplied 15.0f; matches user-observed retail behaviour + ACE's own physics layer default). `IsPickupableTarget` now checks `BF_STUCK` (`acclient.h:6435`) to correctly block signs/banners that share Misc ItemType with real pickup items.
|
||
|
||
**Description:** `ApplyAutoWalkOverlay` in `PlayerMovementController`
|
||
synthesizes `Forward+Run` `MovementInput` during inbound `MoveToObject`
|
||
so the existing motion-interpreter pipeline drives the body. The
|
||
synthesis leaks: motion-interpreter sets `MotionStateChanged=true`,
|
||
which would fire an outbound `MoveToState` "user is running"
|
||
packet to ACE — interpreted as user-took-manual-control and cancels
|
||
ACE's `MoveToChain`. We mitigate with a guard
|
||
(`!_playerController.IsServerAutoWalking` at `GameWindow.cs:6410`)
|
||
plus a 500 ms post-arrival grace period to cover ACE's poll race.
|
||
|
||
Retail's `MoveToManager::HandleMoveToPosition` (decomp 0x0052xxxx)
|
||
steps the body POSITION directly when server `MoveToObject` arrives —
|
||
NO player-input synthesis, NO motion-interpreter involvement, NO
|
||
outbound MoveToState. Holtburger
|
||
([simulation.rs:178-206](references/holtburger/crates/holtburger-core/src/client/simulation.rs))
|
||
follows the same pattern (sets `ServerControlledProjection`, advances
|
||
the body, returns empty).
|
||
|
||
**Acceptance:** Refactor auto-walk to step `_body.Position` (or
|
||
equivalent) directly from the wire-supplied path data + run rate, NOT
|
||
via synthesized input. Motion state during auto-walk becomes a
|
||
SERVER-DRIVEN state (similar to how remote players' motion is driven
|
||
by inbound MoveToState packets), not a USER-DRIVEN one. The 500 ms
|
||
grace period in `EndServerAutoWalk` becomes unnecessary and can be
|
||
deleted; same for the `IsServerAutoWalking` guard at the wire layer
|
||
(no MoveToState would have been built in the first place).
|
||
|
||
Animation cycle currently driven by motion-interpreter's
|
||
`MotionStateChanged → SetCycle(RunForward)` would need a separate
|
||
path: probably mirror how remote-player animation is driven by
|
||
inbound motion packets (the sequencer accepts a `SetCycle` directly).
|
||
|
||
**Files:** `src/AcDream.App/Input/PlayerMovementController.cs`
|
||
(`ApplyAutoWalkOverlay` returns synthesized input today; refactor to
|
||
step body directly + drive animation via `_animationSequencer.SetCycle`
|
||
directly). `src/AcDream.App/Rendering/GameWindow.cs` (delete the
|
||
`!IsServerAutoWalking` guard once the leak is gone).
|
||
|
||
**Estimated scope:** Medium (~50-100 LOC + careful testing of
|
||
animation cycle continuity). Not blocking M1 — the grace-period
|
||
band-aid produces retail-faithful behaviour empirically.
|
||
|
||
---
|
||
|
||
## #74 — [DONE 2026-05-16 · `de44358`] AP cadence is per-frame-while-moving, more chatty than retail
|
||
|
||
**Status:** DONE
|
||
**Severity:** LOW (works; just sends ~60× the packets retail would during smooth motion)
|
||
**Filed:** 2026-05-16
|
||
**Component:** physics / net cadence
|
||
|
||
**Resolution (2026-05-16 · `de44358`):** With #75 (MoveToState suppression refactor) closing the MoveToChain-cancellation race, the per-frame "send while moving" cadence is no longer load-bearing. Reverted to retail's two-branch `ShouldSendPositionEvent` gate (`acclient_2013_pseudo_c.txt:700233-700285`): cell/plane change during the sub-interval; cell-or-frame change after the 1s heartbeat. Added `_lastSentContactPlane` field + extended `NotePositionSent(Vector3, uint, Plane, float)` + added `ApproxPlaneEqual` helper + `PlayerMovementController.ContactPlane` public accessor. Effective rates now match retail: 0 Hz idle, ~1 Hz smooth motion, per-event on cell/plane changes, 0 Hz airborne.
|
||
|
||
**Description:** The diff-driven AP cadence shipped in Commit B fires
|
||
`HeartbeatDue` on **any** position change each frame while grounded
|
||
on walkable (effective ~60 Hz during smooth movement) and a 1 Hz
|
||
heartbeat when idle. Retail's `ShouldSendPositionEvent`
|
||
(`acclient_2013_pseudo_c.txt:700233`) only sends during the
|
||
sub-interval when cell or contact-plane changes, and only sends the
|
||
1 Hz heartbeat if `(cellId, frame)` changed since `last_sent` —
|
||
truly idle = 0 Hz. So retail during continuous smooth movement is
|
||
effectively 1 Hz (cell crosses + plane changes don't happen every
|
||
frame); we are ~60 Hz.
|
||
|
||
**Root cause / status:** Deliberate ACE-targeted choice. The
|
||
per-frame cadence is load-bearing for ACE's `WithinUseRadius` poll
|
||
to see the player arrive at a target during local speculative
|
||
auto-walk (issue #63's workaround chain). Going to 1 Hz would
|
||
re-introduce the arrival-lag bug for far-range Use/PickUp.
|
||
|
||
**Files:** [PlayerMovementController.cs:1240-1275](src/AcDream.App/Input/PlayerMovementController.cs)
|
||
— the `HeartbeatDue = groundedOnWalkable && (positionChanged || intervalElapsed)`
|
||
gate.
|
||
|
||
**Acceptance:** Either (a) fix issue #63 so we honor ACE's
|
||
`MoveToObject` server-side, removing the need for the per-frame
|
||
cadence, then revert to retail's `cell-or-plane-change || (interval && frame-change)`
|
||
shape (~5 LOC change); or (b) document this as a permanent
|
||
divergence and update commit messages / code comments to match.
|
||
|
||
**Estimated scope:** Small (~5 LOC + commit-message rewrite) once
|
||
#63 is fixed. Currently blocked by #63.
|
||
|
||
---
|
||
|
||
## #73 — Retail-message centralization plan — per-feature string sweeps
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (per-feature work, not infrastructure)
|
||
**Filed:** 2026-05-16
|
||
**Component:** ui / retail messages
|
||
|
||
**Description:** Commit A added `AcDream.Core.Ui.RetailMessages` as
|
||
the home for retail-decomp-sourced UI strings (`CannotBeUsed`,
|
||
`CantBePickedUp`, `CannotPickUpCreatures`). The retail decomp has
|
||
~750 more user-facing strings we'll need over time — combat misses,
|
||
spell fizzles, vendor dialogs, "you do not have enough" etc. Rather
|
||
than bulk-port them once, port per-feature as the feature lands:
|
||
when wiring vendor purchase, sweep vendor strings into
|
||
`RetailMessages.Vendor.*`; when wiring spell-cast feedback, sweep
|
||
`RetailMessages.Spell.*`.
|
||
|
||
**Status:** No infrastructure work pending. Pattern is established;
|
||
new strings get added to `RetailMessages.cs` with retail anchor
|
||
comments at the call site that triggered the need.
|
||
|
||
**Files:** [RetailMessages.cs](src/AcDream.Core/Ui/RetailMessages.cs)
|
||
— class-level doc comment already describes the per-feature sweep
|
||
pattern.
|
||
|
||
**Acceptance:** Each phase / feature that adds new user-facing
|
||
strings sweeps its retail-anchor strings into `RetailMessages` and
|
||
calls them by name rather than literal-in-place. Closing condition:
|
||
"all M1 demo strings are in RetailMessages" or similar per-milestone
|
||
gate, decided when M1 ships.
|
||
|
||
---
|
||
|
||
## #72 — Confirm Humanoid TurnRight/TurnLeft `omega.z` base rate via cdb
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (current ±π/2 fallback matches all corroborating
|
||
evidence; cdb probe would settle the open question for good)
|
||
**Filed:** 2026-05-16
|
||
**Component:** physics / rotation / research
|
||
|
||
**Description:** Commit A's rotation rate uses
|
||
`BaseTurnRateRadPerSec = π/2` based on the documented
|
||
`AnimationSequencer.cs:734-741` claim that the Humanoid motion table
|
||
ships TurnRight/TurnLeft with `HasOmega` cleared (forcing the
|
||
convention fallback). The constant has 3 corroborating sources but
|
||
the actual dat content was never dumped — and the run-multiplier
|
||
`run_turn_factor = 1.5` at retail `0x007c8914` from
|
||
`apply_run_to_command` (decomp 0x00527be0) likewise hasn't been
|
||
verified live.
|
||
|
||
**Acceptance:** Set a cdb breakpoint on `CSequence::set_omega`
|
||
(`acclient_2013_pseudo_c.txt` — find exact symbol address) while
|
||
holding A or D in a retail client. Capture the `omega.z` argument
|
||
value walking, then running. If `±π/2` walking and `±π/2 × 1.5 ≈ 2.356`
|
||
running, close as confirmed. If different, file as a regression and
|
||
fix the constants in
|
||
[RemoteMoveToDriver.cs](src/AcDream.Core/Physics/RemoteMoveToDriver.cs).
|
||
|
||
**Estimated scope:** ~30 min cdb session + 1 commit if confirmed,
|
||
or +small fix if different. Not blocking M1.
|
||
|
||
---
|
||
|
||
## #71 — WorldPicker Stage B — polygon refine for retail-accurate clicks
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM (Stage A now causes real play mis-picks through open doors/windows)
|
||
**Filed:** 2026-05-16
|
||
**Component:** selection / picker
|
||
|
||
**Description:** Retail's mouse picker does two-tier sphere-then-polygon
|
||
selection (`acclient_2013_pseudo_c.txt:0x0054c740`
|
||
`Render::GfxObjUnderSelectionRay`):
|
||
1. Per-part sphere reject via `CGfxObj::drawing_sphere`.
|
||
2. Polygon-accurate refine via `CPolygon::polygon_hits_ray` on every
|
||
visual polygon; closest-t polygon hit wins over any sphere hit.
|
||
|
||
Commit B's Stage A
|
||
([WorldPicker.cs](src/AcDream.Core/Selection/WorldPicker.cs)) does
|
||
screen-space rect hit-test against the projected
|
||
`Setup.SelectionSphere` (matching the indicator rect, deliberately
|
||
broader than the visible mesh polygons). Stage B would tighten clicks
|
||
to the visible mesh — under-pick what looks like empty space inside
|
||
the rect, catch visible mesh that pokes past the sphere boundary
|
||
(creature outstretched arm, sign edge).
|
||
|
||
**New evidence (2026-05-28 / Phase A8 visual gate):** User stood outside
|
||
a Holtburg building, saw a vendor through an open doorway/window, clicked
|
||
the visible vendor, and acdream selected the door instead:
|
||
`[B.4b] pick guid=0x7A9B4015 name=Door`. This is exactly the Stage A
|
||
failure mode: the open door's projected `Setup.SelectionSphere` rect is
|
||
closer than the vendor's rect, even though the visible door polygon is not
|
||
under the cursor. The fix is polygon refinement against visible GfxObj
|
||
triangles plus current animated part transforms; do not special-case doors.
|
||
|
||
**Acceptance:** Pipe per-part GfxObj visual polygons through a
|
||
`PickPolygonProvider` interface (don't duplicate mesh decoding —
|
||
hook the existing `ObjectMeshManager` cached data). Two-tier in
|
||
`WorldPicker.Pick`: sphere reject → polygon scan → polygon hit
|
||
dominates sphere hit. Acceptance test: visible-mesh accuracy on
|
||
Holtburg sign, Royal Guard outstretched bow arm, inn-door wood
|
||
frame edges.
|
||
|
||
**Estimated scope:** Medium (~4-6 hours). Defer until visual
|
||
verification surfaces a Stage A miss in real play. The user
|
||
confirmed 2026-05-28 that the door/vendor case is now observable in real
|
||
play, so this should be scheduled soon after A8 rather than left as polish.
|
||
|
||
---
|
||
|
||
## #70 — Triangle apex/size — final retail-feel UX pass
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (cosmetic — indicator already retail-anchored, this is final-feel polish)
|
||
**Filed:** 2026-05-16
|
||
**Component:** ui / target indicator
|
||
|
||
**Description:** Per 2026-05-16 user feedback during the
|
||
`SelectionSphere` indicator ship, the triangle apex direction
|
||
(flipped to point inward at the target) and sprite size (currently
|
||
8 px legs) are heuristic visual choices. Retail uses an actual DAT
|
||
sprite from `UIRegion::GetChild(0x1000003a/3b/3c)` — the bitmap
|
||
shape and size come from the dat, not constants.
|
||
|
||
**Acceptance:** Extract the retail triangle sprite from the dat
|
||
(probably via `tools/UiLayoutMockup` or a new `DatSpriteProbe`) and
|
||
either (a) blit the exact bitmap, or (b) pick a procedural size +
|
||
shape that matches it pixel-for-pixel at standard zoom.
|
||
|
||
**Files:** [TargetIndicatorPanel.cs](src/AcDream.App/UI/TargetIndicatorPanel.cs)
|
||
— `TriangleSize` constant + the four `AddTriangleFilled` calls.
|
||
|
||
**Estimated scope:** Small (~1-2 hours, mostly dat exploration).
|
||
Not blocking M1.
|
||
|
||
---
|
||
|
||
## #69 — [DONE 2026-05-16 · `f035ea3`] Local player rotation isn't animated (no leg/arm cycle while pivoting)
|
||
|
||
**Status:** DONE
|
||
**Severity:** LOW (visual polish — rotation works, just looks stiff)
|
||
**Filed:** 2026-05-15 (B.6 close-range turn-to-face)
|
||
**Component:** motion / animation cycle
|
||
|
||
**Resolution (2026-05-16 · `f035ea3`):** Fixed as part of the auto-walk architectural refactor (issue #75). `DriveServerAutoWalk` now records the per-frame rotation direction in `_autoWalkTurnDirectionThisFrame` (+1 / -1 / 0); the animation override at the bottom of `Update` reads that flag and sets `localAnimCmd` to `TurnLeft` / `TurnRight` during the turn-first phase. User confirmed 2026-05-16 that the auto-walk turn-first case (click target, body rotates before walking) now plays the leg-shuffle animation. User-driven A/D rotation was always working — the original issue description was specific to the auto-walk turn-first case.
|
||
|
||
**Description:** When the auto-walk overlay rotates the local player
|
||
(close-range Use turn-to-face, or turn-first phase of a far-range walk),
|
||
the body's Yaw rotates smoothly but no leg / arm animation plays —
|
||
the body just statue-pivots. Retail played a `TurnLeft` / `TurnRight`
|
||
motion cycle while rotating, visible to observers as the character
|
||
moving their legs / arms to turn.
|
||
|
||
**Cause:** `ApplyAutoWalkOverlay` synthesises `Forward+Run` input
|
||
during the walking phase (so the motion interpreter emits `RunForward`
|
||
cycle commands), but synthesises nothing during the turn-only phase
|
||
— so the motion interpreter emits no command and the sequencer
|
||
holds whatever cycle was last set (typically Ready / idle).
|
||
|
||
**Approach:** While turning (`!walkAligned`), synthesise
|
||
`TurnLeft = delta > 0` / `TurnRight = delta < 0` so the motion
|
||
interpreter emits the turn command. Care needed: the existing
|
||
`Update` body also steps Yaw on `TurnLeft`/`TurnRight` input — if
|
||
both apply, the body rotates twice as fast. Cleanest: set the input
|
||
flags AND skip the overlay's own Yaw step (let Update's existing
|
||
handling do the rotation).
|
||
|
||
**Acceptance:** A retail observer watching `+Acdream` turn to face
|
||
an NPC sees the turning animation play (leg shuffle / arm swing) for
|
||
the duration of the rotation.
|
||
|
||
**Estimated scope:** Small. ~30 LOC in `ApplyAutoWalkOverlay` plus
|
||
verification that retail's `TurnLeft`/`TurnRight` cycle is in the
|
||
human motion table.
|
||
|
||
---
|
||
|
||
## #68 — Remote players don't stop running animation on auto-walk arrival
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW-MEDIUM (visual only — server-side action completes correctly)
|
||
**Filed:** 2026-05-15 (B.7 visual verification)
|
||
**Component:** motion / remote dead-reckoning / animation cycle
|
||
|
||
**Description:** Observing a retail player from acdream as they approach
|
||
an NPC at a distance: the remote body's run animation keeps cycling
|
||
even after the body has visibly stopped at the NPC. Retail-side the
|
||
character stopped; the action (dialogue) fired; but our client's
|
||
animation never transitioned RunForward → Ready.
|
||
|
||
**Suspected:** `RemoteMoveToDriver` detects arrival via
|
||
`DriveResult.Arrived`, but the consumer site (per-tick loop in
|
||
`GameWindow.TickAnimations` or wherever the remote body's cycle is
|
||
driven) doesn't flip the animation cycle back to Ready on arrival.
|
||
Alternatively the cycle persists because ACE doesn't broadcast a
|
||
follow-up `UpdateMotion(Ready)` — relying on the client to detect
|
||
arrival from the wire's distance threshold instead.
|
||
|
||
**Files (likely):**
|
||
- `src/AcDream.App/Rendering/GameWindow.cs` — wherever per-tick motion
|
||
for remote entities reads `RemoteMoveToDriver`'s state. Need to
|
||
call `SetCycle(NonCombat, Ready)` on arrival.
|
||
|
||
**Acceptance:** Retail player observed running up to an NPC visibly
|
||
stops running animation at arrival distance, transitions to idle.
|
||
|
||
---
|
||
|
||
## #67 — [DONE 2026-05-15 · `301281d`] Door Use action doesn't complete after auto-walk arrival
|
||
|
||
**Status:** DONE — fixed by `301281d` (10 Hz heartbeat during motion).
|
||
With ACE seeing our position in near-real-time, its `CreateMoveToChain`
|
||
converges normally for doors as well as NPCs. Root cause was 1 Hz
|
||
position sync on our side, not anything door-specific. User confirmed
|
||
doors work after the heartbeat bump.
|
||
|
||
---
|
||
|
||
## #66 — Local + remote rotation: player flips back, NPCs don't turn
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW-MEDIUM (visual feedback — interaction works,
|
||
just looks wrong)
|
||
**Filed:** 2026-05-15 (B.7 visual verification)
|
||
**Component:** motion / rotation
|
||
|
||
**Description:** Two related visual rotation bugs surfaced together:
|
||
|
||
1. **Local player flips back.** Observing acdream's `+Acdream` from
|
||
retail: when our auto-walk completes and the body has rotated to
|
||
face the target, the broadcast position has the new rotation —
|
||
then the next frame the player snaps back to whatever the camera
|
||
yaw was. Likely cause: after `EndServerAutoWalk`, the synthesised
|
||
input stops and `Update`'s next pass applies the user's real
|
||
`MouseDeltaX` (which may be 0 but other paths might be
|
||
overriding `Yaw`).
|
||
2. **NPCs don't turn to face the player.** ACE broadcasts
|
||
`MovementType=8 TurnToObject` when an NPC starts a Use response
|
||
that requires facing. Our `OnLiveMotionUpdated` handles
|
||
MovementType=6 (MoveToObject) but not 8. The NPC's body stays
|
||
at whatever heading the spawn / last motion left it.
|
||
|
||
**Acceptance:**
|
||
- After auto-walk arrival, local player's facing toward the target
|
||
is preserved (no flip-back observed from a retail client).
|
||
- NPCs (Tirenia, guards, vendors) rotate to face the player when
|
||
using them.
|
||
|
||
**Files (likely):**
|
||
- `src/AcDream.Core.Net/Messages/UpdateMotion.cs` — extend parser
|
||
for MovementType=8 payload (target guid + final-heading flag).
|
||
- `src/AcDream.App/Rendering/GameWindow.cs` `OnLiveMotionUpdated`
|
||
— route MovementType=8 for the local player to a new
|
||
`BeginServerTurnToObject` controller method; route for remote
|
||
guids into the remote-dead-reckon state (extending
|
||
`RemoteMoveToDriver` or adding a sibling driver).
|
||
- `src/AcDream.App/Input/PlayerMovementController.cs` — add the
|
||
turn driver that holds Yaw against user-input overrides until
|
||
aligned.
|
||
|
||
**Replaces / supersedes:** #65 (local-player turn-to-face on
|
||
close-range Use). This issue covers both directions and is the
|
||
broader retail-faithful rotation handling phase.
|
||
|
||
**Estimated scope:** Medium — ~80–120 LOC + tests.
|
||
|
||
---
|
||
|
||
## #65 — Local player doesn't turn to face target on close-range Use
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (functional — Use still completes — but visually awkward)
|
||
**Filed:** 2026-05-15 (B.6/B.7 visual verification)
|
||
**Component:** physics / movement / inbound MoveTo handling
|
||
|
||
**Description:** When the local player has a target selected and is
|
||
already within ACE's `WithinUseRadius` (close-range branch in
|
||
`CreateMoveToChain` at `Player_Move.cs:66`), ACE skips the auto-walk
|
||
chain and just calls `Rotate(target)` server-side. The Use action
|
||
completes, but the local player's body doesn't visibly turn to face
|
||
the target — the character stays at whatever heading the user was
|
||
looking when they clicked.
|
||
|
||
**User-visible:** Stand behind an NPC, click them, press R. Dialogue
|
||
appears, but the character keeps facing away from the NPC. In retail
|
||
the character would have turned to face the NPC before / during the
|
||
Use.
|
||
|
||
**Root cause:** ACE's close-range path sends a `TurnTo` motion
|
||
(MovementType=8 TurnToObject, decomp `0x005241b3` switch case 8).
|
||
Our `OnLiveMotionUpdated` doesn't currently handle MovementType=8 —
|
||
it falls into the locomotion path and ignores the rotation.
|
||
|
||
**Acceptance:** When the user uses an in-range target while facing
|
||
away, the character rotates to face the target before / as the Use
|
||
action fires. No regression on close-range pickup (item still picks
|
||
up cleanly).
|
||
|
||
**Files (likely):**
|
||
- `src/AcDream.Core.Net/Messages/UpdateMotion.cs` — extend parser for MovementType=8 TurnToObject payload.
|
||
- `src/AcDream.App/Input/PlayerMovementController.cs` — add a `BeginServerTurnToObject(targetWorld, useFinalHeading)` method that rotates Yaw at TurnRateRadPerSec each frame until aligned, then clears the state.
|
||
- `src/AcDream.App/Rendering/GameWindow.cs` `OnLiveMotionUpdated` — when inbound motion is MovementType=8 and the guid is `_playerServerGuid`, install the turn on the controller.
|
||
|
||
**Estimated scope:** Small — ~50 LOC plus tests. Pairs naturally with
|
||
B.6 (already does turn-then-walk for far targets via RemoteMoveToDriver's
|
||
heading correction; this is the close-range cousin).
|
||
|
||
---
|
||
|
||
## #64 — Local-player pickup animation does not render
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (visual feedback only — pickup completes correctly)
|
||
**Filed:** 2026-05-14 (B.5 visual verification)
|
||
**Component:** motion / animation routing for local player
|
||
|
||
**Description:** When `+Acdream` picks up an item (B.5 close-range
|
||
path), retail observers see the character play the pickup animation
|
||
correctly, but the local view shows no pickup animation. The item
|
||
despawns, the inventory updates, but the character's own
|
||
bend-down-and-grab animation is missing.
|
||
|
||
**Root cause / hypothesis:** ACE broadcasts `Motion(MotionCommand.Pickup)`
|
||
via `Player_Inventory.AddPickupChainToMoveToChain` (line 711–713,
|
||
`EnqueueBroadcastMotion(motion)`), which arrives as a normal
|
||
`UpdateMotion (0xF74D)` packet. Retail observers route it through
|
||
their remote-creature animation pipeline and render the pickup. For
|
||
the local player, our `OnLiveMotionUpdated` likely filters self-echoes
|
||
(local player drives its own motion via prediction, not server
|
||
echoes) and drops the pickup motion. The pickup is a one-shot
|
||
animation initiated by the server, so the prediction path has no
|
||
trigger — and the echo path is filtered.
|
||
|
||
**Acceptance:** When `+Acdream` picks up an item, the local view shows
|
||
the same pickup animation retail observers see. Probably resolved by
|
||
either (a) admitting server-initiated one-shot motions through the
|
||
local-player motion filter, or (b) generating the pickup animation
|
||
locally on send (mirroring retail's client behavior).
|
||
|
||
**Files:** `src/AcDream.App/Rendering/GameWindow.cs` `OnLiveMotionUpdated`
|
||
(motion routing); the self-echo filter is somewhere along this path.
|
||
|
||
**Estimated scope:** Small-to-medium. Mostly investigation +
|
||
1–2 commits.
|
||
|
||
---
|
||
|
||
## #63 — [DONE 2026-05-16 · `f035ea3`] Server-initiated auto-walk (MoveToObject) not honored
|
||
|
||
**Status:** DONE
|
||
**Severity:** MEDIUM (blocks out-of-range Use + Pickup; close-range
|
||
works fine)
|
||
**Filed:** 2026-05-14 (B.5 visual verification)
|
||
**Component:** motion / inbound MoveToObject handling
|
||
|
||
**Resolution (2026-05-16):** Closed in two parts:
|
||
1. **B.6 slice 2 (2026-05-14):** inbound MoveToObject parsing + `BeginServerAutoWalk` wiring at `GameWindow.cs:3389` — body auto-walks toward the server-supplied destination.
|
||
2. **B.6 #75 refactor (`f035ea3`, 2026-05-16):** `ApplyAutoWalkOverlay → DriveServerAutoWalk` drives the body directly from path data, no input synthesis. The `MoveToState` leak that previously cancelled ACE's `MoveToChain` callback is gone; the chain runs uninterrupted and `TryUseItem` / `TryPickUp` fires server-side on arrival. No client-side retry needed. Walk/run threshold corrected to 1.0m (matches retail-observed; overrides ACE's wire-default 15m).
|
||
|
||
Visual-verified end-to-end: far-range Use on NPCs / doors / spell components / corpses all complete via ACE's server-side callback. The far-range retry workaround from Task 1's first iteration (`c61d049`'s `_pendingPostArrivalAction` arming) was deleted as part of #75 (`f035ea3`).
|
||
|
||
**Description:** When the player triggers a Use or PutItemInContainer
|
||
on a target outside ACE's `WithinUseRadius` (default 0.6 m), ACE
|
||
runs server-side auto-walk via `CreateMoveToChain` →
|
||
`PhysicsObj.MoveToObject` + `EnqueueBroadcastMotion(Motion(MoveToObject, target))`.
|
||
Our client receives the `UpdateMotion(MoveToObject)` broadcast for
|
||
the player but doesn't honor it: the character either visually
|
||
drifts a bit toward the target and snaps back, or just stands still.
|
||
ACE's MoveToChain then times out, the `success: false` path
|
||
broadcasts `InventoryServerSaveFailed (ActionCancelled)`, and the
|
||
pickup/use never completes.
|
||
|
||
**User-visible symptom:** Double-click a ground item from any
|
||
distance, or F-key it from > 0.6 m: character partially walks toward
|
||
the item, then flips back to original position. No pickup.
|
||
|
||
**Reference:** [holtburger simulation.rs:33–41 + 178–191](references/holtburger/crates/holtburger-core/src/client/simulation.rs)
|
||
already implements client-side `MoveToObject` motion projection +
|
||
auto-walk handling. That's the shape of the fix.
|
||
|
||
**Root cause:** Our `OnLiveMotionUpdated` has no handler for the
|
||
`MoveToObject` motion type; the broadcast is silently dropped.
|
||
|
||
**Acceptance:** Double-click a ground item from 2–5 m away. Character
|
||
auto-walks to within use radius, ACE's MoveToChain confirms success,
|
||
pickup completes (including the existing PickupEvent despawn). Same
|
||
behavior for Use on out-of-range NPCs.
|
||
|
||
**Files:** `src/AcDream.App/Rendering/GameWindow.cs` `OnLiveMotionUpdated`
|
||
(routing); likely a new `MoveToObjectMotion` handler in the motion /
|
||
prediction layer + a server-acked position-update echo so ACE sees the
|
||
player has reached the target.
|
||
|
||
**Estimated scope:** Medium. Probably its own phase (B.6 or similar);
|
||
not a one-commit fix. Compose from holtburger's pattern.
|
||
|
||
---
|
||
|
||
## #62 — [DONE 2026-05-14 · `ec9fd52`] PARTSDIAG null-guard for sequencer-driven entities
|
||
|
||
**Status:** DONE
|
||
**Severity:** LOW (latent crash; not reachable for doors today — see notes)
|
||
**Filed:** 2026-05-13 (code-quality review of B.4c Task 1)
|
||
**Component:** diagnostic / `GameWindow.TickAnimations` PARTSDIAG block
|
||
|
||
**Description:** The PARTSDIAG block at `GameWindow.cs:7657` reads
|
||
`ae.Animation.PartFrames.Count` without a null-guard. B.4c introduced
|
||
`Animation = null!` for sequencer-driven door entities (per the same
|
||
pattern at line 7857). Today this is safe: doors never enter
|
||
`_remoteDeadReckon` (ACE never sends UpdatePosition for them), and
|
||
`_remoteDeadReckon` membership is one of the outer guards on the
|
||
PARTSDIAG block. The diagnostic never fires for doors.
|
||
|
||
**Risk:** Future code that admits more non-creature entities via the
|
||
B.4c branch — or extends ACE to send UpdatePosition for doors — would
|
||
make `_remoteDeadReckon` membership reachable for null-Animation
|
||
entities. The next time someone enables `ACDREAM_REMOTE_VEL_DIAG=1`
|
||
and that scenario occurs, the diagnostic crashes the tick.
|
||
|
||
**Acceptance:** PARTSDIAG block tolerates null `ae.Animation`. One-line
|
||
fix:
|
||
```csharp
|
||
int animFrame0Parts = ae.Animation?.PartFrames.Count > 0
|
||
? ae.Animation.PartFrames[0].Frames.Count
|
||
: -1;
|
||
```
|
||
|
||
**Files:** `src/AcDream.App/Rendering/GameWindow.cs:7657` (one-line null-coalescing change).
|
||
|
||
**Estimated scope:** Trivial. One-line edit + a build verification.
|
||
|
||
---
|
||
|
||
## #61 — [DONE 2026-05-18 · `9f069e1`] AnimationSequencer link→cycle boundary flash on one-shot motion (door swing)
|
||
|
||
**Status:** DONE — fixed by `9f069e1` (also widened scope: same bug
|
||
manifested as the local-player run-stop twitch — user-observed during
|
||
the M2 anim-pass session). Root cause was `BuildBlendedFrame` wrapping
|
||
`nextIdx` to `rangeLo` unconditionally at the high-frame boundary —
|
||
correct for looping cycles (idle/run/walk loops), wrong for one-shot
|
||
links. During the ~30 ms fractional tail of any link, the renderer
|
||
blended `frame[end]` with `frame[0]`, producing the flash through the
|
||
anim's starting pose. Fix: gate the wrap on `curr.IsLooping`. Pinned by
|
||
the new `Advance_LinkTailDoesNotBlendIntoLinkFrame0` regression test.
|
||
Visual-verified by the user end-to-end on 2026-05-18.
|
||
|
||
**Severity:** LOW (visual polish — animation works, brief one-frame flash through prior pose at end of swing)
|
||
**Filed:** 2026-05-13 (visual test of B.4c)
|
||
**Component:** animation / `AcDream.Core.Physics.AnimationSequencer` link+cycle transition
|
||
|
||
**Description:** When a door receives `UpdateMotion(NonCombat, On)` via the
|
||
B.4c spawn-time-registered sequencer, the swing-open animation plays
|
||
correctly but exhibits a brief one-frame flash through the closed pose
|
||
at the END of the swing before settling at the open pose. Same flash on
|
||
close (settles at closed pose after one-frame flash through open).
|
||
|
||
**Root cause hypothesis:** `AnimationSequencer.SetCycle` enqueues a
|
||
transition link (the swing motion) followed by the target cycle (likely
|
||
a single-frame static rest pose). If the link's last frame and the
|
||
cycle's frame 0 don't match exactly, the renderer reads one frame of
|
||
the cycle's start pose before the cycle's natural rest. Cumulative
|
||
effect: link plays Closed→Open over N frames → cycle's frame 0 is
|
||
Closed → cycle resets to frame 0 for one render → cycle advances to
|
||
its single rest frame which IS the open pose. Visible as a flap.
|
||
|
||
**Acceptance:** Door open / close cycles play cleanly with no closed/open
|
||
pose flash at the link→cycle transition. Test: in Holtburg, double-click
|
||
inn door, watch swing animation rest at open pose with no intermediate flash.
|
||
|
||
**Files (likely):**
|
||
- `src/AcDream.Core/Physics/AnimationSequencer.cs` — link+cycle queue boundary handling
|
||
- (read the link node's last-frame extraction + the cycle's frame-0 evaluation)
|
||
|
||
**Estimated scope:** Moderate. Requires understanding the sequencer's link-vs-cycle queue semantics and possibly the underlying MotionTable's cycle data shape for doors. Could be a one-line fix (e.g. "preserve last link frame as cycle rest pose") or a deeper sequencer behavior change.
|
||
|
||
**Workaround:** None needed for M1 — the flash is brief enough that doors are usable.
|
||
|
||
---
|
||
|
||
## #60 — `obstruction_ethereal` retail downstream path not ported (M2 combat-HUD impact)
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW for M1 (no observable defect); MEDIUM for M2 (combat contact reporting on ethereal creatures will be wrong)
|
||
**Filed:** 2026-05-13 (final-review surfaced from B.4b)
|
||
**Component:** physics / `CollisionExemption.ShouldSkip` + downstream movement contact handling
|
||
|
||
**Description:** B.4b's L.2g slice 1b widened `CollisionExemption.ShouldSkip` to exempt
|
||
on `ETHEREAL_PS` alone (cite `src/AcDream.Core/Physics/CollisionExemption.cs:62-79`). Retail's
|
||
`acclient_2013_pseudo_c.txt:276782` requires both `ETHEREAL_PS && IGNORE_COLLISIONS_PS` to wrap
|
||
the entire `FindObjCollisions` body — ETHEREAL alone takes the deeper path at line 276795 which
|
||
sets `sphere_path.obstruction_ethereal = 1` and lets downstream movement allow passage WHILE
|
||
STILL REPORTING THE CONTACT. We do not port that downstream path; we just exempt entirely.
|
||
|
||
**M2 impact:** Combat HUD work that relies on physics-contact reporting for ethereal creatures
|
||
(ghosts, partially-phased monsters, spell projectiles with ETHEREAL set) will see no contact at
|
||
all instead of "soft contact with obstruction_ethereal=1". The user will not be able to target
|
||
or interact with such entities via the contact path.
|
||
|
||
**Acceptance:** Port the retail deeper path so `obstruction_ethereal=1` flows through movement +
|
||
collision-reporting layers. Tests should cover: ETHEREAL creature target → contact reported but
|
||
passage allowed; ETHEREAL+IGNORE_COLLISIONS target (door, retail-style) → full exempt.
|
||
|
||
**Estimated scope:** Moderate. Touches `CollisionExemption.cs`, transition/movement layer, and
|
||
sphere-path state propagation. Visible test through a spawned ethereal creature in ACE.
|
||
|
||
---
|
||
|
||
## #59 — [DONE 2026-05-15 · `5e29773`] `WorldPicker` 5m fixed-radius could over-pick at tight thresholds (M1-deferred polish)
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (cosmetic — picker grabs the right entity in Holtburg-tested scenarios)
|
||
**Filed:** 2026-05-13 (final-review surfaced from B.4b)
|
||
**Component:** selection / `AcDream.Core.Selection.WorldPicker.Pick`
|
||
|
||
**Description:** `WorldPicker.Pick` uses a hardcoded 5m sphere around every candidate's
|
||
`Position` regardless of the entity's actual size (`src/AcDream.Core/Selection/WorldPicker.cs:82`).
|
||
This matches `WorldEntity.DefaultAabbRadius` and is sufficient for M1 acceptance: in tight
|
||
doorways, every server-keyed candidate has correct sphere coverage and the closest-wins logic
|
||
plus `ServerGuid==0` skip filter the wrong picks. But the invariant "non-clickable geometry has
|
||
`ServerGuid==0`" is load-bearing — if L.2d ever ports `CBuildingObj` as a server-keyed entity,
|
||
the picker may mis-target buildings. Per-entity `Setup.Radius` would be tighter.
|
||
|
||
**Acceptance:** Either (a) tighten picker to read per-entity Setup.Radius / CylSphere bounds,
|
||
or (b) document the invariant in `WorldPicker.cs` and add a regression test asserting
|
||
`ServerGuid==0` entities never reach the per-candidate hit test.
|
||
|
||
**Estimated scope:** Quick (~1 hour) — wire `Setup.Radius` lookup into the picker and update
|
||
the 6 existing picker tests with realistic radii.
|
||
|
||
---
|
||
|
||
## #58 — [DONE 2026-05-13] Door swing animation: UpdateMotion not wired for non-creature entities
|
||
|
||
**Status:** DONE
|
||
**Closed:** 2026-05-13
|
||
**Severity:** MEDIUM (was M1 demo cosmetic — doors functioned but didn't visually animate)
|
||
**Filed:** 2026-05-13
|
||
**Component:** animation / `UpdateMotion (0xF74D)` routing for non-creature entities
|
||
|
||
**Closure:** Closed by Phase B.4c on branch `claude/phase-b4c-door-anim`
|
||
(4 implementation commits). The complete animation round-trip for door entities
|
||
is now wired and visual-verified at the Holtburg inn doorway: double-click a
|
||
closed door → swing-open animation plays → player walks through → ~30s later
|
||
ACE broadcasts `UpdateMotion (NonCombat, Off)` → swing-close animation plays.
|
||
|
||
Implementation: spawn-time `AnimationSequencer` registration for door entities
|
||
in `GameWindow.OnLiveEntitySpawnedLocked` (Task 1, commit `9053860`), with
|
||
initial state seeded from `spawn.PhysicsState` so closed doors initialize to
|
||
the `Off` cycle and open doors initialize to the `On` cycle. A `[door-cycle]`
|
||
diagnostic line in `OnLiveMotionUpdated` (Task 2, commit `b89f004`) confirms
|
||
each `UpdateMotion` is processed. A shared `IsDoorName` predicate (Task 2
|
||
review, commit `8a9b15e`) eliminates duplication. A stance-value fix (bonus,
|
||
commit `454d88e`) corrected `NonCombat = 0x3D` (not `0x01`), which was causing
|
||
doors to render halfway underground due to empty sequencer frames.
|
||
|
||
Two follow-up items were filed: issue #61 (link→cycle boundary flash — brief
|
||
visual flap at end of swing animation; low severity) and issue #62 (PARTSDIAG
|
||
null-guard for sequencer-driven entities; latent, not currently reachable).
|
||
|
||
See [`docs/research/2026-05-13-b4c-shipped-handoff.md`](research/2026-05-13-b4c-shipped-handoff.md)
|
||
for the full evidence trail, log output, and bonus-discovery narrative. M1
|
||
demo target "open the inn door" now has full visual feedback.
|
||
|
||
**Files (what shipped):**
|
||
- `src/AcDream.App/Rendering/GameWindow.cs` — `IsDoorSpawn` / `IsDoorName` helpers, spawn-time `AnimationSequencer` registration branch in `OnLiveEntitySpawnedLocked`, `_doorSequencers` dict, `[door-cycle]` diagnostic in `OnLiveMotionUpdated`, `TickAnimations` loop extended to advance door sequencers.
|
||
- `src/AcDream.Core/Physics/AnimationSequencer.cs` — no changes required; existing link+cycle API was sufficient.
|
||
|
||
---
|
||
|
||
## #57 — [DONE 2026-05-13] B.4 interaction-handler missing: clicking on doors / NPCs / items silently does nothing
|
||
|
||
**Status:** DONE
|
||
**Closed:** 2026-05-13
|
||
**Severity:** HIGH (was M1 blocker)
|
||
**Filed:** 2026-05-12
|
||
**Component:** input / interaction / `GameWindow.OnInputAction`
|
||
|
||
**Closure:** Closed by Phase B.4b on branch `claude/compassionate-wilson-23ff99`
|
||
(9 implementation commits, Tasks 1-4 per plan + 4 bonus fixes). The
|
||
full round-trip — double-click door → `WorldPicker.BuildRay` + `Pick` →
|
||
`InteractRequests.BuildUse` → ACE `SetState` reply → `ShadowObjectRegistry`
|
||
mutation (via fixed ServerGuid→entity.Id translation) → `CollisionExemption.ShouldSkip`
|
||
exempts (widened to ETHEREAL-alone) → player walks through — was
|
||
visual-verified at the Holtburg inn doorway 2026-05-13. Four bonus
|
||
discoveries were required beyond the original plan: (1) `InputDispatcher`
|
||
had no double-click detection, (2) `OnInputAction` gate blocked
|
||
`DoubleClick` activations, (3) `CollisionExemption` required both
|
||
ETHEREAL+IGNORE_COLLISIONS while ACE sends only ETHEREAL, (4)
|
||
`OnLiveStateUpdated` passed server GUID to a local-entity-ID-keyed
|
||
registry. M1 demo target "open the inn door" met. See
|
||
[docs/research/2026-05-13-b4b-shipped-handoff.md](research/2026-05-13-b4b-shipped-handoff.md)
|
||
for full evidence and rationale.
|
||
|
||
**Files (what shipped):**
|
||
- `src/AcDream.Core/Selection/WorldPicker.cs` (new; formerly zero callers, now wired)
|
||
- `src/AcDream.App/Rendering/GameWindow.cs` — `OnInputAction` switch cases for `SelectLeft` / `SelectDblLeft` / `UseSelected`; `OnLiveStateUpdated` ServerGuid→Id translation; `_entitiesByServerGuid` reverse-lookup dict
|
||
- `src/AcDream.UI.Abstractions/Input/InputDispatcher.cs` — double-click detection
|
||
- `src/AcDream.Core/Physics/CollisionExemption.cs` — widened to ETHEREAL-alone
|
||
|
||
---
|
||
|
||
## #55 — Static-entity slow path reports ~1.45M `meshMissing` per 5s at r4 standstill
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (no visible regression — affects a diagnostic counter, not rendered output)
|
||
**Filed:** 2026-05-11
|
||
**Component:** rendering / `WbDrawDispatcher` static-entity classification path
|
||
|
||
**Description:** During the Phase N.6 slice 1 baseline measurement (`docs/plans/2026-05-11-phase-n6-perf-baseline.md` §2),
|
||
the radius=4 standstill scenario reported `meshMissing ≈ 1,450,000` per 5-second
|
||
`[WB-DIAG]` window. The same scenario while walking drops to near-zero (`meshMissing = 0`
|
||
in the steady state) as new landblocks stream in and previously-missing meshes resolve.
|
||
This suggests the static-entity slow path's mesh-load lifecycle has some delay before
|
||
populating for newly-streamed content but eventually catches up; the standstill case
|
||
keeps re-counting the same set of entities-with-unresolved-meshes for the duration of
|
||
the run. The counter is per-frame so the absolute number scales with FPS — at the
|
||
measured ~150 FPS that's ~290K reports/s, or ~1900 entities each reported each frame.
|
||
|
||
**Root cause / status:** Not investigated. Hypothesis: an entity classification path
|
||
counts mesh-missing on every frame for static entities whose `MeshRef` resolution races
|
||
the streaming loader. The Tier 1 cache (#53) populates only for entities whose
|
||
classification succeeded, so persistently-failing entities run the slow path every frame
|
||
forever and bump `meshMissing` every time. If true, the fix is either (a) cache the
|
||
"this entity's mesh genuinely doesn't exist" result so we stop re-checking, or (b)
|
||
deferred-classify the entity once its `MeshRef` resolves.
|
||
|
||
**Files:** `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (the slow path that
|
||
increments `_meshesMissing`), `src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs`
|
||
(the Tier 1 cache — likely needs to learn about "permanently missing" entries).
|
||
|
||
**Acceptance:** `meshMissing` should drop to near-zero within ~5 seconds of streaming
|
||
settle at any radius/motion combination, not stay at ~1.45M/5s indefinitely at standstill.
|
||
|
||
---
|
||
|
||
## #50 — [DONE 2026-05-11 · accepted WB divergence] Road-edge tree at 0xA9B1 visible in acdream but not retail
|
||
|
||
**Status:** DONE
|
||
**Closed:** 2026-05-11
|
||
**Severity:** LOW (cosmetic; one spawned tree near the road in Holtburg)
|
||
**Filed:** 2026-05-08
|
||
**Component:** scenery placement / Phase N (WorldBuilder rendering migration)
|
||
|
||
**Resolution:** Same disposition as #49 — accepted as WB-upstream
|
||
divergence from retail. The earlier fix attempt (`e279c46`, ACME-style
|
||
per-vertex road check) successfully removed this specific tree but
|
||
over-suppressed scenery elsewhere; revert at `677a726` stood. Without
|
||
a coherent port of ACME's full per-vertex filter set, piecemeal
|
||
patching is net-negative. Left as a documented WB divergence.
|
||
|
||
---
|
||
|
||
**Original investigation (kept for reference):**
|
||
|
||
**Description:** With `ACDREAM_USE_WB_SCENERY=1` (default since commit `b84ecbd`),
|
||
a tree at landblock 0xA9B1 around `(lx=85.08, ly=190.97)` appears in acdream but
|
||
neither retail nor ACME WorldBuilder render it. Upstream Chorizite/WorldBuilder
|
||
DOES render it, so our migration to WB's helpers (Phase N.1) inherited this
|
||
discrepancy from upstream.
|
||
|
||
**Root cause (suspected):** ACME WorldBuilder includes a per-vertex road check that
|
||
skips the entire vertex when its road bit is set (see
|
||
`references/WorldBuilder-ACME-Edition/WorldBuilder/Editors/Landscape/GameScene.cs:1074`).
|
||
The current vertex (4,8) has a road bit set in the dat. ACME skips it;
|
||
Chorizite/WorldBuilder doesn't; we don't.
|
||
|
||
**Fix attempt that didn't work:** commit `e279c46` added the per-vertex road check
|
||
directly to our `GenerateViaWb` (and legacy `Generate` for parity). It successfully
|
||
removed the offending tree but over-suppressed scenery in other landblocks (visual
|
||
regressions during user testing). Reverted in commit `677a726`. ACME's check likely
|
||
interacts with other factors (per-vertex building check, or something else in ACME's
|
||
pipeline) that we'd need to port together, not the road check alone.
|
||
|
||
**Next steps:**
|
||
1. Investigate ACME's full per-vertex filter set (road + building + anything else)
|
||
and port them as a coherent unit, not piecemeal.
|
||
2. OR upstream the per-vertex road check to Chorizite/WorldBuilder (which is now our
|
||
submodule fork) so it lands as a generic ACME-conformance improvement.
|
||
3. OR consider switching fork target from Chorizite/WorldBuilder to ACME WorldBuilder
|
||
for future phases (N.2+).
|
||
|
||
Visually undetectable to most users; one extra tree at one landblock. Defer until
|
||
other Phase N work catches a similar issue and a coherent fix becomes obvious.
|
||
|
||
**Files:**
|
||
- `src/AcDream.Core/World/SceneryGenerator.cs` — `GenerateInternal` is the active path
|
||
- `src/AcDream.Core/World/WbSceneryAdapter.cs` — adapter used by `GenerateInternal`
|
||
- `references/WorldBuilder-ACME-Edition/WorldBuilder/Editors/Landscape/GameScene.cs:1074` — ACME's per-vertex road filter
|
||
|
||
---
|
||
|
||
## #49 — [DONE 2026-05-11 · accepted WB divergence] Scenery (X, Y) placement drifts from retail at some landblocks
|
||
|
||
**Status:** DONE
|
||
**Closed:** 2026-05-11
|
||
**Severity:** LOW (minor cosmetic placement difference)
|
||
**Filed:** 2026-05-06
|
||
**Component:** scenery placement / `SceneryGenerator`
|
||
|
||
**Resolution:** Accepted as WB-upstream divergence from retail. Since
|
||
the N.1 phase (WorldBuilder-backed scenery, see roadmap), acdream
|
||
defers scenery placement math to the WB fork; retail and WB diverge
|
||
slightly here on some landblocks. Piecemeal patching against WB
|
||
upstream would create a maintenance burden disproportionate to the
|
||
visible impact (a handful of trees positioned a few meters off across
|
||
the world). Left as-is; revisit only if WB upstream patches the
|
||
divergence or if a coherent ACME-style filter port (see issue body
|
||
below) becomes worthwhile.
|
||
|
||
The original investigation plan (cdb trace of retail's
|
||
`CLandBlock::get_land_scenes` for diff against acdream's
|
||
`SceneryGenerator` output) is preserved below for historical
|
||
reference if anyone picks this up.
|
||
|
||
---
|
||
|
||
**Original investigation (kept for reference):**
|
||
|
||
**Description:** While verifying the `#48` Z fix at Holtburg
|
||
landblock `0xA9B30001`, the user spotted a scenery tree placed at
|
||
the **wrong (X, Y)** in acdream relative to retail at the same
|
||
character coords. Specifically: a large tree that retail places far
|
||
across the road on the right (east) side appears in acdream on the
|
||
left (west) side, near a chess board / picnic-bench area. Side-by-
|
||
side screenshot pair captured 2026-05-06.
|
||
|
||
This is **not** a Z bug — every tree in the same screenshot has its
|
||
trunk meeting the visible terrain (the `#48` `SampleTerrainZ` fix is
|
||
working). It's also **not** the LandBlockInfo Stab path — the chess
|
||
board / bench themselves are correctly placed, so the landblock
|
||
origin and `lbOffset` math are right.
|
||
|
||
**Hypotheses (need cdb retail trace to disambiguate):**
|
||
|
||
1. The displacement-noise math in `SceneryGenerator` differs from
|
||
retail's `chunk_005A0000` LCG by a constant or a sign flip. Audit
|
||
`eeee4c5` claimed "all MATCH" against the decomp, but a runtime
|
||
trace would prove or disprove.
|
||
2. Coordinate-system handedness: cell-local `(lx, ly)` in our path
|
||
may map to retail's `(ly, lx)` somewhere, rotating tree XY 90°
|
||
around the cell's NW corner.
|
||
3. The `obj.Align != 0` path in retail (`FUN_005a6f60`, aligns the
|
||
object to the landcell polygon's normal) may use a different
|
||
reference point than ours, drifting placement on sloped cells.
|
||
4. Slope filter could reject a cell retail accepts (or vice versa),
|
||
pushing trees into adjacent cells.
|
||
5. Region-table / `SceneInfo` lookup might select a different
|
||
scenery list for the cell type.
|
||
|
||
**Investigation plan (gold-standard, per `project_retail_debugger.md`):**
|
||
|
||
1. Run the existing `ACDREAM_DUMP_SCENERY_Z=1` diagnostic to capture
|
||
acdream's full per-spawn (gfx, world XY, scale, partT) for
|
||
landblock `0xA9B3FFFF`.
|
||
2. Attach cdb to a live retail client at the same Holtburg spot
|
||
(`tools/pdb-extract/check_exe_pdb.py` confirms PDB pairs with
|
||
v11.4186). Set a breakpoint on `CLandBlock::get_land_scenes` (or
|
||
the inner `chunk_005A0000` placement function); capture every
|
||
`(gfxObjId, worldX, worldY, scale, heading)` retail emits for
|
||
the same landblock.
|
||
3. Diff the two tables. The spawn that's offset will be obvious;
|
||
the offset pattern (one tree, all trees, one species, constant
|
||
delta, etc.) determines which hypothesis above is correct.
|
||
|
||
**Files:**
|
||
|
||
- [`src/AcDream.Core/World/SceneryGenerator.cs`](src/AcDream.Core/World/SceneryGenerator.cs) — placement math (LCG noise, displacement, rotation, scale, slope filter)
|
||
- `acclient!CLandBlock::get_land_scenes` (`docs/research/named-retail/acclient_2013_pseudo_c.txt`) — retail entry point
|
||
- `chunk_005A0000.c` — referenced retail source per `SceneryGenerator.cs` comments
|
||
- [`docs/research/named-retail/symbols.json`](docs/research/named-retail/symbols.json) — for cdb breakpoints
|
||
|
||
**Acceptance:** Side-by-side outdoor screenshot pair (acdream vs
|
||
retail, same character coords, same time of day) shows scenery
|
||
positions matching at multiple landblocks. The cdb trace + diagnostic
|
||
diff documents quantitative agreement (zero offset within float
|
||
precision) on at least one landblock end-to-end.
|
||
|
||
**Out of scope here (kept under `#48`):** Z floating. That's fixed.
|
||
|
||
---
|
||
|
||
## #48 — [DONE 2026-05-06 · a469395] A few specific scenery trees hover above terrain (per-GfxObj Z misplacement)
|
||
|
||
**Resolution:** Hypothesis 2 (physics-sampler vs bilinear-fallback Z
|
||
mismatch). The bilinear fallback in `GameWindow.SampleTerrainZ` had
|
||
its two diagonal arms swapped — used the SEtoNW triangle test on
|
||
SWtoNE cells and vice versa. Every scenery hydration in our
|
||
diagnostic ran through the bilinear path (`source=bilinear` in all
|
||
`[scenery-z]` log lines) because physics hadn't yet built a
|
||
`TerrainSurface` for the streaming-in landblock — so on sloped
|
||
cells, scenery sat at a different Z than the visible terrain mesh
|
||
by up to ~1.5 m. The bug was latent since `ff325ab` (2026-04-17)
|
||
which upgraded the fallback from naive 4-corner bilinear to
|
||
triangle-aware barycentric, but with the diagonal-pair tests
|
||
swapped. `TerrainSurface.SampleZ` (used by the physics path / player
|
||
Z) was always correct, so player feet stayed flush — the two paths
|
||
just disagreed and only scenery noticed.
|
||
|
||
Fix: extracted the canonical triangle-pick math into
|
||
`TerrainSurface.InterpolateZInTriangle` (private static); added
|
||
`TerrainSurface.SampleZFromHeightmap` (public static) that reads
|
||
heights directly from the landblock byte array using the same
|
||
canonical math; redirected `GameWindow.SampleTerrainZ` to delegate
|
||
to it. New conformance test
|
||
`SampleZFromHeightmap_AgreesWithInstance_AcrossWholeLandblock` pins
|
||
both sampler paths together at 1500 sample points across both
|
||
diagonals, so future drift gets caught. User visually confirmed
|
||
2026-05-06.
|
||
|
||
The diagnostic dump (`ACDREAM_DUMP_SCENERY_Z=1`,
|
||
`GameWindow.cs:4661`) is kept committed — it's gated by env var,
|
||
zero cost when off, and is the right starting point for `#49`
|
||
(scenery X/Y placement) too.
|
||
|
||
Pseudocode: [`docs/research/2026-05-06-issue-48-fix-pseudocode.md`](docs/research/2026-05-06-issue-48-fix-pseudocode.md).
|
||
|
||
**Status:** DONE
|
||
**Severity:** LOW (cosmetic; ~3 trees per landblock, easy to ignore but obvious once spotted)
|
||
**Filed:** 2026-05-06
|
||
**Component:** rendering / scenery placement / terrain Z sampling
|
||
|
||
**Description:** In outdoor landblocks, a small subset of tree
|
||
scenery instances render visibly **floating above the terrain**
|
||
(trunk base ~0.5–1.5 m above the ground line). The vast majority
|
||
of scenery (other tree species, bushes, rocks) sits flush. The bug
|
||
is **per-GfxObj-id**: the same handful of species float wherever
|
||
they spawn; other species at the same (x, y) cell sit correctly.
|
||
Side-by-side with retail in the same area: retail places the same
|
||
species flush. User-confirmed via screenshot pair 2026-05-06.
|
||
|
||
The user noted this is the only thing left wrong with terrain
|
||
rendering (canopy density / shape were *not* the issue — those
|
||
match retail when looked at carefully). The bug is purely vertical
|
||
offset on a few species.
|
||
|
||
**Investigation 2026-05-06:**
|
||
|
||
[`SceneryGenerator.cs:204`](src/AcDream.Core/World/SceneryGenerator.cs:204)
|
||
returns `LocalPosition.Z = obj.BaseLoc.Origin.Z` (just the
|
||
ObjectDesc's BaseLoc Z offset, no terrain). [`GameWindow.cs:4642`](src/AcDream.App/Rendering/GameWindow.cs:4642)
|
||
adds the terrain ground Z:
|
||
|
||
```csharp
|
||
float groundZ = _physicsEngine.SampleTerrainZ(worldPx, worldPy)
|
||
?? SampleTerrainZ(lb.Heightmap, _heightTable, localX, localY);
|
||
float finalZ = groundZ + spawn.LocalPosition.Z;
|
||
```
|
||
|
||
Both samplers claim to use the AC2D split-direction terrain mesh
|
||
formula. Player feet land flush, so player Z sampling is correct;
|
||
scenery for most species is also flush; only specific GfxObjs
|
||
float.
|
||
|
||
**Three competing hypotheses (need one diagnostic to disambiguate):**
|
||
|
||
1. **Per-GfxObj origin convention.** Most AC tree GfxObjs are
|
||
authored with local origin at the trunk base (mesh vertices
|
||
have `Z >= 0` measured up from the origin). A few species
|
||
may be authored with origin at bbox-center or visual top —
|
||
for those, `finalZ = groundZ + BaseLoc.Z` plants the *center*
|
||
at ground and the visible trunk floats by half its height.
|
||
Per-GfxObj-id ⇒ deterministic across instances ⇒ fits the
|
||
"same 3 species everywhere" pattern.
|
||
|
||
2. **Physics-sampler vs bilinear-fallback Z mismatch on
|
||
NE↔SW-cut cells.** The physics path uses the AC2D
|
||
split-direction formula. The bilinear-fallback at
|
||
`GameWindow.cs:4643` uses naive bilinear over heightmap
|
||
corners — wrong on cells whose visible triangle slopes
|
||
the *other* way. If physics hasn't registered a landblock
|
||
yet when scenery hydrates (timing race), affected scenery
|
||
uses the bilinear sampler and lands on a different Z than
|
||
the visible terrain. Player Z is fine because player movement
|
||
always goes through the physics sampler.
|
||
|
||
3. **Same close-degrade story as #47, applied to scenery.** Some
|
||
tree GfxObjs have `DIDDegrade` tables; slot 0 (close-detail)
|
||
and the base-LOD-3 mesh may have different mesh-local origins.
|
||
We currently draw the base GfxObj id directly for scenery (the
|
||
close-degrade resolver is scoped to humanoid setups only).
|
||
Retail draws slot 0 for nearby trees. If slot-0 has origin at
|
||
trunk-base while base-LOD-3 has origin at bbox-center, those
|
||
species float by exactly the offset between the two origins.
|
||
|
||
**Cheapest first move:** add a one-shot scenery placement dump
|
||
gated by `ACDREAM_DUMP_SCENERY_Z=1` that logs, per spawn:
|
||
|
||
```
|
||
[scenery-z] gfxObj=0xXXXXXXXX setupOrGfx=… worldPos=(x,y,z)
|
||
BaseLoc.Z=… groundZ=… meshZRange=[zMin..zMax]
|
||
hasDIDDegrade=true/false degrades[0]=0xXX
|
||
```
|
||
|
||
User identifies one floating tree → grep that GfxObj id in the
|
||
log → look at meshZRange and `hasDIDDegrade`. That tells us
|
||
hypothesis 1 (zMin > 0 by the float amount), hypothesis 2 (matching
|
||
species correctly placed elsewhere → timing race), or hypothesis 3
|
||
(`hasDIDDegrade=true` and slot 0 mesh has different zMin). One log
|
||
sample answers the question.
|
||
|
||
**Files:**
|
||
|
||
- [`src/AcDream.Core/World/SceneryGenerator.cs:204`](src/AcDream.Core/World/SceneryGenerator.cs:204) — BaseLoc.Z passthrough
|
||
- [`src/AcDream.App/Rendering/GameWindow.cs:4632-4655`](src/AcDream.App/Rendering/GameWindow.cs:4632) — groundZ resolution + finalZ assembly
|
||
- [`src/AcDream.Core/Physics/TerrainSurface.cs`](src/AcDream.Core/Physics/TerrainSurface.cs) — physics sampler (AC2D split-direction formula)
|
||
- `SampleTerrainZ` (private, in GameWindow.cs) — bilinear fallback
|
||
- [`src/AcDream.Core/Meshing/GfxObjDegradeResolver.cs`](src/AcDream.Core/Meshing/GfxObjDegradeResolver.cs) — close-degrade resolver if hypothesis 3 confirmed; would need scenery-scope expansion (drop the `IsIssue47HumanoidSetup` gate or add a scenery-aware variant)
|
||
|
||
**Acceptance:** All scenery species rest flush on the visible
|
||
terrain mesh in side-by-side outdoor screenshots vs retail. No
|
||
regression on the species that already render correctly.
|
||
|
||
**Handoff:** [docs/research/2026-05-06-issue-48-handoff.md](docs/research/2026-05-06-issue-48-handoff.md)
|
||
|
||
---
|
||
|
||
## #39 — Run↔Walk cycle transition not visible on observed player remotes (acdream-as-observer)
|
||
|
||
**Status:** OPEN — VERIFY-PENDING (cases #1/#2/#4/#5 user-verified working 2026-05-06; cases #3/#6/#7 unverified in live test)
|
||
**Severity:** LOW (most cases now visibly correct after the 2026-05-06 fix sequence; remaining unverified cases are direction-flip — believed to work via direct UM but not explicitly exercised)
|
||
**Filed:** 2026-05-03
|
||
**Component:** physics / motion / animation
|
||
|
||
**Description:** When observing a remote-driven player character through
|
||
acdream and the actor toggles Shift while keeping a direction key held
|
||
(Run↔Walk demote/promote), the visible leg cycle does NOT update on the
|
||
observer side. Body position eventually corrects via UpdatePosition
|
||
hard-snaps (causing visible position blips), but the animation cycle
|
||
stays at whatever it was last set to (Run sticks; Walk sticks).
|
||
|
||
Observation matrix:
|
||
|
||
| Observer | Actor | Cycle Run↔Walk | Z on slopes |
|
||
|---|---|---|---|
|
||
| Retail | Retail | ✓ | ✓ |
|
||
| Retail | Acdream | ✓ | ✓ |
|
||
| Acdream | Acdream | ✓ | ✗ (only with env-var path) |
|
||
| Acdream | Retail | ✗ | ✗ |
|
||
|
||
**Root cause / status:**
|
||
|
||
ACE only broadcasts a fresh `UpdateMotion` (UM) when the wire's
|
||
`ForwardCommand` byte changes — i.e. on direction-key state changes
|
||
(W press, W release). Toggling Shift while W is held changes
|
||
`ForwardSpeed` and `HoldKey` but NOT `ForwardCommand`, so ACE does
|
||
NOT broadcast a UM for the demote/promote. The speed change DOES
|
||
propagate via `UpdatePosition` (position-delta velocity changes
|
||
between Run-pace and Walk-pace), confirmed via `[VEL_DIAG]`
|
||
serverSpeed varying ~2.5 m/s (walk) ↔ ~9 m/s (run).
|
||
|
||
Retail's inbound code uses UP-derived velocity to refine the visible
|
||
cycle when no UM tells it. Acdream has the equivalent function —
|
||
`ApplyServerControlledVelocityCycle` in `GameWindow.cs:3274` — but
|
||
it's gated `if (IsPlayerGuid(serverGuid)) return;` for player
|
||
remotes, exactly the case where the gap matters.
|
||
|
||
(Earlier hypothesized as H2 in the 2026-05-03 four-agent investigation
|
||
but marked refuted because the [UPCYCLE] diag never fired — that
|
||
was BECAUSE of the gate; un-gating reveals it firing per UP, which
|
||
is the correct behavior.)
|
||
|
||
**Fix sketch (~10 lines):** un-gate `ApplyServerControlledVelocityCycle`
|
||
for player remotes when `currentMotion` is a locomotion cycle
|
||
(Run/Walk/Sidestep/Backward). UMs still drive direction-key changes
|
||
authoritatively; UP-derived velocity refines the speed bucket within
|
||
the same direction. Add a `LastUMUpdateTime` grace window (e.g.
|
||
500ms) so UMs win when fresh.
|
||
|
||
**Files:**
|
||
|
||
- `src/AcDream.App/Rendering/GameWindow.cs:3274` — `ApplyServerControlledVelocityCycle`
|
||
(the gate `if (IsPlayerGuid(serverGuid)) return;` to remove with conditions)
|
||
- `src/AcDream.App/Rendering/GameWindow.cs:3640-3660` — call site (already
|
||
passes through with HasServerVelocity from synthesized UP-deltas)
|
||
- `src/AcDream.Core/Physics/ServerControlledLocomotion.cs:54-76` —
|
||
`PlanFromVelocity` thresholds (may need re-tuning if banding is observed)
|
||
|
||
**Research:**
|
||
|
||
- `docs/research/2026-05-03-remote-anim-cycle/investigation-prompt.md` —
|
||
full background of the four-agent investigation
|
||
- `docs/research/2026-05-06-locomotion-cycle-transitions/investigation-prompt.md` —
|
||
expansion to the full 7-transition matrix (Run↔Walk forward + backward,
|
||
Fast↔Slow strafe L+R, direction-flip cases) with TTD-driven workflow
|
||
- `docs/research/2026-05-06-locomotion-cycle-transitions/findings-static.md` —
|
||
static-analysis findings + scope of the 2026-05-06 candidate fix
|
||
(case #1, Run↔Walk forward only)
|
||
- This session's diagnostic logs at `tools/diag-logs/walkrun-A1b-*.log`
|
||
(UM_RAW, FWD_WIRE, SETCYCLE traces) confirming ACE's wire pattern
|
||
|
||
**Acceptance:**
|
||
|
||
- Observer in acdream watching a retail-driven character toggle Shift
|
||
while holding W: visible leg cycle switches Run↔Walk within ~200ms
|
||
of the wire change.
|
||
- No regression on the working cases (acdream-on-acdream, retail
|
||
observers, idle↔Run, idle↔Walk).
|
||
- No spurious cycle thrashing during turning while running (ObservedOmega
|
||
doesn't trigger velocity-bucket changes).
|
||
|
||
**Progress 2026-05-06 — Shift-toggle cases (#1, #2, #4, #5) fixed; user-verified:**
|
||
|
||
Five-commit sequence on this branch (`claude/determined-solomon-d0356d`):
|
||
|
||
| Commit | Effect |
|
||
|---|---|
|
||
| `8fa04af` | First candidate — added `RemoteMotion.LastUMTime` + `ApplyPlayerLocomotionRefinement` with 500 ms UM grace + forward-direction hysteresis. **Ineffective** because the call site lived in dead code for player remotes. |
|
||
| `863d96b` | Skip transition link in SetCycle for direct cyclic-locomotion → cyclic-locomotion. **Reduces queue accumulation** (qCount climbs slower); not the actual case-#1 fix but architecturally correct. |
|
||
| `bb026b7` | Per-tick `[CURRNODE]` diagnostic — exposed that `_currNode` was correctly tracking SetCycle's intent and so the bug was elsewhere. Read-only. |
|
||
| `2653b30` | **Wire `ApplyServerControlledVelocityCycle` into the L.3 M2 player-remote path.** Found via the diag — the existing call site at `OnLivePositionUpdated` line ~3879 was unreachable for players because the L.3 M2 routing returns at line 3755. New synth-velocity computation + call inserted in the player branch. **User-verified working** for forward Run↔Walk via Shift toggle. |
|
||
| `cc62e1c` | Handle backward (`CurrentSpeedMod < 0` → preserve negative sign) and sidestep (low byte 0x0F / 0x10 → keep motion ID, refine magnitude). Backward regression resolved. |
|
||
| `349ba65` | Use `SidestepAnimSpeed` (1.25) instead of `WalkAnimSpeed` (3.12) when computing sidestep magnitude — fix #4's mapping was 2.5× too small for slow strafe. |
|
||
|
||
**Wire-level finding refuting the original ISSUES.md root-cause hypothesis: Earlier diagnostic claims that ACE broadcasts UMs on Shift toggle were misread.** A clean test (`launch-39-diag2.log`) holding W and toggling Shift while held shows `[FWD_WIRE]` for retail-driven actor only emitting `Ready ↔ Run` transitions — no Walk wire transitions at all, despite a clear walk-pace ↔ run-pace shift visible in `[VEL_DIAG]`. So retail's outbound DOES go silent on HoldKey-only changes. The earlier launch's many Walk↔Run `[FWD_WIRE]` lines came from W press/release cycles with Shift held continuously — different scenarios.
|
||
|
||
**Verified working (user, 2026-05-06):**
|
||
|
||
- Forward Run↔Walk via Shift toggle (case #1)
|
||
- Backward Walk slow↔fast via Shift toggle (case #2) — animation matches direction, no rubber-band
|
||
- Strafe-left / strafe-right slow↔fast via Shift toggle (cases #4 / #5) — cadence visibly changes
|
||
|
||
**Residual / not yet verified:**
|
||
|
||
- "Not as fast as retail" — ~500 ms `UmGraceSeconds` window adds latency on top of the UP cadence (5–10 Hz). Could be tuned shorter once cases #3 / #6 / #7 are validated.
|
||
- Direction-flip cases (#3 W↔S, #6 A↔D, #7 W↔A/D) — believed to work via direct UM, not explicitly verified yet.
|
||
|
||
**New related issue filed: #45** — local-player slow-strafe-walk renders too slow. Same `SidestepAnimSpeed` vs `WalkAnimSpeed` mismatch pattern as fix #5, but on the local-player render path (`UpdatePlayerAnimation`), not the observer side.
|
||
|
||
## #42 — [DONE 2026-05-05 · ec59a08] Airborne XY drift on observed player remote jumps (~1 m horizontal offset over arc)
|
||
|
||
**Status:** DONE
|
||
**Severity:** MEDIUM (pre-existing PhysicsEngine bug; exposed by L.3 M2 airborne UP no-op + M4 CellId fix)
|
||
**Filed:** 2026-05-05 (root cause confirmed same day)
|
||
**Closed:** 2026-05-05
|
||
**Commit:** `ec59a08`
|
||
**Component:** physics (`PhysicsEngine.ResolveWithTransition` → `FindObjCollisions` self-skip)
|
||
|
||
**Resolution (2026-05-05):** Self-collision in `FindObjCollisions`, not
|
||
any of the three originally-hypothesised mechanisms below. Live
|
||
entities (local player, remotes) register a Cylinder in
|
||
`ShadowObjectRegistry` at spawn (`GameWindow.cs:2545`) which
|
||
`UpdatePosition` keeps tracking the entity's live world position.
|
||
With no self-skip filter, the moving sphere's own cylinder is always
|
||
sitting at the body's exact position and `CylinderCollision` slides
|
||
the sphere out of overlap on every airborne tick. Validated by the
|
||
[SWEEP-OBJ] diagnostic added in commit `a36369d`: every drift event
|
||
showed `gfxObj=0x02000001` (humanoid setup) at `obj.Position` exactly
|
||
matching the body's `pre`. Mirrors retail's `CObjCell::find_obj_collisions`
|
||
self-skip at named-retail line 308931:
|
||
|
||
```c
|
||
if ((physobj->parent == 0 && physobj != arg2->object_info.object))
|
||
result = CPhysicsObj::FindObjCollisions(physobj, arg2);
|
||
```
|
||
|
||
Plumbing: `ObjectInfo.SelfEntityId` field, optional
|
||
`movingEntityId = 0` parameter on `ResolveWithTransition`,
|
||
`PlayerMovementController.LocalEntityId` refreshed per-tick from
|
||
`_entitiesByServerGuid[_playerServerGuid].Id`, remote sweep at
|
||
`GameWindow.cs:6474` passes `kv.Key`. Lock-the-fix unit test at
|
||
`PhysicsEngineTests.ResolveWithTransition_SelfShadowEntry_NotPushedWhenIdMatches`.
|
||
|
||
Verified via two visual + log runs (`launch-42-verify.log` /
|
||
`launch-42-verify2.log`): zero stationary-jump drift across both,
|
||
`gfxObj=0x02000001` phantom no longer appears in `[SWEEP-OBJ]`,
|
||
no >0.5m pushes anywhere. The originally-listed hypotheses (H1
|
||
slope-driven AdjustOffset projection, H2 step-down probe, H3
|
||
EdgeSlide) were all RULED OUT by the first evidence run — `cpN`
|
||
was `(0, 0, 1)` flat for every drift event.
|
||
|
||
**Diagnostic kept in tree:** `ACDREAM_AIRBORNE_DIAG=1` enables the
|
||
`[SWEEP]` + `[SWEEP-OBJ]` traces for future regression hunts.
|
||
|
||
The original investigation log is preserved below for context.
|
||
|
||
**Root cause (verified 2026-05-05 via A/B test):**
|
||
|
||
`ResolveWithTransition` running per-tick during the airborne arc is the
|
||
source of the drift. Verified by A/B-toggling the M4 CellId fix
|
||
(`rmState.CellId = p.LandblockId`) which is the gate that lets the
|
||
sweep run for player-remote jumps:
|
||
|
||
- **CellId line removed** → sweep skipped → jumps render with
|
||
geometrically-correct XY (no drift) but body falls through the
|
||
floor (no terrain catch).
|
||
- **CellId line present** → sweep runs → jumps land correctly but
|
||
arc shows ~1 m horizontal offset from actor's actual XY; body
|
||
snaps back on next inbound UM.
|
||
|
||
So the drift originates inside `ResolveWithTransition` itself, not
|
||
from wire data, not from local Euler integration, not from stale
|
||
velocity. Decision recorded in commit history: kept CellId fix in
|
||
production code so jumps land (`fall-through-floor` is more disruptive
|
||
to gameplay than `~1m visual jitter that resolves on next input`).
|
||
This issue tracks the proper fix.
|
||
|
||
**Description:** When observing a retail-controlled remote that jumps
|
||
in place (no horizontal input), the visible jump arc renders with
|
||
a small horizontal offset from the actor's actual position — typically
|
||
~1 m to one side and slightly forward. Body lands at offset position
|
||
(~X+1m). On the next inbound UM/UP from the actor (e.g., turning or
|
||
moving), the body snaps back to the server's authoritative X.
|
||
|
||
User report 2026-05-05 (after M4 CellId fix): "I stand at position X
|
||
and jump, it looks like im jumping slightly to the left of X like
|
||
1m-ish (if I observe jumping char from behind). It also lands at
|
||
X + 1m-ish. Position resets to X when I issue some other command
|
||
to the client like turning."
|
||
|
||
**Why it surfaced now:**
|
||
|
||
Pre-M2 (legacy path), `OnLivePositionUpdated` hard-snapped
|
||
`rmState.Body.Position = worldPos` on EVERY UP including mid-arc
|
||
airborne ones. ACE broadcasts intermediate UPs at ~5–10 Hz during
|
||
the jump arc with the actor's authoritative mid-arc position;
|
||
each snap kept our local body close to server, masking
|
||
local-integration error.
|
||
|
||
L.3 M2 (commit 40d88b9) implemented the retail-spec airborne no-op
|
||
in `OnLivePositionUpdated`:
|
||
|
||
```csharp
|
||
if (!update.IsGrounded) {
|
||
entity.Position = rmState.Body.Position;
|
||
return;
|
||
}
|
||
```
|
||
|
||
Per `docs/research/2026-05-04-l3-port/03-up-routing.md` § 3:
|
||
|
||
> Air branch (`has_contact == 0`): the function falls through to
|
||
> `return 0`. This is the "AIRBORNE NO-OP" … The body keeps
|
||
> integrating gravity locally; received position is discarded.
|
||
|
||
This matches retail `MoveOrTeleport @ 0x00516330` semantics. But it
|
||
removes the periodic server snapping that was masking ~1 m of
|
||
accumulated local-integration drift. The drift is pre-existing — the
|
||
user reports having seen it before — but is now visible for the
|
||
full arc duration instead of being corrected every ~200 ms.
|
||
|
||
**Likely mechanism (ranked by probability):**
|
||
|
||
1. **Initial-overlap depenetration along non-+Z terrain normal** — at
|
||
jump start the collision sphere is touching the floor at body Z.
|
||
Most outdoor terrain triangles are not perfectly horizontal — their
|
||
normals have a small horizontal component. The sweep's first action
|
||
each tick is to resolve overlap by separating the sphere along the
|
||
contact normal; on a tilted terrain triangle that separation has
|
||
horizontal magnitude. The body gets shoved sideways the first frame
|
||
of the jump and the rest of the arc carries that initial drift.
|
||
Direction-correlation with terrain orientation would confirm
|
||
(test in different landblocks; if drift direction varies with the
|
||
slope of the launch tile, this is it).
|
||
|
||
2. **Step-down probe firing despite `isOnGround: false`** — sweep's
|
||
internal "search for nearest walkable surface" might still scan
|
||
horizontally during airborne ticks even when we pass `isOnGround:
|
||
!rm.Airborne` (= false for airborne). Check whether the
|
||
`stepUpHeight` / `stepDownHeight` parameters are unconditionally
|
||
used inside `ResolveWithTransition` regardless of the
|
||
`isOnGround` flag.
|
||
|
||
3. **EdgeSlide on near-vertical motion against a near-vertical
|
||
surface** — if the sphere even slightly grazes a wall while
|
||
ascending or descending, EdgeSlide projects motion tangent to the
|
||
wall, redirecting some Z velocity into XY. Less likely for
|
||
open-ground stationary jumps but could explain drift near
|
||
buildings.
|
||
|
||
**Fix paths:**
|
||
|
||
a. **Skip initial-overlap depenetration when airborne** — gate the
|
||
"separate from initial contact plane" step inside
|
||
`ResolveWithTransition` on `isOnGround: true`. Trusts the previous
|
||
tick's resolve to have left the body in a non-overlapping position.
|
||
This is the most likely-correct fix if hypothesis (1) is right.
|
||
|
||
b. **Zero step-up/down for airborne sweeps** — pass
|
||
`stepUpHeight: 0f, stepDownHeight: 0f` when `rm.Airborne`. Kills
|
||
hypothesis (2) without other side effects (airborne bodies don't
|
||
step anyway).
|
||
|
||
c. **Stripped airborne sweep** — replace the full sphere sweep with
|
||
a simpler vertical sphere-vs-terrain intersection + wall-collision
|
||
stop. Loses some retail fidelity but eliminates all three
|
||
mechanisms. Probably overkill if (a) or (b) suffices.
|
||
|
||
**Files:**
|
||
|
||
- `src/AcDream.Core/Physics/PhysicsEngine.cs` —
|
||
`ResolveWithTransition` and any internal `CTransition` /
|
||
`find_valid_position` helpers. The initial-overlap depenetration
|
||
path is the primary investigation target.
|
||
- `src/AcDream.App/Rendering/GameWindow.cs:6478+` (legacy airborne
|
||
TickAnimations, the call site) — reference only; not the bug.
|
||
|
||
**Reference:**
|
||
|
||
Retail equivalent at
|
||
`docs/research/named-retail/acclient_2013_pseudo_c.txt`:
|
||
- `CTransition::find_valid_position` (called from `transition()`)
|
||
- `SpherePath` initialization
|
||
- The verbatim retail depenetration logic for airborne bodies
|
||
|
||
If our port differs from retail in this region, that diff is likely
|
||
the bug.
|
||
|
||
**Repro:**
|
||
|
||
1. Launch acdream + retail client side-by-side connected to local ACE.
|
||
2. Have retail char stand still on outdoor terrain at any position X.
|
||
3. Jump in place.
|
||
4. Observe acdream window: arc renders ~1 m offset from X, lands
|
||
offset, snaps back on next UM.
|
||
|
||
To verify the depenetration hypothesis specifically, repeat the jump
|
||
in different landblock spots — drift direction should correlate with
|
||
the local terrain normal, not the actor's facing.
|
||
|
||
**Acceptance:**
|
||
|
||
- Visual jump arc + landing render at the actor's actual XY position,
|
||
no perceptible horizontal offset, no snap-back on next UM.
|
||
- Wall-collision airborne (jumping into building doorways, jumping
|
||
puzzles) still works — fix must not strip collision wholesale.
|
||
|
||
---
|
||
|
||
## #41 — Residual sub-decimeter blips on observed player remotes (M3 baseline)
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (within retail's own DesiredDistance / MinDistance tolerances; visible only on close inspection)
|
||
**Filed:** 2026-05-05
|
||
**Component:** physics / motion / animation (per-tick remote prediction)
|
||
**Phase:** L.2 (Movement & Collision Conformance) — inbound-motion fidelity sub-piece. Blocked on cdb-trace of `CSequence::velocity` for Humanoid running cycle, then porting `add_motion @ 0x005224b0`'s `style_speed × MotionData.velocity` chain.
|
||
|
||
**Description:** With the L.3 M3 path live (queue catch-up + animation
|
||
root motion fallback), observed player remotes chase server position
|
||
smoothly with NO staircase on slopes and NO per-UP rubber-band. However
|
||
small position blips remain — sub-decimeter amplitude, periodic with
|
||
the server's UP cadence (~1 Hz). User report 2026-05-05: "I get very
|
||
small blips now. Running works, walking works, strafing works."
|
||
|
||
The blips fall well within retail's own tolerances:
|
||
|
||
- `DesiredDistance` (queue head reach radius) = 0.05 m
|
||
- `MinDistanceToReachPosition` (primary stall threshold) = 0.20 m
|
||
|
||
So they are NOT a stall trigger and NOT a correctness bug. They're a
|
||
visible artifact of the velocity-synthesis residual: anim root motion
|
||
(`AnimationSequencer.CurrentVelocity = RunAnimSpeed × adjustedSpeed`)
|
||
slightly overshoots server pace between UPs, then queue catch-up walks
|
||
the body back toward the server position on the next UP — a small
|
||
rubber-band that's smaller than M2's pre-fix version but still
|
||
perceptible.
|
||
|
||
**Root cause hypothesis (untested):**
|
||
|
||
The L.3 handoff explicitly flagged this. From `06-acdream-audit.md` § 9
|
||
and `05-position-manager-and-partarray.md` § 7:
|
||
|
||
> Our `CurrentVelocity` carries only the steady-state component of the
|
||
> cycle's intent; the per-frame stride wobble is gone… For Humanoid
|
||
> the dat ships `MotionData.Velocity = 0` so the multiply is a no-op
|
||
> anyway — but the synth uses `RunAnimSpeed × adjustedSpeed` directly.
|
||
|
||
ACE's wire `ForwardSpeed` for a running player is the **server runRate**
|
||
(~2.94 for skill 200), not a unit multiplier. Our synth multiplies
|
||
`RunAnimSpeed` (4.0) by `adjustedSpeed` (~2.94) = ~11.76 m/s, which
|
||
the queue catch-up clamps via `min(catchUp × dt, dist)` but the anim
|
||
fallback applies in full when the queue is idle. If the actual
|
||
server-broadcast pace is closer to 4.0 m/s (RunAnimSpeed alone, with
|
||
runRate as a *frame-rate* multiplier rather than a velocity scalar),
|
||
our fallback overshoots by ~3× and the queue walks it back every UP.
|
||
|
||
Per the handoff: **don't normalize at the wire boundary** (prior
|
||
session tried this, called it a hack). The right fix is porting
|
||
retail's actual behavior in `add_motion @ 0x005224b0` and
|
||
`apply_run_to_command` to determine the correct `CSequence::velocity`
|
||
magnitude.
|
||
|
||
**Files:**
|
||
|
||
- `src/AcDream.Core/Physics/AnimationSequencer.cs` — `CurrentVelocity`
|
||
synthesis at L614–679 (RunAnimSpeed=4.0, WalkAnimSpeed=3.12,
|
||
SidestepAnimSpeed=1.25 × adjustedSpeed)
|
||
- `src/AcDream.Core/Physics/PositionManager.cs` — `ComputeOffset`
|
||
applies `seqVel × dt × orientation` as fallback when queue is idle
|
||
|
||
**Research:**
|
||
|
||
- `docs/research/2026-05-04-l3-port/05-position-manager-and-partarray.md` § 5–7
|
||
- `docs/research/2026-05-04-l3-port/06-acdream-audit.md` § 9 (AnimationSequencer)
|
||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` line 298437
|
||
(`add_motion @ 0x005224b0`) — `CSequence::velocity = style_speed × MotionData.velocity`
|
||
|
||
**Fix path (research first, then port):**
|
||
|
||
1. cdb-trace retail to capture `CSequence::velocity` and
|
||
`MotionData::velocity` for a Humanoid running cycle. Compare against
|
||
our synth (4.0 × 2.94 = 11.76 m/s) to determine the actual retail
|
||
magnitude.
|
||
2. Port `add_motion`'s `style_speed × MotionData.velocity` chain
|
||
verbatim. For Humanoid where `MotionData.Velocity = 0`, port the
|
||
fallback retail uses (likely a separate code path through
|
||
`apply_run_to_command` that derives velocity from the cycle's
|
||
framerate, not a constant).
|
||
3. Remove the `RunAnimSpeed × adjustedSpeed` synth in
|
||
`AnimationSequencer.SetCycle`.
|
||
|
||
**Acceptance:**
|
||
|
||
- Visual blips disappear on flat-ground steady-state running.
|
||
- Side-by-side acdream-as-observer vs retail-as-observer of the same
|
||
server-controlled toon: indistinguishable body trajectory.
|
||
|
||
---
|
||
|
||
## #40 — [DONE 2026-05-05 · 40d88b9] ACDREAM_INTERP_MANAGER=1 env-var path regressed (staircase + blips)
|
||
|
||
**Status:** DONE — closed by L.3 M2 (`feat(motion): L.3 M2 — queue-only chase for grounded player remotes`, commit 40d88b9)
|
||
|
||
**Resolution:** The env-var gate was retired entirely. Both
|
||
`OnLivePositionUpdated` and `TickAnimations` now use
|
||
`IsPlayerGuid(serverGuid)` to route player-remote UPs through the
|
||
retail-faithful queue path (formerly the env-var path, but with two
|
||
key fixes per the L.3 spec):
|
||
|
||
1. `PositionManager.ComputeOffset` is the per-tick translation source
|
||
(REPLACE semantics: queue catch-up overrides anim root motion when
|
||
active, anim stands when queue is idle / head reached). Mirrors
|
||
retail `UpdatePositionInternal @ 0x00512c30`.
|
||
2. `ResolveWithTransition` is **not** called for grounded player
|
||
remotes — server already collision-resolved the broadcast position,
|
||
and sweeping per-tick on tiny queue catch-up deltas amplified
|
||
micro-bounces into visible blips. This was the staircase + blip
|
||
regression. Trade-off documented in audit § 6.
|
||
|
||
User-verified 2026-05-05: smooth body chase, no staircase on slopes,
|
||
no per-UP rubber-band on flat ground. Residual sub-decimeter blips
|
||
filed separately as #41 (velocity-synthesis magnitude).
|
||
|
||
**Filed-original-context (for archive):**
|
||
|
||
**Status:** OPEN (do-not-enable; pending L.3 follow-up rebuild)
|
||
**Severity:** N/A (gated; default behavior unaffected)
|
||
**Filed:** 2026-05-03
|
||
**Component:** physics / motion (per-tick remote prediction)
|
||
|
||
**Description:** The `ACDREAM_INTERP_MANAGER=1` per-frame remote tick
|
||
introduced by commit `e94e791` (L.3.1+L.3.2 Task 3) is a regression and
|
||
should not be enabled. Two visible symptoms:
|
||
|
||
1. **Z staircase on slopes:** observed remotes running up/down hills
|
||
sink into rising terrain or float over receding terrain, then snap
|
||
to correct Z at each `UpdatePosition` arrival. Body never follows
|
||
the terrain mesh between UPs.
|
||
|
||
2. **Position blips during steady-state motion:** XY drifts
|
||
unconstrained between UPs, then UP hard-snaps cause visible jumps.
|
||
|
||
Both symptoms ABSENT when env-var unset (default legacy path).
|
||
|
||
**Root cause:** the env-var path was designed to mirror retail
|
||
`CPhysicsObj::MoveOrTeleport` (acclient @ 0x00516330). MoveOrTeleport
|
||
is retail's network-packet entry point — minimal work. The per-frame
|
||
physics tick is retail's `update_object` (FUN_00515020) — full chain
|
||
including `apply_current_movement` → `UpdatePhysicsInternal` →
|
||
`Transition::FindTransitionalPosition` (collision sweep). The legacy
|
||
path mirrors `update_object` correctly. The env-var path stripped the
|
||
collision sweep on a wrong assumption that this was "more retail-
|
||
faithful" — it was the opposite.
|
||
|
||
Commit B (039149a, 2026-05-03) ported `ResolveWithTransition` into the
|
||
env-var path, but the symptom persisted because the env-var path also
|
||
clears `body.Velocity` for grounded remotes (no Euler integration of
|
||
horizontal motion → sweep input is the catch-up offset only, which
|
||
itself stair-steps because UPs are sampled at ~1 Hz).
|
||
|
||
**Files:**
|
||
|
||
- `src/AcDream.App/Rendering/GameWindow.cs:6042-6260` — env-var per-frame branch
|
||
- `src/AcDream.App/Rendering/GameWindow.cs:6260+` — legacy per-frame branch (works)
|
||
- `src/AcDream.Core/Physics/PositionManager.cs` — class itself is retail-faithful
|
||
(port of CPositionManager::adjust_offset), only the integration was wrong
|
||
|
||
**Research:**
|
||
|
||
- This session's `2026-05-03` chronological commit log + visual verification
|
||
- `docs/research/2026-05-03-remote-anim-cycle/investigation-prompt.md`
|
||
for the four-agent investigation that traced this
|
||
|
||
**Fix path (separate L.3 follow-up phase, NOT this session):**
|
||
|
||
The PositionManager class is correct retail-port. Re-integrate it as
|
||
ADDITIVE refinement on top of the working legacy chain (small
|
||
correction toward queued server positions, applied AFTER
|
||
`apply_current_movement` + `UpdatePhysicsInternal` + collision sweep)
|
||
— not as a REPLACEMENT for them. Match retail's actual `update_object`
|
||
chain ordering: `position_manager::adjust_offset` runs after the
|
||
primary motion + collision resolution.
|
||
|
||
**Acceptance:**
|
||
|
||
- New per-tick path enabled via env-var (or default after stabilization)
|
||
produces the same smooth slope motion + zero blips as the legacy path.
|
||
- Inbound `UpdatePosition` queue catch-up nudges body toward server
|
||
authoritative position without overriding terrain Z snap or causing
|
||
position blips.
|
||
- Verification: side-by-side vs legacy default in 2-client setup,
|
||
identical visible behavior.
|
||
|
||
## #38 — [DONE 2026-05-06 · (this commit)] Chase camera + player feel "30 fps" since L.5 physics-tick gate
|
||
|
||
**Status:** DONE
|
||
**Severity:** MEDIUM (gameplay-feel regression; not a correctness bug)
|
||
**Filed:** 2026-05-01
|
||
**Closed:** 2026-05-06
|
||
**Commit:** `(this commit)`
|
||
**Component:** rendering / physics / camera
|
||
|
||
**Description:** User reports that running around in third-person /
|
||
chase camera feels less smooth than it did before the L.5 physics-tick
|
||
work. FPS counter still reads 60+, but the *motion* of the player
|
||
character + camera looks like it's updating at ~30 fps.
|
||
|
||
**Root cause / status:**
|
||
|
||
Almost certainly the L.5 `_physicsAccum` gate in
|
||
`PlayerMovementController.cs` (lines ~448-456). Retail integrates
|
||
physics at 30 Hz (`MinQuantum = 1/30 s`); we ported that faithfully so
|
||
collision behavior matches. Side effect: `_body.Position` only updates
|
||
on physics ticks, i.e. every 33 ms. Render runs at 60+ Hz but the
|
||
chase camera follows `_body.Position` directly — so the *visible*
|
||
position changes in 33 ms steps, even though we render at 60+ FPS.
|
||
First-person is less affected because the world rotates with Yaw (which
|
||
*does* update every render frame); third-person is hit hardest because
|
||
the character itself is the moving thing.
|
||
|
||
Retail in 2013 didn't see this because render was also ~30 fps —
|
||
render rate ≈ physics rate. Our 60+ Hz render exposes the gap.
|
||
|
||
Discussion + fix options at the end of `docs/research/2026-05-01-retail-motion-trace/findings.md`
|
||
("Other things still don't have…" → camera smoothness discussion in
|
||
chat, not yet captured in the doc — TODO migrate the discussion in).
|
||
|
||
Recommended fix: **render-time interpolation between physics ticks**
|
||
(standard fixed-timestep + interpolated rendering pattern from Quake /
|
||
Source / Unreal). Snapshot `_prevPhysicsPos` and `_currPhysicsPos` at
|
||
each tick; render player + camera target at
|
||
`Lerp(_prev, _curr, _physicsAccum / PhysicsTick)`. Cost: ~33 ms visual
|
||
latency between input and what you see (matches retail's perceived
|
||
latency anyway). Network outbound stays on the discrete tick value —
|
||
no wire change.
|
||
|
||
Quick confirmation test before any code change: temporarily set
|
||
`PhysicsTick` to `1.0/60.0` and see if chase camera feels smooth again.
|
||
If yes, gate is confirmed cause. (Don't ship that — it'd undo the L.5
|
||
collision fixes.)
|
||
|
||
**Files:**
|
||
|
||
- `src/AcDream.App/Input/PlayerMovementController.cs:172` — `PhysicsTick` constant
|
||
- `src/AcDream.App/Input/PlayerMovementController.cs:448-456` — `_physicsAccum` gate
|
||
- `src/AcDream.App/Rendering/GameWindow.cs` — wherever player render position + chase camera read `_body.Position`
|
||
|
||
**Research:**
|
||
|
||
- L.5 background: `memory/project_retail_debugger.md` (the 30 Hz
|
||
MinQuantum gate, the cdb trace evidence)
|
||
- Discussed during 2026-05-01 motion-trace work
|
||
|
||
**Acceptance:**
|
||
|
||
- Chase-camera run-around at 60+ FPS feels as smooth as render rate
|
||
suggests (no perceptual stepping) — user visually confirmed
|
||
2026-05-06.
|
||
- Network outbound (MoveToState / AutonomousPosition cadence + values)
|
||
unchanged from current behavior
|
||
- Collision behavior unchanged (the L.5 wedge / steep-roof scenarios
|
||
still resolve correctly)
|
||
- Observer view from a parallel retail client unchanged
|
||
|
||
## #37 — [DONE 2026-05-11 · resolved by `0bd9b96`] Humanoid coat doesn't extend up to neck (visible "skin stub" between hair and coat)
|
||
|
||
**Status:** DONE
|
||
**Closed:** 2026-05-11
|
||
**Commit:** `0bd9b96` (the #47 humanoid degrade-resolver fix, 2026-05-06)
|
||
**Severity:** LOW (cosmetic; doesn't affect gameplay)
|
||
**Filed:** 2026-05-01
|
||
**Component:** rendering / clothing / textures
|
||
|
||
**Resolution:** Closed by the same mesh-fidelity work that resolved #47.
|
||
The `GfxObjDegradeResolver` (commit `0bd9b96`, 2026-05-06) swapped
|
||
humanoid parts to their higher-detail `Degrade[0].Id` meshes (e.g.
|
||
upper arm `0x01000055 → 0x01001795`, lower arm `0x01000056 → 0x0100178F`).
|
||
The higher-detail meshes include the coat-collar polygons that the
|
||
low-detail meshes were missing — which is what was exposing the
|
||
skin-toned palette indices in the upper-coat region. With the
|
||
correct mesh resolution, those polygons cover the previously-visible
|
||
"skin stub". User confirmed visually 2026-05-11.
|
||
|
||
The original 2026-05-01/2026-05-04 investigation work (palette range
|
||
analysis, SubPalette overlay tracing) is preserved below for
|
||
historical reference; it was a correct read of *what* was rendering,
|
||
but the root cause was the missing collar polygons, not the palette
|
||
gap.
|
||
|
||
---
|
||
|
||
**Original investigation (kept for reference):**
|
||
|
||
**Description:** Every humanoid character (player + NPCs) wearing a coat
|
||
shows a visible skin-colored region at the top of the coat where retail
|
||
shows continuous coat fabric. From the back view: hair → skin stub →
|
||
coat top. In retail: hair → coat collar (no exposed skin). This was
|
||
originally reported as "head/neck protruding forward" — the apparent
|
||
forward shift is an optical illusion caused by the missing coat collar.
|
||
|
||
**Investigation 2026-05-01 (~3 hr session, conclusively ruled out
|
||
many hypotheses):**
|
||
|
||
What we ruled out:
|
||
|
||
- **Animation source.** `ACDREAM_USE_PLACEMENT_BASE=1` (force chars to
|
||
`Setup.PlacementFrames[Resting]` instead of `Animation.PartFrames[0]`)
|
||
→ stub still visible.
|
||
- **Backface culling / mesh winding.** `ACDREAM_NO_CULL=1` (disable
|
||
`glCullFace` entirely) → stub still visible.
|
||
- **Palette overlay (SubPalettes).** `ACDREAM_NO_PALETTE_OVERLAY=1`
|
||
(skip `ComposePalette`) → stub still visible (other colors broke
|
||
as expected — confirms overlay was firing). Bug is NOT a body-skin
|
||
SubPalette being mis-applied to coat fabric.
|
||
- **Bug source = part 16 (head).** `ACDREAM_HIDE_PART=16` → head goes
|
||
away, stub remains UNCHANGED (clean coat top with same shape).
|
||
Stub is NOT from head GfxObj polygons.
|
||
- **Per-part placement frame Origin.** `ACDREAM_NUDGE_Y=-0.1` confirmed
|
||
`+Y = forward` in body-local; head Origin (0, 0.013, 1.587) places
|
||
head correctly relative to spine. Math checks out.
|
||
|
||
What we confirmed (data is correct):
|
||
|
||
- Player Setup `0x02000001` (Aluvian Male), 34 parts.
|
||
- Server (ACE) sends `animParts=34 texChanges=12 subPalettes=10`.
|
||
- Part 9 (upper torso/coat) has gfx `0x0100120D` after AnimPartChange.
|
||
- Part 9 has 2 surfaces, BOTH covered by 2 TextureChanges
|
||
(`oldTex=0x050003D5→0x05001AFE`, `oldTex=0x050003D4→0x05001AFC`).
|
||
- Stub IS from part 9: `ACDREAM_HIDE_PART=9` → entire torso (including
|
||
stub region) disappears.
|
||
- Per-part composition formula (`Scale × Rotation × Translation`)
|
||
matches ACME's `StaticObjectManager.cs:256-258` and retail decomp's
|
||
`Frame::combine` at `0x00518FD0`.
|
||
|
||
**Investigation 2 (2026-05-04, 5 parallel agents + dat probes):**
|
||
|
||
ALL of the obvious hypotheses ruled out:
|
||
|
||
- **Byte-level decode primitive matches ACViewer.** INDEX16/P8/DXT/BGRA paths are byte-identical.
|
||
- **Polygon emission matches retail.** All 43 polygons of gfx `0x0100120D` are `SidesType=0` (ST_SINGLE), all surfaces are `Base1Image` — NO ST_DOUBLE polygons we'd be missing, NO surfaces lacking the `Type & 6` bits that retail's `DrawPolyInternal` skips.
|
||
- **Per-PART texture-override scoping is correct.** `resolvedOverridesByPart[partIdx]` gets per-MeshRef'd; not a global flat map (Agent 3's claim was wrong).
|
||
- **SubPalettes are full-size (Colors.Count=2048) palettes.** Our `subPal.Colors[idx]` indexing matches ACViewer's `newPalette.Colors[j + offset]`.
|
||
- **The `*8` wire un-pack is correctly single-applied** (parser stores raw bytes; ComposePalette multiplies once).
|
||
|
||
**The actual smoking gun (Investigation 2):**
|
||
|
||
For `+Acdream` the server sends 10 SubPaletteSwap ranges that overlay palette indices:
|
||
`[0..320)`, `[576..1024)`, `[1392..1488)`, `[1728..1920)`. **The complement — indices `[320..576)`, `[1024..1392)`, `[1488..1728)`, `[1920..2048)` — is NOT overlaid.** Base palette `0x0400007E` at those indices contains the original red/skin tones (sampled values: `0x46 0x22 0x04`, `0x4A 0x28 0x09`, etc).
|
||
|
||
If the coat texture's UVs at the upper region map to texel-bytes whose palette index lands in one of those non-overlaid ranges, those pixels render with base-palette skin tones. That's the visible "skin stub at the top of the coat".
|
||
|
||
**Working hypothesis:** either
|
||
1. ACE sends incomplete SubPalette ranges (retail-original would cover the full palette)
|
||
2. Retail does *additional* client-side compute that ACE pre-resolves wrongly
|
||
3. The base palette `0x0400007E` itself is supposed to have coat colors at those indices in retail's interpretation (different palette decode)
|
||
|
||
**Next investigation (deferred):**
|
||
|
||
- Diff ACE's `WorldObject_Networking.cs` CharGen ObjDesc construction against retail's
|
||
`ClothingTable::BuildObjDesc` (`acclient_2013_pseudo_c.txt:436261`). Check if ACE
|
||
actually walks every CloSubPaletteRange in the chosen PaletteTemplate, or skips some.
|
||
- RenderDoc capture: confirm which texel/palette-index the upper-region polygons sample.
|
||
- `tools/InspectCoatTex/Program.cs` is the diagnostic harness — extend it.
|
||
|
||
**Files (diagnostic env vars committed for next-session reuse):**
|
||
|
||
- ~~`src/AcDream.App/Rendering/InstancedMeshRenderer.cs:210-275`
|
||
— `ACDREAM_NO_CULL` env var~~ (file deleted in N.5 ship amendment)
|
||
- `src/AcDream.App/Rendering/GameWindow.cs` — `ACDREAM_HIDE_PART=N`
|
||
hides specific humanoid part; `ACDREAM_DUMP_CLOTHING=1` dumps
|
||
AnimPartChanges + TextureChanges + per-part Surface chain coverage.
|
||
- `src/AcDream.App/Rendering/TextureCache.cs:159-204` — `DecodeFromDats`
|
||
is the texture decode entry. Compare against
|
||
`references/WorldBuilder-ACME-Edition/.../TextureHelpers.cs`.
|
||
|
||
**Reproduction:**
|
||
|
||
```powershell
|
||
$env:ACDREAM_LIVE = "1"; $env:ACDREAM_DEVTOOLS = "1"
|
||
# normal launch — visible from chase camera looking at +Acdream's back
|
||
```
|
||
|
||
Stub is visible on +Acdream and on every NPC humanoid (Pathwarden,
|
||
Town Crier, Shopkeeper Renald, etc.).
|
||
|
||
**Acceptance:** Side-by-side retail + acdream rendering of +Acdream
|
||
shows coat extending up to chin level on both. No exposed skin
|
||
between hair and coat.
|
||
|
||
## #L.1 — Hotbar UI panel
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM
|
||
**Filed:** 2026-04-26 (deferred from Phase K)
|
||
**Component:** ui / hotbar
|
||
|
||
**Description:** Number keys 1-9 are bound to `UseQuickSlot_1..9`
|
||
actions but no panel exists. Actions fire (visible via the `[input]`
|
||
console log) but produce no visible result. Phase L feature: drag-drop
|
||
hotbar with up to 5 bars × 9 slots, drag spell/skill icons to slots,
|
||
key activates the slot's contents. Server-side: `CreateShortcutToSelected`
|
||
(action 0x0A9 in retail motion table) sends a `UseSelected` on slot
|
||
fire.
|
||
|
||
**Files:** `src/AcDream.UI.Abstractions/Panels/Hotbar/` (TBC).
|
||
|
||
**Acceptance:** Drag an item or spell into slot 1, press `1`, server
|
||
responds as if the user clicked the item.
|
||
|
||
---
|
||
|
||
## #L.2 — Spellbook favorites panel
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM
|
||
**Filed:** 2026-04-26 (deferred from Phase K)
|
||
**Component:** ui / magic
|
||
|
||
**Description:** In `MagicCombat` scope, 1-9 should fire
|
||
`UseSpellSlot_1..9` (distinct from hotbar). Requires a small UI to
|
||
pin favorite spells + a spellbook tab nav. Cross-references issue
|
||
#L.3 (combat-mode dispatch).
|
||
|
||
---
|
||
|
||
## #L.3 — Combat-mode tracking + scope-aware Insert/PgUp/Delete/End/PgDn dispatch
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM
|
||
**Filed:** 2026-04-26 (deferred from Phase K)
|
||
**Component:** input / combat
|
||
|
||
**Description:** Insert/PgUp/Delete/End/PgDn mean different things in
|
||
melee / missile / magic combat modes (per retail keymap MeleeCombat /
|
||
MissileCombat / MagicCombat blocks). Phase K has the bindings and the
|
||
scope stack; what's missing: `CombatState.CurrentMode` field +
|
||
listener for the server-side `SetCombatMode` packet (likely 0x0053 or
|
||
similar — confirm against ACE source). When mode arrives, push the
|
||
appropriate scope; when leaving combat, pop.
|
||
|
||
---
|
||
|
||
## #L.4 — F-key panels: Allegiance / Fellowship / Skills / Attributes / World / SpellComponents
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW
|
||
**Filed:** 2026-04-26 (deferred from Phase K)
|
||
**Component:** ui
|
||
|
||
**Description:** Retail F3-F6, F8-F12 toggle UI panels for various
|
||
character data. Phase K has the bindings (`ToggleAllegiancePanel`,
|
||
`ToggleFellowshipPanel`, `ToggleSpellbookPanel`,
|
||
`ToggleSpellComponentsPanel`, `ToggleAttributesPanel`,
|
||
`ToggleSkillsPanel`, `ToggleWorldPanel`, `ToggleInventoryPanel`); the
|
||
panels themselves don't exist. Each is its own design feature.
|
||
Inventory (F12) is the most-requested.
|
||
|
||
---
|
||
|
||
## #L.5 — Floating chat windows (Alt+1-4)
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW
|
||
**Filed:** 2026-04-26 (deferred from Phase K)
|
||
**Component:** ui / chat
|
||
|
||
**Description:** Alt+1..4 toggle four floating chat windows in retail.
|
||
Phase K binds the actions; `ChatPanel` currently is a single window.
|
||
Floating windows would need filtered-by-channel-type chat tail
|
||
rendering.
|
||
|
||
---
|
||
|
||
## #L.6 — UI layout save/load (saveui / loadui / lockui)
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW
|
||
**Filed:** 2026-04-26 (deferred from Phase K)
|
||
**Component:** ui
|
||
|
||
**Description:** Retail had `@saveui <name>`, `@loadui <name>`,
|
||
`@lockui` commands for persisting ImGui-style window layouts. ImGui
|
||
has built-in `LoadIniSettingsFromMemory` /
|
||
`SaveIniSettingsToMemory` — wire these to per-named-layout files,
|
||
plus chat-command parsing for the `@` prefixes.
|
||
|
||
---
|
||
|
||
## #L.7 — Joystick / gamepad bindings
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW
|
||
**Filed:** 2026-04-26 (deferred from Phase K)
|
||
**Component:** input
|
||
|
||
**Description:** Retail keymap declares 11 Joystick devices in the
|
||
`Devices` block but no actions are bound by default. acdream uses
|
||
Silk.NET keyboard+mouse only. Adding Silk.NET joystick support + a
|
||
`JoystickInputSource` adapter would unlock controller play.
|
||
`KeyChord.Device` byte already supports values >1, so the binding
|
||
side is ready.
|
||
|
||
---
|
||
|
||
## #L.8 — Plugin / scripting / macro input subscription
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM
|
||
**Filed:** 2026-04-26 (deferred from Phase K)
|
||
**Component:** plugin / input
|
||
|
||
**Description:** CLAUDE.md goal: "Build acdream's plugin API to
|
||
support scripting/macros for player automation." Plugins should be
|
||
able to register custom actions (with namespaced IDs like
|
||
`mymacro.heal-rotation`) and subscribe to `InputAction` events. Phase K
|
||
foundation supports this via the multicast `InputDispatcher`; what's
|
||
missing is the plugin-API surface.
|
||
|
||
---
|
||
|
||
## #32 — Retail edge-slide / cliff-slide / precipice-slide incomplete
|
||
|
||
**Status:** IN-PROGRESS
|
||
**Severity:** HIGH
|
||
**Filed:** 2026-04-29
|
||
**Component:** physics / collision
|
||
|
||
**Description:** When walking along walls, roof edges, cliff edges, or failed
|
||
step-down boundaries, retail often slides along the boundary. acdream still
|
||
hard-blocks or accepts too much in several of these cases.
|
||
|
||
**Root cause / status:** Tracked under Phase L.2c. Wall-adjacent
|
||
`step_up_slide` now feels acceptable in live testing. Local/remote movement
|
||
passes the retail-default `EdgeSlide` flag. The first precipice-slide slice now
|
||
preserves terrain/BSP walkable polygon vertices and runs the retail back-probe
|
||
before `SPHEREPATH::precipice_slide`; edge-slide `Slid` / `Adjusted` results
|
||
now feed the `TransitionalInsert` retry loop instead of being reverted by outer
|
||
validation, and a synthetic diagonal terrain-boundary test covers tangent
|
||
motion. `ACDREAM_DUMP_EDGE_SLIDE=1` now reports whether a failed step-down had
|
||
polygon context.
|
||
|
||
**L.4/L.5 update 2026-04-30:** A retail debugger trace (cdb attached to
|
||
v11.4186 acclient.exe — see #35) confirmed that retail does NOT wedge
|
||
on the steep-roof scenario that produces the wedge in our acdream port.
|
||
Three concrete findings:
|
||
1. Retail's `OBJECTINFO::kill_velocity` rarely fires in normal play —
|
||
gated on `last_known_contact_plane_valid`, which our L.2.4 proximity
|
||
guard tends to clear before steep-poly hits land. Retail trace: 0
|
||
kill_velocity hits across 40,960 update_object calls. Our Phase 3
|
||
reset path now matches retail's gate (only kills when valid).
|
||
2. Retail integrates physics at 30Hz (`MinQuantum = 1/30 s`); render is
|
||
60+ Hz. UpdatePhysicsInternal/update_object ratio = 0.61. We
|
||
ported this gate as L.5 in `PlayerMovementController` via
|
||
`_physicsAccum`. Render still runs at 60+ Hz; only the physics
|
||
integration step is 30Hz.
|
||
3. The remaining wedge cause — body's pre-position drifts to the
|
||
polygon's tangent and gravity's tangent component into surface
|
||
produces a stable retain-collide-revert loop — is a downstream
|
||
consequence of retail's grounded-on-steep escape chain
|
||
(`step_sphere_up` → `step_up_slide` → `cliff_slide`) being
|
||
incompletely ported. Live test confirmed retail-strict Path 6
|
||
produces "lands on roof in falling animation, can't slide off"
|
||
half-state because that chain doesn't produce smooth descent.
|
||
|
||
**Pragmatic ship-state:** BSPQuery Path 6 keeps the L.4 slide-tangent
|
||
deviation (project-along-steep-face-and-return-Slid) for steep-poly
|
||
airborne hits. It produces user-acceptable "slide off the roof"
|
||
behavior at the cost of departing from retail's Path 6 → SetCollide →
|
||
Path 4 → Phase 3 reset chain. Retail-strict requires the
|
||
step_up_slide / cliff_slide audit below; until that lands, slide-tangent
|
||
is the right deviation.
|
||
|
||
Remaining gaps: real-DAT building-edge fixtures, fuller `cliff_slide`
|
||
coverage, `NegPolyHit` dispatch, and the retail-strict
|
||
step_up_slide / cliff_slide audit (filed for follow-up). Named retail
|
||
anchors include `CTransition::edge_slide`, `CTransition::cliff_slide`,
|
||
`SPHEREPATH::precipice_slide`, and `SPHEREPATH::step_up_slide`.
|
||
|
||
**Files:** `src/AcDream.Core/Physics/TransitionTypes.cs`,
|
||
`src/AcDream.Core/Physics/BSPQuery.cs`,
|
||
`tests/AcDream.Core.Tests/`.
|
||
|
||
**Research:** `docs/plans/2026-04-29-movement-collision-conformance.md`,
|
||
`docs/research/2026-04-30-precipice-slide-pseudocode.md`.
|
||
|
||
**Acceptance:** Synthetic and real-DAT tests cover wall-slide, roof-edge slide,
|
||
cliff/precipice slide, failed step-up/step-down, and the jump-clears-edge case.
|
||
|
||
---
|
||
|
||
## #35 — [DONE 2026-04-30] Retail debugger toolchain (cdb + PDB GUID matching)
|
||
|
||
**Status:** DONE
|
||
**Severity:** N/A (infrastructure)
|
||
**Filed + closed:** 2026-04-30
|
||
**Component:** tooling / research
|
||
|
||
**Description:** When the question is "what does retail actually DO at
|
||
runtime?" — wedges, animation flicker, geometry-specific bugs where the
|
||
decomp is correct but the visible behavior is mysterious — there was no
|
||
way to attach a debugger to a live retail acclient.exe and trace it.
|
||
This issue tracks the toolchain that closed that gap.
|
||
|
||
**What shipped:**
|
||
- **`tools/pdb-extract/check_exe_pdb.py`** — reads any PE's CodeView entry
|
||
and reports `MATCH` / `MISMATCH (expected GUID = …)` against our
|
||
`refs/acclient.pdb`. Always run before attaching cdb.
|
||
- **`tools/pdb-extract/dump_pdb_info.py`** — dumps a PDB's expected
|
||
build timestamp + GUID + age. Used to figure out which acclient.exe
|
||
build pairs with our PDB (answer: v11.4186, Sept 2013 EoR).
|
||
- **CLAUDE.md "Retail debugger toolchain" section** — full workflow:
|
||
cdb path, sample `.cdb` script, PowerShell wrapper pattern, watchouts
|
||
(PDB name conventions, `;` parsing, kill-target-on-detach behavior,
|
||
high-hit-rate lag).
|
||
- **Step `-1` added to the development workflow** — "ATTACH cdb TO
|
||
RETAIL (when behavior is the question, not code)". Tells future
|
||
sessions: when guessing has failed twice in a row, don't keep guessing.
|
||
|
||
**Discoveries this toolchain enabled (closed in same session):**
|
||
- Retail integrates physics at 30Hz (`UpdatePhysicsInternal/update_object`
|
||
ratio = 0.61). Drove the L.5 fix in PlayerMovementController.
|
||
- `OBJECTINFO::kill_velocity` rarely fires in normal play (gated on
|
||
last_known_contact_plane_valid). Our acdream port now matches.
|
||
- Retail does NOT wedge on the steep-roof scenario. Confirmed our L.4
|
||
slide-tangent deviation in Path 6 is necessary until the retail
|
||
step_up_slide / cliff_slide chain audit lands.
|
||
|
||
**Files:** `tools/pdb-extract/check_exe_pdb.py`,
|
||
`tools/pdb-extract/dump_pdb_info.py`, `CLAUDE.md`,
|
||
`memory/project_retail_debugger.md`.
|
||
|
||
**Acceptance:** Future sessions can attach cdb to a live retail client
|
||
in under 5 minutes by following the CLAUDE.md workflow.
|
||
|
||
---
|
||
|
||
## #36 — [DONE 2026-05-11 · promoted to Phase C.1.5c] Sky-PES dispatch port (consolidates #2 / #28 / #29 visual gaps)
|
||
|
||
**Status:** DONE (promoted to Phase C.1.5c)
|
||
**Closed:** 2026-05-11
|
||
**Promoted to:** Phase C.1.5c (Sky-PES dispatch chain) — see roadmap `docs/plans/2026-04-11-roadmap.md`
|
||
**Severity:** MEDIUM (aesthetic feature-parity, but addresses a cluster of bugs)
|
||
**Filed:** 2026-04-30
|
||
**Component:** sky / weather / particles
|
||
|
||
**Resolution:** Promoted to a roadmap phase (C.1.5c) — the work is
|
||
multi-commit (decomp dive + persistent-emitter creation + PES timeline
|
||
driver + PES script execution + live-trace verification) and warrants
|
||
a named phase rather than living forever as an "open issue." The
|
||
decomp anchors, live-trace evidence (24,576-frame `GameSky::Draw`
|
||
trace), and 6-step implementation outline in the body below remain
|
||
the authoritative implementation reference; the roadmap phase entry
|
||
is the schedule/scope tracker. **Issues #2 (lightning), #28 (aurora),
|
||
and #29 (cloud thinness) auto-close when C.1.5c ships.**
|
||
|
||
---
|
||
|
||
**Original investigation (kept as implementation reference):**
|
||
|
||
**Description:** Three open sky bugs (#2 lightning, #28 aurora, #29 cloud
|
||
density) all trace back to the same missing infrastructure: retail's
|
||
sky-PES (Particle Effect Script) dispatch chain. We have it now from a
|
||
2026-04-30 cdb live trace.
|
||
|
||
**What retail does (live trace evidence):**
|
||
|
||
```
|
||
Trace over 24,576 GameSky::Draw frames:
|
||
GameSky::Draw = 24,576 (60 Hz render rate)
|
||
GameSky::UseTime = 12,288 (30 Hz — half rate, MinQuantum)
|
||
GameSky::CreateDeletePhysicsObjects = 12,288 (also 30 Hz)
|
||
CPhysicsObj::CallPES = 372 (~150/min average)
|
||
CallPESHook::Execute = 372 (1:1 with CallPES)
|
||
CreateParticleHook::Execute = 62 (15 at cell load + 47 burst at transition)
|
||
CPhysicsObj::create_particle_emitter = 62 (matches CreateParticleHook)
|
||
```
|
||
|
||
**Three findings:**
|
||
1. Retail has **persistent particle emitters** on celestial / sky objects.
|
||
Created at cell load (15 initial) and dynamically as conditions change
|
||
(the trace caught a +47 burst on a region/weather/time transition).
|
||
2. The PES script-hook system (`CallPESHook::Execute` →
|
||
`CPhysicsObj::CallPES`) drives those emitters periodically, ~150
|
||
times per minute on average.
|
||
3. Earlier research said "GameSky doesn't read pes_id" — correct in
|
||
scope, but missed that the dispatch chain runs through the script-
|
||
hook system, not from inside GameSky directly. Cell/region/weather
|
||
handlers schedule PES script hooks; those hooks call into CallPES.
|
||
|
||
**Decomp anchors:**
|
||
- `CallPESHook::Execute` @ `0x00526e20` — script-hook action that fires CallPES
|
||
- `CreateParticleHook::Execute` @ `0x00526ec0` — particle-creation hook
|
||
- `CPhysicsObj::CallPES` @ `0x00511af0`
|
||
- `CPhysicsObj::create_particle_emitter` @ `0x0050f360`
|
||
- `GameSky::CreateDeletePhysicsObjects` @ `0x005073c0`
|
||
- `LongNIHash<ParticleEmitter>` instance — emitter registry
|
||
- `CelestialPosition.pes_id` @ struct offset +0x004 — populated by
|
||
`SkyDesc::GetSky` but consumed downstream of `GameSky` (via the
|
||
hook system, not GameSky itself)
|
||
|
||
**Implementation outline:**
|
||
1. Decomp dive: read `CallPESHook::Execute`, `CreateParticleHook::Execute`,
|
||
`CPhysicsObj::CallPES`, and `GameSky::CreateDeletePhysicsObjects`
|
||
(and any cell/region weather handlers that spawn the dynamic 47).
|
||
2. Identify what triggers `CreateParticleHook` for sky objects — is it
|
||
inside `CreateDeletePhysicsObjects`, the region/weather change handler,
|
||
or somewhere else?
|
||
3. Port the persistent-emitter creation path: when a cell loads or
|
||
weather/time changes, instantiate the appropriate ParticleEmitters
|
||
on celestial objects.
|
||
4. Port the PES timeline driver — periodic dispatch from a script
|
||
timeline into our equivalent `CallPES`.
|
||
5. Port the actual PES script execution (rate of emission, particle
|
||
parameters, etc.) into our particle system.
|
||
6. Live verify with cdb during specific weather windows: aurora at dusk
|
||
on Rainy DayGroup, lightning during storm.
|
||
|
||
**Files** (likely):
|
||
- `src/AcDream.App/Rendering/Sky/SkyRenderer.cs` — emitter wiring
|
||
- `src/AcDream.Core/World/SkyDescLoader.cs` — already parses pes_id
|
||
- `src/AcDream.Core/Particles/*` — particle system foundation
|
||
- `src/AcDream.App/Rendering/ParticleRenderer.cs` — visual layer
|
||
|
||
**Live-trace verification plan (next cdb session):** Reattach to retail
|
||
during a specific aurora moment, log `this` pointer + `pes_id` arg on
|
||
every `CallPES` invocation, log the GfxObj being attached on every
|
||
`create_particle_emitter`. That tells us EXACTLY which celestial
|
||
objects retail PES-drives and with which IDs.
|
||
|
||
**Acceptance:** During the same in-game time/weather where retail shows
|
||
aurora-style light play (Rainy DayGroup, dusk/dawn windows), acdream
|
||
shows comparable colored sky effects. Cloud sheets look as dense /
|
||
purple as retail. Lightning flashes appear during storm windows.
|
||
|
||
**Closes-when-done:** #28, #29, partially #2 (lightning may need
|
||
additional flash-shader work).
|
||
|
||
---
|
||
|
||
## #33 — Live entity collision shape collapses to one cylinder
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM
|
||
**Filed:** 2026-04-29
|
||
**Component:** physics / entities
|
||
|
||
**Description:** Live world entities do not yet use exact retail
|
||
`CSphere` / `CCylSphere` shape semantics. Several paths collapse the entity to
|
||
a simplified root-centered cylinder or fallback radius, which is not enough for
|
||
retail object and creature collision parity.
|
||
|
||
**Root cause / status:** Tracked under Phase L.2d. Requires auditing object
|
||
shape extraction, `Setup.Radius` fallback, building object identity, and live
|
||
entity broadphase records against named retail.
|
||
|
||
**Files:** `src/AcDream.Core/Physics/CollisionPrimitives.cs`,
|
||
`src/AcDream.Core/Physics/ShadowObjectRegistry.cs`,
|
||
`src/AcDream.Core/Physics/PhysicsDataCache.cs`.
|
||
|
||
**Research:** `docs/plans/2026-04-29-movement-collision-conformance.md`.
|
||
|
||
**Acceptance:** Live object collision uses the appropriate retail sphere or
|
||
cylsphere data where available. Tests prove at least one multi-shape object and
|
||
one live creature case no longer use the single-cylinder fallback.
|
||
|
||
---
|
||
|
||
|
||
## #2 — Lightning visual mismatch (sky PES path disproved)
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM
|
||
**Filed:** 2026-04-25
|
||
**Component:** weather / sky / vfx
|
||
|
||
**Description:** Lightning/storm sky visuals still do not match retail. A 2026-04-28 named-retail recheck disproved the prior assumption that `SkyObject.PesObjectId` drives sky-render flash particles: `SkyDesc::GetSky` copies the field into `CelestialPosition.pes_id`, but `GameSky::CreateDeletePhysicsObjects`, `GameSky::MakeObject`, and `GameSky::UseTime` never read it.
|
||
|
||
**Root cause / status:** Open again. The sky-PES path is non-retail and must stay disabled for normal rendering. The remaining mismatch likely lives in the sky/weather mesh material path, the lightning/fog flash path, or another weather subsystem outside `GameSky`; do not reintroduce per-SkyObject PES playback without new decompile evidence.
|
||
|
||
**Files:**
|
||
- `src/AcDream.App/Rendering/Sky/SkyRenderer.cs` — sky/weather mesh draw, material state, pre/post split
|
||
- `src/AcDream.App/Rendering/Shaders/sky.frag` — flash/fog/lightning coloration path
|
||
- `src/AcDream.Core/World/SkyDescLoader.cs` — keep `PesObjectId` parsed for diagnostics, not render playback
|
||
|
||
**Research:**
|
||
- `docs/research/2026-04-28-pes-pseudocode.md` — C.1 correction: `CelestialPosition.pes_id` copied but ignored by GameSky
|
||
- `docs/research/2026-04-23-sky-pes-wiring.md` — earlier decompile trace reached the same no-sky-PES conclusion
|
||
- `docs/research/2026-04-23-lightning-real.md` (decompile trace + dat discovery)
|
||
- `docs/research/2026-04-23-physicsscript.md` (runtime semantics)
|
||
- `docs/research/2026-04-23-lightning-crossfade.md` (crossfade mechanism)
|
||
|
||
**Acceptance:** During a Rainy DayGroup's storm window, visible flashes appear in the sky at the dat-scripted moments, the fragment-shader flash bump briefly brightens the scene, and (later, once thunder audio is wired) a thunder clap plays with a short propagation delay.
|
||
|
||
**See also #36** (Sky-PES dispatch port) — the lightning visuals likely route through the same PES-hook chain that drives aurora and cloud-density. Most of #2's storm-flash visuals will be unblocked by the #36 port.
|
||
|
||
---
|
||
|
||
## #3 — Client clock drifts from retail after ~10 minutes (periodic TimeSync missing)
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM
|
||
**Filed:** 2026-04-25
|
||
**Component:** net / sky
|
||
**Chore tag:** Single-commit fix — well-scoped ~10-line wiring. `WorldTimeService.SyncFromServer(double)` already exists; just needs `WorldSession` to detect header-flag `0x1000000` and call it. Pickup at any opportunistic session.
|
||
|
||
**Description:** Our `WorldTimeService.DayFraction` syncs with the server once at login via `ConnectRequest + TimeSync`, then advances from the local wall-clock. Retail receives periodic `TimeSync` refreshes (header flag `0x1000000`) carrying a fresh `PortalYearTicks double` and re-anchors its clock. Without those, acdream's keyframe state drifts from retail's over 10+ minutes — observed during the 2026-04-24 sky-color debug sessions where retail was at DayFraction 0.976 while acdream was at 0.634.
|
||
|
||
**Root cause / status:** Mechanism is well-understood (see research). `WorldTimeService.SyncFromServer(double)` already exists — we just need to detect the periodic flag in the packet header and call it whenever a fresh tick arrives.
|
||
|
||
**Files:**
|
||
- `src/AcDream.Core.Net/WorldSession.cs` — header-flag parsing; currently only the initial sync is consumed
|
||
- `src/AcDream.Core/World/WorldTimeService.cs` — `SyncFromServer(double ticks)` ready; needs caller wiring
|
||
|
||
**Research:** `docs/research/deepdives/r12-weather-daynight.md` §TimeSync (line ~563). References retail packet-header flag `0x1000000` carrying `PortalYearTicks double`.
|
||
|
||
**Acceptance:** Probe retail via `tools/RetailTimeProbe` and acdream's ACDREAM_DUMP_SKY log at the same wall-clock moment after a 20-minute session without re-login; `abs(acdream.DayFraction - retail.DayFraction) < 0.01`.
|
||
|
||
---
|
||
|
||
|
||
---
|
||
|
||
## #4 — Sky horizon-glow disabled (fog-mix skipped on sky meshes)
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (aesthetic feature-parity, not regression from pre-session state)
|
||
**Filed:** 2026-04-25
|
||
**Component:** sky
|
||
|
||
**Description:** Phase 8.1 (commit `593b76f`) disabled the fog-mix on sky meshes to fix the "entire dome swallowed by fog color" regression. Dereth's keyframe `FogEnd` values (0–2400 m) are calibrated for terrain; sky meshes are authored at radii 1050–14271 m so every sky pixel was past `FogEnd`, saturated to `uFogColor`, destroying stars / moon / dome texture. Disabling the mix restored visibility but we lost retail's horizon-glow effect (gradient from clear zenith to fog-tinted horizon band at dusk/dawn).
|
||
|
||
**Root cause / status:** Three competing hypotheses, none pinned down: (a) retail uses a **different** fog range for sky than terrain; (b) retail applies fog with an **elevation-angle** weighting rather than linear distance; (c) retail's sky meshes **don't participate** in the global fog and the "horizon glow" comes from a different atmospheric-scatter path. Need to identify retail's actual sky-fog behaviour before re-enabling with correct parameters.
|
||
|
||
**Not in the Phase C.1.5c (Sky-PES) cluster.** Unlike #2/#28/#29 — all PES-driven sky visuals consolidated under the C.1.5c phase via former issue #36 — this is a fragment-shader fog-mix problem. Addressing C.1.5c will NOT resolve #4, and #4 should NOT be bundled into Phase C.1.5c scope. The fix likely needs its own decomp dive into retail's sky-fog math + shader work.
|
||
|
||
**Files:**
|
||
- `src/AcDream.App/Rendering/Shaders/sky.frag` — line ~55, `rgb = mix(uFogColor.rgb, rgb, vFogFactor)` currently commented out
|
||
- `src/AcDream.App/Rendering/Shaders/sky.vert` — lines 109-114, `vFogFactor` computation
|
||
|
||
**Research:** `docs/research/2026-04-23-sky-fog.md`. Partial; doesn't pin the sky-specific fog path.
|
||
|
||
**Acceptance:** At dusk in Holtburg, the sky dome shows a clear zenith and a warm fog-tinted horizon band that matches retail's appearance, with stars / moon / sun / clouds all still visible at their correct brightnesses elsewhere in the frame.
|
||
|
||
---
|
||
|
||
## #28 — Aurora ("northern lights") effect not rendered
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (aesthetic feature-parity)
|
||
**Filed:** 2026-04-26
|
||
**Component:** sky / vfx
|
||
|
||
**Description:** Retail renders a dynamic colored "light play" effect in the sky during certain Rainy/Cloudy DayGroup time windows. The user describes it as aurora-borealis-style. acdream renders no comparable effect.
|
||
|
||
**Root cause / status:** Open again. The prior root cause was wrong: `CelestialPosition.pes_id` exists in the retail header and is populated by `SkyDesc::GetSky`, but named retail `GameSky` code does not read it during sky object creation, update, or draw. A 2026-04-28 C.1 experiment that played those PES ids produced colored blobs/wash that did not match retail's broad aurora-like rays, and the path is now debug-only behind `ACDREAM_ENABLE_SKY_PES=1`.
|
||
|
||
Retail header at `acclient.h` line 35451 still documents the copied field:
|
||
|
||
```c
|
||
struct CelestialPosition {
|
||
IDClass<...> gfx_id;
|
||
IDClass<...> pes_id; // ← particle scheduler ID
|
||
float heading; float rotation;
|
||
Vector3 tex_velocity;
|
||
float transparent; float luminosity; float max_bright;
|
||
unsigned int properties;
|
||
};
|
||
```
|
||
|
||
`StarsProbe` confirmed Dereth Rainy DayGroup 3 carries multiple PES-bearing entries (verified 2026-04-27). Sample for the user's observed Warmtide-Rainy state:
|
||
|
||
| OI | Gfx | **PES** | Active window | Notes |
|
||
|----|-----|---------|----|----|
|
||
| 5 | 0x02000714 | 0x330007DB | always | low-rate background |
|
||
| 7 | 0x02000BA6 | 0x33000453 | 0.03–0.19 | early morning |
|
||
| 17 | 0x02000589 | **0x3300042C** | **0.27–0.91** | **active during user's screenshot** |
|
||
|
||
acdream's geometry half is now wired (commit landing 2026-04-27 — `EnsureSetupUploaded` walks `Setup.Parts` for `0x020xxx` IDs). The remaining dynamic visual half is not `SkyObject.PesObjectId`; likely suspects are sky/weather mesh material state, texture transform/blending, or a separate weather/lightning subsystem outside `GameSky`.
|
||
|
||
**Implementation outline:**
|
||
1. Keep `SkyObject.PesObjectId` parsed for diagnostics only.
|
||
2. Compare retail/acdream material state for the active sky/weather GfxObj/Setup ids (`0x02000588`, `0x02000589`, `0x02000714`, `0x02000BA6`).
|
||
3. Trace the named retail sky/weather draw path for texture transforms, translucency, diffusion, luminosity, and any non-GameSky weather effect dispatch.
|
||
4. Only add a new runtime visual path once the decompile has an actual caller.
|
||
|
||
**Decomp pointers:**
|
||
- `SkyDesc::GetSky` named retail `0x00501ec0` — copies `SkyObject.default_pes_object` into `CelestialPosition.pes_id`.
|
||
- `GameSky::CreateDeletePhysicsObjects` named retail `0x005073c0` — creates/updates sky objects from `gfx_id`, does not read `pes_id`.
|
||
- `GameSky::MakeObject` named retail `0x00506ee0` — calls `CPhysicsObj::makeObject(gfx_id, 0, 0)`, no PES.
|
||
- `GameSky::UseTime` named retail `0x005075b0` — updates frame/luminosity/diffusion/translucency, no PES.
|
||
|
||
**Files:**
|
||
- `src/AcDream.Core/World/SkyDescLoader.cs` — carries `PesObjectId` for diagnostics.
|
||
- `src/AcDream.App/Rendering/Sky/SkyRenderer.cs` — likely material/texture-transform parity work.
|
||
- `src/AcDream.App/Rendering/GameWindow.cs` — sky-PES playback remains debug-only, disabled by default.
|
||
|
||
**Acceptance:** When retail shows aurora-style light play at a specific in-game time / weather, acdream shows a visually-comparable effect at the same time.
|
||
|
||
**See #36 (filed 2026-04-30)** — a live cdb trace confirmed retail's aurora rendering uses the script-hook PES dispatch chain (`CallPESHook::Execute` → `CPhysicsObj::CallPES`) on persistent particle emitters, with a cell-load population (15 initial emitters) plus dynamic spawning on region/weather/time transitions (caught a +47 burst). Implementation work consolidated under #36.
|
||
|
||
---
|
||
|
||
## #29 — Cloud surface 0x08000023 still appears thinner than retail despite blend-mode + Setup fixes
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (aesthetic feature-parity)
|
||
**Filed:** 2026-04-27
|
||
**Component:** sky / clouds
|
||
|
||
**Description:** User screenshot comparison showed acdream's clouds let too much sun through; retail's are denser and have a purpleish tint. Two follow-up fixes landed without visible improvement:
|
||
|
||
1. `TranslucencyKindExtensions.FromSurfaceType` now applies retail's Translucent-override at `D3DPolyRender::SetSurface` (decomp 425246-425260) — surface `0x08000023` (Type=`0x10114` = `B1ClipMap | Translucent | Alpha | Additive`) is now correctly classified as `AlphaBlend` instead of `Additive`.
|
||
2. `SkyRenderer.EnsureSetupUploaded` now loads `0x020xxxxx` Setup IDs (e.g. `0x02000588`, `0x02000589`, `0x02000714`, `0x02000BA6`) which were silently dropped. Setup parts are flattened via `SetupMesh.Flatten` and uploaded with their per-part transform baked into vertex positions.
|
||
|
||
Despite both being decomp-correct fixes, the user reports no observable visual change in dual-client comparison. Two follow-up hypotheses:
|
||
|
||
- The Setup objects are tiny placeholder meshes (one `0x010001EC` part each) that exist mainly to anchor a PES emitter — the cloud "density" / "purple sheen" the user perceives is entirely the PES particle layer, not the static mesh.
|
||
- The cloud surface might still be rendering correctly per its dat data, and what looks "thicker" in retail is the additional aurora-like PES sheen overlaid on top.
|
||
|
||
If hypothesis (a) is correct, this issue effectively rolls into **#28** — the PES rendering work would resolve both.
|
||
|
||
**Files:**
|
||
- `src/AcDream.Core/Meshing/TranslucencyKind.cs` — Translucent override
|
||
- `src/AcDream.App/Rendering/Sky/SkyRenderer.cs` — `EnsureSetupUploaded`
|
||
|
||
**Acceptance:** Cloud sheets look as dense/purple as retail in dual-client side-by-side. May require #28 (PES) to land first.
|
||
|
||
**See #36 (filed 2026-04-30)** — confirmed via live cdb trace: retail's cloud density comes from the same PES-driven particle-emitter chain as aurora. Implementation consolidated there.
|
||
|
||
---
|
||
|
||
## #47 — [DONE 2026-05-06 · 0bd9b96] Humanoid Setup 0x02000001 renders bulky / lacks shape detail vs retail
|
||
|
||
**Status:** DONE
|
||
**Closed:** 2026-05-06
|
||
**Commit:** `0bd9b96`
|
||
**Severity:** MEDIUM (cosmetic — characters readable but visibly different from retail)
|
||
**Filed:** 2026-05-06
|
||
**Component:** rendering / mesh / character animation
|
||
|
||
**Resolution:** Root cause was that we drew the base GfxObj id from
|
||
Setup / `AnimPartChange` directly. Retail's `CPhysicsPart::LoadGfxObjArray`
|
||
(`0x0050DCF0`) treats that base id as an **entry point to the
|
||
`DIDDegrade` table**; for close/player rendering it draws
|
||
`Degrades[0].Id`, which is the higher-detail mesh that carries the
|
||
bicep / deltoid / shoulder geometry. ACViewer also has this bug —
|
||
that was the key signal it wasn't acdream-specific.
|
||
|
||
Concrete swaps the resolver now performs:
|
||
- Aluvian Male upper arm `0x01000055` → `0x01001795` (14/17 → 32/60 verts/polys)
|
||
- Aluvian Male lower arm `0x01000056` → `0x0100178F`
|
||
- Heritage variants: `0x010004BF → 0x010017A8`, `0x010004BD → 0x010017A7`,
|
||
`0x010004B7 → 0x0100179A`, etc.
|
||
|
||
Fix landed as `GfxObjDegradeResolver`, default-on and scoped to humanoid
|
||
setups (34-part with ≥8 null-sentinel attachment slots). Set
|
||
`ACDREAM_RETAIL_CLOSE_DEGRADES=0` only for diagnostic before/after
|
||
comparisons. User confirmed visually 2026-05-06.
|
||
|
||
Files: `src/AcDream.Core/Meshing/GfxObjDegradeResolver.cs`,
|
||
`src/AcDream.App/Rendering/GameWindow.cs` (wiring), 5 unit tests in
|
||
`tests/AcDream.Core.Tests/Meshing/GfxObjDegradeResolverTests.cs`.
|
||
Research note: `docs/research/2026-05-06-issue-47-close-degrade-pseudocode.md`.
|
||
|
||
---
|
||
|
||
### Original investigation (kept for reference)
|
||
|
||
**Description:** Every humanoid character using Setup `0x02000001`
|
||
(Aluvian Male) renders in acdream with a "bulky, less-defined" silhouette
|
||
compared to retail's view of the same character. Specifically: shoulders
|
||
look smoother/rounder where retail has pointier shoulder pads; back has
|
||
less contour; arms appear puffier. The effect is identical for player
|
||
characters (`+Acdream`, `+Je`) and for humanoid NPCs using the same
|
||
setup (e.g. Woodsman, Sedor Wystan the Blacksmith, Thelnoth Cort).
|
||
Drudges and other monster setups (e.g. `0x020007DD`) render
|
||
identically to retail, so this is *not* a pipeline-wide bug.
|
||
|
||
The bug is independent of equipment — `+Je` stripped naked still
|
||
shows the same bulky silhouette.
|
||
|
||
**Investigation 2026-05-06 (~3 hr session, ruled out many hypotheses):**
|
||
|
||
What was ruled out:
|
||
|
||
- **0xF625 ObjDescEvent appearance updates being dropped.** Was a real
|
||
bug for skin/hair colors; fixed in commit e471527. Does not affect
|
||
the bulky-shape issue (which persists with the fix in place and
|
||
with no equipment).
|
||
- **Position-pop on equip toggle.** Caused by re-applying with cached
|
||
spawn's stale position; fixed in same commit. Doesn't affect shape.
|
||
- **Clothing/armor overlapping the base body** (HiddenParts hypothesis).
|
||
User stripped naked; bulky shape persists.
|
||
- **ParentIndex hierarchy not walked in `SetupMesh.Flatten`.** Setup
|
||
`0x02000001` has a real hierarchy (`-1, -1, 1, 2, 3, -1, 5, 6, 7, 0,
|
||
9, 10, 11, 12, 13, 14, 15, 0, ...`), but implementing parent-walk
|
||
produced **no visible change** — confirming AC's idle animation
|
||
frames are already in setup-root coordinates, not parent-local.
|
||
- **Equipment / wielded items.** No equipment on `+Je` and bug persists.
|
||
- **Player-specific data flow.** Humanoid NPCs using same setup
|
||
(Woodsman) show same bug.
|
||
|
||
What was confirmed (data captured via `ACDREAM_DUMP_CLOTHING=1`):
|
||
|
||
- Setup `0x02000001`: `setup.Parts.Count = 34`, `flatten.Count = 34`,
|
||
`APC = 34..38` depending on equipment.
|
||
- All 34 parts emit triangles successfully (no silent GfxObj load
|
||
failures). Total ~648-700 tris per character.
|
||
- Idle animation frames place parts at sensible humanoid Z-heights
|
||
(head Z=1.587, mid-body Z=0.5-1.0, ground Z=0.085).
|
||
- Per-part orientations are nearly all 180° around -Z (W≈0,
|
||
Z≈-1) — a setup-wide coordinate-flip convention. Drudges have
|
||
varied per-part orientations.
|
||
- `setup.DefaultScale.Count = 0` for both humans and drudges → all
|
||
parts use Vector3.One scale.
|
||
|
||
**Working hypotheses (next session):**
|
||
|
||
1. **Per-vertex normal style.** AC dat may store per-face normals
|
||
for human GfxObjs (one normal per polygon, copied to all 3
|
||
vertices) but smooth normals for monster GfxObjs. acdream uses
|
||
dat normals directly. Test by computing smooth normals from face
|
||
adjacency and comparing render. User said "not shaders" but the
|
||
screenshots clearly show smooth-vs-faceted lighting differences.
|
||
2. **Lighting setup.** Cell ambient may be too low, leaving back-
|
||
facing surfaces in flat shadow. Compare `uCellAmbient` value
|
||
against retail's behaviour at the same time-of-day.
|
||
3. **Anti-aliasing.** Retail may use MSAA; acdream window may not.
|
||
Polygon edges in acdream would be visibly stair-stepped, reading
|
||
as "more faceted" / blockier.
|
||
4. **Surface flags interpretation.** Specific Surface.Type bits for
|
||
character textures (skin, fabric) may need handling acdream
|
||
doesn't yet do (e.g. `SmoothShade` flag, or a mip bias).
|
||
|
||
**Diagnostic infrastructure landed this session** (env-var-gated, no
|
||
runtime cost when off):
|
||
|
||
- `ACDREAM_DUMP_CLOTHING=1` extended:
|
||
- `setup.Parts.Count`, `flatten.Count`, `APC` count on header line
|
||
- `ParentIndex[]` array dump
|
||
- `DefaultScale[]` array dump
|
||
- `IdleFrame.Frames[]` per-part Origin + Orientation (first 17 parts)
|
||
- `EMIT part=NN gfx=0xXX subMeshes=N tris=N` per part
|
||
- `TOTAL tris=N meshRefs=N` per entity
|
||
|
||
**Files (suspect surface area for next investigation):**
|
||
|
||
- `src/AcDream.Core/Meshing/SetupMesh.cs` — Flatten composition
|
||
- `src/AcDream.Core/Meshing/GfxObjMesh.cs` — polygon emission +
|
||
vertex normal handling (line 142)
|
||
- `src/AcDream.App/Rendering/Shaders/mesh.frag` — lighting eq
|
||
- `src/AcDream.App/Rendering/Shaders/mesh.vert` — normal transform
|
||
|
||
**Acceptance:** Side-by-side screenshots of `+Acdream` (or any humanoid
|
||
NPC using `0x02000001`) viewed from the same angle in acdream and
|
||
retail show matching silhouette and shape definition.
|
||
|
||
---
|
||
|
||
## #46 — Retail observer of acdream sees blippy / laggy movement
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM (degrades external perception of acdream-driven characters)
|
||
**Filed:** 2026-05-06
|
||
**Component:** net / motion (acdream's outbound path: `PlayerMovementController` → `MoveToState` (0xF61C) / `AutonomousPosition` heartbeat → ACE → retail observer)
|
||
**Phase:** L.2 (Movement & Collision Conformance) — outbound-motion fidelity sub-piece. Counterpart to #41 (which is the inbound side); both are L.2 conformance work. If outbound fidelity grows into multi-commit work, consider carving "L.2e — Outbound motion fidelity" as a named sub-piece on the roadmap.
|
||
|
||
**Description:** When viewing acdream's local +Acdream character through a parallel retail acclient.exe, the retail observer sees the character's movement as visibly blippy and laggy — position appears to step in discrete jumps rather than translating smoothly. The local acdream view of the same character looks fine, and acdream observing a retail-driven character (after #39 / #45) also looks fine. The degradation is specifically on the **outbound** side: what acdream sends to ACE for relay to other clients.
|
||
|
||
**Root cause / status:**
|
||
|
||
Unverified. The likely culprits, ranked by suspected probability:
|
||
|
||
1. **AutonomousPosition heartbeat cadence.** `memory/project_retail_motion_outbound.md` notes acdream's fixed 200 ms heartbeat is a probable retail mismatch. Retail's `CommandInterpreter::SendPositionEvent` gates on transient_state (Contact + OnWalkable + valid Position) and may broadcast at a different cadence — fewer / more / variable. If acdream sends too rarely, observer dead-reckons too long between updates and visibly stutters when each AutoPos arrives.
|
||
2. **MoveToState send conditions.** `PlayerMovementController.cs:813-840` decides when a fresh MoveToState fires (state-change detection). If important transitions are missed (e.g., direction changes that don't flip ForwardCommand/SidestepCommand), the observer's last-known motion stays stale and AutoPos updates blip the body to the new authoritative position.
|
||
3. **InstanceSequence / ObjectMovement sequence counters.** ACE rejects out-of-order packets. If acdream's sequence stamping is off, ACE silently drops some packets; observer dead-reckons through the gap.
|
||
4. **Velocity field absent on AutoPos.** ACE relays UPs without HasVelocity for player characters (per `OnLivePositionUpdated` comment). Observer's dead-reckoning between UPs may extrapolate using stale velocity, producing visible position drift that snaps back on the next UP — exactly the blippy pattern.
|
||
|
||
**Verification approach:**
|
||
|
||
- Run two retail clients + one acdream client. Drive acdream; observe acdream's character on retail #1 and on retail #2 (both retail observers see the same wire). Compare to a retail-driven character observed from the same retail clients — does it look smooth there? If yes, the issue is acdream-outbound-specific. If both look blippy, it's something on the ACE side (less likely).
|
||
- cdb-attach a retail observer client and breakpoint `MovementManager::unpack_movement` to count UPs and UMs received per second from the acdream-driven character vs from another retail character. The cadence delta will identify which packet stream is misbehaving.
|
||
- Compare acdream's outbound packet timing against holtburger's `client/movement/system.rs` heartbeat logic — that's the closest known-working reference for how a non-retail client should pace its outbound.
|
||
|
||
**Files:**
|
||
|
||
- `src/AcDream.App/Input/PlayerMovementController.cs` — outbound state-change detection + heartbeat
|
||
- `src/AcDream.Core.Net/WorldSession.cs` — sequence counters + send path
|
||
- `src/AcDream.Core.Net/Net/Outbound/...MoveToState.cs` / `AutonomousPosition.cs` — wire builders
|
||
- `references/holtburger/crates/holtburger-core/src/client/movement/system.rs` — reference cadence
|
||
|
||
**Acceptance:**
|
||
|
||
- Side-by-side comparison: retail observer of acdream-driven character and retail observer of retail-driven character look equally smooth during running, walking, sidestepping, turning, and stopping.
|
||
- No visible "step" pattern when acdream-driven character translates between AutoPos updates.
|
||
|
||
**Cross-reference:**
|
||
|
||
- `memory/project_retail_motion_outbound.md` — 2026-05-01 cdb live trace of retail's outbound (`CommandInterpreter::SendMovementEvent` for WASD, `Event_Jump` per-frame while charging).
|
||
- CLAUDE.md "Outbound motion wire format" — the `WalkForward + HoldKey.Run` ↔ `RunForward` auto-upgrade ACE applies on broadcast.
|
||
|
||
---
|
||
|
||
## #45 — [DONE 2026-05-06 · e9e080d] Local +Acdream sidestep walking renders too slow
|
||
|
||
**Status:** DONE
|
||
**Closed:** 2026-05-06
|
||
**Commit:** `e9e080d`
|
||
**Component:** physics / animation (local player path: `UpdatePlayerAnimation`)
|
||
|
||
**Resolution:** `PlayerMovementController.cs:871` computes `localAnimSpeed` as raw `runRate || 1.0`, but ACE's `BroadcastMovement` converts the inbound `MoveToState.SidestepSpeed` via `speed × 3.12 / 1.25 × 0.5` (`Network/Motion/MovementData.cs:124-131`). Observer-side cycles play at the ACE-scaled value (~1.248 slow / ~3.0 fast clamped); the local cycle was playing at the raw 1.0 / runRate — about 80% of retail cadence for slow strafe.
|
||
|
||
`UpdatePlayerAnimation` now multiplies `animSpeed` by `WalkAnimSpeed / SidestepAnimSpeed × 0.5 = 1.248` when `animCommand` is `SideStepLeft / Right` (low byte 0x0F or 0x10). User-verified: local strafe cadence matches retail / observer-side rendering.
|
||
|
||
**Original investigation note (preserved):** Same constant mismatch pattern as #39 fix #5 (commit `349ba65`) but on the local-player render path instead of the observer-side `ApplyPlayerLocomotionRefinement` — both fixed by aligning the speedMod base to ACE's wire formula.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## #108 — Cellar↔main-floor transition: terrain (grass) sweeps across the upstairs door opening — [CLOSED 2026-06-12 · user-gated]
|
||
|
||
**Status:** CLOSED — user visual gate 2026-06-12 ("Yes it is fixed.")
|
||
after the terrain-backface-cull fix (`96a425a`). Root cause: terrain
|
||
drew double-sided; the grass was the grade sheet's underside seen from
|
||
a below-grade cellar eye. Membership/viewer EXONERATED by the vertical
|
||
cellar-ascent harness (`007af13`).
|
||
|
||
**ROOT CAUSE (2026-06-12): terrain was drawn DOUBLE-SIDED — the grass was
|
||
the UNDERSIDE of the grade sheet.** Two steps:
|
||
1. The membership/viewer re-diagnosis below is **REFUTED** by the vertical
|
||
cellar-ascent harness (`Issue108CellarAscentViewerReplayTests`, dat-backed
|
||
A9B4 corner-building cellar 0x0174→0x0175→0x0171, production
|
||
FindCellList pick + the camera probe chain mirrored verbatim): 0
|
||
outdoor/null viewer resolutions while the eye is below grade, 0 sweep
|
||
failures, 0 fallback branches across boom distance {2.61, 5} × damping
|
||
lag {0, 0.3}. The viewer enters 0x0171 at eye z 94.01 — exactly as the
|
||
head pops above grade (the stairwell portal sits at grade), matching the
|
||
user's wording. The root is INTERIOR the whole window.
|
||
2. Retail terrain is SINGLE-SIDED: `ACRender::landPolysDraw` (0x006b7040)
|
||
draws each land triangle ONLY when the camera is on the POSITIVE (upper)
|
||
side of its plane (`Plane::which_side2` vs `Render::FrameCurrent`). A
|
||
below-grade eye gets NO terrain — through the door retail shows sky.
|
||
WB renders the world with face culling DISABLED frame-globally (WB
|
||
`GameScene.cs:841` — editor heritage), and `TerrainModernRenderer.Draw`
|
||
set no cull state of its own → terrain drew double-sided. From a
|
||
below-grade eye every aperture sight-ray RISES, so the only "terrain" it
|
||
can see is the underside of the z≈94 grade sheet — which painted the
|
||
whole exit-door aperture (the landscape slice's 2D NDC clip planes
|
||
`(nx,ny,0,dw)` have no depth axis and cannot exclude it) and slid down
|
||
off the door exactly as the eye crossed grade.
|
||
**Fix: port the landPolysDraw eye-side gate as terrain backface culling**
|
||
— `TerrainModernRenderer.Draw` now owns Enable(CullFace) + Cull(Back) +
|
||
FrontFace(Ccw) (set→draw→restore; 7th instance of the self-contained-GL-
|
||
state rule). Pins: `LandblockMeshTests.Build_AllTriangles_WindCounter-
|
||
ClockwiseInWorldXY` (every emitted triangle CCW in world XY — cull-safe
|
||
winding) + `TerrainCullOrientationTests` (above-eye ⇒ CCW window winding
|
||
kept / below-eye ⇒ CW culled under the production camera convention).
|
||
**Gate:** climb out of the corner-building cellar — the grass window over
|
||
the exit door must be gone (sky/world through the door instead); plus a
|
||
general outdoor sanity glance (terrain intact from above — a wrong
|
||
FrontFace would blank it).
|
||
**Severity:** MEDIUM
|
||
**Component:** render / terrain (single-sidedness) — membership/viewer EXONERATED
|
||
|
||
During the cellar→main-floor ascent (Holtburg), the door opening visible on the main floor
|
||
shows the outdoor GRASS texture sweeping over it — "like outdoor ground rising up from the
|
||
floor to cover it (as if watching it from below) and lowering back down when crossing up"
|
||
(user gate, 2026-06-10, post-`dac8f6a`).
|
||
|
||
**ROOT CAUSE FOUND (BR-2 visual gate, 2026-06-11):** this is NOT a render depth bug — it
|
||
is a MEMBERSHIP flip. The BR-2 far-Z punch (wired for OUTDOOR roots + look-in ONLY)
|
||
suppressed #108 when wired and #108 returned when reverted; since the punch never runs on a
|
||
clean interior frame, the grass-sweep frames must render through the **outdoor root**, i.e.
|
||
**the player is being classified OUTDOOR mid-cellar** (the #112/#106 cellar membership
|
||
ping-pong family). The outdoor root then draws the landscape, whose terrain crosses the
|
||
doorway region as the eye rises. The punch was MASKING it — and harmfully (it erased the
|
||
depth of dynamic objects standing in doorways, so characters went transparent by their
|
||
overlap with the opening; reverted `88be519`). **Fix belongs in the membership track:**
|
||
stop the cellar-transition root from flipping to outdoor (render is downstream of
|
||
membership). The genuine interior-root exit-door depth seal (retail
|
||
`DrawPortalPolyInternal` maxZ2, kept reserved in `PortalDepthMaskRenderer.cs`) is a
|
||
separate real mechanism to rebuild under BR-3 — it does NOT fix #108.
|
||
|
||
---
|
||
|
||
## #109 — Exit door across the room oscillates between door texture and background color — [DONE 2026-06-11 · T5 gate]
|
||
|
||
**Status:** DONE — user-confirmed at the T5 comprehensive gate ("7. No."
|
||
— the far exit door no longer oscillates). Closed by the holistic-port
|
||
stack (T1 dynamics-last frame order + depth discipline + T2 flood
|
||
fidelity). No isolated fix commit — the discipline retired the class.
|
||
**Severity:** MEDIUM
|
||
**Component:** render / indoor PView (exit-portal region vs door entity draw order)
|
||
|
||
In a Holtburg house with a second exterior door: standing inside and looking at the OTHER
|
||
exit door across the room, the door surface oscillates between its real texture and the
|
||
background color, "almost like a mix of both" (user gate, 2026-06-10, post-`dac8f6a`).
|
||
Suspect family: the per-frame interaction between the exit-portal OutsideView slice for
|
||
that doorway, the doorway depth-clear (`ClearDepthSlice`), and the door ENTITY's draw —
|
||
alternating which wins per frame. Distinct from the (fixed) flood strobe: the flood is
|
||
stable now; this is a draw-order/depth oscillation localized to the door surface.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## #112 — A9B3 hill cottage: containment gap inside the house demotes to outdoor with no re-promotion (transparent interior while walking)
|
||
|
||
**Status:** CLOSED 2026-06-12 (`be03146`) — user gate "OK seems to work"
|
||
after run-speed in/out cycles; live capture shows threshold promotions +
|
||
room tracking + clean exits, zero errors.
|
||
|
||
**ROOT CAUSE (instrumented capture `cottage-112-capture1.log` + dat
|
||
replay):** the cottage's entry cell 0x104 is a 0.22 m-wide THRESHOLD
|
||
band; a running player crosses it between two physics ticks. Our
|
||
membership pick's outdoor-seed branch ran `CheckBuildingTransit` over a
|
||
landcell snapshot and STOPPED — building entry cells were never
|
||
expanded — so the tick after the skip (centre in deep room 0x100) found
|
||
no containing candidate and the pick kept the outdoor landcell
|
||
FOREVER (absorbing state): the render faithfully drew an outdoor frame
|
||
= transparent walls; promotion fired only on touching portal-adjacent
|
||
0x102's own volume. Retail's `CObjCell::find_cell_list` (0x0052b4e0)
|
||
runs ONE growing-array walk for EVERY seed (0052b576: vtable
|
||
`find_transit_cells` over the GROWING array) — recovery fires one tick
|
||
after any skip. Fix = the unified retail walk ported verbatim; pins in
|
||
`Issue112MembershipTests` (tick-skip recovery RED pre-fix; run-speed
|
||
phase-swept entry replay; gap over-fix guard; full promotion-chain
|
||
replay diagnostic). The earlier legs (escape-hatch removal `2d6954e`,
|
||
straddle gate `414c3de`) remain correct — they fixed the demote side;
|
||
this closes the promotion side.
|
||
|
||
(History below predates the close.) NOTE: the #120 reciprocal ping-pong
|
||
fired at exactly A9B3 `0103↔010F` during the 2026-06-11 session — the
|
||
runaway duplicate views were a plausible alternate mechanism for the
|
||
transparent frames; #120's fix (`dede7e4`) landed first.
|
||
**Severity:** MEDIUM-HIGH (any house with interior containment gaps; user-observed
|
||
"sometimes transparent" while walking around inside)
|
||
**Filed:** 2026-06-10 (late — user exploration after #111 closed)
|
||
**Component:** physics / membership (outdoor→indoor promotion) + cell containment
|
||
|
||
**Symptom (user, A9B3 hill cottage — interior cells 0xA9B30100/0x103/0x104, z=116):**
|
||
walking around INSIDE the house, the interior intermittently goes transparent;
|
||
membership ping-pongs indoor↔outdoor `0xA9B3003C` across a ~4 m band (x≈181–185
|
||
world frame; log `issue111-verify7.log` lines 8444-68375). Separately: some objects
|
||
inside lack collision — that part is the known #99 stopgap shape (outdoor object
|
||
sweep gated while indoor-classified; A6.P4 debt), filed here as a data point only.
|
||
|
||
**Mechanism (dat-scan evidenced, scan in this entry):**
|
||
1. The cottage's containment volumes have a REAL GAP inside the visible house:
|
||
(184.9, −109.5, z 116.5) [A9B3-local (184.9, 82.5)] is contained by NO interior
|
||
cell, while 1 m away (184.2, −109.2) is inside 0x100 and (180.5, −109.0) is
|
||
inside 0x103. Walking through the gap demotes the player to outdoor 0x3C
|
||
(correct per containment — the sphere-overlap stickiness releases once the
|
||
center leaves the volume by more than the foot radius).
|
||
2. **Once outdoor-classified, nothing re-promotes from INSIDE a room**: the pick
|
||
with seed 0x3C returns 0x3C even at points the scan proves are inside 0x103 —
|
||
our outdoor→indoor promotion (`CheckBuildingTransit`) fires only on PORTAL
|
||
crossings, so the player stays outdoor (→ outdoor flood → transparent interior)
|
||
until they happen to re-cross the doorway portal.
|
||
|
||
**PRIMARY FIX SHIPPED 2026-06-10 late (`2d6954e`) — residual remains, live gate
|
||
pending.** Oracle reads settled the mechanism: retail keeps curr_cell on a
|
||
null pick result (pc:308788-308825); `CLandCell::point_in_cell` is terrain-poly
|
||
only (:316941); building promotion = `sphere_intersects_cell` per portal-adjacent
|
||
cell (:309827) — all retail-matched in our port EXCEPT the `6dbbf95` escape
|
||
hatch, a non-retail demoter that converted the cottage's interior containment
|
||
gap into an outdoor stranding. Fix: hatch removed; per-tick pick now does
|
||
lateral stab-graph recovery (retail find_visible_child_cell :311444 — the #111
|
||
adjacent-claim shape self-heals, dat-tested) then retail keep-curr. Poisoned
|
||
saves stay covered at the snap (#107/#111 AdjustPosition). P1 retail-golden
|
||
gates explicitly green (11/11).
|
||
|
||
**RESIDUAL RESOLVED 2026-06-10 (`414c3de`, live-binary oracle):** retail's
|
||
gate read straight from the running 2013 client (cdb attach,
|
||
`CEnvCell::find_transit_cells` @ 0052c820 — BN pseudo-C had invented
|
||
portal_side tests in this branch): outdoor cells are admitted IFF a path
|
||
sphere STRADDLES an exterior portal's polygon plane,
|
||
`|dist| < radius + F_EPSILON(0.0002)`. Ported as the straddle gate on the
|
||
membership PICK's outdoor branch; the collision cell SET keeps the A6.P5
|
||
topology widening until #99/A6.P4 (outdoor-registered doors must stay
|
||
findable from indoors). Consequences: (a) the at-doorway gap demote is
|
||
RETAIL-FAITHFUL (gap point 0.23 m from 0x104's door plane < 0.48 m foot
|
||
radius → retail straddles + demotes + self-heals inward) — test renamed
|
||
`...DemotesRetailFaithfully`, expectation unchanged; (b) deep-interior
|
||
containment gaps in ANY house now keep-curr like retail instead of demoting
|
||
(new pins: `A9B3Cottage_GapBeyondStraddleDistance_KeepsCurrCell` +
|
||
`FindTransitCellsSphere_ExitPortalStraddleGate_MatchesRetail`). Live gate
|
||
pending: user re-walks the A9B3 cottage; expect interior to stay rendered
|
||
(brief doorway flicker at most). The missing OBJECT collision in that
|
||
cottage = #99 data point (A6.P4 debt). Scan data: standing-now → [0x103];
|
||
flip-a → []; flip-b → [0x100].
|
||
|
||
---
|
||
|
||
## #114 — Indoor PView shell-clip regions are not draw-quality (clip scoped to outdoor roots)
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM-HIGH (blocks the indoor half of retail's draw-side portal
|
||
clip; several user-visible indoor artifacts to re-test ride on it)
|
||
**Filed:** 2026-06-11 (first user gate on `927fd8f`)
|
||
**Component:** render (PortalVisibilityBuilder regions / ClipFrameAssembler)
|
||
|
||
**Finding:** enabling `GL_CLIP_DISTANCE` for the shell pass (#113 fix) was
|
||
correct for OUTDOOR eyes (phantom staircase gone, user-verified) but exposed
|
||
that INDOOR per-cell clip regions are admission-quality, not draw-quality —
|
||
applying them as geometric crops produced: chopped interior staircase +
|
||
missing candle-holder area and a neighbour room's water barrel visible
|
||
through a clipped-away wall (meeting hall interior, user screenshots
|
||
2026-06-11), and inner walls vanishing momentarily while passing a building
|
||
exit. Scoped in `9ce335e`: clip enabled only for `RootCell.IsOutdoorNode` +
|
||
the DrawPortal look-in path; indoor roots draw unclipped (pre-#113 state).
|
||
|
||
**Suspects for the indoor region quality gap:** (a) knife-edge regions when
|
||
the eye is near/on a portal plane (the §4 family — fixed for admission
|
||
stability, not pixel exactness); (b) `MergeBuildingFrame`/CellView handling
|
||
of cells visible through MULTIPLE portals (first-view-wins drops the other
|
||
aperture → over-crop); (c) the >8-plane slot-0 fallback drawing pass-all
|
||
(under-crop, opposite sign). Retail's reference: exact per-poly software
|
||
clip against the accumulated portal view (`planeMask=0xffffffff` :427922).
|
||
|
||
**Re-test against the scoped build (may be pre-existing, may be #114):**
|
||
1. intermittent transparent interior when ENTERING the hilltop cottage;
|
||
2. particles (candle flames) inside other buildings visible through walls
|
||
(statics' meshes not drawn but their emitters are — particle pass is not
|
||
gated by the same flood);
|
||
3. meeting-hall interior anomalies from the gate screenshots.
|
||
|
||
---
|
||
|
||
## #115 — Camera feels draggy/jittery vs retail when turning in cramped interiors
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW-MEDIUM (feel; no geometry errors reported)
|
||
**Filed:** 2026-06-11 (user, same gate session)
|
||
**Component:** camera (collision sweep / smoothing)
|
||
|
||
**Symptom (user):** "does not feel as smooth as retail — like it's dragging
|
||
over walls instead of gliding when I turn in cramped spaces, a bit jittery."
|
||
Likely the camera-collision sweep (verbatim `SmartBox::update_viewer` port,
|
||
Residual A) lacking retail's smoothing of the collided boom distance, or
|
||
per-tick re-collide jitter against near walls. Pre-existing (not from the
|
||
#113/#112 session — render-only + membership-gate changes). Investigate
|
||
retail's viewer-distance smoothing (update_viewer region) before touching.
|
||
|
||
---
|
||
|
||
## #116 — Slide-response divergence family: near-perpendicular lateral slide lost + first-airborne-frame in-frame slide vs hard stop
|
||
|
||
**Status:** OPEN (narrowed) — one Ghidra-confirmed faithfulness fix
|
||
SHIPPED 2026-06-12; both reported shapes still need a runtime trace.
|
||
**Severity:** LOW-MEDIUM (over-blocking, never under-blocking — no
|
||
walk-throughs; feel-level divergence at walls/doors)
|
||
**Filed:** 2026-06-11 (BR-7 / A6.P4 ship session)
|
||
**Component:** physics (slide response — `SlideSphere` degenerate-offset
|
||
guard + first-contact-frame behavior)
|
||
|
||
**GHIDRA SESSION 2026-06-12 (the BN branch-sign ambiguity RESOLVED via a
|
||
second decompiler — Ghidra MCP, patchmem.gpr, full PDB):**
|
||
- **SHIPPED (faithfulness fix):** `CSphere::slide_sphere` (Ghidra
|
||
`0x00537440`) compares its SQUARED magnitudes against `::F_EPSILON`
|
||
(= 0.000199999995 ≈ 0.0002 = `PhysicsGlobals.EPSILON`): `if (::F_EPSILON
|
||
<= |cross|²)` (crease) and `if (|offset|² < ::F_EPSILON) return
|
||
COLLIDED_TS` (degenerate guard). Our port compared against `EpsilonSq`
|
||
(0.0002² = 4e-8) — a ~5000× too-tight threshold (the BN `test ah,5`
|
||
obscured it). Fixed at `TransitionTypes.cs:3098,3105`; full physics
|
||
suite (612) + full Core (1443) green, no regression. Crease now needs
|
||
≥0.81° between normals (was 0.011°); the guard stops slides under
|
||
~1.41 cm like retail (was 0.2 mm). NOT a register deviation (no row
|
||
existed — it was an undocumented porting error; the fix matches retail).
|
||
⚠️ This does NOT fix either reported shape below.
|
||
- **Shape-1 RE-DIAGNOSED — our `cn=UnitZ` default is RETAIL-FAITHFUL.**
|
||
Ghidra `validate_transition` (`0x0050aa70`) does exactly our
|
||
`TransitionTypes.cs:3701-3702`: `if (collision_normal_valid == 0)
|
||
set_collision_normal(UnitZ)`. So the harness `cn=(0,0,1)` is the
|
||
faithful FALLBACK; the real divergence is UPSTREAM — at tick-22760 our
|
||
`collision_normal_valid` was FALSE (→ UnitZ) where retail's was TRUE
|
||
(it had recorded the door-face normal `(0,+1,0)`). The bug is in the
|
||
COLLISION-RECORDING path (find_collisions / collide_with_environment),
|
||
not slide/validate. Next: replay tick-22760
|
||
(`DoorBugTrajectoryReplayTests`) instrumented to see where our
|
||
collision-normal recording drops the wall normal.
|
||
- **Shape-2 NARROWED — D4 stays skipped.** Ghidra confirms slide_sphere
|
||
applies the slide IN-FRAME (`add_offset_to_check_pos` → SLID_TS), so our
|
||
Z=1.92 is faithful TO slide_sphere and the D4 Z=2.0 hard-stop pin is the
|
||
SUSPECT half. But the threshold fix did NOT change D4 (its offset is a
|
||
real slide, not degenerate), so whether retail's first airborne frame
|
||
REACHES slide_sphere (→1.92) or hard-stops upstream still needs a cdb
|
||
trace of an airborne wall hit before flipping the assertion.
|
||
|
||
**Two pinned shapes, both pre-dating BR-7 (the per-cell shadow port left
|
||
them byte-identical):**
|
||
|
||
1. **Tick-22760 lateral-slide loss** (door capture, 2026-05-24): live
|
||
blocked the southward push at the cottage door face and KEPT the tiny
|
||
lateral component (X −0.0357, cn=(0,+1,0)); the harness hard-stops both
|
||
components (cn=(0,0,1) from the post-stop ground refresh). The movement
|
||
is near-perpendicular to the face, so the projected slide offset is
|
||
tiny and the degenerate-offset guard converts it to a full stop.
|
||
Repro: `DoorBugTrajectoryReplayTests.Diagnostic_Tick22760_DumpEngineInternals`
|
||
(door found + BSP-only dispatched correctly — `[bsp-test]` /
|
||
`[cyl-skip-bsp]` probes prove the cell-set layer is innocent).
|
||
`LiveCompare_DoorBlocksFromOutside_Tick22760` now pins the blocking
|
||
invariant only.
|
||
|
||
2. **D4 first-airborne-frame slide** (`BSPStepUpTests.D4_*`, skipped with
|
||
this issue id): the L.2c pin expects the first airborne wall frame to
|
||
hard-stop (Z stays 2.0) with the slide starting frame 2 off the cached
|
||
sliding normal; since the P1-era `slide_sphere` work the engine slides
|
||
in-frame (Z reaches the 1.92 target on frame 1). Retail's cached-normal
|
||
mechanism (`CPhysicsObj::get_object_info` pc:279992, transient bit 4 →
|
||
`init_sliding_normal`) only governs the NEXT frame — whether retail's
|
||
first-frame response is hard-stop or in-frame slide needs a focused
|
||
oracle read (`collide_with_environment` / `slide_sphere` first-contact
|
||
path) before either the engine or the pin is declared wrong.
|
||
|
||
**Fix shape:** one oracle-driven pass over the slide response
|
||
(`SlideSphere` + first-contact frame), with the 22760 capture and the D4
|
||
fixture as the acceptance pair. Do NOT patch the degenerate-offset guard
|
||
ad hoc — the DO-NOT-RETRY table's slide entries (physics digest) apply.
|
||
|
||
**ORACLE DESK READ DONE (2026-06-12) — needs a LIVE cdb session to
|
||
finish.** Both sides quoted + verified against source (our
|
||
`CSphere::slide_sphere` port = `TransitionTypes.cs:3054-3133`; retail
|
||
`CSphere::slide_sphere` = decomp `0x00537440`, lines 321403-321532).
|
||
Three concrete leads, none safely fixable from the static BN decomp:
|
||
|
||
1. **Shape-1 re-attributed — it is NOT the degenerate-offset guard
|
||
threshold.** Retail's guard kills slides under ~1.4 cm (`|offset|² <
|
||
0.000199999995` at `0x537735`); the lost tick-22760 slide was 3.57 cm
|
||
(`X −0.0357`), well above it — retail would keep it too. The real
|
||
divergence is the COLLISION-NORMAL SOURCE: our harness recorded
|
||
`cn=(0,0,1)` (ground), live retail `cn=(0,+1,0)` (the door face).
|
||
Strong lead: `TransitionTypes.cs:3701-3702` — on a blocked move with
|
||
no valid collision normal we DEFAULT `cn = Vector3.UnitZ` ("push up");
|
||
that exact (0,0,1) is what the harness sees. Whether retail has an
|
||
equivalent default (vs keeping the wall normal) is a runtime question.
|
||
|
||
2. **Shape-2 — retail's slide_sphere applies the slide IN-FRAME**
|
||
(`add_offset_to_check_pos` @`0x53777e`, returns 4=SLID), so our
|
||
in-frame slide to Z=1.92 on frame 1 is likely retail-faithful and the
|
||
D4 frame-1 hard-stop pin (`BSPStepUpTests.D4_*`, expects Z=2.0) is the
|
||
STALE expectation. BUT retail always uses `contact_plane` OR
|
||
`last_known_contact_plane` (`0x53755a`); it has no "airborne wall-only,
|
||
no plane" third branch like ours (`TransitionTypes.cs:3080-3092`) — the
|
||
first-airborne-frame plane state needs a trace before flipping the pin.
|
||
|
||
3. **Candidate epsilon-squaring divergence (real, but explains neither
|
||
shape).** Retail compares SQUARED quantities (`|cross|²` @`0x5375a5`,
|
||
`|offset|²` @`0x537735`) against `0.000199999995` (≈0.0002, NON-squared);
|
||
our port compares against `EpsilonSq = 0.0002²` (line 3105 + the
|
||
`dirLenSq >= EpsilonSq` branch @3098) — potentially ~10⁴× too small.
|
||
DO NOT change this without cdb confirmation: the BN `test ah, 0x5`
|
||
branch polarity (lines 321466-321467/321484-321485) is the exact
|
||
undecodable construct the PosHitsSphere saga warned about, and the
|
||
register reuse garbles which quantity is squared. A wrong guess here
|
||
regresses ALL wall-slide behavior.
|
||
|
||
**Next (cdb session, well-scoped):** (a) `cdb -z uf
|
||
acclient!CSphere::slide_sphere` OR a live attach to disassemble
|
||
`0x00537440` and settle the two `test ah,5` branch signs + the
|
||
squared-vs-not threshold (prefer LIVE attach — prior lesson: static
|
||
`-z uf` misdecodes at OMAP boundaries); (b) live trace the tick-22760
|
||
door push to confirm whether the `cn=(0,0,1)` comes from our
|
||
`UnitZ`-default (lead 1) and what retail's normal is at that instant.
|
||
|
||
---
|
||
|
||
## #117 — Aperture-shaped see-through: doors/interiors visible through terrain hills and through nearer buildings — [DONE 2026-06-11 · 478c549, user re-gate "Yes solved"]
|
||
|
||
**Status:** OPEN
|
||
**Severity:** HIGH (the most visible remaining render artifact post-port)
|
||
**Filed:** 2026-06-11 (T5 comprehensive gate, user items 11a+11b)
|
||
**Component:** render — aperture depth discipline (the T1 punch pass)
|
||
|
||
**Symptom (user, axioms):** (a) "looking downhill I can see certain parts
|
||
of houses (not everything) like doors and some textures" through the
|
||
terrain; (b) "when I'm behind a house I can see the openings on the house
|
||
behind that house through the house in front of me, like doors and
|
||
windows." Both shapes are APERTURE-shaped (doors/windows), which points at
|
||
one mechanism: the far-Z punch erases the depth of NEARER occluders
|
||
(terrain hills, closer buildings) at the punched aperture pixels, so
|
||
interior content + door entities (dynamics drawn last) paint over them.
|
||
|
||
**Diagnosis direction (mechanism, not live-probing):** decomp
|
||
`DrawPortalPolyInternal` (Ghidra 0x0059bc90) for the punch's depth STATE —
|
||
retail either depth-tests the punch polygon against the existing buffer
|
||
(only punching where the aperture is actually visible) or relies on
|
||
far→near per-building draw order so nearer geometry re-establishes depth
|
||
AFTER the punch. Compare against the T1 (`579c8b0`) punch pass wiring.
|
||
|
||
---
|
||
|
||
## #118 — Character clipped + disappears for a moment when exiting houses — [DONE 2026-06-11 · 5a80a2e, user re-gate "Yes solved"]
|
||
|
||
**Status:** DONE — user-confirmed at the 2026-06-11 re-gate ("Yes
|
||
solved"), including the outdoor-NPC-through-doorway companion symptom
|
||
("Yes fixed").
|
||
|
||
**Root cause (pinned by the exit-walk harness, `HouseExitWalkReplayTests`):**
|
||
NOT the cone stack — candidates 1–3 all exonerated (cone-level walk passes
|
||
every step; the camera publishes (eye, ViewerCellId) from the SAME SweepEye
|
||
call and updates before the visibility read, so the pair is coherent; the
|
||
side-test window is ≤ PortalSideEpsilon and never occurs under healthy
|
||
resolution). The mechanism is DEPTH ORDERING: under an interior root, the
|
||
exit-portal SEAL stamps the door fan at TRUE depth after the full depth
|
||
clear, and T1's "ALL dynamics last" then draws the outdoor-classified player
|
||
depth-tested — every fragment beyond the door plane z-fails against the seal
|
||
across the whole aperture. Full vanish once the center exits (harness: the
|
||
entire s=0.04→2.64 m window until the eye crosses, ~2.2 s at walk speed);
|
||
the body's beyond-plane half clips at the plane while straddling.
|
||
|
||
**Retail oracle:** PView::DrawCells (0x005a4840) runs LScape::draw FIRST
|
||
(pc:432719), THEN the gated depth clear (pc:432731) + seals (pc:432786);
|
||
outdoor cell objects draw inside the landscape stage via DrawBlock →
|
||
DrawSortCell (0x005a17c0, pc:430124), and an object draws once per
|
||
overlapped shadow cell (pc:430056-430064) — so a threshold-straddling body
|
||
draws in both stages and neither half clips.
|
||
|
||
**Fix (`RetailPViewRenderer`):** under an interior root, outdoor-classified
|
||
dynamics draw in the OUTSIDE (landscape) stage — before the clear+seal, so
|
||
the seal protects their pixels — and indoor dynamics whose sphere straddles
|
||
an exit-portal plane draw in BOTH stages (`DynamicDrawsInOutsideStage`).
|
||
Outdoor roots keep all-dynamics-last (the BR-2 punch lesson). Pins:
|
||
`ExitWalk_PlayerStaysConeVisible_EveryStep`,
|
||
`ExitWalk_PlayerSurvivesSealDepth_WhenConeVisible`,
|
||
`ExitWalk_StraddlingPlayerDrawsInOutsideStage`.
|
||
**Severity:** MEDIUM-HIGH (every house exit, brief)
|
||
**Filed:** 2026-06-11 (T5 comprehensive gate, user item 10)
|
||
**Component:** render — dynamics handling at the indoor→outdoor transition
|
||
|
||
**Symptom (user):** "We get clipped and disappear when we exit houses.
|
||
Like when we are just outside for a moment." Transition frames where the
|
||
viewer is still indoors and the player is just outside the door.
|
||
|
||
**Narrowed (2026-06-11 post-T5 session — two suspects EXONERATED by
|
||
read):** (1) the partition is correct — the local player entity carries
|
||
its ServerGuid and routes to Dynamics; (2) the entity's `ParentCellId` is
|
||
NOT stale — it syncs per tick from the controller
|
||
(`pe.ParentCellId = result.CellId`, GameWindow ~6855).
|
||
|
||
**Live candidates (the doorway-crossing decision stack):**
|
||
- **Eye/cell incoherence under camera damping (#115 family / BR-8a):**
|
||
the render root comes from the sweep (`RetailChaseCamera.ViewerCellId`)
|
||
while the projection eye (`camPos`) is the DAMPED position — during a
|
||
crossing they can disagree by the damping lag. Retail damps FROM the
|
||
published collided viewer (verified divergence, plan BR-8a), so its
|
||
(eye, cell) pair stays coherent.
|
||
- **Exit-portal side test at the threshold:** with the eye ε-outside the
|
||
door plane while the root is still the interior cell,
|
||
`CameraOnInteriorSide` culls the exit portal → OutsideView EMPTY →
|
||
`SphereVisibleOutside` culls ALL outdoor dynamics (the player) for
|
||
those frames. Retail's AdjustPosition demotes the viewer cell to
|
||
outdoor the moment the point exits (seen_outside → adjust_to_outside),
|
||
making the inconsistent state structurally brief.
|
||
- The doorway-aperture cone test for an outdoor-classified player while
|
||
the viewer is legitimately still inside (cone tightness).
|
||
|
||
**Next step (apparatus, not guessing):** a deterministic exit-walk
|
||
harness over the corner-building cells — drive the production decision
|
||
stack headlessly per step of an eye+player path crossing the doorway
|
||
(viewer-cell resolution → `PortalVisibilityBuilder.Build` →
|
||
`ViewconeCuller` → the `DrawDynamicsLast` visibility predicate) and
|
||
assert the player sphere stays visible on every step. All CPU; the
|
||
failing step pins which candidate fires.
|
||
|
||
---
|
||
|
||
## #119 — Old tower: stairs partially invisible + extraneous water barrel; two meshes permanently invisible at startup
|
||
|
||
**Status:** CLOSED 2026-06-12 — user gate "the tower seem to work good now"
|
||
(run-from-town stairs complete, barrel gone, climb + top stable).
|
||
**Severity:** MEDIUM (pre-existing — "same issue as before" per the user)
|
||
**Filed:** 2026-06-11 (T5 comprehensive gate, user items 9+13)
|
||
**Component:** render — mesh upload / content inclusion
|
||
|
||
**RESOLUTION (2026-06-12) — three root causes, fixed in sequence, each
|
||
pinned by the ACDREAM_DUMP_ENTITY decisive probe (`3cf6bcc`):**
|
||
1. **`2163308` — Tier-1 cross-entity batch serving** (the broken stairs +
|
||
"water barrel"): interior entity ids discarded the landblock X byte
|
||
(`0x40YYFF00` — Holtburg town A9B3's 9th interior stab == the AAB3
|
||
tower staircase, both 0x40B3FF09) AND the classification cache hinted
|
||
entities with the PLAYER's landblock at bucket-draw time, so the
|
||
colliding twins shared one cache key: whichever classified first
|
||
served its batches to the other all session. Town-login + run → the
|
||
staircase drew a town object's 3 zero-RestPose batches (= "the water
|
||
barrel"); tower-login → usually clean. Captured live: `cache=hit:3
|
||
restZero=3` on a 43-part staircase. Fixed: `0x40XXYY##` ids +
|
||
owner-derived cache hints (`ResolveCacheLandblockHint`).
|
||
2. **`987313a` — knife-edge clip port** (climb strobes, top flap family):
|
||
`ProjectToClip` → exact W=0 eye-plane clip per retail
|
||
`ACRender::polyClipFinish` (0x006b6d00); zero-area in-plane views now
|
||
PROPAGATE (segment-key CanonicalKey) like retail's ClipPortals; the
|
||
`EyeInsidePortalOpening` rescue DELETED (CornerFloodReplay passes
|
||
without it under the W=0 port).
|
||
3. **`1ca412d` + `6a9b529` — entity bounds must cover the mesh** (the
|
||
gaze-dependent vanish: stairs visible climbing down, gone climbing
|
||
up): `WorldEntity.RefreshAabb` was a fixed ±5 m ANCHOR box feeding
|
||
both the dispatcher frustum cull and the viewcone sphere — 15 of the
|
||
staircase's 17 m stuck out of it. Final fix derives root-local bounds
|
||
from the dat VERTEX data at hydration (GfxObjBounds +
|
||
LocalBoundsAccumulator, all four hydration sites) — data, not a
|
||
promise; retail needs no equivalent because it viewcone-checks each
|
||
part's dat-authored `CGfxObj.drawing_sphere` per part
|
||
(CPhysicsPart::Draw 0x0050d7a0 → DrawMesh 0x005a09a4).
|
||
|
||
The `[up-null]` lead was exonerated earlier (legit no-draw models); the
|
||
f35cb8b lift fix (below) was real but not THE bug. #113's
|
||
distance-dependent phantom staircase should be RE-CHECKED against
|
||
`2163308` (the town twin wore the tower's staircase batches).
|
||
|
||
**Symptom (user):** the old tower has missing stair parts (pre-existing;
|
||
the tower stairs ARE visible in retail — user axiom recorded 2026-06-11
|
||
in the render digest) and shows a water barrel that retail doesn't.
|
||
|
||
**Lead (from the T5 launch log):** exactly two
|
||
`[up-null] upload returned null for 0x00010002B4 / 0x00010008A8 — caching
|
||
EMPTY render data (permanently invisible)` lines at startup.
|
||
|
||
**Narrowed 2026-06-11 (the [up-null] lead is EXONERATED, dat-proven):**
|
||
`Issue119UpNullGfxObjDumpTests` — both GfxObjs are legitimately no-draw
|
||
models: 0x010002B4 = 9 polys, ALL `NoPos`, all surfaces `Base1Solid`;
|
||
0x010008A8 = 1 poly, `NoPos`, `Base1Solid|Translucent`. Retail's
|
||
skipNoTexture never draws them either (the BR-1 equivalence) — the empty
|
||
cache is the CORRECT terminal state, and the alarming log line was the
|
||
only defect (reworded; it stays as a tripwire for the real-failure shape).
|
||
Second fact, same test: on the hall/tower shell 0x010014C3, ZERO textured
|
||
polys are dropped by the extraction gates (137/149 draw; the 12 dropped
|
||
are the known #113 no-draw orphans) — the per-poly extraction is
|
||
exonerated for building shells, pinned by
|
||
`ShellModel_NoTexturedPolyIsDropped`.
|
||
|
||
**Remaining hypothesis space (needs the re-gate to identify the exact
|
||
tower):** the missing stair parts draw from somewhere other than the
|
||
shell GfxObj's per-poly extraction — most plausibly interior stair-CELL
|
||
shells whose visibility depends on the flood admitting those cells from
|
||
the outside view, or a different building model than assumed. At the
|
||
re-gate: have the user point at the tower (one sentence / approx
|
||
location) — then the cell set + flood can be replayed headlessly like
|
||
#118. The extraneous water barrel remains a separate static-inclusion
|
||
question (which cell owns it; is it admitted by a view it shouldn't be).
|
||
|
||
**User split (re-gate 2026-06-11) — THREE distinct artifacts in the
|
||
area:**
|
||
1. The PHANTOM walkable-but-invisible stairs (the #113 family) is still
|
||
present and now reads as located at the HILL COTTAGE — "the stairs
|
||
half embedded into the outside wall." (#113's reopened
|
||
drawing-BSP-orphan investigation owns this.)
|
||
2. A tower CLOSE TO the hill cottage has the MISSING stairs + the
|
||
extraneous water barrel in its middle — this entry (#119) proper.
|
||
3. The hill house sometimes turns ALL walls transparent when entering —
|
||
tracked under #112; note the #120 ping-pong fired at exactly A9B3
|
||
0103↔010F, so re-check after the #120 fix (`dede7e4`).
|
||
|
||
**DECODED (2026-06-11 evening):** the user's logout position pinned the
|
||
tower (cell 0xAAB30107, AAB3 building[1] model 0x01001117). Dat truth
|
||
(`Issue119TowerDumpTests`): the stairs are ONE static — Setup
|
||
0x020003F2, a 43-part spiral staircase at the tower center (placement
|
||
frames perfect, all parts drawable). Pipeline exonerated layer by layer
|
||
(extraction, hydration ParentCellId=envCellId, per-MeshRef registration,
|
||
dispatcher compose); clean WB_DIAG counters at the tower spawn:
|
||
meshMissing=0, entSeen==entDrawn.
|
||
|
||
**⚠️ USER AXIOM (2026-06-11 late): the barrel is NOT in the tower in
|
||
retail.** The earlier "legit dat barrels on the landings" claim is
|
||
RETRACTED — what the user saw was itself a render artifact. Post-#120
|
||
verdict: "Barrel is gone and more stairs exist" — both improved
|
||
together, consistent with the "barrel" being mis-drawn staircase
|
||
geometry under the corrupted floods. (What the four 0x020005D8 cell
|
||
statics actually render as remains UNVERIFIED — do not assume barrel.)
|
||
|
||
**REMAINING (user, post-#120 build):**
|
||
1. Running UP the tower, the TOP stairs disappear visually but stay
|
||
walkable.
|
||
2. On top of the tower, the roof and edges FLAP into existence and
|
||
back.
|
||
|
||
**ROOT CAUSE FOUND + FIXED 2026-06-11 (`f35cb8b`) — the +0.02 m render
|
||
lift leaked into the portal-visibility graph.**
|
||
`BuildInteriorEntitiesForStreaming` lifts the render-side cell transform
|
||
2 cm (shell z-fighting vs terrain — a DRAW concern) and passed that
|
||
LIFTED transform to `BuildLoadedCell`, so every visibility-graph plane
|
||
sat 2 cm high. The side test's in-plane window is ±10 mm: an eye
|
||
standing ON a floor containing a HORIZONTAL portal (the tower deck lip
|
||
0x010A→0x0107, landings, cellar mouths) sits 10–20 mm BELOW the lifted
|
||
plane → outside the window → the cell behind the portal side-culled out
|
||
of the flood. Captured live at the stair top (the user's climb +
|
||
[viewer-diff]): root=0x010A, eye z=126.803 vs the plane at 126.80,
|
||
flood=1, 0x0107 dropped WHILE LOOKED AT — "stairs disappear and you can
|
||
walk on them"; the roof/edge flap = the same marginal admissions swinging
|
||
with the gaze. Vertical doorways immune (the lift slides their planes
|
||
along themselves) — why this hit exactly stairs/decks/floors. Headless
|
||
replay reproduces ONLY with the lift; fix = BuildLoadedCell gets the
|
||
PHYSICS (unlifted) transform; shells keep their draw lift. Pins:
|
||
`CapturedTopOfStairs_MainCellStaysInFlood` (unlifted asserts admission;
|
||
lifted arm = the mechanism canary). Likely also feeds the #108 residual
|
||
(cellar mouth = a horizontal portal) — re-check at the gate.
|
||
The earlier synthetic roof-lip-band pin
|
||
(`TowerAscent_StaircaseStaysConeVisible_EveryStep`) stays SKIPPED — its
|
||
band came from the harness's AABB root model, not the production sweep;
|
||
re-validate against the real resolver before un-skipping.
|
||
|
||
---
|
||
|
||
## #120 — [pv-ERROR] in-place propagation tripwire: convergence invariant broken at depth 128 (cottage interior cells)
|
||
|
||
**Status:** FIXED 2026-06-11 (`dede7e4`) — pending re-gate (watch for
|
||
zero `[pv-ERROR]` lines in the next launch log)
|
||
**Severity:** HIGH (self-detected invariant break in the new flood growth)
|
||
**Filed:** 2026-06-11 (T5 launch log; fired during normal cottage play)
|
||
**Component:** render — PortalVisibilityBuilder in-place growth (T2/BR-4)
|
||
|
||
**RESOLVED (2026-06-11):** the armed tripwire self-attributed on the
|
||
re-gate launch — a pure TWO-CELL reciprocal ping-pong (`0xA9B4015C ↔
|
||
0x0162` and `0xA9B30103 ↔ 0x010F`, 64 laps each). Mechanism: eye within
|
||
PortalSideEpsilon (±1 cm) of the portal plane → in-plane counts interior
|
||
for BOTH cells → views lap A→B→A; near-edge-on aperture re-clips wobble
|
||
beyond the 1e-3 dedup grid → every lap keys "new". The prior sweeps
|
||
couldn't reproduce because they only loaded the corner building — both
|
||
firing pairs are outside it. `Issue120ReciprocalPingPongTests` loads the
|
||
full landblock and reproduces deterministically (tripwire firings +
|
||
65-polygon CellView piles). Fix: `CellView.Add` rejects polygons
|
||
CONTAINED in an already-stored polygon (a round-trip re-emission is a
|
||
subset of its originator in exact math) — union growth is strictly
|
||
area-increasing, the lap dies at iteration 1. Corner-flood completeness
|
||
pins stay green. PortalSideEpsilon untouched (DO-NOT-RETRY).
|
||
|
||
**Evidence:** `[pv-ERROR] in-place propagation tripwire at depth 128 on
|
||
cell=0xA9B40175 / 0xA9B40174 / 0xA9B40162 — convergence invariant broken,
|
||
investigate` (3+ firings in the T5 session, exactly the cottage interior
|
||
cells the user was walking). T2's in-place growth (which replaced the
|
||
`MaxReprocessPerCell=16` cap) re-propagated one cell's view 128 times
|
||
within a single build — a re-emission cycle the dedup misses, or growth
|
||
ping-ponging through a reciprocal portal pair. May be load-bearing for
|
||
#117/#118 (runaway view growth → wrong clip/punch volumes).
|
||
|
||
**Investigation (2026-06-11, post-T5):** retail RECURSES natively too
|
||
(`AddViewToPortals → FixCellList → AdjustCellView → AddViewToPortals`,
|
||
Ghidra 0x005a52d0/0x005a5250/0x005a5770 — no depth guard), so the
|
||
recursion shape is faithful and retail's safety is FAST CONVERGENCE; our
|
||
depth-128 means slow/non-saturation our dedup admits (each lap of a
|
||
portal cycle nests one level deeper). Two dat-backed harness sweeps over
|
||
the full corner-building cell set could NOT reproduce
|
||
(`CornerFloodReplayTests.PortalPlaneCrossings_InPlacePropagationConverges`
|
||
— ±6 cm across every portal plane, both seed sides — and
|
||
`InCellDirectionSweep_InPlacePropagationConverges` — 3024 builds, in-cell
|
||
eye grid × 8 yaw × 3 pitch): firings = 0. Production-only ingredients
|
||
suspected: the full lookup graph (production reaches far more cells; one
|
||
T5 firing was 0x0162, a different building) and/or the real camera path.
|
||
**Tripwire armed for self-attribution** (`DumpPropagationChain`): the next
|
||
firing logs the root cell, eye, per-cell frequency, and the chain tail —
|
||
the cycle's structure reads directly off the log. Both sweeps stay as
|
||
regression pins (`PortalVisibilityBuilder.ConvergenceTripwireCount`).
|
||
Revisit on the next firing (the #117/#118 re-gate launch will carry it).
|
||
|
||
---
|
||
|
||
## #121 — All world portals invisible (portal swirl VFX gone everywhere)
|
||
|
||
**Status:** FIXED 2026-06-11 — pending re-gate
|
||
**Severity:** HIGH (user: "all portals that were previously showing at
|
||
various places are now gone")
|
||
**Filed:** 2026-06-11 (re-gate launch)
|
||
**Component:** render — particle pass routing under the pview path
|
||
|
||
**Root cause (by read):** dynamics' ATTACHED emitters (portal swirls on
|
||
server-spawned portal entities, creature effects) fell through EVERY
|
||
particle filter under the unified pview path: the landscape slice's
|
||
filter carries outdoor STATICS (+ the #118 outside-stage dynamics), the
|
||
per-cell callback carries cell STATICS, and T4 deleted the old
|
||
`clipRoot==null` global pass from normal frames. T5 never checked
|
||
portals (not on the checklist) — the gap dates to the T3/T4 one-gate
|
||
work, surfaced at this re-gate. **Fix:** a dynamics-owner particle pass
|
||
— `DrawDynamicsLast` hands its cone-surviving dynamics (minus
|
||
outside-stage entities, whose emitters already drew in the landscape
|
||
slice) to a new `DrawDynamicsParticles` callback; GameWindow draws
|
||
Scene-pass emitters filtered to those owner ids (mirror of
|
||
`DrawRetailPViewCellParticles`). Retail shape: emitters draw with their
|
||
owner object.
|
||
|
||
---
|
||
|
||
## #122 — Windows oscillate between background and the correct outside view when entering houses
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM
|
||
**Filed:** 2026-06-11 (re-gate; user: "the oscillating between
|
||
background world and the right outside view is now back on some windows
|
||
when entering houses")
|
||
**Component:** render — window exit-portal region at the root flip
|
||
|
||
The #109 oscillation family, now localized to WINDOWS during house
|
||
ENTRY (the outdoor→interior root flip). Candidate mechanisms:
|
||
(a) the #120 reciprocal ping-pong polluting clip volumes near the
|
||
portal plane during the crossing — the firing sites were exactly
|
||
cottage cells during entry; RE-CHECK after `dede7e4` before
|
||
investigating; (b) the seal/punch handoff on windows across the root
|
||
flip (forceFarZ keys on `clipRoot.IsOutdoorNode`, flipping the window
|
||
aperture between punch and seal semantics frame-to-frame at the
|
||
threshold).
|
||
|
||
---
|
||
|
||
## #123 — Buildings transiently disappear when running close past them
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM
|
||
**Filed:** 2026-06-11 (re-gate; user: "when I pass by close by
|
||
buildings, sometimes the building disappears as I run by")
|
||
**Component:** render — outdoor root, close-range building draw
|
||
|
||
Whole-building transient vanish at close range under the outdoor root.
|
||
Suspects (unverified): the per-building frustum pre-gate on
|
||
`Building.PortalBounds` (T2 draw-driven flood gather) interacting with
|
||
close-range AABB degeneracy; dispatcher frustum cull with a stale
|
||
entity AABB; or the #117 stencil punch marking a near-full-screen
|
||
aperture fan at grazing range while the building's own flood is gated
|
||
off (far-Z holes → sky/fog where the shell should be). Needs evidence
|
||
first: reproduce with `ACDREAM_PROBE_VIS`/`[outdoor-node]` + a capture
|
||
of which draw list the building's shell left.
|
||
|
||
---
|
||
|
||
## #124 — Looking out through an opening: far buildings with openings show missing/transparent back walls
|
||
|
||
**Status:** CLOSED (user-gated 2026-06-12 evening: "124, that one is solved")
|
||
**Severity:** MEDIUM
|
||
**Filed:** 2026-06-11 (re-gate; pre-existing — "still have that issue";
|
||
user 2026-06-12: "especially visible when I look out through a door
|
||
opening when inside a building")
|
||
**Component:** render — per-building look-in floods under INTERIOR roots
|
||
|
||
From inside a building, looking out through a door/window at ANOTHER
|
||
building that has an opening: the far building's back walls are
|
||
missing/transparent. The lead confirmed by decomp: retail runs the
|
||
look-in INSIDE the landscape stage for ANY root — `LScape::draw` is the
|
||
FIRST call of `PView::DrawCells`' outside-view branch (pc:432719),
|
||
strictly BEFORE the depth clear (pc:432732) and the seals (pc:432785);
|
||
`ConstructView(CBldPortal)`'s GetClip runs under the INSTALLED view
|
||
(the doorway region), and all apertures far-Z punch (pass 1) before any
|
||
interior cell draws (pass 2).
|
||
|
||
**Fix (2026-06-12):**
|
||
- The per-building gather (frustum pre-gate on `Building.PortalBounds`)
|
||
now runs for interior roots too; the root's own doorway self-excludes
|
||
via the seed eye-side test.
|
||
- `BuildFromExterior` gained `seedRegion` — the port of retail's
|
||
installed-view clip: interior-root look-ins seed clipped against the
|
||
OutsideView (doorway) polygons, so a building not visible through the
|
||
doorway never floods. Outdoor roots keep the full-screen default.
|
||
- NEW `DrawBuildingLookIns` sub-pass inside the LANDSCAPE stage (before
|
||
the depth clear + seals): per building, punch ALL apertures
|
||
(`DrawLookInPortalPunch`, always far-Z), then draw the flooded cells'
|
||
shells + statics far→near. NOT merged into the main frame — a merged
|
||
cell would draw post-clear and z-fail against the root's seal.
|
||
- Look-in cells join the Prepare/partition set (shells get batches,
|
||
statics route to ByCell, consumed only by the sub-pass).
|
||
|
||
Pins: `Issue124LookInSeedRegionTests` (containing region floods ⊆
|
||
full-screen flood; disjoint region floods nothing; interior-side eye
|
||
never seeds its own exit door). Register: AP-33 (look-in statics drawn
|
||
whole — no per-part viewcone; look-in DYNAMICS deferred — an NPC inside
|
||
a far building stays invisible; both documented).
|
||
|
||
**Gate:** from inside a building, look out the door at another building
|
||
with an open door/window — its interior/back walls render through its
|
||
aperture instead of see-through to the world behind.
|
||
|
||
---
|
||
|
||
## #125 — GL InvalidOperation during staged texture upload: failed uploads are STICKY (never retried) + uncaught crash in GenerateMipmaps
|
||
|
||
**Status:** CLOSED 2026-06-12 — the GL root cause was fixed `fcade06`
|
||
(2026-06-11, live-verified); the remaining sticky-drop DESIGN DEBT is now
|
||
fixed too (bounded upload retry, below). No visual gate (robustness).
|
||
|
||
**RESOLVED (root cause):** the GL errors were the gpu_us QUERY RING's own
|
||
— a glGenQueries name isn't a query object until first glBeginQuery, and
|
||
GetQueryObject on a never-begun name is GL_INVALID_OPERATION. The N.6
|
||
ring assumed ONE Draw/frame with both passes non-empty; the pview
|
||
pipeline's many small Draws routinely skip a pass → the slot read queued
|
||
an error EVERY frame under ACDREAM_WB_DIAG=1; WB's texture-path
|
||
glGetError checks ate the stale errors (the attribution trap) → fake
|
||
upload failures + the ProcessDirtyUpdates throw. Fix: begun-flags per
|
||
slot; read only begun queries. Live-verified in-tower: 0 [wb-error]
|
||
(was 7), no crash, gpu_us reads real values (9–11 µs) for the first
|
||
time under pview, meshMissing=0. **Normal runs (WB_DIAG off) never had
|
||
these errors — this mechanism is RETIRED for #119.**
|
||
|
||
**Remaining debt — FIXED 2026-06-12 (bounded upload retry):** the exact
|
||
stick was the CPU-cache short-circuit, not just the early `TryRemove`: a
|
||
failed `UploadMeshData` (catch → null) consumed the staged item and left
|
||
`_renderData` empty while the prepared data lingered in `_cpuMeshCache`,
|
||
so `PrepareMeshDataAsync`'s cache-hit path (`ObjectMeshManager.cs:448-453`)
|
||
returned it WITHOUT re-staging → never re-uploaded until CPU-cache
|
||
eviction (effectively session-sticky under low cache pressure). Fix: the
|
||
Tick drain (`WbMeshAdapter.cs`) now re-stages a failed upload for the NEXT
|
||
frame via `ObjectMeshManager.UploadOrRequeue`, bounded by
|
||
`MaxUploadRetries` (3) using a counter on the `ObjectMeshData` object
|
||
(resets to 0 on re-prepare). Re-stages are collected and re-enqueued
|
||
AFTER the drain loop — never inside it — so a deterministic failure can't
|
||
spin the queue in one frame; past the cap it gives up with a loud
|
||
`[up-retry] … giving up` line (surfaces a genuine GL defect instead of
|
||
the old silent permanent drop). Retail loads synchronously and has no
|
||
such failure mode; this converges the async pipeline toward that
|
||
guarantee. Build + App.Tests (264) green; no GL-context test seam exists
|
||
for the upload path so the retry is verified by construction + the
|
||
regression suite. The uncaught `GenerateMipmaps` path (open-question c)
|
||
is INTENTIONALLY left to surface errors — adding a blanket catch there
|
||
would mask future real defects (no-workarounds rule); its trigger
|
||
(`fcade06`) is already retired.
|
||
**Filed:** 2026-06-11 (in-tower WB_DIAG launch, `tower-wbdiag3.log` — preserved in the worktree root)
|
||
**Component:** render — WB staged texture pipeline (ObjectMeshManager / ManagedGLTextureArray)
|
||
|
||
**Evidence (one launch, character spawned inside the #119 tower):**
|
||
1. `[wb-error] Error uploading mesh data for 0x0100321D` — GL
|
||
`InvalidOperation` thrown in `ManagedGLTextureArray..ctor:70`
|
||
(TextureAtlasManager ctor → CreateTextureArrayInternal), caught by
|
||
`UploadMeshData`'s try/catch → returns null. **The drop is STICKY:**
|
||
`_preparationTasks.TryRemove` runs BEFORE the upload
|
||
(ObjectMeshManager.cs:685), so a failed upload is never re-prepared —
|
||
that mesh is permanently invisible for the session (only a one-line
|
||
[wb-error] marks it).
|
||
2. Same session, `Tick()` → `GenerateMipmaps()` →
|
||
`ManagedGLTextureArray.ProcessDirtyUpdatesInternal:283` threw the SAME
|
||
GL InvalidOperation **uncaught** → process death (exit 82). Both on the
|
||
render thread (Tick/OnRender) — not a thread-affinity bug.
|
||
|
||
**Why this matters for #119:** the missing tower stairs are per-cell
|
||
Setup statics whose parts are individually uploaded; an intermittent GL
|
||
error burst during atlas creation/flush kills whichever uploads are in
|
||
flight — "partially invisible", varying with load order, hitting the
|
||
late-loading AAB3 interior statics consistently. The dat + extraction +
|
||
registration + dispatcher are all exonerated by read/test
|
||
(Issue119TowerDumpTests; the [up-null] pair was a separate, legitimate
|
||
no-draw class).
|
||
|
||
**Open questions (next session):** (a) what makes the GL context error
|
||
out — a stale error queued by an earlier unchecked call being
|
||
mis-attributed to WB's diligent glGetError checks (classic GL
|
||
attribution trap; suspects: the #117 stencil punch state, the new #118 /
|
||
#121 passes, or a pre-existing per-frame state leak), vs. a genuine
|
||
invalid texture-array creation state; (b) whether upload failures should
|
||
re-enqueue instead of dropping (retail has no such failure mode — the
|
||
sticky drop is OUR invention and must go regardless); (c) the uncaught
|
||
GenerateMipmaps path needs the same handling either way.
|
||
**Repro lever:** the test character's save spawns INSIDE the tower —
|
||
every launch loads the exact content; `ACDREAM_WB_DIAG=1` prints the
|
||
meshMissing counters.
|
||
|
||
---
|
||
|
||
## #126 — Outdoor spawn claim on a building roof is grounded THROUGH the roof to terrain (transparent-interior spawn)
|
||
|
||
**Status:** OPEN — HIGH (every login/logout on any walkable roof)
|
||
**Filed:** 2026-06-11 (tower capture run, `tower-viewer-capture.log` line 1)
|
||
**Component:** physics — login snap (the #107/#111 family)
|
||
|
||
**Evidence:** the user logged out standing ON TOP of the AAB3 tower
|
||
(z=127.2). The next login: `[snap] claim=0xAAB30023 pos=(297.160,
|
||
-129.182,127.200) … terrainZ=112.000 indoor=False -> targetZ=112.000
|
||
targetCell=0xAAB30023`. The snap's OUTDOOR branch always grounds to
|
||
TERRAIN Z — it warped the player from the roof down INTO the tower's
|
||
interior volume at ground level, still outdoor-classified → the
|
||
transparent-interior spawn the user reported ("spawned in the tower and
|
||
it was transparent"), self-healing only after walking out and back in.
|
||
**Fix shape:** an outdoor claim must ground to the nearest WALKABLE
|
||
surface at/below the claim Z (building roofs and GfxObj floors via the
|
||
physics walkable query — the #111 `WalkableFloorZNearest` machinery),
|
||
not raw terrain. Note the snap line even shows a candidate it rejected
|
||
(`bestCell=0xAAB30101 bestZ=124.3`).
|
||
|
||
---
|
||
|
||
## #127 — Per-building flood admissions are BISTABLE per frame under the outdoor root (the building-flap mechanism)
|
||
|
||
**Status:** CLOSED 2026-06-12 — user re-gate ("Seems to have been
|
||
fixed" — ran past distant buildings, no flicker/vanish) + desk
|
||
confirmation. The bistable-admission mechanism died with the **W=0
|
||
polyClipFinish clip port** (`987313a`, the #119/#120 work that
|
||
"kills the knife-edge class everywhere") plus the #120 containment-
|
||
rejection growth fix. NOTE the captured-pair evidence in
|
||
`tower-viewer-capture.log` predates all of those fixes — it was the
|
||
near-eye knife edge, the same class. Pins (both green at HEAD):
|
||
`Issue127FloodFlipReplayTests.CapturedFlipPair_AdmissionIsStable`
|
||
(the original 4 cm flip pair now |A|=|B|, zero diff, all FOVs, both
|
||
pre-gate states) + `DistantBuildingStrafe_NoAdmissionChurn` (the
|
||
regression pin: 0 churn across 21 building groups × {10,30,60,120,190} m
|
||
× 100 mm-steps run-past strafe, both pre-gate states). DO-NOT-RETRY:
|
||
do not re-open the BuildFromExterior seed gates for flap symptoms
|
||
without a FRESH repro at HEAD — the captured-pair lead is dead.
|
||
**Filed:** 2026-06-11 (tower capture run)
|
||
**Component:** render — BuildFromExterior seed admission / per-building
|
||
flood stability
|
||
|
||
**Evidence (`tower-viewer-capture.log`, 551 [viewer] lines in one short
|
||
run):** under the outdoor root near the tower, the merged per-building
|
||
flood size oscillates ±1–3 cells nearly EVERY frame at millimetre eye
|
||
deltas — standing on the tower roof: flood 45↔46↔47↔48 per line with
|
||
the eye moving mm at a time (and one stretch flipping at a byte-static
|
||
eye). Every oscillation = some building's interior cells (including
|
||
this tower's roof-lip cells) dropping in/out of the visible set → the
|
||
roof/edges flap; a building whose cells flap while running past =
|
||
#123. The INTERIOR side shows the same family: inside the tower the
|
||
flood flickers 1↔2–3 with outPolys 0↔1 during the climb.
|
||
**Next:** the [viewer] probe now logs the camera forward (fwd=) — one
|
||
more capture run gives exact (eye, fwd) pairs to replay in a
|
||
deterministic harness; then pin WHICH admission gate is bistable
|
||
(seed side test / in-plane reject / clip-empty / the frustum pre-gate
|
||
on PortalBounds) and stabilize it retail-shaped.
|
||
|
||
---
|
||
|
||
## #128 — Tower staircase invisible with a HEALTHY interior root (session-sticky; renders fine in other sessions)
|
||
|
||
**Status:** CLOSED 2026-06-12 — same root causes as #119 (see its
|
||
RESOLUTION block): the session-sticky invisibility was the Tier-1
|
||
cross-entity batch serving (`2163308` — session order decided which
|
||
colliding twin won the cache slot, exactly the observed
|
||
nondeterminism), and the healthy-root climb invisibility was the ±5 m
|
||
anchor bounds feeding the viewcone sphere (`6a9b529`). The "FullScreen
|
||
views — cone cannot cull" reasoning below missed that the camera
|
||
frustum planes still cull via the same undersized box. User gate
|
||
2026-06-12: tower works.
|
||
**Filed:** 2026-06-11 (tower capture run + user report)
|
||
**Component:** render — entity draw path (suspect: session-order state)
|
||
|
||
**Evidence:** during the user's climb the root was the tower's main
|
||
cell 0xAAB30107 (FullScreen views — the cone CANNOT cull a 0107
|
||
static), yet the 43-part staircase was invisible the whole way up; in a
|
||
different session same build (the in-tower diag spawn,
|
||
`tower-wbdiag4.log` + screenshot) the same staircase rendered perfectly
|
||
with meshMissing=0. Session-sticky, nondeterministic across sessions:
|
||
suspect state accumulated by session order — Tier-1 classification
|
||
cache shapes (#53 family — though the known veto paths read correct),
|
||
LRU eviction + the no-re-prepare-on-re-registration gap, or the #125
|
||
sticky-drop cousin. The user's "barrel" sighting tracks this bug (a
|
||
partial subset of staircase parts rendering ≈ a barrel-shaped pile) —
|
||
NOT dat content (the barrel is NOT in retail — user axiom). **Next:**
|
||
reproduce under ACDREAM_WB_DIAG=1 with the user's session shape (spawn
|
||
mis-grounded inside via #126, walk out/in, climb) and read
|
||
meshMissing + [indoor-lookup]; if meshMissing>0 persists at standstill
|
||
the parts are unloaded (eviction/registration); if 0, instrument the
|
||
staircase entity's per-frame draw decision.
|
||
|
||
---
|
||
|
||
## #129 — Doors/doorways leak through terrain and houses from over a landblock away
|
||
|
||
**Status:** FIX SHIPPED — awaiting user visual gate
|
||
**Severity:** MEDIUM (visible at distance during normal outdoor play)
|
||
**Filed:** 2026-06-12 (user report, post-#119-close session)
|
||
**Component:** render — aperture depth punch at distance (#117 family, AD-18)
|
||
|
||
**Symptom (user):** "leakage of like doors and doorways through the
|
||
terrain and houses over a landblock" — door/doorway-shaped patches
|
||
visible THROUGH intervening terrain and nearer buildings when the
|
||
source building is roughly a landblock (~192 m) or more away.
|
||
|
||
**Root cause (lead 1 confirmed analytically, `Issue129PunchBiasTests`):**
|
||
the #117 mark-pass bias was a CONSTANT 0.0005 NDC. NDC depth is
|
||
non-linear — a constant NDC bias `b` spans ≈ `b·d²/near` meters of eye
|
||
depth at distance `d`. With retail's znear 0.1 that is 0.125 m at 5 m
|
||
but **~190 m at a landblock**: every hill/house in front of a distant
|
||
aperture passed the LEQUAL mark and was far-Z punched → the door-shaped
|
||
leak. Exactly AD-18's recorded "Risk if assumption breaks".
|
||
|
||
**Fix (2026-06-12):** cap the bias's EYE-SPACE span —
|
||
`biasNdc(d) = min(0.0005, 0.5 m × near / d²)`
|
||
(`PortalDepthMaskRenderer.MarkBiasNdc`, mirrored in the vertex shader).
|
||
Below the ~10 m crossover the constant term wins, bit-identical to the
|
||
T5-validated behavior (#108 grass coverage untouched); beyond it the
|
||
punch can never reach an occluder more than 0.5 m in front of the
|
||
aperture plane. Pins: `Issue129PunchBiasTests` (old form spans >100 m
|
||
at a landblock; capped form ≤0.5 m at all distances; close range
|
||
unchanged).
|
||
|
||
**Gate:** the original spot — distant building doors no longer show
|
||
through terrain/houses at ~a landblock; AND the #108 cellar grass-sweep
|
||
stays gone up close. If a >10 m-range #108-class residue appears, the
|
||
cap constant (0.5 m) is the tuning knob — see AD-18.
|
||
|
||
---
|
||
|
||
## #130 — Background-color strip along the TOP outer edge of a doorway when looking out from inside
|
||
|
||
**Status:** FIX 2 SHIPPED — awaiting user visual re-gate
|
||
**Severity:** LOW-MEDIUM (small strip, but on the most-stared-at pixels in the game)
|
||
**Filed:** 2026-06-12 (user report, post-#119-close session)
|
||
**Component:** render — drawn-shell lift vs draw-space portal consumers (AP-32)
|
||
|
||
**Symptom (user):** standing inside looking out through a doorway, a
|
||
thin strip of background (clear/world) color runs along the OUTER edge
|
||
of the TOP of the doorway opening. Survived the scissor fix (`6c4b6d6`)
|
||
— user screenshot 2026-06-12 evening, "very subtle".
|
||
|
||
**Root cause (the REAL strip, pinned by
|
||
`Issue130DoorwayStripTests.UnliftedGate_LeavesTheStripAtTheDrawnTopEdge`):
|
||
the +0.02 m shell render lift.** Cell shells DRAW 2 cm above the dat
|
||
origin (z-fight vs terrain, AP-32); since `f35cb8b` (the #119-residual
|
||
fix) the visibility graph deliberately uses the PHYSICS (unlifted)
|
||
transform — but the OutsideView color gate and the seal fans, which are
|
||
DRAW-space consumers, kept the unlifted polygons. The drawn lintel
|
||
therefore sits one lift-projection ABOVE the gate's top edge —
|
||
**6.7 px at a 2.4 m doorway** (measured) — and that band gets no
|
||
terrain/sky color while the seal also stamps 2 cm low. Regression from
|
||
`f35cb8b` (2026-06-11), NOT from the W=0 clip port. Vertical edges are
|
||
immune (the lift slides them along themselves) — top edge only, exactly
|
||
as reported.
|
||
|
||
**Fix 2:** draw-space consumers re-apply the lift —
|
||
`PortalVisibilityBuilder.Build(drawLiftZ:)` projects the exit-portal
|
||
OutsideView region with the lifted transform (flood admission, side
|
||
tests, CellViews stay physics-space per f35cb8b), and the seal/punch
|
||
fans lift their world verts. One shared constant
|
||
`PortalVisibilityBuilder.ShellDrawLiftZ` now feeds the shell
|
||
registration, the gate, and the fans. AP-32 register row added (the
|
||
lift had no row). Pins: the lifted gate covers the drawn aperture to
|
||
0.00 px across the 147-combo sweep; the unlifted gate shows the 6.7 px
|
||
strip (sensitivity).
|
||
|
||
**Fix 1 (also real, sub-pixel): `6c4b6d6`** — the doorway-slice scissor
|
||
`Floor(origin)+Ceiling(size)` cut up to 1 px off the top/right edges;
|
||
now a conservative outer bound (`NdcScissorRect`, AD-17 doctrine).
|
||
The W=0 clip port `987313a` is exonerated (CPU pipeline sub-pixel exact
|
||
in like-for-like space).
|
||
|
||
**Gate:** stand inside, look out the door with the lintel on screen,
|
||
sweep the gaze — no background strip at the top edge at any alignment
|
||
or distance.
|
||
|
||
---
|
||
|
||
## #131 — Portal swirl invisible when viewed from inside a building through the doorway
|
||
|
||
**Status:** CLOSED (user-gated 2026-06-12 night: "Ok now it works" — fix 4, `d208002`)
|
||
**Severity:** MEDIUM (portals are landmark objects; the through-door view is common)
|
||
**Filed:** 2026-06-12 (user report, #124 gate session)
|
||
**Component:** render — UNATTACHED emitters have no pass under interior roots
|
||
|
||
**Symptom (user, axiom):** "the portal swirl is missing, when I look out
|
||
from inside a house. Appears when I walk out again."
|
||
|
||
**Root cause (confirmed by read + the [outstage] capture):** every
|
||
particle pass under an interior root is id-FILTERED: the landscape
|
||
slice's Scene pass and the cell/dynamics passes all require
|
||
`emitter.AttachedObjectId != 0` and membership in an owner set. An
|
||
UNATTACHED emitter (`AttachedObjectId == 0` — portal swirls, campfires,
|
||
ground effects anchored at a position) therefore draws NOWHERE when the
|
||
root is interior. The outdoor root has the dedicated T3 pass for
|
||
exactly this class (its own comment: "unattached ones had NO pass on
|
||
outdoor-node frames") — the identical hole on interior-root frames was
|
||
never plugged. Walk out → the T3 pass picks the swirl up → "appears
|
||
when I walk out again". The capture corroborated the rest of the chain
|
||
healthy: outside-stage routing + cone PASS for the dynamics, 57
|
||
attached emitters matched and drawn through the doorway.
|
||
|
||
**Fix (2026-06-12):** `DrawUnattachedSceneParticles` — invoked ONCE per
|
||
interior-root frame at the end of the landscape stage (pre-clear; drawn
|
||
later they would z-fail against the doorway seal), after the #124
|
||
look-ins so swirls blend over far interiors, NOT per slice (alpha
|
||
particles must not double-draw — the #121 lesson). Mutually exclusive
|
||
with the outdoor T3 pass by root kind. Residual (documented): unattached
|
||
INDOOR emitters now draw pre-clear and are overpainted by the room's
|
||
shells — same invisibility as before this fix; the proper per-emitter
|
||
cell classification is a future port.
|
||
|
||
**Apparatus (kept, env-gated):** `ACDREAM_PROBE_OUTSTAGE=1` —
|
||
`[outstage]` (per-slice routing + cone verdicts) + `[outstage-pt]`
|
||
(slice id set, attached matched count, unattached count).
|
||
|
||
**FIX 1 INSUFFICIENT (user screenshots, same evening):** the swirl is
|
||
the portal's TRANSLUCENT MESH, not (only) unattached particles. The
|
||
real mechanism — shared with #132 — is the #124 look-in ordering: the
|
||
slice drew the portal mesh (and all scene particles) BEFORE the look-in
|
||
sub-pass; translucents write no depth, so the far building's interior
|
||
(drawn into its far-Z-punched aperture) overpainted them wherever a
|
||
look-in opening sat behind them on screen. Both screenshots show the
|
||
swirl exactly in front of the hall's doorway. Retail cannot have this
|
||
bug: all landscape-stage alpha draws are deferred into ONE flush after
|
||
LScape::draw (`D3DPolyRender::FlushAlphaList`, DrawCells pc:432722).
|
||
|
||
**FIX 2 (the FlushAlphaList deferral, same commit family as #124):**
|
||
the landscape stage is now TWO phases per frame — EARLY per slice: sky,
|
||
terrain, outdoor static meshes (the look-in punches need their depth, the
|
||
#117 lesson); then the #124 look-ins; then LATE per slice: outside-stage
|
||
dynamics' meshes + ALL attached scene particles + weather + the
|
||
unattached pass. (This FIXED #132 indoors but not the portal.)
|
||
|
||
**ROOT CAUSE (fix 4 — structurally forced; fixes 1–3 were
|
||
real-but-adjacent):** the teleport capture flipped `pCell` to
|
||
**0xA9B4017A — the hall's porch EnvCell** (the portal is a SERVER
|
||
object standing inside a look-in cell), and the headless replay of the
|
||
captured indoor frame proved the look-in flood ADMITS 0x017A (14 cells
|
||
incl. the porch — `Issue131SetupProbeTests.Diagnostic_LookInFlood_*`).
|
||
The partition routes server objects to the dynamics-last pass, where
|
||
(a) the viewcone has NO entries for look-in cells → culled, and (b)
|
||
even un-culled they would z-fail post-seal beyond the root's door plane
|
||
(the #118 lesson). This is exactly AP-33's recorded "look-in DYNAMICS
|
||
are not drawn (deferred)" — the deferred case was the town portal.
|
||
Outdoors the merge path puts the porch in the main cone → drawn →
|
||
"appears when I walk out."
|
||
|
||
**Fix 4:** look-in-cell DYNAMICS draw inside `DrawBuildingLookIns`
|
||
pass 2 (with the statics, whole — AP-33's over-include), and their
|
||
emitters ride the same `DrawCellParticles` call (fix 3). Retail
|
||
equivalent: the nested DrawCells draws the cell's objects
|
||
(`DrawObjCellForDummies` pc:432878+). No double-draw: dynamics-last
|
||
keeps culling them (cell absent from the main cone);
|
||
DrawDynamicsParticles only sees dynamics-last cone survivors.
|
||
|
||
**Gate:** stand inside, look out the doorway at the town portal — the
|
||
swirl renders through the door.
|
||
|
||
---
|
||
|
||
## #132 — Candle flame disappears when the through-opening background is behind it
|
||
|
||
**Status:** CLOSED (user-gated 2026-06-12: indoors "now the candle light is visible", outdoors "Candle works now")
|
||
**Severity:** LOW-MEDIUM
|
||
**Filed:** 2026-06-12 (user report, #124 gate session)
|
||
**Component:** render — slice particles drawn before the #124 look-ins
|
||
|
||
**Symptom (user, axiom):** "I have a candle, when I look at the candle
|
||
when a wall is behind it it shows, but if I turn a bit and the opening
|
||
through a house is behind it candle light disappears."
|
||
|
||
**Root cause (= #131's fix-2 mechanism):** the candle/lantern's flame
|
||
is an attached emitter drawn in the landscape slice's Scene-particle
|
||
pass, which ran BEFORE the #124 look-in sub-pass. Particles write no
|
||
depth; whenever a look-in opening ("the opening through a house") sat
|
||
behind the flame on screen, the far building's interior — drawn into
|
||
its far-Z-punched aperture — overpainted the flame. Against a plain
|
||
wall (no look-in aperture behind), nothing overdraws it → visible.
|
||
Background-dependence explained exactly.
|
||
|
||
**Fix:** the landscape stage's two-phase split (see #131 FIX 2): all
|
||
scene particles moved to the LATE phase, after the look-ins.
|
||
|
||
**Gate 1 result (user):** indoors FIXED ("now the candle light is
|
||
visible when I'm in the house when it is in front of the opening") —
|
||
but the OUTDOOR sibling surfaced ("when I go out it is not showing
|
||
unless I turn so the angle doesn't put it in front of the opening"):
|
||
under an OUTDOOR root the merged building interiors draw AFTER the
|
||
landscape stage, so a slice-drawn flame is overpainted by the punched
|
||
aperture's interior — the residual AP-34 had already recorded.
|
||
|
||
**Fix 2 (outdoor):** outdoor roots skip the slice Scene pass; attached
|
||
outdoor-static scene emitters draw in the POST-FRAME pass alongside the
|
||
T3 unattached pass (depth complete there — flames composite correctly
|
||
against interiors). The owner-id filter carries over; cell-pass and
|
||
dynamics-pass emitters keep their own passes (owners never in the
|
||
outdoor-static set → no double-draw).
|
||
|
||
**Gate:** both sides — indoors with the opening behind the candle, and
|
||
outdoors at the angle that previously erased it.
|
||
|
||
---
|
||
|
||
# Recently closed
|
||
|
||
## #113 — Phantom staircase: REOPENED 2026-06-11, folded into the HOLISTIC BUILDING-RENDER PORT
|
||
|
||
**Status:** REOPENED — root cause #2 found (drawing-BSP-orphaned no-draw polys:
|
||
hall model 0x010014C3 keeps its walkable stair-ramp as dict polys {0,1}, in the
|
||
PhysicsBSP, referenced by NO DrawingBSP node — retail never draws them, we
|
||
iterate the dictionary). The mechanical filter (`e46d3d9`) removed the phantom
|
||
everywhere (user-verified) but made DOORS vanish across Holtburg → un-applied
|
||
(`124c6cb`, helper + dat pins kept). Per the user's 2026-06-11 mandate ("solve
|
||
this holistic once and for all"), #113/#114/#108/#109/the door mystery/#99 are
|
||
now ONE effort: map acdream-vs-retail for building draw / interiors / interior
|
||
collision / dynamics / clipping / culling, then port retail's drawing
|
||
discipline. **CHARTER + paste-ready next-session prompt:**
|
||
[docs/research/2026-06-11-building-render-holistic-port-handoff.md](../research/2026-06-11-building-render-holistic-port-handoff.md).
|
||
The shell-clip work below remains in (outdoor-scoped) and correct.
|
||
|
||
### (history) PView shell clip was never GL-enabled — 927fd8f + scope 9ce335e
|
||
|
||
**Status:** FIXED (self-gated by screenshot comparison at the original spot —
|
||
phantom gone; formal user visual gate pending)
|
||
**Closed:** 2026-06-10
|
||
**Component:** render (PView shell pass GL state)
|
||
|
||
**Attribution (dat-evidenced — the filed "A9B3 misplaced interior cell"
|
||
hypothesis is REFUTED):** the building is the Holtburg MEETING HALL — AAB3
|
||
building[0], model `0x010014C3` at AAB3-local (36,84,116), not an A9B3
|
||
building (A9B3 has exactly ONE building, the #112 hill cottage; the user
|
||
stood at the A9B3/AAB3 boundary — `issue112-gate1.log` cell-transit trail —
|
||
and clicked through the hall to the NPC behind it). The hall's interior
|
||
stair cells (0x100..0x106, a ring climbing z 116→124.5 to the deck hatch)
|
||
have geometry COINCIDENT with the shell's west wall (both at local x=29.0).
|
||
|
||
**Root cause:** retail clips drawn CELL geometry to the accumulated portal
|
||
view (`Render::set_view` :343750 + `planeMask=0xffffffff` per cell polygon
|
||
:427922 → `polyClipFinish`). Our equivalent — `UseShellClipRouting` →
|
||
`mesh_modern.vert` `gl_ClipDistance` — was routed with CORRECT tight clip
|
||
regions (`Issue113MeetingHallFloodTests` proves 4–6 planes, door-aperture
|
||
NDC boxes) but was INERT: `gl_ClipDistance` writes are ignored unless
|
||
`GL_CLIP_DISTANCEi` is enabled, and no caller ever enabled it for the shell
|
||
pass (born inert in `1405dd8`). Flooded interior cells drew WHOLE → the
|
||
interior staircase painted across the exterior wall; unpickable because
|
||
it's cell geometry. 5th instance of
|
||
`feedback_render_self_contained_gl_state`.
|
||
|
||
**Fix (`927fd8f`):** enable `GL_CLIP_DISTANCE0..7` around exactly the shell
|
||
pass in `RetailPViewRenderer.DrawEnvCellShells` (no early-outs between set
|
||
and restore). Entities/characters stay unclipped (retail's mesh path is
|
||
viewcone-check, not poly-clip — comment scoped). Known remaining
|
||
approximation: slot-0 fallback slices (>8-plane apertures) still draw
|
||
pass-all — the assembler's scissor fallback remains unimplemented (rare;
|
||
pinned 0 such slices at the hall).
|
||
|
||
**Refuted along the way (evidence in `Issue113PhantomStairsDumpTests`):**
|
||
the unifying misplaced-cell hypothesis — all 17 A9B3 cottage cells share
|
||
one identical dat Position (nothing to misplace); the #112 gap is a real
|
||
20 cm doorway micro-gap, not a displaced volume; missing object collision
|
||
remains #99/A6.P4. A9B3's dat has NO stair geometry anywhere near the spot
|
||
(shell = balcony slabs z119 + turret roof; cells flat 116/118.8).
|
||
|
||
## #111 — ACE-mutated indoor restores: transparent interior / wrong placement at login — [DONE 2026-06-10 · 5f1eb7c + 5706e0e + 2735695]
|
||
|
||
**Status:** DONE (user-gated: clean indoor logins at two different buildings —
|
||
"it worked", "looked great"; further self-testing across houses ongoing)
|
||
**Closed:** 2026-06-10 (late)
|
||
**Commits:** `5f1eb7c` (claim-authoritative snap + [snap] apparatus) →
|
||
`5706e0e` (ground via physics walkable polygons) → `2735695` (entity snap parity)
|
||
**Component:** physics / player snap
|
||
|
||
**The peel (each layer caught live by the [snap] apparatus):**
|
||
1. **bestCell clobber** (`5f1eb7c`): the legacy Resolve floor-pick scanned every
|
||
CellSurface in the landblock (123 at Holtburg) and broke same-height ties by
|
||
iteration order — it clobbered ACE's CLEAN validated claim 0xA9B40171 with
|
||
0xA9B4013F, seeding the poison loop (our heartbeats reported the clobbered
|
||
cell; ACE persisted it; the next login inherited it). Fix: a VALIDATED indoor
|
||
claim is authoritative (retail SetPositionInternal commits the AdjustPosition
|
||
cell and only settles Z); the snap grounds onto the claim's own floor.
|
||
2. **Triangle-soup grounding** (`5706e0e`): CellSurface includes ceiling/roof TOP
|
||
faces — first-hit grounded onto 0x171's 99.475 ceiling (then poisoned ACE's
|
||
save with the roof height); nearest-to-reference self-confirmed the poison.
|
||
Fix: ground via the PHYSICS walkable polygons (normal.Z ≥ PhysicsGlobals.FloorZ,
|
||
retail find_walkable's filter) — `WalkableFloorZNearest`, cell-local plane drop.
|
||
Verified eating the poisoned restore: claim (0x171, z=99.475) → grounded 94.000.
|
||
3. **Entity snap asymmetry** (`2735695`): login entry snapped only the CONTROLLER;
|
||
the renderer kept the entity at the restored height ("spawned 2 m in the air"
|
||
over a fully-correct interior). Fix: entity.SetPosition + ParentCellId at entry,
|
||
parity with the teleport-arrival path.
|
||
|
||
The ACE-side behavior (server persists ITS physics state, not our reports —
|
||
`SetRequestedLocation` feeds ACE's server-side player) is by design and now fully
|
||
survivable: every restore shape observed tonight (clean / adjacent-room /
|
||
cross-building / cellar-sunk / roof-lofted) lands correctly placed or loudly
|
||
corrected ([spawn-adjust]/[snap] lines). The [snap] diagnostic stays (one line
|
||
per login/teleport).
|
||
|
||
## #107 — Indoor-login spawn wedge — [DONE 2026-06-10 · 1090189]
|
||
|
||
**Status:** DONE (live-verified incl. ACE's own poisoned teleport; final indoor
|
||
logout→login gate pending user)
|
||
**Closed:** 2026-06-10
|
||
**Commit:** `1090189`
|
||
**Component:** physics / player snap, teleport arrival, outbound wire pairs
|
||
|
||
**Root cause (capture `resolve-107-login1.jsonl` + dat scan):** ACE restored a
|
||
POISONED (cell, position) pair — cell `0xA9B40162` (one building) with a position
|
||
inside `0xA9B40171` (a different building 55 m away). The entry snap trusted the
|
||
claim verbatim → fake-grounded limbo (no contact plane/walkable; zero-move
|
||
resolves short-circuit) → first movement demoted the claim to outdoor
|
||
mid-building → 2.4 m fall through the cottage floor onto the terrain under the
|
||
house. Second shape: the PortalSpace teleport-arrival detection gated on
|
||
`differentLandblock || farAway>100m` (invented) — ACE's same-landblock short-hop
|
||
corrections matched neither → movement input frozen all session.
|
||
|
||
**Fix (four legs, retail-anchored):** (1) `PhysicsEngine.Resolve` (player snap)
|
||
runs retail `AdjustPosition` first (SetPositionInternal :283892 step 1;
|
||
AdjustPosition :280009) — `[spawn-adjust]` logs corrections; (2) the deferred
|
||
indoor `seen_outside → adjust_to_outside` sub-fallback completed (+
|
||
`CellPhysics.SeenOutside`); (3) PortalSpace arrival = any player position update
|
||
(holtburger-conformant); (4) outbound wire pairs self-consistent (landblock
|
||
frame from the resolver's full cell id, not the position) + the gate-2 hold
|
||
extension (`IsSpawnCellReady`). Live verification: ACE sent a same-lb dist=69.8
|
||
teleport whose destination was ANOTHER poisoned claim (`0xA9B40150`) — arrival
|
||
completed, `[spawn-adjust]` corrected, player fully controllable.
|
||
Tests: `Issue107SpawnDiagnosticTests` (3 dat-backed conformance facts).
|
||
|
||
## #105 — Intermittent white/missing indoor wall textures — [DONE 2026-06-10 · c787201]
|
||
|
||
**Status:** DONE (probe-verified both directions; visual gate pending user)
|
||
**Closed:** 2026-06-10
|
||
**Commit:** `c787201` (fix + the `ACDREAM_PROBE_TEXFLUSH` apparatus)
|
||
**Component:** render (GL texture upload)
|
||
|
||
**Root cause:** `TextureAtlasManager.AddTexture` only STAGES texture content (PBO write +
|
||
`ManagedGLTextureArray._pendingUpdates`); the actual `TexSubImage3D` copies + mipmap
|
||
regeneration happen in `ProcessDirtyUpdates`, which WB drives once per frame via
|
||
`ObjectMeshManager.GenerateMipmaps()` from its render loop (WB `GameScene.cs:975`).
|
||
GameScene is the file the N.4/O-T4 extraction replaced with `GameWindow`, so the per-frame
|
||
driver was silently dropped. Staged updates only reached the GPU as a side effect of PBO
|
||
growth; every layer staged after an array's LAST growth kept undefined `TexStorage3D`
|
||
content behind a valid resident bindless handle — white/garbage walls, `zh==0`, all dat
|
||
tripwires silent (the dat→decode→stage side had delivered correctly). Only
|
||
`ObjectRenderBatch.BindlessTextureHandle` consumers were affected (EnvCellRenderer cell
|
||
shells = indoor walls); entities resolve via `TextureCache` (immediate) and terrain via
|
||
`TerrainAtlas` (immediate) — which is why only indoor walls ever struck. Intermittency =
|
||
background decode-completion order shuffling which textures land in the never-flushed tail.
|
||
|
||
**Fix:** `WbMeshAdapter.Tick()` now calls `GenerateMipmaps()` after the staged-upload
|
||
drain (Tick runs before all draw passes — the WB-equivalent position).
|
||
|
||
**Evidence:** pre-fix `texflush-prefix.log`: pending updates climb 0→48→…→142 and park at
|
||
126 across 34/34 atlas arrays forever at standstill. Post-fix `texflush-postfix.log` +
|
||
`nearplane-reland-1.log`: `after=0` on every line. The earlier exonerations (dat reads
|
||
safe, membership healthy, "not the probes") all stand — this was the predicted
|
||
"between staging and the draw" GL-side loss.
|
||
|
||
**Tripwires:** the four dat-side tripwires stay (permanent anomaly logging);
|
||
`ACDREAM_PROBE_TEXFLUSH` stays env-gated (zero cost off).
|
||
|
||
## #110 — Near plane 0.1 m vs missing indoor textures — [DONE 2026-06-10 · c787201 + re-land]
|
||
|
||
**Status:** DONE (mechanism resolved; near plane exonerated and re-landed; corner press
|
||
USER-GATED 2026-06-10 evening — camera pressed into the corner no longer clips into the
|
||
wall)
|
||
**Closed:** 2026-06-10
|
||
**Component:** render / camera projection
|
||
|
||
**Resolution:** the missing-texture correlation was the pre-existing #105
|
||
(staged-texture-flush drop, see above), NOT a near-plane mechanism. `znear=0.1` merely
|
||
raised #105's trigger probability exactly as the handoff's only-credible-link predicted:
|
||
a closer near plane makes close-up geometry newly visible → more prepare/upload pressure
|
||
indoors → a larger never-flushed tail. With #105 fixed, retail `Render::znear = 0.1`
|
||
(decomp :342173, initializer :1101867) is re-landed on all four cameras — closing the §4
|
||
corner see-through (the 0.3 m-collided eye no longer near-clips the pressed wall).
|
||
User re-gate: corner press PASSED (2026-06-10 evening, "camera does not clip in the wall
|
||
now when pressed into the corner"). Outstanding (low-risk): distance scan for z-shimmer
|
||
(none expected; retail ships 0.1 with D24) + indoor texture watch over coming launches.
|
||
|
||
## #106 — Outdoor membership freezes at landblock boundaries — [DONE 2026-06-09 · 7078264 + 23adc9c + 6dbbf95 + e6913ac]
|
||
|
||
**Status:** DONE (user-verified: collision + solid walls everywhere; probe-verified crossings)
|
||
**Closed:** 2026-06-09
|
||
**Commits:** `7078264` (LandDefs global-lcoord port) + `23adc9c` (legacy Resolve full
|
||
prefixed ids) + `6dbbf95` (bogus-indoor-claim recovery + spawn-ground entry hold) +
|
||
`e6913ac` (in-world streaming before chase entry)
|
||
**Component:** physics, membership
|
||
|
||
**Resolution:** the outdoor candidate proposal (`CellTransit.AddAllOutsideCells`) AND the
|
||
`find_cell_list` containing-cell pick were clamped to the current landblock's 8×8 grid —
|
||
one step over a boundary → zero candidates → membership frozen forever. Retail runs both
|
||
in a GLOBAL landcell grid (lcoord 0..2039); ported as `AcDream.Core.Physics.LandDefs`
|
||
(decomp-cited; BN int8_t + dropped-192f artifacts and ACE's `add_cell_block` "FIXME!"
|
||
same-block guard documented and avoided —
|
||
`docs/research/2026-06-09-landdefs-outside-cells-pseudocode.md`). The `b3ce505` #98 gate
|
||
was investigated first and definitively exonerated (collision-only, indoor-primary-only).
|
||
|
||
**The gate runs surfaced and fixed three adjacent pre-existing bugs** (each wedged the
|
||
verification walk a different way): legacy `PhysicsEngine.Resolve` returned BARE low-word
|
||
cell ids on every computed exit (the 2026-05-12 L.2e finding — a bare indoor id kills wall
|
||
BSP + the #98 gate misfires + the pick can't recover; prefix survival had been a streaming
|
||
race artifact); the membership pick had no recovery from a hydrated-but-not-containing
|
||
indoor claim (ACE's save was poisoned by the wedged session — restored the #83/A1.7 + #90
|
||
sphere-overlap demotion as the pick's escape hatch); and player-mode entry raced terrain
|
||
hydration (free-fall into void — added the spawn-ground auto-entry hold, which exposed and
|
||
fixed the K-fix1 streaming-vs-chase circular gate).
|
||
|
||
**Verification (gate 4, `probe-cell-106-gate4.log`):** 49 clean `[cell-transit]`
|
||
transitions — south crossing `0xA9B40039→0xA9B30040` at y=−0.19 (the originally frozen
|
||
boundary), east crossing `0xA9B3003D→0xAAB30005` at x=192.2 (a third landblock), clean
|
||
single flips at the block corner, and the originally-failing A9B3 cottage tracked
|
||
room-by-room (`0x0104→…→0x0110`, stairs climbing z 116→119). User confirms collision and
|
||
solid walls work everywhere.
|
||
|
||
**Residual NOT this issue:** transient parts-of-screen-turn-background-color artifacts
|
||
while running and at cottage/room enter–exit persist WITH a correctly-following membership
|
||
anchor — gate 4 disproves the capture doc's full attribution of the running distortion to
|
||
the stale anchor. That residual is the render §4 flap family (edge-on doorway grey +
|
||
corner camera-seal) tracked in `claude-memory/project_render_pipeline_digest.md`.
|
||
|
||
---
|
||
|
||
## Cottage doorway "flap" — [DONE 2026-06-03 · 22a184c + e5457f9 + 79fb6e7] membership pick + render-root clobbering (the TWO causes)
|
||
|
||
**Status:** DONE (user-verified inside-looking-out)
|
||
**Closed:** 2026-06-03
|
||
**Commits:** `b44dd14`/`bc56545`/`22a184c`/`e5457f9` (membership Stage 1) + `79fb6e7` (blue-hole render-root)
|
||
**Component:** physics/membership, rendering
|
||
|
||
**Resolution:** The cottage doorway flap (full-screen bluish void + flicker) had TWO independent
|
||
causes, both fixed this session:
|
||
1. **Membership pick ping-pong** — `CellTransit.BuildCellSetAndPickContaining` used an unordered
|
||
`HashSet` + a pre-pick fork in `FindEnvCollisions`. Ported retail's verbatim ordered `CELLARRAY`
|
||
`find_cell_list` pick (current cell at index 0, interior-wins-break) + the collide-then-pick order
|
||
(`find_env_collisions`→`check_other_cells`, removing the pre-pick that swapped collision geometry
|
||
with the cell mid-tick). `[cell-transit]` 47→13→DELTA=0 while standing still. (Stage 1; faithful.)
|
||
2. **Render-root clobbering** — `CellGraph.CurrCell` ("the player's cell", the render root) was
|
||
written by the PER-ENTITY `ResolveWithTransition`/`ResolveCellId`. A jumping Holtburg NPC near the
|
||
doorway overwrote the player's render root every tick → render rooted at the NPC's tiny connector
|
||
cell (0170) instead of the player's room (0171) → only its ~8-tri shell drew, rest = GL clear color
|
||
= the blue void. Fixed: `CurrCell` is now written ONLY by the player
|
||
(`PhysicsEngine.UpdatePlayerCurrCell` via `PlayerMovementController.UpdateCellId`).
|
||
|
||
Diagnosed via `[flap-cam]`/`[shell]`/`[cell-transit]` (player stable in 0171, render rooted at 0170
|
||
for 77,951 frames). **Residuals are NOT the flap** — three known render phases remain (A
|
||
camera-collision: walls grey while inside; B R1b/#104 particles through ground; C R2 outside-looking-in
|
||
transparent walls) + membership Stage 2 (uniform collision + intrinsic entry, faithfulness debt). Full
|
||
record: [`docs/research/2026-06-03-membership-and-bluehole-shipped-handoff.md`](research/2026-06-03-membership-and-bluehole-shipped-handoff.md).
|
||
|
||
## Phase U.4c doorway "flap" — [DONE 2026-05-31 · 0ee328a] indoor visibility rooted at the camera eye
|
||
|
||
**Status:** DONE (Phase U.4c flap sub-step)
|
||
**Closed:** 2026-05-31
|
||
**Commits:** `0ee328a` (fix) + `13d58ca`/`b5f2bf2`/`8941d1e` (characterization)
|
||
**Component:** rendering, visibility
|
||
|
||
**Resolution:** Crossing a doorway, terrain + building shells + cell shells flapped off
|
||
(grey void + floating entities). Root cause (converged on a live `ACDREAM_PROBE_FLAP`
|
||
capture, after disproving a side-test/`PortalSide` hypothesis and a PVS-grounding
|
||
hypothesis): indoor portal visibility was rooted at the 3rd-person camera **eye**, which
|
||
drifts out of the player's cell; `FindCameraCell` then returned a **stale cell for its 3
|
||
grace frames**, and from that stale root the doorway portal was culled as "behind" the eye
|
||
→ the exit cell + terrain dropped. Fix: root indoor visibility (cell resolution + portal-
|
||
side test) at the **player's cell** (retail `CellManager::ChangePosition` tracks `curr_cell`
|
||
by the player; acdream already roots lighting at the player). Eye still drives projection.
|
||
Visual-verified "flap gone." **Residuals are NOT the flap** — see #78 (terrain not gated
|
||
inside, now more visible) + a new camera-collision need (the chase eye is outside the
|
||
player's cell ~79% of frames → eye-projected clip over-includes → transparent outer walls)
|
||
+ U.5 (outside-looking-in). Full record:
|
||
[`docs/research/2026-05-31-u4c-flap-fixed-and-residuals-handoff.md`](research/2026-05-31-u4c-flap-fixed-and-residuals-handoff.md).
|
||
|
||
## #100 — [DONE 2026-05-25 · f48c74aa + a64e6f2] Transparent rectangular patches around every house (terrain rendering)
|
||
|
||
**Status:** DONE
|
||
**Closed:** 2026-05-25
|
||
**Commits:** `f48c74aa`, `a64e6f2`
|
||
**Component:** rendering, terrain
|
||
|
||
**Resolution (2026-05-25 · #100):** Replaced the cell-level
|
||
`hiddenTerrainCells` mechanism with retail's per-vertex Z nudge
|
||
(`zFightTerrainAdjust = 0.00999999978`) applied inside the modern
|
||
terrain vertex shader. Render terrain everywhere; coplanar building
|
||
floors win the depth test by being 1 cm higher than the rendered
|
||
terrain. Physics path untouched. ~50 LOC of `BuildingTerrainCells`
|
||
plumbing removed across LandblockMesh / LoadedLandblock /
|
||
LandblockLoader / GameWindow / GpuWorldState / LandblockStreamer
|
||
plus the corresponding unit test. Retail anchors:
|
||
acclient_2013_pseudo_c.txt:1120769 + :702254.
|
||
|
||
**Description:** Standing outside any Holtburg house, the ground in a
|
||
rectangular footprint around the building appears as a flat dark patch
|
||
instead of cobblestone / grass terrain. Visible as a sharp-edged
|
||
rectangle the size of the house's outdoor footprint. Same shape on
|
||
every house observed.
|
||
|
||
User report 2026-05-24 (with screenshot): "around every house now I
|
||
missing the ground texture, it is transparent. I can see through the
|
||
ground."
|
||
|
||
**Root cause:** Bisect 2026-05-24 — commit `35b37df` is the introducer. It
|
||
added a `hiddenTerrainCells` parameter to `LandblockMesh.Build` that collapses
|
||
terrain triangles owned by buildings to zero-area degenerates. The hide
|
||
mechanism works at outdoor-cell granularity (24 m × 24 m cells), so the entire
|
||
cell terrain was hidden but the cottage geometry only covers a smaller area inside
|
||
it — leaving a dark transparent rectangle. The fix renders terrain everywhere and
|
||
uses retail's Z nudge to ensure building floors win the depth test.
|
||
|
||
---
|
||
|
||
## #101 — [DONE 2026-05-25 · 5240d65 + 6ca872f] Stair-step cylinder phantom blocks player on multi-part EnvCell entity
|
||
|
||
**Closed:** 2026-05-25
|
||
**Commits:** `f6305b1` — feat(physics): #101 — add IsPhantomGfxObjSource predicate; `5240d65` — fix(physics): #101 — suppress mesh-aabb-fallback for phantom GfxObj stabs; `6ca872f` — docs(test): #101 — sync stale GameWindow.cs line ref in test class doc
|
||
**Component:** physics, dat-handling
|
||
|
||
**Resolution.** `PhysicsDataCache.IsPhantomGfxObjSource(gfxObjId)` predicate returns `true` when
|
||
the entity's `SourceGfxObjOrSetupId` has the GfxObj high byte (`0x01`) AND no cached
|
||
`GfxObjPhysics` entry exists (or its `BSP.Root` is null) — i.e., the underlying GfxObj had
|
||
`HasPhysics=False` so `PhysicsDataCache.CacheGfxObj` short-circuited. The inline
|
||
mesh-AABB-fallback gate at `GameWindow.cs:6127` checks this predicate and skips the shadow-shape
|
||
registration entirely when the source is a phantom. The 10 phantom stair cyls from
|
||
`GfxObj 0x0100081A` (`hasPhys=False`) that previously blocked the player at the foot of the
|
||
Holtburg upper-floor staircase are no longer registered. Collision falls through to entity
|
||
`0x40B50089` (GfxObj `0x01000C16`, `hasPhys=True` BSP with walkable inclined polygon at
|
||
`Normal.Z=0.717`, world ramp from (111.10, 25.50, 94.00)→(107.50, 27.10, 97.50)). 3 unit tests
|
||
in `PhysicsDataCachePhantomSourceTests.IsPhantomGfxObjSource_*` (no BSP → true; has BSP →
|
||
false; non-GfxObj high byte → false) shipped alongside the predicate.
|
||
|
||
**Investigation:** [`docs/research/2026-05-25-a6-stairs-cyl-retail-investigation.md`](research/2026-05-25-a6-stairs-cyl-retail-investigation.md).
|
||
**Plan:** [`docs/superpowers/plans/2026-05-25-issue-101-stairs-cyl-phantom.md`](superpowers/plans/2026-05-25-issue-101-stairs-cyl-phantom.md).
|
||
|
||
**Verification.** Visual-verified at Holtburg upper-floor cottage stairs 2026-05-25 — `[cyl-test]`
|
||
count on `obj=0x40B500*` post-fix = 0 (was 7101 pre-fix); `src=0x0100081A` mesh-aabb-fallback
|
||
count = 0 (was 28 pre-fix). Player climbed Z=94→97.5 holding W continuously over the full 45°
|
||
ramp — no phantom diagonal slides.
|
||
|
||
---
|
||
|
||
## #86 — [DONE 2026-05-19 · 3764867 + 4e308d5] Click selection penetrates walls
|
||
|
||
**Closed:** 2026-05-19
|
||
**Commits:** `3764867` — fix(picker): Cluster A #86 — cell-BSP ray occlusion in WorldPicker; `4e308d5` — test(picker): Cluster A #86 — screen-rect cell-occlusion tests
|
||
**Component:** input, interaction
|
||
|
||
**Resolution:** `WorldPicker.Pick` now accepts a `cellOccluder` callback
|
||
(`CellBspRayOccluder`). Before returning a hit, both `Pick` overloads
|
||
consult the occluder's `NearestWallT` value; any candidate entity whose
|
||
ray parameter exceeds the nearest-wall intersection is filtered out.
|
||
The occluder is wired from `GameWindow` using the loaded `PhysicsDataCache`
|
||
cell structs. Entities behind walls from the camera's perspective are no
|
||
longer selectable. Screen-rect occlusion tests verify the filter across
|
||
several hit/miss scenarios.
|
||
|
||
---
|
||
|
||
## #77 — [DONE 2026-05-18 · 3be7000] Auto-walk doesn't engage at walking range; pickup at walking range overshoots and snaps back
|
||
|
||
**Closed:** 2026-05-18
|
||
**Commit:** `3be7000` — fix(physics): close #77 — auto-walk honors ACE CanCharge bit; zero velocity in turn-in-place
|
||
**Component:** physics / `PlayerMovementController` / `GameWindow.OnLiveMotionUpdated` / `CreateObject.ServerMotionState`
|
||
|
||
**Resolution.** Two coupled bugs sharing a root in
|
||
`PlayerMovementController.DriveServerAutoWalk` + `BeginServerAutoWalk`.
|
||
|
||
1. **Walk-vs-run misclassification (the user-visible "always runs at walk range" half).**
|
||
`BeginServerAutoWalk` decided `_autoWalkInitiallyRunning = (initialDist −
|
||
distanceToObject) >= 1.0f`, forcing run at any chase past ~1.6 m.
|
||
ACE's wire-level walk-vs-run answer is the MovementParameters
|
||
**CanCharge** bit (0x10), which `Creature.SetWalkRunThreshold`
|
||
sets when server-side player→target distance ≥ `WalkRunThreshold/2`
|
||
(= 7.5 m default). Retail's `MovementParameters::get_command`
|
||
(decomp `0x0052aa00`, `acclient_2013_pseudo_c.txt:307946+`) gates
|
||
the run path on CanCharge first; the inner walk_run_threshold
|
||
check practically always walks given ACE's 15 m default. The
|
||
hardcoded 1.0 m threshold pushed run into the 3-5 m walk-range the
|
||
user reported should walk.
|
||
|
||
2. **Velocity leak in turn-in-place phase (the user-visible "overshoots
|
||
and snaps back" half).** When the auto-walked body crossed the
|
||
destination, `desiredYaw` flipped ~180°, `walkAligned` dropped to
|
||
false, and the `if (!moveForward) return true;` branch returned
|
||
without zeroing body velocity. The body kept the prior frame's
|
||
running velocity (`RunAnimSpeed × runRate ≈ 11 m/s`) and slid 4-5 m
|
||
past the target before the turn-around rotation completed.
|
||
|
||
**Changes:**
|
||
- `CreateObject.ServerMotionState.CanCharge`: new bool prop reading
|
||
bit 0x10 of `MoveToParameters`. Cross-ref ACE
|
||
`MovementParams.CanCharge = 0x10`.
|
||
- `PlayerMovementController.BeginServerAutoWalk`: replaces the unused
|
||
`walkRunThreshold` parameter with `bool canCharge`; sets
|
||
`_autoWalkInitiallyRunning = canCharge`.
|
||
- `PlayerMovementController.DriveServerAutoWalk` turn-in-place branch:
|
||
calls `_motion.DoMotion(Ready, 1.0)` and zeros body horizontal
|
||
velocity (preserving Z for gravity). No-op for initial-turn with a
|
||
stationary body; fixes overshoot-recovery and settling cases.
|
||
- `GameWindow.OnLiveMotionUpdated`: passes
|
||
`update.MotionState.CanCharge` through; `[autowalk-begin]` trace
|
||
now shows `canCharge=` instead of `walkRunThresh=`.
|
||
- `GameWindow.InstallSpeculativeTurnToTarget`: predicts ACE's
|
||
CanCharge from local distance using ACE's exact 7.5 m rule, so the
|
||
speculative install agrees with the wire-triggered overwrite that
|
||
arrives moments later.
|
||
|
||
**Verification.** Build green; all targeted test projects pass cleanly
|
||
(Core.Net 294/294, UI.Abstractions 419/419, App 10/10; Core 1073 passed
|
||
/ 8 pre-existing failures unchanged). Visual-verified at Holtburg
|
||
2026-05-18: walk-range NPC click walks + Use fires + dialogue appears,
|
||
walk-range F-key pickup walks + no overshoot + item enters inventory,
|
||
far-range pickup (8-10 m+) still runs.
|
||
|
||
**Lesson archived:** `memory/feedback_autowalk_cancharge_bit.md`. When
|
||
ACE already encodes a decision on the wire (CanCharge IS the walk-vs-run
|
||
answer), relay it — don't reinvent the bucket with a locally-computed
|
||
threshold.
|
||
|
||
---
|
||
|
||
## #56 — [DONE 2026-05-12 · 8735c39] `ParticleHookSink` ignores `CreateParticleHook.PartIndex`; multi-emitter scripts collapse to entity root
|
||
|
||
**Closed:** 2026-05-12
|
||
**Commit chain (newest first):**
|
||
- `8735c39` — feat(vfx #C.1.5b): GpuWorldState fires activator for dat-hydrated entities (4 new fire-sites + 5 integration tests; also picks up EnvCell statics & exterior stabs as a side-effect of the activator-guard relaxation)
|
||
- `5ca5827` — feat(vfx #C.1.5b): activator handles dat-hydrated entities + per-part transforms (resolver returns `ScriptActivationInfo(ScriptId, PartTransforms)`; keys by ServerGuid OR entity.Id; GameWindow resolver lambda upgraded; 4 existing + 3 new tests)
|
||
- `11521f4` — fix(vfx #56): `ParticleHookSink` applies `CreateParticleHook.PartIndex` transform (new `_partTransformsByEntity` side-table; `SpawnFromHook` transforms offset through `partTransforms[PartIndex]` before applying entity rotation; 2 new tests + 2 existing pass)
|
||
- `f3bc15e` — feat(vfx #C.1.5b): `SetupPartTransforms` helper for per-part anchor transforms (walks `PlacementFrames[Resting]` → `[Default]` → first-available; 4 tests)
|
||
- `1e3c33b` — docs(vfx #C.1.5b): design + plan for issue #56 + EnvCell DefaultScript
|
||
|
||
**Component:** vfx / `ParticleHookSink` + `EntityScriptActivator` + `GpuWorldState` + `SetupPartTransforms`
|
||
|
||
**Resolution.** Two-slice fix that also folded in slice 2 of the C.1.5 phase work. **Slice A (the #56 fix proper)**: precomputed per-part `Matrix4x4` array at activator-spawn time via the new `SetupPartTransforms.Compute(setup)` helper, threaded through `EntityScriptActivator` → `ParticleHookSink.SetEntityPartTransforms(entityId, partTransforms)` (mirrors the existing `_rotationByEntity` side-table pattern), applied inside `SpawnFromHook` as `partLocal = Transform(offset, partTransforms[PartIndex])` before the existing world-rotation step. Backwards-compatible: entities without registered part transforms fall through to identity (pre-fix behavior). **Slice B (folded in same phase, makes the fix matter for slice 2 visual gates)**: dropped the activator's `ServerGuid==0` early-return guard. Activator now keys by `entity.ServerGuid` when non-zero, else `entity.Id` — collision-free because dat-hydrated entity IDs live in the `0x40xxxxxx` (interior) / `0x80xxxxxx` (scenery) / `0xC0xxxxxx` ranges, all disjoint from server guids. `GpuWorldState` fires the activator from 4 new sites: `AddLandblock` + `AddEntitiesToExistingLandblock` (Far→Near promotion) for OnCreate, `RemoveLandblock` + `RemoveEntitiesFromLandblock` (Near→Far demotion) for OnRemove. Live entities are filtered out by `ServerGuid != 0` on the `AddLandblock` path so pending-bucket merges don't double-fire OnCreate.
|
||
|
||
**Reality discovery folded into spec §3:** the handoff doc's §4 Q1/Q2 (synthetic-ID scheme + new walker class) were mooted by finding that `GameWindow.BuildInteriorEntitiesForStreaming` already hydrates EnvCell `StaticObjects` as `WorldEntity` instances with stable `entity.Id`. No new walker, no synthetic IDs.
|
||
|
||
**Verification.** Build green. 77 Vfx+Meshing+Activator+Streaming tests pass (4 new for SetupPartTransforms + 2 new for ParticleHookSink + 4 updated + 3 new for activator + 5 new for GpuWorldState integration). 8 pre-existing Physics/Input failures unchanged (verified by stash-and-rerun on Task 4). **Visual verification 2026-05-12**: Holtburg Town network portal (entity `0x7A9B405B`, script `0x3300126D`) — swirl no longer ground-buried, emitters distributed across the arch; Holtburg Inn fireplace flames over the firebox; cottage chimney smoke; spell cast on `+Acdream` cast-anim particles — all match retail.
|
||
|
||
**Acceptance reproducer:** the C.1.5a verification log captured portal A entity `0x7A9B405B` swirl compressed to a partly-ground-buried point. Post-fix at the same portal, the swirl extends through the arch in retail-matching shape.
|
||
|
||
## #53 — [DONE 2026-05-11 · f928e66] A.5/tier1-redo: entity-classification cache retry
|
||
|
||
**Closed:** 2026-05-11
|
||
**Commit chain (newest first):**
|
||
- `f928e66` — incomplete-entity flag must persist across same-entity tuples (mid-list null-renderData)
|
||
- `c55acdc` — skip cache populate when classification is incomplete (drudge fix)
|
||
- `95ebbf3` — key cache by `(entityId, landblockHint)` tuple to defeat ID collision
|
||
- `71d0edc` — namespace stab Ids globally (`0xC0LLBB01..`) for Tier 1 cache safety
|
||
- `4df1914` — clarify `DebugCrossCheck`'s wiring status
|
||
- `f16604b` — DEBUG cross-check + tripwire + 2 tests
|
||
- `489174f` — wire `InvalidateLandblock` callback at LB demote/unload
|
||
- `1d1afcd` — wire `InvalidateEntity` at live-entity despawn
|
||
- `f7e38c2` — cache-hit fast path must fire per-entity, not per-tuple
|
||
- `0cbef3c` — cache-hit fast path + dispatcher integration tests
|
||
- `00fa8ae` — cache `Populate` must flush at entity boundary, not per-MeshRef tuple
|
||
- `2f489a8` — cache-miss populate on first frame for static entities
|
||
- `28513ea` — optional `CachedBatch` collector + `restPose` param on `ClassifyBatches`
|
||
- `a65a241` — inject `EntityClassificationCache` into `WbDrawDispatcher`
|
||
- `60fbfce` — plumb `landblockId` through `_walkScratch`
|
||
- `a171e70`, `aea4460`, `694815c`, `773e970` — cache `InvalidateLandblock` / `InvalidateEntity` / `Populate` / skeleton+first test
|
||
- `c02405c` — extract `GroupKey` to namespace-scope `internal`
|
||
- `2f8a574` — implementation plan
|
||
- `4abb838` — mutation audit + cache design spec
|
||
|
||
**Component:** rendering / `WbDrawDispatcher` / `EntityClassificationCache` / `LandblockLoader`
|
||
|
||
**Resolution.** New `EntityClassificationCache` keyed by `(entityId, landblockHint)` tuple in `src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs`. The dispatcher routes static entities (NOT in `_animatedEntities`) through the cache — first-frame slow-path populates flat `CachedBatch[]` (one entry per (partIdx, batchIdx) with the part-relative `RestPose` and resolved `BindlessTextureHandle`); subsequent-frame cache hits skip classification entirely and append `cached.RestPose * entityWorld` to each matching group. Animated entities bypass. Invalidation fires from `RemoveLiveEntityByServerGuid` (per-entity, `0xF747`/`0xF625`) and `RemoveEntitiesFromLandblock` (per-LB, Near→Far demote + unload).
|
||
|
||
**Perf result.** Entity dispatcher cpu_us **median ~1200 µs, p95 ~1500 µs** at horizon-safe + High preset on AMD Radeon RX 9070 XT @ 1440p. Pre-Tier-1 baseline was ~3500m / ~4000p95. ~66% reduction in median, ~63% in p95. Well under the A.5 spec budget (median ≤ 2.0 ms, p95 ≤ 2.5 ms). No `BUDGET_OVER` flag observed.
|
||
|
||
**Verification.** Build green; full suite 1711 passed / 8 pre-existing physics/input failures unchanged; N.5b sentinel 112/112; visual gate confirmed via `+Acdream` test character (NPCs animate, lifestone renders, multi-part buildings + scenery + Nullified Statue of a Drudge on top of the Foundry all render fully — no airborne geometry, no Z-fighting, no missing parts, no wrong textures).
|
||
|
||
**Lessons surfaced during implementation (4 bug-fix iterations):**
|
||
|
||
1. **Audit must verify ID uniqueness for cache keys.** The original mutation audit verified `Position`/`Rotation`/`MeshRefs` stability post-spawn but didn't verify `entity.Id` was globally unique. Stabs from `LandblockLoader.BuildEntitiesFromInfo` restarted at `nextId = 1` per landblock → cross-LB collisions. Scenery (`0x80LLBB00 + localIndex`) and interior (`0x40LLBB00 + localCounter`) overflow at >256 items/LB. Cache key collision produced "buildings up in the air with wrong textures." Fixed by namespacing stab Ids (`71d0edc`) then by changing cache key to `(entityId, landblockHint)` tuple (`95ebbf3`) — defensive against ALL future hydration paths.
|
||
|
||
2. **Per-tuple iteration with per-entity cache state is a recurring trap.** Three separate bugs caught by code review or visual gate hit this same root cause:
|
||
- Populate fired per-tuple → multi-MeshRef entities lost all but the last MeshRef's batches (`00fa8ae`).
|
||
- Cache hit fired per-tuple → multi-MeshRef entities drew N× copies, severe Z-fighting (`f7e38c2`).
|
||
- Incomplete-flag reset fired per-tuple → mid-list null-MeshRef trees populated partial cache, branches never rendered (`f928e66`).
|
||
|
||
The fix pattern in all three: track previous entity Id (`prevTupleEntityId` / `lastHitEntityId`); execute per-entity logic only on actual entity-change detected against that tracker, not unconditionally per tuple.
|
||
|
||
3. **Async mesh loading interacts with cache populate.** WB's `ObjectMeshManager.PrepareMeshDataAsync` decodes meshes off the main thread. If a MeshRef's GfxObj is still decoding at first-frame visibility, `TryGetRenderData` returns null and the slow path skips it. Without the drudge fix (`c55acdc`), the cache populated a partial classification and cache hits served it forever — even after the missing mesh loaded. With the fix, the dispatcher tracks `currentEntityIncomplete` per entity and drops the populate scratch when any MeshRef returned null; the slow path retries every frame until all meshes load.
|
||
|
||
4. **A/B diagnostic env-var paid for itself.** `ACDREAM_DISABLE_TIER1_CACHE=1` forces every static entity through the slow path. Used twice during debugging to instantly differentiate "bug is in the cache" vs "bug is elsewhere entirely." Kept in tree (read once in `WbDrawDispatcher` ctor) for future cache investigations.
|
||
|
||
**Memory.** See `~/.claude/projects/C--Users-erikn-source-repos-acdream/memory/project_tier1_cache.md` for the audit-gap and per-tuple-vs-per-entity pattern documented for future cache work.
|
||
|
||
---
|
||
|
||
## #54 — [DONE 2026-05-10 · bf31e59] A.5/jobkind-plumbing: far-tier worker loads full entity layer then strips
|
||
|
||
**Closed:** 2026-05-10
|
||
**Commits:** `bf31e59` (factory signature change to 2-arg + back-compat overload + far-tier early-out)
|
||
**Component:** streaming / LandblockStreamer
|
||
|
||
**Resolution.** `LandblockStreamer.cs` primary ctor now takes `Func<uint, LandblockStreamJobKind, LoadedLandblock?>` so the factory can branch on the job kind. A back-compat overload preserves the old single-arg signature for existing test code (5 ctor sites in `LandblockStreamerTests.cs` resolved to the overload with no test changes). `BuildLandblockForStreaming(uint, JobKind)` in `GameWindow.cs` early-outs for `LoadFar` with a heightmap-only path (`_dats.Get<LandBlock>(landblockId)` + `Array.Empty<WorldEntity>()`); near-tier path is unchanged. The Bug A post-load entity strip in `LandblockStreamer.HandleJob` is retained as a `Debug.Assert` + Release safety net. Per-LB worker cost on far-tier dropped from ~tens of ms (LandBlockInfo + scenery + interior) to ~sub-ms (single LandBlock dat read).
|
||
|
||
**Verification.** Build green; 1688/1696 tests pass (8 pre-existing physics/input failures unchanged); 30 streaming-targeted tests (LandblockStreamer + StreamingController + StreamingRegion) all green via the back-compat overload.
|
||
|
||
---
|
||
|
||
## #52 — [DONE 2026-05-10 · e40159f] A.5/lifestone-missing: Holtburg lifestone not rendering
|
||
|
||
**Closed:** 2026-05-10
|
||
**Commits:** `e40159f` (alpha-test discard removal + cull state restoration + uDrawIDOffset uniform)
|
||
**Component:** rendering / WbDrawDispatcher / shaders
|
||
|
||
**Resolution.** Three independent root causes regressed with the WB rendering migration (Phase N.5 retirement amendment, commit `dcae2b6`, 2026-05-08). The original ISSUE #52 hypothesis (Bug A far-tier strip catching the lifestone) was wrong — the lifestone is server-spawned (WCID 509, Setup `0x020002EE`) and never goes through the far-tier strip. Real causes:
|
||
|
||
1. **Alpha-test discard.** `mesh_modern.frag` transparent pass discarded fragments with `α >= 0.95`. The lifestone crystal core surface `0x080011DE` decoded with α≥0.95 across its visible surface, so 100% of the crystal's fragments were discarded — invisible. The original N.5 §2 rationale ("high-α belongs in opaque pass") doesn't hold for surfaces dat-flagged transparent: those pixels can't reach the opaque pass at all. Fix: remove the high-α discard from the transparent pass; keep `α < 0.05` as a fragment-cost optimization.
|
||
|
||
2. **Cull state regression.** Legacy `StaticMeshRenderer` had Phase 9.2's `Enable(CullFace) + Back + CCW` setup at the top of its translucent pass (commit `6f1971a`, 2026-04-11) — fix for "lifestone crystal one face missing" reported at the time. When `dcae2b6` deleted the legacy renderer, the new `WbDrawDispatcher` never inherited that GL state, so closed-shell translucents composited back-faces over front-faces in iteration order under `DepthMask(false)`. Fix: re-establish Phase 9.2's exact setup at the top of Phase 8.
|
||
|
||
3. **`uDrawIDOffset` indexing bug.** `gl_DrawIDARB` resets to 0 at the start of each `glMultiDrawElementsIndirect` call. The transparent pass starts at byte offset `_opaqueDrawCount * stride` in the indirect buffer, but the vertex shader read `Batches[gl_DrawIDARB]` directly — so transparent draws read from `Batches[0..transparentCount)` (the OPAQUE section) instead of `Batches[opaqueCount..end)`. The lifestone crystal's apparent texture flickered to whatever opaque batch sorted to index 0 each frame; with the player character in view, this often appeared as a lifestone wearing the player's body / face textures. Fix: add `uniform int uDrawIDOffset` to `mesh_modern.vert`, change `Batches[gl_DrawIDARB]` to `Batches[uDrawIDOffset + gl_DrawIDARB]`, and set the uniform per-pass in `WbDrawDispatcher` (0 for opaque, `_opaqueDrawCount` for transparent). Mirrors WorldBuilder's `BaseObjectRenderManager.cs:845`.
|
||
|
||
**Verification.** User-confirmed visually via `+Acdream` test character at the Holtburg outdoor lifestone (Z=94 platform). Tests 1688/1696 passing (8 pre-existing physics/input failures unchanged). N.5b conformance sentinel 94/94 clean.
|
||
|
||
**Lesson.** The WB rendering migration's "lift legacy state into the new dispatcher" was incomplete in two non-obvious ways: (a) GL state setup that lived inside legacy per-pass blocks, and (b) shader uniforms that the legacy per-draw flow didn't need but the multi-draw-indirect flow does. Future WB-migration work should systematically diff the legacy renderer's GL setup + shader I/O against the new dispatcher's. The `uDrawIDOffset` bug was particularly hidden because it only manifested for entities that mixed transparent draws with the visible opaque sort order — single-pass content (pure opaque or pure transparent) was unaffected.
|
||
|
||
---
|
||
|
||
## #13 — [DONE 2026-05-10 · d3b58c9..078919c] PlayerDescription trailer past enchantments
|
||
|
||
**Closed:** 2026-05-10
|
||
**Commits:** `d3b58c9` (scaffold) → `6587034` (rename nit) → `becbde6` (OptionFlags+Options1) → `9a0dfe0` (TrailerTruncated + diag) → `f7a5eea` (Shortcuts) → `8cbb991` (HotbarSpells) → `75e8e26` (DesiredComps) → `b17dc3b` (SpellbookFilters) → `98eebef` (Options2) → `d9a5e40` (strict Inventory+Equipped) → `91693ea` (heuristic GAMEPLAY_OPTIONS walker) → `58095d8` (combined fixture test) → `078919c` (ItemRepository wiring)
|
||
**Component:** net / player-state
|
||
**Plan:** [`docs/superpowers/plans/2026-05-10-issue-13-pd-trailer.md`](../docs/superpowers/plans/2026-05-10-issue-13-pd-trailer.md)
|
||
|
||
**Resolution.** `PlayerDescriptionParser` now walks every trailer
|
||
section through Inventory + Equipped, ported faithfully from holtburger
|
||
`events.rs:503-625` + `shortcuts.rs:13-34`. The trickiest piece —
|
||
`gameplay_options` — uses a 4-byte-aligned forward heuristic
|
||
(`TryHeuristicInventoryStart`) that probes candidate offsets with a
|
||
strict `(inventory + equipped consume to EOF)` test, mirroring
|
||
holtburger's `find_inventory_start_after_gameplay_options`.
|
||
|
||
The trailer walk is wrapped in its own inner try/catch (separate from
|
||
the outer parse-wide catch) so a malformed trailer cannot destroy the
|
||
already-extracted attribute / skill / spell / enchantment data. A new
|
||
`Parsed.TrailerTruncated` flag lets callers distinguish a clean parse
|
||
from a graceful-degradation parse (set true if the inner catch fires;
|
||
log under `ACDREAM_DUMP_VITALS=1`).
|
||
|
||
`GameEventWiring`'s `PlayerDescription` handler now registers each
|
||
inventory entry with `ItemRepository.AddOrUpdate(...)` and applies
|
||
`MoveItem(...)` for equipped entries so paperdoll picks up
|
||
`CurrentlyEquippedLocation` at login. The acceptance criterion
|
||
"`ItemRepository.Count` after login > 0" is now exercised by
|
||
`PlayerDescription_RegistersInventoryEntries_InItemRepository` in
|
||
`GameEventWiringTests`.
|
||
|
||
12 tasks, 13 commits, +9 PD parser tests + 1 wiring test (20 PD tests
|
||
total, 282 Net.Tests pass). Code-review nits during the run produced
|
||
two refactor commits: `Shortcut → ShortcutEntry` rename to avoid a
|
||
homograph with the `CharacterOptionDataFlag.Shortcut` flag bit
|
||
(`6587034`); `TrailerTruncated` flag + diagnostic logging
|
||
(`9a0dfe0`).
|
||
|
||
Forward-looking notes (low priority, no follow-up issues filed):
|
||
|
||
- `WeenieClassId = inv.ContainerType` for inventory entries is a
|
||
placeholder; `CreateObject` overwrites it with the real weenie class
|
||
later in the login sequence.
|
||
- The 10,000 count cap throws `FormatException` on validation failure,
|
||
which the inner catch treats the same as truncation. If a future
|
||
diagnostic UI needs to distinguish "EOF mid-section" from "garbage
|
||
count rejected", split `TrailerTruncated` into two flags. For now
|
||
the `ACDREAM_DUMP_VITALS=1` log message gives the developer enough
|
||
signal.
|
||
|
||
Files: `src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs`,
|
||
`src/AcDream.Core.Net/GameEventWiring.cs`,
|
||
`tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs`,
|
||
`tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs`.
|
||
|
||
---
|
||
|
||
## #51 — [DONE 2026-05-09 · da56063 + N.5b SHIP] WB's terrain-split formula diverges from retail's `FSplitNESW`
|
||
|
||
**Closed:** 2026-05-09
|
||
**Commit:** `da56063` (black-terrain fix; landed within Phase N.5b — see
|
||
`docs/superpowers/plans/2026-05-09-phase-n5b-terrain-modern.md` for the
|
||
ship commit chain)
|
||
**Component:** terrain math / Phase N.5b
|
||
|
||
**Resolution: Path C.** Phase N.5b lifted terrain rendering onto the
|
||
modern path (bindless atlas + `glMultiDrawElementsIndirect`) WITHOUT
|
||
adopting WB's `TerrainUtils.CalculateSplitDirection`. The pre-implementation
|
||
divergence test (`tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs`)
|
||
confirmed the two formulas disagree on **49.98%** of sweep cells —
|
||
fundamentally incompatible with our shared physics + visual mesh, which
|
||
both rely on retail's `FSplitNESW` (constants `0x0CCAC033` / `0x421BE3BD` /
|
||
`0x6C1AC587` / `0x519B8F25`).
|
||
|
||
Path C: keep retail's `FSplitNESW` formula via `LandblockMesh.Build` →
|
||
`TerrainBlending.CalculateSplitDirection`; mirror WB's `TerrainRenderManager`
|
||
architectural pattern (single global VBO/EBO + slot allocator + bindless
|
||
atlas + multi-draw indirect) but feed it acdream's mesh. Modern dispatcher
|
||
(`TerrainModernRenderer`) replaces `TerrainChunkRenderer` (deleted in T9
|
||
along with `TerrainRenderer` + `terrain.vert/.frag`).
|
||
|
||
Path A (substitute WB's formula) was killed by the divergence test.
|
||
Path B (fork-patch WB's renderer to use retail's formula) was rejected
|
||
for permanent maintenance burden. Path C ships the architectural
|
||
pattern while preserving retail-formula compliance.
|
||
|
||
Visual mesh and physics both still consume retail's `FSplitNESW`; they
|
||
remain in lockstep, no triangle-Z hover. The N.6 / N.7 sequencing
|
||
implication this issue carried (substitute physics math only when the
|
||
visual mesh migrates) is moot — neither side ever switches to WB's
|
||
formula.
|
||
|
||
**Files added:**
|
||
- `src/AcDream.App/Rendering/TerrainModernRenderer.cs`
|
||
- `src/AcDream.Core/Terrain/TerrainSlotAllocator.cs`
|
||
- `src/AcDream.App/Rendering/Shaders/terrain_modern.vert`
|
||
- `src/AcDream.App/Rendering/Shaders/terrain_modern.frag`
|
||
- `tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs` (the
|
||
test that killed Path A)
|
||
|
||
**Files deleted (T9):**
|
||
- `src/AcDream.App/Rendering/TerrainChunkRenderer.cs`
|
||
- `src/AcDream.App/Rendering/TerrainRenderer.cs`
|
||
- `src/AcDream.App/Rendering/Shaders/terrain.vert`
|
||
- `src/AcDream.App/Rendering/Shaders/terrain.frag`
|
||
|
||
---
|
||
|
||
## #43 — [DONE 2026-05-05 · 9e4772a] Slope staircase on observed player remotes (anim-only fallback ignored slope)
|
||
|
||
**Closed:** 2026-05-05
|
||
**Commit:** `9e4772a`
|
||
**Component:** motion (`PositionManager.ComputeOffset` queue-empty fallback)
|
||
|
||
**Resolution:** Grounded player remotes showed a ~5 Hz Z staircase when
|
||
running up/down hills. `PositionManager.ComputeOffset` has two modes:
|
||
queue-active (3D direction toward server's broadcast position, Z
|
||
follows naturally) and queue-empty / head-reached (`seqVel × dt`
|
||
rotated into world). Every locomotion cycle bakes Z=0 in body-local,
|
||
so the world result has Z=0 too. With server UPs at ~5 Hz and
|
||
catchUpSpeed = 2× maxSpeed, body chases each waypoint in ~100ms (Z
|
||
ramps), then sits in seqVel-only mode for ~100ms (Z flat) until the
|
||
next UP. Visible 5 Hz staircase.
|
||
|
||
Fix mirrors retail's `CTransition::adjust_offset` contact-plane
|
||
projection (named-retail acclient_2013_pseudo_c.txt:272296-272346),
|
||
applied at the queue-empty boundary instead of inside the sweep.
|
||
`ComputeOffset` gains an optional `Vector3? terrainNormal`; when
|
||
the seqVel fallback runs and the supplied normal is non-trivial,
|
||
`rootMotionWorld -= N × dot(rootMotionWorld, N)`. XY motion gains a
|
||
Z component proportional to slope × forward speed; body Z follows the
|
||
terrain mesh between UPs. No-op on flat ground (N ≈ +Z, dot ≈ 0) so
|
||
no regression to L.3 M2's flat-ground verification.
|
||
|
||
`GameWindow.TickAnimations` grounded-remote path samples
|
||
`PhysicsEngine.SampleTerrainNormal` (a thin public wrapper over the
|
||
existing internal `SampleTerrainWalkable`) at the body's current XY
|
||
each tick and passes it to `ComputeOffset`.
|
||
|
||
Two unit tests in `PositionManagerTests`: 30° east-tilted slope
|
||
(asserts `(3.0, 0, −1.732)` for 4 m/s east motion over 1s — body
|
||
descends along slope) + flat-ground no-op (asserts unchanged
|
||
behaviour with `N = +Z`).
|
||
|
||
Verified via `launch-slope-verify.log` over a 34m vertical traversal:
|
||
9,193 queue-empty-with-non-zero-offset.Z ticks on slopes (the path
|
||
that previously stair-cased), 26,497 sloped-normal ticks total, zero
|
||
#42 regressions.
|
||
|
||
**Diagnostic kept in tree:** `ACDREAM_SLOPE_DIAG=1` enables the
|
||
`[SLOPE]` per-tick trace (`bodyZ` before/after, offset, queue active,
|
||
sampled `cpN.Z`) for future regression hunts.
|
||
|
||
---
|
||
|
||
## #31 — [DONE 2026-04-29] Low outdoor cell id can go stale after transition movement
|
||
|
||
**Closed:** 2026-04-29
|
||
**Commit:** `(this commit)`
|
||
**Resolution:** `ResolveWithTransition` now refreshes outdoor cell ownership
|
||
from the resolved world position while the sphere sweep runs. Intra-landblock
|
||
24m outdoor seams update the low cell id, and full-cell callers crossing a
|
||
landblock seam get the destination landblock prefix plus the correct outdoor
|
||
low cell.
|
||
|
||
---
|
||
|
||
## #34 — [DONE 2026-04-29] Missing routine local/server correction diagnostic
|
||
|
||
**Closed:** 2026-04-29
|
||
**Commit:** `(this commit)`
|
||
**Resolution:** Added `ACDREAM_DUMP_MOVE_TRUTH=1`, which logs local resolved
|
||
position/contact/cell, outbound movement fields, server `UpdatePosition` echo,
|
||
and local/server correction delta for the player in grep-friendly
|
||
`move-truth OUT` / `move-truth ECHO` lines.
|
||
|
||
---
|
||
|
||
## #30 — [DONE 2026-04-29] AutonomousPosition contact byte is too often grounded
|
||
|
||
**Closed:** 2026-04-29
|
||
**Commit:** `(this commit)`
|
||
**Resolution:** `GameWindow` now derives the movement contact byte from
|
||
`MovementResult.IsOnGround` and passes it explicitly to both `MoveToState.Build`
|
||
and `AutonomousPosition.Build`. Added packet tests proving both builders encode
|
||
an explicit airborne contact byte.
|
||
|
||
---
|
||
|
||
## #27 — [DONE 2026-04-26] Cloud meshes appeared missing or faint vs retail
|
||
|
||
**Closed:** 2026-04-26
|
||
**Commit:** `4678b3e fix(sky): apply per-Surface Translucency + Luminosity for retail-faithful weather`
|
||
**Resolution:** Resolved as a side-effect of the Bug A fix. The original observation came from a session where every sky mesh got `effEmissive = 1.0` (saturated `vTint` to white), which made stars/clouds look full-bright instead of time-of-day-tinted. Fix 2 corrected the emissive default to `sub.SurfLuminosity` so cloud surfaces (Lum=0.0) now run through the ambient+diffuse vertex-lit path and pick up keyframe tint. Fix 1 separately plumbed `surface.Translucency` to the shader, picking up the 0.25 translucency on cloud surface `0x08000023` (75% opacity). Visual verification under Phase 0 of the followup plan: clouds and colors now match retail at LCG-picked DayGroups across the day cycle.
|
||
|
||
---
|
||
|
||
## #1 — [DONE 2026-04-26] Rain falls only to horizon, not to the player's feet
|
||
|
||
**Closed:** 2026-04-26
|
||
**Commits:** `3e0da49` (sky pass split + retail -120m Z offset), `4678b3e` (Surface.Translucency + Luminosity correctness), `d95a8d2` (legacy emitter delete)
|
||
**Resolution:** Two-part fix. First, rain rendering was completely re-architected to match retail's `LScape::draw` pattern at `0x00506330` — sky pass before the landblock loop (`RenderSky`), weather pass after (`RenderWeather`). Weather meshes now overlay terrain instead of being painted over. Camera anchored inside the rain cylinder via the retail-correct -120m Z offset (constant `0xc2f00000` in `GameSky::UpdatePosition` at `0x00506dd0`). Second, the per-Surface `Translucency` float (rain = 0.5) and `Luminosity` float (rain = 0.1484) were both being ignored by the renderer; plumbed end-to-end so streaks contribute at retail-correct intensity instead of 6.7× too bright. Legacy camera-attached particle emitter (`UpdateWeatherParticles` + `BuildRainDesc` + `BuildSnowDesc`) deleted; world-space mesh is the only path now. Snow rides the same fix automatically. Filed alongside two follow-up issues from the visual-verify session: `#27` (cloud rendering parity), `#28` (aurora/northern lights).
|
||
|
||
---
|
||
|
||
## #26 — [DONE 2026-04-26] Stars rendered as a square in one corner of the sky
|
||
|
||
**Closed:** 2026-04-26
|
||
**Commit:** `7b88fde fix(sky): drive wrap mode from mesh UV range — fixes Bug B (stars-as-square)`
|
||
**Resolution:** SkyRenderer's wrap-mode heuristic was `GL_CLAMP_TO_EDGE unless TexVelocity != 0`, which mis-classified the inner sky/star layer `0x010015EF` (UVs in `[0.398, 4.602]`, TexVel=0). Most of the dome sampled the texture's edge texels; only the small region where UVs fell in `[0,1]` showed actual texture content. Fixed by computing `NeedsUvRepeat` per submesh from the actual UV range during `GfxObjMesh.Build()` and driving the wrap-mode choice from that flag plus the existing scrolling check. Outer dome `0x010015EE/F0/F1/F2` (UVs strictly in `[0,1]`) keeps `CLAMP_TO_EDGE` so no seam regression. Probe `tools/StarsProbe/` (commit `991fb9a`) committed alongside as the diagnostic that found this.
|
||
|
||
---
|
||
|
||
## #25 — [DONE 2026-04-26] Phase K.3 — Settings panel + click-to-rebind UI
|
||
|
||
**Closed:** 2026-04-26
|
||
**Commit:** `(this commit)`
|
||
**Resolution:** `SettingsPanel` with click-to-rebind UX (modal capture
|
||
via `InputDispatcher.BeginCapture`, Esc cancels, conflict prompt with
|
||
Yes/No, draft / Save / Cancel semantics), F11 toggle + ImGui
|
||
MainMenuBar entry, per-action / per-section / reset-all-defaults
|
||
buttons. Roadmap + ISSUES + memory crib + CLAUDE.md updated.
|
||
|
||
---
|
||
|
||
## #24 — [DONE 2026-04-26] Phase K.2 — auto-enter player mode + MMB mouse-look
|
||
|
||
**Closed:** 2026-04-26
|
||
**Commit:** `af74eac`
|
||
**Resolution:** Auto-enter player mode at login (one-shot guard
|
||
reusing the existing Tab handler logic); MMB-hold mouse-look
|
||
(`CameraInstantMouseLook` — cursor-locked camera + character yaw
|
||
drive together); `Tab → ChatPanel.FocusInput()`; `DebugPanel`
|
||
"Toggle Free-Fly Mode" button.
|
||
|
||
---
|
||
|
||
## #23 — [DONE 2026-04-26] Phase K.1c — retail-default keymap + JSON persistence
|
||
|
||
**Closed:** 2026-04-26
|
||
**Commit:** `da18910`
|
||
**Resolution:** ~149 retail-faithful bindings byte-precise to
|
||
`docs/research/named-retail/retail-default.keymap.txt`;
|
||
`%LOCALAPPDATA%\acdream\keybinds.json` with merge-over-defaults
|
||
migration; acdream debug F-keys relocated to `Ctrl+F*`.
|
||
|
||
---
|
||
|
||
## #22 — [DONE 2026-04-26] Phase K.1b — cut handlers over to dispatcher
|
||
|
||
**Closed:** 2026-04-26
|
||
**Commit:** `256e962`
|
||
**Resolution:** Drop the legacy mouse-X-character-yaw path; fix
|
||
`WantCaptureMouse` gating; single input path via the multicast
|
||
`InputDispatcher`.
|
||
|
||
---
|
||
|
||
## #21 — [DONE 2026-04-26] Phase K.1a — input architecture skeleton
|
||
|
||
**Closed:** 2026-04-26
|
||
**Commit:** `84512d3`
|
||
**Resolution:** Action enum, multicast `InputDispatcher` with scope
|
||
stack, `KeyChord` / `Binding` / `KeyBindings`, Silk.NET adapters;
|
||
parallel to existing handlers (no behavior change).
|
||
|
||
---
|
||
|
||
## #20 — [DONE 2026-04-25] CombatChatTranslator — retail-faithful combat-text formatters
|
||
|
||
**Closed:** 2026-04-25
|
||
**Commit:** `3d26c8e`
|
||
**Resolution:** Retail-faithful combat-text formatters into `ChatLog` ("You hit drudge for 50 slashing damage"). Subscribes to `CombatState`'s `DamageTaken` / `DamageDealtAccepted` / `EvadedIncoming` / `MissedOutgoing` / `AttackDone` / `KillLanded` events; templates ported verbatim from holtburger `panels/chat.rs:221-308`.
|
||
|
||
---
|
||
|
||
## #19 — [DONE 2026-04-25] TurbineChat codec (0xF7DE) + ChatChannelInfo
|
||
|
||
**Closed:** 2026-04-25
|
||
**Commit:** `ca968fc`
|
||
**Resolution:** Full `0xF7DE` codec with three payload variants (`EventSendToRoom`, `RequestSendToRoomById`, `Response`), UTF-16LE strings with variable-length prefix, `SetTurbineChatChannels (0x0295)` parser, unified `ChatChannelInfo` (Legacy + Turbine variants), `TurbineChatState`. **Note: ACE doesn't run a TurbineChat server — codec is ready for retail-server-emulating setups.**
|
||
|
||
---
|
||
|
||
## #18 — [DONE 2026-04-25] Holtburger inbound chat parity + Windows-1252 codec
|
||
|
||
**Closed:** 2026-04-25
|
||
**Commit:** `ff5ed9e`
|
||
**Resolution:** `EmoteText (0x01E0)` / `SoulEmote (0x01E2)` / `ServerMessage (0xF7E0)` / `PlayerKilled (0x019E)` parsers + `WeenieError` routing through `GameEventWiring`. Global codec switch from `Encoding.ASCII` to `Encoding.GetEncoding(1252)`; matches retail + holtburger; accented names round-trip correctly.
|
||
|
||
---
|
||
|
||
## #17 — [DONE 2026-04-25] ChatPanel input field + slash commands
|
||
|
||
**Closed:** 2026-04-25
|
||
**Commit:** `f14296c`
|
||
**Resolution:** `ChatPanel` gains Enter-to-submit input field; `ChatInputParser` recognises `/say` `/t` `/tell` `/r` `/g` `/f` `/a` `/m` `/p` `/v` `/cv` `/lfg` `/trade` `/role` `/society` `/olthoi`; `ChatVM` tracks `LastIncomingTellSender` for `/r` reply.
|
||
|
||
---
|
||
|
||
## #16 — [DONE 2026-04-25] LiveCommandBus + WorldSession chat senders
|
||
|
||
**Closed:** 2026-04-25
|
||
**Commit:** `8e6e5a0`
|
||
**Resolution:** Real `ICommandBus` impl + `WorldSession.SendTalk` / `SendTell` / `SendChannel` wrappers + `SendChatCmd` record + `ChannelResolver` legacy-id mapping per holtburger.
|
||
|
||
---
|
||
|
||
## #15 — [DONE 2026-04-25] DebugPanel migration
|
||
|
||
**Closed:** 2026-04-25
|
||
**Commit:** `56037a4`
|
||
**Resolution:** Migrates the 473-LOC StbTrueTypeSharp `DebugOverlay` to an ImGui `DebugPanel` with collapsing-headers + checkbox diagnostics + combat-event tail. Deletes `DebugOverlay.cs`; `TextRenderer` + `BitmapFont` kept for future HUD-in-world (D.6 damage floaters, name plates).
|
||
|
||
---
|
||
|
||
## #14 — [DONE 2026-04-25] IPanelRenderer widget extension
|
||
|
||
**Closed:** 2026-04-25
|
||
**Commit:** `b131514`
|
||
**Resolution:** Adds 14 widget signatures (`TextColored` / `Checkbox` / `Combo` / `InputTextSubmit` / `BeginTable` / etc.) to `IPanelRenderer` + `ImGuiPanelRenderer` impl. Foundation for I.2 DebugPanel and I.4 ChatPanel input.
|
||
|
||
---
|
||
|
||
## #7 — [DONE 2026-04-25] PlayerDescription parser stops after spells (enchantment block parsed)
|
||
|
||
**Closed:** 2026-04-25
|
||
**Commit:** `feat(net): #7 PlayerDescriptionParser — enchantment block walker + StatMod flow`
|
||
**Resolution:** Extended `PlayerDescriptionParser` past the spell block to parse the Enchantment trailer per holtburger `events.rs:462-501`. Added `EnchantmentEntry` record with full wire payload (16 fields including the `StatMod` triad — type/key/val) + `EnchantmentBucket` (Multiplicative / Additive / Cooldown / Vitae per `EnchantmentMask`). `Parsed` now exposes `IReadOnlyList<EnchantmentEntry> Enchantments`. `GameEventWiring` routes each entry through the new `Spellbook.OnEnchantmentAdded(ActiveEnchantmentRecord)` overload with `StatModType` / `StatModKey` / `StatModValue` / `Bucket` populated. 2 new parser tests cover the enchantment block schema + Vitae singleton.
|
||
|
||
The remaining trailer sections (options / shortcuts / hotbars / inventory / equipped) are not yet parsed; filed as #13. Stopping after enchantments is intentional — it covers the highest-value section (issue #6 lights up) and avoids the heuristic `gameplay_options` walker that #13 needs.
|
||
|
||
---
|
||
|
||
## #12 — [DONE 2026-04-25] Capture full Enchantment wire payload (StatMod) on ActiveEnchantmentRecord
|
||
|
||
**Closed:** 2026-04-25
|
||
**Commit:** `feat(net): #7 PlayerDescriptionParser — enchantment block walker + StatMod flow`
|
||
**Resolution:** Closed alongside #7 in the same commit. `ActiveEnchantmentRecord` extended with optional `StatModType`, `StatModKey`, `StatModValue`, `Bucket` fields. `Spellbook` got an `OnEnchantmentAdded(ActiveEnchantmentRecord)` overload that accepts the full record. `EnchantmentMath.GetMod` aggregator now consumes the StatMod data: multiplicative bucket (1) → multiplier ×= val; additive bucket (2) → additive += val; vitae bucket (8) → multiplier ×= val (applied last, matching retail `CEnchantmentRegistry::EnchantAttribute` semantics). 5 new EnchantmentMath StatMod-aware tests cover: multiplicative buffs aggregate, additive buffs sum, stat-key mismatch is filtered out, vitae applies multiplicatively, family-stacking picks the higher spell-id buff.
|
||
|
||
`ParseMagicUpdateEnchantment` (the live-update opcode 0x02C2) is **not** yet extended — it still uses the 4-field summary. That's a separate refactor; PlayerDescription's enchantment block is the load-bearing path for issue #6, and that's now flowing.
|
||
|
||
---
|
||
|
||
## #6 — [DONE 2026-04-25 architecture; data flowing as of #12] Vital max ignores enchantment buffs + vitae
|
||
|
||
**Closed:** 2026-04-25
|
||
**Commit:** `feat(player): #6 fold enchantment buffs into vital max via EnchantmentMath`
|
||
**Resolution:** Ported `CEnchantmentRegistry::EnchantAttribute` (PDB `0x00594570`) as `EnchantmentMath.GetMod(IEnumerable<ActiveEnchantmentRecord>, SpellTable, statKey)` returning `(Multiplier, Additive)`. Family-stacking dedup via `SpellTable.Family` (only one buff per family bucket wins, by highest spell-id as a generation proxy). `Spellbook.GetVitalMod(statKey)` delegates. `LocalPlayerState.GetMaxApprox` reworked to apply `(unbuffed × mult) + add` with retail's min-vital clamp (`>= 5` if base ≥ 5 else `>= 1`, matches `CreatureVital::GetMaxValue` at PDB `0x0058F2DD`). Stat-key constants (`MaxHealth=1`, `MaxStamina=3`, `MaxMana=5`) verified against `docs/research/named-retail/acclient.h` line 37287-37301.
|
||
|
||
**Architecture in place; data still flat.** Until ISSUES.md #12 lands the wire-format extension that captures `StatMod (type/key/val)` on `ActiveEnchantmentRecord`, the per-enchantment modifier value isn't aggregated yet — `EnchantmentMath.GetMod` returns `Identity (1.0, 0.0)` for every stat key. Once #12 wires the data, the existing aggregator + formula light up automatically. Live `+Acdream` Stam/Mana percent will continue to read ~95% until #12 lands.
|
||
|
||
6 new EnchantmentMathTests cover: empty list returns Identity, no-table-entries returns Identity, stat-key constants match ACE enum, Identity is `(1, 0)`, family-stacking dedup, family=0 (no-bucket) treated as separate.
|
||
|
||
---
|
||
|
||
## #11 — [DONE 2026-04-25] Spell metadata loader (spells.csv → SpellTable)
|
||
|
||
**Closed:** 2026-04-25
|
||
**Commit:** `feat(spells): #11 SpellTable — hydrate metadata from spells.csv at startup`
|
||
**Resolution:** Added `SpellMetadata` record + `SpellTable` CSV loader (hand-rolled RFC 4180-ish parser for the quoted Description column with embedded commas). Wired into `Spellbook` constructor as optional metadata source; `Spellbook.TryGetMetadata(spellId, out)` returns the static record when found. `GameWindow` loads `data/spells.csv` from bin output at construction (file copied via `<None Include>` in `AcDream.App.csproj` from `docs/research/data/spells.csv`). Falls back to `SpellTable.Empty` + console warning if the file is missing (e.g. tooling contexts). 10 new tests covering: empty table, header-only, simple row, quoted description with commas, blank lines skipped, bad spell-id rows skipped, lookup hit/miss, RFC 4180 escaped-quote parsing.
|
||
|
||
---
|
||
|
||
## #9 — [DONE 2026-04-25] Address-correction sweep on `acclient_function_map.md`
|
||
|
||
**Closed:** 2026-04-25
|
||
**Commit:** `docs(research): #9 sweep acclient_function_map.md against PDB symbols`
|
||
**Resolution:** Wrote `tools/pdb-extract/check_function_map.py` that cross-checks 63 hand-curated entries against `docs/research/named-retail/symbols.json`. Findings: **zero entries matched address-and-name exactly** (confirms ~0x800-0xC10 byte delta vs the binary that produced our Ghidra chunks — different build revision). 38 entries corrected by PDB name lookup; 25 entries either lack PDB symbol records (inlined / non-public) or had wrong class assignments (e.g. `0x5387C0` claimed as `CTransition::find_collisions` was actually `CPolygon::polygon_hits_sphere`). Updated `acclient_function_map.md` with corrected addresses, kept legacy addresses in a "Was" column for traceability, added a top-of-file sweep summary.
|
||
|
||
---
|
||
|
||
## #10 — [DONE 2026-04-25] Wire `KillerNotification (0x01AD)`
|
||
|
||
**Closed:** 2026-04-25
|
||
**Commit:** `docs(issues): #8/#9/#11 filed; #10 wired (KillerNotification)`
|
||
**Resolution:** Orphan parser at `GameEvents.ParseKillerNotification` existed but was never registered for dispatch in `GameEventWiring.cs`. Added a `combat.OnKillerNotification(victimName, victimGuid)` method on `CombatState` that fires a new `KillLanded` event, then registered the handler. One-line dispatch + 12-line CombatState method + one regression test fixture in `GameEventWiringTests`.
|
||
|
||
---
|
||
|
||
## #8 — [DONE 2026-04-25] pdb-extract tool: PDB → symbols.json + types.json
|
||
|
||
**Closed:** 2026-04-25
|
||
**Commit:** `tools(pdb-extract): #8 PDB -> symbols.json + types.json sidecar`
|
||
**Resolution:** Pure-Python (no deps) MSF 7.00 PDB parser at `tools/pdb-extract/pdb_extract.py`. Reads `refs/acclient.pdb` (Sept 2013 EoR build), extracts S_PUB32 records from the symbol stream + named class/struct types from TPI, and writes JSON sidecars to `docs/research/named-retail/`:
|
||
- `symbols.json` — 18,366 named functions (`address` + demangled `name` + raw `mangled`)
|
||
- `types.json` — 5,371 named class/struct records (`name` + `size` + `kind`)
|
||
|
||
Best-effort MSVC C++ demangler handles the common `?Method@Class@@<sig>` patterns + ctors (`??0`) + dtors (`??1`); operator overloads and vtables left mangled. Spot-check verified: `CEnchantmentRegistry::EnchantAttribute` resolves to `0x00594570` exactly as the discovery agent reported. Runtime <1s.
|
||
|
||
Regen workflow: `py tools/pdb-extract/pdb_extract.py refs/acclient.pdb`. The committed JSON outputs are stable + ~3 MB combined; ripgrep/jq on them is faster than re-parsing.
|
||
|
||
---
|
||
|
||
## #5 — [DONE 2026-04-25] VitalsPanel stamina/mana bars always null
|
||
|
||
**Closed:** 2026-04-25
|
||
**Commit:** `feat(player): #5 PlayerDescription parser — Stam/Mana via attribute block`
|
||
**Resolution:** First attempt (commit `d42bf57`) used `AppraiseInfoParser` for `PlayerDescription (0x0013)` — wrong wire format. ACE source confirmed via `GameEventPlayerDescription.WriteEventBody`: PlayerDescription is hand-written (DescriptionPropertyFlag-driven property hashtables, vector flags, attribute block, skills, spells, options/inventory tail) — distinct from `IdentifyObjectResponse (0x00C9)`'s `AppraiseInfo.Write`. Pivoted to a real port: new `PlayerDescriptionParser.cs` that walks property hashtables (Int32/Int64/Bool/Double/String/Did/Iid + Position) gated on the property flags, then reads vector flags + has_health + the attribute block where vitals 7/8/9 carry `ranks/start/xp/current`. Also redesigned `LocalPlayerState` to track per-vital snapshots (replacing the sentinel-API of attempt 1) plus per-attribute snapshots, with `GetMaxApprox` applying the retail formula `vital.(ranks+start) + attribute_contribution` (Endurance/2 for Health, Endurance for Stamina, Self for Mana). Live verified: `+Acdream` shows three bars; ~95% reading on Stam/Mana traced to active buff multipliers (filed as #6). Wire-port also added `PrivateUpdateVital (0x02E7)` + `PrivateUpdateVitalCurrent (0x02E9)` for delta updates per holtburger `UpdateVital`. ~700 LOC C#, 30+ new tests.
|
||
|
||
<!--
|
||
Example:
|
||
|
||
## #0 — [DONE 2026-04-24 · 593b76f] Sky cube edges visible as cross in daytime sky
|
||
|
||
**Closed:** 2026-04-24
|
||
**Commit:** `593b76f sky(phase-8.1): CLAUDE_TO_EDGE on static sky meshes`
|
||
**Resolution:** Switched to `GL_CLAMP_TO_EDGE` wrap mode for static sky
|
||
meshes; scrolling cloud layers kept `GL_REPEAT`. The 5 dome walls were
|
||
sampling opposite-edge pixels via UV wrap + LINEAR filtering, producing
|
||
visible seam lines that formed a cube outline across the view.
|
||
-->
|