acdream/docs/architecture/acdream-architecture.md
Erik 6dc7ba51ee feat(physics): C4 route 4b-3 — remote teleport + cell-less through the canonical placement
Flips the last remote classification (SetPosition: teleport-advanced and
cell-less) onto 4b-1's RuntimeRemotePlacementDriveController, runs retail's
teleport_hook before the placement, and deletes the legacy remote-teleport
machinery. Contract: docs/research/2026-08-04-c4-route-4b-3-contract.md.

Retail: MoveOrTeleport @0x00516330's branch @0x00516386 -> teleport_hook
@0x005163EF -> SetFlags(0x1012) @0x00516414 -> SetPosition @0x00516420 ->
return 1 @0x00516438. The hook @0x00514ED0 runs BEFORE the placement and
regardless of its outcome. Retail places this branch unconditionally, at any
distance and any contact state (arg4 is read only @0x0051638E, after the
branch) — which is what retires AP-137's cell-less enqueue-vs-place delta.

D1 — the classifier's cell-less input is now the PRE-merge committed cell.
Retail's predicate is `this_1->cell == 0`, the BODY's own cell at
MoveOrTeleport entry (this_1 is assigned from this @0x00516334). acdream fed
the POST-merge canonical.FullCellId, which RefreshSnapshot ->
RefreshDerivedState -> SetFullCell has already stamped with the accepted wire
cell; a zero wire cell fails validation into RejectedData first. The shipped
remote cell-less predicate was therefore dead code, not merely different from
remotePlacementRequired. Threaded via a builder overload; route 1's overload
is untouched. The graphical !IsSpatiallyVisible arm of
projectionRequiresTeleportHook is deleted — a presentation predicate with no
retail analogue that fired the teleport machinery on a routine hot path.

Deleted: RemoteTeleportController (605), RemoteTeleportPlacement (85),
RemoteShadowPlacementSynchronizer (49), their 1,709 lines of tests, the
remotePlacementRequired predicate, the TeleportHookRequired plumbing, the
legacy pre-operation ConstrainTo fallback, and the player arm's legacy
!IsGrounded fallback. Net -2,030 lines.

Structural fix (two independent Opus reviews, round 1 FAIL/FAIL): three of the
four MAJORs were one defect — OnPosition carried two parallel inline copies of
the routing tail (player-guid, NPC-guid) that had drifted. Extracted
RunRemoteArmTail (3 call sites) and ApplyWireAirborneLeftoverBookkeeping (2),
both branches now share one implementation.

  A1  ToConstraintArm mapped AirborneSnap -> AirborneNoOperation, so the NPC
      arm armed ConstrainTo ZERO times for an out-of-contact wire-grounded
      creature — a regression this slice introduced while closing a
      structurally identical hole. Now maps to NearInterpolate; switch made
      total with a throwing default proven unreachable.
  R1  D2's write-nothing shape existed on the player arm only; NPC packets
      fell through and wrote the body. Retail makes no player/NPC distinction.
  R2  report_collision_end(this,1) @0x00514F31 was bound to
      ShadowObjects.Suspend, a port of a DIFFERENT retail function
      (remove_shadows_from_cells) that teleport_hook never calls. Now routes
      to RuntimeCollisionReportingState.LeaveWorld, which wraps the private
      ForceEnd in an admission-blocking transaction so a DoCollisionEnd
      callback cannot recreate the contact table.
  R3/A2 A teleported NPC synthesized ServerVelocity from the teleport distance
      (~1,000+ m/s) and planned a run cycle from it. Both the install and
      RemoteServerControlledVelocityCycle.Apply now gate on !isTeleportRoute.

BISECT HAZARD — A1's fix is correct only BECAUSE R1 landed. AirborneSnap is
reachable wire-airborne on the NPC arm only while D2's shape is missing there.
Reverting R1 alone silently inverts A1 into the opposite divergence: arming
where retail returns 0. Revert both or neither.

Also in the velocity hunk: the NPC block's two !IsPlayerGuid(update.Guid)
guards were dropped when it was wrapped in `if (!isTeleportRoute)`. Safe — all
five exit paths of the enclosing IsPlayerGuid block return, so the predicate is
unconditionally false below it — but it was unremarked by both reviews.

