Move pre-window loading, startup application, live settings mutation, toon context, quality reapply, and SettingsVM loans behind one RuntimeSettingsController. Preserve retail command behavior, ordered target publication, draft semantics, and retryable failure convergence while removing duplicate GameWindow state and feature bodies. Co-authored-by: Codex <codex@openai.com>
896 lines
52 KiB
Markdown
896 lines
52 KiB
Markdown
# acdream — Comprehensive Architecture Plan
|
||
|
||
## Vision
|
||
|
||
A modern C# .NET 10 Asheron's Call client that:
|
||
- **Behaves identically to the retail client** — same physics, same
|
||
animations, same terrain, same collision, same network protocol
|
||
- **Looks identical to the retail client** — same meshes, same textures,
|
||
same lighting, same blending, rendered via modern Silk.NET OpenGL
|
||
- **Adds a plugin API** the retail client never had — native C# plugins
|
||
+ Lua macro scripting for player automation
|
||
- **Is NOT a 1:1 C++ port** — uses modern C# patterns (composition over
|
||
inheritance, interfaces, dependency injection) while matching retail
|
||
behavior exactly
|
||
|
||
**Current program status is intentionally not duplicated in this architecture
|
||
document.** Start at [`../README.md`](../README.md), then use the milestone and
|
||
roadmap linked there. This file governs ownership and dependency shape even
|
||
when the active gameplay milestone changes.
|
||
|
||
## Guiding Principle
|
||
|
||
**The code is modern. The behavior is retail.**
|
||
|
||
Every AC-specific algorithm is ported faithfully from the **named retail
|
||
decomp** at `docs/research/named-retail/` — Sept 2013 EoR build PDB
|
||
(18,366 named functions, 5,371 named struct types) + Binary Ninja
|
||
pseudo-C with 99.6% function-name recovery + verbatim retail header
|
||
struct definitions. The older Ghidra `FUN_xxx` chunks at
|
||
`docs/research/decompiled/` (688K lines) remain a fallback for the
|
||
obfuscated/packed minority. The code AROUND those algorithms is modern
|
||
C# with clean architecture. The plugin API exposes game state through
|
||
well-defined interfaces that the retail client never had.
|
||
|
||
---
|
||
|
||
## Layer Architecture
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ LAYER 5: Plugin API │
|
||
│ IGameState, IEvents, IActions, IPacketPipeline, IOverlay │
|
||
│ Plugin host (ALC), Lua macro engine (MoonSharp) │
|
||
│ ► acdream-unique — not in retail client │
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ LAYER 4: Game Objects │
|
||
│ GameEntity (one per world object) │
|
||
│ ├── PhysicsBody (ported from decompiled) │
|
||
│ ├── AnimSequencer (ported from decompiled) │
|
||
│ ├── CellTracker (ported from decompiled) │
|
||
│ ├── AppearanceState (ObjDesc: palettes, textures, parts)│
|
||
│ └── MotionState (ported from decompiled) │
|
||
│ ► behavior matches retail, code is modern C# composition │
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ LAYER 3: World Systems │
|
||
│ TerrainSystem (heightmap, blending, scenery) │
|
||
│ CellSystem (LandCells, EnvCells, portals, BSP) │
|
||
│ StreamingSystem (background loading, LOD, frustum cull) │
|
||
│ ► behavior matches retail, streaming is acdream-unique │
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ LAYER 2: Network │
|
||
│ WorldSession (ISAAC, fragments, game messages) │
|
||
│ MessageRouter (opcode dispatch, sequence tracking) │
|
||
│ ► wire-format identical to retail │
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ LAYER 1: Renderer │
|
||
│ Silk.NET OpenGL 4.3 core profile │
|
||
│ TerrainModernRenderer, WbDrawDispatcher, EnvCellRenderer │
|
||
│ Shaders (terrain blending, mesh lighting, translucency) │
|
||
│ ► completely different from retail (D3D7), same visual │
|
||
│ output │
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ LAYER 0: Platform │
|
||
│ .NET 10, Silk.NET window/input, DatReaderWriter │
|
||
│ ► acdream-unique infrastructure │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### UI Architecture (two coexisting presentation stacks)
|
||
|
||
The 2026-04-24 design began with a swappable renderer abstraction. D.2b proved
|
||
that retail fidelity needs a retained LayoutDesc/DAT tree, while ImGui remains
|
||
valuable as permanent devtools. The current architecture therefore has two
|
||
presentation stacks over shared state, ViewModels, events, and commands. Full
|
||
history and the corrected contract live in
|
||
`docs/plans/2026-04-24-ui-framework.md`.
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ DEVELOPER UI │
|
||
│ IPanel/IPanelRenderer → permanent ImGui devtools │
|
||
│ GAMEPLAY UI │
|
||
│ LayoutDesc/DAT → UiRoot retained widgets + controllers │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ SHARED CONTRACTS │
|
||
│ ViewModels, commands, input actions, state/event services │
|
||
│ ► one model and mutation path, two presentation projections│
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ Game state + events (unchanged) │
|
||
│ IGameState / IEvents / WorldSession — UI only reads │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
`AcDream.UI.Abstractions` owns backend-neutral ViewModels, commands, input,
|
||
and the `IPanel`/`IPanelRenderer` devtools contract. `AcDream.App/UI` owns the
|
||
retained gameplay tree, LayoutDesc importer, window runtime, and panel
|
||
controllers. Neither presentation stack owns independent game-state truth.
|
||
Chat submission follows the same rule: both presentation stacks enter the
|
||
shared `ChatCommandRouter`, which emits distinct backend-neutral intents for a
|
||
retail client command (`ExecuteClientCommandCmd`), an ACE-owned command
|
||
(`SendServerCommandCmd`), or ordinary chat (`SendChatCmd`). App-layer
|
||
handlers and controllers translate those intents to `WorldSession`; panels
|
||
never inspect or construct wire messages.
|
||
Plugins register retained gameplay markup through the BCL-only
|
||
`AcDream.Plugin.Abstractions.IUiRegistry`; they do not import App or ImGui
|
||
assemblies. Core `SelectionState` is the sole selected-object owner for world,
|
||
radar, inventory, paperdoll, toolbar, use/examine consumers, and plugins;
|
||
`IPluginHost.Selection` exposes that same state and retail-style old/new callback.
|
||
Temporary pointer modes are separate App orchestration in `InteractionState` and
|
||
must never become a competing selection owner.
|
||
|
||
External-container state follows the same ownership rule. Core
|
||
`ExternalContainerState` owns the expected/current `ClientUISystem::groundObject`
|
||
identity, Core.Net applies authoritative `ViewContents` and
|
||
`CloseGroundContainer` events, and App `ExternalContainerLifecycleController`
|
||
owns the replacement-only `NoLongerViewingContents` side effect. The retained
|
||
`ExternalContainerController` is a projection over that state; mounting or
|
||
disabling the gameplay UI cannot change the session lifetime.
|
||
|
||
Every retained gameplay window has a typed `RetailWindowHandle` and at most one
|
||
`IRetainedPanelController` lifecycle owner. Multi-controller windows use
|
||
`RetainedPanelControllerGroup`; the manager disposes controllers exactly once in
|
||
reverse ownership order. `UiHost` removes Silk device subscriptions first, then
|
||
disposes the manager/controllers, then its GL renderer. `GameWindow.OnClosing`
|
||
tears this runtime down before session and game-state sources.
|
||
|
||
`RetailUiRuntime` is the production composition boundary. `GameWindow` creates
|
||
the GL/DAT resolvers and supplies focused state/action binding records in one
|
||
mount call; the runtime owns all LayoutDesc imports, controller construction,
|
||
window registration, plugin mounts, cursor feedback, layout persistence, and the
|
||
retained tick/draw/restore/dispose paths. Panel-specific construction must not
|
||
move back into `GameWindow.OnLoad`.
|
||
|
||
Magic follows the same boundary. Core `Spellbook` is the one learned/favorite/
|
||
desired/enchantment state projection; Core.Net owns exact manifest and live
|
||
message parsing; App `SpellCastingController` owns validated cast intent; and
|
||
the retained spell bar, spell/component book, and effects controllers only
|
||
project that state and invoke supplied actions. ACE remains authoritative after
|
||
the targeted/untargeted request for turning, animation, mana/components,
|
||
fizzle, impact, damage, and completion.
|
||
The shared LayoutDesc importer resolves inherited controller trees with retail
|
||
`LayoutDesc::InqFullDesc`/`ElementDesc::Incorporate` child identity semantics;
|
||
panel controllers never reconstruct missing inherited widgets by hand.
|
||
`RetailPanelUiController` is the single retained owner of retail
|
||
`gmPanelUI::RecvNotice_SetPanelVisibility` semantics: one active main-panel
|
||
child, optional DAT-property-driven deferred restoration, and visibility
|
||
reconciliation for persistence. Toolbar buttons and the authored Helpful/
|
||
Harmful effects indicators both send panel IDs into this owner; neither owns a
|
||
parallel window-lifecycle map.
|
||
|
||
---
|
||
|
||
## Project Structure (current + target)
|
||
|
||
```
|
||
src/
|
||
AcDream.Core/ Layer 2-4: no GL, no Silk.NET, pure logic
|
||
Physics/
|
||
PhysicsBody.cs -> body state / integration foundation (done)
|
||
CollisionPrimitives.cs -> retail primitive helpers (partial, active)
|
||
MotionInterpreter.cs -> motion state machine (done, still L.1 polish)
|
||
AnimationSequencer.cs -> animation playback + root-motion data (done, L.1 active)
|
||
TerrainSurface.cs -> triangle-aware terrain contact (done)
|
||
BSPQuery.cs -> partial retail BSP dispatcher (active in L.2)
|
||
TransitionTypes.cs -> SpherePath / CollisionInfo / transition helpers (active in L.2)
|
||
PhysicsDataCache.cs -> GfxObj / Setup / CellStruct collision data (done, active)
|
||
ShadowObjectRegistry.cs -> broadphase for nearby physics objects (active)
|
||
PhysicsEngine.cs -> ResolveWithTransition active player path
|
||
ProjectilePhysicsStepper.cs -> pure retail projectile integration + sweep
|
||
RetailFrameMath.cs -> retail full-3D vector-heading frame construction
|
||
CellBsp.cs -> not a first-class runtime owner yet (L.2e)
|
||
World/
|
||
GameEntity.cs -> target unified entity, not current reality
|
||
WorldState.cs -> target entity owner
|
||
CellTracker.cs -> target per-entity cell management
|
||
SceneryGenerator.cs -> verified against decompiled (done)
|
||
LandblockLoader.cs -> done
|
||
Terrain/
|
||
LandblockMesh.cs -> verified against ACME (done)
|
||
TerrainBlending.cs -> verified against ACME (done)
|
||
Meshing/
|
||
GfxObjMesh.cs -> cross-checked against ACME (done)
|
||
SetupMesh.cs -> cross-checked (done)
|
||
Textures/
|
||
SurfaceDecoder.cs -> done
|
||
Dat/
|
||
MotionResolver.cs -> done (target move from Meshing/)
|
||
|
||
AcDream.Core.Net/ Layer 2: networking
|
||
WorldSession.cs -> done (wire-compatible with ACE)
|
||
NetClient.cs -> done
|
||
Messages/ -> done (CreateObject, MoveToState, etc.)
|
||
|
||
AcDream.Plugin.Abstractions/ Layer 5: plugin interfaces
|
||
IAcDreamPlugin.cs -> done
|
||
IPluginHost.cs -> done
|
||
IGameState.cs -> done
|
||
IEvents.cs -> done
|
||
ISelectionService.cs -> done
|
||
|
||
AcDream.App/ Layer 1 + Layer 4 wiring
|
||
Physics/
|
||
ProjectileController.cs -> live-record projectile orchestration/corrections
|
||
RemotePhysicsUpdater.cs -> ordinary/Hidden remote narrow-tick integration
|
||
LiveEntityOrdinaryPhysicsUpdater.cs -> manager-less body Transition commits
|
||
RemoteInboundMotionDispatcher.cs -> animation-optional retail UM funnel
|
||
RemoteTeleportController.cs -> incarnation-scoped loaded/pending placement owner
|
||
RemoteTeleportHook.cs -> ordered retail teleport teardown seam
|
||
RemoteTeleportPlacement.cs -> collision-seated SetPosition transition commit
|
||
World/
|
||
LiveEntityRuntime.cs -> canonical identity/state/body/projectile/spatial ownership
|
||
RetailInboundEventDispatcher.cs -> update-thread packet/frame FIFO barrier
|
||
LiveEntityPresentationController.cs -> Hidden/NoDraw/effect/collision presentation
|
||
LiveEntityTeardown.cs -> failure-isolated multi-owner lifecycle drain
|
||
RetailLiveFrameCoordinator.cs -> object/physics-before-network frame barrier
|
||
Rendering/
|
||
GameWindow.cs -> still owns too much runtime wiring
|
||
LiveEntityAnimationScheduler.cs -> canonical ordinary live-object workset
|
||
RetailStaticAnimatingObjectScheduler.cs -> retail static-animation workset
|
||
StaticLiveRootCommitter.cs -> static root pose/collision commit boundary
|
||
TerrainModernRenderer.cs -> mandatory bindless+MDI terrain path
|
||
Wb/WbDrawDispatcher.cs -> ordinary live/static entity draw dispatch
|
||
Wb/EnvCellRenderer.cs -> indoor cell-shell draw path
|
||
ParticleRenderer.cs -> DAT particle billboard/full-mesh dispatch
|
||
RetailAlphaQueue.cs -> shared delayed world alpha ordering/flush owner
|
||
TextureCache.cs -> done
|
||
ChaseCamera.cs -> done
|
||
FlyCamera.cs -> done
|
||
Streaming/
|
||
StreamingController.cs -> done
|
||
GpuWorldState.cs -> done
|
||
WorldRevealCoordinator.cs -> shared login/portal lifetime owner
|
||
WorldRevealReadinessBarrier.cs -> canonical destination predicate
|
||
Input/
|
||
PlayerMovementController.cs -> active movement driver
|
||
Plugins/
|
||
AppPluginHost.cs -> done
|
||
```
|
||
|
||
---
|
||
|
||
## Movement And Collision Architecture
|
||
|
||
Phase L.2 is the current organizing program for physics, collision,
|
||
boundaries, buildings, sliding, cell ownership, movement packets, and server
|
||
authority. Detailed plan: `docs/plans/2026-04-29-movement-collision-conformance.md`.
|
||
|
||
The active player movement spine is:
|
||
|
||
```text
|
||
InputDispatcher / PlayerMovementController
|
||
-> MotionInterpreter + local body prediction
|
||
-> PhysicsEngine.ResolveWithTransition
|
||
-> TransitionTypes + BSPQuery + ShadowObjectRegistry
|
||
-> ResolveResult contact/cell state
|
||
-> MoveToState / AutonomousPosition outbound messages
|
||
-> WorldSession server echo or correction handling
|
||
```
|
||
|
||
What exists and is active:
|
||
|
||
- `PhysicsEngine.ResolveWithTransition` is the path used for local player
|
||
collision resolution and the shared continuous object sweep.
|
||
- `ProjectilePhysicsStepper` is the pure Core driver for retail projectile
|
||
clock quanta, final-state acceleration, velocity/angular integration,
|
||
AlignPath, Setup-sphere transition sweeps, and collision response. It owns no
|
||
live entity, network, rendering, or App state; the App controller supplies
|
||
those boundaries.
|
||
- `ProjectileController` is that App boundary. It installs one body on the
|
||
canonical `LiveEntityRecord`, advances it on the update/render thread,
|
||
commits through `WorldEntity.SetPosition`, publishes the effect root,
|
||
synchronizes the existing shadow, and calls `RebucketLiveEntity` only when
|
||
the full cell changes. Pending/leave-world residence suspends the shadow's
|
||
cell rows while retaining its registration for exact re-entry. Timestamp-
|
||
gated State/Vector/Position/Movement packets correct or stop that same body;
|
||
every production MovementManager delegates full-cell reads/writes to the
|
||
incarnation-scoped live record, and projectile acquisition adopts its body
|
||
without replaying vectors already installed when that body was created,
|
||
regardless of component creation order. Accepted SetState flags reach that
|
||
canonical body before optional projectile acquisition, so absent/unsupported
|
||
DAT shape data cannot leave collision state stale; a non-finite local receipt
|
||
clock uses the controller's last finite game-clock value and cannot consume
|
||
first-time Missile classification. Shared-body acquisition validates the
|
||
adopted body's current frame/vectors, not obsolete CreateObject inputs.
|
||
Initial and late Setup resolution share GameWindow's `_datLock`, preserving
|
||
the single-reader `DatCollection` invariant while streaming is active.
|
||
Generic remote translation is suppressed while projectile integration
|
||
owns the frame;
|
||
delete, generation replacement, pickup/parent leave-world, and session reset
|
||
use `LiveEntityRuntime`'s normal lifecycle and never create a second GUID map.
|
||
- `LiveEntityAnimationScheduler` is the one update-thread walk over spatial
|
||
live roots. It admits each incarnation's `RetailObjectQuantumClock`, advances
|
||
PartArray state, then selects exactly one movement owner: remote motion, the
|
||
retained projectile body, or `LiveEntityOrdinaryPhysicsUpdater` for a
|
||
manager-less canonical body. Every hook boundary revalidates record,
|
||
component, and object-clock epoch before transition, manager-tail, pose, or
|
||
rebucket commits. `RetailStaticAnimatingObjectScheduler` remains a separate
|
||
workset, matching `CPhysics::UseTime`: Setup DefaultAnimation is installed at
|
||
PartArray construction, while DAT and live PhysicsState-Static owners advance
|
||
whole elapsed intervals through `animate_static_object` semantics. A live
|
||
static owner shares the record's canonical PartArray and PhysicsBody; raw
|
||
static omega commits the root, effect pose, and collision shadow together,
|
||
while zero omega does not reflood collision and Hidden never restores it.
|
||
The typed animation view copies the runtime's concrete spatial dictionary
|
||
through reusable storage, so update and render classification allocate
|
||
nothing and scale with resident owners rather than retained KnownObjects.
|
||
- `RemoteInboundMotionDispatcher` is the single remote
|
||
`MovementManager::unpack_movement` owner. Animated and animation-less live
|
||
objects differ only by the presence of a PartArray dispatch sink; packet
|
||
head interrupt/style routing, MoveTo cases, case-0 wholesale state, sticky,
|
||
and long-jump ordering cannot drift between two `GameWindow` branches.
|
||
- Ordinary remote/manager-less Transition commits publish the resolved body,
|
||
`WorldEntity` root, and full cell before invoking the canonical rebucket
|
||
writer. That writer is a re-entrant lifetime boundary: the caller immediately
|
||
revalidates the exact record/runtime and publishes no collision shadow or
|
||
manager tail if the destination became pending or the GUID was replaced.
|
||
`LiveEntityPresentationController` owns the symmetric non-projectile
|
||
collision-residency edge: loaded-to-pending suspends the retained shadow,
|
||
hydration restores it immediately, and `OnLiveEntityReady` reconciles an
|
||
object that materialized pending before its collision registration existed.
|
||
Direct local-player and authoritative remote publication use the same
|
||
ordering: commit root, rebucket, then prove exact-record spatial residency
|
||
before touching collision. Remote shadows are pose/cell-gated, including
|
||
complete sign-invariant orientation for offset/multipart Setup shapes and a
|
||
forced refresh on cell-only transitions. Projectile residency remains
|
||
exclusively in `ProjectileController`.
|
||
- `RetailInboundEventDispatcher` wraps every live-object session callback and
|
||
each complete live-object frame phase. Synchronous App observers can enqueue
|
||
another packet, but that packet cannot interleave halfway through the older
|
||
packet's collision, rebucket, shadow, pose, hook, or manager tail. The live
|
||
record retains independent Position, State, Vector, and Movement authority
|
||
versions plus a narrow shared velocity version for the three packet families
|
||
that can install velocity. This mirrors retail's single update-thread FIFO
|
||
without incorrectly making an accepted State cancel an accepted Position.
|
||
Its direct frame/packet path accepts state plus a cached static callback and
|
||
performs zero allocation; only real nested reentrancy materializes a queued
|
||
heterogeneous operation.
|
||
- Projectile prediction is a two-phase transaction around retail's
|
||
`process_hooks` slot. `BeginQuantum` holds a candidate while leaving the
|
||
canonical body at its begin frame; `CompleteQuantum` commits only if the
|
||
exact record/runtime and prediction version still match. Position, Vector,
|
||
and State corrections therefore win if accepted between the two halves.
|
||
Teleport placement uses the same exact-record and per-channel authority
|
||
checks, so a newer Position supersedes an older resolver while an independent
|
||
State is preserved alongside the completed placement.
|
||
- `EntityEffectPoseRegistry` assigns a monotonic lifetime to every local-ID
|
||
incarnation. `AnimationHookFrameQueue` snapshots it before semantic
|
||
`AnimationDone` callbacks and revalidates it before every semantic completion
|
||
and every routed hook. A delete/local-ID reuse during capture or during an
|
||
earlier hook can never advance the displaced sequencer or send the old
|
||
owner's remaining sound, particle, or light hooks to its replacement.
|
||
- `RemoteTeleportController` owns the placement half of a fresh remote
|
||
teleport after `RemoteTeleportHook` has torn down movement/target state. It
|
||
collision-seats loaded destinations through `RemoteTeleportPlacement`; an
|
||
unloaded destination retains one generation- and PositionSequence-scoped
|
||
pending placement and resolves the latest accepted frame when that same
|
||
projection becomes visible. It neither owns GUID identity nor reconstructs
|
||
an entity. A placement failure after hydration restores the captured source
|
||
frame/cell/contact rather than leaving a visible collisionless projection;
|
||
source-resident shadows restore immediately, while an unloaded source
|
||
delegates one incarnation-scoped restore to
|
||
`LiveEntityPresentationController`, shared with Hidden/UnHide. A newer
|
||
placement transfers that marker into an explicit active-placement generation
|
||
before rebucketing even while Hidden. That generation suppresses every
|
||
intervening Hidden/UnHide and projection restore until the controller reaches
|
||
a stable result, then it either restores after collision seating, re-defers
|
||
its rollback source, or hands a Hidden result back for UnHide. The typed
|
||
`ILiveEntityRemotePlacementRuntime` seam permits a same-incarnation wrapper
|
||
rebind only around the canonical body; pending placement adopts that wrapper
|
||
before hydration and cannot silently lose ownership. Clearing a motion or
|
||
projectile component retains the incarnation's body/contract identity until
|
||
logical teardown. The production wrapper exposes an immutable body, while
|
||
hydration defensively validates arbitrary implementations against the record
|
||
and rolls the retained body back on mismatch. Runtime binding snapshots the
|
||
interface Body getter once for validation, assignment, and state mutation.
|
||
`GpuWorldState`
|
||
rebuckets atomically and commits spatial visibility before draining its
|
||
transition FIFO; `LiveEntityRuntime` rejects delayed duplicate edges. A
|
||
rollback/rebucket inside a visibility observer therefore cannot expose a
|
||
remove/add pulse, leave stale final visibility, or reorder the final
|
||
presentation edge. Observer failures are reported only after both spatial and
|
||
canonical runtime cell/projection state have committed.
|
||
Every projection operation also carries a per-record mutation token; if a
|
||
synchronous observer replaces the GUID or reprojects that same record, the
|
||
displaced outer operation cannot overwrite the newer cell or presentation
|
||
indices.
|
||
- `BSPQuery` contains the partial retail-style BSP collision dispatcher used by
|
||
the transition path.
|
||
- `TransitionTypes` carries `SpherePath`, `CollisionInfo`, `ObjectInfo`,
|
||
transition validation, step-up/down, contact-plane handling, and partial
|
||
slide behavior.
|
||
- `PhysicsDataCache` loads GfxObj, Setup, and CellStruct physics data from DATs.
|
||
- `ShadowObjectRegistry` gives movement a broadphase over nearby objects and
|
||
buildings.
|
||
- `TerrainSurface` uses triangle-aware terrain contact; older "bilinear terrain
|
||
Z" descriptions are historical B.3 language, not current architecture.
|
||
|
||
What remains incomplete:
|
||
|
||
- `CELLARRAY`, `CObjCell::find_cell_list`, adjacent-cell checks, and low outdoor
|
||
cell id updates across 24m seams.
|
||
- `cell_bsp` / `CellBSP` as the authoritative runtime owner for indoor and
|
||
building collision.
|
||
- Building portal transit and normal walking through building entry/exit
|
||
boundaries.
|
||
- Full retail `edge_slide`, `cliff_slide`, `precipice_slide`, and `NegPolyHit`
|
||
dispatch behavior.
|
||
- Exact `CSphere` / `CCylSphere` object-shape parity, especially for live
|
||
entities that currently collapse to a simplified cylinder fallback.
|
||
- Routine local/server correction diagnostics. ACE accepting a position is a
|
||
compatibility signal, not proof of fine retail collision parity.
|
||
|
||
Ownership by phase:
|
||
|
||
- B.3 is shipped MVP history: first resolver foundation and tests.
|
||
- L.1 owns animation/motion parity, including root-motion coupling.
|
||
- L.2 owns the movement/collision conformance stack listed above.
|
||
- G.3 owns dungeon streaming and portal-space delivery after L.2e gives it
|
||
trustworthy cell/building boundaries.
|
||
|
||
---
|
||
|
||
## GameEntity: The Unified Entity (target refactor)
|
||
|
||
`LiveEntityRuntime` is now the shipped bridge to this target. It owns one
|
||
`LiveEntityRecord` per accepted server-object incarnation, ServerGuid-to-local-id
|
||
translation, accepted snapshots/timestamp gates, the canonical `PhysicsBody`,
|
||
animation/remote-motion/projectile components, parent-event state,
|
||
effect-profile defaults, and exact logical
|
||
teardown. Live `PhysicsDesc` effect fields replace Setup defaults on that same
|
||
record; rebucketing never recreates them. `GpuWorldState`
|
||
owns spatial buckets only: register/rebucket/unregister are separate operations,
|
||
logical teardown removes the exact `WorldEntity` incarnation rather than every
|
||
projection sharing its server GUID, and per-GUID/session mutation epochs prevent
|
||
a callback from resurrecting an outer CreateObject after delete/reset. Per-GUID
|
||
epoch tombstones remain until session clear, so delete-then-create cannot repeat
|
||
an outer operation's epoch (the ABA problem). Resource registration is an atomic
|
||
boundary, superseded cleanup failures surface at the runtime boundary, and
|
||
visibility observers are failure-isolated without interrupting canonical commits.
|
||
Landblock reloads reuse the same `WorldEntity` without replaying renderer or
|
||
script creation. Its canonical materialized view remains stable across pending
|
||
landblocks, while a separate visible-only view feeds radar, picking, status, and
|
||
targeting. Raw server PhysicsState and the final state produced by retail's
|
||
ordered Lighting/NoDraw/Hidden side effects are stored separately.
|
||
`LiveEntityPresentationController` drains those accepted transitions only after
|
||
the renderer/effect owner is ready: Hidden suppresses the retained root's mesh,
|
||
collision, interaction, radar, and target eligibility while preserving the
|
||
logical record, scripts, particles, lights, full cell, and local identity.
|
||
Direct attached children receive retail's NoDraw mutation, and typed
|
||
Hidden/UnHide effects resolve through the live PhysicsScriptTable.
|
||
Pickup/parent leave-world clears cell membership and pauses root
|
||
movement/animation without destroying retained owners. `GameWindow` retains
|
||
storage-free typed views while its large feature loops are extracted.
|
||
|
||
DAT decoding for runtime VFX crosses one narrow `AcDream.Content.Vfx` seam.
|
||
`RetailAnimationLoader` and `RetailPhysicsScriptLoader` still read through the
|
||
single shared `DatCollection`, delegate ordinary animation-hook schemas to
|
||
Chorizite.DatReaderWriter, and replace only its incomplete
|
||
`CreateBlockingParticleHook` model with retail's inherited CreateParticle
|
||
payload. Core consumes the loaders through delegates/interfaces and therefore
|
||
does not depend on Content or a database implementation.
|
||
|
||
Runtime effect delivery is owned by App-layer `EntityEffectController`. It
|
||
retains pre-materialization F754/F755 packets in one mixed FIFO per server GUID,
|
||
resolves the canonical local ID through `LiveEntityRuntime` only after
|
||
renderer/resources/profile are ready, and resolves typed/default/part-default
|
||
hooks through the live owner's current `EntityEffectProfile`. It does not own a
|
||
second GUID map. The same profile publishes Setup/static or network/live
|
||
SoundTable ownership at readiness and on every same-generation PhysicsDesc
|
||
replacement, including present-zero clearing. Core `PhysicsScriptRunner` mirrors retail's one
|
||
serial FIFO per owner: duplicate plays append, owners progress independently,
|
||
`CallPES` uses an injected uniform delay, and every hook traverses the shared
|
||
`AnimationHookRouter`. Existing queues on cell-less or Frozen live roots do
|
||
not advance; a new ordinary F754/F755/default play for an already-created
|
||
cell-less object is dropped as in retail. Hidden/UnHide is the retail exception:
|
||
`CPhysicsObj::set_hidden` resolves the typed script and calls
|
||
`play_script_internal`, queuing it until owner eligibility returns. Attached
|
||
children advance through their eligible parent. Every effect
|
||
resource uses the canonical, globally unique `WorldEntity.Id`; static
|
||
allocators fail before their namespace can wrap. `EntityEffectPoseRegistry`
|
||
publishes the final root and indexed rigid animated-part transforms after animation
|
||
and equipped-child composition. Animation hooks are captured during sequence
|
||
advance and drained only after those poses are current; PhysicsScripts then run,
|
||
attached emitters and object lights refresh, and particle simulation advances.
|
||
World-released particles retain their birth positions and parent-local particles
|
||
follow their current owner while in-world. Pending spatial projections skip
|
||
particle updates and drawing exactly like retail's cell-less object gate without
|
||
ending emitter state or identity; absolute creation timestamps remain unchanged,
|
||
so elapsed particles/durations expire on re-entry without a backlog burst. Their
|
||
anchors may still accept authoritative pose correction before that update.
|
||
Missing emitter DAT records fail diagnostically
|
||
without a synthesized effect. `LiveEntityLightController` keeps live light
|
||
projection/re-entry outside `GameWindow`; `LightingHookSink` remains the Core
|
||
hook and per-frame pose consumer. It also owns retail's Lighting-bit/
|
||
`SetLightHook` latch so spatial withdrawal preserves logical state while a
|
||
true state transition creates or destroys the Setup lights. Stable indexed
|
||
poses exclude Setup visual scale and remain separate from drawable `MeshRefs`.
|
||
Light registration follows final runtime visibility edges; equipped-child
|
||
updates are parent-before-child with retained per-child buffers and cascade
|
||
withdrawal when an ancestor pose disappears. A later pose publication drains
|
||
the waiting attachment graph transitively, so A→B→C recovers B before C in the
|
||
same parent-first pass. Recovery is edge-triggered by object/appearance/pose
|
||
publication; permanent missing DAT or holding parts are never polled per frame.
|
||
|
||
The remaining aggregation is primarily `_playerController`'s player-specific
|
||
movement plus the separate `WorldEntity`/animation/physics component types.
|
||
Those should become ONE class:
|
||
|
||
```csharp
|
||
public sealed class GameEntity
|
||
{
|
||
// Identity
|
||
public uint ServerGuid { get; }
|
||
public uint SetupId { get; }
|
||
public string? Name { get; }
|
||
|
||
// Spatial (ported from CPhysicsObj)
|
||
public PhysicsBody Physics { get; } // position, velocity, gravity
|
||
public CellTracker Cell { get; } // which cell we're in
|
||
|
||
// Appearance (ported from CPartArray)
|
||
public AnimationSequencer Animation { get; } // frame playback
|
||
public AppearanceState Appearance { get; } // ObjDesc overrides
|
||
|
||
// Motion (ported from CMotionInterp)
|
||
public MotionInterpreter Motion { get; } // walk/run/turn state
|
||
|
||
// Render output (consumed by WbDrawDispatcher)
|
||
public IReadOnlyList<MeshRef> MeshRefs { get; }
|
||
|
||
// Per-frame update (matches retail update_object)
|
||
public void Update(float dt)
|
||
{
|
||
Motion.ApplyCurrentMovement(); // set velocity from motion state
|
||
Physics.UpdateObject(dt); // integrate position
|
||
PhysicsEngine.ResolveWithTransition(); // current L.2 collision spine
|
||
Cell.UpdateCell(Physics.Position); // target: retail cell ownership
|
||
Animation.Advance(dt); // advance animation frames
|
||
RebuildMeshRefs(); // compute per-part transforms
|
||
}
|
||
}
|
||
```
|
||
|
||
Target state: every entity in the world — player, NPC, monster, lifestone,
|
||
door, chest — becomes a `GameEntity`. The renderer iterates them and draws.
|
||
The plugin API exposes them as `WorldEntitySnapshot`. GameWindow becomes thin.
|
||
|
||
Lifecycle invariant in both the current split model and the target `GameEntity`:
|
||
an `ObjDescEvent` changes appearance in place. It may replace resolved meshes,
|
||
palette ranges, part overrides, and visual bounds, but it must preserve entity
|
||
identity plus animation, motion, physics, collision, selection, and
|
||
dead-reckoning owners. Only a real delete/despawn tears those owners down. This
|
||
matches retail's `CPhysicsObj::DoObjDescChangesFromDefault` behavior.
|
||
|
||
### Runtime resource ownership and bounded residency
|
||
|
||
Logical lifetime, spatial residency, and physical GPU lifetime are separate
|
||
contracts. A live entity or landblock owns stable logical references; moving it
|
||
between buckets does not reacquire resources. Appearance replacement acquires
|
||
the complete new mesh/texture set before publication, then releases the old set.
|
||
Despawn and landblock demotion withdraw every public render reference before
|
||
their physical resources become reclaimable. All of these transactions are
|
||
retryable and generation-scoped, so a failed release cannot silently strand a
|
||
half-retired owner or affect a reused server GUID.
|
||
|
||
Runtime content residency is deliberately bounded rather than proportional to
|
||
every region visited:
|
||
|
||
- `RuntimeDatCollectionFactory` keeps DAT indexes on demand but uses
|
||
`FileCachingStrategy.Never`; `DatCollection` remains the sole raw reader.
|
||
- Each typed DAT facade has a 256-entry / 64 MiB estimated LRU. Unknown object
|
||
graphs are conservatively charged at least 128 KiB. Canonical decoded texture
|
||
pixels use a separate 128-entry / 64 MiB cache.
|
||
- Standalone bindless textures retain at most 256 unowned entries / 32 MiB and
|
||
retire at most one per frame. Owner-scoped composite textures use a 64 MiB
|
||
unowned budget and 128 MiB physical budget, admitting at most 16 uploads or
|
||
8 MiB per frame.
|
||
- `ObjectMeshManager` may retain at most 32 empty texture atlases / 64 MiB.
|
||
`GlobalMeshBuffer` owns reclaimable vertex/index ranges capped at 384 MiB and
|
||
128 MiB respectively, with an 896 MiB physical ceiling that includes an
|
||
in-progress migration and its retired predecessor.
|
||
|
||
OpenGL deletion and range/slot reuse are not synonymous with logical release.
|
||
`GpuFrameFlightController` fences three frames in flight. Mesh-buffer stores,
|
||
texture handles, atlas layers, terrain slots, and landblock render records enter
|
||
retirement only after they are no longer publishable, and their physical ids are
|
||
recycled only after the corresponding fence signals. Shutdown follows the same
|
||
dependency order and remains retryable: UI/controllers and render registrations
|
||
withdraw first, then owner leases and caches, then GL backing stores. This keeps
|
||
drivers from reading freed memory without adding a portal-specific purge or a
|
||
visual-distance reduction.
|
||
|
||
---
|
||
|
||
## Per-Frame Update Order (current runtime)
|
||
|
||
```
|
||
UpdateFrameOrchestrator
|
||
1. retry retained live-entity teardown tombstones
|
||
2. normalize/publish the host and PhysicsScript clocks
|
||
3. converge streaming origin, readiness, residence, and rescued projections
|
||
4. sample semantic input, raw mouse, and combat intent
|
||
5. advance local/ordinary/static live objects and deferred effect tails
|
||
6. drain inbound session traffic in one GpuWorldState mutation batch
|
||
7. run the local post-network CommandInterpreter position tail
|
||
8. reconcile roots, children, emitters, and lights without advancing time
|
||
9. expire liveness
|
||
10. advance local teleport/reveal/tunnel presentation
|
||
11. evaluate one-shot player-mode entry
|
||
12. publish fly/chase/player camera presentation
|
||
```
|
||
|
||
`GameWindow.OnUpdate` owns only the profiler scope and one orchestrator handoff.
|
||
The accepted host order above is preserved as TS-53; it is not overclaimed as
|
||
the exact retail `Client::UseTime` order. See the completed Slice 6 ledger and
|
||
`memory/project_gamewindow_decomposition.md`.
|
||
|
||
`GameWindow.OnRender` likewise owns only one handoff to the shipped
|
||
`RenderFrameOrchestrator`. Its frozen graph begins per-resource frames and
|
||
render-thread uploads; draws world/PView and its two shared-alpha scopes;
|
||
renders portal and paperdoll private viewports, retained gameplay UI, ImGui
|
||
devtools, and screenshots; then closes the GPU-flight transaction in
|
||
`finally`. Both UI stacks coexist in the same frame.
|
||
|
||
---
|
||
|
||
## Render Pipeline (SSOT — current accepted state)
|
||
|
||
> The modern path (Phase N.5, mandatory) is
|
||
> `WbDrawDispatcher` (entities) + `EnvCellRenderer` (indoor cell shells) +
|
||
> `TerrainModernRenderer` (terrain), fed by the unified PView stack. This is the
|
||
> authoritative current draw model; the 2026-05-31 reset handoff is historical.
|
||
|
||
**One visibility owner.** `RetailPViewRenderer.DrawInside` is the production
|
||
world gate. Its root is the collided camera/viewer cell, or the synthetic
|
||
outdoor cell adaptation; the player's current cell separately owns sunlight
|
||
and indoor lighting. A null root exists only for login/debug/streaming-gap
|
||
fallback frames. `RetailPViewRenderer` is the one authoritative PView owner and
|
||
product family: it builds the main frame, deliberate per-building exterior
|
||
floods for the synthetic outdoor root, and separate interior-root look-in
|
||
frames. No second per-frame ACME visibility BFS competes with that family.
|
||
|
||
**Current draw discipline.** Outside-view slices draw sky, terrain, and outdoor
|
||
statics first. Interior-root building look-ins then punch all entry apertures
|
||
before drawing their shell and contents. The landscape shared-alpha scope
|
||
flushes before the root-specific depth boundary. Interior roots perform the
|
||
conditional depth clear and then write true-depth exit seals; the synthetic
|
||
outdoor root retains world depth and writes far-Z building-entry punches.
|
||
Opaque EnvCell shells, immediate far-to-near transparent EnvCell shells, cell
|
||
statics and their particles, then the surviving main-stage dynamics and their
|
||
particles follow. Look-in and outside-stage dynamics intentionally draw in
|
||
their earlier landscape phases. The final world shared-alpha scope flushes
|
||
before private portal/paperdoll viewports and UI.
|
||
|
||
The modern renderer intentionally does not hard-clip every shell or entity to
|
||
the accumulated polygon. It combines PView admission and viewcone checks with
|
||
retail's punch/seal depth discipline; terrain/outside slices use the bounded
|
||
clip-plane/scissor adaptation. World Wb translucents and Scene particles share
|
||
one stable far-to-near `RetailAlphaQueue`; EnvCell transparent shells and
|
||
private viewports remain immediate. Projectiles are ordinary live-entity draws,
|
||
never a separate global pass.
|
||
|
||
Retail anchors are `SmartBox::RenderNormalMode @ 0x00453AA0`,
|
||
`PView::DrawInside @ 0x005A5860`, `PView::DrawCells @ 0x005A4840`,
|
||
`LScape::draw @ 0x00506330`, `D3DPolyRender::FlushAlphaList @ 0x0059D2E0`,
|
||
and `SceneTool::EndFrame @ 0x0043FB30`. The deliberate modern/PView
|
||
adaptations are audited in the retail divergence register; Slice 7 changes
|
||
their ownership only, not their behavior.
|
||
|
||
### Streaming publication ownership
|
||
|
||
The streaming worker may read dats and build CPU payloads, but it must not mutate
|
||
live render-frame registries. A near-tier job produces one `LandblockBuild` whose
|
||
optional `EnvCellLandblockBuild` owns the complete portal-cell and drawable-shell
|
||
sets for that landblock. The exact build object crosses the `LandblockStreamer`
|
||
completion channel. On the render thread, `StreamingController` applies that one
|
||
completion to `CellVisibility`, cell physics state, `EnvCellRenderer`, and finally
|
||
`GpuWorldState` during the same update-frame drain.
|
||
|
||
This is the publication boundary: per-job builders are private and single-use;
|
||
completed payloads are immutable snapshots; live landblock stores are replaced as
|
||
complete units. Process-wide pending bags or renderer-owned pending lists are not
|
||
valid streaming seams because a duplicate load or a second landblock can drain or
|
||
replace state produced by another job. CPU mesh extraction may still be scheduled
|
||
onto `ObjectMeshManager`'s thread-safe work queue, but the worker never publishes
|
||
partially hydrated cell membership or shell placement to the renderer.
|
||
|
||
### World-reveal readiness ownership
|
||
|
||
`WorldRevealCoordinator` is the single App-layer owner of each login or portal
|
||
reveal lifetime. It composes the canonical `WorldRevealReadinessBarrier` with
|
||
generation-scoped lifecycle observation, so begin, preparation, readiness,
|
||
materialization, world visibility, completion, and cancellation cannot be
|
||
wired independently. The barrier joins `StreamingController`/`GpuWorldState`
|
||
static-mesh publication, `WbDrawDispatcher` composite-texture warmup, and
|
||
`PhysicsEngine` destination residency. Login's sky-only gate and portal-space
|
||
transit consume the same predicate; they differ only in presentation.
|
||
Composite preparation advances after `WbMeshAdapter.Tick` on the render
|
||
thread, never from the streaming worker. This is acdream's asynchronous equivalent of retail
|
||
`SmartBox::UseTime` holding position completion while
|
||
`CellManager::blocking_for_cells` is set.
|
||
|
||
`WorldRevealLifecycleTelemetry` is an observation seam owned by the
|
||
coordinator, not a second lifecycle state machine. It records generation-scoped
|
||
edges and rejects a normal-world viewport observed before canonical readiness.
|
||
`WorldLifecycleAutomationController` consumes only coordinator snapshots plus
|
||
resource counters; `FrameScreenshotController` performs the requested
|
||
default-framebuffer readback after world and retained UI draw on the render
|
||
thread. The external connected gate launches normal Release processes and
|
||
observes these artifacts, but never mutates streaming, physics, GL, or entity
|
||
state from a worker thread.
|
||
|
||
### Teleport cell identity at the streaming boundary
|
||
|
||
The network and physics layers carry retail's complete `Position`: an
|
||
authoritative `objcell_id` plus a frame. Any streaming decision that asks
|
||
whether a teleport crossed a landblock compares the source and destination cell
|
||
IDs. It must never reconstruct either landblock by flooring render/world XYZ.
|
||
Dungeon EnvCells have arbitrary frame origins, including valid negative local
|
||
coordinates, so XYZ-to-grid inference can evict the landblock the player is
|
||
still standing in.
|
||
|
||
`TeleportLandblockTransition` owns this App-layer classification. When the
|
||
source cell is unavailable before first placement, it uses the typed current
|
||
streaming-center landblock as the fallback identity. The existing async
|
||
recenter/hydration path runs only for a real cell-ID landblock change; a
|
||
same-landblock teleport keeps the already-published render and physics unit.
|
||
This is the asynchronous adapter for retail's direct full-`Position` placement
|
||
(`SmartBox::TeleportPlayer @ 0x00453910`).
|
||
|
||
### Portal-space presentation boundary
|
||
|
||
`TeleportAnimSequencer` is the pure Core owner of retail's seven-state
|
||
teleport lifecycle, exact 1/2/5-second thresholds, frame-aligned exit window,
|
||
and 100-sample `UIGlobals::GetAnimLevel` view-plane curve. App-layer
|
||
`PortalTunnelPresentation` owns the synthetic DAT Setup (`0x02000306`), its
|
||
40-frame/s animation (`0x030005AC`), `CSequence`, SmartBox-FOV camera, distant light, mesh
|
||
references, and animation-hook delivery. It renders through the existing
|
||
modern `WbDrawDispatcher` as a replacement 3-D viewport, after world rendering
|
||
and before retained UI. The shared dispatcher is reset to no world-cell clips
|
||
and no world point lights for this pass. Chat, gameplay windows, toolbar,
|
||
cursor, and input continue normally above it. The synthetic object never
|
||
enters `LiveEntityRuntime`, collision, picking, radar, or server GUID state.
|
||
|
||
`TeleportViewPlaneController` is the App-layer projection adapter for retail
|
||
`SmartBox::SetOverrideFovDistance` / `Render::set_vdst`. It captures the active
|
||
game projection at teleport begin, then applies the same table-eased
|
||
view-plane distance and near plane to the portal and world viewports through
|
||
the four retail `FADE` states. Those states do not drive a black alpha layer:
|
||
the portal and world 3-D viewports switch directly at the transition view
|
||
distance, while retained UI remains independently composed. Portal camera
|
||
direction is a roll around its local AC
|
||
`+Y` forward axis, so the animated scene remains a passage rather than yawing
|
||
away from it. Destination placement also calls the retail chase camera's
|
||
`set_viewer(player, reset_sought=1)` equivalent; the normal damped/swept camera
|
||
path then re-extends from the arriving player.
|
||
|
||
One reset seam ends a logical transit on successful completion, replacement
|
||
by a newer teleport sequence, or session teardown. It clears the destination,
|
||
streaming priority, sequencer edges, projection override, and portal scene
|
||
together, preventing transition state from crossing destination or session
|
||
lifetimes. `TeleportTransitCoordinator` correlates each fresh F751 sequence
|
||
with exactly one accepted `UpdatePosition`. Because the Position can arrive on
|
||
either side of F751, a pre-notification packet is buffered only when it advances
|
||
retail's `TELEPORT_TS`; after notification, the first accepted matching packet
|
||
is consumed even if that timestamp already advanced. Duplicate/older F751
|
||
notifications are rejected with wrap-safe ordering. Canonical physics remains
|
||
independent, and streaming recentering compares destination identity with the
|
||
actual current streaming center rather than the player's still-unplaced source
|
||
cell. A live teleport received while acdream's developer fly/orbit camera is
|
||
active re-enters the existing player-mode lifecycle first; retail has no
|
||
detached camera mode, and destination placement therefore always retains the
|
||
canonical local physics controller and chase-camera handoff.
|
||
|
||
The destination residency gate supplies retail's `EndTeleportAnimation` edge
|
||
once asynchronous streaming is ready. Player placement remains authoritative
|
||
and separate from presentation; arrival resets camera viewer state but does
|
||
not reset the character animation sequence. `RetailLiveFrameCoordinator`
|
||
matches `SmartBox::UseTime @ 0x00455410`: it advances local and remote object/
|
||
projectile runtime, composes final animated/equipped poses, drains animation
|
||
hooks, then advances particles before owner scripts and inbound network dispatch.
|
||
`RetailLocalPlayerFrameController` owns the player's exactly-once tick and sends
|
||
input-originated movement/jump output on the object side of that barrier. After
|
||
inbound dispatch, the retail command-interpreter phase evaluates periodic
|
||
AutonomousPosition from current authoritative state, then a non-advancing spatial
|
||
reconciliation updates equipped children and live effect/light anchors. Consequently a
|
||
teleport Hidden transition cannot freeze the last fraction of recall and replay
|
||
it after UnHide, and ForcePosition/F751 cannot combine old one-shot input with a new cell.
|
||
The draw pass consumes that completed update snapshot and never advances live
|
||
animation independently. See
|
||
`docs/research/2026-07-15-retail-portal-space-pseudocode.md`.
|
||
|
||
## Roadmap Model
|
||
|
||
The old R1-R8 architecture sequence was a useful early refactor sketch, but it
|
||
is no longer the execution plan. The strategic source of truth is now
|
||
`docs/plans/2026-04-11-roadmap.md`, with per-phase details in `docs/plans/`
|
||
and `docs/superpowers/specs/`.
|
||
|
||
Current movement/collision ownership:
|
||
|
||
- **B.3** is shipped MVP history: first collision resolver foundation.
|
||
- **L.1/R6** own animation/motion parity and the complete retail object-frame
|
||
update order over canonical `LiveEntityRuntime` records.
|
||
- **L.2** owns the remaining movement and collision conformance:
|
||
`docs/plans/2026-04-29-movement-collision-conformance.md`.
|
||
- **G.3 is shipped**: dungeon streaming, portal-space presentation, collision,
|
||
lighting, doors, and the full round-trip gate landed. The separate far-
|
||
teleport unstreamed-edge residual is issue #153, not unfinished G.3.
|
||
- **M3 landed 2026-07-21** with connected magic/portal acceptance and the final
|
||
two-client observer gate; Track LH is the future presentation-independent
|
||
Linux/headless extraction.
|
||
- **The behavior-preserving thin-`GameWindow` ownership campaign is the active
|
||
prerequisite before new M4 subsystem bodies.** Selection/interaction Slice 1
|
||
landed with `WorldSelectionQuery`, `SelectionInteractionController`, and the
|
||
shared retail inventory-request owner; Slice 2 moved final animation
|
||
presentation into `LiveEntityAnimationPresenter`; Slice 3 moved the complete
|
||
session generation into `LiveSessionController` plus its focused host and
|
||
routers; Slice 4 moved hydration, teardown, inbound authority, network
|
||
updates, and reusable motion-runtime policy over canonical
|
||
`LiveEntityRuntime`. Slices 5–7 complete landblock presentation and the
|
||
update/render frame graphs. Slice 8 checkpoints A–G now own the native
|
||
callback shell, live-session composition, world environment, reversible
|
||
input/pointer lifetime, physical framebuffer publication, the sole gameplay
|
||
action-priority graph, typed commands, the retained-root item-drop edge, and
|
||
two-phase runtime settings. `RuntimeSettingsController` is the sole settings
|
||
persistence/current-state owner, supplies the immutable pre-window snapshot,
|
||
applies startup pacing/display/FOV/audio once, and later borrows complete
|
||
runtime targets without replay. Explicit resource ownership and frame slots
|
||
are the next checkpoint. The reconciled ledger and sequence live in
|
||
`docs/architecture/code-structure.md`. This decision does not reclassify the
|
||
remaining L.2 conformance work or Track LH.
|
||
|
||
Full `GameEntity` type aggregation remains a separate, higher-risk target after
|
||
the thin-`GameWindow` campaign. The current campaign reuses canonical
|
||
`LiveEntityRuntime` rather than introducing another entity owner. Do not
|
||
resurrect old R1-R8 phase numbers for new work; update the roadmap instead.
|
||
|
||
---
|
||
|
||
## Development Workflow (mandatory for ALL work)
|
||
|
||
```
|
||
For every AC-specific behavior:
|
||
|
||
0. GREP NAMED → Search docs/research/named-retail/ by class::method
|
||
1. FALLBACK → Use older docs/research/decompiled/ chunks only if needed
|
||
2. CROSS-CHECK → Verify against ACE + ACME + holtburger where relevant
|
||
3. PSEUDOCODE → Translate to readable pseudocode
|
||
4. PORT → Faithful C# translation
|
||
5. TEST → Conformance test against retail/decomp golden values
|
||
6. INTEGRATE → Surgical wiring into the existing system
|
||
7. VERIFY → Visual + functional test
|
||
```
|
||
|
||
For acdream-specific code (renderer, plugin API, streaming):
|
||
- Design for clean interfaces
|
||
- Test independently
|
||
- No AC-specific magic — those live in the ported layer
|
||
|
||
---
|
||
|
||
## Reference Hierarchy
|
||
|
||
| Domain | Primary Oracle | Secondary |
|
||
|--------|---------------|-----------|
|
||
| Physics/collision | `docs/research/named-retail/` | ACE Physics/ + older decompiled chunks |
|
||
| Animation | `docs/research/named-retail/` + ACE Animation/ | — |
|
||
| Terrain | ACME ClientReference.cs | named retail / older decompiled chunks |
|
||
| Rendering | WorldBuilder (Silk.NET) | ACViewer |
|
||
| Protocol | holtburger | AC2D |
|
||
| Server behavior | ACE | — |
|
||
|
||
---
|
||
|
||
## Success Criteria
|
||
|
||
The client is "done" when:
|
||
1. You can log in to an ACE server
|
||
2. Walk around the entire world (streaming loads new areas)
|
||
3. Enter and exit buildings through doorways
|
||
4. See all NPCs, monsters, and players animated correctly
|
||
5. Open doors, talk to NPCs, pick up items
|
||
6. Send and receive chat
|
||
7. A Lua macro can automate gameplay
|
||
8. Side-by-side with the retail client, the world looks the same
|