diff --git a/CLAUDE.md b/CLAUDE.md index 76a06dd..243963b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,24 +2,35 @@ ## Goal -Build **acdream**, a modern open-source C# .NET 10 Asheron's Call client. The -end state is a working client that: +Build **acdream**, a modern open-source C# .NET 10 Asheron's Call client. +A faithful port of the retail AC client's behavior to modern C# + Silk.NET, +with a plugin API the original never had. -- Loads the retail AC dat files and renders the world (terrain, static meshes, - dynamic entities, characters) -- Connects to an ACE server and plays as a character -- Exposes a **first-class plugin API** so players can write native scripts and - macros to automate gameplay — this is a core architectural requirement, not - a bolt-on +**The code is modern. The behavior is retail.** -The codebase is organized by phase. Current phase state lives in memory -(`memory/project_phase_*_state.md`), current phase plans live in `docs/plans/`, -and the long-term vision lives in `memory/project_acdream.md`. +Every AC-specific algorithm is ported from the decompiled retail client +(`docs/research/decompiled/`, 22,225 functions, 688K lines of C). The code +around those algorithms is modern C# with clean architecture. The plugin API +exposes game state through well-defined interfaces. + +**Architecture:** `docs/architecture/acdream-architecture.md` is the +single source of truth for how the client is structured. All work must +align with this document. When the architecture doc and reality diverge, +update one or the other — never leave them out of sync. + +**Execution phases:** R1→R8 in the architecture doc. Each phase has clear +goals, test criteria, and builds on the previous. Don't skip phases. + +The codebase is organized by layer (see architecture doc). Current phase +state lives in memory (`memory/project_*.md`), plans in `docs/plans/`, +research in `docs/research/`. ## How to operate -**You are the lead engineer on this project at all times. Stop as little as -possible.** Drive work autonomously and continuously through full phases and +**You are the lead engineer AND architect on this project at all times.** +You own the architecture (`docs/architecture/acdream-architecture.md`), +the execution plan (phases R1–R8), the development workflow, and all +technical decisions. Stop as little as possible. Drive work autonomously and continuously through full phases and across commit boundaries. Do not stop mid-phase for routine progress check-ins, permission asks on low-stakes design calls, or "should I continue?" confirmations. The user has repeatedly authorized direct-to-main commits, multi-commit sessions, diff --git a/docs/architecture/acdream-architecture.md b/docs/architecture/acdream-architecture.md new file mode 100644 index 0000000..3fb1e96 --- /dev/null +++ b/docs/architecture/acdream-architecture.md @@ -0,0 +1,343 @@ +# 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) +- `_entitiesByServerGuid` dict (server GUID lookup) +- `GpuWorldState._loaded[lb].Entities` (per-landblock lists) +- `_playerController` (player-specific movement) + +This 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 StaticMeshRenderer) + public IReadOnlyList 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. + +1. Create `GameEntity` class in `AcDream.Core/World/` +2. Move `AnimatedEntity` fields into `GameEntity.Animation` +3. Move `WorldEntity` fields into `GameEntity.Physics` + position +4. Move `_entitiesByServerGuid` into `WorldState` +5. Move animation tick from `GameWindow.TickAnimations` into `GameEntity.Update` +6. GameWindow.OnRender reads `GameEntity.MeshRefs` instead of `WorldEntity.MeshRefs` + +**Test:** Everything looks the same as before. No visual change. + +### Phase R2: Thin GameWindow +**Goal:** GameWindow does only GL calls + input dispatch. + +1. Extract entity creation from `OnLiveEntitySpawned` into `WorldState.SpawnEntity` +2. Extract motion updates from `OnLiveMotionUpdated` into `WorldState.UpdateMotion` +3. Extract player movement from the giant OnUpdate block into `PlayerController` +4. 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. + +1. Port CellBSP from decompiled code (sphere_intersects_cell) +2. Port Transition.FindValidPosition (swept sphere collision) +3. Wire into GameEntity.Update between physics and cell tracking +4. 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. + +1. Port full MotionInterp.PerformMovement from decompiled (all 5 movement types) +2. Port Links table resolution for smooth transitions +3. Port idle modifiers (fidgets) +4. 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. + +1. Port AdjustPlanes (FUN_00532440) — face normals + per-vertex lighting +2. Extract global lighting constants from decompiled DAT addresses +3. 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. + +1. Server-authoritative Z (trust server position, local is cosmetic) +2. Proper MoveToState with full RawMotionState packing +3. Keepalive ping (5s idle) +4. 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. + +1. Use/UseWithTarget game actions +2. Door open animation (server sends UpdateMotion → animate) +3. Chat send/receive +4. 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. + +1. IGameState exposes all GameEntity fields +2. IEvents fires for all world changes +3. IActions covers: Move, Cast, Use, Say, Pickup, Drop +4. IPacketPipeline hooks all 4 stages +5. 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: +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 diff --git a/docs/research/acclient_architecture_map.md b/docs/research/acclient_architecture_map.md new file mode 100644 index 0000000..698cff7 --- /dev/null +++ b/docs/research/acclient_architecture_map.md @@ -0,0 +1,832 @@ +# AC Client — Complete Architecture Map + +**Sources used:** +- `docs/research/acclient_function_map.md` — 70+ decompiled function addresses +- `docs/research/acclient_animation_pseudocode.md` — full animation system pseudocode +- `docs/research/2026-04-12-movement-deep-dive.md` — movement cross-reference +- `references/ACE/Source/ACE.Server/Physics/` — ACE's C# physics port (read directly) + +--- + +## System Overview (dependency graph) + +``` +PhysicsEngine + └─ iterates PhysicsObj list → update_object() + +PhysicsObj (root entity) + ├─ Position (ObjCellID + AFrame) + ├─ CurCell (ObjCell ptr) + ├─ CurLandblock (Landblock ptr) + ├─ PartArray (skeleton + animation) + │ ├─ Setup (geometry: GfxObj parts, spheres, BSP) + │ ├─ Sequence (animation playback state machine) + │ └─ MotionTableManager (transition resolver) + ├─ MovementManager + │ ├─ MotionInterp (raw→interpreted motion state machine) + │ └─ MoveToManager (pathfinding / move-to target) + ├─ PositionManager (sticky/constraint) + ├─ WeenieObject (game-logic bridge — callbacks into WorldObject) + ├─ Children / Parent (attachment hierarchy) + └─ ShadowObjects (multi-cell presence) + +Cell hierarchy (ObjCell subtypes): + ObjCell (abstract base) + ├─ SortCell (has a BuildingObj for building collision) + │ └─ LandCell (outdoor cell: 2 terrain polygons, water) + └─ EnvCell (indoor/dungeon cell: portals, static objs, visibility list) + +Terrain hierarchy: + LandblockStruct (raw height/terrain arrays, polygon list, SWtoNEcut flags) + └─ Landblock (8x8 grid of LandCells, static object list) + └─ LandCell[64] (per-cell collision polygon pair) +``` + +--- + +## 1. PhysicsObj — Root Physics Entity + +**ACE file:** `PhysicsObj.cs` +**Decompiled:** `chunk_00510000.c`, `chunk_00500000.c` — base at 0x510000 + +### Data owned + +| Field | Type | Decompiled offset | Purpose | +|-------|------|-------------------|---------| +| ID | uint | — | Object identity (GUID) | +| Position | Position | — | ObjCellID + AFrame (origin + quaternion) | +| CurCell | ObjCell | — | Current home cell | +| CurLandblock | Landblock | — | Current landblock | +| State | PhysicsState | +0xA8 | Bitmask: Static, Hidden, Gravity, Ethereal, HasPhysicsBSP, Missile, Frozen... | +| TransientState | TransientStateFlags | +0xAC | Contact, OnWalkable, Sliding, Active, CheckEthereal... | +| Elasticity | float | +0xB0 | Bounce coefficient | +| UpdateTime | double | +0xD8 | LastUpdateTime for delta accumulation | +| Velocity | Vector3 | +0xE0/E4/E8 | World-space velocity (m/s) | +| Acceleration | Vector3 | +0xEC/F0/F4 | World-space acceleration (gravity = -9.8 Z when Gravity flag set) | +| Omega | Vector3 | +0xF8/FC/100 | Angular velocity | +| WeenieObject | ptr | +0x12C | Callback bridge into game logic | +| ContactPlane | Plane | — | Ground contact plane from last collision | +| SlidingNormal | Vector3 | — | Surface normal during slide | +| CachedVelocity | Vector3 | — | Velocity between position samples (for rendering) | +| PartArray | PartArray | — | Skeleton and animation state | +| MovementManager | MovementManager | — | Owns MotionInterp + MoveToManager | +| Children / Parent | ChildList / PhysicsObj | — | Attachment tree | +| ShadowObjects | Dict | — | Per-cell presence records | +| CollisionTable | Dict | — | Active obj-obj collision records | +| Scale | float | — | Uniform scale factor | + +### Behavior (key methods) + +| Method | Address | What it does | +|--------|---------|--------------| +| `update_object()` | 0x515020 | Per-frame entry: computes deltaTime, calls `UpdateObjectInternal` in fixed-step loop (MaxQuantum slices) | +| `UpdateObjectInternal(quantum)` | — | Calls `UpdatePositionInternal` → `transition()` → `SetPositionInternal`. Also calls `PartArray.HandleMovement`, `MovementManager.UseTime`, `PositionManager.UseTime` | +| `UpdatePositionInternal(quantum)` | 0x513730 | Calls `PartArray.Update` (animation frame advance) → `PositionManager.AdjustOffset` → builds `newFrame` → calls `UpdatePhysicsInternal` | +| `UpdatePhysicsInternal(quantum)` | 0x5111D0 | Euler: `pos += vel*dt + 0.5*accel*dt²`; applies friction; clamps max velocity (50.0) | +| `calc_acceleration()` | 0x511420 | Sets gravity (-9.8 Z) when Gravity flag set | +| `set_velocity()` | 0x511EC0 | Stores velocity, clamps to MaxVelocity | +| `set_local_velocity()` | 0x511FA0 | Body→world transform then set_velocity | +| `set_on_walkable()` | 0x511DE0 | Sets/clears OnWalkable transient flag | +| `report_collision_start()` | 0x511560 | Fires WeenieObj.HandleEnvironmentCollision | +| `report_collision_end()` | 0x513AC0 | Fires WeenieObj.HandleCollisionEnd | +| `handle_obj_collision()` | 0x513B60 | Obj-obj dispatch → WeenieObj.HandleCollisionStart or HandleMissileCollision | +| `handle_collision()` | 0x515280 | Elasticity bounce response | +| `update_animation()` | — | Animation-only update path (for entities that animate but don't move) | +| `UpdateAnimationInternal(quantum)` | 0x5111D0 area | `PartArray.Update(quantum)` → `set_frame()` → `PartArray.HandleMovement()` | +| `DestroyObject()` | — | `leave_cell` → `remove_shadows_from_cells` → `leave_world` → `exit_world` | +| `DoMotion(motion, params)` | — | Routes through `MovementManager.PerformMovement` | +| `DoInterpretedMotion(motion, params)` | — | Routes through `PartArray.DoInterpretedMotion` | + +### Dependencies + +- Calls INTO: `PartArray`, `MovementManager`, `PositionManager`, `WeenieObject` (vtable callbacks), `ObjCell` (via transition), `Transition` +- Called BY: `PhysicsEngine.update()` (top-level loop) + +--- + +## 2. PartArray — Skeleton + Animation Frame State + +**ACE file:** `PartArray.cs` +**Decompiled:** (embedded within CPhysicsObj chunk) + +### Data owned + +| Field | Type | Purpose | +|-------|------|---------| +| State | uint | Bitmask (HasPhysicsBSP = 0x10000) | +| Owner | PhysicsObj | Back-pointer | +| Sequence | Sequence | Animation playback state machine | +| MotionTableManager | MotionTableManager | Transition resolver (wraps MotionTable) | +| Setup | Setup | Dat resource: GfxObj parts, sphere collision shapes, BSP | +| NumParts | int | Part count | +| Parts | List\ | Individual mesh parts (each has a Position) | +| Scale | Vector3 | Part-level scale | +| LastAnimFrame | AnimationFrame | Most recently computed pose | + +### Behavior + +| Method | What it does | +|--------|--------------| +| `CreateMesh(owner, setupDID)` | Factory: loads Setup from dat, creates parts, sets placement frame 0x65 | +| `CreateSetup(owner, setupDID, createParts)` | Full creature setup factory | +| `Update(quantum, ref offsetFrame)` | Calls `Sequence.Update(quantum, ref offsetFrame)` — advances animation, accumulates velocity | +| `SetFrame(frame)` | Applies AFrame to all Parts | +| `HandleMovement()` | Calls `MotionTableManager.CheckForCompletedMotions()` | +| `DoInterpretedMotion(motion, params)` | Routes into `MotionTableManager.DoInterpretedMotion` | +| `AddPartsShadow(cell, count)` | Registers all parts into a shadow cell (multi-cell presence) | +| `AnimationDone(success)` | Notifies MotionTableManager that a one-shot completed | + +### Three PartArray creation factories + +1. `CreateMesh` — simple static mesh (no animation MotionTableManager) +2. `CreateParticle` — particle emitter (special Setup, no skeleton) +3. `CreateSetup` — full creature (has MotionTableManager + Sequence) + +--- + +## 3. Sequence — Animation Playback State Machine + +**ACE file:** `Animation/Sequence.cs` +**Decompiled struct:** `chunk_00520000.c` (FUN_00526110, FUN_005261D0, FUN_00525EB0) + +### Data owned + +``` +Sequence { + AnimList LinkedList // pending animation queue + FirstCyclic LinkedListNode // marks start of looping section + CurrAnim LinkedListNode // currently playing node + Velocity Vector3 // accumulated linear velocity from animation + Omega Vector3 // accumulated angular velocity + HookObj PhysicsObj // entity to fire events on + FrameNumber float // current fractional frame position + PlacementFrame AnimationFrame // static pose when no animation active + PlacementFrameID int + IsTrivial bool // optimization: skip update if idle +} +``` + +Decompiled layout (raw offsets in AnimNode / Sequence structs): +``` +Sequence +0x04 = listHead (AnimNode* front) +Sequence +0x08 = listTail (AnimNode* back) +Sequence +0x0C = current (currently playing ptr) +Sequence +0x10..0x24 = linearVelocity + angularVelocity (3+3 floats) +Sequence +0x28 = hasCallback +Sequence +0x30 = framePosition (double) +Sequence +0x38 = activeNode +Sequence +0x3C = overrideFrame +``` + +### AnimSequenceNode (ACE) / AnimNode (decompiled) + +``` +AnimNode (28 bytes = 0x1C) { + vtable* // AnimData vtable + next* // forward link in doubly-linked list + prev* // backward link + frameCount // total frames in this animation + speedScale // playback speed multiplier (negative = reverse) + startFrame // inclusive start (swap with endFrame when speedScale < 0) + endFrame // inclusive end +} +``` + +### Behavior (key methods) + +| Method | Address | What it does | +|--------|---------|--------------| +| `Update(quantum, ref offsetFrame)` | — | Calls `update_internal(frameRate, ...)` | +| `update_internal(frameRate, ...)` | 0x5261D0 | Core per-frame loop: advance `framePosition` by `rate*dt`; fire frame-trigger events; call `advance_to_next_animation` on boundary | +| `advance_to_next_animation(...)` | 0x525EB0 | Pops exhausted node; loads next; seeds new `framePosition` from GetStartFramePosition | +| `AppendAnimation(node)` | 0x526110 | Push AnimNode onto pending list tail | +| `GetCurrAnimFrame()` | — | Returns `CurrAnim.get_part_frame(frameNumber)` or `PlacementFrame` | +| `clear_animations()` | — | Empties AnimList, resets CurrAnim | +| `clear_physics()` | — | Zeros Velocity + Omega | +| `is_first_cyclic()` | — | True if at or before the FirstCyclic marker (idle check) | +| `CombinePhysics(vel, omega)` | — | Adds to Velocity + Omega accumulators | + +### Frame position arithmetic + +- **Forward** (speedScale > 0): framePosition advances from `startFrame` toward `endFrame+1` +- **Reverse** (speedScale < 0): framePosition advances from `endFrame+1-epsilon` toward `startFrame` +- **Boundary**: when `floor(framePosition) > endFrame` (forward) or `< startFrame` (reverse), node is exhausted; `advance_to_next_animation` is called with remaining time +- **Events**: at each whole-frame crossing, frame-trigger events fire (`FireApproachEvent` forward, `FireLeaveEvent` reverse) + +--- + +## 4. MotionInterp — Motion State Machine + +**ACE file:** `Animation/MotionInterp.cs` +**Decompiled:** `chunk_00520000.c` (FUN_00528xxx, FUN_005293xx) + +### Data owned + +| Field | Type | Decompiled offset | +|-------|------|-------------------| +| WeenieObj | WeenieObject | +0x04 | +| PhysicsObj | PhysicsObj | +0x08 | +| RawState | RawMotionState | +0x14 — wire format: ForwardCmd, SidestepCmd, TurnCmd, speeds, HoldKey | +| InterpretedState | InterpretedMotionState | +0x44 — resolved state after interpretation | +| CurrentSpeedFactor | float | — | +| StandingLongJump | bool | +0x70 | +| JumpExtent | float | +0x74 | +| MyRunRate | float | +0x7C | +| PendingMotions | LinkedList\ | — | + +### Constants + +``` +BackwardsFactor = 0.65 +MaxSidestepRate = 3.0 +RunAnimSpeed = 4.0 +RunTurnFactor = 1.5 +SidestepAnimSpeed = 1.25 +SidestepFactor = 0.5 +WalkAnimSpeed = 3.12 +``` + +### Behavior + +| Method | Address | What it does | +|--------|---------|--------------| +| `PerformMovement(mvs)` | 0x529A90 | Top-level dispatch (switch on MovementType 1-5) | +| `DoMotion(motion, params)` | 0x529930 | Raw motion command → adjust_motion → DoInterpretedMotion | +| `DoInterpretedMotion(motion, params)` | 0x528F70 | Core: contact_allows_move check → PhysicsObj.DoInterpretedMotion → add_to_queue → InterpretedState.ApplyMotion | +| `StopInterpretedMotion(motion, params)` | 0x529080 | Stop specific interpreted motion | +| `StopMotion(motion, params)` | 0x529140 | Stop specific raw motion | +| `StopCompletely()` | 0x528A50 | Reset to Ready/idle, clear all pending | +| `adjust_motion(motion, speed, holdKey)` | 0x5287F0 | Apply speed adjustments for run/walk/strafe | +| `apply_raw_movement()` | 0x5293F0 | Convert RawMotionState → InterpretedMotionState | +| `apply_current_movement()` | 0x529210 | Set physics velocity from InterpretedState | +| `get_state_velocity(motion)` | 0x528960 | Compute velocity for current motion (calls WeenieObj.InqRunRate) | +| `contact_allows_move(motion)` | 0x528DD0 | Slope angle check: can we move in this direction given terrain normal? | +| `jump(params)` | 0x529390 | Initiate jump sequence | +| `jump_is_allowed(extent)` | 0x528EC0 | Full jump permission check | +| `get_leave_ground_velocity()` | 0x528CD0 | Compute 3D launch vector | +| `LeaveGround()` | 0x529710 | Called when becoming airborne; resets jump state | +| `HitGround()` | 0x5296D0 | Landing handler; fires WeenieObj landing callbacks | + +### Motion command bit flags (from PerformMovement — 0x523400) + +``` +Bit 31 set (int < 0): STANCE CHANGE — transition to new stance/style +Bit 30 set (0x40000000): CYCLE MOTION — looping (Walk, Run, TurnLeft...) +Bit 28 set (0x10000000): LINKED MOTION — one-shot that links back to cycle +Bit 29 set (0x20000000): SUBSTATE ANIM — play a substate override +``` + +--- + +## 5. MotionTable / MotionTableManager — Animation Data + Transition Resolution + +**ACE files:** `Animation/MotionTable.cs`, `Animation/MotionTableManager` (implicit in PartArray) +**Decompiled:** `chunk_00520000.c` (FUN_00521770, FUN_005231D0, FUN_005232B0, FUN_00523000, FUN_00523400) + +### MotionTable data (loaded from dat resource 0x09xxxxxx) + +``` +MotionTable { + ID uint + DefaultStyle uint // default stance (e.g. NonCombat) + StyleDefaults Dict + Cycles Dict<(style<<16)|substate, MotionData> + Modifiers Dict + Links Dict> +} +``` + +Each `MotionData` (= `TransitionLink` in decompiled) contains: +- `animId` — dat animation resource to play +- `speedScale` — playback speed multiplier +- `startFrame`, `endFrame`, `frameCount` +- Linear/angular velocity of the animation +- Chain of AnimNode entries (for multi-step transitions) + +### Transition lookup — `FindBestTransition` (0x5232B0) + +``` +1. Direct lookup: (fromStyle<<16 | substate) → (toStyle or substate) +2. Speed-sensitive variant: same but uses exact speed matching +3. Fallback via defaultMotionId +4. Fallback via defaultTransMap +``` + +The composite hash key encodes both states in a single uint, looked up via `LookupTransitionNode` (0x5231D0). + +### PerformMovement dispatch — `FUN_00523400` + +``` +1. Look up current animation ID for current state +2. Guard: already in this cycle? Early-out. +3. Branch on motion command type (stance/cycle/linked/substate): + STANCE CHANGE: + a. Find exit-substate transition → AddAnimationsToSequence + b. Find enter-new-stance transition → AddAnimationsToSequence + c. Update currentState + CYCLE MOTION: + a. Find exit-current-cycle transition (one-shot) + b. Find enter-new-cycle transition (sets FirstCyclic marker) + c. AddAnimationsToSequence for both + LINKED MOTION: + a. Find link animation + b. Append after current FirstCyclic marker + SUBSTATE ANIM: + a. Find substate animation + b. Append with override flag +``` + +`AddAnimationsToSequence` (0x523000): +- Applies linear/angular velocity from the TransitionLink to the Sequence +- Iterates the chain of AnimNode entries +- For each: `BuildTempAnimNode` → `Sequence.AppendAnimation` + +--- + +## 6. Transition — Collision / Movement Resolution + +**ACE file:** `Animation/Transition.cs` (note: namespace is Animation not Collision) +**Decompiled:** `chunk_00530000.c` (FUN_005384E0, FUN_005387C0, FUN_00538180) + +### Data owned + +``` +Transition { + ObjectInfo ObjectInfo // what's moving (state flags: IsPlayer, IsViewer, Ethereal...) + SpherePath SpherePath // the swept sphere path from old→new position + CollisionInfo CollisionInfo // accumulates contact plane, sliding normal, hit objects + CellArray CellArray // cells the sphere path passes through +} +``` + +`SpherePath` tracks: +- `GlobalSphere[]` — world-space sphere(s) representing the object's collision volume +- `CheckPos` — current position being tested +- `CurPos` — resolved valid position +- `CurCell` — resolved cell +- `StepDown` — flag for step-down terrain contact +- `PlacementAllowsSliding` — whether sliding is allowed + +`CollisionInfo` tracks: +- `ContactPlane` — ground/wall plane +- `SlidingNormal` — lateral deflection normal +- `CollidedWithEnvironment` — hit static geo +- `AddObject(obj, state)` — record object collision + +### Key collision methods + +| Method | Address | What it does | +|--------|---------|--------------| +| `FindValidPosition()` | — | Main loop: sweep sphere through cells, test polygon collision | +| `find_collisions()` | 0x5387C0 | Iterates CellArray, calls cell.FindCollisions on each | +| `collide_with_point()` | 0x538180 | Point collision handler | +| `AdjustOffset(offset)` | — | Projects movement vector along contact/sliding planes | +| `Sphere.slide_sphere()` | 0x538EB0 | Slide along a surface | +| `Sphere.land_on_sphere()` | 0x538F50 | Step down to surface | +| `Polygon.sphere_intersects_poly()` | 0x539500 | Sphere-polygon contact test | +| `Polygon.find_walkable_collision()` | 0x53A040 | Returns edge normal for walkable surface | +| `Polygon.find_time_of_collision()` | 0x539BA0/0x539DF0 | Ray-plane-polygon t (sphere/cylinder variants) | + +### Transition outcomes + +``` +TransitionState { + Invalid = 0 // bad input + OK = 1 // no collision / passthrough + Collided = 2 // hit solid surface + Adjusted = 3 // position corrected (slide/step) + Slid = 4 // sliding along surface +} +``` + +--- + +## 7. LandCell / EnvCell / SortCell — Cell Management + +### Class hierarchy + +``` +ObjCell (abstract base) + ID, WaterType, Pos, ObjectList, ShadowObjectList + ClipPlanes, VisibleCells, VoyeurTable + → AddObject / RemoveObject + → abstract FindCollisions(Transition) + → abstract find_transit_cells(...) + +SortCell : ObjCell + Building (BuildingObj — handles building collision + transit cell lookup) + → FindCollisions: delegates to Building.find_building_collisions + +LandCell : SortCell + Polygons List // always exactly 2 (the two terrain triangles) + InView bool + → FindCollisions: FindEnvCollisions (terrain polygon) → base (building) → FindObjCollisions + → FindEnvCollisions: find_terrain_poly → ValidateWalkable + +EnvCell : ObjCell + CellStructure CellStruct // geometry from Environment dat + Portals List + StaticObjectIDs List + StaticObjectFrames List + StaticObjects List + VisibleCellIDs List + VisibleCells Dict + Flags EnvCellFlags + SeenOutside bool + → PostInit: build_visible_cells + init_static_objects + → FindCollisions: CellStruct BSP collision +``` + +### ObjCell key fields + +``` +ID uint // cell ID: high 16 = landblock (0xXXYY), low 16 = cell index + // outdoor: 0x0001..0x0040 (64 outdoor cells per LB) + // indoor: 0x0100..0xFFFD (EnvCell IDs) +Pos Position // cell's own origin frame +ObjectList List // entities currently in this cell +``` + +### Cell ID conventions + +``` +outdoor cell: (blockX << 24) | (blockY << 16) | cellIndex where cellIndex = 0x0001..0x0040 +indoor cell: (blockX << 24) | (blockY << 16) | cellIndex where cellIndex = 0x0100..0xFFFD +special: 0xFFFF = "lost" / no cell +``` + +--- + +## 8. CLandBlock / CLandBlockStruct — Terrain + +**ACE file:** `Common/LandblockStruct.cs`, `Common/Landblock.cs` +**Decompiled:** `chunk_00530000.c` (0x530690, 0x531780, 0x531D10, 0x532A50, 0x532EB0, 0x532D10) + +### LandblockStruct (raw terrain data) + +``` +LandblockStruct { + ID uint + TransDir LandDefs.Direction + SideVertexCount, SidePolyCount, SideCellCount int + WaterType LandDefs.WaterType + Height List // 9×9 = 81 height values (0..127 → 0..63.5 meters) + Terrain List // 8×8 = 64 terrain type codes (palCode) + VertexArray VertexArray // computed vertices (9×9 = 81 positions) + Polygons List // computed polygons (8×8×2 = 128 triangles) + SWtoNEcut List // per-cell split direction (64 entries) + LandCells ConcurrentDict // 64 outdoor cells +} +``` + +**Key algorithms (decompiled addresses):** + +| Method | Address | What it does | +|--------|---------|--------------| +| `IsSWtoNECut(x, y)` | 0x531D10 | Inner split test with 0xCCAC033 constants | +| `ConstructPolygons()` | 0x532A50 | Outer 8×8 loop using FSplitNESW formula | +| `GetCellRotation / ConstructUVs` | 0x532EB0 | PalCode computation for UV rotation | +| `unpack()` | 0x532D10 | Deserialize from dat stream | +| `get_packed_size()` | 0x531F10 | Returns 0xF4 (244 bytes) | +| `AdjustPlanes()` | 0x532440 | Normal accumulation + lighting | +| `CalcCellWater()` | 0x532290 | Water depth check | + +**CRITICAL — terrain split formula:** + +The CORRECT render formula (from AC2D, confirmed by ACME `ClientReference.cs`): +```cpp +bool FSplitNESW(DWORD x, DWORD y) { + DWORD dw = x * y * 0x0CCAC033 - x * 0x421BE3BD + y * 0x6C1AC587 - 0x519B8F25; + return (dw & 0x80000000) != 0; +} +// x = blockX * 8 + cellX (GLOBAL cell coordinates) +// y = blockY * 8 + cellY +// true = NE/SW split (SW corner → NE corner diagonal) +``` + +### Landblock (manager) + +``` +Landblock { + // manages the 8×8 grid of outdoor LandCells + // static object list (from LandBlockInfo dat) + // links to neighbor landblocks for cross-boundary movement +} +``` + +**Key methods (decompiled):** + +| Method | Address | What it does | +|--------|---------|--------------| +| `Init / constructor` | 0x530690 | Initialize landblock fields | +| `release_all()` | 0x5307E0 | Free all resources | +| `init_static_objs()` | 0x531780 | Load static objects from LandBlockInfo dat | +| `release_visible_cells()` | 0x531000 | Free cell data | +| `grab_visible_cells()` | 0x5301E0 | BFS neighbor expansion | +| `add_server_object()` | 0x530650 | Add entity to landblock | + +--- + +## 9. Position / ObjCell — Coordinate System + +**ACE file:** `Common/Position.cs` + +### Position + +``` +Position { + ObjCellID uint // which cell this position is in + Frame AFrame // origin (Vector3) + orientation (Quaternion) +} +``` + +### AFrame + +``` +AFrame { + Origin Vector3 // landblock-local XYZ (meters within landblock = 192×192m area) + Orientation Quaternion // WXYZ quaternion + + LocalToGlobal(pt) // rotate point by orientation, add origin + GlobalToLocal(pt) // inverse: subtract origin, inverse-rotate + Combine(a, b) // concatenate two frames + GRotate(omega*dt) // apply angular velocity +} +``` + +### Coordinate conventions + +``` +Landblock size: 192 meters × 192 meters (8×8 cells, each 24m × 24m) +Height range: 0..127 height units → 0..63.5 meters (scale × 0.5) +Cell terrain polygon: each cell has 2 triangles (SW or NE split) +Global origin: (0,0) = SW corner of landblock grid +AC heading: 0 = facing west (-X), PI/2 = north (+Y), PI = east (+X) +Gravity: Z-down, gravity = -9.8 m/s² +``` + +### LandDefs (coordinate utilities) (0x5AAA30 area) + +| Method | Address | What it does | +|--------|---------|--------------| +| `get_vars()` | 0x5AAA30 | Set 8 coordinate constants | +| `get_outside_lcoord()` | 0x5AABB0 | Cell ID → world coord | +| `AdjustToOutside()` | 0x5AAC70 | Normalize position to outdoor coords | +| `get_block_dir()` | 0x5AAB50 | Quadrant → Direction enum | + +--- + +## 10. WeenieObject — Game Logic Bridge + +**ACE file:** `Common/WeenieObject.cs` + +### Data owned + +``` +WeenieObject { + ID uint + UpdateTime double + WorldObjectInfo WorldObjectInfo // weak reference to WorldObject + IsMonster bool + IsCombatPet bool + Faction1Bits FactionBits + PlayerKillerStatus PlayerKillerStatus +} +``` + +### Vtable callbacks (called FROM physics INTO game logic) + +| Vtable offset | ACE method | Called from physics when... | +|--------------|-----------|---------------------------| +| +0x30 | `InqJumpVelocity(extent)` | Computing jump Z velocity | +| +0x34 | `InqRunRate()` | Computing movement speed for entity | +| +0x3C | `CanJump(extent)` | Checking if jump is permitted | +| +0x4C | `HandleCollisionEnd(obj)` | Object collision ended | +| +0x50 | `HandleCollisionStart(obj)` | Object collision started | +| +0x54 | `HandleMissileCollision(obj)` | Missile hit entity | +| +0x58 | `HandleEnvironmentCollision(normal)` | Hit static geometry | + +### Key predicates + +``` +IsPlayer() — ID in 0x50000001..0x5FFFFFFF +IsCreature() — has creature WorldObject +IsMonster — creature and not combat pet +IsPK() — PlayerKillerStatus == PK +``` + +--- + +## Per-Frame Update Order + +### Client entity update (static/NPC — `update_animation` path) + +``` +1. PhysicsEngine.update() + └─ for each active PhysicsObj: + └─ update_animation() + ├─ compute deltaTime = now - UpdateTime + ├─ while deltaTime > MaxQuantum (0.034s): + │ └─ UpdateAnimationInternal(MaxQuantum) + │ ├─ PartArray.Update(quantum, ref newFrame) + │ │ └─ Sequence.update_internal(frameRate, ...) + │ │ ├─ advance framePosition by rate*dt + │ │ ├─ fire frame-trigger events + │ │ └─ advance_to_next_animation on boundary + │ ├─ set_frame(newFrame) // update all parts' positions + │ └─ PartArray.HandleMovement() // check completed motions + └─ UpdateTime = now +``` + +### Moving entity update (player — `update_object` path) + +``` +1. PhysicsEngine.update() + └─ for each active PhysicsObj: + └─ update_object() + ├─ compute deltaTime + ├─ while deltaTime > MaxQuantum: + │ └─ UpdateObjectInternal(quantum) + │ ├─ UpdatePositionInternal(quantum, ref newFrame) + │ │ ├─ PartArray.Update(quantum, ref offsetFrame) // animation → root motion + │ │ ├─ PositionManager.AdjustOffset(offsetFrame) // sticky/constraint + │ │ ├─ build newFrame = Combine(Position.Frame, offsetFrame) + │ │ └─ UpdatePhysicsInternal(quantum, ref newFrame) // Euler integration + │ │ ├─ pos += vel*dt + 0.5*accel*dt² + │ │ ├─ calc_friction + │ │ └─ vel += accel*dt + │ ├─ transition(oldPos, newPos) // collision resolve + │ │ ├─ build SpherePath (swept sphere) + │ │ ├─ find_transit_cells → CellArray + │ │ ├─ for each cell: cell.FindCollisions(transition) + │ │ │ ├─ LandCell: terrain polygon test → ValidateWalkable + │ │ │ ├─ SortCell/BuildingObj: building BSP test + │ │ │ └─ ObjCell: obj-obj collision + │ │ └─ AdjustOffset → clamp to contact/sliding planes + │ ├─ SetPositionInternal(transit) // apply resolved pos + │ │ ├─ change_cell if needed + │ │ └─ update ShadowObjects + │ ├─ MovementManager.UseTime() // motion interp tick + │ │ └─ MotionInterp.apply_current_movement() + │ │ └─ set_local_velocity from InterpretedState + │ ├─ PartArray.HandleMovement() // completed motion check + │ ├─ PositionManager.UseTime() + │ ├─ DetectionManager.CheckDetection() (server only) + │ └─ TargetManager.HandleTargetting() (server only) + └─ UpdateTime = now + +2. Rendering (acdream-specific, AFTER physics): + └─ For each PhysicsObj: + ├─ PartArray.Sequence.GetCurrAnimFrame() // get current pose + ├─ apply bone transforms + └─ submit to GPU +``` + +### Animation-only entities (static NPCs, breathing creatures) + +Use `update_animation` instead of `update_object`. Same inner loop but: +- No physics integration (no velocity/gravity) +- No collision testing +- No cell change detection + +--- + +## Entity Lifecycle + +### Creation (from CreateObject network message) + +``` +1. Network receives CreateObject (opcode 0xF745) +2. Parse PhysicsDesc → position, state flags, scale, motion table ID +3. Parse WeenieDesc → name, type, weenie class +4. PhysicsObj.Create() + ├─ PartArray.CreateSetup(owner, setupDID) // load Setup dat resource + │ ├─ load GfxObj parts (mesh + textures) + │ ├─ load sphere/cylsphere collision shapes + │ └─ set MotionTableManager (loads MotionTable dat resource) + ├─ set_initial_frame(position.Frame) + ├─ enter_cell(position) // attach to ObjCell + │ ├─ CurCell = cell + │ └─ cell.AddObject(this) + ├─ make_moveable() if not static // allocate MovementManager + │ └─ MovementManager.Init(this, weenieObj) + │ └─ MotionInterp.Init(this, weenieObj) + ├─ set_default_script() if HasDefaultScript + ├─ set_default_animation() if HasDefaultAnimation + └─ StartTimer() // seed UpdateTime +``` + +### Per-frame updates (see above) + +### Motion state change (from UpdateMotion / local input) + +``` +Client input / network: + DoMotion(motion, params) + └─ MovementManager.PerformMovement(RawCommand, motion, params) + └─ MotionInterp.DoMotion(motion, params) + ├─ adjust_motion (speed, hold key) + ├─ DoInterpretedMotion(interpretedMotion, params) + │ ├─ contact_allows_move check + │ ├─ PhysicsObj.DoInterpretedMotion() + │ │ └─ PartArray.DoInterpretedMotion() + │ │ └─ MotionTableManager.GetObjectSequence() + │ │ └─ MotionTable.get_link() → AddAnimationsToSequence() + │ │ └─ Sequence.AppendAnimation(animNode) + │ ├─ add_to_queue(contextID, motion, jump_error) + │ └─ InterpretedState.ApplyMotion(motion, params) + └─ RawState.ApplyMotion(motion, params) +``` + +### Position update (from server UpdatePosition / AutonomousPosition) + +``` +Network: UpdatePosition (0xF748) or AutonomousPosition + → SetPosition(newPos, SetPositionFlags) + ├─ Validate cell ID + ├─ AdjustPosition → find correct ObjCell + ├─ ForceIntoCell or transition() + └─ update_position() + ├─ set all part positions + └─ sync ShadowObjects +``` + +### Cell transition (crossing landblock/cell boundary) + +``` +During transition(): + if transit.SpherePath.CurCell != CurCell: + change_cell(newCell) + ├─ oldCell.RemoveObject(this) + ├─ CurCell = newCell + ├─ newCell.AddObject(this) + └─ (if crossing landblock boundary) + ├─ CurLandblock.remove_entity(this) + └─ newLandblock.add_entity(this) + calc_cross_cells() // update ShadowObjects for multi-cell presence +``` + +### Destruction (from DeleteObject / leave world) + +``` +DestroyObject() + ├─ leave_cell(false) // cell.RemoveObject + clear ShadowObjects + ├─ remove_shadows_from_cells() + ├─ leave_world() // notify landblock + ├─ exit_world() // notify ObjMaint + └─ ObjMaint.DestroyObject() +``` + +--- + +## System Dependency Matrix + +| System | Calls Into | Called By | +|--------|-----------|----------| +| PhysicsEngine | PhysicsObj.update_object / update_animation | Server tick loop | +| PhysicsObj | PartArray, MovementManager, PositionManager, WeenieObject, Transition, ObjCell | PhysicsEngine | +| PartArray | Sequence, MotionTableManager, Setup | PhysicsObj | +| Sequence | AnimSequenceNode (dat AnimData), frame trigger hooks | PartArray, MotionTableManager | +| MotionTableManager | MotionTable, Sequence.AppendAnimation | PartArray, MotionInterp | +| MotionTable | dat loader (DatLoader.FileTypes.MotionTable) | MotionTableManager | +| MotionInterp | PhysicsObj.DoInterpretedMotion, WeenieObject.InqRunRate | MovementManager | +| MovementManager | MotionInterp, MoveToManager | PhysicsObj | +| WeenieObject | WorldObject (game logic layer) | PhysicsObj (callbacks) | +| Transition | ObjCell.FindCollisions, Polygon, Sphere | PhysicsObj.UpdateObjectInternal | +| ObjCell/LandCell | Polygon, LandblockStruct | Transition, PhysicsObj.enter_cell | +| EnvCell | CellStruct (BSP), Portal list, static PhysicsObjs | Transition, Landblock | +| LandblockStruct | Polygon, VertexArray, LandDefs | LandCell, Landblock | +| LandDefs | (math utilities only) | LandblockStruct, Position, ObjCell | +| Position | AFrame | PhysicsObj, ObjCell, Transition | + +--- + +## acdream Implementation Notes + +### What acdream currently has vs. what it needs + +| System | Status | Gap | +|--------|--------|-----| +| Position / AFrame | Complete | None known | +| LandblockStruct | Complete (render side) | Split formula may still use physics formula — verify against AC2D formula | +| Terrain Z sampling | Broken | Needs per-triangle barycentric (not bilinear) using AC2D FSplitNESW formula | +| Sequence | Integrated (AnimationSequencer) | Need to verify boundary handling matches decompiled update_internal | +| MotionTable | Integrated (SetCycle) | Need to verify transition chain building (AddAnimationsToSequence) | +| MotionInterp | Not ported | Need to port DoMotion/DoInterpretedMotion → SetCycle/SetLinkedMotion call chain | +| Transition / Collision | Partially ported (CollisionPrimitives, PhysicsBody) | Not yet wired into per-frame update_object | +| Cell management | LandCell / EnvCell exist | No runtime cell transition logic | +| WeenieObject callbacks | Not ported | Server handles this; client side is game state queries only | +| Physics integration | Not done | Euler integration + gravity missing from client | +| Movement deduplication | Broken | Sends MoveToState every frame instead of on state change | +| AutonomousPosition | Missing | Not sent | +| Sequence counters | Zeroed | Must extract from CreateObject and echo back | + +### Priority for B.3 / next phase + +1. **MotionInterp port** — DoMotion → apply_current_movement → set_local_velocity: this is what drives physics velocity from animation state +2. **Terrain FSplitNESW fix** — swap to AC2D render formula, add per-triangle Z sampling +3. **update_object per-frame loop** — wire Transition into position update (already have CollisionPrimitives) +4. **Cell transition** — change_cell / calc_cross_cells when moving between cells +5. **MoveToState deduplication** — send once per state change, not per frame +6. **AutonomousPosition heartbeat** — every 1 second +7. **Sequence counters** — extract from CreateObject messages