Phase A.5 — Two-tier Streaming + Horizon LOD shipped. Headline: 2.3 km terrain horizon (radius=4 near + 12 far) with off-thread mesh build, fog blend at N₁, mipmaps + 16x AF, MSAA 4x + A2C foliage, depth-write audit, BUDGET_OVER diag, Quality Preset system (Low/Medium/ High/Ultra) with env-var overrides + F11 mid-session re-apply. ~999 tests pass, 8 pre-existing physics/input failures unchanged. Two structural-to-A.5 bug fixes shipped post-T26: - Bug A (9217fd9): far-tier worker strips entities (T13/T16 had only wired the controller side; far-tier was loading full entity layers, ~71K entities instead of ~10K, 5x perf regression). - Bug B (0ad8c99): WalkEntities scratch list reused across frames (was 480 KB / frame allocation). Tier 1 entity-classification cache attempted as polish (3639a6f), reverted (9b49009) — broke animation by caching mutable per-frame state. Retry deferred to post-A.5 polish phase (ISSUE #53). Deferred to post-A.5 polish: - Tier 1 retry with animation-mutation audit (ISSUE #53) - Lifestone missing visual (ISSUE #52) - JobKind plumbing through BuildLandblockForStreaming (ISSUE #54) - Tier 2 (static/dynamic split) + Tier 3 (GPU compute cull) — separate multi-week phases. Roadmap at docs/plans/2026-05-10-perf-tiers-2-3-roadmap.md. SHIP commit:9245db5.
115 KiB
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.
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/*.mdnote.
Conventions
- Sequential integer IDs (
#1,#2, …). Commits that close an issue reference the ID in the message (e.g.fix #3: periodic TimeSync parsing). StatusisOPEN,IN-PROGRESS, orDONE. 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
#54 — A.5/jobkind-plumbing: far-tier worker loads full entity layer then strips
Status: OPEN Severity: LOW (correctness/perf; worker wastes CPU on far-tier LandBlockInfo + scenery generation that is immediately discarded) Filed: 2026-05-10 Component: streaming / LandblockStreamer
Description: Bug A's fix (commit 9217fd9) patches at the worker output — after a far-tier job completes the full LoadNear path, the result's entity list is stripped before posting to the completion queue. This means far-tier LBs still load LandBlockInfo + run SceneryGenerator + call LandblockLoader.BuildEntitiesFromInfo even though those results are thrown away. At N₂=12, that is ~544 far-tier LBs × unnecessary dat reads + scenery math on promotion sequences.
Proper fix: plumb LandblockStreamJobKind through BuildLandblockForStreaming so far-tier jobs call only LandBlock heightmap read + LandblockMesh.Build, skipping LandBlockInfo + SceneryGenerator entirely. The function signature change is ~5 lines; wiring is ~10 lines. Estimated 30 min–1 hour total.
Files:
src/AcDream.App/Streaming/LandblockStreamer.cs—HandleJob+BuildLandblockForStreaming
Acceptance: Far-tier LB worker path reads only the LandBlock dat file (no LandBlockInfo, no SceneryGenerator call). Verified by adding a counter diagnostic or via dotnet-trace showing the dat-read call count per job kind.
#53 — A.5/tier1-redo: entity-classification cache broke animation (reverted)
Status: OPEN Severity: MEDIUM (perf gap; the classification cache would save ~1-2ms/frame but cannot land until animation-mutation audit is done) Filed: 2026-05-10 Component: rendering / WbDrawDispatcher / AnimationSequencer
Description: Tier 1 entity-classification cache (commit 3639a6f) was reverted at 9b49009 due to an animation regression. The cache stored meshRef.PartTransform at first-classify time. For static entities this is stable. For animated entities, AnimationSequencer mutates meshRef.PartTransform every frame to apply the current skeletal pose. The cache froze the pose, causing NPCs and some animated entities to stop animating (some buildings also showed at wrong positions, likely entities incorrectly flagged as animated).
Root cause: the "trust MeshRefs as the source of truth" comment in the dispatcher gave false confidence — MeshRefs IS the source of truth, but it is mutated EVERY frame for animated entities.
Next attempt needs:
- Audit
AnimationSequencer+AnimationHookRouterto identify ALL per-frame mutations ofMeshRefstate (not justPartTransform— are any other fields mutated?). - Redesign cache to: (a) bypass animated entities entirely (classify them each frame, cache only static entities), OR (b) cache only the animation-invariant subset of the classification key (group key, texture handle, blend mode) while reading the per-frame pose from the live
MeshRef. - Test specifically with a moving animated NPC visible on screen before shipping.
Estimated: 1 week including audit + redesign + retest.
Files:
src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs— dispatcher classification logicsrc/AcDream.Core/Animation/AnimationSequencer.cs— mutation sourcesrc/AcDream.Core/Animation/AnimationHookRouter.cs— secondary mutation source
#52 — A.5/lifestone-missing: Holtburg lifestone not rendering
Status: OPEN Severity: MEDIUM (visible missing landmark; lifestone is the player's respawn anchor and should always be visible) Filed: 2026-05-10 Component: streaming / rendering
Description: The Holtburg lifestone (spinning blue crystal) has not rendered since earlier in A.5 development. Reproduce: launch live client, walk to Holtburg town center, look toward the lifestone position. Should see the spinning blue crystal; instead see nothing.
Root cause (suspected, two candidates):
- Bug A's far-tier strip (commit
9217fd9) may be incorrectly stripping a near-tier entity. The lifestone's server GUID is0x5000000A; its dat object may be registering via theLandBlockInfopath but getting stripped as if it were a far-tier entity due to a tier-classification race or incorrect LB-tier tracking. - Separate regression from earlier in the A.5 development chain — possibly introduced when entity registration was restructured during T13/T16 streaming controller wiring.
Investigation approach:
- Add a
[STREAMING-DIAG]log line when far-tier stripping drops an entity — log the entity's GfxObj ID and LB address so the lifestone's GfxObj ID appears in the log if it is being stripped. - If not in the strip log, check whether the lifestone's LB is registering as near-tier at all during first-tick bootstrap.
- Bisect to find the commit that broke it if the above two checks don't isolate the cause.
Acceptance: Launch live, walk to Holtburg center, spinning blue crystal visible at the lifestone position. No regression on other static entities in the area.
#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:
- Investigate ACME's full per-vertex filter set (road + building + anything else) and port them as a coherent unit, not piecemeal.
- 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.
- 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—GenerateInternalis the active pathsrc/AcDream.Core/World/WbSceneryAdapter.cs— adapter used byGenerateInternalreferences/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):
- The displacement-noise math in
SceneryGeneratordiffers from retail'schunk_005A0000LCG by a constant or a sign flip. Auditeeee4c5claimed "all MATCH" against the decomp, but a runtime trace would prove or disprove. - 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. - The
obj.Align != 0path 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. - Slope filter could reject a cell retail accepts (or vice versa), pushing trees into adjacent cells.
- Region-table /
SceneInfolookup might select a different scenery list for the cell type.
Investigation plan (gold-standard, per project_retail_debugger.md):
- Run the existing
ACDREAM_DUMP_SCENERY_Z=1diagnostic to capture acdream's full per-spawn (gfx, world XY, scale, partT) for landblock0xA9B3FFFF. - Attach cdb to a live retail client at the same Holtburg spot
(
tools/pdb-extract/check_exe_pdb.pyconfirms PDB pairs with v11.4186). Set a breakpoint onCLandBlock::get_land_scenes(or the innerchunk_005A0000placement function); capture every(gfxObjId, worldX, worldY, scale, heading)retail emits for the same landblock. - 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— 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 pointchunk_005A0000.c— referenced retail source perSceneryGenerator.cscommentsdocs/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.
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
returns LocalPosition.Z = obj.BaseLoc.Origin.Z (just the
ObjectDesc's BaseLoc Z offset, no terrain). GameWindow.cs:4642
adds the terrain ground Z:
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):
-
Per-GfxObj origin convention. Most AC tree GfxObjs are authored with local origin at the trunk base (mesh vertices have
Z >= 0measured up from the origin). A few species may be authored with origin at bbox-center or visual top — for those,finalZ = groundZ + BaseLoc.Zplants 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. -
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:4643uses 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. -
Same close-degrade story as #47, applied to scenery. Some tree GfxObjs have
DIDDegradetables; 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— BaseLoc.Z passthroughsrc/AcDream.App/Rendering/GameWindow.cs:4632-4655— groundZ resolution + finalZ assemblysrc/AcDream.Core/Physics/TerrainSurface.cs— physics sampler (AC2D split-direction formula)SampleTerrainZ(private, in GameWindow.cs) — bilinear fallbacksrc/AcDream.Core/Meshing/GfxObjDegradeResolver.cs— close-degrade resolver if hypothesis 3 confirmed; would need scenery-scope expansion (drop theIsIssue47HumanoidSetupgate 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
#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 gateif (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—PlanFromVelocitythresholds (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 investigationdocs/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 workflowdocs/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
UmGraceSecondswindow 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:
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:
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 toreturn 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):
-
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).
-
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 passisOnGround: !rm.Airborne(= false for airborne). Check whether thestepUpHeight/stepDownHeightparameters are unconditionally used insideResolveWithTransitionregardless of theisOnGroundflag. -
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—ResolveWithTransitionand any internalCTransition/find_valid_positionhelpers. 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 fromtransition())SpherePathinitialization- The verbatim retail depenetration logic for airborne bodies
If our port differs from retail in this region, that diff is likely the bug.
Repro:
- Launch acdream + retail client side-by-side connected to local ACE.
- Have retail char stand still on outdoor terrain at any position X.
- Jump in place.
- 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 mMinDistanceToReachPosition(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
CurrentVelocitycarries only the steady-state component of the cycle's intent; the per-frame stride wobble is gone… For Humanoid the dat shipsMotionData.Velocity = 0so the multiply is a no-op anyway — but the synth usesRunAnimSpeed × adjustedSpeeddirectly.
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—CurrentVelocitysynthesis at L614–679 (RunAnimSpeed=4.0, WalkAnimSpeed=3.12, SidestepAnimSpeed=1.25 × adjustedSpeed)src/AcDream.Core/Physics/PositionManager.cs—ComputeOffsetappliesseqVel × dt × orientationas fallback when queue is idle
Research:
docs/research/2026-05-04-l3-port/05-position-manager-and-partarray.md§ 5–7docs/research/2026-05-04-l3-port/06-acdream-audit.md§ 9 (AnimationSequencer)docs/research/named-retail/acclient_2013_pseudo_c.txtline 298437 (add_motion @ 0x005224b0) —CSequence::velocity = style_speed × MotionData.velocity
Fix path (research first, then port):
- cdb-trace retail to capture
CSequence::velocityandMotionData::velocityfor a Humanoid running cycle. Compare against our synth (4.0 × 2.94 = 11.76 m/s) to determine the actual retail magnitude. - Port
add_motion'sstyle_speed × MotionData.velocitychain verbatim. For Humanoid whereMotionData.Velocity = 0, port the fallback retail uses (likely a separate code path throughapply_run_to_commandthat derives velocity from the cycle's framerate, not a constant). - Remove the
RunAnimSpeed × adjustedSpeedsynth inAnimationSequencer.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):
PositionManager.ComputeOffsetis 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 retailUpdatePositionInternal @ 0x00512c30.ResolveWithTransitionis 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:
-
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
UpdatePositionarrival. Body never follows the terrain mesh between UPs. -
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 branchsrc/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-03chronological commit log + visual verification docs/research/2026-05-03-remote-anim-cycle/investigation-prompt.mdfor 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
UpdatePositionqueue 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—PhysicsTickconstantsrc/AcDream.App/Input/PlayerMovementController.cs:448-456—_physicsAccumgatesrc/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 toSetup.PlacementFrames[Resting]instead ofAnimation.PartFrames[0]) → stub still visible. - Backface culling / mesh winding.
ACDREAM_NO_CULL=1(disableglCullFaceentirely) → stub still visible. - Palette overlay (SubPalettes).
ACDREAM_NO_PALETTE_OVERLAY=1(skipComposePalette) → 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.1confirmed+Y = forwardin 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
0x0100120Dafter 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'sStaticObjectManager.cs:256-258and retail decomp'sFrame::combineat0x00518FD0.
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
0x0100120DareSidesType=0(ST_SINGLE), all surfaces areBase1Image— NO ST_DOUBLE polygons we'd be missing, NO surfaces lacking theType & 6bits that retail'sDrawPolyInternalskips. - 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'snewPalette.Colors[j + offset]. - The
*8wire 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
- ACE sends incomplete SubPalette ranges (retail-original would cover the full palette)
- Retail does additional client-side compute that ACE pre-resolves wrongly
- The base palette
0x0400007Eitself is supposed to have coat colors at those indices in retail's interpretation (different palette decode)
Next investigation (deferred):
- Diff ACE's
WorldObject_Networking.csCharGen ObjDesc construction against retail'sClothingTable::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.csis the diagnostic harness — extend it.
Files (diagnostic env vars committed for next-session reuse):
(file deleted in N.5 ship amendment)src/AcDream.App/Rendering/InstancedMeshRenderer.cs:210-275—ACDREAM_NO_CULLenv varsrc/AcDream.App/Rendering/GameWindow.cs—ACDREAM_HIDE_PART=Nhides specific humanoid part;ACDREAM_DUMP_CLOTHING=1dumps AnimPartChanges + TextureChanges + per-part Surface chain coverage.src/AcDream.App/Rendering/TextureCache.cs:159-204—DecodeFromDatsis the texture decode entry. Compare againstreferences/WorldBuilder-ACME-Edition/.../TextureHelpers.cs.
Reproduction:
$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:
- Retail's
OBJECTINFO::kill_velocityrarely fires in normal play — gated onlast_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). - 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 inPlayerMovementControllervia_physicsAccum. Render still runs at 60+ Hz; only the physics integration step is 30Hz. - 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 reportsMATCH/MISMATCH (expected GUID = …)against ourrefs/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
.cdbscript, PowerShell wrapper pattern, watchouts (PDB name conventions,;parsing, kill-target-on-detach behavior, high-hit-rate lag). - Step
-1added 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_objectratio = 0.61). Drove the L.5 fix in PlayerMovementController. OBJECTINFO::kill_velocityrarely 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:
- 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).
- The PES script-hook system (
CallPESHook::Execute→CPhysicsObj::CallPES) drives those emitters periodically, ~150 times per minute on average. - 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 CallPESCreateParticleHook::Execute@0x00526ec0— particle-creation hookCPhysicsObj::CallPES@0x00511af0CPhysicsObj::create_particle_emitter@0x0050f360GameSky::CreateDeletePhysicsObjects@0x005073c0LongNIHash<ParticleEmitter>instance — emitter registryCelestialPosition.pes_id@ struct offset +0x004 — populated bySkyDesc::GetSkybut consumed downstream ofGameSky(via the hook system, not GameSky itself)
Implementation outline:
- Decomp dive: read
CallPESHook::Execute,CreateParticleHook::Execute,CPhysicsObj::CallPES, andGameSky::CreateDeletePhysicsObjects(and any cell/region weather handlers that spawn the dynamic 47). - Identify what triggers
CreateParticleHookfor sky objects — is it insideCreateDeletePhysicsObjects, the region/weather change handler, or somewhere else? - Port the persistent-emitter creation path: when a cell loads or weather/time changes, instantiate the appropriate ParticleEmitters on celestial objects.
- Port the PES timeline driver — periodic dispatch from a script
timeline into our equivalent
CallPES. - Port the actual PES script execution (rate of emission, particle parameters, etc.) into our particle system.
- 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 wiringsrc/AcDream.Core/World/SkyDescLoader.cs— already parses pes_idsrc/AcDream.Core/Particles/*— particle system foundationsrc/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 splitsrc/AcDream.App/Rendering/Shaders/sky.frag— flash/fog/lightning coloration pathsrc/AcDream.Core/World/SkyDescLoader.cs— keepPesObjectIdparsed for diagnostics, not render playback
Research:
docs/research/2026-04-28-pes-pseudocode.md— C.1 correction:CelestialPosition.pes_idcopied but ignored by GameSkydocs/research/2026-04-23-sky-pes-wiring.md— earlier decompile trace reached the same no-sky-PES conclusiondocs/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 consumedsrc/AcDream.Core/World/WorldTimeService.cs—SyncFromServer(double ticks)ready; needs caller wiring
Research: docs/research/deepdives/r12-weather-daynight.md §TimeSync (line ~563). References retail packet-header flag 0x1000000 carrying PortalYearTicks double.
Acceptance: Probe retail via tools/RetailTimeProbe and acdream's ACDREAM_DUMP_SKY log at the same wall-clock moment after a 20-minute session without re-login; abs(acdream.DayFraction - retail.DayFraction) < 0.01.
#4 — Sky horizon-glow disabled (fog-mix skipped on sky meshes)
Status: OPEN Severity: LOW (aesthetic feature-parity, not regression from pre-session state) Filed: 2026-04-25 Component: sky
Description: Phase 8.1 (commit 593b76f) disabled the fog-mix on sky meshes to fix the "entire dome swallowed by fog color" regression. Dereth's keyframe FogEnd values (0–2400 m) are calibrated for terrain; sky meshes are authored at radii 1050–14271 m so every sky pixel was past FogEnd, saturated to uFogColor, destroying stars / moon / dome texture. Disabling the mix restored visibility but we lost retail's horizon-glow effect (gradient from clear zenith to fog-tinted horizon band at dusk/dawn).
Root cause / status: Three competing hypotheses, none pinned down: (a) retail uses a different fog range for sky than terrain; (b) retail applies fog with an elevation-angle weighting rather than linear distance; (c) retail's sky meshes don't participate in the global fog and the "horizon glow" comes from a different atmospheric-scatter path. Need to identify retail's actual sky-fog behaviour before re-enabling with correct parameters.
Files:
src/AcDream.App/Rendering/Shaders/sky.frag— line ~55,rgb = mix(uFogColor.rgb, rgb, vFogFactor)currently commented outsrc/AcDream.App/Rendering/Shaders/sky.vert— lines 109-114,vFogFactorcomputation
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:
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:
- Keep
SkyObject.PesObjectIdparsed for diagnostics only. - Compare retail/acdream material state for the active sky/weather GfxObj/Setup ids (
0x02000588,0x02000589,0x02000714,0x02000BA6). - Trace the named retail sky/weather draw path for texture transforms, translucency, diffusion, luminosity, and any non-GameSky weather effect dispatch.
- Only add a new runtime visual path once the decompile has an actual caller.
Decomp pointers:
SkyDesc::GetSkynamed retail0x00501ec0— copiesSkyObject.default_pes_objectintoCelestialPosition.pes_id.GameSky::CreateDeletePhysicsObjectsnamed retail0x005073c0— creates/updates sky objects fromgfx_id, does not readpes_id.GameSky::MakeObjectnamed retail0x00506ee0— callsCPhysicsObj::makeObject(gfx_id, 0, 0), no PES.GameSky::UseTimenamed retail0x005075b0— updates frame/luminosity/diffusion/translucency, no PES.
Files:
src/AcDream.Core/World/SkyDescLoader.cs— carriesPesObjectIdfor 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:
TranslucencyKindExtensions.FromSurfaceTypenow applies retail's Translucent-override atD3DPolyRender::SetSurface(decomp 425246-425260) — surface0x08000023(Type=0x10114=B1ClipMap | Translucent | Alpha | Additive) is now correctly classified asAlphaBlendinstead ofAdditive.SkyRenderer.EnsureSetupUploadednow loads0x020xxxxxSetup IDs (e.g.0x02000588,0x02000589,0x02000714,0x02000BA6) which were silently dropped. Setup parts are flattened viaSetupMesh.Flattenand 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
0x010001ECpart 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 overridesrc/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. Setup0x02000001has 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
+Jeand 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..38depending 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 = 0for both humans and drudges → all parts use Vector3.One scale.
Working hypotheses (next session):
- 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.
- Lighting setup. Cell ambient may be too low, leaving back-
facing surfaces in flat shadow. Compare
uCellAmbientvalue against retail's behaviour at the same time-of-day. - Anti-aliasing. Retail may use MSAA; acdream window may not. Polygon edges in acdream would be visibly stair-stepped, reading as "more faceted" / blockier.
- Surface flags interpretation. Specific Surface.Type bits for
character textures (skin, fabric) may need handling acdream
doesn't yet do (e.g.
SmoothShadeflag, or a mip bias).
Diagnostic infrastructure landed this session (env-var-gated, no runtime cost when off):
ACDREAM_DUMP_CLOTHING=1extended:setup.Parts.Count,flatten.Count,APCcount on header lineParentIndex[]array dumpDefaultScale[]array dumpIdleFrame.Frames[]per-part Origin + Orientation (first 17 parts)EMIT part=NN gfx=0xXX subMeshes=N tris=Nper partTOTAL tris=N meshRefs=Nper entity
Files (suspect surface area for next investigation):
src/AcDream.Core/Meshing/SetupMesh.cs— Flatten compositionsrc/AcDream.Core/Meshing/GfxObjMesh.cs— polygon emission + vertex normal handling (line 142)src/AcDream.App/Rendering/Shaders/mesh.frag— lighting eqsrc/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:
- AutonomousPosition heartbeat cadence.
memory/project_retail_motion_outbound.mdnotes acdream's fixed 200 ms heartbeat is a probable retail mismatch. Retail'sCommandInterpreter::SendPositionEventgates 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. - MoveToState send conditions.
PlayerMovementController.cs:813-840decides 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. - 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.
- Velocity field absent on AutoPos. ACE relays UPs without HasVelocity for player characters (per
OnLivePositionUpdatedcomment). 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_movementto 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.rsheartbeat 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 + heartbeatsrc/AcDream.Core.Net/WorldSession.cs— sequence counters + send pathsrc/AcDream.Core.Net/Net/Outbound/...MoveToState.cs/AutonomousPosition.cs— wire buildersreferences/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::SendMovementEventfor WASD,Event_Jumpper-frame while charging).- CLAUDE.md "Outbound motion wire format" — the
WalkForward + HoldKey.Run↔RunForwardauto-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
#13 — [DONE 2026-05-10 · d3b58c9..078919c] PlayerDescription trailer past enchantments
Closed: 2026-05-10
Commits: d3b58c9 (scaffold) → 6587034 (rename nit) → becbde6 (OptionFlags+Options1) → 9a0dfe0 (TrailerTruncated + diag) → f7a5eea (Shortcuts) → 8cbb991 (HotbarSpells) → 75e8e26 (DesiredComps) → b17dc3b (SpellbookFilters) → 98eebef (Options2) → d9a5e40 (strict Inventory+Equipped) → 91693ea (heuristic GAMEPLAY_OPTIONS walker) → 58095d8 (combined fixture test) → 078919c (ItemRepository wiring)
Component: net / player-state
Plan: docs/superpowers/plans/2026-05-10-issue-13-pd-trailer.md
Resolution. PlayerDescriptionParser now walks every trailer
section through Inventory + Equipped, ported faithfully from holtburger
events.rs:503-625 + shortcuts.rs:13-34. The trickiest piece —
gameplay_options — uses a 4-byte-aligned forward heuristic
(TryHeuristicInventoryStart) that probes candidate offsets with a
strict (inventory + equipped consume to EOF) test, mirroring
holtburger's find_inventory_start_after_gameplay_options.
The trailer walk is wrapped in its own inner try/catch (separate from
the outer parse-wide catch) so a malformed trailer cannot destroy the
already-extracted attribute / skill / spell / enchantment data. A new
Parsed.TrailerTruncated flag lets callers distinguish a clean parse
from a graceful-degradation parse (set true if the inner catch fires;
log under ACDREAM_DUMP_VITALS=1).
GameEventWiring's PlayerDescription handler now registers each
inventory entry with ItemRepository.AddOrUpdate(...) and applies
MoveItem(...) for equipped entries so paperdoll picks up
CurrentlyEquippedLocation at login. The acceptance criterion
"ItemRepository.Count after login > 0" is now exercised by
PlayerDescription_RegistersInventoryEntries_InItemRepository in
GameEventWiringTests.
12 tasks, 13 commits, +9 PD parser tests + 1 wiring test (20 PD tests
total, 282 Net.Tests pass). Code-review nits during the run produced
two refactor commits: Shortcut → ShortcutEntry rename to avoid a
homograph with the CharacterOptionDataFlag.Shortcut flag bit
(6587034); TrailerTruncated flag + diagnostic logging
(9a0dfe0).
Forward-looking notes (low priority, no follow-up issues filed):
WeenieClassId = inv.ContainerTypefor inventory entries is a placeholder;CreateObjectoverwrites it with the real weenie class later in the login sequence.- The 10,000 count cap throws
FormatExceptionon validation failure, which the inner catch treats the same as truncation. If a future diagnostic UI needs to distinguish "EOF mid-section" from "garbage count rejected", splitTrailerTruncatedinto two flags. For now theACDREAM_DUMP_VITALS=1log message gives the developer enough signal.
Files: src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs,
src/AcDream.Core.Net/GameEventWiring.cs,
tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs,
tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs.
#51 — [DONE 2026-05-09 · da56063 + N.5b SHIP] WB's terrain-split formula diverges from retail's FSplitNESW
Closed: 2026-05-09
Commit: da56063 (black-terrain fix; landed within Phase N.5b — see
docs/superpowers/plans/2026-05-09-phase-n5b-terrain-modern.md for the
ship commit chain)
Component: terrain math / Phase N.5b
Resolution: Path C. Phase N.5b lifted terrain rendering onto the
modern path (bindless atlas + glMultiDrawElementsIndirect) WITHOUT
adopting WB's TerrainUtils.CalculateSplitDirection. The pre-implementation
divergence test (tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs)
confirmed the two formulas disagree on 49.98% of sweep cells —
fundamentally incompatible with our shared physics + visual mesh, which
both rely on retail's FSplitNESW (constants 0x0CCAC033 / 0x421BE3BD /
0x6C1AC587 / 0x519B8F25).
Path C: keep retail's FSplitNESW formula via LandblockMesh.Build →
TerrainBlending.CalculateSplitDirection; mirror WB's TerrainRenderManager
architectural pattern (single global VBO/EBO + slot allocator + bindless
atlas + multi-draw indirect) but feed it acdream's mesh. Modern dispatcher
(TerrainModernRenderer) replaces TerrainChunkRenderer (deleted in T9
along with TerrainRenderer + terrain.vert/.frag).
Path A (substitute WB's formula) was killed by the divergence test. Path B (fork-patch WB's renderer to use retail's formula) was rejected for permanent maintenance burden. Path C ships the architectural pattern while preserving retail-formula compliance.
Visual mesh and physics both still consume retail's FSplitNESW; they
remain in lockstep, no triangle-Z hover. The N.6 / N.7 sequencing
implication this issue carried (substitute physics math only when the
visual mesh migrates) is moot — neither side ever switches to WB's
formula.
Files added:
src/AcDream.App/Rendering/TerrainModernRenderer.cssrc/AcDream.Core/Terrain/TerrainSlotAllocator.cssrc/AcDream.App/Rendering/Shaders/terrain_modern.vertsrc/AcDream.App/Rendering/Shaders/terrain_modern.fragtests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs(the test that killed Path A)
Files deleted (T9):
src/AcDream.App/Rendering/TerrainChunkRenderer.cssrc/AcDream.App/Rendering/TerrainRenderer.cssrc/AcDream.App/Rendering/Shaders/terrain.vertsrc/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+ demangledname+ rawmangled)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.