The single most important document in the project. Defines: Architecture: 6-layer stack (Platform → Renderer → Network → World → Game Objects → Plugin API). The code is modern C#; the behavior matches the retail client exactly. GameEntity: the unified entity class that replaces the current scattered state (WorldEntity + AnimatedEntity + guid dicts + player controller). Every world object is a GameEntity with PhysicsBody + AnimationSequencer + CellTracker + MotionInterpreter + AppearanceState. Per-frame update order: Network → Streaming → Input → Entity tick (motion → physics → collision → cell → animation) → Render → Plugin. Execution plan (R1-R8): R1: GameEntity refactor (unify scattered state) R2: Thin GameWindow (extract to proper systems) R3: CellBSP + wall collision (indoor transitions) R4: Complete animation state machine R5: Lighting from decompiled AdjustPlanes R6: Server compliance (authoritative Z, keepalive) R7: Interaction (doors, NPCs, chat, inventory) R8: Plugin API completion (Lua macros) Also updates CLAUDE.md to establish the architect role and reference the architecture doc as the single source of truth. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
14 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 decompiled retail client (docs/research/decompiled/, 688K lines). 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 │
└──────────────────────────────────────────────────────────────┘
Project Structure (target)
src/
AcDream.Core/ Layer 2-4: no GL, no Silk.NET, pure logic
Physics/
PhysicsBody.cs ← ported from decompiled (done)
CollisionPrimitives.cs ← ported from decompiled (done)
MotionInterpreter.cs ← ported from decompiled (done)
AnimationSequencer.cs ← ported from decompiled (done)
CellBsp.cs ← TODO: port from decompiled
Transition.cs ← TODO: port from decompiled
TerrainSurface.cs ← verified against ACME (done)
World/
GameEntity.cs ← TODO: unified entity (replaces scattered state)
WorldState.cs ← TODO: owns all entities
CellTracker.cs ← TODO: 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 (move here 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 ← TODO: thin down to GL calls only
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 ← done (uses ported physics)
Plugins/
AppPluginHost.cs ← done
GameEntity: The Unified Entity (TODO — the big 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
// TODO: Transition.FindValidPosition // collision resolve
Cell.UpdateCell(Physics.Position); // check cell transitions
Animation.Advance(dt); // advance animation frames
RebuildMeshRefs(); // compute per-part transforms
}
}
Every entity in the world — player, NPC, monster, lifestone, door, chest —
is a GameEntity. The renderer iterates them and draws. The plugin API
exposes them as WorldEntitySnapshot. GameWindow becomes thin.
Per-Frame Update Order (matches retail)
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)
└── Read WASD/mouse → MotionInterpreter.DoMotion →
send MoveToState/AutonomousPosition to server
4. Entity tick (ALL entities, 30Hz fixed step)
└── For each GameEntity: entity.Update(dt)
This runs: motion → physics → collision → cell → animation
5. Render tick
└── For each GameEntity: read MeshRefs, draw
TerrainRenderer.Draw, StaticMeshRenderer.Draw
(frustum cull, translucency pass, etc.)
6. Plugin tick
└── Fire IEvents, drain IActions queue
Execution Plan: How to Get There
Phase R1: GameEntity Refactor (the foundation)
Goal: Replace the scattered entity state with unified GameEntity.
- Create
GameEntityclass inAcDream.Core/World/ - Move
AnimatedEntityfields intoGameEntity.Animation - Move
WorldEntityfields intoGameEntity.Physics+ position - Move
_entitiesByServerGuidintoWorldState - Move animation tick from
GameWindow.TickAnimationsintoGameEntity.Update - GameWindow.OnRender reads
GameEntity.MeshRefsinstead ofWorldEntity.MeshRefs
Test: Everything looks the same as before. No visual change.
Phase R2: Thin GameWindow
Goal: GameWindow does only GL calls + input dispatch.
- Extract entity creation from
OnLiveEntitySpawnedintoWorldState.SpawnEntity - Extract motion updates from
OnLiveMotionUpdatedintoWorldState.UpdateMotion - Extract player movement from the giant OnUpdate block into
PlayerController - GameWindow.OnUpdate calls: network.Tick → streaming.Tick → input.Tick → worldState.Tick → render
Test: Everything works the same. GameWindow.cs drops from 2000+ to ~500 lines.
Phase R3: CellBSP + Wall Collision
Goal: Entities can't walk through walls.
- Port CellBSP from decompiled code (sphere_intersects_cell)
- Port Transition.FindValidPosition (swept sphere collision)
- Wire into GameEntity.Update between physics and cell tracking
- Indoor transitions become correct (wall stops you, doorway lets you through)
Test: Walk into building wall → stopped. Walk through doorway → enter.
Phase R4: Complete Animation State Machine
Goal: Every animation works for every entity type.
- Port full MotionInterp.PerformMovement from decompiled (all 5 movement types)
- Port Links table resolution for smooth transitions
- Port idle modifiers (fidgets)
- Jump animation (wire jump motion command through the pipeline)
Test: All entity types animate correctly. Transitions are smooth.
Phase R5: Lighting from Retail
Goal: Sun, ambient, per-vertex lighting match retail.
- Port AdjustPlanes (FUN_00532440) — face normals + per-vertex lighting
- Extract global lighting constants from decompiled DAT addresses
- Replace hardcoded shader constants with ported values
Test: Side-by-side with retail client shows matching lighting.
Phase R6: Server Compliance
Goal: ACE accepts all movement, no rubber-banding.
- Server-authoritative Z (trust server position, local is cosmetic)
- Proper MoveToState with full RawMotionState packing
- Keepalive ping (5s idle)
- Graceful session management
Test: Walk around, other clients see smooth movement. No ACE errors.
Phase R7: Interaction
Goal: Click NPCs, open doors, pick up items, chat.
- Use/UseWithTarget game actions
- Door open animation (server sends UpdateMotion → animate)
- Chat send/receive
- Basic inventory (pickup/drop)
Test: Open a door, talk to an NPC, send a chat message.
Phase R8: Plugin API Completion
Goal: Plugins can observe and control everything.
- IGameState exposes all GameEntity fields
- IEvents fires for all world changes
- IActions covers: Move, Cast, Use, Say, Pickup, Drop
- IPacketPipeline hooks all 4 stages
- Lua macro engine (MoonSharp) ships as a built-in plugin
Test: A Lua script auto-loots gems. A C# plugin displays an overlay.
Development Workflow (mandatory for ALL work)
For every AC-specific behavior:
1. DECOMPILE → Find the function in docs/research/decompiled/
2. CROSS-CHECK → Verify against ACE + ACME + holtburger
3. PSEUDOCODE → Translate to readable pseudocode
4. PORT → Faithful C# translation
5. TEST → Conformance test against decompiled 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 | Decompiled acclient.exe | ACE Physics/ |
| Animation | Decompiled + ACE Animation/ | — |
| Terrain | ACME ClientReference.cs | Decompiled |
| 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