Register: AP-137 REWRITTEN (not deleted) to the surviving acdream-only
divergences — null classification during the login window and Rejected*
through UnroutedCatchUp keep a row. AD-42's RemoteTeleportController citation
retired; AP-136/AP-138 writer lists corrected to the two surviving non-Position
rebucket writers; AP-138 gains the teleport arm as a second producer of the
visible-without-collision residual (retirement path remains #309). AP-135 is
untouched and its two airborne bookkeeping writes are preserved on both arms.
AP-131 does not retire; #276 does not close.

Proof obligation 1: ParkCollisionResidents' overlap throw stays unreachable —
the teleport arm adds packets to the same TryBeginExclusiveAuthoredPlacement
one-operation-per-key machinery the far arm uses, opens no new operation shape,
and every DeferredCell outcome cancels synchronously with
restoreCancelledPark: true. The guarded property remains
HasOldPrefixPlacementDebt's stall, not a throw (4b-1's B2 caveat stands).

Correction to an earlier claim: LiveEntityPresentationController's
_activePlacementOwners was NOT write-never at HEAD —
remotePlacementRequired -> BeginPlacement -> Begin -> BeginAuthoritativePlacement
was a live writer chain. It becomes write-never BECAUSE this slice deletes that
chain, which is why deleting the dead half is behaviour-preserving.

Probe: ACDREAM_PROBE_REMOTE_TELEPORT=1 emits one [remote-teleport] line per
routed arm (guid, cause, hook-ran, placement status). TEMPORARY, strip with the
probe family.

Carried, disclosed not fixed: no dedicated bidirectional collision-partner test
for R2 (the wiring, not LeaveWorld itself, is what lacks coverage); the
stress test's teleport step drives hand-written field assignments rather than
the canonical arm; the per-packet runTeleportHook closure allocation (network
path, not the resolve path Slice I's 0 B discipline governs — file before
route 5 adds a fourth call site). B2: IRuntimeCollisionReportObserver has zero
production implementations, so retail's bidirectional DoCollisionEnd half still
reaches no gameplay consumer — this fix closes the wrong-function binding, not
that nobody listens.

Complete Release suite MEASURED at 11,013 passed / 4 skipped / 0 failed
(baseline 11,027/4/0; net -14 = ~33 deleted test cases against ~19 added).
Neither known flake fired (#302 PortalProjectionTests GC-allocation, #308
NakEmissionTests wall-clock).

STILL OWED: the two-client connected gate, which MUST use an NPC/creature
teleport target. Both round-1 MAJORs lived on the NPC arm and the velocity
cycle early-returns for 0x50xxxxxx guids, so a player target structurally
cannot observe A1, A2, or R3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:00:10 +02:00

