acdream/docs/ISSUES.md
Erik b1af56eb19 fix(physics): L.4 — steep airborne hits slide-tangent (interim, deviates from retail)
Phase L.4 closes the "stuck in falling animation on a steep roof" bug
the user reported on 2026-04-30 ("I jump up, I land on it. It should not
even let me land, should just slide with a falling animation"). After
this commit the body no longer sticks to a steep roof when jumping
into it — it slides along the slope while keeping the falling animation.

Two pieces:

1. BSPQuery Path 6 steep-poly slide
   When an airborne sphere hits a polygon whose world normal Z is below
   FloorZ (≈ 0.6642, slope > ~49°), the previous flow was:
   Path 6 SetCollide → Path 4 set_walkable → ContactPlane committed →
   body "lands" on the steep poly with Contact bit + falling animation.
   This left the player stuck mid-slope because OnWalkable was cleared
   but Contact stayed set.

   The new branch detects the steep normal in Path 6 BEFORE SetCollide
   is called. Instead of entering the landing path, it removes the
   into-wall component of the move (project onto the steep face), sets
   CollisionNormal + SlidingNormal, and returns Slid. Same shape as
   Path 5's step-up fallback and CylinderCollision. The resolver retries;
   the sphere is now outside the poly; FindCollisions returns OK;
   ValidateTransition commits the slid position. ContactPlane is never
   set, so the body stays airborne with falling animation.

2. PlayerMovementController L.3a-bounce carve-out + Inelastic stop
   Re-enables the velocity-reflection bounce when the contact normal is
   upward-facing but steeper than walkable (0 < N.Z < FloorZ). The base
   L.3a rule suppresses bounce on landing transitions to avoid micro-
   bounce on flat terrain; that suppression also stuck the player to
   too-steep roofs they shouldn't land on. This carve-out re-enables
   the reflection specifically for the steep upward case.

Also lands related L.2c precipice / edge-slide work that was in flight:

- TransitionTypes EdgeSlideAfterStepDownFailed: walkable-poly-steep
  cliff route + steep-ContactPlane cliff route ordering, so that
  CliffSlide fires when the stored walkable polygon itself is too
  steep (Path 4 had previously accepted it as a "landing" via the
  permissive LandingZ threshold).
- CliffSlide reference-normal selection: prefer LastWalkable, fall back
  to LastKnownContactPlane only when walkable, else use world-up. This
  prevents the cross(steepN, steepN) = 0 degenerate case that left the
  cliff slide as a no-op when both current and last-known were steep.
- Phase 2 / step-down branch / edge-slide branch / cliff-slide
  diagnostic helpers gated on ACDREAM_DUMP_EDGE_SLIDE / ACDREAM_DUMP_STEEP_ROOF.
- Two new airborne-mover regression tests in BSPStepUpTests +
  PhysicsEngineTests covering wall-slide and edge tangent motion.

DEVIATION FROM RETAIL — DOCUMENTED FOR FOLLOW-UP

The Path 6 steep slide is NOT what retail does. Retail's flow on the
same hit is:

  Path 6 SetCollide (no steep check) → Path 4 find_walkable returns
  nothing for steep → Phase 3 reset path: restore_check_pos +
  kill_velocity → return COLLIDED → validate_transition reverts CheckPos
  to CurPos and forces OK.

Net retail behavior: position reverts to pre-failed-move (typically
just below the roof in the common jump-up case), velocity zeroed,
gravity rebuilds Z next frame, body falls back down naturally with
the falling animation. The "freeze" framing I used earlier was wrong;
in the typical case retail just bounces the body off and lets gravity
take over.

Strict retail behavior would match the user's intent better in the
common case AND avoid the bounce-energy-accumulation we saw with the
slide-tangent approach (V grew to ~50 m/s in continuous-contact frames).
However, retail's behavior degenerates in the edge case of an overhead
landing onto a steep slope (body would freeze mid-air above the roof).

This commit ships the slide-tangent fix as an interim "much better"
state per user verification on 2026-04-30. Follow-up work to match
retail strictly: revert Path 6 steep-slide, audit Phase 3 reset to
ensure kill_velocity (matching OBJECTINFO::kill_velocity ->
CPhysicsObj::set_velocity({0,0,0}, 0)) actually fires, and re-test.

Refs:
  - acclient_2013_pseudo_c.txt:323784-323821 (Path 6 SetCollide)
  - acclient_2013_pseudo_c.txt:273191-273239 (Phase 3 reset path)
  - acclient_2013_pseudo_c.txt:272563-272596 (validate_transition revert)
  - acclient_2013_pseudo_c.txt:274467-274475 (kill_velocity)
  - acclient_2013_pseudo_c.txt:282699-282715 (handle_all_collisions bounce)

Tests: 833/833 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 13:22:07 +02:00

39 KiB
Raw Blame History

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/*.md note.

Conventions

  • Sequential integer IDs (#1, #2, …). Commits that close an issue reference the ID in the message (e.g. fix #3: periodic TimeSync parsing).
  • Status is OPEN, IN-PROGRESS, or DONE. DONE items move to the Recently closed section at the bottom with closed-date + commit SHA.
  • Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
  • Promoting to a Phase: mark as DONE (promoted to Phase X) + commit SHA where the Phase entry landed.

Template

Copy this block when adding a new issue:

## #NN — Short title

**Status:** OPEN
**Severity:** HIGH | MEDIUM | LOW
**Filed:** YYYY-MM-DD
**Component:** e.g. sky, physics, net, ui

**Description:** One paragraph — what's wrong or what's missing.

**Root cause / status:** What we know so far. Empty if unknown.

**Files:** Path references with approximate line numbers.

**Research:** Links to `docs/research/*.md` if applicable.

**Acceptance:** How we'll know it's fixed.

Active issues

#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. Remaining gaps: live visual confirmation of the retry-loop fix, real-DAT building-edge fixtures, fuller cliff_slide coverage, and NegPolyHit dispatch. 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.


#33 — Live entity collision shape collapses to one cylinder

Status: OPEN Severity: MEDIUM Filed: 2026-04-29 Component: physics / entities

Description: Live world entities do not yet use exact retail CSphere / CCylSphere shape semantics. Several paths collapse the entity to a simplified root-centered cylinder or fallback radius, which is not enough for retail object and creature collision parity.

Root cause / status: Tracked under Phase L.2d. Requires auditing object shape extraction, Setup.Radius fallback, building object identity, and live entity broadphase records against named retail.

Files: src/AcDream.Core/Physics/CollisionPrimitives.cs, src/AcDream.Core/Physics/ShadowObjectRegistry.cs, src/AcDream.Core/Physics/PhysicsDataCache.cs.

Research: docs/plans/2026-04-29-movement-collision-conformance.md.

Acceptance: Live object collision uses the appropriate retail sphere or cylsphere data where available. Tests prove at least one multi-shape object and one live creature case no longer use the single-cylinder fallback.


#2 — Lightning visual mismatch (sky PES path disproved)

Status: OPEN Severity: MEDIUM Filed: 2026-04-25 Component: weather / sky / vfx

Description: Lightning/storm sky visuals still do not match retail. A 2026-04-28 named-retail recheck disproved the prior assumption that SkyObject.PesObjectId drives sky-render flash particles: SkyDesc::GetSky copies the field into CelestialPosition.pes_id, but GameSky::CreateDeletePhysicsObjects, GameSky::MakeObject, and GameSky::UseTime never read it.

Root cause / status: Open again. The sky-PES path is non-retail and must stay disabled for normal rendering. The remaining mismatch likely lives in the sky/weather mesh material path, the lightning/fog flash path, or another weather subsystem outside GameSky; do not reintroduce per-SkyObject PES playback without new decompile evidence.

Files:

  • src/AcDream.App/Rendering/Sky/SkyRenderer.cs — sky/weather mesh draw, material state, pre/post split
  • src/AcDream.App/Rendering/Shaders/sky.frag — flash/fog/lightning coloration path
  • src/AcDream.Core/World/SkyDescLoader.cs — keep PesObjectId parsed for diagnostics, not render playback

Research:

  • docs/research/2026-04-28-pes-pseudocode.md — C.1 correction: CelestialPosition.pes_id copied but ignored by GameSky
  • docs/research/2026-04-23-sky-pes-wiring.md — earlier decompile trace reached the same no-sky-PES conclusion
  • docs/research/2026-04-23-lightning-real.md (decompile trace + dat discovery)
  • docs/research/2026-04-23-physicsscript.md (runtime semantics)
  • docs/research/2026-04-23-lightning-crossfade.md (crossfade mechanism)

Acceptance: During a Rainy DayGroup's storm window, visible flashes appear in the sky at the dat-scripted moments, the fragment-shader flash bump briefly brightens the scene, and (later, once thunder audio is wired) a thunder clap plays with a short propagation delay.


#3 — Client clock drifts from retail after ~10 minutes (periodic TimeSync missing)

Status: OPEN Severity: MEDIUM Filed: 2026-04-25 Component: net / sky

Description: Our WorldTimeService.DayFraction syncs with the server once at login via ConnectRequest + TimeSync, then advances from the local wall-clock. Retail receives periodic TimeSync refreshes (header flag 0x1000000) carrying a fresh PortalYearTicks double and re-anchors its clock. Without those, acdream's keyframe state drifts from retail's over 10+ minutes — observed during the 2026-04-24 sky-color debug sessions where retail was at DayFraction 0.976 while acdream was at 0.634.

Root cause / status: Mechanism is well-understood (see research). WorldTimeService.SyncFromServer(double) already exists — we just need to detect the periodic flag in the packet header and call it whenever a fresh tick arrives.

Files:

  • src/AcDream.Core.Net/WorldSession.cs — header-flag parsing; currently only the initial sync is consumed
  • src/AcDream.Core/World/WorldTimeService.csSyncFromServer(double ticks) ready; needs caller wiring

Research: docs/research/deepdives/r12-weather-daynight.md §TimeSync (line ~563). References retail packet-header flag 0x1000000 carrying PortalYearTicks double.

Acceptance: Probe retail via tools/RetailTimeProbe and acdream's ACDREAM_DUMP_SKY log at the same wall-clock moment after a 20-minute session without re-login; abs(acdream.DayFraction - retail.DayFraction) < 0.01.



#13 — PlayerDescription trailer past enchantments (options / shortcuts / hotbars / desired_comps / spellbook_filters / options2 / gameplay_options / inventory / equipped)

Status: OPEN Severity: LOW (no current user-visible bug; future panels will need the data) Filed: 2026-04-25 Component: net / player-state

Description: PlayerDescriptionParser walks through enchantments (Phase H, 2026-04-25). The trailer beyond that — Options1 / Shortcuts / HotbarSpells (8 lists) / DesiredComps / SpellbookFilters / Options2 / GameplayOptions blob / Inventory / Equipped — is not yet parsed. Required for future Spellbook UI panel, hotbar UI, inventory UI, character options panel.

Root cause / status: Holtburger events.rs:462-625 has the full layout. The trickiest piece is gameplay_options — a variable-length opaque blob; holtburger uses a heuristic forward search (find_inventory_start_after_gameplay_options) for plausibly-aligned inventory-count + GUID pairs to find the inventory start. Other sections are well-formed.

Files:

  • src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs — extend Parsed record + walker.
  • tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs — add fixtures per section.
  • src/AcDream.Core.Net/GameEventWiring.cs — route parsed.Inventory + Equipped to ItemRepository.

Research: holtburger events.rs:462-625; references/actestclient/TestClient/messages.xml.

Acceptance: All sections of a real-world PlayerDescription parse to completion (no truncation). New tests cover synthetic fixtures per section. ItemRepository.Count after login > 0.



#4 — Sky horizon-glow disabled (fog-mix skipped on sky meshes)

Status: OPEN Severity: LOW (aesthetic feature-parity, not regression from pre-session state) Filed: 2026-04-25 Component: sky

Description: Phase 8.1 (commit 593b76f) disabled the fog-mix on sky meshes to fix the "entire dome swallowed by fog color" regression. Dereth's keyframe FogEnd values (02400 m) are calibrated for terrain; sky meshes are authored at radii 105014271 m so every sky pixel was past FogEnd, saturated to uFogColor, destroying stars / moon / dome texture. Disabling the mix restored visibility but we lost retail's horizon-glow effect (gradient from clear zenith to fog-tinted horizon band at dusk/dawn).

Root cause / status: Three competing hypotheses, none pinned down: (a) retail uses a different fog range for sky than terrain; (b) retail applies fog with an elevation-angle weighting rather than linear distance; (c) retail's sky meshes don't participate in the global fog and the "horizon glow" comes from a different atmospheric-scatter path. Need to identify retail's actual sky-fog behaviour before re-enabling with correct parameters.

Files:

  • src/AcDream.App/Rendering/Shaders/sky.frag — line ~55, rgb = mix(uFogColor.rgb, rgb, vFogFactor) currently commented out
  • src/AcDream.App/Rendering/Shaders/sky.vert — lines 109-114, vFogFactor computation

Research: docs/research/2026-04-23-sky-fog.md. Partial; doesn't pin the sky-specific fog path.

Acceptance: At dusk in Holtburg, the sky dome shows a clear zenith and a warm fog-tinted horizon band that matches retail's appearance, with stars / moon / sun / clouds all still visible at their correct brightnesses elsewhere in the frame.


#28 — Aurora ("northern lights") effect not rendered

Status: OPEN Severity: LOW (aesthetic feature-parity) Filed: 2026-04-26 Component: sky / vfx

Description: Retail renders a dynamic colored "light play" effect in the sky during certain Rainy/Cloudy DayGroup time windows. The user describes it as aurora-borealis-style. acdream renders no comparable effect.

Root cause / status: Open again. The prior root cause was wrong: CelestialPosition.pes_id exists in the retail header and is populated by SkyDesc::GetSky, but named retail GameSky code does not read it during sky object creation, update, or draw. A 2026-04-28 C.1 experiment that played those PES ids produced colored blobs/wash that did not match retail's broad aurora-like rays, and the path is now debug-only behind ACDREAM_ENABLE_SKY_PES=1.

Retail header at acclient.h line 35451 still documents the copied field:

struct CelestialPosition {
    IDClass<...> gfx_id;
    IDClass<...> pes_id;          // ← particle scheduler ID
    float heading; float rotation;
    Vector3 tex_velocity;
    float transparent; float luminosity; float max_bright;
    unsigned int properties;
};

StarsProbe confirmed Dereth Rainy DayGroup 3 carries multiple PES-bearing entries (verified 2026-04-27). Sample for the user's observed Warmtide-Rainy state:

OI Gfx PES Active window Notes
5 0x02000714 0x330007DB always low-rate background
7 0x02000BA6 0x33000453 0.030.19 early morning
17 0x02000589 0x3300042C 0.270.91 active during user's screenshot

acdream's geometry half is now wired (commit landing 2026-04-27 — EnsureSetupUploaded walks Setup.Parts for 0x020xxx IDs). The remaining dynamic visual half is not SkyObject.PesObjectId; likely suspects are sky/weather mesh material state, texture transform/blending, or a separate weather/lightning subsystem outside GameSky.

Implementation outline:

  1. Keep SkyObject.PesObjectId parsed for diagnostics only.
  2. Compare retail/acdream material state for the active sky/weather GfxObj/Setup ids (0x02000588, 0x02000589, 0x02000714, 0x02000BA6).
  3. Trace the named retail sky/weather draw path for texture transforms, translucency, diffusion, luminosity, and any non-GameSky weather effect dispatch.
  4. Only add a new runtime visual path once the decompile has an actual caller.

Decomp pointers:

  • SkyDesc::GetSky named retail 0x00501ec0 — copies SkyObject.default_pes_object into CelestialPosition.pes_id.
  • GameSky::CreateDeletePhysicsObjects named retail 0x005073c0 — creates/updates sky objects from gfx_id, does not read pes_id.
  • GameSky::MakeObject named retail 0x00506ee0 — calls CPhysicsObj::makeObject(gfx_id, 0, 0), no PES.
  • GameSky::UseTime named retail 0x005075b0 — updates frame/luminosity/diffusion/translucency, no PES.

Files:

  • src/AcDream.Core/World/SkyDescLoader.cs — carries PesObjectId for diagnostics.
  • src/AcDream.App/Rendering/Sky/SkyRenderer.cs — likely material/texture-transform parity work.
  • src/AcDream.App/Rendering/GameWindow.cs — sky-PES playback remains debug-only, disabled by default.

Acceptance: When retail shows aurora-style light play at a specific in-game time / weather, acdream shows a visually-comparable effect at the same time.


#29 — Cloud surface 0x08000023 still appears thinner than retail despite blend-mode + Setup fixes

Status: OPEN Severity: LOW (aesthetic feature-parity) Filed: 2026-04-27 Component: sky / clouds

Description: User screenshot comparison showed acdream's clouds let too much sun through; retail's are denser and have a purpleish tint. Two follow-up fixes landed without visible improvement:

  1. TranslucencyKindExtensions.FromSurfaceType now applies retail's Translucent-override at D3DPolyRender::SetSurface (decomp 425246-425260) — surface 0x08000023 (Type=0x10114 = B1ClipMap | Translucent | Alpha | Additive) is now correctly classified as AlphaBlend instead of Additive.
  2. SkyRenderer.EnsureSetupUploaded now loads 0x020xxxxx Setup IDs (e.g. 0x02000588, 0x02000589, 0x02000714, 0x02000BA6) which were silently dropped. Setup parts are flattened via SetupMesh.Flatten and uploaded with their per-part transform baked into vertex positions.

Despite both being decomp-correct fixes, the user reports no observable visual change in dual-client comparison. Two follow-up hypotheses:

  • The Setup objects are tiny placeholder meshes (one 0x010001EC part each) that exist mainly to anchor a PES emitter — the cloud "density" / "purple sheen" the user perceives is entirely the PES particle layer, not the static mesh.
  • The cloud surface might still be rendering correctly per its dat data, and what looks "thicker" in retail is the additional aurora-like PES sheen overlaid on top.

If hypothesis (a) is correct, this issue effectively rolls into #28 — the PES rendering work would resolve both.

Files:

  • src/AcDream.Core/Meshing/TranslucencyKind.cs — Translucent override
  • src/AcDream.App/Rendering/Sky/SkyRenderer.csEnsureSetupUploaded

Acceptance: Cloud sheets look as dense/purple as retail in dual-client side-by-side. May require #28 (PES) to land first.



Recently closed

#31 — [DONE 2026-04-29] Low outdoor cell id can go stale after transition movement

Closed: 2026-04-29 Commit: (this commit) Resolution: ResolveWithTransition now refreshes outdoor cell ownership from the resolved world position while the sphere sweep runs. Intra-landblock 24m outdoor seams update the low cell id, and full-cell callers crossing a landblock seam get the destination landblock prefix plus the correct outdoor low cell.


#34 — [DONE 2026-04-29] Missing routine local/server correction diagnostic

Closed: 2026-04-29 Commit: (this commit) Resolution: Added ACDREAM_DUMP_MOVE_TRUTH=1, which logs local resolved position/contact/cell, outbound movement fields, server UpdatePosition echo, and local/server correction delta for the player in grep-friendly move-truth OUT / move-truth ECHO lines.


#30 — [DONE 2026-04-29] AutonomousPosition contact byte is too often grounded

Closed: 2026-04-29 Commit: (this commit) Resolution: GameWindow now derives the movement contact byte from MovementResult.IsOnGround and passes it explicitly to both MoveToState.Build and AutonomousPosition.Build. Added packet tests proving both builders encode an explicit airborne contact byte.


#27 — [DONE 2026-04-26] Cloud meshes appeared missing or faint vs retail

Closed: 2026-04-26 Commit: 4678b3e fix(sky): apply per-Surface Translucency + Luminosity for retail-faithful weather Resolution: Resolved as a side-effect of the Bug A fix. The original observation came from a session where every sky mesh got effEmissive = 1.0 (saturated vTint to white), which made stars/clouds look full-bright instead of time-of-day-tinted. Fix 2 corrected the emissive default to sub.SurfLuminosity so cloud surfaces (Lum=0.0) now run through the ambient+diffuse vertex-lit path and pick up keyframe tint. Fix 1 separately plumbed surface.Translucency to the shader, picking up the 0.25 translucency on cloud surface 0x08000023 (75% opacity). Visual verification under Phase 0 of the followup plan: clouds and colors now match retail at LCG-picked DayGroups across the day cycle.


#1 — [DONE 2026-04-26] Rain falls only to horizon, not to the player's feet

Closed: 2026-04-26 Commits: 3e0da49 (sky pass split + retail -120m Z offset), 4678b3e (Surface.Translucency + Luminosity correctness), d95a8d2 (legacy emitter delete) Resolution: Two-part fix. First, rain rendering was completely re-architected to match retail's LScape::draw pattern at 0x00506330 — sky pass before the landblock loop (RenderSky), weather pass after (RenderWeather). Weather meshes now overlay terrain instead of being painted over. Camera anchored inside the rain cylinder via the retail-correct -120m Z offset (constant 0xc2f00000 in GameSky::UpdatePosition at 0x00506dd0). Second, the per-Surface Translucency float (rain = 0.5) and Luminosity float (rain = 0.1484) were both being ignored by the renderer; plumbed end-to-end so streaks contribute at retail-correct intensity instead of 6.7× too bright. Legacy camera-attached particle emitter (UpdateWeatherParticles + BuildRainDesc + BuildSnowDesc) deleted; world-space mesh is the only path now. Snow rides the same fix automatically. Filed alongside two follow-up issues from the visual-verify session: #27 (cloud rendering parity), #28 (aurora/northern lights).


#26 — [DONE 2026-04-26] Stars rendered as a square in one corner of the sky

Closed: 2026-04-26 Commit: 7b88fde fix(sky): drive wrap mode from mesh UV range — fixes Bug B (stars-as-square) Resolution: SkyRenderer's wrap-mode heuristic was GL_CLAMP_TO_EDGE unless TexVelocity != 0, which mis-classified the inner sky/star layer 0x010015EF (UVs in [0.398, 4.602], TexVel=0). Most of the dome sampled the texture's edge texels; only the small region where UVs fell in [0,1] showed actual texture content. Fixed by computing NeedsUvRepeat per submesh from the actual UV range during GfxObjMesh.Build() and driving the wrap-mode choice from that flag plus the existing scrolling check. Outer dome 0x010015EE/F0/F1/F2 (UVs strictly in [0,1]) keeps CLAMP_TO_EDGE so no seam regression. Probe tools/StarsProbe/ (commit 991fb9a) committed alongside as the diagnostic that found this.


#25 — [DONE 2026-04-26] Phase K.3 — Settings panel + click-to-rebind UI

Closed: 2026-04-26 Commit: (this commit) Resolution: SettingsPanel with click-to-rebind UX (modal capture via InputDispatcher.BeginCapture, Esc cancels, conflict prompt with Yes/No, draft / Save / Cancel semantics), F11 toggle + ImGui MainMenuBar entry, per-action / per-section / reset-all-defaults buttons. Roadmap + ISSUES + memory crib + CLAUDE.md updated.


#24 — [DONE 2026-04-26] Phase K.2 — auto-enter player mode + MMB mouse-look

Closed: 2026-04-26 Commit: af74eac Resolution: Auto-enter player mode at login (one-shot guard reusing the existing Tab handler logic); MMB-hold mouse-look (CameraInstantMouseLook — cursor-locked camera + character yaw drive together); Tab → ChatPanel.FocusInput(); DebugPanel "Toggle Free-Fly Mode" button.


#23 — [DONE 2026-04-26] Phase K.1c — retail-default keymap + JSON persistence

Closed: 2026-04-26 Commit: da18910 Resolution: ~149 retail-faithful bindings byte-precise to docs/research/named-retail/retail-default.keymap.txt; %LOCALAPPDATA%\acdream\keybinds.json with merge-over-defaults migration; acdream debug F-keys relocated to Ctrl+F*.


#22 — [DONE 2026-04-26] Phase K.1b — cut handlers over to dispatcher

Closed: 2026-04-26 Commit: 256e962 Resolution: Drop the legacy mouse-X-character-yaw path; fix WantCaptureMouse gating; single input path via the multicast InputDispatcher.


#21 — [DONE 2026-04-26] Phase K.1a — input architecture skeleton

Closed: 2026-04-26 Commit: 84512d3 Resolution: Action enum, multicast InputDispatcher with scope stack, KeyChord / Binding / KeyBindings, Silk.NET adapters; parallel to existing handlers (no behavior change).


#20 — [DONE 2026-04-25] CombatChatTranslator — retail-faithful combat-text formatters

Closed: 2026-04-25 Commit: 3d26c8e Resolution: Retail-faithful combat-text formatters into ChatLog ("You hit drudge for 50 slashing damage"). Subscribes to CombatState's DamageTaken / DamageDealtAccepted / EvadedIncoming / MissedOutgoing / AttackDone / KillLanded events; templates ported verbatim from holtburger panels/chat.rs:221-308.


#19 — [DONE 2026-04-25] TurbineChat codec (0xF7DE) + ChatChannelInfo

Closed: 2026-04-25 Commit: ca968fc Resolution: Full 0xF7DE codec with three payload variants (EventSendToRoom, RequestSendToRoomById, Response), UTF-16LE strings with variable-length prefix, SetTurbineChatChannels (0x0295) parser, unified ChatChannelInfo (Legacy + Turbine variants), TurbineChatState. Note: ACE doesn't run a TurbineChat server — codec is ready for retail-server-emulating setups.


#18 — [DONE 2026-04-25] Holtburger inbound chat parity + Windows-1252 codec

Closed: 2026-04-25 Commit: ff5ed9e Resolution: EmoteText (0x01E0) / SoulEmote (0x01E2) / ServerMessage (0xF7E0) / PlayerKilled (0x019E) parsers + WeenieError routing through GameEventWiring. Global codec switch from Encoding.ASCII to Encoding.GetEncoding(1252); matches retail + holtburger; accented names round-trip correctly.


#17 — [DONE 2026-04-25] ChatPanel input field + slash commands

Closed: 2026-04-25 Commit: f14296c Resolution: ChatPanel gains Enter-to-submit input field; ChatInputParser recognises /say /t /tell /r /g /f /a /m /p /v /cv /lfg /trade /role /society /olthoi; ChatVM tracks LastIncomingTellSender for /r reply.


#16 — [DONE 2026-04-25] LiveCommandBus + WorldSession chat senders

Closed: 2026-04-25 Commit: 8e6e5a0 Resolution: Real ICommandBus impl + WorldSession.SendTalk / SendTell / SendChannel wrappers + SendChatCmd record + ChannelResolver legacy-id mapping per holtburger.


#15 — [DONE 2026-04-25] DebugPanel migration

Closed: 2026-04-25 Commit: 56037a4 Resolution: Migrates the 473-LOC StbTrueTypeSharp DebugOverlay to an ImGui DebugPanel with collapsing-headers + checkbox diagnostics + combat-event tail. Deletes DebugOverlay.cs; TextRenderer + BitmapFont kept for future HUD-in-world (D.6 damage floaters, name plates).


#14 — [DONE 2026-04-25] IPanelRenderer widget extension

Closed: 2026-04-25 Commit: b131514 Resolution: Adds 14 widget signatures (TextColored / Checkbox / Combo / InputTextSubmit / BeginTable / etc.) to IPanelRenderer + ImGuiPanelRenderer impl. Foundation for I.2 DebugPanel and I.4 ChatPanel input.


#7 — [DONE 2026-04-25] PlayerDescription parser stops after spells (enchantment block parsed)

Closed: 2026-04-25 Commit: feat(net): #7 PlayerDescriptionParser — enchantment block walker + StatMod flow Resolution: Extended PlayerDescriptionParser past the spell block to parse the Enchantment trailer per holtburger events.rs:462-501. Added EnchantmentEntry record with full wire payload (16 fields including the StatMod triad — type/key/val) + EnchantmentBucket (Multiplicative / Additive / Cooldown / Vitae per EnchantmentMask). Parsed now exposes IReadOnlyList<EnchantmentEntry> Enchantments. GameEventWiring routes each entry through the new Spellbook.OnEnchantmentAdded(ActiveEnchantmentRecord) overload with StatModType / StatModKey / StatModValue / Bucket populated. 2 new parser tests cover the enchantment block schema + Vitae singleton.

The remaining trailer sections (options / shortcuts / hotbars / inventory / equipped) are not yet parsed; filed as #13. Stopping after enchantments is intentional — it covers the highest-value section (issue #6 lights up) and avoids the heuristic gameplay_options walker that #13 needs.


#12 — [DONE 2026-04-25] Capture full Enchantment wire payload (StatMod) on ActiveEnchantmentRecord

Closed: 2026-04-25 Commit: feat(net): #7 PlayerDescriptionParser — enchantment block walker + StatMod flow Resolution: Closed alongside #7 in the same commit. ActiveEnchantmentRecord extended with optional StatModType, StatModKey, StatModValue, Bucket fields. Spellbook got an OnEnchantmentAdded(ActiveEnchantmentRecord) overload that accepts the full record. EnchantmentMath.GetMod aggregator now consumes the StatMod data: multiplicative bucket (1) → multiplier ×= val; additive bucket (2) → additive += val; vitae bucket (8) → multiplier ×= val (applied last, matching retail CEnchantmentRegistry::EnchantAttribute semantics). 5 new EnchantmentMath StatMod-aware tests cover: multiplicative buffs aggregate, additive buffs sum, stat-key mismatch is filtered out, vitae applies multiplicatively, family-stacking picks the higher spell-id buff.

ParseMagicUpdateEnchantment (the live-update opcode 0x02C2) is not yet extended — it still uses the 4-field summary. That's a separate refactor; PlayerDescription's enchantment block is the load-bearing path for issue #6, and that's now flowing.


#6 — [DONE 2026-04-25 architecture; data flowing as of #12] Vital max ignores enchantment buffs + vitae

Closed: 2026-04-25 Commit: feat(player): #6 fold enchantment buffs into vital max via EnchantmentMath Resolution: Ported CEnchantmentRegistry::EnchantAttribute (PDB 0x00594570) as EnchantmentMath.GetMod(IEnumerable<ActiveEnchantmentRecord>, SpellTable, statKey) returning (Multiplier, Additive). Family-stacking dedup via SpellTable.Family (only one buff per family bucket wins, by highest spell-id as a generation proxy). Spellbook.GetVitalMod(statKey) delegates. LocalPlayerState.GetMaxApprox reworked to apply (unbuffed × mult) + add with retail's min-vital clamp (>= 5 if base ≥ 5 else >= 1, matches CreatureVital::GetMaxValue at PDB 0x0058F2DD). Stat-key constants (MaxHealth=1, MaxStamina=3, MaxMana=5) verified against docs/research/named-retail/acclient.h line 37287-37301.

Architecture in place; data still flat. Until ISSUES.md #12 lands the wire-format extension that captures StatMod (type/key/val) on ActiveEnchantmentRecord, the per-enchantment modifier value isn't aggregated yet — EnchantmentMath.GetMod returns Identity (1.0, 0.0) for every stat key. Once #12 wires the data, the existing aggregator + formula light up automatically. Live +Acdream Stam/Mana percent will continue to read ~95% until #12 lands.

6 new EnchantmentMathTests cover: empty list returns Identity, no-table-entries returns Identity, stat-key constants match ACE enum, Identity is (1, 0), family-stacking dedup, family=0 (no-bucket) treated as separate.


#11 — [DONE 2026-04-25] Spell metadata loader (spells.csv → SpellTable)

Closed: 2026-04-25 Commit: feat(spells): #11 SpellTable — hydrate metadata from spells.csv at startup Resolution: Added SpellMetadata record + SpellTable CSV loader (hand-rolled RFC 4180-ish parser for the quoted Description column with embedded commas). Wired into Spellbook constructor as optional metadata source; Spellbook.TryGetMetadata(spellId, out) returns the static record when found. GameWindow loads data/spells.csv from bin output at construction (file copied via <None Include> in AcDream.App.csproj from docs/research/data/spells.csv). Falls back to SpellTable.Empty + console warning if the file is missing (e.g. tooling contexts). 10 new tests covering: empty table, header-only, simple row, quoted description with commas, blank lines skipped, bad spell-id rows skipped, lookup hit/miss, RFC 4180 escaped-quote parsing.


#9 — [DONE 2026-04-25] Address-correction sweep on acclient_function_map.md

Closed: 2026-04-25 Commit: docs(research): #9 sweep acclient_function_map.md against PDB symbols Resolution: Wrote tools/pdb-extract/check_function_map.py that cross-checks 63 hand-curated entries against docs/research/named-retail/symbols.json. Findings: zero entries matched address-and-name exactly (confirms ~0x800-0xC10 byte delta vs the binary that produced our Ghidra chunks — different build revision). 38 entries corrected by PDB name lookup; 25 entries either lack PDB symbol records (inlined / non-public) or had wrong class assignments (e.g. 0x5387C0 claimed as CTransition::find_collisions was actually CPolygon::polygon_hits_sphere). Updated acclient_function_map.md with corrected addresses, kept legacy addresses in a "Was" column for traceability, added a top-of-file sweep summary.


#10 — [DONE 2026-04-25] Wire KillerNotification (0x01AD)

Closed: 2026-04-25 Commit: docs(issues): #8/#9/#11 filed; #10 wired (KillerNotification) Resolution: Orphan parser at GameEvents.ParseKillerNotification existed but was never registered for dispatch in GameEventWiring.cs. Added a combat.OnKillerNotification(victimName, victimGuid) method on CombatState that fires a new KillLanded event, then registered the handler. One-line dispatch + 12-line CombatState method + one regression test fixture in GameEventWiringTests.


#8 — [DONE 2026-04-25] pdb-extract tool: PDB → symbols.json + types.json

Closed: 2026-04-25 Commit: tools(pdb-extract): #8 PDB -> symbols.json + types.json sidecar Resolution: Pure-Python (no deps) MSF 7.00 PDB parser at tools/pdb-extract/pdb_extract.py. Reads refs/acclient.pdb (Sept 2013 EoR build), extracts S_PUB32 records from the symbol stream + named class/struct types from TPI, and writes JSON sidecars to docs/research/named-retail/:

  • symbols.json — 18,366 named functions (address + demangled name + raw mangled)
  • types.json — 5,371 named class/struct records (name + size + kind)

Best-effort MSVC C++ demangler handles the common ?Method@Class@@<sig> patterns + ctors (??0) + dtors (??1); operator overloads and vtables left mangled. Spot-check verified: CEnchantmentRegistry::EnchantAttribute resolves to 0x00594570 exactly as the discovery agent reported. Runtime <1s.

Regen workflow: py tools/pdb-extract/pdb_extract.py refs/acclient.pdb. The committed JSON outputs are stable + ~3 MB combined; ripgrep/jq on them is faster than re-parsing.


#5 — [DONE 2026-04-25] VitalsPanel stamina/mana bars always null

Closed: 2026-04-25 Commit: feat(player): #5 PlayerDescription parser — Stam/Mana via attribute block Resolution: First attempt (commit d42bf57) used AppraiseInfoParser for PlayerDescription (0x0013) — wrong wire format. ACE source confirmed via GameEventPlayerDescription.WriteEventBody: PlayerDescription is hand-written (DescriptionPropertyFlag-driven property hashtables, vector flags, attribute block, skills, spells, options/inventory tail) — distinct from IdentifyObjectResponse (0x00C9)'s AppraiseInfo.Write. Pivoted to a real port: new PlayerDescriptionParser.cs that walks property hashtables (Int32/Int64/Bool/Double/String/Did/Iid + Position) gated on the property flags, then reads vector flags + has_health + the attribute block where vitals 7/8/9 carry ranks/start/xp/current. Also redesigned LocalPlayerState to track per-vital snapshots (replacing the sentinel-API of attempt 1) plus per-attribute snapshots, with GetMaxApprox applying the retail formula vital.(ranks+start) + attribute_contribution (Endurance/2 for Health, Endurance for Stamina, Self for Mana). Live verified: +Acdream shows three bars; ~95% reading on Stam/Mana traced to active buff multipliers (filed as #6). Wire-port also added PrivateUpdateVital (0x02E7) + PrivateUpdateVitalCurrent (0x02E9) for delta updates per holtburger UpdateVital. ~700 LOC C#, 30+ new tests.