Saves the comprehensive inventory of what WorldBuilder provides
(terrain, scenery, static objects, EnvCells, portals, sky, particles,
texture decode, mesh extraction, visibility) vs what acdream still
ports from retail decomp (network, physics, animation, movement, UI,
plugin, audio, chat).
This is the load-bearing reference for the strategic shift from
"port retail algorithms ourselves" to "rely on WorldBuilder for
rendering + dat-handling, port only what WB doesn't cover."
Updates CLAUDE.md:
- Adds top-level instruction: read the inventory FIRST before
re-porting anything in the 🟢 list
- Reframes references/WorldBuilder/ as acdream's rendering BASE,
not just a reference repo
- Updates the "Reference hierarchy by domain" table to point
rendering/dat questions at WorldBuilder, with retail decomp as
cross-check
Subsequent commits will fork WorldBuilder and replace our terrain/
scenery/object rendering with calls into the fork.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
757 lines
39 KiB
Markdown
757 lines
39 KiB
Markdown
# 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** as of
|
||
2026-05-08. 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 once wired up), 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".
|
||
|
||
**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 <exe>`
|
||
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 <output-path>
|
||
.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 <script>.cdb *>&1 |
|
||
Tee-Object -FilePath <log>
|
||
```
|
||
|
||
5. **User reproduces the scenario** in the retail client window
|
||
(jump on roof, hit wall, etc.). Breakpoints fire, log fills.
|
||
|
||
6. **cdb auto-detaches** when the threshold breakpoint fires `qd`.
|
||
Retail keeps running unaffected. Read the log offline.
|
||
|
||
### Known watchouts
|
||
|
||
- **PDB function names use snake_case for some classes**
|
||
(BSPTREE, CTransition, OBJECTINFO, COLLISIONINFO, SPHEREPATH) and
|
||
**PascalCase for others** (CPhysicsObj). The Binary Ninja decomp
|
||
shows snake_case for everything; the PDB has Turbine's actual
|
||
PascalCase for CPhysicsObj. Always look up symbols with `x` first
|
||
to find the actual name.
|
||
|
||
- **`bp acclient!Class::method`** sets a breakpoint by symbol. The
|
||
cdb command parser splits on `;`, so don't put `;` inside the
|
||
action string — use newlines or escape carefully.
|
||
|
||
- **Symbol path: do NOT use `.sympath srv*<server>;<local>`** — the
|
||
`;` is a cdb command separator, gets split. Use `.sympath <local>`
|
||
(no symbol server, just our refs/) since we don't need Microsoft
|
||
system DLL symbols.
|
||
|
||
- **Killing cdb kills the debuggee.** Use `qd` (quit detached) inside
|
||
a breakpoint action to detach cleanly. `Stop-Process cdb` will
|
||
take the retail client down with it.
|
||
|
||
- **High breakpoint hit rates produce game lag.** Each breakpoint hit
|
||
traps the process briefly. For frequent functions
|
||
(transitional_insert at ~10K/sec) the cumulative cost is enough to
|
||
make retail feel sluggish. Mitigate by setting a tight auto-detach
|
||
threshold (e.g., 30,000 hits) and/or moving counters to less-frequent
|
||
functions.
|
||
|
||
- **acclient.exe is 32-bit + uses thiscall.** When dumping struct
|
||
fields in breakpoint actions, `this` is in `ecx`. Use cdb's
|
||
`dt acclient!ClassName @ecx` for full struct dump.
|
||
|
||
### When NOT to use this
|
||
|
||
- **Pure code-port questions** — the decomp at `docs/research/named-retail/`
|
||
has the answer. Don't waste time on cdb if `grep` is enough.
|
||
- **Visual / rendering bugs** — debugger doesn't help with shaders or
|
||
framebuffers; use RenderDoc or similar.
|
||
- **Network protocol questions** — `holtburger` references + ACE source
|
||
+ Wireshark are the right tools, not cdb.
|
||
|
||
This toolchain was used to settle the L.5 steep-roof investigation:
|
||
30Hz physics tick (vs our 60Hz), `kill_velocity` gating,
|
||
`set_collide` rate per minute. See commit history around 2026-04-30
|
||
for the trace data and the decisions it drove.
|
||
|
||
## Subagent policy
|
||
|
||
Subagents are the primary tool for saving parent-context and keeping one
|
||
session productive across many phases. Use them liberally for:
|
||
|
||
- Bounded implementation chunks with a clear spec (one file, one test suite,
|
||
a targeted refactor)
|
||
- Parallel independent tasks with no shared state
|
||
- Research that would otherwise fill the parent context with file reads
|
||
|
||
**Model selection:**
|
||
|
||
- **Default: Sonnet.** Use Sonnet for all execution work — implementers,
|
||
research agents, spec-following work, test writing, refactors, repeated
|
||
patterns. Sonnet is the right cost/context/capability tradeoff for this
|
||
codebase and has been validated on every phase since Phase 2a. Do not
|
||
reach for Opus unless you have a specific reason.
|
||
- **Opus only for load-bearing quality review** — code review of a phase
|
||
boundary, a design that must be right the first time, a gnarly
|
||
cross-system refactor. "This feels hard" is not enough; specify why it
|
||
needs Opus in the task description.
|
||
- Never use Haiku for acdream work unless the task is literally checking
|
||
whether another process is alive.
|
||
|
||
**Prompt discipline:** when dispatching a subagent, include the relevant
|
||
spec path, the files it should read, the acceptance criteria (build + test
|
||
green), and the commit message style. Subagents inherit CLAUDE.md so they
|
||
follow the same rules.
|
||
|
||
## Roadmap discipline
|
||
|
||
acdream's plan lives in two files committed to the repo:
|
||
|
||
- **`docs/plans/2026-04-11-roadmap.md`** — the strategic roadmap. Single
|
||
source of truth for what's shipped, what's next, and the agreed order.
|
||
When you're about to pick up new work, read this first. When you ship a
|
||
phase or sub-step, move it from "ahead" to "shipped" in the same commit
|
||
that lands the work (or the very next commit).
|
||
|
||
- **`docs/superpowers/specs/*.md`** — per-phase detailed implementation
|
||
specs. Each active phase has one. When you're about to write code for a
|
||
named phase, read its spec, follow its component boundaries, and match its
|
||
acceptance criteria. Do not drift from the spec without explicit user
|
||
approval.
|
||
|
||
**Rules:**
|
||
|
||
1. Before starting a new phase or sub-piece, re-read the roadmap and the
|
||
relevant spec. State which phase you're on in the first action you take.
|
||
|
||
2. When reality and the plan diverge — the user observes a bug that doesn't
|
||
fit any existing phase, a technical discovery makes a phase description
|
||
wrong, a sub-piece turns out to be larger than expected — **pause and
|
||
brainstorm** with the `superpowers:brainstorming` skill before writing
|
||
code. Update the roadmap in the same session.
|
||
|
||
3. When shipping a phase, update the roadmap's "shipped" table and commit
|
||
the update in the same commit as (or immediately after) the
|
||
implementation commit.
|
||
|
||
4. Do not invent new phase numbers / letters on the fly. If you need a new
|
||
phase, add it to the roadmap first with the user, then reference it by
|
||
its assigned identifier. "Phase 11" and "Phase 9.3" conjured
|
||
mid-sentence are process smells — they mean the plan got out of sync
|
||
with the work.
|
||
|
||
5. If a single session ends up shipping work that spans multiple roadmap
|
||
phases, that's fine, but each commit message should name the phase it
|
||
belongs to (e.g. `feat(core): Phase A.1 — streaming region`).
|
||
|
||
The roadmap is not sacred — it changes. It IS the source of truth at any
|
||
given moment. When it's wrong, fix it. When it's right, follow it.
|
||
|
||
## Issue tracking — `docs/ISSUES.md`
|
||
|
||
Tactical rolling list of known bugs + small deferred features. Scope:
|
||
anything that fits in one-to-two commits; larger work stays as a Phase
|
||
in the roadmap. Maintenance rules:
|
||
|
||
1. **Start-of-session:** scan OPEN issues in `docs/ISSUES.md` — any of
|
||
them in the area you're about to touch? Plan accordingly.
|
||
2. **End-of-session:** if this session observed a defect and didn't fix
|
||
it inline, add an issue. If this session fixed an OPEN issue, move
|
||
it to **Recently closed** with the commit SHA.
|
||
3. **Commit messages:** reference issue IDs when a commit closes one
|
||
(`fix #3: periodic TimeSync parsing`). Makes `git log --grep='#3'`
|
||
return the full fix history.
|
||
4. **Promotion:** if an issue grows into multi-commit work, promote it
|
||
to a Phase in the roadmap. Close the issue as
|
||
`DONE (promoted to Phase X)` and reference the roadmap commit.
|
||
|
||
The roadmap is strategic (month-scale, phase-level). ISSUES is tactical
|
||
(week-scale, bug-level). They reference each other but don't duplicate.
|
||
|
||
## Running the client against the live server
|
||
|
||
The user runs a **local ACE (Asheron's Call Emulator) server on
|
||
`127.0.0.1:9000`** that stays up continuously. Iteration loop: launch the
|
||
acdream client, connect, test, close the window, rebuild, relaunch.
|
||
|
||
### Connection details
|
||
|
||
| Setting | Value |
|
||
|---|---|
|
||
| Host / port | `127.0.0.1` / `9000` |
|
||
| Account | `testaccount` |
|
||
| Password | `testpassword` |
|
||
| Character | `+Acdream` (server guid `0x5000000A`) — this is a `+` GM-marker character for dev testing |
|
||
| DAT directory | `%USERPROFILE%\Documents\Asheron's Call\` (contains `client_portal.dat`, `client_cell_1.dat`, `client_highres.dat`, `client_local_English.dat`) |
|
||
|
||
### Launch command
|
||
|
||
The canonical launch is via `dotnet run` with environment variables set.
|
||
Use PowerShell (Windows native) — bash struggles with the apostrophe in
|
||
`"Asheron's Call"`:
|
||
|
||
```powershell
|
||
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
|
||
$env:ACDREAM_LIVE = "1"
|
||
$env:ACDREAM_TEST_HOST = "127.0.0.1"
|
||
$env:ACDREAM_TEST_PORT = "9000"
|
||
$env:ACDREAM_TEST_USER = "testaccount"
|
||
$env:ACDREAM_TEST_PASS = "testpassword"
|
||
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug
|
||
```
|
||
|
||
Always pipe to a log file for post-run diagnostic grep:
|
||
`2>&1 | Tee-Object -FilePath "launch.log"`. Run in the background via
|
||
the `run_in_background: true` parameter so the tool doesn't block waiting
|
||
for the window to close.
|
||
|
||
### Logout-before-reconnect
|
||
|
||
**ACE keeps your last session alive briefly after a disconnect.** If you
|
||
relaunch the client within a few seconds of the last close, the handshake
|
||
fails with `live: session failed: CharacterList not received` and the
|
||
process exits with code 29. Wait ~3–5 seconds between launches, or explicitly
|
||
kill stale processes:
|
||
|
||
```powershell
|
||
Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force
|
||
Start-Sleep -Seconds 3
|
||
# ... then launch ...
|
||
```
|
||
|
||
The user has repeatedly confirmed this — don't treat exit-29-after-rapid-relaunch
|
||
as a code bug. It's a server-side session-cleanup delay.
|
||
|
||
### Test character
|
||
|
||
`+Acdream` at server guid `0x5000000A`. Starts at or near Holtburg. Has
|
||
basic stats; `ACDREAM_RUN_SKILL` / `ACDREAM_JUMP_SKILL` env vars (default
|
||
200) set the *client-side* skill value used by `PlayerWeenie.InqRunRate`
|
||
for local motion prediction. **These are NOT synced to the server** —
|
||
ACE's own character data is authoritative for broadcast motion. If you
|
||
see a speed/anim mismatch between local and observer views, the fix is
|
||
to sync the runSkill from ACE via `UpdateMotion.ForwardSpeed` echo (wired
|
||
via `PlayerMovementController.ApplyServerRunRate`) or from
|
||
`PlayerDescription (0x0013)`.
|
||
|
||
### Diagnostic env vars
|
||
|
||
- `ACDREAM_DUMP_MOTION=1` — dump every inbound `UpdateMotion` (guid,
|
||
stance, cmd, speed) + resulting `SetCycle` call. Massive for remote-
|
||
animation debugging.
|
||
- `ACDREAM_STREAM_RADIUS=N` — tune landblock visible-window radius
|
||
(default 2 = 5×5).
|
||
- `ACDREAM_NO_AUDIO=1` — suppress OpenAL init for headless / driver-
|
||
broken setups.
|
||
- `ACDREAM_REMOTE_VEL_DIAG=1` — dump per-tick / per-UM remote motion
|
||
diagnostics (`[UM_RAW]`, `[SCFAST]`, `[SCFULL]`, `[SETCYCLE]`,
|
||
`[FWD_WIRE]`, `[OMEGA_DIAG]`, `[SEQSTATE]`, `[PARTSDIAG]`,
|
||
`[VEL_DIAG]`, `[UPCYCLE]`). Heavy.
|
||
- *(retired 2026-05-05 by L.3 M2/M3)* `ACDREAM_INTERP_MANAGER` was an
|
||
env-var gate on an experimental per-tick remote motion path. L.3 M2
|
||
(commit 40d88b9) replaced both gates (`OnLivePositionUpdated` +
|
||
`TickAnimations`) with `IsPlayerGuid(...)` so player remotes use the
|
||
retail-faithful queue routing (InterpolationManager queue catch-up +
|
||
PositionManager combiner) unconditionally. NPCs and airborne player
|
||
remotes still flow through the legacy `apply_current_movement` +
|
||
`ResolveWithTransition` path. The env-var no longer toggles anything.
|
||
|
||
### Outbound motion wire format (acdream → ACE)
|
||
|
||
Important quirk for cross-checking observed remote behavior. acdream's
|
||
`PlayerMovementController` + `MoveToState` builder encode motion as:
|
||
|
||
| Local input | Wire `ForwardCommand` | Wire `HoldKey` | Wire `ForwardSpeed` |
|
||
|---|---|---|---|
|
||
| W (run) | `WalkForward` (0x05) | `Run` (2) | server runRate (~2.4–2.94) |
|
||
| W + Shift (walk) | `WalkForward` (0x05) | `None` (1) | 1.0 |
|
||
|
||
ACE auto-upgrades `WalkForward + HoldKey.Run` → `RunForward (0x07)`
|
||
when relaying to remote observers. So our INBOUND parser sees
|
||
`fwd=0x07` for "remote is running." This matches retail's encoding.
|
||
|
||
When the local player toggles Shift while keeping W held (Run↔Walk
|
||
demote/promote), acdream sends a fresh `MoveToState` with the new
|
||
HoldKey + ForwardSpeed. Retail's outbound likely does the same, but
|
||
ACE's behavior on relay is uncertain — see `#L.X` in ISSUES.md for
|
||
the open Run↔Walk cycle bug on observed retail-driven remotes.
|
||
|
||
### Visual verification workflow
|
||
|
||
1. Make a code change.
|
||
2. `dotnet build` — must be green before launch.
|
||
3. Launch (background). Give it ~8 s to reach in-world state.
|
||
4. User tests in the running client (moves, interacts, watches remote
|
||
chars). Close window when done.
|
||
5. Read `launch.log` or the task output file for diagnostic lines.
|
||
6. Iterate.
|
||
|
||
Never launch without first confirming the build is green. A failed launch
|
||
from a compile error wastes the user's testing time and can kill an
|
||
already-running ACE session via the handshake race.
|
||
|
||
### What the user watches for
|
||
|
||
- **Own character** in acdream: correct animation, correct speed, proper
|
||
transitions (walk → run, run → stop, jump → land, strafe, turn).
|
||
- **Remote toons from a retail client viewing acdream**: the user often
|
||
runs the retail AC client in parallel and watches the acdream `+Acdream`
|
||
character from there. When they report "lagging forward" or "walking
|
||
when I should be running" that's the **retail observer view**, not the
|
||
acdream view. Distinguish these carefully — they're different code paths.
|
||
- **NPCs / monsters in acdream**: animate their emotes, idle, attacks,
|
||
stop transitions, and keep their visual position tracked smoothly
|
||
between the 5–10 Hz UpdatePosition bursts (dead-reckoning).
|
||
|
||
## Reference repos: check ALL FOUR, not just one
|
||
|
||
When researching a protocol detail, dat format, rendering algorithm, or
|
||
any "how does AC do X" question, **check all four of the vendored
|
||
references in `references/`** before committing to an approach. Do not
|
||
settle on the first hit and move on — cross-reference at least two of
|
||
these, ideally all four:
|
||
|
||
- **`references/ACE/`** — ACEmulator server. Authority on the wire
|
||
protocol (packet framing, ISAAC, game message opcodes, serialization
|
||
order). The things a server has to know to parse and produce bytes.
|
||
- **`references/ACViewer/`** — MonoGame-based dat viewer that actually
|
||
renders characters + world. Authority on the client-side visual
|
||
pipeline: ObjDesc application, palette overlays, texture decoding
|
||
for the palette-indexed formats. See
|
||
`ACViewer/Render/TextureCache.cs::IndexToColor` for the canonical
|
||
subpalette overlay algorithm.
|
||
- **`references/WorldBuilder/`** — **acdream's rendering + dat-handling
|
||
BASE (not just a reference).** As of 2026-05-08 acdream is moving to
|
||
fork WorldBuilder upstream and depend on the fork for terrain,
|
||
scenery, static objects, EnvCells, portals, sky, particles, texture
|
||
decoding, mesh extraction, visibility/culling. WorldBuilder is
|
||
MIT-licensed, exact-stack match (Silk.NET + .NET), and verified to
|
||
render the world correctly. **Before re-porting any rendering or
|
||
dat-handling algorithm from retail decomp, check
|
||
`docs/architecture/worldbuilder-inventory.md` first.** If WB has it,
|
||
use WB's port. If WB doesn't have it (network, physics, animation,
|
||
movement, UI, plugin, audio, chat), port from retail decomp as
|
||
before.
|
||
- **`references/Chorizite.ACProtocol/`** — clean-room C# protocol
|
||
library generated from a protocol XML description. Useful sanity check
|
||
on field order, packed-dword conventions, type-prefix handling. The
|
||
generated Types/*.cs files have accurate field comments (e.g. "If
|
||
it is 0, it defaults to 256*8") that ACE's server-side code doesn't.
|
||
- **`references/holtburger/`** — **Almost-complete Rust TUI AC client.**
|
||
Not just a crate or a handshake reference: it's a full client that
|
||
logs in, plays the game, sends/receives chat, handles combat, and
|
||
renders state in a terminal. **This is acdream's most authoritative
|
||
reference for client-side behavior** — anything about how a client
|
||
is *supposed* to talk to the server lives here. Specifically:
|
||
- Handshake / login flow including all the post-EnterWorld
|
||
messages retail clients send (LoginComplete, ack pump,
|
||
DDDInterrogation responses, etc).
|
||
- The proper ACK_SEQUENCE pattern (every received packet with
|
||
sequence > 0 gets an ack queued back; not periodic).
|
||
- Outbound game-action message construction with sequence
|
||
numbering.
|
||
- Message routing and session lifecycle.
|
||
Look here FIRST when implementing anything in `WorldSession` or
|
||
the message-builder layer. ACE shows what the server expects;
|
||
holtburger shows what a real client actually sends.
|
||
|
||
- **`references/AC2D/`** — **C++ AC client emulator.** Oldest reference,
|
||
fixed-function OpenGL, but has the **real AC terrain split formula**
|
||
(`FSplitNESW` with constants `0x0CCAC033`, `0x421BE3BD`, `0x6C1AC587`,
|
||
`0x519B8F25`) which differs from WorldBuilder's physics-path formula.
|
||
Also has the complete `0xF61C` movement packet format with flag bits
|
||
and the `stMoveInfo` sequence counters. Key lesson from AC2D: it does
|
||
NOT do client-side terrain Z — it sends movement keys to the server
|
||
and uses the server's authoritative Z. See
|
||
`docs/research/2026-04-12-movement-deep-dive.md` for the full analysis.
|
||
|
||
Pattern: when you encounter an unknown behavior, grep all four for the
|
||
relevant term, read each hit, and compose a multi-source understanding
|
||
BEFORE writing acdream code. A single reference can be misleading; the
|
||
intersection of all four is almost always the truth. The user has
|
||
repeatedly had to remind me about this when I narrowly searched one ref
|
||
and missed obvious answers in another.
|
||
|
||
### Reference hierarchy by domain
|
||
|
||
**NEVER GUESS an algorithm, formula, constant, wire format, or coordinate
|
||
convention.** Every AC-specific behavior has a reference implementation in
|
||
one of the repos below. If you find yourself writing AC-specific code
|
||
without having read the matching reference first, STOP and read it. The
|
||
triangle-boundary Z bug cost 5 failed fix attempts because we guessed
|
||
instead of checking ACME's `ClientReference.cs` — which had the exact
|
||
decompiled client code and would have fixed it in minutes.
|
||
|
||
**The rule: read the reference FIRST, write code SECOND. Always.**
|
||
|
||
| Domain | Primary Oracle | Secondary | Notes |
|
||
|--------|---------------|-----------|-------|
|
||
| **Any AC-specific algorithm** | **`docs/research/named-retail/`** (PDB-named decomp + verbatim retail header structs from Sept 2013 EoR build) | the existing references below | The retail client itself, fully named. 18,366 functions + 5,371 struct types + 1.4 M lines of pseudo-C in one searchable tree. Beats every other reference for "what does the real client do." Use for everything in the 🔴 list (network, physics, animation, movement, UI, plugin, audio, chat). |
|
||
| **Terrain** (split direction, height sampling, palCode, vertex position, normals) | **WorldBuilder `TerrainGeometryGenerator.cs` + `TerrainUtils.cs`** | retail decomp for cross-check | WB is acdream's terrain base. ACME's port is older/SUPERSEDED by WB. |
|
||
| **Terrain blending** (texture atlas, alpha masks, road overlays) | **WorldBuilder `LandSurfaceManager.cs`** | ACME `LandSurfaceManager.cs` (same algo, less complete) | WB is acdream's blending base. |
|
||
| **Scenery** (procedural placement: trees, bushes, rocks, fences) | **WorldBuilder `SceneryRenderManager.cs` + `SceneryHelpers.cs`** | retail decomp `CLandBlock::get_land_scenes` | WB is acdream's scenery base. Re-porting from retail decomp is what caused the edge-vertex bug. |
|
||
| **GfxObj / Setup rendering** (mesh extraction, multi-part assembly, ObjDesc) | **WorldBuilder `StaticObjectRenderManager.cs` + `ObjectMeshManager.cs`** | ACME `StaticObjectManager.cs` (includes CreaturePalette, GfxObjRemapping, HiddenParts — useful for character appearance which WB doesn't cover) | WB for static objects, ACME for character appearance. |
|
||
| **Texture decoding** (INDEX16, P8, DXT, BGRA, alpha) | **WorldBuilder `TextureHelpers.cs`** | ACME `TextureHelpers.cs`; ACViewer's `IndexToColor` is canonical for subpalette overlay | WB is acdream's decode base. |
|
||
| **EnvCell / dungeon rendering** (cell geometry, portal visibility, collision mesh) | **WorldBuilder `EnvCellRenderManager.cs` + `PortalRenderManager.cs`** | ACME `EnvCellManager.cs` (more complete for collision); ACViewer `Physics/Common/EnvCell.cs` | WB is acdream's geometry base; ACME for collision until ported. |
|
||
| **Particles / sky** (particle systems, weather, sky particles) | **WorldBuilder `SkyboxRenderManager.cs` + `ParticleEmitterRenderer.cs` + `ParticleBatcher.cs`** | retail decomp | WB is acdream's particle base. |
|
||
| **Visibility / culling** (frustum, cell visibility) | **WorldBuilder `VisibilityManager.cs` + `Frustum.cs`** | — | WB. |
|
||
| **Network protocol** (wire format, packet framing, fragment assembly, ISAAC) | **holtburger** `crates/holtburger-session/` | AC2D `cNetwork.cpp` (simpler, good for cross-check) | ACE shows the server side; holtburger + AC2D show the client side. |
|
||
| **Client behavior** (what to send when, login flow, ack pattern, keepalive) | **holtburger** `crates/holtburger-core/src/client/` | AC2D `cNetwork.cpp` + `cInterface.cpp` | holtburger is the most complete; AC2D is simpler but confirmed working. |
|
||
| **Movement** (MoveToState format, AutonomousPosition, sequence counters, speed) | **holtburger** `client/movement/` | AC2D `cNetwork.cpp:2592-2664` (0xF61C format) | See `docs/research/2026-04-12-movement-deep-dive.md` for the full cross-reference. |
|
||
| **Server expectations** (what ACE accepts/rejects, validation thresholds) | **ACE** `Source/ACE.Server/Network/` | — | Only ACE knows what the server actually validates. |
|
||
| **Silk.NET / .NET 10 idioms** (GL calls, shader setup, VAO patterns) | **WorldBuilder original** | ACME (same stack) | Both use the same backend; original has cleaner isolated examples. |
|
||
| **Protocol field order** (packed dwords, type prefixes, flag enums) | **Chorizite.ACProtocol** `Types/*.cs` | holtburger (cross-check) | Generated from protocol XML; has accurate field comments. |
|
||
|
||
### ACME key files quick reference
|
||
|
||
These are the files you should open FIRST when working on any rendering
|
||
or dat-interpretation task:
|
||
|
||
- **`WorldBuilder.Tests/ClientReference.cs`** — decompiled retail AC
|
||
client C# port. `IsSWtoNECut`, `GetPalCode`, `GetVertexHeight`,
|
||
`GetVertexPosition`. **The ground truth.** If your code disagrees
|
||
with this file, your code is wrong.
|
||
- **`WorldBuilder.Tests/TerrainConformanceTests.cs`** — 4M+ cell sweep
|
||
proving ACME matches retail. Port these into acdream's test suite for
|
||
any algorithm you touch.
|
||
- **`StaticObjectManager.cs`** — GfxObj+Setup+CreaturePalette pipeline.
|
||
- **`EnvCellManager.cs`** — dungeon cells + portal visibility.
|
||
- **`TerrainGeometryGenerator.cs`** — `GetHeight()`, `GetNormal()`,
|
||
`CalculateSplitDirection()` matching the mesh index buffer.
|
||
- **`TextureHelpers.cs`** — INDEX16, BGRA, DXT decode helpers.
|
||
|
||
### holtburger key files quick reference
|
||
|
||
These are the files you should open FIRST when working on any networking
|
||
or client-behavior task:
|
||
|
||
- **`client/movement/system.rs`** — the movement state machine (when to
|
||
send MoveToState vs AutonomousPosition, deduplication logic).
|
||
- **`client/movement/actions.rs`** — MoveToState, AutonomousPosition,
|
||
Jump wire format builders.
|
||
- **`client/movement/types.rs`** — RawMotionState packed format with
|
||
all flag bits documented.
|
||
- **`session/send.rs`** — packet construction, checksum, ISAAC, ACK
|
||
piggybacking.
|
||
- **`client/messages.rs`** — post-login message handlers (PlayerCreate
|
||
→ LoginComplete, DddInterrogation → response, PlayerTeleport).
|
||
- **`spatial/physics.rs`** — dead-reckoning solver (how the client
|
||
advances position between server updates).
|