acdream/CLAUDE.md
Erik 95b6874c12 docs(render): Phase U.4c — flap fixed + residuals handoff (checkpoint)
Canonical handoff (research note) for the U.4c flap fix + the three residuals the
visual gate revealed (#78 terrain-not-gated-inside, camera-collision need, U.5).
Records the full hypothesis journey (H1/H2 both evidence-disproven) so the next
session doesn't re-walk them. ISSUES.md: flap recorded in Recently-closed; #78
annotated (more visible post-fix). CLAUDE.md: U-phase orientation updated with the
flap-fixed status + the canonical handoff pointer + camera-collision-next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 17:01:23 +02:00

1728 lines
99 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 code lives in our tree as of Phase O (shipped 2026-05-21).**
Phase N.4 (2026-05-08) adopted WB's rendering + dat-handling base as a
project reference. Phase O (2026-05-21) extracted the ~33 files / ~7.7K LOC
we actually use into our own namespaces and dropped the two external project
references. `DatCollection` is now the **only** dat reader in process —
`DefaultDatReaderWriter` is gone. `references/WorldBuilder/` remains in-tree
as a read-reference (MIT-licensed; grep it freely), but nothing in
`src/AcDream.*` references it as a project dependency.
**Where the extracted code lives (post-Phase O):**
- `src/AcDream.Core/Rendering/Wb/` — pure dat/mesh helpers (5 files, ~782 LOC):
`TerrainUtils`, `TerrainEntry`, `RegionInfo`, `SceneryHelpers`,
`TextureHelpers`. No GL dependency; safe to use from Core.
- `src/AcDream.App/Rendering/Wb/` — GL infrastructure + mesh pipeline (~27 files,
~7K LOC): `ObjectMeshManager`, `WbMeshAdapter`, `WbDrawDispatcher`,
`LandblockSpawnAdapter`, `EntitySpawnAdapter`, `TextureCache`,
`GlobalMeshBuffer`, shader infrastructure, and the EnvCell/portal/scenery/
terrain-blending pipeline classes.
Before re-implementing any AC-specific rendering or dat-handling algorithm,
**read `docs/architecture/worldbuilder-inventory.md` FIRST**. The inventory
describes what we extracted (now in our tree) and what we still write ourselves.
Re-porting from retail decomp when we already have 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.
**WB rendering cribs (all paths now in `src/AcDream.App/Rendering/Wb/`):**
- `WbMeshAdapter.cs` — single seam over `ObjectMeshManager`. Owns the mesh
pipeline, drains its staged-upload queue per frame via `Tick()`, populates
`AcSurfaceMetadataTable` with per-batch translucency / luminosity / fog
metadata. Consumes `DatCollection` via `DatCollectionAdapter` (O-D7 fallback
path; `ObjectMeshManager` has 26 internal `_dats.X` call sites that exceed
the inline-swap threshold — the adapter bridges our `IDatCollection` to the
`IDatReaderWriter` interface WB's internals expect).
- `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.
- `LandblockSpawnAdapter.cs` / `EntitySpawnAdapter.cs` — bridge spawn lifecycle
to 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.
- **The 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.)
- **`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 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 original WB), not
per-blend-mode subpasses. Opaque pass discards `α<0.95`; transparent
pass discards `α≥0.95` AND `α<0.05`. Native `Additive` blend renders
as alpha-blend on GfxObj surfaces — falsifiable; if a magic-content
regression shows up, add a third indirect call with
`glBlendFunc(SrcAlpha, One)` per spec §6 fallback (~30 min change).
- **Per-instance highlight (selection blink) is reserved — open
backlog, no scheduled phase.** `mesh_modern.vert`'s `InstanceData`
struct has a documented hook for `vec4 highlightColor`. Whoever
eventually picks it up finds the hook there; the change is localized:
extend `InstanceData` stride 64→80 bytes, add the field, mix into
fragment color in `mesh_modern.frag`. ~30 min when the time comes.
- `src/AcDream.App/Rendering/TerrainModernRenderer.cs` — terrain dispatcher
on N.5's modern primitives. Mirrors the original WB `TerrainRenderManager`
pattern (single global VBO/EBO + slot allocator + `glMultiDrawElementsIndirect`)
but driven by acdream's `LandblockMesh.Build` so retail's `FSplitNESW`
formula is preserved (issue #51 resolved). Atlas handles bound via the
uvec2 + `sampler2DArray(handle)` constructor pattern (NOT the direct
`uniform sampler2DArray` + `glProgramUniformHandleARB` form, which
GL_INVALID_OPERATIONs on at least one driver).
- **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; produces
`TwoTierDiff` with 5 transition lists per tick), `StreamingController`
(render-thread coordinator: routes `TwoTierDiff` to the worker queue and
drains completions up to `MaxCompletionsPerFrame` per frame),
`LandblockStreamer` (single background worker thread: `LoadFar` = heightmap
+ mesh only, `LoadNear` = heightmap + `LandBlockInfo` + scenery + mesh,
`PromoteToNear` = `LandBlockInfo` + scenery only),
`GpuWorldState` (render-thread entity state: `AddEntitiesToExistingLandblock`
for promotions, `RemoveEntitiesFromLandblock` for 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`.
**Execution model:** the active source of truth is the **milestones doc**
(`docs/plans/2026-05-12-milestones.md`) for "what are we building right
now" and the **strategic roadmap** (`docs/plans/2026-04-11-roadmap.md`)
for the per-phase ledger of what's shipped, what's in flight, and what
comes next. **Ignore the old "R1→R8" sequence** — it was an early refactor
sketch that no longer matches reality (see the "Roadmap Model" section in
`docs/architecture/acdream-architecture.md`). Per-phase detailed specs
live under `docs/superpowers/specs/`.
The codebase is organized by layer (see architecture doc + the **Code
Structure Rules** section below). Plans live in `docs/plans/`,
research in `docs/research/`, persistent project memory in `memory/`.
**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`](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.
## Code Structure Rules
These are the structural rules the project commits to. They are
**process rules** (where code goes, what depends on what), not style
rules (formatting / naming). They exist to keep the layer split honest
and to stop `GameWindow.cs` from continuing to grow into a 10k-line
god object. The full rationale + the extraction sequence we're
pursuing live in [`docs/architecture/code-structure.md`](docs/architecture/code-structure.md).
1. **No new substantial feature bodies in `GameWindow.cs`.** It is
already over 10,000 lines and owns runtime wiring. New runtime
work goes into a dedicated controller / sink / orchestrator class
in `src/AcDream.App/` (or deeper in `AcDream.Core` when it's pure
logic). Adding a handful of fields and a one-paragraph method to
wire an extracted class in is fine; adding a new ~200-line feature
directly is not. When in doubt, file a small follow-up extraction
as part of the change.
2. **`AcDream.Core` must not depend on the window / GL / backend
projects, except via documented interop seams.** As of Phase O
(2026-05-21), the only allowed seams are the extracted helpers in
`src/AcDream.Core/Rendering/Wb/` (`TerrainUtils`, `TerrainEntry`,
`RegionInfo`, `SceneryHelpers`, `TextureHelpers` — stateless, no GL).
The former `WorldBuilder.Shared` and `Chorizite.OpenGLSDLBackend.Lib`
project references are gone; their code now lives in our tree at those
paths. New Core code that needs a GL surface must define an interface
in Core and let `AcDream.App` implement it — never the reverse. If you
need to add a project reference to Core, the change must come with an
inventory-doc
update explaining why.
3. **UI panels target `AcDream.UI.Abstractions` only.** No panel may
import `AcDream.UI.ImGui` or any backend namespace. ViewModels,
commands, and the `IPanel` / `IPanelRenderer` contract are the
surface; everything else is backend. This is what lets us swap
D.2a (ImGui) for D.2b (retail-look) later without rewriting
panels.
4. **Startup environment variables enter through a typed options
object.** `AcDream.App.RuntimeOptions` is the single source of
truth for startup configuration. `Program.cs` reads the
environment once into `RuntimeOptions` and passes it into
`GameWindow`. Don't sprinkle `Environment.GetEnvironmentVariable`
reads through new code paths; add a field to `RuntimeOptions` and
pipe it through.
5. **Runtime probes (diagnostic toggles) belong in diagnostic owner
classes.** Today `AcDream.Core.Physics.PhysicsDiagnostics` owns the
`ACDREAM_PROBE_*` family. The pattern: one static class per
subsystem, exposing typed bool/int properties read from env vars
once at startup (with optional runtime-toggleable counterparts for
the DebugPanel). Per-call-site `Environment.GetEnvironmentVariable`
reads in new code are a process smell — if a flag survives one
phase, promote it to a diagnostic owner. The dozens of existing
`ACDREAM_DUMP_*` reads scattered through `GameWindow` are tech
debt; do not add more.
6. **Tests live in the project matching the layer under test.** Core
tests in `tests/AcDream.Core.Tests/`, UI tests in
`tests/AcDream.UI.Abstractions.Tests/`, network tests in
`tests/AcDream.Core.Net.Tests/`. App-layer tests (RuntimeOptions
parsing, etc.) belong in `tests/AcDream.App.Tests/`. When adding a
new test project, register it in `AcDream.slnx`.
## 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 (milestones doc + strategic roadmap), 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.
**No workarounds without explicit approval.** When you spot a bug or
encounter a behavioral mismatch, fix the underlying cause — do not ship a
band-aid, suppression flag, grace period, retry loop, or any other "make
the symptom go away" shortcut, unless the user has explicitly approved
that shape OR you are building a NEW feature with a different design.
This rule exists because every workaround creates architectural debt that
masks the real issue, makes future refactors harder, and erodes the
codebase's retail-faithfulness. Examples of disallowed shortcuts: an
`if (problematicState) return early` guard at the symptom site instead of
investigating why the state happened; a timer-based "settle period" to
hide a race; a flag like `_suppressXDuringY` to mask a wire-level mistake;
a `try/catch` swallowing an exception that signals a real problem. If you
notice a fix is starting to look like a workaround mid-implementation,
stop, file the proper investigation as an issue with full reproduction
notes, and either (a) ask the user before shipping the workaround, or
(b) invest the time to fix the root cause. The user has explicitly
authorized "spend more time, get it right" over "ship a shortcut and
file the cleanup." Quote them: "we should have no workarounds unless I
say so or we want a different feature."
**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 `/investigate` skill
(`.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 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.
- **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 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.
## MCP servers (live tooling)
Two MCP servers extend the static decomp + cdb workflow with live
introspection. **Ghidra MCP** requires Ghidra to be running with a
CodeBrowser open in the target project; **WireMCP** auto-loads at
Claude Code startup.
### Ghidra MCP (LaurieWired v1.4, HTTP)
Starts an HTTP server on **port 8080** (or **8081** if 8080 is
taken — first-open-wins) when a CodeBrowser tool opens a program.
Currently serving **`patchmem.gpr`** — the 2013 v11.4186 build with
full PDB applied, same source as `docs/research/named-retail/`. Use
this when grep'ing `acclient_2013_pseudo_c.txt` returns too much
noise and you want the decomp for one specific function or address
without dumping the whole file into context.
Probe: `curl http://127.0.0.1:8081/methods?limit=3`
Useful endpoints (GET unless noted):
- `/methods?limit=N` — function names
- `/list_functions?limit=N` — `Name at HHHHHHHH` lines
- `/decompile_function?address=0xHHHHHHHH` — decompiled C for one function
- `/function_xrefs?name=...` — callers / callees
- `/classes`, `/namespaces`, `/strings`
- POST `/rename_function_by_address`, POST `/set_decompiler_comment`
NO endpoints for: signature setting, namespace setting, script
execution, save-project. Those still require Ghidra's GUI or
`analyzeHeadless`. Full endpoint catalog + Ghidra project layout in
`memory/reference_ghidra_projects.md`.
### WireMCP (stdio, Node, user-scope)
Wraps `tshark` at `C:\Program Files\Wireshark\tshark.exe`
(auto-detected via the Windows fallback path in `WireMCP/index.js`).
Direct fit for ACE wire-protocol work — capture loopback
(`127.0.0.1:9000`) to cross-check inbound message parsing (`0xF61C`
movement, `0xF74A` pickup despawn, `0xF7DE` chat, etc.) against the
actual bytes, or diff ACE's outbound vs. the holtburger reference.
Replaces ad-hoc Wireshark sessions in the conversation.
Tools exposed:
- `capture_packets` — short live capture on an interface, returns JSON
- `get_summary_stats` — protocol hierarchy stats
- `get_conversations` — TCP/UDP conversation table
- `analyze_pcap` — parse a saved `.pcap` file
- `check_threats`, `check_ip_threats` — URLhaus / threat-feed lookups
- `extract_credentials` — grep for creds across protocols (rarely relevant)
Installed at `C:\Users\erikn\source\repos\WireMCP\` (clone of
`0xKoda/WireMCP`). Registered via `claude mcp add wiremcp --scope user`.
**When NOT to use WireMCP:** decoding the AC packet *format* — that
lives in `holtburger`, ACE, and `Chorizite.ACProtocol`. WireMCP shows
you the bytes on the wire; the reference repos tell you what they
mean.
## 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 (M0M7) from "Connect & explore" to "v1.0", each
defined by a concrete playable scenario and ~610 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.
**M1 landed 2026-05-16** via Phase B.6 (`d640ed7`). L.2 collision +
B.4 interaction + B.5 pickup + B.6 server-driven auto-walk all
shipped. The four demo targets work end-to-end: walk Holtburg, open
inn door, click NPC, pick up item. Freeze list active — M1's phases
are off-limits until M7 polish. Writeup at top of M1 block in
`docs/plans/2026-05-12-milestones.md`.
**Phase O — DatPath Unification — SHIPPED 2026-05-21.** ONE thing
touches the DATs. ~33 WB files (~7.7K LOC) extracted into
`src/AcDream.{Core,App}/Rendering/Wb/`. Project references to
`WorldBuilder.Shared` + `Chorizite.OpenGLSDLBackend` dropped.
`DefaultDatReaderWriter` eliminated; `DatCollection` is the only dat
reader. `WbMeshAdapter` consumes it via `DatCollectionAdapter`
(O-D7 fallback; 26 `_dats.*` call sites exceeded the inline-swap
threshold). `references/WorldBuilder/` stays in-tree as read-reference.
Visual side-by-side passed: Holtburg town, inn interior, dungeon all
render identically to pre-O. Spec:
[`docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md`](docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md).
**2026-05-30 — RENDER PIPELINE PIVOT (read this first).** The two-pipe
(inside / outside) render approach is **ABANDONED**. acdream inherited a
WorldBuilder-style split — a normal outdoor draw plus a separate flat
`RenderInsideOut` stencil pass toggled on `cameraInsideBuilding` — and that
split is the root cause of every indoor seam bug (the flap, missing/transparent
walls, terrain bleeding into interiors). Retail has no such split; it renders
through one portal-visibility traversal (`PView`) and is seamless by
construction. We are building **Phase U — a single unified retail-faithful
render pipeline**. This supersedes the A8/A8.F two-pipe arc (issue #103). The
camera-collision work (retail `SmartBox::update_viewer` spring arm) + a
physics viewer-cap fix **SHIPPED this session and are kept** (they're real and
retail-faithful, just not the seam fix). Full decision + scope + next-session
pickup prompt:
[`docs/research/2026-05-30-unified-render-pipeline-decision-and-handoff.md`](docs/research/2026-05-30-unified-render-pipeline-decision-and-handoff.md).
The M1.5 narrative below is history retained for context.
**2026-05-31 — U.4c doorway FLAP FIXED** (`0ee328a`, visual-verified "flap gone").
Root cause (converged on a live `ACDREAM_PROBE_FLAP` capture, after disproving an
H2 `PortalSide` side-test fix and an H1 PVS-grounding hypothesis): indoor visibility
was rooted at the 3rd-person camera **eye**, which drifts out of the player's cell →
`FindCameraCell` returns a STALE cell for its grace frames → the doorway portal is
culled as behind-the-eye → exit cell + terrain + shells drop. Fix: root indoor
visibility (cell resolution + portal-side test) at the **player's cell**
(retail `CellManager::ChangePosition`; matches the existing lighting decision). Eye
still drives projection. **The flap is done; the indoor pipeline is NOT yet seamless**
the visual gate revealed three SEPARATE residuals: (1) **#78** outdoor terrain not gated
inside (now more visible since terrain draws again); (2) **camera collision** needed (the
chase eye is outside the player's cell ~79% of frames → the eye-projected clip
over-includes → transparent outer walls); (3) **U.5** outside-looking-in (deferred).
Camera collision (retail `SmartBox::update_viewer` keeping the eye in the cell) is the
highest-leverage next step. CANONICAL handoff (read first next session):
[`docs/research/2026-05-31-u4c-flap-fixed-and-residuals-handoff.md`](docs/research/2026-05-31-u4c-flap-fixed-and-residuals-handoff.md).
Apparatus `ACDREAM_PROBE_FLAP` + `tools/A8CellAudit` are committed + ready. Do NOT retry
H1 (PVS grounding) or H2 (`PortalSide` side-test) — both evidence-disproven.
**Currently working toward: M1.5 — Indoor world feels right** (resumed
from 2026-05-20 baseline after Phase O ship). **A6.P1 + A6.P2 + A6.P3
slice 1 SHIPPED 2026-05-21.** **A6.P3 slice 2 v2 SHIPPED 2026-05-22**
(commit `f8d669b`): tried removing the L622 per-tick CP seed
(`892019b` v1) but it broke BSP step_up at the last step of stairs;
reverted + added a benign no-op-if-unchanged guard inside
`CollisionInfo.SetContactPlane`. Slice 2 outcome: **#96 partially
addressed — accepted as documented retail divergence** (the per-tick
seed is load-bearing for `AdjustOffset` slope-projection on sub-step 1
which BSP step_up depends on; matching retail would require deeper
refactor of AdjustOffset). Slice 2 verification surfaced a NEW
M1.5-blocking bug: **user cannot walk UP out of cottage cellar — stuck
at last step due to cell-resolver ping-pong (filed as issue #98,
Finding 3 family).** **A6.P3 slice 3 SHIPPED 2026-05-22** (commits `8898166` v1 +
`3e140cf` v2): cell-resolver stickiness added in `ResolveCellId`'s
indoor branch (point-in check against `fallbackCellId`'s CellBSP
before falling through to FindCellList). Data confirms ping-pong is
FULLY CLOSED — scen4 cellar capture shows 1 cell-transit (login
teleport) vs 20+ pre-fix. **#90 workaround now redundant — deferred
to A6.P4 removal. #98 APPARATUS COMPLETE 2026-05-23 evening**
(commits `35b37df` triage → `f62a873` cell-dump probe → `3f56915`
fixtures → `856aa78` replay harness → `6f666c1` cdb script →
`28c282a` divergence comparison doc). Four sessions of speculative
fixes (10+ variants) shipped the wrong diagnosis each time; this
session shipped the APPARATUS that turns evidence-driven analysis
into a 200ms test loop. Real divergence: retail's sphere is at
world Z ≈ 94.48 (resting on cottage floor) when find_walkable
accepts; acdream's failing-frame sphere is at world Z ≈ 92.01
(2.47m lower). Retail's ContactPlane writes during cellar-up are
ONLY flat floors (cellar floor or cottage floor), never the ramp.
Retail's find_crossed_edge fires once in 35K BPs; ours uses it
heavily. **Fix targets (priority): (1) Transition.AdjustOffset
slope projection / DoStepUp WalkInterp handling — ramp climb
doesn't gain Z; (2) cottage-cell candidacy using wrong sphere
reference; (3) find_crossed_edge over-use; (4) ramp polygon normal
divergence (low confidence).** Full divergence reading +
fix-plan pickup prompt at
[`docs/research/2026-05-23-a6-p3-issue98-replay-comparison.md`](docs/research/2026-05-23-a6-p3-issue98-replay-comparison.md).
Current A6 phase:
**A6.P3 — PAUSED 2026-05-23 (full day). Trajectory replay harness shipped
but BLOCKED on a new bug surfaced during commissioning.** Read
[`docs/research/2026-05-23-a6-p3-issue98-harness-handoff.md`](docs/research/2026-05-23-a6-p3-issue98-harness-handoff.md)
as the canonical pickup document — it has the chronological commit list,
the apparatus inventory, the exclusion list (do-not-retry), and three
concrete next-session options ranked by recommendation.
The session shipped further apparatus + first failed fix attempt + revert:
`8a232a3` (`[step-walk-adjust]` probe inside `Transition.AdjustOffset`
revealing branch tokens and per-call zGain), `8daf7e7` (findings note
at [`docs/research/2026-05-23-a6-stepwalkadjust-findings.md`](docs/research/2026-05-23-a6-stepwalkadjust-findings.md)
+ capture snapshot), `0cb4c59` (Shape 1 fix: gate `BSPQuery.AdjustSphereToPlane`'s
two `SetContactPlane` call sites by `Normal.Z >= 0.99`), `402ec10`
(revert — Shape 1 broke OnWalkable tracking, sphere went into falling
state on every sloped surface). **Refined diagnosis:** AdjustOffset is
CORRECT (145/146 calls take `into-plane` branch, +0.045 m mean zGain
per call when offset points into ramp); the climb CAPS at world Z ≈
92.80 because step-up's downward step-down probe finds no walkable
within 0.6 m below the proposed position (cottage floor is ABOVE).
Earlier "Fix targets 14" priority list is OBSOLETE — AdjustOffset
projection is not the problem. The actual bug is in the step-up
validation at the ramp top. **Honest next-session moves**: (1) build
deterministic trajectory replay harness so fix attempts iterate in
<500ms instead of 5-minute live-test cycles; (2) pivot to a less-
coupled M1.5 issue while #98 awaits the harness; (3) targeted decomp
research on `CEnvCell::find_env_collisions` `BSPTREE::find_collisions`
indoor CP-setting chain (prior research worked on the outdoor
`CLandCell` path; indoor was never fully traced). Session-end ISSUES.md
entry has the full reading and pickup prompt. **NO further #98 fix
attempts until apparatus or research has converged six+ failed
attempts in the saga is the signal.**
**Late-day extension (2026-05-23 PM):** trajectory replay harness shipped
(commits `4c9290c` `5c6bdbe`). Mechanics work runs 200 ticks in <100 ms.
Five tests pass. NEW finding: the cellar ramp polygon is in a GfxObj
(static building piece), not the cell's PhysicsPolygons. Harness now
includes `RegisterStairRampGfxObj` for synthetic stair construction
and `AttachSyntheticBsp` to wrap hydrated cells (which have BSP=null)
with a one-leaf BSP that exposes the indoor BSP collision path.
**NEW BLOCKER:** even with full apparatus, sphere goes airborne at
tick 1 with `hit=(0,1,0)` (a +Y wall normal matching no registered
geometry). 6 hypotheses tested via the harness, none isolated root cause.
Per systematic-debugging skill's "question architecture" rule, stop and
reflect. Next session: build a side-by-side comparison harness that
captures live PlayerMovementController state and diffs against the
test harness evidence-first instead of speculation-first.
Findings doc:
[`docs/research/2026-05-21-a6-cdb-capture-findings.md`](docs/research/2026-05-21-a6-cdb-capture-findings.md).
**Evening extension v2 (2026-05-23 PM late) apparatus shipped + root
cause identified.** Four commits (`fb5fba6` `44614ab` `0f2db62`
`f29c9d5`). The side-by-side comparison harness was built and exercised:
- `PhysicsResolveCapture` ships a JSON Lines writer for every player-side
`ResolveWithTransition` call. Off by default; turn on via
`ACDREAM_CAPTURE_RESOLVE=<path>`. Filtered to `IsPlayer` so NPC / remote
DR doesn't pollute.
- Two live captures from a cottage-cellar session (41K + 70K records).
- Three `LiveCompare_*` tests load 3 representative records (spawn,
on-ramp, first-cap). Spawn + on-ramp PASS bit-perfect; the first-cap
test originally FAILED with a clear divergence and that divergence
pinpoints the root cause.
- **The cap is caused by `obj=0xA9B47900` a landblock-baked cottage
GfxObj.** Cottage floor polygons live in this GfxObj's polygon table
(registered as a ShadowEntry), NOT in any cottage cell. The harness's
cell fixtures (0xA9B40143/146/147) don't include the cottage GfxObj,
so the harness fails to reproduce the live cn=(0,0,-1) cap.
- User's confirming observation: jumping in the cellar caps at the same
Z purely vertical motion. This rules out every step-up / AdjustOffset
hypothesis from the prior 6-shape saga. The bug is the head sphere
hitting the cottage floor at Z=94.0 from below (math: foot Z=92.74
+ sphereHeight 1.20 = head center 93.94, head top 94.42, intersects
cottage floor Z=94.0).
- The first-cap test is now in documents-the-bug form (PASSES while
bug exists; FAILS when fix lands). Test baseline maintained at
1178 + 8 (serial run).
- 13 new cell fixtures cover the full 0xA9B4014X neighborhood (272 KB).
Findings doc (canonical pickup):
[`docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md`](docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md).
**Evening v2 follow-on — apparatus convergence SHIPPED 2026-05-23 PM.**
Two commits (`cc3afbc` `97fec19`):
- `cc3afbc` adds the GfxObj dump infrastructure (`ACDREAM_DUMP_GFXOBJS`)
mirroring the existing `ACDREAM_DUMP_CELLS` pattern, with new
`GfxObjDump`/`GfxObjDumpSerializer` parallel to `CellDump`. The new
env var triggers `PhysicsDataCache.CacheGfxObj` to write the full
resolved polygon table as JSON when a listed id caches. Closes the
gap that the existing `[resolve-bldg]` probe couldn't fill (the BSP
wire site that populates `LastBspHitPoly` was never wired, so the
probe only emitted GfxObj-level metadata, not per-poly geometry).
- `97fec19` lands the cottage GfxObj fixture (`0x01000A2B`, 74 polygons,
BSP radius 13.989m matching live), the new `RegisterCottageGfxObj`
harness helper, and a minimum-stub landblock so
`TryGetLandblockContext` succeeds at the cellar XY. Harness now
reproduces the live `cn=(0,0,-1)` cap bit-perfect. The full per-field
round-trip uncovers ONE residual: live preserves +0.0266m of +X
motion through the cap (edge-slide along the cottage floor); harness
blocks all motion. Captured in
`LiveCompare_FirstCap_ResidualXMotionDivergence_DocumentsNextInvestigation`
in documents-the-bug form.
- All 21 issue-#98-relevant tests (12 harness + 4 GfxObjDumpRoundTrip +
1 new PhysicsDiagnosticsTests + 4 CellDumpRoundTripTests) pass
deterministically in isolation.
- Pre-existing test suite flakiness observed (819 failures across runs
of the same code, from PhysicsResolveCapture / PhysicsDiagnostics
statics leaking between test classes). INDEPENDENT of A6.P3 verified
by stashing the cottage helper and reproducing the same flaky range.
Out of scope for this session; tracked as follow-up.
**Evening v3 finding (2026-05-23 PM, even later) NEW root-cause
hypothesis identified:** the cottage-floor cap is a SYMPTOM. The actual
bug is **stale ramp contact plane causing per-tick Z drift** that makes
the cap reachable in the first place.
Evidence:
- Body's contact plane at cap = ramp's plane (n=(0, 0.7190, 0.6950),
d=-69.5035) from the live capture's `bodyBefore`
- Cellar ramp's actual world XY: X∈[129.7, 131.3], Y∈[10.19, 13.09]
(computed from the cellar cell fixture's vertex data + WorldTransform)
- Player position at cap: world (141.5, 7.22, 92.74) **10 m away**
from the ramp in cell-local X
- `AdjustOffset` projects requested motion along the contact-plane
perpendicular. Math: dot((0.0266, -0.4022, 0), (0, 0.719, 0.695))
= -0.2892 projected = (0.0266, -0.1943, +0.2010). **+0.201 m of
Z gain per tick**, applied because the engine believes the player
is on the slope.
- Head sphere top at cap = foot Z + 1.68 = 94.42. Cottage floor at
Z=94.00. **Head sphere exceeds cottage floor by 0.42 m** cap fires
- If the contact plane refreshed to the flat cellar floor when the
player walked off the ramp, AdjustOffset would produce zero Z gain
(no Z component in requested motion + horizontal-plane perpendicular).
No drift, no cap.
How this question surfaced: user asked "we know how retail OPENs it
from above, how hard can it be to know how to open it from below?"
that reframing made the question "what's different about our state
when walking up vs down?" The answer: **nothing, actually the
cottage geometry is the same. But our contact plane is wrong.** The
six prior fix attempts were all investigating the cap-event mechanics
(step-up, slope projection at the cap, edge-slide, SidesType, +X
residual). None questioned why the contact plane was the ramp at all
when the player was 10 m from the ramp.
**Next-session move:** verify the stale-contact-plane hypothesis
chronologically against the live capture (walk the JSONL records, find
the last tick the player was on the actual ramp, quantify Z drift),
then locate the walkable-refresh code path in
`Transition.FindEnvCollisions` / `SpherePath.SetWalkable` that's
supposed to detect a new walkable polygon under the sphere and
overwrite the contact plane. Retail decomp anchor:
`CObjCell::find_env_collisions`. Full pickup prompt at the bottom of
[`docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md`](docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md).
**A6.P4 door bug `pos_hits_sphere` near-miss recording shipped
2026-05-25 PM** (commit `3253d84`). Single-line ordering fix in
`BSPQuery.PosHitsSphere`: `if (hit) hitPoly = poly;` now precedes the
front-face cull, matching retail's `CPolygon::pos_hits_sphere` at
`acclient_2013_pseudo_c.txt:322974-322993` where `*arg5 = this` fires
on static-overlap BEFORE `dot(N, movement) >= 0 → return 0`. With this
ordering, Path 5's existing `if (hitPoly0 is not null)` near-miss
branch (`BSPQuery.cs:1869`) finally fires `NegPolyHitDispatch`
sets `path.NegPolyHit`, the outer `transitional_insert` loop dispatches
via `slide_sphere`, and the sphere slides along walls it's touching
instead of squeezing through. The handoff hypothesized swept-sphere +
closest-considered-polygon tracking; reading retail showed both
`pos_hits_sphere` and `polygon_hits_sphere_slow_but_sure` are STATIC
tests using motion only for the cull the fix is just the ordering.
3 new REDGREEN unit tests in `BSPQueryTests.FindCollisions_Path5_*`
cover: overlap + parallel motion (REDGREEN), overlap + away motion
(REDGREEN), overlap + into motion (regression guard, already passed).
Zero regressions in full Core suite with-fix failure set is a strict
subset of baseline (14 vs 17, the 14 are pre-existing static-leak
flakiness + 2 stale-capture document-the-bug tests). Issue #98
`LiveCompare_FirstCap_FixClosesCottageFloorCap` regression test
passes. **Needs visual verification at Holtburg cottage door inside-
out off-center ~50 cm scenario** before A6.P4 is marked complete
sphere should block at the door surface with no squeeze-through. The
"runs a bit into the door" over-penetration symptom is hypothesized
to close together with the squeeze-through (continuous near-miss
recording while approaching a wall means the sphere slides along it
substep-by-substep rather than catastrophically penetrating then
recovering), but separate investigation if the symptom persists.
Original demo scenario (Holtburg Sewer end-to-end) is unreachable: sewer
doesn't exist on this server, and **issue #95** (portal-graph visibility
blowup) blocks any substitute dungeon. Revised M1.5 demo split into
building/cellar half (PARTIALLY ACHIEVABLE post-slice-1; cellar-ascent
blocked on #98) + dungeon half (blocked on #95). Issues in scope: #80,
#81, #83, #88, #90 (workaround removal after slice 3), **#95**
(visibility; not A6 scope), **#96** (L622 seed; retail divergence
accepted), **#97** (phantom collisions; may close as #98 side-effect),
**#98** (cellar-ascent stuck; A6.P3 slice 3 target), L-indoor,
L-spotlight, indoor sling-out (Finding 3 family with #98), and the
`TryFindIndoorWalkablePlane` definition deletion (A6.P4). **M2
("Kill a drudge") is deferred until M1.5 lands.** Full M1.5 writeup at
the corresponding block in `docs/plans/2026-05-12-milestones.md`.
**A6.P8 — Mesh-AABB-fallback phantom suppression for GfxObj-only stabs — SHIPPED 2026-05-25.**
Three commits: `f6305b1` (PhysicsDataCache.IsPhantomGfxObjSource + 3 unit tests),
`5240d65` (GameWindow.cs wire-in at line 6127), `6ca872f` (test-class doc
line-ref sync from code review). Issue #101 CLOSED the 10 phantom stair
cyls on the Holtburg upper-floor cottage staircase are gone; collision
falls through to entity `0x40B50089` (GfxObj `0x01000C16`, `hasPhys=True`
BSP with walkable inclined polygon at `Normal.Z=0.717`, world ramp from
(111.10, 25.50, 94.00)→(107.50, 27.10, 97.50)). Visual-verified end-to-end
2026-05-25: holding W continuously climbs Z=94→97.5 over the full 45°
ramp; no phantom diagonal slides (`[cyl-test]` count on `obj=0x40B500*`
post-fix = 0 vs 7101 pre-fix). Spec:
[`docs/superpowers/plans/2026-05-25-issue-101-stairs-cyl-phantom.md`](docs/superpowers/plans/2026-05-25-issue-101-stairs-cyl-phantom.md).
**Issue #100 Transparent ground around buildings SHIPPED 2026-05-25 (primary acceptance);
visibility-culling follow-up handed off.** Three commits: `f48c74a` (terrain shader Z nudge,
retail `zFightTerrainAdjust = 0.00999999978` applied per-vertex in `terrain_modern.vert`),
`a64e6f2` (removed ~50 LOC of `hiddenTerrainCells` / `BuildingTerrainCells` plumbing across
LandblockMesh / LoadedLandblock / LandblockLoader / GameWindow / GpuWorldState /
LandblockStreamer + 2 dead tests), `84e3b72` (docs SHA stabilization follow-up).
Visual-verified 2026-05-25 PM at Holtburg: 24m × 24m transparent rectangles around
every cottage are GONE; ground reads as continuous cobblestone / grass. Plan:
[`docs/superpowers/plans/2026-05-25-issue-100-terrain-cutout.md`](docs/superpowers/plans/2026-05-25-issue-100-terrain-cutout.md);
predecessor research [`docs/research/2026-05-25-issue-100-terrain-cutout-handoff.md`](docs/research/2026-05-25-issue-100-terrain-cutout-handoff.md).
**Secondary finding from visual verification:** outdoor terrain mesh visible inside
cottage cellars at certain camera angles (clears when camera moves closer; gameplay
unaffected). High-confidence root cause: **indoor-cell visibility culling not gating
outdoor terrain** same family as filed issue #78 (outdoor stabs visible through inn
floor) and #95 (dungeon portal-graph blowup). Per user direction, NOT filed as a new
issue; treated as additional evidence for #78. Next session investigates + ports
retail's `CEnvCell::find_visible_child_cell` (decomp anchor
`acclient_2013_pseudo_c.txt:311397`) and/or WB's `RenderInsideOut` stencil pipeline.
Full handoff with pickup prompt:
[`docs/research/2026-05-25-issue-100-shipped-and-culling-handoff.md`](docs/research/2026-05-25-issue-100-shipped-and-culling-handoff.md).
**Today's pre-M1.5 baseline (2026-05-20).** Five surgical fixes
shipped to close the user-reported "logged in inside the inn, ran
through walls" bug: A4 (multi-cell BSP iteration, `691493e`),
#89 (sphere-overlap in CheckBuildingTransit, `7ac8f54`),
#90 (sphere-overlap stickiness in ResolveCellId, `4ca3596` — WORKAROUND,
flagged for removal in A6.P4), #91 (indoor cell shadows in
FindObjCollisions, `c0d8405`), #92 (server cell id at player-mode
entry, `23ab173`). 1147 + 8 baseline maintained throughout. Walls
+ furniture block correctly at Holtburg inn and surrounding cottages
as of visual verification 2026-05-20. M1.5 starts from this baseline.
**M2 ("Kill a drudge") — deferred.** Equip a sword, walk to a drudge,
swing, see damage in chat, watch the swing animation, drudge dies
and drops loot, pick up the loot, open inventory and see it. Phases
to ship after M1.5: F.2 (Inventory panel), F.3 (Combat math + damage
flow), F.5a (visible-at-login dev panels Attributes / Skills /
Equipped / Inventory list, minimal ImGui), L.1c (combat animation
wiring), L.1b (command router prereq). ~610 weeks once M1.5 lands.
**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:**
1. **One active milestone at a time.** Work that isn't on the critical
path to M1 gets filed in `docs/ISSUES.md` with a `post-M1` tag 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."
2. **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.
3. **Crossing a milestone is a textual event, not a video event.**
When a milestone's demo scenario is functionally complete, update
`2026-05-12-milestones.md` with a one-paragraph writeup describing
what works end-to-end, flip the freeze list, and update the
"currently working toward" line in this CLAUDE.md to the next
milestone. Do NOT ask the user to record a demo video they find
it pointless. The milestones doc + the CLAUDE.md flip ARE the
milestone artifact. Phases ship; milestones land.
4. **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.
**Indoor walking Phase 2 Portal-based cell tracking shipped
2026-05-19.** Six commits:
- `1969c55` CellBSP + Portals wired into CellPhysics (`PortalInfo` struct, `VisibleCellIds`)
- `aad6976` `CellTransit.FindCellList` + `FindTransitCellsSphere` + `AddAllOutsideCells`; `ResolveCellId` rename
- `069534a` `BuildingPhysics` + `CheckBuildingTransit` for outdoorindoor entry via `BldPortalInfo`
- `702b30a` code-review polish (DRY cell-id derivation, `PortalFlags.ExactMatch` enum, docs)
- `3ffe1e4` critical fix: pass foot-sphere center (`GlobalSphere[0].Origin`) not `CheckPos` to `ResolveCellId`
- `eb0f772` `TryFindIndoorWalkablePlane` synthesizes indoor walkable plane from cell floor poly
**#86** (click selection penetrates walls) **CLOSED** (Phase 1 Cluster A).
**#84** (blocked by air indoors) **FULLY CLOSED.** Spawn-in-building variant
closed by Phase 1 (Phase D AABB containment). Wall-block-from-inside variant
closed by Phase 2 (portal-graph traversal).
**#85** (pass through walls outsidein) **CLOSED** by Phase 2.
`CheckBuildingTransit` promotes CellId via the building-shell portal graph
on outdoorindoor entry; indoor-BSP collision fires from both sides.
**#87** (indoor portal-based cell tracking) **CLOSED** by Phase 2.
**#88** (indoor static objects vibrate) **FILED** (pre-existing, Medium).
**#89** (port `BSPQuery.SphereIntersectsCellBsp`) **FILED** (Low, documented
approximation in `CheckBuildingTransit`).
Diagnostic infrastructure: `[indoor-bsp]`, `[cell-cache]`, `[cell-transit]`,
`[check-bldg]` probes all stay in place.
Handoff: [`docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md`](docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md).
Phase 1 handoff: [`docs/research/2026-05-19-cluster-a-shipped-handoff.md`](docs/research/2026-05-19-cluster-a-shipped-handoff.md).
**Indoor walking Phase A4 — Multi-cell BSP iteration shipped 2026-05-20.**
Three commits land the slices (with one revert/reapply during visual
verification proving A4 wasn't the cause of the bug that surfaced):
- `e6369e2` `CellTransit.FindCellSet` overload exposes the candidate set
- `493c5e5` `Transition.CheckOtherCells` + `ApplyOtherCellResult` combine helper
- `691493e` wire `CheckOtherCells` into `FindEnvCollisions` (orig `967d065`, revert `3add110`, reapply)
Ports retail's `CTransition::check_other_cells` at
`acclient_2013_pseudo_c.txt:272717-272798`. After the primary cell's BSP
returns OK, every other cell the foot-sphere overlaps is queried. Halt
on first Collided/Adjusted/Slid; Slid clears the contact-plane fields.
10 new unit tests; 1139 + 8 baseline maintained.
**Visual verification surfaced a separate, pre-existing M2 blocker**:
at the Holtburg inn doorway, the CellId ping-pongs between outdoor
`0xA9B40022` and indoor vestibule `0xA9B40164` every few ticks. Indoor
BSP DOES detect walls (Collided/Adjusted/Slid fire on push-back), but
the push-back exits the indoor CellBSP volume ResolveCellId
reclassifies as outdoor wall checks bypassed on outdoor ticks net
appearance "walls walk through." Bug reproduces fully with A4 reverted
(see `launch-revert2.log`), confirming A4 is not the cause. A4 is
correct and tested but **dormant in practice** until the ping-pong is
fixed. Handoff:
[`docs/research/2026-05-20-phase-a4-shipped-cell-pingpong-finding.md`](docs/research/2026-05-20-phase-a4-shipped-cell-pingpong-finding.md).
**Next: cell-tracking ping-pong fix.** Retail oracle:
`acclient_2013_pseudo_c.txt:308742-308783` (`CObjCell::find_cell_list`
Position-variant). Look for the cell-array hysteresis / stickiness
logic that prevents flipping CellId on a single push-back. Likely
modifies `PhysicsEngine.ResolveCellId` to prefer the previous indoor
classification when the sphere is close to the indoor CellBSP volume.
**Next phase is Claude's choice** per work-order autonomy. Candidates:
M2 critical path (F.2 / F.3 / F.5a / L.1c / L.1b kill-a-drudge demo);
or the pre-existing "next phase candidates" list below.
**Previously in Phase L.2 (Movement & Collision Conformance).** L.2a slices
1+2+3 + L.2d slice 1+1.5 + L.2g slice 1 + L.2g slice 1b + L.2g slice 1c +
**Phase B.4b** + **Phase B.4c** all shipped and visual-verified 2026-05-13;
**Phase B.5** (ground-item pickup, F-key) shipped and visual-verified
2026-05-14. The M1 demo target *"pick up an item"* is met for the
close-range path single-click a ground item to select, walk within
~0.6 m of it, press F, and the item is removed from the world and added
to the player's inventory. Wire chain: `InteractRequests.BuildPickUp`
sends `PutItemInContainer (0xF7B1/0x0019)`; ACE despawns the item with
`GameMessagePickupEvent (0xF74A)` (NOT `0xF747 DeleteObject` the
distinction surfaced during visual testing and is fixed by the new
`PickupEvent.cs` parser routed through the shared `EntityDeleted`
event). The M1 demo target *"open the inn door"* remains met from B.4b
+ B.4c. Issue #57 (B.4 handler gap) is closed. Issue #58 (door swing
animation) is closed by B.4c. Issues #61 (linkcycle boundary flash),
#62 (PARTSDIAG null-guard), **#63 (server-initiated MoveToObject
auto-walk not honored blocks out-of-range pickup / Use)**, and **#64
(local-player pickup animation does not render)** are filed as
M1-deferred follow-up.
**B.5 ship handoff:** [`docs/research/2026-05-14-b5-shipped-handoff.md`](docs/research/2026-05-14-b5-shipped-handoff.md)
full evidence for the 5 commits across InteractRequests / GameWindow / WorldSession + the bonus `PickupEvent (0xF74A)` wire-handler fix that closes the despawn gap.
**B.4c ship handoff:** [`docs/research/2026-05-13-b4c-shipped-handoff.md`](docs/research/2026-05-13-b4c-shipped-handoff.md)
full evidence for the 4 commits + 2 bonus discoveries (stance-value wrong
`0x01` vs `0x3D` causing underground doors; linkcycle boundary flash).
**B.4b ship handoff:** [`docs/research/2026-05-13-b4b-shipped-handoff.md`](docs/research/2026-05-13-b4b-shipped-handoff.md)
full evidence for the 9 commits + 4 bonus discoveries (double-click dead
code, DoubleClick gate, CollisionExemption, ServerGuidId translation).
**L.2g slice 1 ship handoff:** [`docs/research/2026-05-12-l2g-slice1-shipped-handoff.md`](docs/research/2026-05-12-l2g-slice1-shipped-handoff.md).
**L.2d ship handoff:** [`docs/research/2026-05-13-l2d-slice1-shipped-handoff.md`](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` (FarNear promotion) for OnCreate,
`RemoveLandblock` + `RemoveEntitiesFromLandblock` (NearFar 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`](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](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`](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 3050× 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`](docs/plans/2026-05-11-phase-n6-perf-baseline.md).
Plan archived at [`docs/superpowers/plans/2026-05-11-phase-n6-slice1.md`](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):**
- **"Click an NPC" verification spike (M1 critical path).** B.4b's
`WorldPicker` + `BuildUse` is already wired. The question is whether ACE
NPCs respond to a Use message from our testaccount and what they broadcast
back (TalkDirect? MoveToObject?). Spike: stand near a Holtburg NPC,
double-click, read what ACE sends back. If ACE responds with recognizable
packets, wire the handlers; if it is silent, investigate ACE's NPC handler
configuration. ~30 min spike, outcome determines whether NPC interaction
needs a full phase or is a one-commit fix.
- **Phase B.6 Client-side MoveToObject auto-walk handling (closes #63).**
ACE auto-walks the player to out-of-range Use / Pickup targets via
`CreateMoveToChain` + `EnqueueBroadcastMotion(MoveToObject)`, but our client
doesn't honor the inbound motion broadcast character drifts toward the
target and snaps back, ACE's chain times out. Reference implementation
exists in `references/holtburger/crates/holtburger-core/src/client/simulation.rs`
(the `approximate_move_to_object_projection_target` + `MoveToObject` case).
Unlocks double-click pickup, F-key pickup from any distance, Use on
out-of-range NPCs / corpses. Probably 1-2 commits + visual verification.
- **Triage the chronic open-issue list** in `docs/ISSUES.md` #2 (lightning),
#4 (sky horizon-glow), #28 (aurora), #29 (cloud thinness), #37 (humanoid
coat), #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.
- **Issue #61 AnimationSequencer linkcycle boundary flash** (M1-deferred
polish). Brief flap at end of door-swing animations. Low severity; does
not block M1 demo. Address before milestone demo record if distracting.
- **Issue #62 PARTSDIAG null-guard** (trivial latent fix). One-line
null-coalescing guard in `GameWindow.TickAnimations`. Address any time a
diagnostic-related PR is open nearby.
**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 6366 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:**
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 after a disconnect, and the duration
depends on HOW the client exited.** Two cases:
1. **Graceful close (client sent logout packet to ACE):** session clears
in ~35 seconds. Wait briefly between launches.
2. **Hard kill (Stop-Process, crash, force-close):** no logout packet
reached ACE. ACE keeps the session marked logged-in until its own
timeout observed in practice at ~3+ minutes. Subsequent relaunches
fail with `live: session failed: CharacterList not received` (exit 29)
the entire time. **There is no admin command available to us to kick
the stale session.** Either wait it out, or use the graceful path
below.
**Prefer the graceful close path when ending a launch.** PowerShell's
`Stop-Process` is a hard kill it bypasses the client's shutdown hook
which is where the logout packet would have been sent. The graceful
alternative sends WM_CLOSE so the window's close handler runs:
```powershell
$proc = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue
if ($proc) {
$proc.CloseMainWindow() | Out-Null
if (-not $proc.WaitForExit(5000)) {
# Fell through to hard-kill — session WILL be stuck on ACE.
$proc | Stop-Process -Force
}
}
Start-Sleep -Seconds 3
# ... then launch ...
```
If `WaitForExit(5000)` returns false (the client didn't exit in 5 seconds
after WM_CLOSE), the client is unresponsive and a hard kill is the only
option accept that ACE will be unhappy for a few minutes.
**When recovering from a hard-killed session that ACE still considers
active:** the only honest answer is to wait. Don't bother retrying every
30 seconds make a single retry attempt ~3 minutes after the kill, and
if it still fails wait another 2 minutes before trying again. The user
will likely volunteer when ACE has cleared the session if you ask.
The user has repeatedly confirmed this don't treat exit-29-after-relaunch
as a code bug. It's a server-side session-cleanup delay whose duration is
governed by whether the previous shutdown was graceful or forced.
### 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.
- `ACDREAM_PROBE_RESOLVE=1` L.2a slice 1+2+3 (2026-05-12). One
`[resolve]` line per `PhysicsEngine.ResolveWithTransition` call:
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 if `ACDREAM_DEVTOOLS=1`.
- `ACDREAM_PROBE_CELL=1` L.2a slice 1 (2026-05-12). One
`[cell-transit]` line per `PlayerMovementController.CellId`
change: old new cell, world position, reason tag
(`resolver` / `teleport`). Low volume only fires on actual cell
crossings. Runtime-toggleable via the same DebugPanel section.
- `ACDREAM_PROBE_PUSH_BACK=1` A6.P1 cdb probe spike (2026-05-21).
Emits three line types per physics tick: `[push-back]` (per
`BSPQuery.AdjustSphereToPlane` call), `[push-back-disp]` (per
`BSPQuery.FindCollisions` dispatch), `[push-back-cell]` (per
`Transition.CheckOtherCells` off-cell hit). Heavy under motion
(~100500 lines/sec). Pair with retail's cdb breakpoint set at
`tools/cdb/a6-probe.cdb` for the A6.P1 capture protocol.
Runtime-toggleable via the DebugPanel "Diagnostics" section.
- `ACDREAM_CAPTURE_RESOLVE=<path>` A6.P3 #98 live capture of every
player-side `PhysicsEngine.ResolveWithTransition` call (2026-05-23 PM
apparatus). Each call appends one JSON Lines record with full inputs,
PhysicsBody snapshot before AND after, plus the `ResolveResult`.
Filtered to `IsPlayer` mover flag NPC / remote DR calls don't
pollute. Pairs with the trajectory replay harness comparison test
(`CellarUpTrajectoryReplayTests.Capture_*`) to diff captured vs harness
state per field the first divergence pinpoints missing apparatus
state. Capture is OFF when the env var is unset (one null-check
cost per call).
- *(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.42.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 (RunWalk
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 RunWalk 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 510 Hz UpdatePosition bursts (dead-reckoning).
## Reference repos: cross-check the relevant ones
The `references/` tree holds **six** vendored projects (ACE, ACViewer,
WorldBuilder, Chorizite.ACProtocol, holtburger, AC2D). They overlap in
some areas and disagree in others. Before committing to an approach,
**cross-reference at least two of them** for the domain you're working
in the per-domain hierarchy in the next section tells you which to
read first. A single reference can be misleading; the intersection of
the relevant references 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.
The six references:
- **`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.** WorldBuilder is not just a reference: as of Phase N.4 (shipped
2026-05-08), `ObjectMeshManager` is the production mesh pipeline,
`WbMeshAdapter` is the seam, and `WbDrawDispatcher` is the production
draw path. The modern path (`N.5`) is **mandatory** missing bindless
throws at startup, there is no legacy fallback. **Before re-porting
any rendering or dat-handling algorithm from retail decomp, read
`docs/architecture/worldbuilder-inventory.md` first.** The inventory
tells you what WB covers (terrain, scenery, static objects, EnvCells,
portals, sky, particles, texture decoding, mesh extraction,
visibility/culling) and what we still write ourselves (the 🔴 list:
network, physics, animation, movement, UI, plugin, audio, chat).
WorldBuilder is MIT-licensed and exact-stack with us (Silk.NET +
.NET); the divergences we've documented (e.g. WB's terrain split
formula vs retail's `FSplitNESW`) are called out in the inventory
doc.
- **`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).