Document Phase N.5b shipping (terrain on the modern rendering path via
Path C — `TerrainModernRenderer` mirrors WB's `TerrainRenderManager`
pattern but consumes acdream's `LandblockMesh.Build` so retail's
`FSplitNESW` formula stays in lockstep with physics + visual mesh).
Changes:
- `docs/plans/2026-04-11-roadmap.md` — add N.5b row to the Shipped
table; promote N.5b's "Phases ahead" entry to ✓ SHIPPED with the
Path C resolution + perf reality check; refresh N.6 scope to note
Terrain has joined the modern path (legacy `Texture2D` retirement
scope narrows to Sky + Debug); update top-of-doc Status line.
- `docs/ISSUES.md` — close issue #51 (WB terrain-split formula
divergence). Move from OPEN to "Recently closed" with the Path C
resolution: never adopted WB's formula; modern dispatcher uses
retail's via `LandblockMesh.Build`. References `da56063` (the
black-terrain fix that landed within the N.5b ship chain).
- `CLAUDE.md` — add `TerrainModernRenderer.cs` to the WB integration
cribs list with the GL_INVALID_OPERATION caveat (use uvec2 +
`sampler2DArray(handle)` constructor, NOT direct
`uniform sampler2DArray` + `glProgramUniformHandleARB`). Update
the "Currently in flight" preamble: N.6 builds on N.5 + N.5b;
add an N.5b shipped paragraph linking the perf baseline doc.
- `docs/plans/2026-05-09-phase-n5b-perf-baseline.md` — new doc
capturing the radius=5 Holtburg perf measurement (modern 6.4-7.0
µs median vs legacy 1.5 µs — modern is ~4× SLOWER on CPU at
radius=5). Documents the spec acceptance criterion #5 amendment,
the architectural wins that DO hold (zero glBindTexture/frame,
constant-cost dispatch as A.5 raises radius, per-LB frustum cull),
and the three high-value gotchas surfaced during implementation.
User-memory updates (outside repo, not in this commit):
- `memory/project_phase_n5b_state.md` — full N.5b state file with
the three gotchas captured.
- `memory/MEMORY.md` — index entry pointing at the state file.
Build: dotnet build green. No code changes in this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2057 lines
109 KiB
Markdown
2057 lines
109 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
|
||
|
||
## #50 — Road-edge tree at 0xA9B1 visible in acdream but not retail
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (cosmetic; one spawned tree near the road in Holtburg)
|
||
**Filed:** 2026-05-08
|
||
**Component:** scenery placement / Phase N (WorldBuilder rendering migration)
|
||
|
||
**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 — Scenery (X, Y) placement drifts from retail at some landblocks
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM (visible misplacement; species-specific or per-cell, not a global offset)
|
||
**Filed:** 2026-05-06
|
||
**Component:** scenery placement / `SceneryGenerator`
|
||
|
||
**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
|
||
**Severity:** MEDIUM (visible animation desync; not a correctness/wire bug)
|
||
**Filed:** 2026-05-03
|
||
**Component:** physics / motion / animation
|
||
|
||
**Description:** When observing a remote-driven player character through
|
||
acdream and the actor toggles Shift while keeping a direction key held
|
||
(Run↔Walk demote/promote), the visible leg cycle does NOT update on the
|
||
observer side. Body position eventually corrects via UpdatePosition
|
||
hard-snaps (causing visible position blips), but the animation cycle
|
||
stays at whatever it was last set to (Run sticks; Walk sticks).
|
||
|
||
Observation matrix:
|
||
|
||
| Observer | Actor | Cycle Run↔Walk | Z on slopes |
|
||
|---|---|---|---|
|
||
| Retail | Retail | ✓ | ✓ |
|
||
| Retail | Acdream | ✓ | ✓ |
|
||
| Acdream | Acdream | ✓ | ✗ (only with env-var path) |
|
||
| Acdream | Retail | ✗ | ✗ |
|
||
|
||
**Root cause / status:**
|
||
|
||
ACE only broadcasts a fresh `UpdateMotion` (UM) when the wire's
|
||
`ForwardCommand` byte changes — i.e. on direction-key state changes
|
||
(W press, W release). Toggling Shift while W is held changes
|
||
`ForwardSpeed` and `HoldKey` but NOT `ForwardCommand`, so ACE does
|
||
NOT broadcast a UM for the demote/promote. The speed change DOES
|
||
propagate via `UpdatePosition` (position-delta velocity changes
|
||
between Run-pace and Walk-pace), confirmed via `[VEL_DIAG]`
|
||
serverSpeed varying ~2.5 m/s (walk) ↔ ~9 m/s (run).
|
||
|
||
Retail's inbound code uses UP-derived velocity to refine the visible
|
||
cycle when no UM tells it. Acdream has the equivalent function —
|
||
`ApplyServerControlledVelocityCycle` in `GameWindow.cs:3274` — but
|
||
it's gated `if (IsPlayerGuid(serverGuid)) return;` for player
|
||
remotes, exactly the case where the gap matters.
|
||
|
||
(Earlier hypothesized as H2 in the 2026-05-03 four-agent investigation
|
||
but marked refuted because the [UPCYCLE] diag never fired — that
|
||
was BECAUSE of the gate; un-gating reveals it firing per UP, which
|
||
is the correct behavior.)
|
||
|
||
**Fix sketch (~10 lines):** un-gate `ApplyServerControlledVelocityCycle`
|
||
for player remotes when `currentMotion` is a locomotion cycle
|
||
(Run/Walk/Sidestep/Backward). UMs still drive direction-key changes
|
||
authoritatively; UP-derived velocity refines the speed bucket within
|
||
the same direction. Add a `LastUMUpdateTime` grace window (e.g.
|
||
500ms) so UMs win when fresh.
|
||
|
||
**Files:**
|
||
|
||
- `src/AcDream.App/Rendering/GameWindow.cs:3274` — `ApplyServerControlledVelocityCycle`
|
||
(the gate `if (IsPlayerGuid(serverGuid)) return;` to remove with conditions)
|
||
- `src/AcDream.App/Rendering/GameWindow.cs:3640-3660` — call site (already
|
||
passes through with HasServerVelocity from synthesized UP-deltas)
|
||
- `src/AcDream.Core/Physics/ServerControlledLocomotion.cs:54-76` —
|
||
`PlanFromVelocity` thresholds (may need re-tuning if banding is observed)
|
||
|
||
**Research:**
|
||
|
||
- `docs/research/2026-05-03-remote-anim-cycle/investigation-prompt.md` —
|
||
full background of the four-agent investigation
|
||
- `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)
|
||
|
||
**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 — Humanoid coat doesn't extend up to neck (visible "skin stub" between hair and coat)
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (cosmetic; doesn't affect gameplay)
|
||
**Filed:** 2026-05-01
|
||
**Component:** rendering / clothing / textures
|
||
|
||
**Description:** Every humanoid character (player + NPCs) wearing a coat
|
||
shows a visible skin-colored region at the top of the coat where retail
|
||
shows continuous coat fabric. From the back view: hair → skin stub →
|
||
coat top. In retail: hair → coat collar (no exposed skin). This was
|
||
originally reported as "head/neck protruding forward" — the apparent
|
||
forward shift is an optical illusion caused by the missing coat collar.
|
||
|
||
**Investigation 2026-05-01 (~3 hr session, conclusively ruled out
|
||
many hypotheses):**
|
||
|
||
What we ruled out:
|
||
|
||
- **Animation source.** `ACDREAM_USE_PLACEMENT_BASE=1` (force chars to
|
||
`Setup.PlacementFrames[Resting]` instead of `Animation.PartFrames[0]`)
|
||
→ stub still visible.
|
||
- **Backface culling / mesh winding.** `ACDREAM_NO_CULL=1` (disable
|
||
`glCullFace` entirely) → stub still visible.
|
||
- **Palette overlay (SubPalettes).** `ACDREAM_NO_PALETTE_OVERLAY=1`
|
||
(skip `ComposePalette`) → stub still visible (other colors broke
|
||
as expected — confirms overlay was firing). Bug is NOT a body-skin
|
||
SubPalette being mis-applied to coat fabric.
|
||
- **Bug source = part 16 (head).** `ACDREAM_HIDE_PART=16` → head goes
|
||
away, stub remains UNCHANGED (clean coat top with same shape).
|
||
Stub is NOT from head GfxObj polygons.
|
||
- **Per-part placement frame Origin.** `ACDREAM_NUDGE_Y=-0.1` confirmed
|
||
`+Y = forward` in body-local; head Origin (0, 0.013, 1.587) places
|
||
head correctly relative to spine. Math checks out.
|
||
|
||
What we confirmed (data is correct):
|
||
|
||
- Player Setup `0x02000001` (Aluvian Male), 34 parts.
|
||
- Server (ACE) sends `animParts=34 texChanges=12 subPalettes=10`.
|
||
- Part 9 (upper torso/coat) has gfx `0x0100120D` after AnimPartChange.
|
||
- Part 9 has 2 surfaces, BOTH covered by 2 TextureChanges
|
||
(`oldTex=0x050003D5→0x05001AFE`, `oldTex=0x050003D4→0x05001AFC`).
|
||
- Stub IS from part 9: `ACDREAM_HIDE_PART=9` → entire torso (including
|
||
stub region) disappears.
|
||
- Per-part composition formula (`Scale × Rotation × Translation`)
|
||
matches ACME's `StaticObjectManager.cs:256-258` and retail decomp's
|
||
`Frame::combine` at `0x00518FD0`.
|
||
|
||
**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 — Sky-PES dispatch port (consolidates #2 / #28 / #29 visual gaps)
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM (aesthetic feature-parity, but addresses a cluster of bugs)
|
||
**Filed:** 2026-04-30
|
||
**Component:** sky / weather / particles
|
||
|
||
**Description:** Three open sky bugs (#2 lightning, #28 aurora, #29 cloud
|
||
density) all trace back to the same missing infrastructure: retail's
|
||
sky-PES (Particle Effect Script) dispatch chain. We have it now from a
|
||
2026-04-30 cdb live trace.
|
||
|
||
**What retail does (live trace evidence):**
|
||
|
||
```
|
||
Trace over 24,576 GameSky::Draw frames:
|
||
GameSky::Draw = 24,576 (60 Hz render rate)
|
||
GameSky::UseTime = 12,288 (30 Hz — half rate, MinQuantum)
|
||
GameSky::CreateDeletePhysicsObjects = 12,288 (also 30 Hz)
|
||
CPhysicsObj::CallPES = 372 (~150/min average)
|
||
CallPESHook::Execute = 372 (1:1 with CallPES)
|
||
CreateParticleHook::Execute = 62 (15 at cell load + 47 burst at transition)
|
||
CPhysicsObj::create_particle_emitter = 62 (matches CreateParticleHook)
|
||
```
|
||
|
||
**Three findings:**
|
||
1. Retail has **persistent particle emitters** on celestial / sky objects.
|
||
Created at cell load (15 initial) and dynamically as conditions change
|
||
(the trace caught a +47 burst on a region/weather/time transition).
|
||
2. The PES script-hook system (`CallPESHook::Execute` →
|
||
`CPhysicsObj::CallPES`) drives those emitters periodically, ~150
|
||
times per minute on average.
|
||
3. Earlier research said "GameSky doesn't read pes_id" — correct in
|
||
scope, but missed that the dispatch chain runs through the script-
|
||
hook system, not from inside GameSky directly. Cell/region/weather
|
||
handlers schedule PES script hooks; those hooks call into CallPES.
|
||
|
||
**Decomp anchors:**
|
||
- `CallPESHook::Execute` @ `0x00526e20` — script-hook action that fires CallPES
|
||
- `CreateParticleHook::Execute` @ `0x00526ec0` — particle-creation hook
|
||
- `CPhysicsObj::CallPES` @ `0x00511af0`
|
||
- `CPhysicsObj::create_particle_emitter` @ `0x0050f360`
|
||
- `GameSky::CreateDeletePhysicsObjects` @ `0x005073c0`
|
||
- `LongNIHash<ParticleEmitter>` instance — emitter registry
|
||
- `CelestialPosition.pes_id` @ struct offset +0x004 — populated by
|
||
`SkyDesc::GetSky` but consumed downstream of `GameSky` (via the
|
||
hook system, not GameSky itself)
|
||
|
||
**Implementation outline:**
|
||
1. Decomp dive: read `CallPESHook::Execute`, `CreateParticleHook::Execute`,
|
||
`CPhysicsObj::CallPES`, and `GameSky::CreateDeletePhysicsObjects`
|
||
(and any cell/region weather handlers that spawn the dynamic 47).
|
||
2. Identify what triggers `CreateParticleHook` for sky objects — is it
|
||
inside `CreateDeletePhysicsObjects`, the region/weather change handler,
|
||
or somewhere else?
|
||
3. Port the persistent-emitter creation path: when a cell loads or
|
||
weather/time changes, instantiate the appropriate ParticleEmitters
|
||
on celestial objects.
|
||
4. Port the PES timeline driver — periodic dispatch from a script
|
||
timeline into our equivalent `CallPES`.
|
||
5. Port the actual PES script execution (rate of emission, particle
|
||
parameters, etc.) into our particle system.
|
||
6. Live verify with cdb during specific weather windows: aurora at dusk
|
||
on Rainy DayGroup, lightning during storm.
|
||
|
||
**Files** (likely):
|
||
- `src/AcDream.App/Rendering/Sky/SkyRenderer.cs` — emitter wiring
|
||
- `src/AcDream.Core/World/SkyDescLoader.cs` — already parses pes_id
|
||
- `src/AcDream.Core/Particles/*` — particle system foundation
|
||
- `src/AcDream.App/Rendering/ParticleRenderer.cs` — visual layer
|
||
|
||
**Live-trace verification plan (next cdb session):** Reattach to retail
|
||
during a specific aurora moment, log `this` pointer + `pes_id` arg on
|
||
every `CallPES` invocation, log the GfxObj being attached on every
|
||
`create_particle_emitter`. That tells us EXACTLY which celestial
|
||
objects retail PES-drives and with which IDs.
|
||
|
||
**Acceptance:** During the same in-game time/weather where retail shows
|
||
aurora-style light play (Rainy DayGroup, dusk/dawn windows), acdream
|
||
shows comparable colored sky effects. Cloud sheets look as dense /
|
||
purple as retail. Lightning flashes appear during storm windows.
|
||
|
||
**Closes-when-done:** #28, #29, partially #2 (lightning may need
|
||
additional flash-shader work).
|
||
|
||
---
|
||
|
||
## #33 — Live entity collision shape collapses to one cylinder
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM
|
||
**Filed:** 2026-04-29
|
||
**Component:** physics / entities
|
||
|
||
**Description:** Live world entities do not yet use exact retail
|
||
`CSphere` / `CCylSphere` shape semantics. Several paths collapse the entity to
|
||
a simplified root-centered cylinder or fallback radius, which is not enough for
|
||
retail object and creature collision parity.
|
||
|
||
**Root cause / status:** Tracked under Phase L.2d. Requires auditing object
|
||
shape extraction, `Setup.Radius` fallback, building object identity, and live
|
||
entity broadphase records against named retail.
|
||
|
||
**Files:** `src/AcDream.Core/Physics/CollisionPrimitives.cs`,
|
||
`src/AcDream.Core/Physics/ShadowObjectRegistry.cs`,
|
||
`src/AcDream.Core/Physics/PhysicsDataCache.cs`.
|
||
|
||
**Research:** `docs/plans/2026-04-29-movement-collision-conformance.md`.
|
||
|
||
**Acceptance:** Live object collision uses the appropriate retail sphere or
|
||
cylsphere data where available. Tests prove at least one multi-shape object and
|
||
one live creature case no longer use the single-cylinder fallback.
|
||
|
||
---
|
||
|
||
|
||
## #2 — Lightning visual mismatch (sky PES path disproved)
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM
|
||
**Filed:** 2026-04-25
|
||
**Component:** weather / sky / vfx
|
||
|
||
**Description:** Lightning/storm sky visuals still do not match retail. A 2026-04-28 named-retail recheck disproved the prior assumption that `SkyObject.PesObjectId` drives sky-render flash particles: `SkyDesc::GetSky` copies the field into `CelestialPosition.pes_id`, but `GameSky::CreateDeletePhysicsObjects`, `GameSky::MakeObject`, and `GameSky::UseTime` never read it.
|
||
|
||
**Root cause / status:** Open again. The sky-PES path is non-retail and must stay disabled for normal rendering. The remaining mismatch likely lives in the sky/weather mesh material path, the lightning/fog flash path, or another weather subsystem outside `GameSky`; do not reintroduce per-SkyObject PES playback without new decompile evidence.
|
||
|
||
**Files:**
|
||
- `src/AcDream.App/Rendering/Sky/SkyRenderer.cs` — sky/weather mesh draw, material state, pre/post split
|
||
- `src/AcDream.App/Rendering/Shaders/sky.frag` — flash/fog/lightning coloration path
|
||
- `src/AcDream.Core/World/SkyDescLoader.cs` — keep `PesObjectId` parsed for diagnostics, not render playback
|
||
|
||
**Research:**
|
||
- `docs/research/2026-04-28-pes-pseudocode.md` — C.1 correction: `CelestialPosition.pes_id` copied but ignored by GameSky
|
||
- `docs/research/2026-04-23-sky-pes-wiring.md` — earlier decompile trace reached the same no-sky-PES conclusion
|
||
- `docs/research/2026-04-23-lightning-real.md` (decompile trace + dat discovery)
|
||
- `docs/research/2026-04-23-physicsscript.md` (runtime semantics)
|
||
- `docs/research/2026-04-23-lightning-crossfade.md` (crossfade mechanism)
|
||
|
||
**Acceptance:** During a Rainy DayGroup's storm window, visible flashes appear in the sky at the dat-scripted moments, the fragment-shader flash bump briefly brightens the scene, and (later, once thunder audio is wired) a thunder clap plays with a short propagation delay.
|
||
|
||
**See also #36** (Sky-PES dispatch port) — the lightning visuals likely route through the same PES-hook chain that drives aurora and cloud-density. Most of #2's storm-flash visuals will be unblocked by the #36 port.
|
||
|
||
---
|
||
|
||
## #3 — Client clock drifts from retail after ~10 minutes (periodic TimeSync missing)
|
||
|
||
**Status:** OPEN
|
||
**Severity:** MEDIUM
|
||
**Filed:** 2026-04-25
|
||
**Component:** net / sky
|
||
|
||
**Description:** Our `WorldTimeService.DayFraction` syncs with the server once at login via `ConnectRequest + TimeSync`, then advances from the local wall-clock. Retail receives periodic `TimeSync` refreshes (header flag `0x1000000`) carrying a fresh `PortalYearTicks double` and re-anchors its clock. Without those, acdream's keyframe state drifts from retail's over 10+ minutes — observed during the 2026-04-24 sky-color debug sessions where retail was at DayFraction 0.976 while acdream was at 0.634.
|
||
|
||
**Root cause / status:** Mechanism is well-understood (see research). `WorldTimeService.SyncFromServer(double)` already exists — we just need to detect the periodic flag in the packet header and call it whenever a fresh tick arrives.
|
||
|
||
**Files:**
|
||
- `src/AcDream.Core.Net/WorldSession.cs` — header-flag parsing; currently only the initial sync is consumed
|
||
- `src/AcDream.Core/World/WorldTimeService.cs` — `SyncFromServer(double ticks)` ready; needs caller wiring
|
||
|
||
**Research:** `docs/research/deepdives/r12-weather-daynight.md` §TimeSync (line ~563). References retail packet-header flag `0x1000000` carrying `PortalYearTicks double`.
|
||
|
||
**Acceptance:** Probe retail via `tools/RetailTimeProbe` and acdream's ACDREAM_DUMP_SKY log at the same wall-clock moment after a 20-minute session without re-login; `abs(acdream.DayFraction - retail.DayFraction) < 0.01`.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## #13 — PlayerDescription trailer past enchantments (options / shortcuts / hotbars / desired_comps / spellbook_filters / options2 / gameplay_options / inventory / equipped)
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (no current user-visible bug; future panels will need the data)
|
||
**Filed:** 2026-04-25
|
||
**Component:** net / player-state
|
||
|
||
**Description:** `PlayerDescriptionParser` walks through enchantments (Phase H, 2026-04-25). The trailer beyond that — Options1 / Shortcuts / HotbarSpells (8 lists) / DesiredComps / SpellbookFilters / Options2 / GameplayOptions blob / Inventory / Equipped — is not yet parsed. Required for future Spellbook UI panel, hotbar UI, inventory UI, character options panel.
|
||
|
||
**Root cause / status:** Holtburger `events.rs:462-625` has the full layout. The trickiest piece is `gameplay_options` — a variable-length opaque blob; holtburger uses a heuristic forward search (`find_inventory_start_after_gameplay_options`) for plausibly-aligned inventory-count + GUID pairs to find the inventory start. Other sections are well-formed.
|
||
|
||
**Files:**
|
||
- `src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs` — extend `Parsed` record + walker.
|
||
- `tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs` — add fixtures per section.
|
||
- `src/AcDream.Core.Net/GameEventWiring.cs` — route `parsed.Inventory` + `Equipped` to ItemRepository.
|
||
|
||
**Research:** holtburger `events.rs:462-625`; `references/actestclient/TestClient/messages.xml`.
|
||
|
||
**Acceptance:** All sections of a real-world PlayerDescription parse to completion (no truncation). New tests cover synthetic fixtures per section. `ItemRepository.Count` after login > 0.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## #4 — Sky horizon-glow disabled (fog-mix skipped on sky meshes)
|
||
|
||
**Status:** OPEN
|
||
**Severity:** LOW (aesthetic feature-parity, not regression from pre-session state)
|
||
**Filed:** 2026-04-25
|
||
**Component:** sky
|
||
|
||
**Description:** Phase 8.1 (commit `593b76f`) disabled the fog-mix on sky meshes to fix the "entire dome swallowed by fog color" regression. Dereth's keyframe `FogEnd` values (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.
|
||
|
||
**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)
|
||
|
||
**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
|
||
|
||
## #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.
|
||
-->
|