acdream/docs/architecture/worldbuilder-inventory.md
Erik 749e8ceeb1 fix(rendering): bound portal resource lifetime
Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
2026-07-18 21:35:16 +02:00

25 KiB
Raw Blame History

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/ — GL 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 our IDatCollection to the IDatReaderWriter interface WB's internals expect (O-D7 fallback; ObjectMeshManager has 26 internal _dats.* call sites — above the 20-site inline-swap threshold).

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 → inline BCn/palette 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 (now a thin wrapper owning the staged-queue/worker-pool/Dispose-quiesce lifecycle and all GL upload — constructs one MeshExtractor and delegates every former Prepare* call site to it), ObjectRenderData/ObjectRenderBatch (hold a GL TextureAtlasManager field), TextureAtlasManager, DatCollectionAdapter (concrete DatCollection-backed implementation of the now-Content-namespaced IDatReaderWriter), 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.

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.

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 GL draw per alternating texture. RetailPViewRenderer drains the landscape scope before the optional depth clear, and GameWindow drains the final scope before private viewports/UI. 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.

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, covering eviction and revisit churn. 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.

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.

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 APIIGameState, 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 🟢.