Phase O extracts the WB pieces we actually use (mesh pipeline, texture decode, GL state, scenery, terrain blending, EnvCell/portal decode — roughly 3-5K LOC) into src/AcDream.Core/Rendering/Wb/, swaps their dat dependency from DefaultDatReaderWriter to our DatCollection, and drops the WorldBuilder.Shared + Chorizite.OpenGLSDLBackend project references. WB stays in references/ as a read reference, not as a project dependency. MIT attribution in NOTICE.md. Tagline: ONE thing touches the DATs. Discipline: verbatim copy first, no "improvements" while extracting. Refactors land in follow-up phases. Out of scope: re-porting from retail decomp; perf optimization; API cleanup. User direction 2026-05-21: pre-empts M1.5. M1.5 paused at its 2026-05-20 baseline; A6/A7 don't touch dat infrastructure so no rework needed when it resumes. Files: - docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md (new, full spec) - docs/plans/2026-04-11-roadmap.md (Phase O block inserted before M1.5; M1.5 marked PAUSED) - CLAUDE.md (Currently-working-toward line updated; M1.5 block marked paused) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
237 lines
17 KiB
Markdown
237 lines
17 KiB
Markdown
# Phase O — DatPath Unification — Design Spec
|
||
|
||
**Filed:** 2026-05-21
|
||
**Status:** DRAFT (not yet scheduled)
|
||
**Owner:** TBD
|
||
**Estimated effort:** 1-2 focused weeks of work; one ship-window, no slicing across multiple weeks.
|
||
|
||
> **Tagline**: ONE thing touches the DATs. Today we have two readers in
|
||
> process (acdream's `DatCollection` + WorldBuilder's
|
||
> `DefaultDatReaderWriter`) reading the same files independently. Phase O
|
||
> collapses that to one.
|
||
|
||
---
|
||
|
||
## 1. Problem statement
|
||
|
||
As of Phase N.4 ship (2026-05-08), WorldBuilder is integrated as our
|
||
rendering + dat-handling base. Concretely:
|
||
|
||
- `WbMeshAdapter.cs:79` constructs **`_wbDats = new DefaultDatReaderWriter(datDir)`** — WB's own dat reader.
|
||
- Our **`DatCollection`** is 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:27` comment).
|
||
- **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-229` has 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." The double-reader is the most concrete manifestation.
|
||
|
||
**WB is MIT-licensed**, so the path to fixing this is not to "wrap" WB but to **extract its load-bearing code into our repo** and route it through our `DatCollection`. This is the long-term clean architecture: 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. | 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. |
|
||
|
||
---
|
||
|
||
## 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 │ │
|
||
│ │ │ LandSurfaceManager │ │
|
||
▼ │ │ OpenGLGraphicsDevice │ │
|
||
┌──────────────┐ │ └────────────────────────────┘ │
|
||
│ Dat files │ └──────────────────────────────────┘
|
||
│ (same files, │ ▲
|
||
│ two readers)│──────────────┘
|
||
└──────────────┘
|
||
```
|
||
|
||
### Phase O target
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ acdream subsystems │
|
||
│ - Network / Physics / Animation / Clothing / Audio / UI │
|
||
│ - Mesh pipeline (extracted from WB.ObjectMeshManager) │
|
||
│ - Texture decode (extracted from WB.TextureHelpers) │
|
||
│ - Scenery (extracted from WB.SceneryHelpers etc.) │
|
||
│ - Terrain blend (extracted from WB.LandSurfaceManager) │
|
||
│ - GL state (extracted from WB.OpenGLGraphicsDevice) │
|
||
└──────────────────────────┬──────────────────────────────────────┘
|
||
│
|
||
│ DatCollection (the ONLY reader)
|
||
▼
|
||
┌──────────────┐
|
||
│ Dat files │
|
||
└──────────────┘
|
||
```
|
||
|
||
### What stays acdream-original (unchanged by this phase)
|
||
|
||
- `TerrainModernRenderer` (Phase N.5b — already ours, uses `LandblockMesh.Build` with retail's `FSplitNESW`)
|
||
- Network, physics, animation, movement, UI, audio, chat, streaming controller, plugin API
|
||
- Our `DatCollection` itself (becomes the **only** dat reader)
|
||
|
||
---
|
||
|
||
## 4. Component changes (the actual extract list)
|
||
|
||
### 4.1 Mesh pipeline (highest-value extraction)
|
||
|
||
| WB source | New acdream home | Adaptation |
|
||
|---|---|---|
|
||
| `Chorizite.OpenGLSDLBackend.Lib.ObjectMeshManager` | `src/AcDream.Core/Rendering/Wb/ObjectMeshManager.cs` | Replace `IDatReaderWriter` field with `DatCollection`. Constructor takes our `DatCollection`. All `_dats.Get<T>()` calls preserved. |
|
||
| `Chorizite.OpenGLSDLBackend.Lib.ObjectRenderBatch` + supporting types (`TextureKey`, etc.) | Same directory | Verbatim copy. No interface changes. |
|
||
| `Chorizite.OpenGLSDLBackend.Lib.SceneryHelpers` | `src/AcDream.Core/Rendering/Wb/SceneryHelpers.cs` | Verbatim. |
|
||
| `Chorizite.OpenGLSDLBackend.Lib.SceneryRenderManager` | Same | Verbatim + DatCollection swap. |
|
||
|
||
### 4.2 Texture pipeline
|
||
|
||
| WB source | New acdream home | Adaptation |
|
||
|---|---|---|
|
||
| `Chorizite.OpenGLSDLBackend.Lib.TextureHelpers` | `src/AcDream.Core/Rendering/Wb/TextureHelpers.cs` | Verbatim copy. Pure functions — no dat dependency. |
|
||
| `Chorizite.OpenGLSDLBackend.Lib.LandSurfaceManager` | `src/AcDream.Core/Rendering/Wb/LandSurfaceManager.cs` | Verbatim + DatCollection swap. |
|
||
|
||
### 4.3 GL infrastructure
|
||
|
||
| WB source | New acdream home | Adaptation |
|
||
|---|---|---|
|
||
| `Chorizite.OpenGLSDLBackend.OpenGLGraphicsDevice` | `src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs` | Verbatim. No dat dependency. |
|
||
| `Chorizite.OpenGLSDLBackend.Frustum` + `VisibilityManager` | Same | Verbatim. |
|
||
|
||
### 4.4 EnvCell decode
|
||
|
||
| WB source | New acdream home | Adaptation |
|
||
|---|---|---|
|
||
| `Chorizite.OpenGLSDLBackend.Lib.EnvCellRenderManager` (if used at runtime) | `src/AcDream.Core/Rendering/Wb/EnvCellRenderManager.cs` | Verbatim + DatCollection swap. |
|
||
| `Chorizite.OpenGLSDLBackend.Lib.PortalRenderManager` | Same | Verbatim. |
|
||
|
||
### 4.5 What we DROP
|
||
|
||
- `_wbDats = new DefaultDatReaderWriter(datDir)` in `WbMeshAdapter.cs:79` — replaced with the existing `_dats: DatCollection` field.
|
||
- Cross-check code in `WbMeshAdapter.cs:224-229` (`if WE find the cell but WB doesn't`) — no longer relevant after unification.
|
||
- The two project references in `AcDream.App.csproj` and `AcDream.Core.csproj` to `WorldBuilder.Shared` + `Chorizite.OpenGLSDLBackend`.
|
||
|
||
---
|
||
|
||
## 5. Task breakdown
|
||
|
||
| Task | Description | Estimated effort |
|
||
|---|---|---|
|
||
| O-T1 | Audit WB call graph — produce a closure of every WB type/file we transitively use. Generate a "must extract" list with line counts. | 0.5d |
|
||
| O-T2 | Create `src/AcDream.Core/Rendering/Wb/` directory structure. Add `NOTICE.md` entry with MIT attribution to WB. | 0.25d |
|
||
| O-T3 | **Extract texture / GL infrastructure** (`TextureHelpers`, `OpenGLGraphicsDevice`, `Frustum`, `VisibilityManager`) — verbatim copy, no dat dependency means low risk. Build green. | 1d |
|
||
| O-T4 | **Extract mesh pipeline** (`ObjectMeshManager`, `ObjectRenderBatch`, `TextureKey`) — verbatim copy. Replace `IDatReaderWriter` with `DatCollection`. Build green. Existing tests green. | 2d |
|
||
| O-T5 | **Extract scenery + terrain pipelines** (`SceneryHelpers`, `SceneryRenderManager`, `LandSurfaceManager`) — verbatim + DatCollection swap. Build green. Visual verification on outdoor terrain. | 1.5d |
|
||
| O-T6 | **Extract EnvCell + portal renderers** if used at runtime (`EnvCellRenderManager`, `PortalRenderManager`) — verbatim + DatCollection swap. Visual verification on indoor scenes. | 1d |
|
||
| O-T7 | **Drop project references** from `AcDream.App.csproj` and `AcDream.Core.csproj`. Drop `_wbDats` and the cross-check code from `WbMeshAdapter`. Build green. | 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). User confirms "looks identical." | 1d (incl. user time) |
|
||
| O-T9 | **Ship** — single merge to main with one descriptive commit per task (T3..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** | | **~7-8 working days** + visual verification cycles |
|
||
|
||
---
|
||
|
||
## 6. Acceptance criteria
|
||
|
||
- [ ] `dotnet build` green across the solution.
|
||
- [ ] `dotnet test` green; no test count regression (currently ~1147).
|
||
- [ ] **Zero references to `WorldBuilder.*` or `Chorizite.OpenGLSDLBackend.*` namespaces** in `AcDream.App.csproj` and `AcDream.Core.csproj` (`PackageReference` for `Chorizite.DatReaderWriter` stays — that's the NuGet DatReaderWriter lib, not WB).
|
||
- [ ] **Zero `using WorldBuilder.*`** in `src/AcDream.*` (the extracted code lives in our namespace now).
|
||
- [ ] **`DefaultDatReaderWriter` referenced in exactly zero places** in our source. `DatCollection` is the only dat reader.
|
||
- [ ] Resident memory measured at `streaming: radius=4` + 50 entities visible: **>50 MB reduction** vs. pre-Phase-O main (validates the duplication is actually gone).
|
||
- [ ] **Visual side-by-side with main**: Holtburg town, an inn interior, a dungeon — all render identically. User confirms.
|
||
- [ ] `NOTICE.md` includes WB attribution per its MIT license.
|
||
- [ ] `references/WorldBuilder/` directory remains in the repo for read-reference, but is not referenced by any csproj.
|
||
- [ ] `CLAUDE.md` updated: 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.md` updated to reflect the new ownership.
|
||
|
||
---
|
||
|
||
## 7. Risks + mitigations
|
||
|
||
| Risk | Likelihood | Severity | Mitigation |
|
||
|---|---|---|---|
|
||
| Re-introduce bugs WB had already fixed (edge-vertex, triangle-Z) | Medium | High | **Discipline: verbatim copy first.** No "improvements." A separate post-Phase-O ticket can refactor. |
|
||
| WB has implicit assumptions about `DefaultDatReaderWriter` semantics our `DatCollection` doesn't match (caching, thread safety) | Medium | High | Task O-T1 must audit the actual call surface used. If `DatCollection` diverges, add a thin adapter layer rather than monkey-patch WB code. |
|
||
| Hidden transitive WB dependencies (some helper class we didn't notice we use) | Low | Medium | Task O-T1 closure pass should catch this. If it slips, build will fail loudly. |
|
||
| Visual regression we don't catch in side-by-side | Low | Medium | Take screenshots of three reference scenes BEFORE starting; compare AFTER. Don't trust eyeballs alone. |
|
||
| User upstream-tracks WB for some reason | Low | Low | We keep `references/WorldBuilder/` in-tree. Re-syncing diffs is a manual port if needed (same as today). |
|
||
| Phase O is scoped too tightly and a piece of WB we use is missed | Medium | Medium | T1's audit is the safety. If we miss one, build fails before T7's reference-drop. |
|
||
|
||
---
|
||
|
||
## 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 `ObjectMeshManager` has a 30% improvement waiting in it, this phase ships it as-is.
|
||
- **API cleanup** of the extracted code. If the constructor has 8 parameters and it's ugly, ugly it stays. Refactor in a follow-up.
|
||
- **Refactoring `WbMeshAdapter` itself.** Phase O drops the second reader; the adapter shape stays.
|
||
- **`tools/InspectCoatTex` or other tools that use the NuGet `Chorizite.DatReaderWriter`**. Those keep working — they use the NuGet `DatReaderWriter`, not the WB project reference.
|
||
|
||
---
|
||
|
||
## 9. Open questions
|
||
|
||
1. **Does WB's `ObjectMeshManager` thread-model match `LandblockStreamer`'s worker thread?**
|
||
- Phase O-T1 must answer. If WB assumes single-threaded mesh-pipeline access and we call into it from the worker, we have a real bug today that gets exposed louder after extraction.
|
||
- Fallback: if WB's code is not thread-safe, route the mesh-prepare call through a dispatcher to the render thread (extra latency, but correct).
|
||
|
||
2. **Should we extract WB's `TerrainRenderManager`** even though we already wrote `TerrainModernRenderer`?
|
||
- Today: we use OUR terrain renderer (Phase N.5b). WB's `TerrainRenderManager` is *not* in our render path.
|
||
- But: `LandSurfaceManager` (terrain texture blending) IS WB code we use, and it might pull on `TerrainRenderManager` types.
|
||
- Recommendation: extract `LandSurfaceManager` only; leave `TerrainRenderManager` in `references/` since we don't use it.
|
||
|
||
3. **What's the right namespace for extracted code?**
|
||
- Option A: `AcDream.Core.Rendering.Wb.*` — preserves "this came from WB" context.
|
||
- Option B: `AcDream.Core.Rendering.Meshes.*`, `AcDream.Core.Rendering.Textures.*` — clean namespaces, drops the WB association.
|
||
- Recommendation: **Option A** for now (keeps the audit trail), with a follow-up phase to fold into Option B once dust settles.
|
||
|
||
4. **Do we extract the `Chorizite.DatReaderWriter` NuGet package code itself?**
|
||
- **No.** That's a separate, well-maintained library on NuGet. We keep depending on it via `PackageReference`. The phase is specifically about the WB project references (`WorldBuilder.Shared` + `Chorizite.OpenGLSDLBackend`) — the *application* code, not the *protocol* library.
|
||
|
||
---
|
||
|
||
## 10. Naming + filing
|
||
|
||
- **Phase name**: O (next free letter — A-N + R are in use).
|
||
- **Phase tagline**: "ONE thing touches the DATs."
|
||
- **Roadmap entry**: add to `docs/plans/2026-04-11-roadmap.md` under "Phases ahead" with cross-reference to this spec.
|
||
- **Issue tracking**: file 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, etc.
|
||
- **Audit a single dat-touching code path** instead of two when investigating bugs like #37 (the coat-stub palette overlay).
|
||
- **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, not just papered over.
|