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

35 KiB
Raw Blame History

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 UpdatePositionInternaltransition()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_cellremove_shadows_from_cellsleave_worldexit_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: BuildTempAnimNodeSequence.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):

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