Chunk 5 (consumer cutover): WalkProductionWorldData's per-cell views are borrowed from ShadowObjectRegistry.GetRetailPartEntriesInCell and resolved through RenderSceneQuery.TryGetByLocalEntityId; every render-side sweep, bucket, parent-cell and root-position fallback is deleted (AD-116 for the one-frame registry→scene window, counted in UnregisteredRenderMembershipCount). A live entity with visual parts but no collision geometry registers render-only (LiveEntityCollisionBuilder computes the part array before the empty-shapes gate). Closeout fixes found while landing it: - RefloodOwnerForLandblock forwards the retained part array — a reflood is retail's recalc_cross_cells over the SAME CPartArray; without it every owner touched by a landblock replacement commit lost its render membership. - Non-colliding DAT statics register render-only from BOTH publishers (LandblockPhysicsPublisher.PublishStaticEntity, LandblockPhysicsContentBuilder.RegisterRenderOnlyStatic). The G2 self-gate pixel diff caught them vanishing (Facility Hub wall panels): retail floods every object regardless of collision (CEnvCell::init_static_objects 0x0052c350, add_shadows_to_cells 0x00514ae0). - S2 dual review fix batch (arch + retail lens, lead-verified): Suspend clears the retail product (remove_shadows_from_cells 0x00511230 is one transaction); AttachChild/DetachChild advance the mutation revision so a prepared SetPosition cannot clobber a child's rows; an attached child never floods on its own re-registration; RemoveLandblock and the non-rooted RetireOwnerFromLandblock prune retail rows (render-only statics end with their landblock); a render-only owner's no-cell-array commit republishes at its destination cell (AD-117); an empty non-null part array is treated as null; per-move closures/LINQ replaced by index loops; EnvCell shells stay out of the scene's LocalEntityId index (payload-less records); the index predicate compares the id; the dead per-cell scene indices are deleted. Register: AD-116 (chunk 5), AD-117 (four residual Contract A/B readings). Evidence: s2-membership-ownership-map.md §8 (chunk 5) and §9 (closeout). Gates (Release): Core 4,984/4,984; Content 214/214; Runtime 1,884/1,884; App hermetic lane 6,760/6,760; App InstalledDat lane 217 pass / 1 skip / 2 pre-existing #383 layout-fixture failures; App Windows lane 1/1. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
797 lines
47 KiB
Markdown
797 lines
47 KiB
Markdown
# WorldBuilder Inventory — what we extracted, adapted, or left behind
|
||
|
||
> **Phase O shipped 2026-05-21.** The ~33 WB files we actually use have
|
||
> been extracted into our tree. `references/WorldBuilder/` stays as a
|
||
> **read-reference only** — nothing in `src/AcDream.*` references it as a
|
||
> project dependency. `DatCollection` is now the only dat reader in process.
|
||
>
|
||
> Use this document to:
|
||
> 1. Know **where our extracted code lives** (look for the "Extracted to"
|
||
> column / notes in each section below).
|
||
> 2. Know **what WB still has** that we haven't needed yet — grep
|
||
> `references/WorldBuilder/` if you ever need to add something.
|
||
> 3. Know **what WB never had** (the 🔴 list) — those are always ours.
|
||
|
||
**Pre-O status (archived for context):** As of Phase N.4 (2026-05-08)
|
||
acdream relied heavily on WorldBuilder as a project reference for rendering
|
||
and dat-handling. WorldBuilder is MIT-licensed, verified by visual inspection
|
||
to render the AC world correctly (terrain, scenery, slabs, dungeons, slopes,
|
||
particles), and uses the same Silk.NET + .NET stack we target.
|
||
|
||
**Post-O integration model:** Extracted WB code lives in two locations in
|
||
our tree (see CLAUDE.md for the full breakdown):
|
||
- `src/AcDream.Core/Rendering/Wb/` — pure helpers (no GL): `TerrainUtils`,
|
||
`TerrainEntry`, `RegionInfo`, `SceneryHelpers`, `TextureHelpers`.
|
||
- `src/AcDream.App/Rendering/Wb/` — Vulkan/RHI infrastructure + mesh pipeline:
|
||
`ObjectMeshManager`, `WbMeshAdapter`, `WbDrawDispatcher`, texture cache,
|
||
shader infra, EnvCell/portal/scenery/terrain-blending pipeline classes.
|
||
|
||
**EnvCell streaming seam:** `EnvCellLandblockBuildBuilder` is an acdream-owned
|
||
adapter around the extracted WB rendering path. One streaming job privately builds
|
||
the complete portal-cell + shell-placement payload, and `EnvCellRenderer.CommitLandblock`
|
||
publishes that completed snapshot on the render thread. WB's geometry-id arithmetic
|
||
and mesh preparation remain unchanged; the transaction wrapper exists because
|
||
acdream streams asynchronously while the WB editor's manager owned its own loading
|
||
loop. Do not reintroduce worker-side `RegisterCell` calls or shared pending cell
|
||
collections.
|
||
|
||
`DatCollectionAdapter` bridges the sole `DatCollection` to Content's
|
||
`IDatReaderWriter`. Since MP1c, production `ObjectMeshManager` no longer reads
|
||
DAT; the adapter remains the bounded typed-object access seam for runtime
|
||
non-render content plus explicit bake/equivalence tooling.
|
||
|
||
**MP1a (2026-07-05): CPU mesh-extraction half moved to `AcDream.Content`.**
|
||
The GL-free portion of the former `ObjectMeshManager` — dat read → polygon
|
||
walk → vertex/index build → palette/conditional BCn texture decode →
|
||
`ObjectMeshData` — is now `MeshExtractor` in a new `src/AcDream.Content/`
|
||
assembly (no Silk.NET dependency), so the MP1b bake tool can run the exact
|
||
same extraction code offline without an OpenGL context. This was a
|
||
mechanical, verbatim move (namespace + visibility only) per
|
||
`docs/superpowers/plans/2026-07-05-mp1a-content-extraction.md` — no
|
||
behavior change, no divergence-register row.
|
||
|
||
- `src/AcDream.Content/MeshExtractor.cs` — the `Prepare*` family
|
||
(`PrepareMeshData`, `PrepareSetupMeshData`, `PrepareGfxObjMeshData`,
|
||
`PrepareEnvCellMeshData`, `PrepareCellStructMeshData`,
|
||
`PrepareCellStructEdgeLineData`) + private helpers (`CollectParts`,
|
||
`CollectEmittersFromScript`, `ComputeBounds`, `BuildPolygonIndices`,
|
||
`BuildCellStructPolygonIndices`) and the decoded-texture cache /
|
||
`ThreadLocal<BcDecoder>` that back them.
|
||
- `src/AcDream.Content/ObjectMeshData.cs` — the CPU-side boundary records:
|
||
`VertexPositionNormalTexture`, `StagedEmitter`, `ObjectMeshData`,
|
||
`MeshBatchData`, `TextureBatchData`.
|
||
- `src/AcDream.Content/TextureKey.cs` — the atlas dedup key, lifted out of
|
||
the GL-owning `TextureAtlasManager` (which stays in App and now
|
||
references the lifted struct).
|
||
- `src/AcDream.Content/UploadFormats.cs` — Content-owned
|
||
`UploadPixelFormat`/`UploadPixelType` enums carried by
|
||
`MeshBatchData`/`TextureBatchData` instead of
|
||
`Silk.NET.OpenGL.PixelFormat`/`PixelType` (Content must stay
|
||
Silk.NET-free — the bake tool must not ship GL binaries). Underlying
|
||
values are the GL ABI constants, numerically identical to the Silk.NET
|
||
members; App casts at its single upload boundary (the `AddTexture` call
|
||
in `UploadGfxObjMeshData`) via a lifted nullable enum conversion —
|
||
value- and null-preserving.
|
||
- `src/AcDream.Content/IDatReaderWriter.cs`, `EdgeLineBuilder.cs` — GL-free
|
||
dependencies of the extractor, moved (namespace-only) alongside it.
|
||
- **Side-stage sink seam:** `CollectEmittersFromScript` pre-loads particle
|
||
GfxObj meshes mid-extraction and, pre-MP1a, enqueued them directly onto
|
||
`ObjectMeshManager._stagedMeshData`. The extractor now takes an
|
||
`Action<ObjectMeshData>? sideStagedSink` constructor parameter; App wires
|
||
it to `_stagedMeshData.Enqueue`, preserving the original
|
||
immediate-enqueue semantics exactly — including on a mid-`Prepare*`
|
||
throw (preloads staged before a malformed-dat texture-decode exception
|
||
survive, as they always did). The MP1b bake tool passes its own
|
||
collector.
|
||
- **Stays in `src/AcDream.App/Rendering/Wb/`:** `ObjectMeshManager` (the
|
||
staged-queue/worker-pool/Dispose-quiesce lifecycle and all Vulkan/RHI upload;
|
||
production workers now consume `IPreparedAssetSource`),
|
||
`ObjectRenderData`/`ObjectRenderBatch`
|
||
(hold a GL `TextureAtlasManager` field), `TextureAtlasManager`,
|
||
`GeometryUtils` (used only by App-side raycasting, not by extraction),
|
||
`AcSurfaceMetadata`/
|
||
`AcSurfaceMetadataTable` (not on the extraction path), `Building.cs`
|
||
(explicitly out of scope).
|
||
- `AcDream.Core` is untouched; `AcDream.Content` references `AcDream.Core`
|
||
(for `TextureHelpers`, `Sphere`/`BoundingBox` via `Chorizite.Core.Lib`);
|
||
`AcDream.App` references `AcDream.Content`. `AcDream.Core` does NOT
|
||
reference `AcDream.Content` (one-way dependency, per Code Structure Rule 2).
|
||
- Reason: MP1 (`docs/superpowers/specs/2026-07-05-modern-pipeline-design.md`
|
||
§6.1) — the bake tool needs the identical mesh/texture extraction code
|
||
running with no GL context, so baked pak output and live-client output stay
|
||
byte-identical.
|
||
|
||
**MP1b EnvCell content identity correction (2026-07-24).** The extracted
|
||
WorldBuilder `EnvCellRenderManager.GetEnvCellGeomId` 31× polynomial is not a
|
||
safe unique resource key. A guarded full retail-DAT catalog found the concrete
|
||
collision `0x00030175` versus `0x01BC0105`: different environment/surface
|
||
tuples both map to `0x00000002020E8C13` and contain different polygons.
|
||
`AcDream.Core.Rendering.Wb.EnvCellGeometryIdentity` now owns one namespaced
|
||
FNV-1a identity shared by the App streaming build and `acdream-bake`; the
|
||
legacy calculation remains executable only for the conformance test that proves
|
||
the collision. The bake additionally compares the complete source tuple before
|
||
aliasing and fails on any collision. This is an acdream resource-ownership seam,
|
||
not a second DAT interpreter; `DatCollection` and `MeshExtractor` remain the
|
||
only reader/extractor path.
|
||
|
||
**MP1c production prepared-asset cutover (2026-07-24).** Production
|
||
world-mesh workers no longer invoke `MeshExtractor` or rebuild Setup, GfxObj,
|
||
EnvCell, Surface, palette, and texture graphs during portals. The validated
|
||
machine-local `acdream.pak` is opened through Content's
|
||
`IPreparedAssetSource`; typed GfxObj and EnvCell requests deserialize immutable
|
||
`ObjectMeshData` while retaining the existing App worker, staging, render-thread
|
||
upload, cache, ownership, and shutdown contracts. The prepared render payload
|
||
persists exact batch translucency so App does not reconstruct a
|
||
`GfxObjMesh` for metadata. Setup activation uses the package TOC as an explicit
|
||
type-presence index before reading valid Setup records through the bounded DAT
|
||
cache. `DatPreparedAssetSource` and `MeshExtractor` remain explicit
|
||
bake/equivalence/UI-Studio tools, not a production fallback. Portal → HighRes
|
||
→ Language → Cell lookup precedence is encoded directly in
|
||
`DatCollectionAdapter.TryResolvePreferred`. The connected physical and
|
||
installed-DAT gates are recorded in
|
||
`docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md`.
|
||
|
||
**Launcher cumulative-overlay extension (2026-08-25).** Production still has
|
||
no live-DAT fallback and consumes the same prepared-payload contracts. For a
|
||
bounded recipe migration, App and Headless may receive one complete base pak
|
||
plus one cumulative filtered pak through `LayeredPreparedAssetSource`. The
|
||
overlay is probed first: Missing falls through to the base, while a present but
|
||
corrupt render or collision payload remains authoritative corruption. Both
|
||
mapped owners share one composite lifetime and there is never an overlay
|
||
chain. The launcher binds the overlay to the base digest in the optional
|
||
`pak/content.current.json` sidecar; format/global extraction migrations retain
|
||
the explicit full-rebuild path. A tiny `pak/content.client-pending` marker
|
||
keeps either result non-launchable until the matching client is confirmed,
|
||
including across a crash/restart. Design and gates:
|
||
`docs/plans/2026-08-25-launcher-content-stabilization.md`.
|
||
|
||
**PAK v2 resource closeout (2026-08-27).** Format 2 / bake recipe 6 retains
|
||
the fixed header and sorted random-access TOC while moving texture arrays into
|
||
one globally shared type-8 payload partition and independently applying
|
||
adaptive Brotli compression with raw fallback. Unedited DXT1/3/5 surfaces
|
||
retain exact DAT BC blocks through Vulkan upload; clip maps and authored
|
||
translucency continue through the RGBA edit path. The reader bounds decoded
|
||
blobs and retains shared texture arrays in a 64 MiB / 1,024-entry LRU. The
|
||
complete installed package is 597,229,424 bytes versus 29,908,271,024 bytes
|
||
for format 1; four- and nine-worker bakes have identical SHA-256. The
|
||
authoritative connected before/after route reduced heavy-route working set by
|
||
48.0%, private bytes by 26.1%, and prepared-mesh GPU bytes by 25.9% with equal
|
||
or faster matching reveal/frame percentiles. Format changes remain mandatory
|
||
launcher-confirmed full rebuilds with beside-active validation and atomic
|
||
promotion; overlays never cross a format boundary. Design and evidence:
|
||
`docs/plans/2026-08-27-pak-v2-resource-campaign.md`.
|
||
|
||
**Retail GfxObj view-sphere correction (2026-08-31).** Bake recipe 7
|
||
regenerates prepared GfxObj render records so their portal-view admission
|
||
uses the authored DrawingBSP root sphere retained by retail
|
||
`CGfxObj::Serialize`, rather than a sphere synthesized from the extracted
|
||
vertex AABB. The binary package format remains 2. Because this field affects
|
||
every GfxObj and the old records cannot be corrected without their source
|
||
DrawingBSP, migration 6→7 is an explicit full rebuild.
|
||
|
||
The production leaf follows retail `RenderDeviceD3D::DrawMesh` /
|
||
`DrawMeshInternal`: portal viewcones perform coarse authored-sphere
|
||
admission, but an admitted GfxObj is submitted once as a complete mesh.
|
||
Likewise `PView::DrawCells` calls `DrawEnvCell` once per admitted cell and
|
||
draws its complete constructed shell. Portal polygons are not GPU mesh clips;
|
||
pixel occlusion comes from the ordered walk and depth buffer.
|
||
|
||
**Retail static render-shadow cell membership (2026-08-31; ownership moved
|
||
to `ShadowObjectRegistry` by Campaign OVERHAUL S2 chunk 2; collision cutover
|
||
by S2 chunk 3, 2026-09-02).** Indoor statics are not rendered solely from
|
||
their authored parent EnvCell. Retail's `CEnvCell::init_static_objects`
|
||
creates a `CPhysicsObj` for every static, then `calc_cross_cells_static` +
|
||
`CPartArray::AddPartsShadow` places every visual part in each crossed cell's
|
||
`shadow_part_list`, including decorative GfxObjs that have no collision BSP —
|
||
one CELLARRAY flood feeds BOTH the collision `shadow_object_list` and the
|
||
render `shadow_part_list` (Contract B). `ShadowObjectRegistry` now mirrors
|
||
this exactly: `RegisterMultiPart`/`Register`'s optional `partArray:`
|
||
parameter, when supplied (every production call site since chunk 1b), drives
|
||
ONE Contract A flood (`ComputeContractACellArray`) whose result becomes BOTH
|
||
`GetOwnerCells`/`_entityToCells`/`_cells` (collision) and the retained
|
||
per-entity retail CELLARRAY (`TryGetRetailCellArray`) plus per-cell part
|
||
entries (`GetRetailPartEntriesInCell`) — not two independent floods, and not
|
||
rebuilt per frame. A caller that supplies no part array (only test/legacy
|
||
call sites remain) keeps the pre-chunk-3 collision-only dispatch untouched.
|
||
**Consumer cutover to a borrowed per-cell view (S2 chunk 5, 2026-09-03).**
|
||
`WalkProductionWorldData`'s three per-frame scene sweeps (indoor static,
|
||
outdoor static, dynamic) and their five per-cell bucket dictionaries are
|
||
deleted. `GetCellStatics`/`GetCellDynamics`/`GetOutdoorStatics`/
|
||
`GetOutdoorDynamics` instead read
|
||
`ShadowObjectRegistry.GetRetailPartEntriesInCell` for the QUERIED cell
|
||
directly — already in retail CELLARRAY-then-part-array insertion order —
|
||
collapse the entries to their distinct owning entity ids (an entity's own
|
||
run of entries for one cell is always contiguous, since the registry
|
||
removes-then-re-adds a whole entity's rows atomically), and resolve each id
|
||
back to its `RenderProjectionRecord` through
|
||
`RenderSceneQuery.TryGetByLocalEntityId` — a new presentation-side index
|
||
`ArchRenderScene` maintains alongside its existing per-cell/per-class
|
||
indices, keyed by the SAME `RenderSourceMetadata.LocalEntityId` the
|
||
registry is keyed by (App-only presentation state, never a second
|
||
membership source). Statics vs. dynamics is the record's own
|
||
`ProjectionClass` (`LiveDynamicRoot`/`EquippedChild` are dynamic; every
|
||
other class, including `ActiveAnimatedStatic`, is static — the same split
|
||
`ArchRenderScene`'s own internal indexing already used); building shells
|
||
are excluded from both — they draw at their own building's shell turn
|
||
(`_shellsByAnchor`, still filled by the ONE surviving narrow sweep, since
|
||
buildings are the landcell building channel, out of S2's scope), never at
|
||
the cell's ordinary object-list turn. A structural consequence carried
|
||
over from chunk 3: an object mixing a colliding BSP part with a decorative
|
||
non-BSP part gets a collision row for its BSP part in every cell the
|
||
combined CELLARRAY reaches, including cells the BSP part alone would not
|
||
have crossed — membership is decided by the whole-part-array flood, not
|
||
per part; the BSP polygon test still decides actual contact at query time.
|
||
An entity the registry HAS flooded into a cell but whose projected record
|
||
the presentation journal has not applied yet this frame (the transient
|
||
race between the physics publisher and the projection journal — AD-116)
|
||
contributes to NO cell for that frame; there is no fallback to an authored
|
||
parent cell or a root-position cell any more (both deleted this chunk),
|
||
matching retail's own rule that an object not yet in a cell is not drawn.
|
||
Every distinct entity id this happens for in one frame is counted once in
|
||
`WalkProductionWorldData.UnregisteredRenderMembershipCount` (renamed from
|
||
`UnregisteredStaticRenderFallbackCount`, which under the pre-chunk-5 model
|
||
also covered the separate dynamic-record fallback) and reported by one
|
||
print-only `[walk-membership]` line — gated on
|
||
`RenderingDiagnostics.ProbeFacilityStairsEnabled`, the walk family's
|
||
existing diagnostic flag — at the start of the next frame when nonzero.
|
||
A primitive-only Setup may reuse its already-authored cylsphere collision
|
||
cell set; BSP-bearing and pure-visual statics use the all-visual-part box
|
||
walk. The Facility Hub stair Setup `0x02000623` is the installed-DAT
|
||
regression: its authored parent is `0x8A02015F`, while its visual parts also
|
||
register in `0x8A02015E` and the adjoining vertical cells.
|
||
|
||
**Moving multipart objects and equipped children (S2 chunk 4, 2026-09-02).**
|
||
Moving multipart objects follow the sibling retail route rather than their
|
||
authored/feet cell alone: `CPhysicsObj::add_shadows_to_cells` installs every
|
||
`CPartArray` part in every cell of the object's retained `CELLARRAY`, and
|
||
`CPhysicsPart::Draw` stamps each part only after that part passes a cell's
|
||
portal test. `WalkProductionWorldData.GetCellDynamics`/`GetOutdoorDynamics`
|
||
therefore read the SAME per-cell `GetRetailPartEntriesInCell` borrowed view
|
||
the static getters use (S2 chunk 5), filtered to the dynamic
|
||
`ProjectionClass`es — not a per-record `TryGetRetailCellArray` lookup with
|
||
its own fallback any more; an entity the registry has flooded into a cell
|
||
but the presentation scene cannot resolve yet contributes to no cell there,
|
||
counted by the shared `UnregisteredRenderMembershipCount` (AD-116), while
|
||
the walk classifier owns a separate projection+part drawn-pass stamp.
|
||
Retail advances that stamp after
|
||
`LScape::draw` + `FlushAlphaList` and before the interior depth clear
|
||
(`PView::DrawCells` @0x005A4886), so a part may draw once in the landscape
|
||
pass and once again in the post-clear interior-cell pass; acdream re-arms the
|
||
classifier at that exact walk boundary. Never replace the per-part stamp with
|
||
a whole-projection drawn-once gate: at a stair portal the torso may pass in
|
||
the first cell while head/hair fails and must retry through another crossed
|
||
cell. One binding exception is the local player: retail
|
||
`RenderDeviceD3D::DrawMeshInternal` bypasses `Get/SetDrawnThisFrame` when
|
||
`CPhysicsPart::IsPartOfPlayerObj` is true, repainting the player's parts at
|
||
every crossed-cell turn so later wall/depth ordering remains correct.
|
||
|
||
Retail's move-path product is the SAME `add_shadows_to_cells` call the
|
||
registration path uses, not a second independent flood:
|
||
`CPhysicsObj::SetPositionInternal` (0x00515330) takes the transition's own
|
||
`cell_array` on a successful move and calls
|
||
`remove_shadows_from_cells`/`add_shadows_to_cells` with it directly — the
|
||
`calc_cross_cells_static` cylsphere/bbox dispatch never re-runs on a move.
|
||
`ShadowObjectRegistry.ReplacePositionRows` mirrors this exactly:
|
||
`PublishRetailProductFromExactCells` publishes the retail render product from
|
||
the SAME exact cell list collision just republished into, replacing the
|
||
independent Contract A recompute an earlier chunk ran on every move; retail's
|
||
`num_cells > 0` keep-when-empty gate covers both products identically, so a
|
||
move with no resolvable transition array leaves both untouched.
|
||
|
||
`add_shadows_to_cells` also recursively passes the root's same `CELLARRAY` to
|
||
every object in `children` (Contract B); acdream's attached projections
|
||
(equipped weapons/shields/ammunition) own no independent collision shapes, so
|
||
`ShadowObjectRegistry.AttachChild`/`DetachChild` publish PART ENTRIES only —
|
||
never a collision row — into every cell of the resolved root's current retail
|
||
CELLARRAY, re-publishing automatically whenever that array changes
|
||
(registration, move, staged apply, `ReplaceMultiPartPayload`).
|
||
`EquippedChildRenderController.TryRealize` calls `AttachChild` at the exact
|
||
moment an attached projection's `WorldEntity` is registered against its
|
||
accepted parent; `CommitProjectionRemoval` (the withdrawal/unparent/teardown
|
||
funnel) calls `DetachChild`. A nested attachment (a child of a child)
|
||
resolves to the ultimate root by walking the existing attach chain at the
|
||
registry, bounded and cycle-safe — `WalkProductionWorldData` never walks an
|
||
accepted-parent chain of its own; a child's part entries surface through the
|
||
SAME per-cell borrowed view (S2 chunk 5) every other dynamic record uses,
|
||
resolved to its own projection via `RenderSceneQuery.TryGetByLocalEntityId`,
|
||
identically to any other dynamic record.
|
||
|
||
**Slice I3 prepared collision extension (2026-07-25).** At its introduction,
|
||
the package remained format 1 and retained mesh type values 1–3; bake-tool 4
|
||
appended typed GfxObj,
|
||
Setup, CellStruct, and EnvCell-topology collision payloads. Core owns the
|
||
immutable flat records and deterministic raw-DAT flattener. Content owns the
|
||
strict little-endian codec and `IPreparedCollisionSource`.
|
||
`PakPreparedAssetSource` implements the render and collision interfaces over
|
||
one mmap; it does not create a second DAT reader or mapping. CellStruct
|
||
payloads alias only after exact serialized-byte comparison, while each
|
||
EnvCell topology remains independently keyed. Production traversal remained
|
||
on the parsed graph oracle until the later Slice-I cutover. Full-catalog evidence:
|
||
`docs/research/2026-07-25-slice-i3-prepared-collision-package.md`.
|
||
|
||
**Slice I5 dual-publication seam (2026-07-25).** Near-tier
|
||
`LandblockBuild` payloads now carry one immutable prepared-collision closure
|
||
outside the DAT lock. `LandblockPhysicsPublisher` installs the parsed oracle
|
||
and flat view under the same retained receipt; live objects use the strict
|
||
`LiveCollisionAssetPublisher`. Cell/topology ownership is landblock-scoped and
|
||
withdrawn on replacement, demotion, removal, and reset. The connected
|
||
graph-authoritative referee completed 14,064 exact samples with no mismatch or
|
||
fault. This is still one `DatCollection` and one prepared-package mmap, not a
|
||
second reader or a WorldBuilder runtime dependency. Evidence:
|
||
`docs/research/2026-07-25-slice-i5-dual-collision-shadow.md`.
|
||
|
||
**Slice I6/I7 flat-authoritative closeout (2026-07-25).** Production
|
||
`PhysicsDataCache` now publishes only immutable flat GfxObj, Setup,
|
||
CellStruct, and EnvCell-topology records from the validated package. Stable
|
||
world state strips temporary `PhysicsDatBundle` source material, and near
|
||
landblock builds discard raw Environment/GfxObj collision graphs once the
|
||
flat closure exists. Parsed graph constructors remain explicit
|
||
test/bake/equivalence oracles only; gameplay has no graph fallback or referee.
|
||
Both exact-binary connected routes report `0/0/0` retained parsed collision
|
||
graphs at every stable checkpoint while flat residency remains populated.
|
||
This remains one `DatCollection`, one preparation algorithm, and one package
|
||
mmap. Evidence:
|
||
`docs/research/2026-07-25-slice-i7-closeout.md`.
|
||
|
||
**Prepared indoor-transit consumer correction (2026-07-26).** The package
|
||
already retained exact portal planes through
|
||
`FlatEnvCellTopology.PolygonIndex` plus the aliased CellStruct portal-polygon
|
||
table. The production `CellTransit` consumer now reads that prepared
|
||
relationship directly; it no longer treats the intentionally absent parsed
|
||
`CellPhysics.PortalPolygons` dictionary as “this cell has no portals.” No
|
||
package schema, bake, DAT reader, collision formula, or render portal graph
|
||
changed. Evidence:
|
||
`docs/research/2026-07-26-prepared-indoor-transit-regression.md`.
|
||
|
||
**Cell availability semantics (2026-07-31, corrected after full-catalog
|
||
audit).** Raw and prepared CellStruct publication retains a `CellPhysics`
|
||
record when the physics root is empty but requires a valid containment root.
|
||
The installed 729,888-record raw and prepared catalogs contain zero rootless
|
||
containment payloads. A malformed null/-1 root is quarantined atomically; the
|
||
recursive inside base case applies only to a missing positive child below a
|
||
valid root. Registration-side outdoor floods still add outside cells but skip
|
||
transit when the active CLandCell is unavailable, and every later outdoor
|
||
candidate independently requires its own visible landcell before building
|
||
transit. The existing reflood retries after terrain/cell hydration. Both raw
|
||
and prepared point-in-cell paths preserve retail's zero-portals guard. No
|
||
package schema or DAT reader changed.
|
||
Evidence: `docs/research/2026-07-31-cell-availability-semantics.md`.
|
||
|
||
**Retail VFX hook compatibility seam (2026-07-14).** Chorizite.DatReaderWriter
|
||
2.1.7 models `CreateBlockingParticleHook` as the common hook header only, while
|
||
retail inherits the complete `CreateParticleHook` payload. The narrow readers in
|
||
`src/AcDream.Content/Vfx/` read raw bytes through the existing `DatCollection`
|
||
database, delegate every ordinary hook to the package, and substitute only the
|
||
retail blocking-particle shape. Both live animation playback and PhysicsScript
|
||
loading use those cached readers; `MeshExtractor` uses the same PhysicsScript
|
||
loader when preloading emitter meshes. This is not a second DAT reader:
|
||
`DatCollection` remains the sole database owner and access path. Retail anchors:
|
||
`CreateBlockingParticleHook::Execute` `0x00526EF0` and
|
||
`ParticleManager::CreateBlockingParticleEmitter` `0x0051B8A0`.
|
||
|
||
**Retail particle visibility/degradation seam (2026-07-17).** WorldBuilder's
|
||
particle simulator remains a useful DAT-integrator and batching reference, but
|
||
it does not carry the live retail client's `CObjCell::IsInView` degradation
|
||
path. `EmitterDescRegistry` now resolves the hardware particle GfxObj and its
|
||
ordered `GfxObjDegradeInfo` entries through the same sole `DatCollection`;
|
||
`ParticleSystem` ports the retail finite/infinite degraded branches; App feeds
|
||
the unified PView interior set plus the landscape renderer's independently
|
||
computed outdoor landcell set through a focused
|
||
`ParticleVisibilityController`. Examination and dedicated-pass emitters carry
|
||
an explicit bypass policy; missing/portal world views are empty rather than
|
||
fail-open.
|
||
No WorldBuilder dependency or second DAT access layer was introduced. Retail
|
||
anchors: `CPhysicsPart::GetMaxDegradeDistance` `0x0050D510`,
|
||
`GfxObjDegradeInfo::get_max_degrade_distance` `0x0051E2D0`,
|
||
`CPhysicsObj::ShouldDrawParticles` `0x0050FE60`, and
|
||
`ParticleEmitter::UpdateParticles` `0x0051D180`.
|
||
|
||
Hardwareless ParticleEmitterInfo records are also retained exactly. Retail
|
||
`ParticleEmitter::SetInfo @ 0x0051CE90` returns false when
|
||
`hw_gfxobj_id == INVALID_DID`; it does not substitute the software GfxObj.
|
||
`EmitterDescRegistry` negative-caches that authored outcome and the hook sink
|
||
reports it once per DAT ID rather than once per owner. This is classification
|
||
and bounded diagnostics around the existing `DatCollection`, not a fallback
|
||
reader or invented VFX. Evidence:
|
||
`docs/research/2026-07-26-retail-hardwareless-particle-emitter-diagnostics.md`.
|
||
|
||
**Retail shared world-alpha seam (2026-07-18).** WorldBuilder's editor
|
||
renderers classify translucent mesh batches correctly, but they have no live
|
||
retail `CPartCell`/`CShadowPart` list and render particles in a separate
|
||
batcher. `src/AcDream.App/Rendering/RetailAlphaQueue.cs` is therefore an
|
||
acdream-owned runtime seam above the extracted mesh pipeline. During the main
|
||
world frame, `WbDrawDispatcher` and `ParticleRenderer` submit transparent
|
||
GfxObj subsets and scene particles into one stable far-to-near stream keyed by
|
||
the transformed DAT `SortCenter`; only adjacent compatible entries may batch.
|
||
Billboard particle textures are resident bindless `sampler2DArray` handles in
|
||
the per-instance vertex ABI, so different textures preserve that sorted order
|
||
inside one instanced draw; only a DAT blend-mode boundary splits the run. This
|
||
keeps dense particle fields from becoming one Vulkan draw per alternating
|
||
texture. `WalkFrameDriver` drains the landscape and world scopes at the exact
|
||
alpha barriers emitted by `RetailFrameWalk`; the frame orchestrator reaches
|
||
private viewports/UI only after the world stream completes.
|
||
Sky and sealed off-screen render targets remain independent. No DAT reader,
|
||
mesh decoder, or second scene graph was introduced. Retail anchors:
|
||
`CPhysicsPart::UpdateViewerDistance` `0x0050E030`,
|
||
`RenderDeviceD3D::DrawObjCellForDummies` `0x005A0760`,
|
||
`CShadowPart::insertion_sort` `0x006B5130`,
|
||
`D3DPolyRender::AddMeshToAlphaList` `0x0059C230`, and
|
||
`D3DPolyRender::FlushAlphaList` `0x0059D2E0`. The modern per-cell-order and
|
||
EnvCell-shell residual is tracked explicitly as AP-34. WorldBuilder does not
|
||
choose visible cells, portal slices, or cross-cell order: it receives the
|
||
walk's ordered records and GPU clip slots and performs asset preparation plus
|
||
order-preserving Vulkan batching only.
|
||
|
||
**Retail portal-space viewport adapter (2026-07-15).**
|
||
`src/AcDream.App/Rendering/PortalTunnelPresentation.cs` uses the extracted
|
||
Setup/GfxObj mesh pipeline and mandatory `WbDrawDispatcher` for retail's
|
||
synthetic CreatureMode tunnel object. The adapter does not duplicate mesh or
|
||
DAT decoding: it resolves the two client-enum assets through `DatCollection`,
|
||
uses the shared `RetailAnimationLoader`, registers Setup part refs through
|
||
`WbMeshAdapter`, and submits the animated `SetupMesh` through the existing
|
||
dispatcher. It clears world clip routing and point lights for the private scene
|
||
before installing retail's distant light. The lifecycle, camera, animation,
|
||
and draw ordering are acdream-owned ports of `gmSmartBoxUI`; WorldBuilder never
|
||
implemented this UI viewport.
|
||
|
||
**Portal destination render-readiness seam (2026-07-16).**
|
||
`GpuWorldState.IsRenderReady` does not treat dictionary publication as a draw
|
||
barrier. `LandblockSpawnAdapter` retains the complete required-id set for each
|
||
landblock: atlas-tier GfxObjs plus the synthetic geometry ids prepared by the
|
||
independent EnvCell shell pipeline. `WbMeshAdapter.IsRenderDataReady` opens the
|
||
gate only after `ObjectMeshManager` has real GPU render data. Its bounded CPU
|
||
cache retains texture payloads and stages a missing GPU object through a
|
||
deduplicated upload queue on cache hit only when the exact renderer owner is
|
||
still live, covering eviction and revisit churn without letting an unowned
|
||
cache hit recreate stale staged work. Reacquiring an exact owner stages once.
|
||
GPU upload is deliberately not an ownership acquire: atlas GfxObjs use their
|
||
landblock/entity pins, while synthetic EnvCell geometry uses a no-generic-decode
|
||
pin balanced from each landblock snapshot. A late upload after all owners have
|
||
released enters the evictable LRU instead of resurrecting a reference. After
|
||
publication pins the synthetic ids, the controller replays their immutable
|
||
environment/cell-structure/surface preparation descriptors; if a formerly
|
||
unowned mesh was evicted between worker scheduling and publication, this
|
||
schema-aware replay re-stages it without a generic GfxObj lookup.
|
||
Near-to-Far demotion is a separate App transaction: it releases EnvCell
|
||
rendering and landblock mesh pins while retaining the terrain slot. Core's
|
||
matching physics demotion preserves the terrain surface but removes indoor
|
||
cells, portals, buildings, and static shadow registrations.
|
||
|
||
**Cost-budgeted publication seam (2026-07-24).** WorldBuilder's editor path
|
||
publishes a complete manager-owned scene; acdream's live streamer instead
|
||
advances prepared render, physics, static, building, EnvCell, and spatial
|
||
receipts under one typed frame meter. Stable cursor work may span frames, while
|
||
building/EnvCell replacement and the final `GpuWorldState` spatial identity
|
||
swap remain observer-atomic. Destination live-object render ownership prepares
|
||
while the world is quiesced. `GpuWorldState.IsLiveEntityProjectionResident`
|
||
answers that spatial ownership question; availability-gated drawing,
|
||
collision, picking, radar/status targeting, effects, and audio continue to use
|
||
`IsLiveEntityVisible`. This is an acdream async-integration seam around the
|
||
extracted WB pipeline, not a second mesh or DAT implementation. Connected
|
||
evidence:
|
||
`docs/research/2026-07-24-slice-e-cost-budgeted-streaming-report.md`.
|
||
|
||
**Bounded residency and GPU retirement seam (2026-07-18).** Runtime DAT access
|
||
keeps raw file payload caching disabled and layers bounded typed-object and
|
||
decoded-pixel LRUs above the single `DatCollection`. `ObjectMeshManager`, the
|
||
standalone bindless texture cache, and the owner-scoped composite texture-array
|
||
cache all distinguish an active owner from an evictable unowned entry. Appearance
|
||
changes and landblock demotion acquire-before-publish and withdraw-before-release;
|
||
rebucketing never creates a second owner. `GlobalMeshBuffer` uses reclaimable,
|
||
coalescing vertex/index ranges and migrates incrementally within explicit physical
|
||
ceilings. Texture layers, terrain slots, mesh ranges, and old backing stores are
|
||
returned only after `GpuFrameFlightController` observes the frame fence that can
|
||
no longer reference them. This lifetime machinery is acdream-owned integration
|
||
around the extracted WB mesh pipeline; it does not add a second DAT decoder or a
|
||
reduced-distance rendering path.
|
||
|
||
**Unified residency policy seam (2026-07-24).** The extracted WB caches remain
|
||
the physical owners of mesh, arena, atlas, and texture resources.
|
||
`AcDream.App.Rendering.Residency.ResidencyManager` adds an acdream-owned typed
|
||
policy/diagnostic layer over them: generation-safe asset handles, independent
|
||
owner tokens and leases, immutable startup budgets, aggregate accounting, and
|
||
bounded trim requests. It never stores or deletes a GL name. Existing owners
|
||
perform logical eviction and fence-delayed physical release on the render
|
||
thread. The same ledger observes the prepared-package mapping, prepared/staged
|
||
CPU mesh data, decoded animation/audio data, and retained shared-alpha scratch
|
||
without double-counting the package's clean memory-mapped pages as committed
|
||
heap. Deterministic pressure tests exceed every configured ceiling and prove
|
||
zero-charge teardown. Connected evidence:
|
||
`docs/research/2026-07-24-slice-d-unified-residency-report.md`.
|
||
|
||
**OH2/S1 exact CellStruct surface-index construction (2026-09-02).** Bake
|
||
recipe 8 replaces `MeshExtractor.PrepareCellStructMeshData`'s NoPos-based
|
||
approximation with retail's exact `D3DPolyRender::ConstructMesh` (`0x0059DFA0`)
|
||
side/candidate table: side candidates come only from `CPolygon::sides_type`
|
||
(`AcDream.Core.Meshing.CellStructSideCandidates`); `NoPos`/`NoNeg` mean "this
|
||
side's UV-index array is absent" only (`CPolygon::UnPack` `0x00538650`), never
|
||
face suppression. The subset/material owner is the source surface-array
|
||
index, not the resolved Surface DID or texture format:
|
||
`TextureBatchData.SourceSurfaceIndex`/`RetailSurfaceMask`/`RawSurfaceType`/
|
||
`IsCellShell` carry that fact through the prepared package, and
|
||
`CellSurfaceSubsets.InAscendingSurfaceOrder` recovers the retail subset walk
|
||
order independent of the (Width,Height,Format) storage grouping the package
|
||
still uses for atlas dedup. Built-EnvCell draw admission is
|
||
`(Surface.Type & (BASE1_IMAGE|BASE1_CLIPMAP)) != 0`
|
||
(`RenderDeviceD3D::DrawEnvCell` `0x0059F170` →
|
||
`D3DPolyRender::DrawMesh(..., arg4=1)` `0x0059D4A0`), applied after surface
|
||
resolution via `RetailUntexturedSurfacePolicy.IsUntextured`, not approximated
|
||
before it. `CellMesh.Build`'s render-production role is retired; the
|
||
streaming build job now calls `CellMesh.HasDrawableGeometry` — the identical
|
||
exact admission rule, evaluated as a predicate only, with no mesh retained —
|
||
to decide whether a CellStruct contributes drawable geometry. Fixed retail
|
||
`D3DCULL_CW` (`RenderMeshSubset` `0x0059CA10`) remains the raster cull for
|
||
every constructed cell-shell subset after fan expansion; the authored
|
||
`sides_type` is not read as GPU cull state. Retires AP-234. Contract:
|
||
`docs/research/2026-09-01-overhaul/oh2-cellstruct-surface-contract.md`.
|
||
|
||
**Workflow:** Before re-implementing any AC-specific rendering or dat-handling
|
||
algorithm, **check this inventory first**. If we already extracted it (🟢
|
||
sections), it's in `src/AcDream.App/Rendering/Wb/` — use our copy. If WB has
|
||
it but we haven't extracted it yet, grep `references/WorldBuilder/` and extract
|
||
as needed. Retail decomp remains the oracle for things WB never had (🔴 list).
|
||
|
||
Attribution: WorldBuilder is MIT-licensed. `NOTICE.md` includes WB attribution.
|
||
|
||
---
|
||
|
||
## Read-reference layout (under `references/WorldBuilder/`, not project-referenced)
|
||
|
||
- **`Chorizite.OpenGLSDLBackend/`** — full OpenGL renderer (Silk.NET). The
|
||
components we use are extracted into `src/AcDream.App/Rendering/Wb/`.
|
||
- **`WorldBuilder.Shared/`** — data models, dat parsers, landscape module.
|
||
The helpers we use are extracted into `src/AcDream.Core/Rendering/Wb/`.
|
||
- **`WorldBuilder/`** — Avalonia desktop app shell (not taken).
|
||
- **`WorldBuilder.{Windows,Linux,Mac}/`** — platform entry points (not taken).
|
||
- **`WorldBuilder.Server/`** — collab editing backend (not taken).
|
||
- **`Tests/` + `WorldBuilder.Shared.Benchmarks/`** — test harness (study only).
|
||
|
||
**Upstream NuGet dependencies** (these stay as NuGet packages, we don't
|
||
vendor them):
|
||
|
||
| Package | Version | Purpose |
|
||
|---|---|---|
|
||
| `Chorizite.Core` | 0.0.18 | Plugin framework — contains `Chorizite.Core.Lib.BoundingBox`, `Chorizite.Core.Render.*` interfaces used by every render manager |
|
||
| `Chorizite.DatReaderWriter` | 2.1.x | dat parsing (we already use 2.1.7) |
|
||
| `Chorizite.DatReaderWriter.Extensions` | 1.1.x | extra dat helpers |
|
||
| `BCnEncoder.Net` | 2.2.x | DXT decode (we already use) |
|
||
| `SixLabors.ImageSharp` | 3.1.x | image loading |
|
||
| `Silk.NET.OpenGL` + `Silk.NET.SDL` | 2.23.x | GL + windowing (we use Silk's own windowing, they use SDL) |
|
||
| `MP3Sharp` | 1.0.5 | MP3 decode |
|
||
|
||
---
|
||
|
||
## 🟢 RENDERING — take wholesale or adapt
|
||
|
||
These are what makes WB "perfect". Anything in this section, we should
|
||
use from WB rather than re-implement.
|
||
|
||
### Terrain
|
||
|
||
| Component | What it does |
|
||
|---|---|
|
||
| `TerrainRenderManager` | Full pipeline (per-chunk GPU buffers, draw orchestration) |
|
||
| `LandSurfaceManager` | Texture blending atlas (palCode, alpha masks, road overlays) |
|
||
| `TerrainGeometryGenerator` | Heightmap → mesh, normals, OnRoad, GetHeight, GetNormal |
|
||
| `TerrainChunk` | 16×16 landblock chunk geometry |
|
||
| `TextureAtlasManager` | Texture atlas builder |
|
||
| `VertexLandscape` | Terrain vertex format |
|
||
|
||
**Modern terrain adapter:** acdream's bindless path uses `TerrainAtlas` plus
|
||
`TerrainModernRenderer` rather than WB's draw manager, but retains
|
||
`LandSurfaceManager`'s layer-indexed `TerrainTex.TexTiling` contract. The
|
||
36-entry table is uploaded to `terrain_modern.frag`; base, overlay, and road
|
||
layers each use their owning repeat count while alpha masks stay at cell scale.
|
||
Retail `TexMerge::CopyAndTile` (`0x00503580`) and `TexMerge::Merge`
|
||
(`0x005038C0`) are the behavior oracle; see
|
||
`docs/research/2026-07-13-retail-terrain-texture-tiling-pseudocode.md`.
|
||
|
||
### Scenery (procedural placement: trees, bushes, rocks, fences)
|
||
|
||
| Component | What it does |
|
||
|---|---|
|
||
| `SceneryRenderManager` | Generate + render per-vertex scenery |
|
||
| `SceneryHelpers` | Displace / RotateObj / ScaleObj / ObjAlign / CheckSlope |
|
||
| `SceneryInstance` | Per-spawn instance data |
|
||
|
||
acdream's streamed projection assigns generated instances local runtime IDs
|
||
through `AcDream.Core.World.ProceduralSceneryIdAllocator`. The namespace is
|
||
`0x8XXYYIII` (full X/Y bytes plus a 12-bit counter); bit 31 remains the stable
|
||
scenery classification seam. This is projection identity, not placement
|
||
behavior. The former 8-bit counter rejected dense retail-DAT landblocks before
|
||
their render transaction could publish (#218).
|
||
|
||
### Static objects (buildings, slabs, props — Setup + GfxObj + ObjDesc)
|
||
|
||
| Component | What it does |
|
||
|---|---|
|
||
| `StaticObjectRenderManager` | Master pipeline for static objects |
|
||
| `ObjectRenderManagerBase` + `BaseObjectRenderManager` | Common render base |
|
||
| `ObjectMeshManager` | Mesh extraction from Setup/GfxObj, ObjDesc application |
|
||
|
||
### Dungeons / interiors
|
||
|
||
| Component | What it does |
|
||
|---|---|
|
||
| `EnvCellRenderManager` | Dungeon interior cell geometry |
|
||
| `PortalRenderManager` | Portal traversal / visibility |
|
||
|
||
### Sky + atmosphere
|
||
|
||
| Component | What it does |
|
||
|---|---|
|
||
| `SkyboxRenderManager` | Skybox rendering |
|
||
| `ParticleEmitterRenderer` + `ParticleBatcher` + `ActiveParticleEmitter` | Particle systems (sky particles, weather, magic) |
|
||
|
||
### Visibility / culling
|
||
|
||
| Component | What it does |
|
||
|---|---|
|
||
| `VisibilityManager` + `VisibilitySnapshot` | Frustum + cell visibility |
|
||
| `Frustum` | Frustum-cull math |
|
||
|
||
### Other rendering helpers
|
||
|
||
| Component | What it does |
|
||
|---|---|
|
||
| `MinimapRenderer` | Top-down minimap |
|
||
| `GlobalMeshBuffer` | Shared GPU mesh buffer; one reclaimable vertex/index allocation per mesh, released by `ObjectMeshManager` eviction |
|
||
| `GpuResourceManager` | GPU resource lifecycle |
|
||
| `InstanceData` | Instanced draw data |
|
||
| `TextureHelpers` | INDEX16, P8, BGRA, DXT decode + alpha (canonical port) |
|
||
| `DebugRenderer` + `DebugRendererLineDrawer` + `EdgeLineBuilder` | Debug primitives |
|
||
|
||
### Shaders (22 total)
|
||
|
||
Located at `Chorizite.OpenGLSDLBackend/Shaders/`:
|
||
|
||
`Landscape.{vert,frag}` · `StaticObject.{vert,frag}` · `StaticObjectModern.{vert,frag}` · `Particle.{vert,frag}` · `PortalStencil.{vert,frag}` · `Outline.{vert,frag}` · `Simple3D.{vert,frag}` · `InstancedLine.{vert,frag}` · `Text.{vert,frag}` · `UI.{vert,frag}` · `Gizmo.{vert,frag}` (editor-only)
|
||
|
||
---
|
||
|
||
## 🟢 LOW-LEVEL GL / FRAMEWORK — take or replace with our own
|
||
|
||
Either take WB's wrappers wholesale, or keep our own and adapt the
|
||
render managers to use ours. These wrappers are stateless or
|
||
near-stateless and are the easiest to swap.
|
||
|
||
| Component | What it does |
|
||
|---|---|
|
||
| `OpenGLGraphicsDevice` | Silk.NET.OpenGL wrapper |
|
||
| `OpenGLRenderer` | Render orchestration |
|
||
| `GLSLShader` | Shader compile/link/uniforms |
|
||
| `GLHelpers` + `GLStateScope` | GL state utility |
|
||
| `ManagedGLFrameBuffer` / `ManagedGLIndexBuffer` / `ManagedGLTexture` / `ManagedGLTextureArray` / `ManagedGLUniformBuffer` / `ManagedGLVertexArray` / `ManagedGLVertexBuffer` | GL resource wrappers |
|
||
| `TextureParameters` | Sampler config |
|
||
| `GpuMemoryTracker` | Memory tracking |
|
||
| `Camera2D` / `Camera3D` / `CameraBase` / `ICamera` / `CameraController` | Camera primitives |
|
||
| `GameScene` + `SingleObjectScene` + `SceneData` + `ModernRenderData` + `RenderPass` | Scene / pass structures |
|
||
|
||
---
|
||
|
||
## 🟢 GEOMETRY / MATH UTILS — take wholesale
|
||
|
||
| Component | File |
|
||
|---|---|
|
||
| `TerrainUtils` (OnRoad, GetNormal, GetHeight, GetRoad, palCode) | `WorldBuilder.Shared/Modules/Landscape/Lib/TerrainUtils.cs` |
|
||
| `TerrainCacheManager` | `…/Lib/TerrainCacheManager.cs` |
|
||
| `TerrainRaycast` | `…/Lib/TerrainRaycast.cs` |
|
||
| `GeometryUtils` | `WorldBuilder.Shared/Lib/GeometryUtils.cs` |
|
||
| `RaycastingUtils` (ray-vs-sphere/AABB/triangle) | `WorldBuilder.Shared/Lib/RaycastingUtils.cs` |
|
||
| `DoubleNumerics` (double-precision Vector/Matrix) | `WorldBuilder.Shared/Lib/DoubleNumerics.cs` |
|
||
| `DatUtils` | `WorldBuilder.Shared/Lib/DatUtils.cs` |
|
||
| `BoundingBoxExtensions` | `Chorizite.OpenGLSDLBackend/Lib/BoundingBoxExtensions.cs` |
|
||
|
||
---
|
||
|
||
## 🟢 DATA MODELS — take selectively
|
||
|
||
| Component | What it does |
|
||
|---|---|
|
||
| `RegionInfo` | Landblock metadata wrapper (LandblockSizeInUnits, CellSizeInUnits, etc.) |
|
||
| `TerrainEntry` | Per-vertex terrain (Type/Scenery/Road/Height) |
|
||
| `MergedLandblock` | Merged dat data |
|
||
| `CellSplitDirection` | SW-NE vs NE-SW |
|
||
| `Cell` | Generic cell wrapper |
|
||
| `ObjectId` | Object identifier |
|
||
| `Position` | World position |
|
||
| `ACEnums` | AC-specific enums |
|
||
| `WbBuildingPortal` / `WbCellPortal` | Portal structures |
|
||
| `BuildingObject` | Building data |
|
||
|
||
---
|
||
|
||
## 🟡 EDITOR-ONLY — leave behind / delete in fork
|
||
|
||
These exist for the editor experience and have no place in a game
|
||
client. Delete in fork.
|
||
|
||
- **`Modules/Landscape/Tools/*`** — `BrushTool`, `BucketFillTool`,
|
||
`RoadLineTool`, `RoadVertexTool`, `InspectorTool`,
|
||
`ObjectManipulationTool`, `Gizmo*` (DragHandler, HitTester, Renderer,
|
||
State), `TexturePainting*`, `SceneRaycaster`,
|
||
`LandscapeBrush`, `LandscapeToolBase`, `LandscapeToolContext`,
|
||
`IToolSettingsProvider`, `ILandscapeBrush`, `ILandscapeEditorService`,
|
||
`ILandscapeRaycastService`, `ILandscapeTool`, `ITexturePaintingTool`
|
||
- **`Modules/Landscape/Commands/*`** — undo/redo command pattern for
|
||
editor (Add/Delete/Move/Rename/Reorder/etc.)
|
||
- **`LandscapeDocument` + `LandscapeLayer` + `LandscapeLayerGroup` + `LandscapeChunk` + `LandscapeLayerChunk` + `LandscapeLayerBase`** — editor document model
|
||
- **`Modules/Landscape/Models/TerrainPatch*` + `LandblockChangedEventArgs`** — editor mutation events
|
||
- **`Modules/Landscape/Services/ILandscapeCacheService` + `ILandscapeDataProvider` + `ILandscapeObjectService` + impls** — editor data flow
|
||
- **All `Migrations/*`** — SQLite schema migrations (project file format)
|
||
- **`Repositories/*`** + **`Services/*`** — project storage, dat repository, AceDb, SignalR sync, document manager, undo stack, world coordinates, keyword DB, project migration, semantic kernel AI helpers
|
||
- **`Hubs/*`** — collaborative editing via SignalR
|
||
- **`StaticObject` (editor model)** — replace with our own scene-state data model fed from network
|
||
- **`BackendGizmoDrawer` + `GizmoRenderer`** — editor gizmos
|
||
- **`ProjectStructures, IProject, Project`** — editor project files
|
||
- **`KeyBinding`** — editor input binding
|
||
- **`ViewportInputEvent[Extensions]`** — editor viewport input
|
||
- **`EditorState`** — editor state container
|
||
|
||
---
|
||
|
||
## 🟡 AUDIO / FONT — we already have alternatives
|
||
|
||
Keep ours; don't take theirs.
|
||
|
||
- **`AudioPlaybackEngine`** — uses MP3Sharp. We have OpenAL.
|
||
- **`FontRenderer`** — uses ImageSharp. We have BitmapFont/StbTrueTypeSharp + ImGui.
|
||
|
||
---
|
||
|
||
## 🔴 NOT IN WORLDBUILDER — port from retail decomp ourselves
|
||
|
||
WorldBuilder is a dat editor; it does not have:
|
||
|
||
- **Network protocol** — UDP framing, ISAAC, packet codec, ACE message
|
||
layer (we have this; oracle is `references/holtburger`)
|
||
- **Physics** — collision (CPhysicsObj transitions, BSP queries, sphere
|
||
sweeps), step-up, walkable validation (we have partial; oracle is the
|
||
retail decomp at `docs/research/named-retail/`)
|
||
- **Animation** — motion sequencer, cycle/non-cycle parts, animation
|
||
frame interpolation (we have this; oracle is retail decomp)
|
||
- **Movement** — local player WASD → MoveToState wire, remote-entity
|
||
motion via UpdateMotion + dead-reckoning (we have this; oracle is
|
||
`references/holtburger` + retail decomp)
|
||
- **Game UI** — chat, vitals, inventory, spell book, allegiance, options
|
||
(we have this; ImGui-based today, custom-toolkit later)
|
||
- **Plugin API** — `IGameState`, `IEvents`, `IActions`, `IPacketPipeline`,
|
||
`IOverlay` (we have this — acdream-unique)
|
||
- **Game events** — combat, allegiance, spell casting, quest events
|
||
(we have this; oracle is ACE for opcodes + retail for client behavior)
|
||
- **Audio** — OpenAL pipeline, sound triggers (we have this)
|
||
- **TurbineChat** + **slash commands** (we have this)
|
||
- **Login + character selection flow** (we have this)
|
||
- **World-object mouse selection** — WorldBuilder supplies mesh/DAT access but
|
||
no retail client picker. Our narrow `RetailSelectionGeometryCache` reuses
|
||
`DatCollection` to expose each GfxObj drawing-BSP root sphere and visual
|
||
polygons; `WbDrawDispatcher` supplies the normal draw's current part
|
||
transforms to the named-retail selection accumulator.
|
||
|
||
---
|
||
|
||
## What this means for the workflow (post-Phase O)
|
||
|
||
The CLAUDE.md "grep named → decompile → verify → port" workflow stays
|
||
the rule for everything in the 🔴 list (network, physics, animation,
|
||
movement, UI, plugin, audio, chat).
|
||
|
||
For anything in 🟢 that we've already extracted: **the code is in our
|
||
tree at `src/AcDream.{Core,App}/Rendering/Wb/`**. Read it there — don't
|
||
grep `references/WorldBuilder/` unless you want to compare against the
|
||
original. Re-porting from retail decomp when we already have a tested
|
||
port is still how we'd get the scenery edge-vertex bug back.
|
||
|
||
For anything in 🟢 that we have NOT yet extracted: grep
|
||
`references/WorldBuilder/` to find the source, then extract it using the
|
||
Phase O pattern (verbatim copy → adapt constructor to accept
|
||
`IDatCollection` via `DatCollectionAdapter` where needed → add to
|
||
`src/AcDream.App/Rendering/Wb/`). Do NOT add a new project reference back
|
||
to `WorldBuilder.Shared` or `Chorizite.OpenGLSDLBackend` — Phase O
|
||
permanently removed those.
|
||
|
||
When we discover a behavior mismatch with retail (rare — the extracted
|
||
code is the same as the original), the resolution is: reconcile extracted
|
||
code ↔ retail decomp ↔ holtburger ↔ ACE ↔ ACViewer (the existing
|
||
reference hierarchy in CLAUDE.md). Our extracted code ranks at the top
|
||
of that hierarchy for anything 🟢.
|