# 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
