Final cross-cutting review of N.5 found that Task 15's deletion of mesh_instanced.vert/.frag left InstancedMeshRenderer orphaned — ACDREAM_USE_WB_FOUNDATION=0 silently rendered terrain+sky only with no entities. The SHIP commit's "[x] ACDREAM_USE_WB_FOUNDATION=0 still works" claim was inaccurate. Resolution: formal retirement of the legacy renderer path within N.5 instead of deferring to N.6. Deleted: - src/AcDream.App/Rendering/InstancedMeshRenderer.cs - src/AcDream.App/Rendering/StaticMeshRenderer.cs - src/AcDream.App/Rendering/Wb/WbFoundationFlag.cs GameWindow simplified — capability detection is unconditional, missing bindless throws NotSupportedException with a clear message at startup. WbDrawDispatcher + mesh_modern shader load are mandatory after init. No escape hatch. GpuWorldState simplified — WbFoundationFlag.IsEnabled guards on AddLandblock/RemoveLandblock removed; adapter calls are unconditional when the adapter is non-null. PendingSpawnIntegrationTests updated — WbFoundationFlag.ForTestsOnly_ForceEnable static ctor removed (flag is gone; adapter calls are unconditional). The ApplyLoadedTerrain physics-data loop was also simplified: the EnsureUploaded sub-loop that fed InstancedMeshRenderer is gone; _pendingCellMeshes is now explicitly cleared to prevent unbounded accumulation (the worker thread still populates it, but WB handles EnvCell geometry through its own pipeline). Spec §2 Decision 5 + §10 Out-of-Scope updated. Plan ship-amendment section added. Roadmap updated (N.5 ships with retirement; N.6 scope narrowed to perf-only). CLAUDE.md "WB integration cribs" updated. Perf baseline doc updated. WbDrawDispatcher class summary docstring corrected to describe the as-shipped SSBO + multi-draw-indirect path. ISSUES.md #51 updated (terrain not in N.5 scope; deferred to N.7). Bindless support is now a hard requirement. Modern desktop GPUs universally expose GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters; if a user hits the NotSupportedException, that's a real bug report worth investigating, not a silent fallback. Build: 0 errors, 0 warnings. Tests: 71/71 (Wb+MatrixComposition+TextureCacheBindless filter). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
843 lines
45 KiB
Markdown
843 lines
45 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, 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.** `mesh_modern.vert`'s
|
||
`InstanceData` struct has a documented hook for `vec4 highlightColor`
|
||
— Phase B.4 follow-up adds the field + plumbs server-side selection
|
||
state. Stride grows from 64 → 80 bytes when added; shader updates
|
||
trivially (read the field from `Instances[instanceIndex]` + mix into
|
||
fragment color).
|
||
|
||
**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.
|
||
|
||
**Currently in flight: Phase N.6 — Perf polish.**
|
||
Roadmap entry at [`docs/plans/2026-04-11-roadmap.md`](docs/plans/2026-04-11-roadmap.md).
|
||
Builds on N.5. Legacy renderers (`InstancedMeshRenderer`, `StaticMeshRenderer`,
|
||
`WbFoundationFlag`) were retired in the N.5 ship amendment — N.6 scope is
|
||
perf-only: WB atlas adoption, persistent-mapped buffers, GPU-side culling,
|
||
GL_TIME_ELAPSED query double-buffering, direct N.4 vs N.5 perf measurement,
|
||
legacy `Texture2D`/`sampler2D` TextureCache path retirement (Sky/Terrain/Debug).
|
||
Plan + spec written when work begins.
|
||
|
||
**Phase N.5 (Modern Rendering Path) shipped + amended 2026-05-08.** `WbDrawDispatcher`
|
||
on bindless textures + `glMultiDrawElementsIndirect`. CPU dispatcher 1.23ms/frame
|
||
at Holtburg (~810 fps). **Ship amendment:** `InstancedMeshRenderer`,
|
||
`StaticMeshRenderer`, `WbFoundationFlag` deleted in same phase — modern path is
|
||
mandatory; missing bindless throws at startup. Plan archived at
|
||
[`docs/superpowers/plans/2026-05-08-phase-n5-modern-rendering.md`](docs/superpowers/plans/2026-05-08-phase-n5-modern-rendering.md).
|
||
|
||
**Phase N.4 (Rendering Pipeline Foundation) shipped 2026-05-08.** WB's
|
||
`ObjectMeshManager` is integrated and is the production rendering path
|
||
(mandatory as of N.5 ship amendment). Plan archived at
|
||
[`docs/superpowers/plans/2026-05-08-phase-n4-rendering-foundation.md`](docs/superpowers/plans/2026-05-08-phase-n4-rendering-foundation.md).
|
||
|
||
**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).
|