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>
35 KiB
AC Client — Complete Architecture Map
Sources used:
docs/research/acclient_function_map.md— 70+ decompiled function addressesdocs/research/acclient_animation_pseudocode.md— full animation system pseudocodedocs/research/2026-04-12-movement-deep-dive.md— movement cross-referencereferences/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
CreateMesh— simple static mesh (no animation MotionTableManager)CreateParticle— particle emitter (special Setup, no skeleton)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
startFrametowardendFrame+1 - Reverse (speedScale < 0): framePosition advances from
endFrame+1-epsilontowardstartFrame - Boundary: when
floor(framePosition) > endFrame(forward) or< startFrame(reverse), node is exhausted;advance_to_next_animationis called with remaining time - Events: at each whole-frame crossing, frame-trigger events fire (
FireApproachEventforward,FireLeaveEventreverse)
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 playspeedScale— playback speed multiplierstartFrame,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 volumeCheckPos— current position being testedCurPos— resolved valid positionCurCell— resolved cellStepDown— flag for step-down terrain contactPlacementAllowsSliding— whether sliding is allowed
CollisionInfo tracks:
ContactPlane— ground/wall planeSlidingNormal— lateral deflection normalCollidedWithEnvironment— hit static geoAddObject(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):
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
- MotionInterp port — DoMotion → apply_current_movement → set_local_velocity: this is what drives physics velocity from animation state
- Terrain FSplitNESW fix — swap to AC2D render formula, add per-triangle Z sampling
- update_object per-frame loop — wire Transition into position update (already have CollisionPrimitives)
- Cell transition — change_cell / calc_cross_cells when moving between cells
- MoveToState deduplication — send once per state change, not per frame
- AutonomousPosition heartbeat — every 1 second
- Sequence counters — extract from CreateObject messages