L.2g slice 1 is CODE-COMPLETE: parser + registry mutator + WorldSession dispatcher + GameWindow subscriber (4 commits:2459f28,d538915,536a608,108e386). Build clean, 6 new tests pass, baseline-stable across the full suite. Per-commit + final integration code reviews all approved. Visual verification deferred: while running the Holtburg-doorway test, Phase B.4's outbound Use handler turned out to be unwired. The wire builders (InteractRequests.BuildUse), classes (SelectionState, WorldPicker), input-action enums, and keybindings all exist — but GameWindow.OnInputAction has no case for SelectDblLeft, so the click silently does nothing. The inbound L.2g chain we just landed can't fire until something sends an outbound Use. This commit captures the handoff + reframes next-session work: * docs/research/2026-05-12-l2g-slice1-shipped-handoff.md (NEW) Full evidence: 4 shipped commits, end-to-end code flow, B.4 discovery explanation, 4 minor + 1 Important review notes (the Important one is a test-coverage gap that the B.4b visual test will settle automatically), reproducibility recipe, next-session pick. * CLAUDE.md "Currently in Phase L.2" paragraph: L.2g slice 1 code shipped; visual test deferred to B.4b. Next-phase-candidates list: L.2g slice 1 (now done) replaced with the B.4b candidate pointing at the slice scope. * docs/plans/2026-04-29-movement-collision-conformance.md L.2g section gains a "Current shipped slice (2026-05-12):" table listing the 4 commits. * docs/plans/2026-05-12-milestones.md M1 phase-list updated: L.2g slice 1 (code) shipped; B.4 renamed "B.4 / B.4b" with the gap-discovery note + B.4b shape. * docs/ISSUES.md New issue #57 (HIGH) for the B.4 interaction-handler gap. Promoted to Phase B.4b; will close as DONE (promoted to Phase B.4b) when B.4b's design spec lands. * Memory file project_interaction_pipeline.md (in personal memory dir, not in this commit) updated to reflect reality. Next session: Phase B.4b (~30-50 LOC, 1-2 subagent dispatches, ~30 min). Subscribe SelectDblLeft -> WorldPicker.Pick -> InteractRequests.BuildUse -> _liveSession.SendGameMessage. Same Holtburg-doorway visual test verifies both L.2g slice 1 and B.4b in one pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
60 KiB
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'sObjectMeshManager. Owns the WB pipeline, drains its staged-upload queue per frame viaTick(), populatesAcSurfaceMetadataTablewith 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 oneglDrawElementsInstancedBaseVertexBaseInstanceper group withBaseInstancepointing 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, andStaticMeshRendererare deleted. MissingGL_ARB_bindless_textureorGL_ARB_shader_draw_parametersthrowsNotSupportedExceptionat 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 —DrawElementsInstancedwithindices=0will 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.SurfaceIdis unset — the actual surface id lives inbatch.Key.SurfaceId(theTextureKeystruct). ObjectMeshManager.IncrementRefCountonly bumps a counter — it does NOT trigger mesh loading. You must explicitly callPrepareMeshDataAsync(id, isSetup)to fire the background decode. Result auto-enqueues to_stagedMeshDatawhichTick()drains.WbMeshAdapterdoes 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 (_instanceSsbomat4 per instance @ binding=0;_batchSsbo(uvec2 textureHandle, uint layer, uint flags)per group @ binding=1;_indirectBufferDrawElementsIndirectCommand[]opaque-section + transparent-section). TwoglMultiDrawElementsIndirectcalls per frame, one per pass. Total ~12-15 GL calls per frame for entity rendering regardless of scene complexity. TextureCacherequiresBindlessSupportfor the WB modern path. ThreeBindless-suffixedGetOrUpload*methods return 64-bit handles made resident at upload time, backed by parallel Texture2DArray uploads (UploadRgba8AsLayer1Array). The legacyuint-returning methods stay for Sky / Terrain / Debug / particle paths that still sample viasampler2D. 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.95ANDα<0.05. NativeAdditiveblend renders as alpha-blend on GfxObj surfaces — falsifiable; if a magic-content regression shows up, add a third indirect call withglBlendFunc(SrcAlpha, One)per spec §6 fallback (~30 min change). - Per-instance highlight (selection blink) is reserved — open
backlog, no scheduled phase.
mesh_modern.vert'sInstanceDatastruct has a documented hook forvec4 highlightColor. Whoever eventually picks it up finds the hook there; the change is localized: extendInstanceDatastride 64→80 bytes, add the field, mix into fragment color inmesh_modern.frag. ~30 min when the time comes. src/AcDream.App/Rendering/TerrainModernRenderer.cs— terrain dispatcher on N.5's modern primitives. Mirrors WB'sTerrainRenderManagerpattern (single global VBO/EBO + slot allocator +glMultiDrawElementsIndirect) but driven by acdream'sLandblockMesh.Buildso retail'sFSplitNESWformula is preserved (issue #51 resolved). Atlas handles bound via the uvec2 +sampler2DArray(handle)constructor pattern (NOT the directuniform sampler2DArray+glProgramUniformHandleARBform, which GL_INVALID_OPERATIONs on at least one driver).- Two-tier streaming architecture (Phase A.5, shipped 2026-05-10).
src/AcDream.App/Streaming/owns the full streaming pipeline. Key types:StreamingRegion(two-radius Chebyshev window: N₁=near, N₂=far; producesTwoTierDiffwith 5 transition lists per tick),StreamingController(render-thread coordinator: routesTwoTierDiffto the worker queue and drains completions up toMaxCompletionsPerFrameper frame),LandblockStreamer(single background worker thread:LoadFar= heightmap- mesh only,
LoadNear= heightmap +LandBlockInfo+ scenery + mesh,PromoteToNear=LandBlockInfo+ scenery only),GpuWorldState(render-thread entity state:AddEntitiesToExistingLandblockfor promotions,RemoveEntitiesFromLandblockfor demotions). Default: N₁=4 (81 near LBs, full detail), N₂=12 (544 far LBs, terrain only). Quality Preset system (QualitySettings.From(preset)) controls both radii and MSAA/anisotropic/A2C/completions-per-frame as a unit. Spec:docs/superpowers/specs/2026-05-09-phase-a5-two-tier-streaming-design.md.
- mesh only,
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_chat_pipeline.md (chat pipeline as of
Phase I), memory/project_input_pipeline.md (input pipeline as of
Phase K). UI architecture full design at
docs/plans/2026-04-24-ui-framework.md.
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
- The request is an investigation, audit, analysis, review, or "report-only"
— no edits, no writes, no diagnostic code drops until you've delivered the
report and the user explicitly approves a fix. Use the
/investigateskill (.claude/skills/investigate/SKILL.md) to enter this mode cleanly. - A referenced commit, file path, branch, or doc doesn't exist where the user said it would. Ask one short question; don't go hunting across branches or worktrees. A 1-line clarification beats 30 minutes of wrong-branch exploration.
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. You pick what comes next per the Milestone discipline section — never present the user a menu like "should we do X or Y?" or ask "what next?". Just choose and announce the choice in one sentence. Work-order selection is Claude's job, not the user's.
- 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 droppingnormal.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.
-
GREP NAMED FIRST. Before any decompilation work, search
docs/research/named-retail/acclient_2013_pseudo_c.txtbyclass::methodname. 99.6% of functions have real names from the Sept 2013 EoR build PDB.docs/research/named-retail/acclient.hhas every retail struct verbatim.docs/research/named-retail/symbols.jsonis greppable by name or address (regenerate viapy 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). -
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 usingtools/decompile_acclient.py. Use the function map atdocs/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). -
CROSS-REFERENCE. Check the decompiled code against ACE's C# port (
references/ACE/Source/ACE.Server/Physics/) and ACME'sClientReference.cs. The decompiled code is ground truth; ACE and ACME are interpretation aids. If they disagree, the decompiled code wins. -
WRITE PSEUDOCODE. Translate the decompiled C into readable pseudocode before porting to C#. Save it in
docs/research/*_pseudocode.mdfor future reference. This step catches misinterpretations before they become bugs. -
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.
-
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. -
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.
- Do not replace working retail-faithful logic with a modern redesign without explicit user approval. Two campaigns (267 min remote-entity prediction+rubber-band replacing hard-snap; speculative shader edits in the sky-fog work) had to be reverted in full because the redesign regressed behavior the original port had right. When you see "I could simplify this with X" on a retail-port, flag the tradeoff and ask before deleting the existing path. Retail-faithful first; "cleaner" second.
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 olderdecompiled/chunks) - Conformance tests exist for the critical paths
- The code was cross-referenced against at least 2 reference repos
dotnet buildgreen,dotnet testgreen- 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.exev11.4186 (linker timestamp2013-09-06 00:17:42 UTC, CodeView GUID9e847e2f-777c-4bd9-886c-22256bb87f32). Pairs exactly with ourrefs/acclient.pdb. - Debugger:
cdb.exe(console WinDbg) atC:\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.pdbprints the PDB's expected timestamp + GUID + age. Use to figure out which build to look for if the chain ever breaks.
Workflow
-
Verify the binary matches the PDB:
py tools/pdb-extract/check_exe_pdb.py "C:/Turbine/Asheron's Call/acclient.exe"Expect:
=== MATCH: this exe pairs with our acclient.pdb === -
Have the user launch retail client and connect to local ACE. Retail must already be in-world before attaching.
-
Write a
.cdbscript 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" ... ggc= "go conditional" (continue without breaking). Auto-detach viaqdafter a hit-count threshold to avoid manual cleanup. -
Launch cdb in the background via a PowerShell wrapper:
& "C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe" ` -pn acclient.exe -cf <script>.cdb *>&1 | Tee-Object -FilePath <log> -
User reproduces the scenario in the retail client window (jump on roof, hit wall, etc.). Breakpoints fire, log fills.
-
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
xfirst to find the actual name. -
bp acclient!Class::methodsets 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 cdbwill 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,
thisis inecx. Use cdb'sdt acclient!ClassName @ecxfor 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 ifgrepis enough. - Visual / rendering bugs — debugger doesn't help with shaders or framebuffers; use RenderDoc or similar.
- Network protocol questions —
holtburgerreferences + 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.
Milestone discipline
acdream operates at two altitudes above the daily commit:
docs/plans/2026-05-12-milestones.md— the morale + scope layer. Seven milestones (M0–M7) from "Connect & explore" to "v1.0", each defined by a concrete playable scenario and ~6–10 weeks of work. This is where you orient when the project feels half-built and you're not sure what to work on. Phases are too granular to feel like progress; this doc is the multi-week target.docs/plans/2026-04-11-roadmap.md— the strategic roadmap (next section). Phase-level index. This is where you orient when you know the milestone and need the next concrete sub-phase.
Currently working toward: M1 — Walkable + clickable world. L.2 collision + B.4 interaction. Demo target: walk through Holtburg without getting stuck, open the inn door, click an NPC, pick up an item. Estimated 4–6 weeks from 2026-05-12.
Work-order autonomy — the meta-rule. You decide what to work on next, always. The user does NOT pick between phases, milestones, or "what's next?" alternatives. The milestone discipline + the per-milestone phase list + the roadmap IS the work order — drive it. Never ask the user "want me to start X or Y?" or present a menu of options. If two next steps are genuinely equivalent, state which one you picked and why in one sentence and start — don't ask. The user retains the right to redirect if they think you're wrong, but the default is Claude drives, user reviews. The user finds decision fatigue from constant work-order choices draining — that's literally what triggered the milestones doc on 2026-05-12. Honoring this rule is the single biggest morale lever. This is the meta-rule that makes the four below actually work.
The four motivation-keeping rules:
-
One active milestone at a time. Work that isn't on the critical path to M1 gets filed in
docs/ISSUES.mdwith apost-M1tag and muted. This is the single rule that kills the "jumping between things" feeling. If a phase isn't part of the current milestone, it doesn't get touched — even if it's tempting, even if it would be "quick", even if it would be "while I'm here." -
Frozen phases are off-limits. M0's ~25 shipped phases are frozen until M7's polish pass. Concretely: no rework on streaming, chat, input, the WB rendering migration, sky/lighting, the particle system, or the network handshake. Those are done. Don't revisit them — even if you see something that could be 10% better. Visual nice-to-haves and architecture second-guesses on frozen phases are explicitly post-M7. The freeze list per milestone lives in the milestones doc.
-
Each milestone hit gets a recorded demo video. When M1 lands, record ~30 seconds of the demo scenario, drop it at
docs/milestones/M1-walkable-clickable.mp4, and pin a still + a one-paragraph writeup at the top of2026-05-12-milestones.md. The freeze list updates. The "currently working toward" line in this CLAUDE.md updates to M2. Crossing a milestone is a real event with an artifact — that's the morale instrument. Phases ship; milestones land. -
State both altitudes at session start. First action of any session: "Currently working toward M1 — Walkable + clickable world. Current phase: L.2. Next concrete step: [whatever]." This keeps the high-level orientation visible alongside the immediate task and makes mid-session drift obvious.
When reality and the milestones diverge — a phase grows beyond the milestone's scope, a demo scenario turns out to be unreachable without a new sub-phase, the order needs reshuffling — update the milestones doc in the same session you discover the divergence. Same rule as the roadmap.
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 Phase L.2 (Movement & Collision Conformance). L.2a slices
1+2+3 + L.2d slice 1+1.5 + L.2g slice 1 shipped 2026-05-12. L.2g slice 1
is CODE-COMPLETE (parser + registry mutator + WorldSession dispatcher +
GameWindow subscriber, 4 commits, build green, 6 new tests pass), but
its visual verification is deferred to the B.4b session — clicking
on a door does nothing today because Phase B.4's input-action handler
was never wired (the wire builders and bindings exist, but
GameWindow.OnInputAction has no case for SelectDblLeft, so the
outbound Use never sends). The natural next step is Phase B.4b —
finish the outbound Use handler wiring (subscribe SelectDblLeft →
WorldPicker.Pick → InteractRequests.BuildUse → send), then re-run
the Holtburg inn-doorway visual test which verifies both L.2g slice 1
and B.4b in one pass. Estimated 30-50 LOC, ~30 min, 1-2 subagent
dispatches.
L.2g slice 1 ship handoff: docs/research/2026-05-12-l2g-slice1-shipped-handoff.md
— full evidence + the 4 minor review notes + the 1 Important test-coverage
gap for ShouldSkip (the B.4b visual test's hex-dump will settle whether
ACE sends state=0x4 alone or 0x14).
Design spec: docs/superpowers/specs/2026-05-12-l2g-dynamic-physicsstate-design.md.
Implementation plan: docs/superpowers/plans/2026-05-12-phase-l2g-slice1.md.
L.2d ship handoff: docs/research/2026-05-13-l2d-slice1-shipped-handoff.md.
Phase L.2a (Truth & Diagnostics) slices 1-3 shipped 2026-05-12.
Three commits land the L.2 "make every bad movement outcome explainable"
diagnostic foundation. Slice 1 (ebef820) adds runtime-toggleable
ACDREAM_PROBE_RESOLVE (one [resolve] line per
PhysicsEngine.ResolveWithTransition call) + ACDREAM_PROBE_CELL (one
[cell-transit] line per PlayerMovementController.CellId change),
both backed by a new AcDream.Core.Physics.PhysicsDiagnostics static
class and mirrored as DebugPanel checkboxes. Slice 2 (e0c08bc) extends
the [resolve] line with obj=0x... attribution. Slice 3 (a068292)
populates the previously-stub CollisionInfo.CollideObjectGuids /
LastCollidedObjectGuid (declared in TransitionTypes.cs but never
written anywhere) at the per-object iteration in FindObjCollisions,
so the slice-2 promise is now actually delivered. Visual-verified at
the Holtburg Town doorway: probes captured 140 wall hits attributed to
obj=0xA9B47900 (landblock-baked static = the building itself,
NOT a door entity), confirming L.2d sub-direction as port
CBuildingObj collision + per-cell walkability rather than door-
state-toggle. Plus a definitive L.2e finding: player CellId tracked
as bare low byte (0x00000029) with no landblock prefix.
Phase C.1.5b (per-part PES transforms + dat-hydrated entity DefaultScript)
shipped 2026-05-12. Closes issue #56. SetupPartTransforms.Compute(setup)
walks PlacementFrames[Resting] → [Default] → first-available and
returns one Matrix4x4 per Setup part; ParticleHookSink.SpawnFromHook
now transforms each CreateParticleHook.Offset through
partTransforms[PartIndex] before applying entity rotation, so
multi-emitter scripts distribute across mesh parts instead of collapsing
to entity root. The EntityScriptActivator.OnCreate ServerGuid==0
guard was relaxed: it now keys by entity.ServerGuid when non-zero, else
entity.Id (the 0x40xxxxxx interior-entity range is collision-free
with server guids, so no synthetic-ID scheme is needed). GpuWorldState
fires the activator from 4 new sites — AddLandblock +
AddEntitiesToExistingLandblock (Far→Near promotion) for OnCreate,
RemoveLandblock + RemoveEntitiesFromLandblock (Near→Far demotion)
for OnRemove — so dat-hydrated EnvCell statics (inn fireplaces, building
decorations) and exterior stabs (cottage chimneys) now activate their
Setup.DefaultScript automatically. Reality discovery during design
(folded into spec §3): EnvCell StaticObjects are already hydrated as
WorldEntity instances by GameWindow.BuildInteriorEntitiesForStreaming
with stable entity.Id in 0x40xxxxxx — the handoff's §4 Q1/Q2
(synthetic ID scheme, separate walker class) were mooted by this.
Visual-verified 2026-05-12 at Holtburg Town network portal (no
ground-burial, distributed swirl), Inn fireplace flames, cottage
chimney smoke, and a spell cast on +Acdream. Plan archived at
docs/superpowers/plans/2026-05-13-phase-c1.5b.md.
Phase C.1.5a (portal PES wiring) shipped 2026-05-11 (merge 88bda12).
Server-spawned WorldEntity entities fire their Setup.DefaultScript
through PhysicsScriptRunner on enter-world via the
EntityScriptActivator (src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs).
Visual-verified at the Holtburg Town network portal: 10-hook portal
script fires end-to-end with correct color, persistence, orientation,
multi-emitter dispatch. Filed #56 for per-part transform handling
(resolved in C.1.5b above). Plan archived at
docs/superpowers/plans/2026-05-12-phase-c1.5a-portals.md.
Phase N.6 slice 1 (gpu_us fix + radius=12 perf baseline) shipped
2026-05-11 (merge 9b447d4). Fixed gpu_us double-buffering in
WbDrawDispatcher (ring-of-3 query slots, read-before-overwrite,
vendor-neutral). Captured authoritative perf baseline at Holtburg radii
4 / 8 / 12. Conclusion: CPU dominates GPU by 30–50× at every radius;
GPU sits at 3.6% of frame budget; per-LB walk is the next bottleneck.
Baseline-doc recommendation: do C.1.5 next, then a reduced-scope slice 2
(atlas + persistent-mapped buffers dropped from slice-2 scope). Baseline
at docs/plans/2026-05-11-phase-n6-perf-baseline.md.
Plan archived at docs/superpowers/plans/2026-05-11-phase-n6-slice1.md.
Issue #55 filed (static-entity slow path reports ~1.45M meshMissing
per 5s at r4 standstill — diagnostic, not a visible regression).
Post-A.5 polish phase complete 2026-05-11. All three post-A.5
issues closed: #52 (lifestone, e40159f), #54 (JobKind, bf31e59),
#53 (Tier 1 entity cache, f928e66). Phase A.5 + post-A.5 polish
together comprise the streaming + rendering perf foundation for the
project.
Next phase candidates (in rough preference order):
- Phase B.4b — finish the outbound Use handler wiring.
Direct M1 blocker discovered while running the L.2g slice 1 visual
test: the wire builders (
InteractRequests.BuildUse), classes (SelectionState,WorldPicker), input-action enum entries (SelectDblLeftetc.), and keybindings (LMB-dblclick →SelectDblLeft) all ship today, butGameWindow.OnInputAction's switch has NO case for any of theSelect*actions — clicking on a door fires the diagnostic[input] SelectDblLeft Pressbut nothing downstream listens. Memory fileproject_interaction_pipeline.mdupdated to reflect this reality. Shape: subscribeInputAction.SelectDblLeft→ build a world ray from current mouse →WorldPicker.Pick(...)→ store in_selection→ callInteractRequests.BuildUse(seq, guid)_liveSession.SendGameMessage(body). Probably also subscribeSelectLeftfor select-without-use andUseSelectedfor the R hotkey. Estimate: 30-50 LOC, 1-2 subagent dispatches, ~30 min. Verifies L.2g slice 1 in the same Holtburg-doorway visual test once it lands. Full context:docs/research/2026-05-12-l2g-slice1-shipped-handoff.md"Why the visual test is deferred" section.
- Triage the chronic open-issue list in
docs/ISSUES.md— #2 (lightning), #4 (sky horizon-glow), #28 (aurora), #29 (cloud thinness), #37 (humanoid coat), #50 (stray tree), #41 (remote-motion blips) have been open since April/early-May and keep getting deferred. Either link each to a future phase or downgrade. ~1 hour, surfaces what's chronic vs. linked-to-a-phase. - More Phase C visual-fidelity work (C.2 dynamic point lights, C.3 palette tuning, C.4 double-sided translucent polys) closing the "world reads as old / broken vs. retail" backlog.
- N.6 slice 2 at reduced scope (atlas opportunities only — persistent- mapped buffers and other slice-2 items dropped per slice-1 baseline doc).
- Perf tiers 2/3 (
docs/plans/2026-05-10-perf-tiers-2-3-roadmap.md) only if user wants sustained 500+ FPS. With Tier 1 dispatcher at ~1.2 ms the project comfortably hits 200-400 FPS at radius=12 standstill; escalation is optional from here.
Earlier rendering + streaming arc (2026-05-08 → 2026-05-10).
Phases N.4 → N.5 → N.5b → A.5 shipped the modern rendering
pipeline + two-tier streaming foundation: WB ObjectMeshManager as
production mesh path (N.4); bindless + glMultiDrawElementsIndirect
for entities (N.5, ~12-15 GL calls/frame) and terrain via Path C
preserving retail's FSplitNESW formula (N.5b, closes #51); two-tier
streaming N₁=4 / N₂=12 + QualityPreset system (A.5). Modern path is
mandatory as of N.5 ship amendment — InstancedMeshRenderer,
StaticMeshRenderer, WbFoundationFlag all deleted; missing bindless
throws at startup. Detail + decomp anchors + plan archives in roadmap
shipped-table rows 63–66 at docs/plans/2026-04-11-roadmap.md.
Engineering gotchas (bindless Dispose order, texture target lock-in,
uvec2 sampler-handle pattern, WB-vs-retail formula divergence)
documented inline at the relevant call sites and in
feedback_wb_migration_*.md memory entries.
Rules:
-
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.
-
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:brainstormingskill before writing code. Update the roadmap in the same session. -
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.
-
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.
-
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:
- Start-of-session: scan OPEN issues in
docs/ISSUES.md— any of them in the area you're about to touch? Plan accordingly. - 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.
- Commit messages: reference issue IDs when a commit closes one
(
fix #3: periodic TimeSync parsing). Makesgit log --grep='#3'return the full fix history. - 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":
$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:
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 inboundUpdateMotion(guid, stance, cmd, speed) + resultingSetCyclecall. 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.ACDREAM_PROBE_RESOLVE=1— L.2a slice 1+2+3 (2026-05-12). One[resolve]line perPhysicsEngine.ResolveWithTransitioncall: input + target + output position/cell, ok-vs-partial, grounded-in, contact-plane status, wall normal if hit, responsible entity guid (post-slice-3 attribution plumbing), env flag, walkable polygon valid. Heavy (~30 Hz × every entity). Runtime-toggleable via the DebugPanel "Diagnostics" section ifACDREAM_DEVTOOLS=1.ACDREAM_PROBE_CELL=1— L.2a slice 1 (2026-05-12). One[cell-transit]line perPlayerMovementController.CellIdchange: old → new cell, world position, reason tag (resolver/teleport). Low volume — only fires on actual cell crossings. Runtime-toggleable via the same DebugPanel section.- (retired 2026-05-05 by L.3 M2/M3)
ACDREAM_INTERP_MANAGERwas an env-var gate on an experimental per-tick remote motion path. L.3 M2 (commit40d88b9) replaced both gates (OnLivePositionUpdated+TickAnimations) withIsPlayerGuid(...)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 legacyapply_current_movement+ResolveWithTransitionpath. 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
- Make a code change.
dotnet build— must be green before launch.- Launch (background). Give it ~8 s to reach in-world state.
- User tests in the running client (moves, interacts, watches remote chars). Close window when done.
- Read
launch.logor the task output file for diagnostic lines. - 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
+Acdreamcharacter 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. SeeACViewer/Render/TextureCache.cs::IndexToColorfor 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, checkdocs/architecture/worldbuilder-inventory.mdfirst. 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 2568") 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
WorldSessionor 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 (FSplitNESWwith constants0x0CCAC033,0x421BE3BD,0x6C1AC587,0x519B8F25) which differs from WorldBuilder's physics-path formula. Also has the complete0xF61Cmovement packet format with flag bits and thestMoveInfosequence 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. Seedocs/research/2026-04-12-movement-deep-dive.mdfor 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).