docs: comprehensive architecture plan for acdream

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>
This commit is contained in:
Erik 2026-04-13 14:23:50 +02:00
parent a722c29759
commit adf626367e
3 changed files with 1199 additions and 13 deletions

View file

@ -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<uint, ShadowObj> | — | Per-cell presence records |
| CollisionTable | Dict<uint, CollisionRecord> | — | 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\<PhysicsPart\> | 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<AnimSequenceNode> // pending animation queue
FirstCyclic LinkedListNode<AnimSequenceNode> // marks start of looping section
CurrAnim LinkedListNode<AnimSequenceNode> // 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\<MotionNode\> | — |
### 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<style, defaultSubstate>
Cycles Dict<(style<<16)|substate, MotionData>
Modifiers Dict<motionId, MotionData>
Links Dict<style, Dict<substate, MotionData>>
}
```
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<Polygon> // 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<CellPortal>
StaticObjectIDs List<uint>
StaticObjectFrames List<AFrame>
StaticObjects List<PhysicsObj>
VisibleCellIDs List<ushort>
VisibleCells Dict<uint, EnvCell>
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<PhysicsObj> // 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<byte> // 9×9 = 81 height values (0..127 → 0..63.5 meters)
Terrain List<ushort> // 8×8 = 64 terrain type codes (palCode)
VertexArray VertexArray // computed vertices (9×9 = 81 positions)
Polygons List<Polygon> // computed polygons (8×8×2 = 128 triangles)
SWtoNEcut List<bool> // per-cell split direction (64 entries)
LandCells ConcurrentDict<int, ObjCell> // 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