acdream/docs/research/acclient_architecture_map.md
Erik adf626367e 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>
2026-04-13 14:23:50 +02:00

832 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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