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