User report: 'You should be face to face with the NPC before sending
use. So first is rotation, when you are facing, then using.' and
'it does not face it completely.'
Two changes:
1. Split alignment thresholds in ApplyAutoWalkOverlay:
walkAligned (30°) — gate for synthesised Forward+Run motion
during far-range approach; body walks
while finishing residual turn within 30°.
aligned (5°) — gate for arrival-fire. Final facing
before the auto-walk ends and the action
re-sends. Matches retail's tight pre-Use
rotation tolerance.
Within-arrival check still requires alignment; without alignment
the body holds in turn-only mode regardless of distance.
2. Defer wire Use/PickUp packet for CLOSE-range targets. SendUse
and SendPickUp now check IsCloseRangeTarget(guid): if the player
is already within the target's use-radius, we install the
speculative overlay, set _pendingPostArrivalAction, and RETURN
without sending the wire packet. AutoWalkArrived fires after the
local rotation completes (alignment within 5°); the existing
re-send handler then fires SendUse with isRetryAfterArrival=true,
sending the wire packet at that moment. Effect: rotate first,
THEN Use — the NPC/door/item only sees the action after the
character has turned to face it.
Far-range path unchanged: send immediately, ACE auto-walks,
arrival re-sends.
Filed #69: turn animation (leg/arm cycle while pivoting). The body
now rotates but doesn't play the TurnLeft/TurnRight cycle the user
wants to see. Separate scope — needs motion-interpreter integration.
2748 lines
153 KiB
Markdown
2748 lines
153 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.
|
||
```
|
||
|
||
---
|
||
|
||
# Active issues
|
||
|
||
## #69 — Local player rotation isn't animated (no leg/arm cycle while pivoting)
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (visual polish — rotation works, just looks stiff)
|
||
**Filed:** 2026-05-15 (B.6 close-range turn-to-face)
|
||
**Component:** motion / animation cycle
|
||
|
||
**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 — Server-initiated auto-walk (MoveToObject) not honored
|
||
|
||
**Status:** OPEN
|
||
**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
|
||
|
||
**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 — AnimationSequencer link→cycle boundary flash on one-shot motion (door swing)
|
||
|
||
**Status:** OPEN
|
||
**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.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
# Recently closed
|
||
|
||
## #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.
|
||
-->
|