Deep investigation of the retail AC client's GUI subsystem, driven by 6
parallel Opus research agents, plus the first cut of a retail-faithful
retained-mode widget toolkit that scaffolds Phase D.
Research (docs/research/retail-ui/):
- 00-master-synthesis.md — cross-slice synthesis + port plan
- 01-architecture-and-init.md — WinMain, CreateMainWindow, frame loop,
Keystone bring-up (7 globals mapped)
- 02-class-hierarchy.md — key finding: UI lives in keystone.dll,
not acclient.exe; CUIManager + CUIListener
MI pattern, CFont + CSurface + CString
- 03-rendering.md — 24-byte XYZRHW+UV verts, per-font
256x256 atlas baked from RenderSurface,
TEXTUREFACTOR coloring, DrawPrimitiveUP
- 04-input-events.md — Win32 WndProc → Device (DAT_00837ff4)
→ widget OnEvent(+0x128); full event-type
table (0x01 click, 0x07 tooltip ~1000ms,
0x15 drag-begin, 0x21 enter, 0x3E drop)
- 05-panels.md — chat, attributes, skills, spells, paperdoll
(25-slot layout), inventory, fellowship,
allegiance — with wire-message bindings
- 06-hud-and-assets.md — vital orbs (scissor fill), radar
(0x06001388/0x06004CC1, 1.18× shrink),
compass strip, dat asset catalog
Key insight: keystone.dll owns the actual widget toolkit — we cannot
port a class hierarchy from the decompile because it's not there.
Instead we implement our own retained-mode toolkit with retail-faithful
behavior (event codes, focus/modal/capture, drag-drop state machine)
and will consume the same portal.dat fonts + sprites so the visual
identity is preserved.
C# scaffold (src/AcDream.App/UI/):
- UiEvent — 24-byte event struct + retail event-type constants
(0x01 click, 0x15 drag-begin, 0x201 WM_LBUTTONDOWN,
etc.) matching retail decompile switches
- UiElement — base widget: children, ZOrder, focus/capture flags,
virtual OnDraw/OnEvent/OnHitTest/OnTick; children-
first hit test + back-to-front composite
- UiPanel — panel, label, button primitives
- UiRenderContext — 2D draw context with translate stack
- UiRoot — top-of-tree + Device responsibilities (mouse/
keyboard state, focus, modal, capture, drag-drop,
tooltip timer); WorldMouseFallThrough/
WorldKeyFallThrough preserves existing camera
controls when no widget consumes
- UiHost — packages UiRoot + TextRenderer + input wiring
helpers for one-line integration into GameWindow
- README.md — orientation for future agents
Roadmap (docs/plans/2026-04-11-roadmap.md):
- D.1 marked shipped (debug overlay from 2026-04-17)
- D.2 expanded to include the retail UI framework landed here
- D.3-D.7 added: AcFont, dat sprites, core panels, HUD, CursorManager
- D.8 remains sound
All existing 470 tests pass. 0 warnings, 0 errors.
14 KiB
acdream — strategic roadmap
Status: Living document. Updated 2026-04-11 after Phase 6, 7.1, 9.1, 9.2 landed.
Purpose: One source of truth for where the project is and where it's going. Every observed defect or missing feature has a named phase that owns it; when something looks wrong in-game, look here to find the phase that'll address it. Implementation details live in per-phase specs under docs/superpowers/specs/, not in this file.
Phases already shipped
| Phase | What landed | Verification |
|---|---|---|
| 1 | Terrain rendering, plugin host scaffold | Visual ✓ |
| 2a | Static stabs/buildings (126 entities) | Visual ✓ |
| 2b | Textured 3×3 landblock grid + FlyCamera + IGameState | Visual ✓ |
| 2c | Procedural scenery (419 trees/rocks/bushes) | Visual ✓ |
| 2d | Interior EnvCell walker (475 static interior objects) | Visual ✓ |
| 3a/3b | Directional sun lighting + per-vertex terrain normals | Visual ✓ |
| 3c | Per-cell terrain texture blending (alpha atlas) | Visual ✓ |
| 4 | Full UDP codec + handshake + character login + WorldSession | Live ✓ |
| 5 | ObjDesc: AnimPartChange + TextureChanges + SubPalettes + ObjScale + Placement.Resting | Live ✓ |
| 6.1 | Idle motion frame resolution (MotionResolver MVP) | Live ✓ |
| 6.2 | Server-sent MovementData stance + forward command honored |
Live ✓ |
| 6.3 | Server-supplied MotionTableId override (fixes drudge statue) |
Live ✓ |
| 6.4 | Per-frame animation playback (breathing, idle cycles) | Live ✓ |
| 6.5 | Slerp between keyframes for smooth animation | Live ✓ |
| 6.6 | UpdateMotion (0xF74C) parser + dispatch to animation tick |
Live ✓ |
| 6.7 | UpdatePosition (0xF748) parser + position reseating |
Live ✓ |
| 7.1 | EnvCell room geometry — walls/floors/ceilings via CellStruct + Environment dats | Visual ✓ |
| 9.1 | Translucent render pass (AlphaBlend / Additive / InvAlpha + per-kind blend funcs) | Visual ✓ |
| 9.2 | Back-face culling in translucent pass (fixes lifestone crystal) | Visual ✓ |
| A.1 | Streaming landblock loader — runtime-configurable visible window (default 5×5, ACDREAM_STREAM_RADIUS), camera-centered offline / player-centered live, hysteresis-based unloads, pending-spawn list for late CreateObject events |
Live ✓ |
| A.2 | Frustum culling — per-landblock AABB test (Gribb-Hartmann), terrain + static-mesh renderers skip culled landblocks, perf overlay in window title | Visual ✓ |
| A.3 | Background net receive thread — dedicated daemon thread buffers UDP into Channel, render thread drains | Visual ✓ |
| B.3 | Physics collision engine — TerrainSurface (heightmap Z), CellSurface (indoor floor polygon projection), PhysicsEngine (resolver with step-height + cell transitions). Populated from streaming pipeline. | Tests ✓ |
| B.2 | Player movement mode — Tab-toggled WASD ground walking, walk/run/idle animations, third-person chase camera, MoveToState + AutonomousPosition outbound, portal entry. Outdoor-only MVP. | Live ✓ |
Plus polish that doesn't get its own phase number:
- FlyCamera default speed lowered + Shift-to-boost
- SurfaceDecoder: PFID_P8 / PFID_R8G8B8 / PFID_X8R8G8B8 decoders
- GfxObjMesh: emit both pos and neg sides of double-sided polygons
- EnvCell mesh Z-lift to fix ground-floor / terrain flicker
Phases ahead — agreed order
Phase A — Foundation (in progress)
Goal: walk across 10+ landblocks without crashes, without hitches at landblock boundaries, and without framerate cratering.
Sub-pieces:
- ✓ SHIPPED — A.1 — Streaming landblock loader. Runtime-configurable visible window (default 5×5,
ACDREAM_STREAM_RADIUSenv var override). Center follows the fly camera offline and the server-sent player position in live mode. Currently runs synchronously on the render thread — the original async-worker design hit DatCollection's lack of thread safety and was reverted for correctness. The Channel-based outbox API is preserved so async loading can return cleanly whenPhase A.3introduces a thread-safe dat wrapper. Pending-spawn list inGpuWorldStateparks liveCreateObjectevents whose target landblock hasn't been streamed in yet and back-fills them when it arrives, so spawn-vs-streaming races are no longer racy at all.MaxCompletionsPerFrame=4spreads the 5×5 first-frame load over ~7 frames (~116ms) to avoid GPU OOM. - ✓ SHIPPED — A.2 — Frustum culling. Per-landblock AABB test (Gribb-Hartmann plane extraction + positive-vertex AABB test) in both
TerrainRenderer.DrawandStaticMeshRenderer.Draw. Per-entity culling deferred. LOD deferred to Phase C. Performance overlay in window title shows FPS, frame time, visible/total landblock ratio, entity count, animated count. ~160fps uncapped at 5×5 radius. - ✓ SHIPPED — A.3 — Background net receive thread. Dedicated daemon thread continuously pulls raw UDP datagrams from the kernel buffer into a
Channel<byte[]>. Render thread'sTick()drains the channel. All decode, fragment assembly, ISAAC crypto, event dispatch, and ack-sending remain on the render thread — minimal change that prevents packet drops during frame stalls. Thread starts afterEnterWorld()completes;PumpOnce()during handshake still reads the socket directly. - A.4 — Async dat decoding. Folded into the streaming worker — it's the worker's read path, not a separate subsystem. Called out here because regressions in dat caching could land on this surface.
Acceptance:
- Walk across 10+ landblocks in any direction, no crashes, no empty voids.
- Landblock-boundary crossings produce no visible hitch.
- Runtime window radius toggleable via environment variable.
Detailed spec: docs/superpowers/specs/2026-04-11-foundation-phase-design.md
Phase B — Gameplay / interaction
Goal: actually play the game — walk the character on the server, click NPCs, pick up items, chat, basic combat loop.
Sub-pieces:
- ✓ SHIPPED — B.1 — Outbound ack pump. Shipped as Phase 4.9 — per-packet ACK_SEQUENCE, not periodic. Server no longer drops idle clients.
- ✓ SHIPPED — B.2 — Player movement mode. Tab-toggled WASD ground walking with collision-resolved outdoor terrain, walk/run/idle/turn-right animations, third-person chase camera, outbound MoveToState (0xF61C) + AutonomousPosition (0xF753) server messages, portal entry works. Outdoor→indoor transition disabled for MVP (CellSurface floor polygons too aggressive without portal-based detection). Minor polish remaining: strafe animation, turn-left animation. Spec:
docs/superpowers/specs/2026-04-12-player-movement-design.md. - ✓ SHIPPED — B.3 — Physics collision engine. TerrainSurface (heightmap bilinear Z), CellSurface (indoor floor polygon projection via barycentric interpolation), PhysicsEngine (top-level resolver with step-height enforcement, outdoor↔indoor cell transitions, gravity reporting). Populated from streaming pipeline. 16 unit tests with fake data. Spec:
docs/superpowers/specs/2026-04-12-physics-collision-engine-design.md. - B.4 —
Use/UseWithTarget/PickUp. Outbound interaction messages. Drives opening doors, looting, talking to vendors. - B.5 — Chat.
SendTell,SendChatoutbound + receive/display inbound (display side depends on Phase D.1).
References:
references/ACE/Source/ACE.Server/Network/Handlers/MovementHandler.csreferences/ACE/Source/ACE.Server/Network/Handlers/UseObjectHandler.csreferences/holtburger/src/session/send.rsfor outbound packet-building patterns
Acceptance: walk on-server with your character, open a door, talk to an NPC, send a chat message and see the echo.
Phase C — Polish / visuals
Goal: close the visible gaps that make the world read as "old / broken" compared to retail.
Sub-pieces:
- C.1 — VFX / particle system.
PhysicsScriptparser, per-entityParticleEmitterstate, billboarded-quad particle renderer that lives in the Phase 9.1/9.2 translucent pass. Delivers portal swirls, chimney smoke, and fireplace flames in one implementation. - C.2 — Dynamic point lights. Fireplaces and lamps need local lighting; small upgrade to the mesh shader to accumulate N (e.g., 4) nearest point lights per draw. Uniform-buffer or UBO-friendly layout.
- C.3 — Palette range tuning. Small per-range offset/length tweaks to match retail skin/hair/eye colors. Mostly diff and verify work, no architecture change.
- C.4 — Double-sided translucent polys. Edge case left by Phase 9.2: neg-side translucent polys are culled because cull is always BACK. Fix by tracking per-sub-mesh
CullModeand flipping GL state per draw (or drawing twice with opposite cull). Minor. - C.5 — Shadow mapping (optional). Deferred unless it becomes a bottleneck in screenshots — dynamic shadows are a known complexity trap.
References:
references/ACE/Source/ACE.DatLoader/FileTypes/PhysicsScript.csfor the emitter schemareferences/ACViewer/ACViewer/Physics/Particles/for the visual modelreferences/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ParticleBatcher.csfor the Silk.NET-flavored implementation
Acceptance: portals look like swirly gates, chimneys smoke, fireplaces burn, character skin matches retail screenshots.
Phase D — UI / HUD + Sound
Goal: chat window, nameplates, inventory, and audio. Can run concurrently with Phase B or C because it doesn't touch gameplay/net/rendering surfaces.
Sub-pieces:
- D.1 — 2D ortho overlay + font rendering. ✅ SHIPPED 2026-04-17 as the dev-facing debug overlay (StbTrueTypeSharp system-font atlas +
TextRenderer+DebugOverlay). - D.2 — Retail UI framework + first panels. Research + scaffold landed 2026-04-17 (see
docs/research/retail-ui/). Ships:UiRoot+UiElement+UiPanel+UiHostwith retail-faithful event codes (0x01click,0x15drag-begin,0x3Edrop,0x201WM_LBUTTONDOWN, tooltip delay ~1000ms, etc.)- Focus / modal / capture / drag-drop / hover state machine
WorldMouseFallThrough/WorldKeyFallThroughpreserves existing camera+player controls- First concrete panel (chat window) uses wire messages Phase 4.7 already parses
- D.3 — AcFont from portal.dat. Replace stb_truetype system font with retail
FontDBObjs (0x40000000..0x40000FFF) baked fromRenderSurfacesource sheets — see research slice 03 §4. Preserves retail visual identity. - D.4 — Dat sprites + 9-slice panel backgrounds. Load
RenderSurface(0x06xxxxxx) as GL textures; addDrawSpritetoUiRenderContext. Enables retail panel art. - D.5 — Core panels. Attributes (
chunk_00470000.c:FUN_0047ba70), Skills (same), Paperdoll (chunk_004A0000.c:FUN_004A5200), Inventory, Spellbook (chunk_004C0000.c), Fellowship, Allegiance. Each uses the port sketches in slice 05. - D.6 — HUD. Vital orbs (scissor-rect partial fill, dat sprites
0x060013B2), radar (0x06001388/0x06004CC1, 1.18× range factor), compass strip (scrolling U), target name plate, damage floaters, selection indicator. See slice 06. - D.7 — Cursor manager. OS + dat-sourced custom cursors (
FUN_0043c1c0GDI HCURSOR builder pattern from slice 03). - D.8 — Sound.
SoundTableparser,Sounddat decode, audio engine (OpenAL via Silk.NET.OpenAL), per-entity 3D positional audio, optional music.
Reference docs: docs/research/retail-ui/00-master-synthesis.md + slices 01-06. Every AC-specific behavior has a decompiled FUN_ / DAT_ citation.
Acceptance: chat messages display in a retail-style panel, health/stamina/mana orbs fill correctly, attributes panel shows player stats, inventory opens with drag-drop working, and sound plays on hit/footstep.
Phase E — Long-tail
Not detailed here; each gets its own brainstorm when it becomes relevant.
- Dungeon landblocks (
0xAAAA0000family) + teleport-on-door-click + server-side portal handling - Phase 7.2 multi-floor stair walking — cells reachable via portals the cell-walker doesn't cross
- Player character full rig (held weapons, spell effects, death/revive animation)
- Weather + day/night cycle
- Spellcasting pipeline
- Group/fellowship UI
Cross-cutting work tracked in parallel
- Test coverage. Each phase lands with unit + integration tests in
tests/. Current count: 98 Core + 96 Core.Net = 194. Keep the ratio as new phases land. - Memory files. Project state under
memory/project_phase_*_state.mdis updated when a phase ships.MEMORY.mdis the index. CLAUDE.mddiscipline. Check all four references (ACE, ACViewer, WorldBuilder, Chorizite) before committing to an approach. WorldBuilder is the closest stack match and should be checked first.
Explicitly out of scope
- Server emulation — we use ACE for server, never reimplement.
- Account creation — direct user to ACE tooling.
- Anti-cheat / GM tools / live-ops — irrelevant for personal use.
- Cross-platform support — Windows-only; the dat path assumptions depend on retail Windows install layout. Silk.NET is cross-platform but we don't promise.
- Custom game content — this is a client for existing AC data, not a toolchain.
"When will my specific complaint be fixed?" — quick lookup
| Observation | Phase |
|---|---|
| Drudge statue in wrong pose | 6.3 FIXED ✓ |
| Characters in T-pose / wrong idle | 6.1 FIXED ✓ |
| No breathing on NPCs | 6.4 + sentinel fix FIXED ✓ |
| Lifestone crystal has one side missing | 9.2 FIXED ✓ |
| Ground floor flickering with terrain | 7.1 FIXED ✓ |
| Houses missing second floors / walls | 7.1 FIXED ✓ (interior mesh landed) |
| Character clothing missing / wrong | 5 FIXED ✓ |
| Statue wrong color / wrong scale | 5 FIXED ✓ |
| Holtburg sign half-buried | 5 FIXED ✓ |
| Can't walk past the loaded 3×3 window | A.1 FIXED ✓ (5×5 default, ACDREAM_STREAM_RADIUS to tune) |
| Frame hitch crossing landblock boundary | Phase A.3 (synchronous loader for now; async returns when DatCollection is thread-safe) |
| Walking around doesn't move me on the server | Phase B (Gameplay) |
| Can't talk to NPCs | Phase B |
| Can't open a door | Phase B |
| Portals render as a rotating black disk | Phase C.1 (VFX) |
| Chimneys have no smoke | Phase C.1 |
| Houses have no fireplace fire | Phase C.1 |
| No fireplace / torch lighting | Phase C.2 |
| Skin/hair color slightly off | Phase C.3 |
| No chat window | Phase D.2 |
| No sound | Phase D.4 |
| Dungeons / foundry interior missing | Phase E |
If you see something not on this list, add it here and assign a phase.