1255 lines
77 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 Vulkan
- **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 Vulkan 1.3 (the only backend since Campaign V V11) │
│ 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, and for most of the
project's life ImGui coexisted beside it as permanent devtools. **Campaign V
slice V11 ended that (2026-07-29):** ImGui was a GL-only frontend, the campaign
did not port it, and deleting OpenGL deleted it. There is now **one** presentation
stack. Full history and the corrected contract live in
`docs/plans/2026-04-24-ui-framework.md`.
```
┌─────────────────────────────────────────────────────────────┐
│ GAMEPLAY UI (the only stack) │
│ LayoutDesc/DAT → UiRoot retained widgets + controllers │
├─────────────────────────────────────────────────────────────┤
│ SHARED CONTRACTS │
│ ViewModels, commands, input actions, state/event services │
│ ► one model and mutation path, one presentation projection │
├─────────────────────────────────────────────────────────────┤
│ Game state + events (unchanged) │
│ IGameState / IEvents / WorldSession — UI only reads │
└─────────────────────────────────────────────────────────────┘
```
`AcDream.UI.Abstractions` — the `IPanel`/`IPanelRenderer` contract, the
ViewModels and the commands — **survives intact**. It was always
backend-agnostic, which is exactly what Code Structure Rule 3 was written to
protect, and it is what a future developer-panel host would bind to. Only the
ImGui *backend* was deleted. `ACDREAM_DEVTOOLS=1` still selects Vulkan's
debug-utils extensions and now logs that the developer UI is gone; replacing it
is issue **#258**, deliberately unscheduled.
`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
presentation 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 renderer. `GameWindow.OnClosing`
tears this runtime down before session and game-state sources.
`RetailUiRuntime` is the production composition boundary. `GameWindow` creates
the device/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; Runtime `RuntimeActionState.SpellCast` owns validated cast
intent and last-request state; App `MagicRuntime` owns DAT formula/component
policy, live-object target queries, transport, and presentation binding. 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 Vulkan, 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)
FlatCollisionAssets.cs -> immutable array/index collision schema (Slice I2)
FlatCollisionAssetBuilder.cs -> deterministic DAT graph preparation (Slice I2/I3)
PhysicsDataCache.cs -> flat prepared GfxObj / Setup / CellStruct / EnvCell collision owner
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.Runtime/ presentation-independent client kernel boundary
RuntimeAssemblyMarker.cs -> J0 dependency boundary
GameRuntime*.cs -> J1/J4 borrowed views, synchronous commands,
ordered deltas/checkpoints, clock,
generation + teardown
Session/ -> J2 canonical session lifetime, ordered
inbound routing + retryable teardown
Entities/
RuntimeEntityDirectory.cs -> sole GUID/incarnation/local-ID authority
RuntimeEntityRecord.cs -> presentation-free accepted entity state
RuntimeEntityObjectLifetime.cs -> one entity/object lifetime root
RuntimeEntityObjectEventStream.cs -> canonical ordered entity/object deltas
RuntimeEntityObjectViews.cs -> direct allocation-free borrowed views
InboundPhysicsStateController.cs -> retail timestamp/snapshot authority,
including gate-only dormant acceptance
ParentAttachmentState.cs -> generation-exact parent relations plus raw
missing-parent Create admission
RuntimeInitialCreateAdmissionFreezer.cs -> immutable parser-payload copy
boundary for dormant initial placement
RuntimeInitialCreateResidenceState.cs -> exact-incarnation initial
placement lease and accepted mixed-update FIFO
RuntimeInitialCreateContinuationExecutor.cs -> retry-idempotent
adoption + retail Create tail + strict-order
FIFO/replay execution over the residence
Gameplay/
RuntimeCommunicationState.cs -> one chat/social owner + ordered stream
RuntimeInventoryState.cs -> exact object-table borrower + inventory
transaction/session-state owner
RuntimeCharacterState.cs -> coupled spellbook/local-player plus
character-option/run-jump owner
RuntimeActionState.cs -> exact selection/interaction plus combat and
spell-cast intent owner
InteractionState.cs -> presentation-free cursor-target mode state
RuntimeInteractionTransactionState.cs -> use/appraisal/pickup FIFO,
exact approach and busy borrower
Physics/
RuntimePhysicsState.cs -> per-session engine/cache/scratch/shadows,
collision receipts, bodies/hosts/worksets
RuntimeCollisionReportingState.cs -> exact-key retail collision table,
environment latch, ordered callbacks, and
SetPosition report-result ownership
RuntimeSetPositionState.cs -> exact placement/lost-cell operations,
authored mover retention, ordered host
receipts, and collision-generation wake
RuntimePlacementProjectionChannel.cs -> generation-gated public host
observation/retry/exact-ack seam over the
one Runtime SetPosition receipt owner
RuntimeRemotePhysicsUpdater.cs -> presentation-free remote simulation
RuntimeOrdinaryPhysicsUpdater.cs -> presentation-free object simulation
RuntimeProjectile.cs -> canonical projectile component/prediction owner
RuntimeProjectilePhysicsUpdater.cs -> presentation-free projectile simulation
World/
RuntimeWorldEnvironmentState.cs -> canonical calendar/time/weather owner
RuntimeWorldTransitState.cs -> canonical reveal generation/readiness owner
Platform/
ApplicationPathSet.cs -> shared BCL-only XDG/Windows config, data,
cache, plugin, screenshot, and diagnostic paths
RuntimeGenerationReset.cs -> one retryable canonical-generation reset
-> Slice J complete; graphical and no-window hosts share one GameRuntime
-> may reference Core, Core.Net, Content, and Plugin.Abstractions only
-> must never reference App, UI, Silk.NET, OpenAL, or Arch
AcDream.Headless/ Linux/Windows no-window production host
Program.cs -> CLI entry only
Configuration/ -> strict versioned process/session config
Credentials/ -> redacted env/stdin/owner-only-file providers
Hosting/ -> one GameRuntime/session/lease/policy lifetime
Policies/ -> typed Runtime-view/command consumers
-> references Runtime only; no presentation/backend package
-> Slice K complete: portable single/multi-session production host,
deterministic scheduler/bot API, shared immutable process content,
1/5/10/30-root isolation/endurance, numeric resource envelope, and
exact connected/terminal convergence
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
Platform/
GraphicalHostPlatformServices.cs -> one startup OS/architecture/RID,
native-manifest, path, pacing owner
GraphicalWindowBackendSelection.cs -> immutable GLFW 3.4
Win32/X11/Wayland selection and
packaged-native preference
Gpu/Vk/VulkanCapabilityRecord.cs -> pre-render platform/device/audio/
input capability report + startup gate
Gpu/Vk/VulkanBringUpHost.cs -> active device/descriptor/pipeline/
offscreen-readback validation
GraphicalLegacyConfigurationMigrator.cs -> no-overwrite Windows
LocalAppData migration
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
RemoteTeleportHook.cs -> ordered retail teleport teardown seam (teleport_hook port; C4 route 4b-3 runs it from LiveEntityNetworkUpdateController's teleport arm dispatch, through RuntimeRemotePlacementDriveController)
World/
LiveEntityRuntime.cs -> exact-key App projection/lifecycle host
LiveEntityProjectionStore.cs -> materialized sidecars by RuntimeEntityKey
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 -> native composition/callback shell
FramePacingWaiterFactory.cs -> startup-selected Windows/Linux waiter
LinuxMonotonicFramePacingWaiter.cs -> absolute CLOCK_MONOTONIC sleep
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 -> graphical host/reveal projection adapter
WorldRevealReadinessBarrier.cs -> graphical destination-readiness source
Input/
PlayerMovementController.cs -> active movement driver
Plugins/
AppPluginHost.cs -> done
```
The 4B2 production SetPosition routes and shared local-controller body remain
dormant. Runtime now owns the exact collision table, environment latch, and
report-result semantics needed by that cutover. Activation still waits for
exact authored mover preparation, presentation-only rebucketing,
placement-prefix quiescence, and an atomic Runtime body/controller publication
transaction to land as one reviewed cutover.
---
## 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.
- **C4 route 4b-3 (2026-08-04) deleted `RemoteTeleportController` /
`RemoteTeleportPlacement` / `RemoteShadowPlacementSynchronizer` outright** —
605 + 85 + 49 lines of App-layer incarnation-scoped placement machinery,
replaced by routing the remote teleport/cell-less classification through
the SAME canonical `RuntimeRemotePlacementDriveController` the far snap
(C4 route 4b-2) already uses (`ApplyAcceptedRemoteTeleport`, sharing
`StoresAcceptedDestination`/`StoreAcceptedDestinationPose`). `teleport_hook`
(@0x00514ED0) still runs first, via `RemoteTeleportHook` invoked from
`LiveEntityNetworkUpdateController`'s teleport-arm dispatch (both the
player-guid and NPC-guid branches share one `RunRemoteArmTail` helper for
the routing-decision/currency/constraint-arm sequence — see the C4 4b-3
fix round, 2026-08-04, for why the two branches were unified there after
independently drifting). There is no separate "loaded vs pending
destination" placement path anymore: an unresolved destination collision
generation retains a preparation-stage retry inside the SAME drive
controller that far snap already retries through, not a second incarnation-
scoped machine, and `_activePlacementOwners`'s Hidden/UnHide visibility-edge
protection is gone with its only writer chain — the synchronous, single-
frame teleport commit removes the multi-frame window that protection
existed for.
`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` owns immutable flat GfxObj, Setup, CellStruct, and EnvCell
collision assets loaded from the validated prepared package. Production
retains no parsed DAT collision graph; graph construction is restricted to
bake/equivalence tools and explicit test oracles.
- Landblock collision activation is generation-owned by
`RuntimePhysicsState`. Graphical and no-window hosts populate a private
`PreparedLandblockCollisionGeneration` over bounded cursors; its cache,
`CellGraph`, engine landblock, buildings, static shadows, and retained-owner
refloods are never visible through the borrowed live engine. Retained owners
comprise every non-suspended dynamic or adjacent-root static touching the
target prefix (including a withdrawn repair marker); target-root statics come
from the authored replacement. Runtime mutation-gates their exact capture,
refreshes each through the host work meter, and builds every cache/graph/
shadow replacement through one-work-unit seal cursors. Stable per-prefix
owner slots replace the former registry-global mutation gate. One Runtime-
scoped versioned journal coalesces repeated live mutations by owner instead
of copying the owner into every draft on every event. Each draft reconciles
only the latest exact state for owners changed during its lifetime, one owner
per seal step, so unrelated or continuously moving owners cannot restart the
target cursors. Once discovered, a relevant owner receives exact subscribed
updates without restoring global fanout. First entry to or departure from a
target after the global slot cursor has passed is routed through the owner's
changed prefix to that one matching draft. During topology construction a
visited unrelated owner receives only a cheap coalesced dirty notification;
its exact mirror is deferred to one metered seal unit. Once the topology seal
exists, observed owners temporarily write through exactly until activation,
so the finite pre-seal queue drains even when several unrelated owners move
continuously. Production activates in that same update-thread call. Slots older than a
newer draft's captured root are superseded at the tail rather than reused
behind live cursors; new drafts start at their captured suffix, obsolete
slots compact incrementally, and the journal clears with the last draft.
Empty prefix containers are reclaimed under GUID churn; seal cursors retain
their captured slot lists.
Cache, CellGraph, engine, and shadow topology share one complete off-side
`CollisionWorldState`. Admission captures the current root reference in O(1)
and materializes the non-target leaves through the same one-work-unit frame
meter; a dense resident world is never cloned synchronously. After an older
preparation commits, its exact landblock delta queues into every later draft
and drains one cache, graph, landblock, or owner leaf per seal step. A later
demotion or withdrawal cancels matching queued/active rebases and tombstones
that prefix in unfinished source scans, then retires one owner/cache/graph/
outdoor leaf per seal step from growable retirement storage. Commit rechecks
both retirement and rebase state after sealing, so retired topology cannot
return or cause a drafts-times-world-size update spike.
The host immediately performs the zero-work root transfer in the same update-
thread call that completes final reconciliation, so continuous unrelated
movement cannot manufacture a required quiet frame between seal and commit.
Deterministic preparation order prevents a later draft from exposing early,
inheriting cancelled topology, or overwriting a committed prefix. Final activation is one
zero-allocation volatile root transfer that preserves public facade identity,
revokes staging, and then emits `CollisionGenerationCommitted`. Cancellation disposes only the named
staging generation and never withdraws the previous active world. Thus
readers see the complete old generation or complete new generation, never a
mixed cell/cache/shadow world.
- `ShadowObjectRegistry` gives movement a per-cell broadphase over nearby
objects and buildings. Streaming reflood is structurally part of the Runtime
collision-generation commit; there is no independent post-publication
reflood suffix.
- `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)
The shipped bridge is now split at the Runtime/App boundary.
`AcDream.Runtime.Entities.RuntimeEntityDirectory` owns the only current
server-GUID/incarnation map, Runtime local-ID allocation/reverse lookup,
accepted snapshots and timestamp gates, parent state, session/operation
versions, and exact tombstones. `RuntimeEntityRecord` is presentation-free.
Initial world placement has one deliberate dormant exception to ordinary
snapshot publication. While an exact `RuntimeInitialCreateResidenceState`
lease is waiting for its first canonical placement, retail timestamp gates may
accept later same-incarnation Create, ObjDesc, Parent, Pickup, Position,
Movement, State, and Vector packets, but neither the canonical record, public
accepted snapshot, event stream, nor presentation changes. Runtime retains
deep-frozen typed actions in one monotonic arrival-ordered FIFO under the exact
entity key. A Create whose parent is not yet addressable is retained even
earlier as a complete raw packet, before child timestamp admission, and is
guarded by a non-reused admission token. Delete, generation replacement,
reset, GUID reuse, and reentrant teardown discard only the matching ownership.
The admission checkpoint (`30012361`) intentionally stopped before executing
the FIFO. The continuation executor (`5db3de3c`,
`RuntimeInitialCreateContinuationExecutor`) completes the mechanism: one
synchronous, retry-idempotent `Execute` transaction adopts the acknowledged
initial placement exactly once, emits the local player's after-enter-world
hook request, replays deferred missing-parent raw Creates and queued parent
relations by parent GUID (whole-bucket detach, FIFO dispatch,
cancellation-aware restore windows), and drains the mixed FIFO strictly by
sequence — classifying each retained Position at execution time with live
inputs and driving authored placements through the canonical
`RuntimeSetPositionState` lifecycle with retryable yields. Apply bodies are
shared with the legacy fused inbound paths through gate-less instance seams
that keep the one snapshot store in lockstep; same-incarnation Create
envelopes apply atomically with buffered publication; every abandonment path
retires the residence and converges the combined ownership ledger. The
executor has NO production caller yet — graphical and no-window Create still
use legacy `RegisterEntity` — and the next checkpoint must switch both
production routes onto this owner rather than create another snapshot or
placement path.
`LiveEntityRuntime` is the App projection/lifecycle host.
`RegisterLiveEntity` first creates or refreshes canonical Runtime state without
an App record. `MaterializeLiveEntity` claims the Runtime local ID and creates
one graphical `LiveEntityRecord` sidecar in `LiveEntityProjectionStore`, keyed
by exact `RuntimeEntityKey` (local ID plus `INSTANCE_TS`). The sidecar owns the
App animation, hydration, and effect/render presentation state.
`RuntimeEntityRecord` owns the canonical `PhysicsBody`, physics host, remote
motion, projectile component, clocks, state, and full cell.
Live `PhysicsDesc` effect fields replace Setup defaults on that same exact
sidecar; rebucketing never recreates it.
`RuntimeActionState` is the one action-state root. It constructs and owns the
exact `SelectionState`, `CombatState`, and presentation-free
`InteractionState` instances borrowed by direct/headless and graphical
consumers. App retained widgets, input controllers, plugins, session routing,
and `GameWindow` hold views or exact borrowed children; none may construct a
parallel production action owner. Its `RuntimeInteractionTransactionState`
child owns the use throttle/source/target, appraisal identity, typed ordered
interaction FIFO, and exact post-arrival pickup token while borrowing the one
J4 inventory busy/request gate. App supplies picking, movement installation,
wire transport, toasts, and retained drag/pending-slot presentation. Hidden,
delete/GUID reuse, reset, disposal, callback re-entry, and transport failure
all converge without retaining a second owner or changing retail's press-time
order (`f5f7b417`). The same root now owns the exact combat-attack,
combat-target, combat-mode, and spell-cast intent children. Retained bars and
graphical input adapters borrow those exact owners, while direct Runtime
commands drive the same operations and snapshots. App retains world/content
queries, transport, animation, sound, particles, and response text
(`20df9d15`). `RuntimeLocalPlayerMovementState` owns the exact local movement
controller, construction seam, autorun latch, typed view, and terminal ledger;
graphical input and direct commands borrow that owner, and outbound
MTS/jump/AP cadence lives in Runtime (`aa3f4a60`). One
`RuntimePhysicsState` now owns the per-session engine, production cache/cell
graph, transition scratch, shadow registry, typed collision admissions,
canonical body/host/remote components, ordinary/remote worksets, simulation,
and full-cell commits. App supplies prepared collision and animation/shape
inputs, then projects committed snapshots (`7e6033d0`). The same Runtime
physics owner now owns the canonical projectile component, exact-key workset,
prediction/correction lifecycle, retail projectile simulation, and cell
commits. App resolves immutable Setup/DAT collision shape data and projects
committed render, shadow, and effect-pose results only (`2aee3356`).
`GpuWorldState` owns spatial buckets only and retains the exact projection key
through loaded/pending movement, landblock retirement/reload, origin recenter,
quiescence, and visibility transition delivery. Register, rebucket, withdraw,
and unregister remain separate operations. Logical teardown removes the exact
`WorldEntity` incarnation rather than every projection sharing its server GUID.
Runtime per-GUID/session operation versions prevent a callback from
resurrecting an outer CreateObject after delete/reset, and exact tombstones
prevent ABA reuse. 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.
Focused exact-key materialized queries remain stable across pending landblocks;
focused visible queries feed 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.
When retail's 25-second leave-visibility deadline expires, active ownership
still ends completely. The ACE adapter retains only the accepted
`EntitySpawn` in `DormantLiveEntityStore`, because ACE can keep that GUID in
`KnownObjects` and omit a later CreateObject on revisit. A matching landblock
load re-enters the ordinary timestamp- and generation-gated hydration path;
F747, a newer incarnation, or session reset removes the cold snapshot. Dormant
records own no frame-time, physics, render, effect, audio, or GPU resource.
`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,
asks Runtime for the current canonical incarnation, then resolves the exact
App projection only after renderer/resources/profile are ready. It resolves
typed/default/part-default hooks through that projection'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.
Runtime content residence has one typed policy and diagnostic ledger without
centralizing physical storage. `ResidencyManager` owns generation-safe logical
asset identity, owner tokens, leases, immutable budgets, and aggregate
accounting. Specialized render/content owners continue to own their mesh
ranges, texture arrays, staging payloads, decoded animations/audio, and
retained shared-alpha scratch. The manager never sees a device handle: policy runs as
a single writer during update/resource maintenance, while logical eviction and
fence-delayed physical release remain render-thread owner operations. Prepared
package address space is reported separately from committed CPU bytes, and
allocator capacity is not double-counted as live GPU content. Slice D's
pressure and connected evidence is
`docs/research/2026-07-24-slice-d-unified-residency-report.md`.
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.
Vulkan object destruction 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 device 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,
devtools, and screenshots; then closes the GPU-flight transaction in
`finally`. There is one UI stack: the retained tree.
---
## 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.
`RuntimeOptions.StreamingWorkBudgets` is the single startup profile for
update-thread streaming cost. `StreamingWorkMeter` owns the frame-scoped
admission ledger; `LandblockStreamResultCost` assigns deterministic charges to
immutable completion arrays and logical retained entries without claiming to
measure CLR allocator overhead. `LandblockStreamer` exposes one allocation-free
single-consumer peek/read source so the update thread prices a result before
adopting it. `StreamingController` is the sole scheduler: it admits through the
typed meter into reusable destination/control/unload/Near/Far FIFOs, preserves
exact reference identity and retry position, rejects stale generations, and
executes every class through the same meter. The canonical reveal generation
supplies one exact destination cell/radius reservation; the meter protects its
configured share across time, completion admissions, retained CPU bytes,
entity operations, requested GPU bytes, and device-retirement admissions. A
completion retains the reveal generation that classified it, so a stale
completion or teardown cannot consume or clear a replacement generation's
reservation. Priority changes order and reserved share, never the global
budget. Lifecycle artifacts publish worker and per-class backlog, retained
bytes/age, per-lane work, yields, oversizes, overruns, and pending
publication/retirement facts.
At a hard login/portal reveal edge, `WorldGenerationQuiescence` makes the old
generation unavailable through the same `WorldRevealCoordinator` lifetime.
World rendering and spatial queries return no old content; object/effect clocks,
liveness, and spatial reconciliation stop; world-space audio is silenced; UI,
network/event/command delivery, portal presentation, destination streaming, and
readiness continue. Canonical live records remain retained until physical
teardown converges, so quiescence neither reconstructs server identity nor
invents Hidden/UnHide transitions. A shared-origin recenter atomically removes
the complete old spatial generation and captures one exact receipt per
landblock before the origin changes. Expensive script, physics, render, and GPU
release then advances only from `StreamingController.Tick` through stable
per-owner entity/stage cursors on the frame's one meter. Publication remains
fenced only by an older receipt for the same canonical landblock; the active
destination's exact receipt may advance ahead of unrelated cleanup without
reordering the remaining FIFO.
Accepted publication is likewise retained at its exact priority-queue head.
`LandblockPresentationPipeline` meters render, physics, static, building, and
EnvCell receipts one operation at a time; each owner captures stable ordered
inputs once and retries only its unfinished cursor. Building and EnvCell
replacements are constructed off-side and atomically replace their complete
owner snapshots. The final `GpuWorldState.MutationBatch` is intentionally one
observer-atomic boundary so bucket identity, render pins, activation, and the
outer visibility notice cannot be observed partially. Settings and native
callbacks may defer policy changes, but cannot bypass the frame meter to drain
pending publication.
Loaded spatial residency is deliberately distinct from world availability.
During login/portal quiescence, destination live objects acquire and retain
their renderer projection so composite/readiness preparation can converge
offscreen. `GpuWorldState.IsLiveEntityProjectionResident` is the ownership
predicate used by `LiveEntityRuntime`; `IsLiveEntityVisible` additionally
applies generation availability and remains the consumer predicate for
drawing, collision, picking, radar/status targeting, effects, and audio. CPU
mesh-cache hits may stage a missing GPU upload only while their exact owner
token is live, preventing an evicted projection from recreating stale work.
The Slice E connected closeout is recorded in
`docs/research/2026-07-24-slice-e-cost-budgeted-streaming-report.md`.
### World-reveal readiness ownership
`RuntimeWorldTransitState` is the single presentation-independent owner of
each login or portal reveal generation. It owns the exact destination, typed
atomic readiness latch, materialization/simulation edge, viewport observation,
completion, cancellation, retail wait cue, and portal materialization count.
Stale generations, wrong destinations, invalid readiness shapes, reordered
edges, and post-cancel acknowledgements cannot mutate the active generation.
Once readiness is accepted it remains latched; later sampled graphical
readiness may fall as resources transition without closing the accepted edge.
`WorldRevealCoordinator` is the graphical host adapter. It evaluates App's
`WorldRevealReadinessBarrier`, acknowledges that typed result to Runtime, and
owns only generation-scoped graphical resource receipts: the streaming
destination reservation, render-resource profile, selection/audio projection
edges, and viewport release. Runtime issues an exact
`RuntimeWorldHostProjectionToken` for the reveal generation and destination
cell, then retains the typed registration, simulation-release,
reservation-release, and terminal acknowledgement suffix until App has
actually projected it. Wrong-generation/cell acknowledgements cannot consume
another reveal's work. Callback failure and re-entrancy preserve the exact
remaining suffix instead of duplicating or leaking a reservation. App's former
lifecycle telemetry/generation owner is deleted and
`WorldGenerationAvailabilityState` is a read-only Runtime projection. While the
normal world viewport is withheld, mesh and composite upload owners may admit
more small destination items per frame, but retain the ordinary 8 MiB byte,
array, buffer, and mipmap ceilings. The exact profile ends at the retail
portal-to-world viewport swap; it does not reduce the readiness radius or
reveal incomplete content. 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. There is
no timeout that reveals incomplete content: a delayed portal keeps its DAT
tunnel live and, after five seconds, projects retail's centered
`"In Portal Space - Please Wait..."` retained notice until readiness converges.
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.
`RuntimeStateCheckpoint` is the normalized observation seam. It records the
canonical reveal snapshot plus environment and transit ownership ledgers; it is
not a second lifecycle state machine. `WorldLifecycleAutomationController`
consumes that borrowed Runtime checkpoint plus graphical resource counters and
rejects a stable checkpoint while any host acknowledgement remains.
`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, GPU, 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.
Portal, paperdoll, and creature-appraisal viewports share
`SyntheticEntityMeshReferenceOwner` but own independent leases. World
landblock retirement therefore cannot evict a mesh still used by retained UI.
The paperdoll keeps its last successful private object while the SmartBox
player is temporarily unavailable and clears it only at the character-session
boundary, matching `gmPaperDollUI::RedressCreature`.
`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. Runtime's instance-scoped `RuntimeWorldTransitState` correlates each
fresh F751 sequence with exactly one accepted `UpdatePosition` and owns the
pending/active sequence plus exact destination lifetime. Because 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.
Placement and materialization must match the exact Runtime generation,
sequence, and destination cell. `LocalPlayerTeleportController` borrows that
truth and owns only the graphical portal, render-space translation/recenter,
placement callback, tunnel, and retained UI. 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 Runtime records plus exact-key App projection
sidecars.
- **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 has completed
its implementation and automated closeout; only the user's final connected
visual matrix remains 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 57 complete landblock presentation and the
update/render frame graphs. Slice 8 checkpoints AH 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, sole terrain/sky render-resource lifetime,
retained Host/runtime lease, atomic frame-root pair, and prepared portal
fallback/transfer. `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. Checked GPU construction/state boundaries
retain exact cleanup obligations across failure. The executable nine-phase
production pipeline and terminal session start are complete. Checkpoint J
moves the exact shutdown dependency graph into `GameWindowLifetime`, whose
typed roots preserve hard session/GPU barriers, reportable physical detach,
retry/no-replay progress, and native-window-last release. Checkpoint K adds
deferred same-frame canonical resource snapshots and closes #232 through two
clean fresh-process nine-stop routes without weakening process limits.
`GameWindow` is 1,622 raw lines; 293 focused tests, the 7,823/5 Release suite,
connected lifecycle/reconnect, canonical soaks, corrected-diff reviews, and
the Slice-7 framebuffer comparison pass. The reconciled ledger lives 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; its GL calls now read as RHI intent) | ACViewer |
| Protocol | holtburger | `docs/research/named-retail/` (AC2D was the secondary until 2026-07-29 — retired reference; historical analysis remains in `docs/research/2026-04-12-movement-deep-dive.md`) |
| 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