Formalize Phase L.2 as the active holistic movement/collision program, align the roadmap and architecture docs, file tactical physics follow-ups, and refresh collision memory away from rewrite-from-zero guidance. Co-authored-by: OpenAI Codex <codex@openai.com>
17 KiB
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
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 │
│ TerrainRenderer, StaticMeshRenderer, TextureCache │
│ 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 (companion stack, spans Layers 1 & 5)
The UI is split into its own three-layer stack with a swappable backend,
designed 2026-04-24. Full design: docs/plans/2026-04-24-ui-framework.md.
┌─────────────────────────────────────────────────────────────┐
│ UI BACKEND (swappable) │
│ ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui │
│ (Phase D.2a, short-term) │
│ or custom retail-look toolkit (Phase D.2b, later) │
├─────────────────────────────────────────────────────────────┤
│ AcDream.UI.Abstractions (stable contract) │
│ ViewModels, Commands, IPanel, IPanelHost, IPanelRenderer │
│ ► plugin-facing UI API lives HERE, not in the backend │
├─────────────────────────────────────────────────────────────┤
│ Game state + events (unchanged) │
│ IGameState / IEvents / WorldSession — UI only reads │
└─────────────────────────────────────────────────────────────┘
The backend is pluggable; ViewModels / Commands / IPanelRenderer are
stable across the swap. ImGui persists forever as the
ACDREAM_DEVTOOLS=1 devtools overlay regardless of which backend owns
the game UI. See memory/project_ui_architecture.md for the session
crib-sheet version.
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
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
AcDream.App/ Layer 1 + Layer 4 wiring
Rendering/
GameWindow.cs -> still owns too much runtime wiring
TerrainRenderer.cs -> done
StaticMeshRenderer.cs -> done
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:
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.ResolveWithTransitionis the path used for local player collision resolution.BSPQuerycontains the partial retail-style BSP collision dispatcher used by the transition path.TransitionTypescarriesSpherePath,CollisionInfo,ObjectInfo, transition validation, step-up/down, contact-plane handling, and partial slide behavior.PhysicsDataCacheloads GfxObj, Setup, and CellStruct physics data from DATs.ShadowObjectRegistrygives movement a broadphase over nearby objects and buildings.TerrainSurfaceuses 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/CellBSPas 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, andNegPolyHitdispatch behavior. - Exact
CSphere/CCylSphereobject-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)
Currently, entity state is scattered across:
WorldEntity(position, rotation, mesh refs)AnimatedEntity(animation frame, setup, sequencer)_entitiesByServerGuiddict (server GUID lookup)GpuWorldState._loaded[lb].Entities(per-landblock lists)_playerController(player-specific movement)
This should become ONE class:
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 StaticMeshRenderer)
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.
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
TerrainRenderer.Draw, StaticMeshRenderer.Draw
(frustum cull, translucency pass, etc.)
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.
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 owns animation/motion parity, including root-motion coupling.
- L.2 owns movement and collision conformance:
docs/plans/2026-04-29-movement-collision-conformance.md. - G.3 owns dungeon streaming and portal-space delivery after L.2e lands
trustworthy
cell_bsp,CELLARRAY, adjacent-cell checks, and building entry/exit boundaries.
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:
- You can log in to an ACE server
- Walk around the entire world (streaming loads new areas)
- Enter and exit buildings through doorways
- See all NPCs, monsters, and players animated correctly
- Open doors, talk to NPCs, pick up items
- Send and receive chat
- A Lua macro can automate gameplay
- Side-by-side with the retail client, the world looks the same