Add a canonical documentation map, modernize the public README, and align milestone, roadmap, architecture, issue, divergence, and session guidance with the July 20 baseline. Correct the far-teleport residual to issue #153, close visually accepted indicator and terrain-tiling work, record the remaining detail-overlay and build-warning debt, and deprecate the duplicate legacy bug ledger. Co-authored-by: OpenAI Codex <codex@openai.com>
859 lines
50 KiB
Markdown
859 lines
50 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
|
|
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)
|
|
|
|
```
|
|
1. Network tick
|
|
└── Drain inbound queue → process CreateObject, UpdateMotion,
|
|
UpdatePosition, PlayerTeleport → create/update GameEntities
|
|
|
|
2. Streaming tick
|
|
└── Compute observer position → load/unload landblocks →
|
|
create terrain + scenery GameEntities
|
|
|
|
3. Input tick (player mode only)
|
|
└── InputDispatcher scopes → PlayerMovementController →
|
|
MotionInterpreter/body prediction → ResolveWithTransition →
|
|
send MoveToState/AutonomousPosition to server
|
|
|
|
4. Entity / animation tick
|
|
└── Current code still has scattered world/entity state. L.1 owns
|
|
animation parity; L.2 owns movement/collision conformance.
|
|
|
|
5. Render tick
|
|
└── Read current entity mesh refs, draw
|
|
TerrainModernRenderer + WbDrawDispatcher + EnvCellRenderer
|
|
(frustum cull, translucency pass, portal visibility, etc.)
|
|
World Wb translucents + Scene particles submit to RetailAlphaQueue;
|
|
RetailPViewRenderer flushes the landscape scope before the conditional
|
|
depth clear and GameWindow flushes the final scope before private
|
|
viewports/UI. Sky/private viewport particles remain immediate.
|
|
Projectiles remain ordinary live-entity draws; there is no global or
|
|
projectile-specific render pass.
|
|
|
|
6. Plugin tick
|
|
└── Fire IEvents, drain IActions queue
|
|
|
|
6a. UI tick
|
|
IPanelHost.Draw → iterate registered IPanel instances, build
|
|
ViewModels from IGameState, dispatch user Commands via ICommandBus.
|
|
Backend-agnostic — ImGui or custom retail-look draws here depending
|
|
on which is compiled in. See docs/plans/2026-04-24-ui-framework.md.
|
|
```
|
|
|
|
---
|
|
|
|
## Render Pipeline (SSOT — current state + unified-PView target)
|
|
|
|
> The modern path (Phase N.5, mandatory) is
|
|
> `WbDrawDispatcher` (entities) + `EnvCellRenderer` (indoor cell shells) +
|
|
> `TerrainModernRenderer` (terrain), fed by the portal-visibility stack. This section is
|
|
> the authoritative description of how indoor/outdoor rendering is *supposed* to work and
|
|
> where the code currently diverges. Canonical reset handoff:
|
|
> `docs/research/2026-05-31-render-architecture-reset-handoff.md`.
|
|
|
|
**The principle (retail PView).** acdream must render the world the way retail does —
|
|
through **one** portal-visibility traversal whose output **gates every geometry type
|
|
uniformly**. From the player's cell, walk the portal graph; each visible cell carries a
|
|
screen-space clip region (its portal opening, recursively intersected); the **outside**
|
|
(terrain + outdoor scenery) is reached only through **exit portals** and is clipped to
|
|
those openings. Interior cell shells, interior statics, and the outside are **all**
|
|
clipped to their PView region. This is why retail is **seamless by construction**. Decomp
|
|
anchors: `PView::ConstructView` (`:433750`), `InitCell` (`:432896`), `DrawCells`
|
|
(`:432715`), `CEnvCell::find_visible_child_cell` (`:311397`), `SmartBox::update_viewer`
|
|
(`:92761`). Reference port acdream owns but never invokes: WB `RenderInsideOut` /
|
|
`VisibilityManager`.
|
|
|
|
**The one rule:** *compute visibility once; enforce it once, for all geometry.* Indoors,
|
|
you see the outside **only** through portal openings (clipped); an empty outside-view
|
|
(windowless interior) draws **no** outdoor geometry. Outdoors, the gate is "everything."
|
|
|
|
**Current divergence (the patchwork — what the reset must fix).** acdream computes the
|
|
visibility correctly (`CellVisibility.ComputeVisibility` → `PortalVisibilityBuilder.Build`,
|
|
a `ConstructView` port → `ClipFrameAssembler`) but then **enforces it three different,
|
|
inconsistent ways**:
|
|
1. `TerrainModernRenderer` — gated by `TerrainClipMode {Skip|Scissor|Planes}` (the Scissor
|
|
fallback over-includes).
|
|
2. `EnvCellRenderer` — gated by the per-cell clip slot (≈correct; the shells DO render —
|
|
proven by the `[shell]` probe, `ACDREAM_PROBE_SHELL`).
|
|
3. `WbDrawDispatcher` — gated by `ParentCellId ∈ visibleCellIds`, **but outdoor stabs
|
|
(`ParentCellId==null`) bypass the gate** → outdoor scenery/terrain shows from inside
|
|
(issue #78).
|
|
|
|
Three gates that must agree but don't → structural seams (transparent walls,
|
|
terrain-through-floor, grey enclosure). **The reset consolidates them into the single
|
|
PView gate** (outside content clipped to the `OutsideView` region; no `ParentCellId==null`
|
|
bypass; no Scissor over-include). This is a **consolidation of existing machinery**
|
|
(`PortalVisibilityBuilder` + `ClipFrame`), not a rewrite. Do NOT add a fourth special-case
|
|
gate to mask a seam — that anti-pattern produced the patchwork.
|
|
|
|
### 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.
|
|
|
|
### 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** owns current connected magic/portal acceptance; Track LH is the future
|
|
presentation-independent Linux/headless extraction.
|
|
|
|
The GameEntity / thin GameWindow refactor remains a valid target architecture,
|
|
but it is not a prerequisite for L.2. Do not resurrect old R1-R8 phase numbers
|
|
for new work; add or update roadmap phases 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
|