# acdream β€” project instructions for Claude ## Goal Build **acdream**, a modern open-source C# .NET 10 Asheron's Call client. A faithful port of the retail AC client's behavior to modern C# + Silk.NET, with a plugin API the original never had. **The code is modern. The behavior is retail.** Every AC-specific algorithm is ported from the **named retail decomp** at `docs/research/named-retail/` β€” Sept 2013 EoR build PDB (18,366 named functions, 5,371 named struct/class types) + Binary Ninja pseudo-C with 99.6% function-name recovery + verbatim retail header struct definitions. The older Ghidra `FUN_xxx` chunks under `docs/research/decompiled/` (22,225 functions, 688K lines) remain a fallback for chunk-by-chunk address-range navigation. **Grep `named-retail/acclient_2013_pseudo_c.txt` by `class::method` BEFORE decompiling fresh.** The code around those algorithms is modern C# with clean architecture. The plugin API exposes game state through well-defined interfaces. **Architecture:** `docs/architecture/acdream-architecture.md` is the single source of truth for how the client is structured. All work must align with this document. When the architecture doc and reality diverge, update one or the other β€” never leave them out of sync. **WorldBuilder is acdream's rendering + dat-handling base, integrated as of Phase N.4 ship (2026-05-08).** WB's `ObjectMeshManager` is the production mesh pipeline; `WbMeshAdapter` is the seam; `WbDrawDispatcher` is the production draw path (default-on, see `WbFoundationFlag`). Before re-implementing any AC-specific rendering or dat-handling algorithm, **read `docs/architecture/worldbuilder-inventory.md` FIRST**. If WorldBuilder has it, port from WorldBuilder (or call into our fork via the adapter), not from retail decomp. WorldBuilder is MIT-licensed, verified to render the world correctly, and uses the same Silk.NET stack we target. Re-porting from retail decomp when WB already has a tested port is how subtle bugs (the scenery edge-vertex bug, the triangle-Z bug) keep slipping in. Retail decomp remains the oracle for network, physics, animation, movement, UI, plugin, audio, chat β€” see the inventory doc's πŸ”΄ list for the full scope of "we still write this ourselves". **WB integration cribs:** - `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs` β€” single seam over WB's `ObjectMeshManager`. Owns the WB pipeline, drains its staged-upload queue per frame via `Tick()`, populates `AcSurfaceMetadataTable` with per-batch translucency / luminosity / fog metadata. - `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` β€” production draw path. Groups all visible (entity, batch) pairs, single-uploads the matrix buffer, fires one `glDrawElementsInstancedBaseVertexBaseInstance` per group with `BaseInstance` pointing at the slice. Per-entity frustum cull, opaque front-to-back sort, palette-hash memoization. - `src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs` / `EntitySpawnAdapter.cs` β€” bridge spawn lifecycle to WB ref-counts. Atlas tier (procedural) goes via Landblock; per-instance tier (server-spawned, palette/texture overrides) goes via Entity. - **Modern path is mandatory as of N.5 ship amendment (2026-05-08).** `WbFoundationFlag`, `InstancedMeshRenderer`, and `StaticMeshRenderer` are deleted. Missing `GL_ARB_bindless_texture` or `GL_ARB_shader_draw_parameters` throws `NotSupportedException` at startup. There is no legacy fallback. - **WB's modern rendering path** (GL 4.3 + bindless) packs every mesh into a single global VAO/VBO/IBO. Each batch references its slice via `FirstIndex` (offset into IBO) + `BaseVertex` (offset into VBO). Honor those offsets when issuing draws β€” `DrawElementsInstanced` with `indices=0` will draw every entity's first triangle from the global mesh, not the per-batch range. (This is exactly the exploded-character bug we hit during Task 26.) - **WB's `ObjectRenderBatch.SurfaceId` is unset** β€” the actual surface id lives in `batch.Key.SurfaceId` (the `TextureKey` struct). - **`ObjectMeshManager.IncrementRefCount` only bumps a counter** β€” it does NOT trigger mesh loading. You must explicitly call `PrepareMeshDataAsync(id, isSetup)` to fire the background decode. Result auto-enqueues to `_stagedMeshData` which `Tick()` drains. `WbMeshAdapter` does this for you on first registration. - **N.5 modern dispatch** (`docs/superpowers/specs/2026-05-08-phase-n5-modern-rendering-design.md`) uses bindless textures + multi-draw indirect on top of N.4's grouped pipeline. Per frame: three SSBO uploads (`_instanceSsbo` mat4 per instance @ binding=0; `_batchSsbo` `(uvec2 textureHandle, uint layer, uint flags)` per group @ binding=1; `_indirectBuffer` `DrawElementsIndirectCommand[]` opaque-section + transparent-section). Two `glMultiDrawElementsIndirect` calls per frame, one per pass. Total ~12-15 GL calls per frame for entity rendering regardless of scene complexity. - **`TextureCache` requires `BindlessSupport`** for the WB modern path. Three `Bindless`-suffixed `GetOrUpload*` methods return 64-bit handles made resident at upload time, backed by parallel Texture2DArray uploads (`UploadRgba8AsLayer1Array`). The legacy `uint`-returning methods stay for Sky / Terrain / Debug / particle paths that still sample via `sampler2D`. After N.6 retires legacy renderers, the legacy upload path + caches can be deleted. - **Translucency model is two-pass alpha-test** (matches WB), not per-blend-mode subpasses. Opaque pass discards `Ξ±<0.95`; transparent pass discards `Ξ±β‰₯0.95` AND `Ξ±<0.05`. Native `Additive` blend renders as alpha-blend on GfxObj surfaces β€” falsifiable; if a magic-content regression shows up, add a third indirect call with `glBlendFunc(SrcAlpha, One)` per spec Β§6 fallback (~30 min change). - **Per-instance highlight (selection blink) is reserved β€” open backlog, no scheduled phase.** `mesh_modern.vert`'s `InstanceData` struct has a documented hook for `vec4 highlightColor`. Whoever eventually picks it up finds the hook there; the change is localized: extend `InstanceData` stride 64β†’80 bytes, add the field, mix into fragment color in `mesh_modern.frag`. ~30 min when the time comes. - `src/AcDream.App/Rendering/TerrainModernRenderer.cs` β€” terrain dispatcher on N.5's modern primitives. Mirrors WB's `TerrainRenderManager` pattern (single global VBO/EBO + slot allocator + `glMultiDrawElementsIndirect`) but driven by acdream's `LandblockMesh.Build` so retail's `FSplitNESW` formula is preserved (issue #51 resolved). Atlas handles bound via the uvec2 + `sampler2DArray(handle)` constructor pattern (NOT the direct `uniform sampler2DArray` + `glProgramUniformHandleARB` form, which GL_INVALID_OPERATIONs on at least one driver). **Execution phases:** R1β†’R8 in the architecture doc. Each phase has clear goals, test criteria, and builds on the previous. Don't skip phases. The codebase is organized by layer (see architecture doc). Current phase state lives in memory (`memory/project_*.md`), plans in `docs/plans/`, research in `docs/research/`. **UI strategy:** three-layer split β€” swappable backend (ImGui.NET + `Silk.NET.OpenGL.Extensions.ImGui` for Phase D.2a, custom retail-look toolkit for D.2b later) / stable `AcDream.UI.Abstractions` layer (ViewModels + Commands + `IPanel` / `IPanelRenderer`) / unchanged game state. **As of Phase I (2026-04-25), ImGui hosts every dev/debug panel** β€” Vitals, Chat, Debug. The previous custom-StbTrueTypeSharp `DebugOverlay` was deleted in I.2; `TextRenderer` + `BitmapFont` are kept alive specifically for the future world-space HUD (D.6 β€” damage floaters, name plates) where ImGui can't reach into the 3D scene. D.2b remains the long-term retail-look path (panels reskinned one at a time using dat assets); ImGui persists forever as the `ACDREAM_DEVTOOLS=1` overlay. **All plugin-facing UI targets `AcDream.UI.Abstractions` β€” never import a backend namespace from a panel.** Full design: `docs/plans/2026-04-24-ui-framework.md`. Memory cribs: `memory/project_ui_architecture.md` (architecture), `memory/project_chat_pipeline.md` (chat pipeline as of Phase I), `memory/project_input_pipeline.md` (input pipeline as of Phase K). **Input pipeline:** `src/AcDream.UI.Abstractions/Input/` (action enum, `KeyChord`, `KeyBindings`, multicast `InputDispatcher` with scope stack + modal capture for rebind UX) + `src/AcDream.App/Input/` (Silk.NET adapters). Retail-default keymap loaded from `%LOCALAPPDATA%\acdream\keybinds.json` at startup (falls back to `KeyBindings.RetailDefaults()` matching `docs/research/named-retail/retail-default.keymap.txt`). The Settings panel (F11 / View β†’ Settings) lets users remap any action via click-to-rebind. As of Phase K (2026-04-26), ALL keyboard / mouse input flows through the dispatcher β€” no IsKeyPressed polling outside the per-frame movement queries. ## How to operate **You are the lead engineer AND architect on this project at all times.** You own the architecture (`docs/architecture/acdream-architecture.md`), the execution plan (phases R1–R8), the development workflow, and all technical decisions. Stop as little as possible. Drive work autonomously and continuously through full phases and across commit boundaries. Do not stop mid-phase for routine progress check-ins, permission asks on low-stakes design calls, or "should I continue?" confirmations. The user has repeatedly authorized direct-to-main commits, multi-commit sessions, and cross-phase jumps when the work is sequenced in the roadmap. The only thing that genuinely requires stopping is **visual confirmation** β€” the user needs to look at the running client and tell you whether it matches retail. Everything else is your call. **Only stop and wait for the user when:** - Visual verification is the acceptance test ("does the drudge look right now?") - The roadmap and the observed bug disagree and you need to brainstorm a new phase or sub-step (use `superpowers:brainstorming`, not a freeform chat) - A genuinely destructive or hard-to-reverse action is on the table outside the normal commit workflow (force push, history rewrite, deleting memory files, reverting multiple commits) - Memory or committed history shows a clear user preference you're about to diverge from **Things you should just do without asking:** - Continue to the next planned sub-step of a phase after the previous one lands clean β€” including immediately starting work on the next phase if the current one is done - Pick between two roughly equivalent implementations; justify the choice in the commit message - Refactor small amounts of surrounding code when genuinely needed to land a change cleanly (but not "while I'm here" scope creep) - Run the test suite, build the project, commit to main with co-author attribution - Add diagnostic logging when you need evidence, then strip it when the evidence is in hand - Spawn subagents for bounded implementation chunks (see Subagent policy) Before claiming a phase or sub-step is done: run `dotnet build` and `dotnet test` green, commit with a message that explains the "why", update memory if there's a durable lesson, update the roadmap's "shipped" table if a phase just landed, and move to the next todo item. **If you catch yourself about to ask "should I continue?", the answer is always yes β€” keep going.** The single exception is visual verification; otherwise, act. ## Communication style The user is a strong systems / C# / network programmer but **less practiced at 3D math, physics, graphics, and animation**. They want to learn β€” they're not asking for dumbed-down content, but for explanations that build understanding alongside the work. When discussing 3D / physics / graphics / animation / dat-format / protocol-internals topics: - **Name the concept in plain language first**, then introduce the term of art. "The angle of a slope (we call its straight-up component `Normal.Z`)" rather than dropping `normal.Z = cos ΞΈ` with no anchor. - **Give units**: degrees, meters, cm β€” NOT raw floats. "FloorZ β‰ˆ 0.66 means slopes up to about 49Β° are walkable" rather than "FloorZ = 0.66417414f". Floats are for the code; English is for the conversation. - **Use analogies for spatial concepts** when they fit. A BSP tree is "a way of slicing space into nested rooms"; a contact plane is "the imaginary floor under the player's feet"; a sphere sweep is "rolling a ball forward through space and stopping it on contact"; a cross product is "the direction perpendicular to two arrows"; a dot product is "how aligned two arrows are (1 = same, 0 = perpendicular, -1 = opposite)". - **Don't pile on multiple new concepts in one paragraph.** If a problem touches step-up AND step-down AND edge-slide AND walkable-polygon tracking, walk through them one at a time, each with what it does and why it exists. - **Show the math when it matters, but explain it.** Don't just drop a formula and move on; tag it with "what this means geometrically". - **Use frame-by-frame walk-throughs** for control-flow-heavy physics: "frame N: player here, lands. Frame N+1: state checks…" beats a function-call trace for understanding what's happening in motion. - **Flag terms of art** the first time they appear in a session, even if they're sprinkled through code comments. "Broadphase", "BSP", "step-up", "ContactPlane", "ValidateWalkable" β€” they earn their meaning the first time you spell it out. The goal is collaborative learning. Don't simplify the content; just make sure every term and number is grounded so the user can keep up and build intuition over time. ## Development workflow: grep named β†’ decompile β†’ verify β†’ port **This is the mandatory workflow for implementing ANY AC-specific behavior.** The triangle-boundary Z bug cost 5 failed fix attempts from guessing. The animation frame-swap bug cost 4 failed attempts. Every time we checked the decompiled code first, we got it right on the first try. **Now we have named retail symbols too β€” Step 0 cuts most lookups from 30 minutes to 5 seconds. And as of 2026-04-30, when "what does retail actually DO at runtime?" is the question and decomp alone isn't enough, attach cdb to a live retail client (Step -1).** ### For each new feature or bug fix: -1. **ATTACH cdb TO RETAIL (when behavior is the question, not code).** For "what does retail actually DO frame-by-frame?" questions β€” wedges, weird animation flicker, geometry-specific bugs, anything where the decomp is correct but it's not clear how it produces the visible behavior β€” **don't guess; attach the Windows debugger to a live retail client and trace it.** See "Retail debugger toolchain" below for setup. We discovered the steep-roof wedge had a 30Hz physics-tick cause this way; would have taken weeks of guessing without the trace. 0. **GREP NAMED FIRST.** Before any decompilation work, search `docs/research/named-retail/acclient_2013_pseudo_c.txt` by `class::method` name. 99.6% of functions have real names from the Sept 2013 EoR build PDB. `docs/research/named-retail/acclient.h` has every retail struct verbatim. `docs/research/named-retail/symbols.json` is greppable by name or address (regenerate via `py tools/pdb-extract/pdb_extract.py refs/acclient.pdb`). Only fall back to Step 1 below if the named pseudo-C lacks a function (rare β€” covers only the obfuscated/packed minority). 1. **DECOMPILE FIRST (fallback).** Only when grep-named-first returned nothing. Find the matching function in the older Ghidra chunks at `docs/research/decompiled/` or decompile a new region using `tools/decompile_acclient.py`. Use the function map at `docs/research/acclient_function_map.md` (cross-port index) + `docs/research/named-retail/symbols.json` (raw PDB names) to find known functions. If the function isn't mapped yet, search by characteristic constants (motion commands, magic numbers, string literals). 2. **CROSS-REFERENCE.** Check the decompiled code against ACE's C# port (`references/ACE/Source/ACE.Server/Physics/`) and ACME's `ClientReference.cs`. The decompiled code is ground truth; ACE and ACME are interpretation aids. If they disagree, the decompiled code wins. 3. **WRITE PSEUDOCODE.** Translate the decompiled C into readable pseudocode before porting to C#. Save it in `docs/research/*_pseudocode.md` for future reference. This step catches misinterpretations before they become bugs. 4. **PORT FAITHFULLY.** Translate the pseudocode to C# line-by-line. Use the same variable names, the same control flow, the same boundary conditions. Do not "improve" or "simplify" the algorithm β€” the retail client's code works; our job is to match it. 5. **CONFORMANCE TEST.** Write tests that verify our port matches the decompiled behavior. Use golden values from the decompiled code or from ACME's conformance tests. If the function touches terrain, port the 4M-cell sweep from `TerrainConformanceTests.cs`. 6. **INTEGRATE SURGICALLY.** When wiring ported code into the renderer or game loop, change the MINIMUM necessary. Keep existing working transform pipelines, only replace the specific computation. The animation sequencer integration proved this: replacing the slerp source was safe; replacing the entire transform composition broke everything. ### What NOT to do: - **Do not guess** at AC-specific algorithms, formulas, constants, wire formats, or coordinate conventions. Ever. **The named retail decomp has the answer for almost everything; guessing is no longer a recoverable error, it's negligence.** If you can't find it in `docs/research/named-retail/`, file a research note and ASK before writing. - **Do not "fix" the decompiled code.** If the retail client does something that looks wrong, it's probably right. Verify before changing. - **Do not skip the pseudocode step.** The frame-swap bug was caused by misreading the decompiled C directly into C# without an intermediate translation. - **Do not integrate via subagent** unless the subagent has the full context of the existing code it's modifying. The first animation sequencer integration was done by a subagent that didn't understand the transform pipeline β€” it broke everything. ### Phase completion checklist: Before marking any phase as done: - [ ] Every AC-specific algorithm has a decompiled reference cited in comments (named symbol + address from `named-retail/symbols.json`, OR function address + chunk file from older `decompiled/` chunks) - [ ] Conformance tests exist for the critical paths - [ ] The code was cross-referenced against at least 2 reference repos - [ ] `dotnet build` green, `dotnet test` green - [ ] Visual verification by the user (if applicable) - [ ] Roadmap updated - [ ] Memory updated if there's a durable lesson ## Retail debugger toolchain (live runtime trace) **When the question is "what does retail actually DO frame-by-frame?"** the decomp alone is often not enough β€” code paths interact with state (LastKnownContactPlane, transient flags, accumulated counters) in ways that aren't obvious from reading. As of 2026-04-30 we have a working toolchain to attach Windows' console debugger (cdb.exe) to a live retail acclient.exe with full PDB symbols and capture state at any breakpoint. **Use this when guessing has failed twice in a row.** ### What we have - **Matching binary**: `C:\Turbine\Asheron's Call\acclient.exe` v11.4186 (linker timestamp `2013-09-06 00:17:42 UTC`, CodeView GUID `9e847e2f-777c-4bd9-886c-22256bb87f32`). Pairs exactly with our `refs/acclient.pdb`. - **Debugger**: `cdb.exe` (console WinDbg) at `C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe`. Install via Microsoft Store WinDbg (~50 MB). 32-bit version is required for acclient.exe. - **PDB**: `refs/acclient.pdb` (29 MB, Sept 2013 EoR build). 18,366 named functions + 5,371 named struct types resolve. - **Symbol verifier**: `tools/pdb-extract/check_exe_pdb.py ` reads any acclient.exe and prints whether it pairs with our PDB (`MATCH` / `MISMATCH (expected GUID = ...)`). Always run this on a candidate binary BEFORE attaching. - **PDB metadata dumper**: `tools/pdb-extract/dump_pdb_info.py refs/acclient.pdb` prints the PDB's expected timestamp + GUID + age. Use to figure out which build to look for if the chain ever breaks. ### Workflow 1. **Verify the binary matches the PDB:** ```bash py tools/pdb-extract/check_exe_pdb.py "C:/Turbine/Asheron's Call/acclient.exe" ``` Expect: `=== MATCH: this exe pairs with our acclient.pdb ===` 2. **Have the user launch retail client** and connect to local ACE. Retail must already be in-world before attaching. 3. **Write a `.cdb` script** that arms breakpoints with non-blocking actions (count + log + `gc`). Pattern: ``` .logopen .sympath C:\Users\erikn\source\repos\acdream\refs .symopt+ 0x40 .reload /f acclient.exe r $t0 = 0 bp acclient!CTransition::transitional_insert "r $t0 = @$t0 + 1; .if (@$t0 % 5000 == 0) { .printf \"...\" }; .if (@$t0 >= 30000) { qd } .else { gc }" bp acclient!OBJECTINFO::kill_velocity "r $t1 = @$t1 + 1; gc" ... g ``` `gc` = "go conditional" (continue without breaking). Auto-detach via `qd` after a hit-count threshold to avoid manual cleanup. 4. **Launch cdb in the background** via a PowerShell wrapper: ```powershell & "C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe" ` -pn acclient.exe -cf