acdream/docs/ISSUES.md
Erik b37b7137f6 docs(motion): #42 root cause confirmed — ResolveWithTransition airborne drift
A/B-tested 2026-05-05 with user observing retail-controlled remote:

  - With CellId fix removed: jumps render with geometrically-correct
    XY (no drift) but body falls through the floor.
  - With CellId fix applied: jumps land cleanly but arc shows ~1 m
    horizontal offset; snaps back on next UM.

Confirms the drift originates inside ResolveWithTransition, not from
wire data, local Euler error, or stale velocity. CellId fix kept in
place because falling through the floor is more disruptive than
~1 m visual jitter that resolves on next input.

#42 updated with the verified diagnosis, three ranked-by-probability
hypotheses for the in-sweep mechanism (initial-overlap depenetration
along non-+Z terrain normal is the leading candidate), three matching
fix paths, and a deterministic repro recipe for the next session.

The right next step is investigating PhysicsEngine.ResolveWithTransition
and comparing against retail's CTransition::find_valid_position
(docs/research/named-retail/) — out of scope for the L.3 motion port,
files as a follow-up PhysicsEngine bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:47:40 +02:00

1434 lines
74 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.
```
---
# Active issues
## #39 — Run↔Walk cycle transition not visible on observed player remotes (acdream-as-observer)
**Status:** OPEN
**Severity:** MEDIUM (visible animation desync; not a correctness/wire bug)
**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
- 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).
## #42 — Airborne XY drift on observed player remote jumps (~1 m horizontal offset over arc)
**Status:** OPEN
**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)
**Component:** physics (`PhysicsEngine.ResolveWithTransition` airborne behavior)
**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 ~510 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)
**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 L614679 (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` § 57
- `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 — Chase camera + player feel "30 fps" since L.5 physics-tick gate
**Status:** OPEN
**Severity:** MEDIUM (gameplay-feel regression; not a correctness bug)
**Filed:** 2026-05-01
**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)
- 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 — Humanoid coat doesn't extend up to neck (visible "skin stub" between hair and coat)
**Status:** OPEN
**Severity:** LOW (cosmetic; doesn't affect gameplay)
**Filed:** 2026-05-01
**Component:** rendering / clothing / textures
**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`.
**Remaining hypothesis space (untested):**
1. **Texture decode produces skin pixels** for `0x05001AFE/0x05001AFC`
where ACME / retail produces coat pixels. Compare our SurfaceDecoder
against ACME's `TextureHelpers.cs` for INDEX16 / palette-indexed
chains.
2. **Polygon-to-surface mapping off-by-one.** Specific polygons of
part 9 reference an unintended surface. Add a dump: for each polygon
in gfx 0x0100120D, print `PosSurface` index + the resolved Surface id.
3. **Multi-layer texture composition retail does and we skip.** AC's
"ApplyCloth" or similar layered texture step. Grep
`acclient_2013_pseudo_c.txt` for `BlendBaseLayer`, `LayerSurfaces`,
any composition method that combines multiple textures into one.
4. **UV mapping bug.** Part 9's polygon UVs map to a skin region of
the coat texture. Dump per-vertex UV vs vertex Z; if a high-Z vertex
has UV.v near a skin region, that's the source.
**Files (diagnostic env vars committed for next-session reuse):**
- `src/AcDream.App/Rendering/InstancedMeshRenderer.cs:210-275`
`ACDREAM_NO_CULL` env var
- `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 — Sky-PES dispatch port (consolidates #2 / #28 / #29 visual gaps)
**Status:** OPEN
**Severity:** MEDIUM (aesthetic feature-parity, but addresses a cluster of bugs)
**Filed:** 2026-04-30
**Component:** sky / weather / particles
**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
**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`.
---
---
## #13 — PlayerDescription trailer past enchantments (options / shortcuts / hotbars / desired_comps / spellbook_filters / options2 / gameplay_options / inventory / equipped)
**Status:** OPEN
**Severity:** LOW (no current user-visible bug; future panels will need the data)
**Filed:** 2026-04-25
**Component:** net / player-state
**Description:** `PlayerDescriptionParser` walks through enchantments (Phase H, 2026-04-25). The trailer beyond that — Options1 / Shortcuts / HotbarSpells (8 lists) / DesiredComps / SpellbookFilters / Options2 / GameplayOptions blob / Inventory / Equipped — is not yet parsed. Required for future Spellbook UI panel, hotbar UI, inventory UI, character options panel.
**Root cause / status:** Holtburger `events.rs:462-625` has the full layout. The trickiest piece is `gameplay_options` — a variable-length opaque blob; holtburger uses a heuristic forward search (`find_inventory_start_after_gameplay_options`) for plausibly-aligned inventory-count + GUID pairs to find the inventory start. Other sections are well-formed.
**Files:**
- `src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs` — extend `Parsed` record + walker.
- `tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs` — add fixtures per section.
- `src/AcDream.Core.Net/GameEventWiring.cs` — route `parsed.Inventory` + `Equipped` to ItemRepository.
**Research:** holtburger `events.rs:462-625`; `references/actestclient/TestClient/messages.xml`.
**Acceptance:** All sections of a real-world PlayerDescription parse to completion (no truncation). New tests cover synthetic fixtures per section. `ItemRepository.Count` after login > 0.
---
---
## #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 (02400 m) are calibrated for terrain; sky meshes are authored at radii 105014271 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.
**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.030.19 | early morning |
| 17 | 0x02000589 | **0x3300042C** | **0.270.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.
---
---
# Recently closed
## #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.
-->