Plan: 7 tasks decomposing spec T2..T9 with bite-sized TDD-style steps, exact file paths, commit-message templates, and a T4 safety-check branch (refactor in place if ObjectMeshManager._dats call sites <=20; fall back to thin adapter otherwise). Spec fix: §4.1 mesh-pipeline files now correctly placed under src/AcDream.App/Rendering/Wb/ instead of Core (ObjectMeshManager uses Silk.NET.OpenGL types from Managed* wrappers, and CLAUDE.md forbids Core depending on GL). §4.2's layer split (TextureHelpers in Core, rest in App) was already correct. Plan task order: T2 (setup) -> T5 (Core helpers, lowest risk) -> T3 (App GL infra) -> T4 (App mesh pipeline + dat-shim) -> T7 (drop refs + cleanup) -> T8 (visual verification) -> T9 (ship). T5 moved earlier than spec order to validate the namespace migration flow on small-blast-radius files before the load-bearing T4. Self-review: all 12 spec decisions (O-D1..O-D12) mapped to plan tasks; placeholders intentional + explained (MIT license body fetched at T2 step 4; commit-message parameters filled at task close). Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
25 KiB
Phase O — DatPath Unification — Design Spec
Filed: 2026-05-21
Status: ACTIVE
Amended: 2026-05-21 (post O-T1 audit; see docs/research/2026-05-21-phase-o-t1-wb-audit.md)
Estimated effort: ~7-8 working days, one ship-window.
Tagline: ONE thing touches the DATs. Today we have two readers in process (acdream's
DatCollection+ WorldBuilder'sDefaultDatReaderWriter) reading the same files independently. Phase O collapses that to one.
Amendment summary (2026-05-21): O-T1 audit revealed three components the original spec listed (
LandSurfaceManager,EnvCellRenderManager,PortalRenderManager) plus the open §9.2 question onTerrainRenderManagerare NOT in acdream's actual call graph — we already have our own ports or never used them. T6 is eliminated entirely; T5 shrinks to stateless helpers only. T4 grows by 0.5d to include a refactor ofObjectMeshManagerto takeDatCollectiondirectly (avoiding a permanent adapter indirection). Net effort estimate unchanged. Open question O-Q1 (thread-model) closed: verified safe — no worker-thread access to WB code.
1. Problem statement
As of Phase N.4 ship (2026-05-08), WorldBuilder is integrated as our rendering + dat-handling base. Concretely:
WbMeshAdapter.cs:79constructs_wbDats = new DefaultDatReaderWriter(datDir)— WB's own dat reader.- Our
DatCollectionis constructed independently at startup for the rest of the client (network, physics, animation, clothing, audio, UI, etc.).
Both readers open the same four files (client_portal.dat,
client_cell_1.dat, client_highres.dat, client_local_English.dat)
with independent file handles and independent in-memory index caches.
Costs:
- ~50-100 MB duplicated index cache memory (per
WbMeshAdapter.cs:27comment). - Double seek cost when the same dat block is read by both pipelines (mesh path via WB; surface metadata side-table via our
DatCollection). - Cross-check awkwardness —
WbMeshAdapter.cs:224-262has explicit "if WE find the cell but WB doesn't" diagnostic code, born from the divergence. - Architectural smell — a third-party feedback signal (AC community comment, 2026-05-21) flagged WB's dat-touching as "built for tools, not runtime."
WB is MIT-licensed, so the path to fixing this is to extract its
load-bearing code into our repo and route it through our DatCollection.
One reader, one cache, one source of dat truth.
2. Decisions log
| # | Decision | Why |
|---|---|---|
| O-D1 | Extract WB code verbatim into our repo. No re-port from retail decomp. No "improvements" while extracting. Discipline applies to algorithms (meshing math, texture decode, particle pipeline) — NOT to mechanical changes like parameter type renames. | CLAUDE.md is explicit: "Re-porting from retail decomp when WB already has a tested port is how subtle bugs (scenery edge-vertex, triangle-Z) keep slipping in." Verbatim copy preserves all the corner-case fixes WB already has. |
| O-D2 | Make extracted code consume DatCollection, not IDatReaderWriter. |
Single source of dat truth. The whole point of the phase. |
| O-D3 | Drop the WorldBuilder.Shared + Chorizite.OpenGLSDLBackend project references at the end of the phase. |
If we still reference WB after extraction, we haven't actually finished the work. |
| O-D4 | Keep WB in references/ for reading/comparison. Don't delete the vendored directory. |
We'll still want to grep WB during ports of NEW pieces (e.g., minimap renderer if/when we add it). |
| O-D5 | MIT attribution per WB convention. Add NOTICE.md entry crediting WorldBuilder for the extracted code. |
License compliance. |
| O-D6 | One ship-window, not sliced. Either the whole extraction lands and references/WorldBuilder is dropped, or we roll back the entire phase. |
Half-extracted state (some WB code in our repo, some still referenced) is worse than either endpoint. |
| O-D7 | Refactor ObjectMeshManager to take DatCollection directly (not via an adapter). Safety check at T4 — fall back to thin DatCollectionAdapter : IDatReaderWriter if _dats.X call-site count inside ObjectMeshManager exceeds 20. |
After extraction, ObjectMeshManager is OUR code; our code should use our types. An adapter would be permanent tech debt obscuring data flow. O-D1's "verbatim copy" discipline applies to algorithms, not parameter types. |
| O-D8 | Drop four originally-listed components from the extract list: LandSurfaceManager, EnvCellRenderManager, PortalRenderManager, TerrainRenderManager. |
O-T1 audit confirmed these aren't reachable from acdream's code graph. LandSurfaceManager and TerrainRenderManager have our own ports (TerrainBlending.cs, TerrainModernRenderer.cs); EnvCell/Portal are rendered via the mesh pipeline, not via WB's dedicated renderers. |
| O-D9 | Promote 3 internal types in Chorizite to public when extracted: EmbeddedResourceReader, TextureFormatExtensions, BufferUsageExtensions. |
We vendor them; we control the namespace. Keeping internal would force same-assembly placement with no benefit. |
| O-D10 | Strip [MemoryPackable] from TerrainEntry when copying into our tree. |
We don't serialize the struct. Avoids adding MemoryPack as a NuGet dep for an unused attribute. |
| O-D11 | Namespace AcDream.Core.Rendering.Wb.* for extracted code (vs topic-based namespaces). |
Preserves the "this came from WB" audit trail. A later phase can re-organize once the dust settles. |
| O-D12 | Drop ResolveId(uint) and the [indoor-upload] NULL_RESULT diagnostic block in WbMeshAdapter.cs at T7. |
Only caller of ResolveId is the diagnostic; the diagnostic depends on the second _wbDats which goes away. The block has served its Phase 2 cell-resolution-divergence investigation purpose. |
3. Architecture overview
Today (Phase N.4–A.5 state)
┌─────────────────────────┐ ┌──────────────────────────────────┐
│ acdream subsystems │ │ WorldBuilder (referenced project)│
│ - Network │ │ ┌────────────────────────────┐ │
│ - Physics │ ───→ │ │ DefaultDatReaderWriter │ │
│ - Animation │ │ │ (opens 4 .dat files) │ │
│ - Clothing/Audio/UI │ │ └────────────┬───────────────┘ │
│ - Surface metadata │ │ │ │
└──────────┬──────────────┘ │ ┌────────────▼───────────────┐ │
│ │ │ ObjectMeshManager │ │
│ DatCollection │ │ TextureHelpers │ │
│ (opens same 4 files) │ │ SceneryHelpers │ │
│ │ │ OpenGLGraphicsDevice │ │
▼ │ └────────────────────────────┘ │
┌──────────────┐ └──────────────────────────────────┘
│ Dat files │ ▲
│ (same files, │──────────────┘
│ two readers)│
└──────────────┘
Phase O target
┌─────────────────────────────────────────────────────────────────┐
│ acdream subsystems │
│ - Network / Physics / Animation / Clothing / Audio / UI │
│ - Mesh pipeline (extracted from WB.ObjectMeshManager, │
│ refactored to take DatCollection) │
│ - Texture decode (extracted from WB.TextureHelpers) │
│ - Scenery helpers (extracted from WB.SceneryHelpers) │
│ - Terrain helpers (extracted from WB.TerrainUtils + TerrainEntry)│
│ - GL infra (extracted from WB.OpenGLGraphicsDevice etc.) │
└──────────────────────────┬──────────────────────────────────────┘
│
│ DatCollection (the ONLY reader)
▼
┌──────────────┐
│ Dat files │
└──────────────┘
What stays acdream-original (unchanged by this phase)
TerrainModernRenderer.cs(Phase N.5b — usesLandblockMesh.Buildwith retail'sFSplitNESW).TerrainBlending.cs(our port of WB'sLandSurfaceManager— already lives in acdream).- Network, physics, animation, movement, UI, audio, chat, streaming controller, plugin API.
- Our
DatCollection(becomes the only dat reader). - The
Wb*adapter layer (WbMeshAdapter,WbDrawDispatcher,LandblockSpawnAdapter,EntitySpawnAdapter, etc.) — those stay inAcDream.App/Rendering/Wb/; they bridge our world to the extracted code.
4. Component changes (audit-corrected)
4.1 Mesh pipeline (T4 — the load-bearing extraction)
Layer placement: mesh-pipeline files live under
src/AcDream.App/Rendering/Wb/(NOT Core), becauseObjectMeshManagerand its supports useSilk.NET.OpenGLtypes directly (ManagedGL*, GLSLShader, etc.) and CLAUDE.md forbids Core depending on GL.
| WB source | New acdream home | Adaptation |
|---|---|---|
Chorizite.OpenGLSDLBackend.Lib.ObjectMeshManager |
src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs |
Refactor: replace IDatReaderWriter field/ctor-param with DatCollection (Core type — App can reference it). Update _dats.X call sites. Safety check at T4: if >20 sites, fall back to thin adapter. |
Chorizite.OpenGLSDLBackend.Lib.ObjectRenderBatch + ObjectRenderData |
Same directory | Verbatim copy. |
| Particle batcher + emitter (T4 supports) | Same | Verbatim copy. |
Chorizite.OpenGLSDLBackend.Lib.DebugRenderSettings |
Same | Verbatim copy (constructor parameter type only). |
GlobalMeshBuffer, modern render data structs |
Same | Verbatim copy. |
4.2 Texture pipeline + GL infrastructure (T3)
Layer split:
TextureHelpersis pure (no GL, no dat) → goes to Core. All other GL infra → App.
| WB source | New acdream home | Adaptation |
|---|---|---|
Chorizite.OpenGLSDLBackend.Lib.TextureHelpers |
src/AcDream.Core/Rendering/Wb/TextureHelpers.cs |
Verbatim. Pure functions — no dat / no GL. |
Chorizite.OpenGLSDLBackend.OpenGLGraphicsDevice |
src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs |
Verbatim. Touches GL. |
ManagedGL{Texture,TextureArray,VertexBuffer,IndexBuffer,VertexArray,FrameBuffer,UniformBuffer} |
App Wb/ directory |
Verbatim. Touches GL. |
GLSLShader, GLHelpers, GLStateScope |
App Wb/ directory |
Verbatim. Touches GL. |
EmbeddedResourceReader (internal → public) |
App Wb/ directory |
Promote internal → public. |
TextureFormatExtensions, BufferUsageExtensions (internal → public) |
App Wb/ directory |
Promote internal → public. |
4.3 Stateless helpers (T5)
| WB source | New acdream home | Adaptation |
|---|---|---|
Chorizite.OpenGLSDLBackend.Lib.SceneryHelpers |
src/AcDream.Core/Rendering/Wb/SceneryHelpers.cs |
Verbatim. Pure functions. |
WorldBuilder.Shared.Modules.Landscape.Lib.TerrainUtils |
src/AcDream.Core/Rendering/Wb/TerrainUtils.cs |
Verbatim. |
WorldBuilder.Shared.Modules.Landscape.Models.TerrainEntry |
Same | Verbatim except strip [MemoryPackable]. |
WorldBuilder.Shared.Modules.Landscape.Models.CellSplitDirection |
Same | Verbatim enum. |
4.4 NOT extracted (dropped from spec §4)
Audit confirmed these are not in acdream's reachable closure. Documented here for posterity so the next person looking at the spec knows why they're missing.
| Component | Why not extracted |
|---|---|
LandSurfaceManager |
Already ported as src/AcDream.Core/Terrain/TerrainBlending.cs. |
SceneryRenderManager |
Acdream uses only the stateless SceneryHelpers. The render pipeline is WbDrawDispatcher. |
EnvCellRenderManager |
Acdream renders env cells via ObjectMeshManager.PrepareEnvCellMeshData + WbDrawDispatcher. |
PortalRenderManager |
Same — portal cells go through the same path. |
TerrainRenderManager |
Already replaced by src/AcDream.App/Rendering/TerrainModernRenderer.cs (Phase N.5b). |
FontRenderer, MinimapRenderer, BackendGizmoDrawer, AudioPlaybackEngine |
Editor-only or replaced by our own subsystems. |
WorldBuilder.Shared/Hubs, Migrations, Repositories, editor Services |
Editor-only (SignalR, EF Core). |
4.5 What we DROP outright
_wbDats = new DefaultDatReaderWriter(datDir)inWbMeshAdapter.cs:79— replaced with the existing_dats: DatCollectionfield passed straight toObjectMeshManager.- The
[indoor-upload] NULL_RESULTcross-check block atWbMeshAdapter.cs:224-262and the_pendingEnvCellRequeststracker. Phase 2 cell-resolution diagnostic; no longer needed. _wbDats?.Dispose()inWbMeshAdapter.Dispose().- The two
<ProjectReference>entries inAcDream.App.csproj:38-39andAcDream.Core.csproj:27-28toWorldBuilder.Shared+Chorizite.OpenGLSDLBackend. - The two
WorldBuilder.Shared/Services/{IDatReaderWriter,DefaultDatReaderWriter}.csfiles — never copied into our repo. tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs— one-time data-collection test that informed the N.5b path-C decision; job done.
5. Task breakdown
| Task | Description | Effort |
|---|---|---|
| O-T1 | DONE 2026-05-21. Audit WB call graph — produce closure of every WB type/file we transitively use. Output: docs/research/2026-05-21-phase-o-t1-wb-audit.md. |
0.5d ✓ |
| O-T2 | Create src/AcDream.Core/Rendering/Wb/ + src/AcDream.App/Rendering/Wb/ (already exists) directory structure. Add NOTICE.md entry with MIT attribution to WB. |
0.25d |
| O-T3 | Extract texture / GL infrastructure (~15 files, ~3.1K LOC): TextureHelpers, OpenGLGraphicsDevice, ManagedGL*, GLSLShader, GLHelpers, GLStateScope. Promote 3 internal types to public. Verify: does our closure touch SixLabors.ImageSharp? If yes, strip imports / inline byte handling. If no, document. Build green. |
1d |
| O-T4 | Extract mesh pipeline (~8 files, ~3.3K LOC): ObjectMeshManager, ObjectRenderBatch, ObjectRenderData, supports. First 30 min: count _dats.X call sites inside ObjectMeshManager. If ≤20, refactor in place to take DatCollection. If >20, write thin DatCollectionAdapter : IDatReaderWriter and pass that. Document the choice + actual count in the T4 commit. Build green. All existing tests green. |
2.5d |
| O-T5 | Extract stateless helpers (5 files, ~782 LOC): SceneryHelpers (Chorizite), TerrainUtils + TerrainEntry + CellSplitDirection (WB.Shared). Strip [MemoryPackable] from TerrainEntry. Update using lines in SceneryGenerator.cs, WbSceneryAdapter.cs, SurfaceDecoder.cs, and tests/AcDream.Core.Tests/Textures/TextureDecodeConformanceTests.cs to point at the new AcDream.Core.Rendering.Wb.* namespace. Build green. |
0.5d |
| eliminated by O-D8 | ||
| O-T7 | Drop project references from AcDream.App.csproj and AcDream.Core.csproj. Drop _wbDats field + ctor + dispose + the [indoor-upload] NULL_RESULT block from WbMeshAdapter. Delete SplitFormulaDivergenceTest.cs. Build green + tests green (minus the deleted test). |
0.5d |
| O-T8 | Verification gate — visual side-by-side with main against retail in three scenes: Holtburg town (outdoor + scenery), inn interior (EnvCell), and a dungeon (portals). Screenshots captured BEFORE T3 starts, compared after T7. User confirms "looks identical." Measure resident memory at radius=4 + 50 entities visible; confirm ≥50 MB reduction vs main. | 1d (incl. user time) |
| O-T9 | Ship — single merge to main with one descriptive commit per task (T2..T7), then a "drop WB references" final commit. Update CLAUDE.md to remove the "WB is referenced as projects" language and replace with "extracted into src/AcDream.Core/Rendering/Wb/." Update docs/architecture/worldbuilder-inventory.md. |
0.5d |
| Total | ~6.75d focused work + 1d verification + 0.5d ship ≈ 7.75d |
6. Acceptance criteria
Build + test:
dotnet buildgreen across the solution.dotnet testgreen; no regression vs the 1147 + 8 baseline minusSplitFormulaDivergenceTest.cs's test count (delete is deliberate, not a regression).
Reference deletion (the architectural goal):
- Zero references to
WorldBuilder.*orChorizite.OpenGLSDLBackend.*namespaces inAcDream.App.csprojandAcDream.Core.csproj. (PackageReferenceforChorizite.DatReaderWriterstays — that's the NuGet DatReaderWriter lib, not WB.) - Zero
using WorldBuilder.*orusing Chorizite.OpenGLSDLBackend*insrc/AcDream.*(extracted code lives inAcDream.Core.Rendering.Wb.*now). DefaultDatReaderWriterreferenced in exactly zero places in our source.DatCollectionis the only dat reader._wbDatsfield + ctor + dispose removed fromWbMeshAdapter.cs.[indoor-upload] NULL_RESULTblock at lines 224-262 removed.SplitFormulaDivergenceTest.csdeleted.
Memory + visual (user-facing wins):
- Resident memory at
streaming: radius=4+ 50 entities visible: >50 MB reduction vs. pre-Phase-O main. - Visual side-by-side with main: Holtburg town, an inn interior, a dungeon — all render identically. User confirms via screenshots taken BEFORE T3 and AFTER T7.
New per audit:
- T4 commit message documents the
ObjectMeshManagerdat-shim path taken (refactor in place if ≤20 sites, or adapter if >20). With the actual count. - T3 commit message documents the 3 internal-to-public type promotions in Chorizite (
EmbeddedResourceReader,TextureFormatExtensions,BufferUsageExtensions). - T3 commit message states whether
SixLabors.ImageSharpwas reachable. If yes: documents which paths were stripped. If no: explicit "ImageSharp not reachable" note.
Docs + attribution:
NOTICE.mdincludes WB attribution per its MIT license.references/WorldBuilder/directory remains in the repo as a read-reference; not in any csproj.CLAUDE.mdupdated: WB is now a read reference, not a project dependency. The "WB integration cribs" section is rewritten to point at our extracted code.docs/architecture/worldbuilder-inventory.mdupdated to reflect the new ownership.
7. Risks + mitigations (audit-updated)
| Risk | Likelihood | Severity | Mitigation |
|---|---|---|---|
| Re-introduce bugs WB already fixed (edge-vertex, triangle-Z) | Medium | High | Verbatim copy at the algorithm level. The refactor change at T4 is API-shape only; meshing math / texture decode / particle pipeline stay byte-identical. |
ObjectMeshManager refactor reveals >20 _dats.X call sites |
Low-Medium | Medium | T4 safety check. First 30 min of T4 is a grep + count. If threshold breached, fall back to thin DatCollectionAdapter : IDatReaderWriter and document. Either path keeps T4 in its 2.5d budget. |
DatCollection doesn't implement a method ObjectMeshManager calls |
Medium | Medium | T4 audit, fill the gap when found. Add missing methods to DatCollection (we own it) rather than stub them. Bounded — at most a handful of methods. |
SixLabors.ImageSharp shows up in the closure at T3 |
Low | Low | Verify-at-T3 + strip. Grep T3 source on first pass; if found, replace with our existing byte-handling or BCnEncoder calls. |
| Visual regression we don't catch in side-by-side | Low | Medium | Screenshot Holtburg + inn + dungeon BEFORE starting T3. Compare after T7. Don't trust eyeballs alone. |
Loss of [indoor-upload] diagnostic removes useful Phase 2 evidence |
Low | Low | The diagnostic's findings are already documented in commit history and the audit. The block was a one-time probe; its job is done. |
| Hidden transitive WB deps we missed | Low | Low | Audit complete (33 files, ~7.7K LOC, fully bucketed). Build break at T7 would catch any miss. Was Medium pre-audit — reduced to Low. |
| User upstream-tracks WB for some reason | Low | Low | references/WorldBuilder/ stays in-tree as read-reference. Re-sync diffs are manual ports (same as today). |
8. Out of scope (explicitly)
- Re-porting from retail decomp anywhere in the extracted code. We copy WB verbatim. If a retail-faithfulness audit is needed later, file a separate phase.
- Performance optimization of extracted code. Even if WB's
ObjectMeshManagerhas a 30% improvement waiting in it, this phase ships it as-is. - API cleanup of the extracted code beyond the dat-surface change at T4. If the constructor has 8 parameters and it's ugly, ugly it stays. Refactor in a follow-up.
- Refactoring
WbMeshAdapteritself. Phase O drops the second reader; the adapter shape stays. tools/InspectCoatTexor other tools that use the NuGetChorizite.DatReaderWriter. Those keep working — they use the NuGetDatReaderWriter, not the WB project reference.- Re-organizing the extracted namespace. O-D11 picked
AcDream.Core.Rendering.Wb.*; topic-based reorganization is a follow-up phase.
9. Open questions
All originally-open questions have been resolved by the O-T1 audit or this brainstorm:
- O-Q1 (thread-model): CLOSED. Verified safe — adapters run render-thread only; WB's own code uses
ConcurrentDictionary+ locks as defense in depth. See O-T1 audit §6. - O-Q2 (TerrainRenderManager): CLOSED. Confirmed not reachable; we use our own
TerrainModernRenderer.LandSurfaceManageralso not reachable (we have our own port). Both dropped from extract list per O-D8. - O-Q3 (namespace): CLOSED. Adopted
AcDream.Core.Rendering.Wb.*(option A) per O-D11. - O-Q4 (Chorizite.DatReaderWriter NuGet): CLOSED. Stays as NuGet — separate from WB project refs.
- O-Q5 (NEW — SixLabors.ImageSharp): Deferred to T3. Verify reachability during extraction. Strip if found; document if not.
10. Naming + filing
- Phase name: O (letter remains).
- Phase tagline: "ONE thing touches the DATs."
- Roadmap entry: already in
docs/plans/2026-04-11-roadmap.mdunder "currently active". Move to "shipped" at T9. - Issue tracking: filed as a Phase, not an issue (multi-commit, multi-day, infrastructural).
11. After Phase O ships
Things that become unblocked or cheaper:
- Single-cache memory pressure budget. Easier to size streaming radii, MSAA, anisotropic budget.
- Audit a single dat-touching code path instead of two when investigating bugs like the cell-resolution divergence the
[indoor-upload]probe was built to investigate. - Future N.6+ rendering work doesn't have to ask "is this WB's concern or ours?" — it's ours.
- AC community-friendly architecture — the "WB is for tools" criticism is addressed at the structural level.
- A follow-up phase to refactor namespaces from
AcDream.Core.Rendering.Wb.*into topic-based (Meshes.*,Textures.*) becomes possible. Estimated 0.5d when scheduled.