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>
832 lines
35 KiB
Markdown
832 lines
35 KiB
Markdown
# AC Client — Complete Architecture Map
|
||
|
||
**Sources used:**
|
||
- `docs/research/acclient_function_map.md` — 70+ decompiled function addresses
|
||
- `docs/research/acclient_animation_pseudocode.md` — full animation system pseudocode
|
||
- `docs/research/2026-04-12-movement-deep-dive.md` — movement cross-reference
|
||
- `references/ACE/Source/ACE.Server/Physics/` — ACE's C# physics port (read directly)
|
||
|
||
---
|
||
|
||
## System Overview (dependency graph)
|
||
|
||
```
|
||
PhysicsEngine
|
||
└─ iterates PhysicsObj list → update_object()
|
||
|
||
PhysicsObj (root entity)
|
||
├─ Position (ObjCellID + AFrame)
|
||
├─ CurCell (ObjCell ptr)
|
||
├─ CurLandblock (Landblock ptr)
|
||
├─ PartArray (skeleton + animation)
|
||
│ ├─ Setup (geometry: GfxObj parts, spheres, BSP)
|
||
│ ├─ Sequence (animation playback state machine)
|
||
│ └─ MotionTableManager (transition resolver)
|
||
├─ MovementManager
|
||
│ ├─ MotionInterp (raw→interpreted motion state machine)
|
||
│ └─ MoveToManager (pathfinding / move-to target)
|
||
├─ PositionManager (sticky/constraint)
|
||
├─ WeenieObject (game-logic bridge — callbacks into WorldObject)
|
||
├─ Children / Parent (attachment hierarchy)
|
||
└─ ShadowObjects (multi-cell presence)
|
||
|
||
Cell hierarchy (ObjCell subtypes):
|
||
ObjCell (abstract base)
|
||
├─ SortCell (has a BuildingObj for building collision)
|
||
│ └─ LandCell (outdoor cell: 2 terrain polygons, water)
|
||
└─ EnvCell (indoor/dungeon cell: portals, static objs, visibility list)
|
||
|
||
Terrain hierarchy:
|
||
LandblockStruct (raw height/terrain arrays, polygon list, SWtoNEcut flags)
|
||
└─ Landblock (8x8 grid of LandCells, static object list)
|
||
└─ LandCell[64] (per-cell collision polygon pair)
|
||
```
|
||
|
||
---
|
||
|
||
## 1. PhysicsObj — Root Physics Entity
|
||
|
||
**ACE file:** `PhysicsObj.cs`
|
||
**Decompiled:** `chunk_00510000.c`, `chunk_00500000.c` — base at 0x510000
|
||
|
||
### Data owned
|
||
|
||
| Field | Type | Decompiled offset | Purpose |
|
||
|-------|------|-------------------|---------|
|
||
| ID | uint | — | Object identity (GUID) |
|
||
| Position | Position | — | ObjCellID + AFrame (origin + quaternion) |
|
||
| CurCell | ObjCell | — | Current home cell |
|
||
| CurLandblock | Landblock | — | Current landblock |
|
||
| State | PhysicsState | +0xA8 | Bitmask: Static, Hidden, Gravity, Ethereal, HasPhysicsBSP, Missile, Frozen... |
|
||
| TransientState | TransientStateFlags | +0xAC | Contact, OnWalkable, Sliding, Active, CheckEthereal... |
|
||
| Elasticity | float | +0xB0 | Bounce coefficient |
|
||
| UpdateTime | double | +0xD8 | LastUpdateTime for delta accumulation |
|
||
| Velocity | Vector3 | +0xE0/E4/E8 | World-space velocity (m/s) |
|
||
| Acceleration | Vector3 | +0xEC/F0/F4 | World-space acceleration (gravity = -9.8 Z when Gravity flag set) |
|
||
| Omega | Vector3 | +0xF8/FC/100 | Angular velocity |
|
||
| WeenieObject | ptr | +0x12C | Callback bridge into game logic |
|
||
| ContactPlane | Plane | — | Ground contact plane from last collision |
|
||
| SlidingNormal | Vector3 | — | Surface normal during slide |
|
||
| CachedVelocity | Vector3 | — | Velocity between position samples (for rendering) |
|
||
| PartArray | PartArray | — | Skeleton and animation state |
|
||
| MovementManager | MovementManager | — | Owns MotionInterp + MoveToManager |
|
||
| Children / Parent | ChildList / PhysicsObj | — | Attachment tree |
|
||
| ShadowObjects | Dict<uint, ShadowObj> | — | Per-cell presence records |
|
||
| CollisionTable | Dict<uint, CollisionRecord> | — | Active obj-obj collision records |
|
||
| Scale | float | — | Uniform scale factor |
|
||
|
||
### Behavior (key methods)
|
||
|
||
| Method | Address | What it does |
|
||
|--------|---------|--------------|
|
||
| `update_object()` | 0x515020 | Per-frame entry: computes deltaTime, calls `UpdateObjectInternal` in fixed-step loop (MaxQuantum slices) |
|
||
| `UpdateObjectInternal(quantum)` | — | Calls `UpdatePositionInternal` → `transition()` → `SetPositionInternal`. Also calls `PartArray.HandleMovement`, `MovementManager.UseTime`, `PositionManager.UseTime` |
|
||
| `UpdatePositionInternal(quantum)` | 0x513730 | Calls `PartArray.Update` (animation frame advance) → `PositionManager.AdjustOffset` → builds `newFrame` → calls `UpdatePhysicsInternal` |
|
||
| `UpdatePhysicsInternal(quantum)` | 0x5111D0 | Euler: `pos += vel*dt + 0.5*accel*dt²`; applies friction; clamps max velocity (50.0) |
|
||
| `calc_acceleration()` | 0x511420 | Sets gravity (-9.8 Z) when Gravity flag set |
|
||
| `set_velocity()` | 0x511EC0 | Stores velocity, clamps to MaxVelocity |
|
||
| `set_local_velocity()` | 0x511FA0 | Body→world transform then set_velocity |
|
||
| `set_on_walkable()` | 0x511DE0 | Sets/clears OnWalkable transient flag |
|
||
| `report_collision_start()` | 0x511560 | Fires WeenieObj.HandleEnvironmentCollision |
|
||
| `report_collision_end()` | 0x513AC0 | Fires WeenieObj.HandleCollisionEnd |
|
||
| `handle_obj_collision()` | 0x513B60 | Obj-obj dispatch → WeenieObj.HandleCollisionStart or HandleMissileCollision |
|
||
| `handle_collision()` | 0x515280 | Elasticity bounce response |
|
||
| `update_animation()` | — | Animation-only update path (for entities that animate but don't move) |
|
||
| `UpdateAnimationInternal(quantum)` | 0x5111D0 area | `PartArray.Update(quantum)` → `set_frame()` → `PartArray.HandleMovement()` |
|
||
| `DestroyObject()` | — | `leave_cell` → `remove_shadows_from_cells` → `leave_world` → `exit_world` |
|
||
| `DoMotion(motion, params)` | — | Routes through `MovementManager.PerformMovement` |
|
||
| `DoInterpretedMotion(motion, params)` | — | Routes through `PartArray.DoInterpretedMotion` |
|
||
|
||
### Dependencies
|
||
|
||
- Calls INTO: `PartArray`, `MovementManager`, `PositionManager`, `WeenieObject` (vtable callbacks), `ObjCell` (via transition), `Transition`
|
||
- Called BY: `PhysicsEngine.update()` (top-level loop)
|
||
|
||
---
|
||
|
||
## 2. PartArray — Skeleton + Animation Frame State
|
||
|
||
**ACE file:** `PartArray.cs`
|
||
**Decompiled:** (embedded within CPhysicsObj chunk)
|
||
|
||
### Data owned
|
||
|
||
| Field | Type | Purpose |
|
||
|-------|------|---------|
|
||
| State | uint | Bitmask (HasPhysicsBSP = 0x10000) |
|
||
| Owner | PhysicsObj | Back-pointer |
|
||
| Sequence | Sequence | Animation playback state machine |
|
||
| MotionTableManager | MotionTableManager | Transition resolver (wraps MotionTable) |
|
||
| Setup | Setup | Dat resource: GfxObj parts, sphere collision shapes, BSP |
|
||
| NumParts | int | Part count |
|
||
| Parts | List\<PhysicsPart\> | Individual mesh parts (each has a Position) |
|
||
| Scale | Vector3 | Part-level scale |
|
||
| LastAnimFrame | AnimationFrame | Most recently computed pose |
|
||
|
||
### Behavior
|
||
|
||
| Method | What it does |
|
||
|--------|--------------|
|
||
| `CreateMesh(owner, setupDID)` | Factory: loads Setup from dat, creates parts, sets placement frame 0x65 |
|
||
| `CreateSetup(owner, setupDID, createParts)` | Full creature setup factory |
|
||
| `Update(quantum, ref offsetFrame)` | Calls `Sequence.Update(quantum, ref offsetFrame)` — advances animation, accumulates velocity |
|
||
| `SetFrame(frame)` | Applies AFrame to all Parts |
|
||
| `HandleMovement()` | Calls `MotionTableManager.CheckForCompletedMotions()` |
|
||
| `DoInterpretedMotion(motion, params)` | Routes into `MotionTableManager.DoInterpretedMotion` |
|
||
| `AddPartsShadow(cell, count)` | Registers all parts into a shadow cell (multi-cell presence) |
|
||
| `AnimationDone(success)` | Notifies MotionTableManager that a one-shot completed |
|
||
|
||
### Three PartArray creation factories
|
||
|
||
1. `CreateMesh` — simple static mesh (no animation MotionTableManager)
|
||
2. `CreateParticle` — particle emitter (special Setup, no skeleton)
|
||
3. `CreateSetup` — full creature (has MotionTableManager + Sequence)
|
||
|
||
---
|
||
|
||
## 3. Sequence — Animation Playback State Machine
|
||
|
||
**ACE file:** `Animation/Sequence.cs`
|
||
**Decompiled struct:** `chunk_00520000.c` (FUN_00526110, FUN_005261D0, FUN_00525EB0)
|
||
|
||
### Data owned
|
||
|
||
```
|
||
Sequence {
|
||
AnimList LinkedList<AnimSequenceNode> // pending animation queue
|
||
FirstCyclic LinkedListNode<AnimSequenceNode> // marks start of looping section
|
||
CurrAnim LinkedListNode<AnimSequenceNode> // currently playing node
|
||
Velocity Vector3 // accumulated linear velocity from animation
|
||
Omega Vector3 // accumulated angular velocity
|
||
HookObj PhysicsObj // entity to fire events on
|
||
FrameNumber float // current fractional frame position
|
||
PlacementFrame AnimationFrame // static pose when no animation active
|
||
PlacementFrameID int
|
||
IsTrivial bool // optimization: skip update if idle
|
||
}
|
||
```
|
||
|
||
Decompiled layout (raw offsets in AnimNode / Sequence structs):
|
||
```
|
||
Sequence +0x04 = listHead (AnimNode* front)
|
||
Sequence +0x08 = listTail (AnimNode* back)
|
||
Sequence +0x0C = current (currently playing ptr)
|
||
Sequence +0x10..0x24 = linearVelocity + angularVelocity (3+3 floats)
|
||
Sequence +0x28 = hasCallback
|
||
Sequence +0x30 = framePosition (double)
|
||
Sequence +0x38 = activeNode
|
||
Sequence +0x3C = overrideFrame
|
||
```
|
||
|
||
### AnimSequenceNode (ACE) / AnimNode (decompiled)
|
||
|
||
```
|
||
AnimNode (28 bytes = 0x1C) {
|
||
vtable* // AnimData vtable
|
||
next* // forward link in doubly-linked list
|
||
prev* // backward link
|
||
frameCount // total frames in this animation
|
||
speedScale // playback speed multiplier (negative = reverse)
|
||
startFrame // inclusive start (swap with endFrame when speedScale < 0)
|
||
endFrame // inclusive end
|
||
}
|
||
```
|
||
|
||
### Behavior (key methods)
|
||
|
||
| Method | Address | What it does |
|
||
|--------|---------|--------------|
|
||
| `Update(quantum, ref offsetFrame)` | — | Calls `update_internal(frameRate, ...)` |
|
||
| `update_internal(frameRate, ...)` | 0x5261D0 | Core per-frame loop: advance `framePosition` by `rate*dt`; fire frame-trigger events; call `advance_to_next_animation` on boundary |
|
||
| `advance_to_next_animation(...)` | 0x525EB0 | Pops exhausted node; loads next; seeds new `framePosition` from GetStartFramePosition |
|
||
| `AppendAnimation(node)` | 0x526110 | Push AnimNode onto pending list tail |
|
||
| `GetCurrAnimFrame()` | — | Returns `CurrAnim.get_part_frame(frameNumber)` or `PlacementFrame` |
|
||
| `clear_animations()` | — | Empties AnimList, resets CurrAnim |
|
||
| `clear_physics()` | — | Zeros Velocity + Omega |
|
||
| `is_first_cyclic()` | — | True if at or before the FirstCyclic marker (idle check) |
|
||
| `CombinePhysics(vel, omega)` | — | Adds to Velocity + Omega accumulators |
|
||
|
||
### Frame position arithmetic
|
||
|
||
- **Forward** (speedScale > 0): framePosition advances from `startFrame` toward `endFrame+1`
|
||
- **Reverse** (speedScale < 0): framePosition advances from `endFrame+1-epsilon` toward `startFrame`
|
||
- **Boundary**: when `floor(framePosition) > endFrame` (forward) or `< startFrame` (reverse), node is exhausted; `advance_to_next_animation` is called with remaining time
|
||
- **Events**: at each whole-frame crossing, frame-trigger events fire (`FireApproachEvent` forward, `FireLeaveEvent` reverse)
|
||
|
||
---
|
||
|
||
## 4. MotionInterp — Motion State Machine
|
||
|
||
**ACE file:** `Animation/MotionInterp.cs`
|
||
**Decompiled:** `chunk_00520000.c` (FUN_00528xxx, FUN_005293xx)
|
||
|
||
### Data owned
|
||
|
||
| Field | Type | Decompiled offset |
|
||
|-------|------|-------------------|
|
||
| WeenieObj | WeenieObject | +0x04 |
|
||
| PhysicsObj | PhysicsObj | +0x08 |
|
||
| RawState | RawMotionState | +0x14 — wire format: ForwardCmd, SidestepCmd, TurnCmd, speeds, HoldKey |
|
||
| InterpretedState | InterpretedMotionState | +0x44 — resolved state after interpretation |
|
||
| CurrentSpeedFactor | float | — |
|
||
| StandingLongJump | bool | +0x70 |
|
||
| JumpExtent | float | +0x74 |
|
||
| MyRunRate | float | +0x7C |
|
||
| PendingMotions | LinkedList\<MotionNode\> | — |
|
||
|
||
### Constants
|
||
|
||
```
|
||
BackwardsFactor = 0.65
|
||
MaxSidestepRate = 3.0
|
||
RunAnimSpeed = 4.0
|
||
RunTurnFactor = 1.5
|
||
SidestepAnimSpeed = 1.25
|
||
SidestepFactor = 0.5
|
||
WalkAnimSpeed = 3.12
|
||
```
|
||
|
||
### Behavior
|
||
|
||
| Method | Address | What it does |
|
||
|--------|---------|--------------|
|
||
| `PerformMovement(mvs)` | 0x529A90 | Top-level dispatch (switch on MovementType 1-5) |
|
||
| `DoMotion(motion, params)` | 0x529930 | Raw motion command → adjust_motion → DoInterpretedMotion |
|
||
| `DoInterpretedMotion(motion, params)` | 0x528F70 | Core: contact_allows_move check → PhysicsObj.DoInterpretedMotion → add_to_queue → InterpretedState.ApplyMotion |
|
||
| `StopInterpretedMotion(motion, params)` | 0x529080 | Stop specific interpreted motion |
|
||
| `StopMotion(motion, params)` | 0x529140 | Stop specific raw motion |
|
||
| `StopCompletely()` | 0x528A50 | Reset to Ready/idle, clear all pending |
|
||
| `adjust_motion(motion, speed, holdKey)` | 0x5287F0 | Apply speed adjustments for run/walk/strafe |
|
||
| `apply_raw_movement()` | 0x5293F0 | Convert RawMotionState → InterpretedMotionState |
|
||
| `apply_current_movement()` | 0x529210 | Set physics velocity from InterpretedState |
|
||
| `get_state_velocity(motion)` | 0x528960 | Compute velocity for current motion (calls WeenieObj.InqRunRate) |
|
||
| `contact_allows_move(motion)` | 0x528DD0 | Slope angle check: can we move in this direction given terrain normal? |
|
||
| `jump(params)` | 0x529390 | Initiate jump sequence |
|
||
| `jump_is_allowed(extent)` | 0x528EC0 | Full jump permission check |
|
||
| `get_leave_ground_velocity()` | 0x528CD0 | Compute 3D launch vector |
|
||
| `LeaveGround()` | 0x529710 | Called when becoming airborne; resets jump state |
|
||
| `HitGround()` | 0x5296D0 | Landing handler; fires WeenieObj landing callbacks |
|
||
|
||
### Motion command bit flags (from PerformMovement — 0x523400)
|
||
|
||
```
|
||
Bit 31 set (int < 0): STANCE CHANGE — transition to new stance/style
|
||
Bit 30 set (0x40000000): CYCLE MOTION — looping (Walk, Run, TurnLeft...)
|
||
Bit 28 set (0x10000000): LINKED MOTION — one-shot that links back to cycle
|
||
Bit 29 set (0x20000000): SUBSTATE ANIM — play a substate override
|
||
```
|
||
|
||
---
|
||
|
||
## 5. MotionTable / MotionTableManager — Animation Data + Transition Resolution
|
||
|
||
**ACE files:** `Animation/MotionTable.cs`, `Animation/MotionTableManager` (implicit in PartArray)
|
||
**Decompiled:** `chunk_00520000.c` (FUN_00521770, FUN_005231D0, FUN_005232B0, FUN_00523000, FUN_00523400)
|
||
|
||
### MotionTable data (loaded from dat resource 0x09xxxxxx)
|
||
|
||
```
|
||
MotionTable {
|
||
ID uint
|
||
DefaultStyle uint // default stance (e.g. NonCombat)
|
||
StyleDefaults Dict<style, defaultSubstate>
|
||
Cycles Dict<(style<<16)|substate, MotionData>
|
||
Modifiers Dict<motionId, MotionData>
|
||
Links Dict<style, Dict<substate, MotionData>>
|
||
}
|
||
```
|
||
|
||
Each `MotionData` (= `TransitionLink` in decompiled) contains:
|
||
- `animId` — dat animation resource to play
|
||
- `speedScale` — playback speed multiplier
|
||
- `startFrame`, `endFrame`, `frameCount`
|
||
- Linear/angular velocity of the animation
|
||
- Chain of AnimNode entries (for multi-step transitions)
|
||
|
||
### Transition lookup — `FindBestTransition` (0x5232B0)
|
||
|
||
```
|
||
1. Direct lookup: (fromStyle<<16 | substate) → (toStyle or substate)
|
||
2. Speed-sensitive variant: same but uses exact speed matching
|
||
3. Fallback via defaultMotionId
|
||
4. Fallback via defaultTransMap
|
||
```
|
||
|
||
The composite hash key encodes both states in a single uint, looked up via `LookupTransitionNode` (0x5231D0).
|
||
|
||
### PerformMovement dispatch — `FUN_00523400`
|
||
|
||
```
|
||
1. Look up current animation ID for current state
|
||
2. Guard: already in this cycle? Early-out.
|
||
3. Branch on motion command type (stance/cycle/linked/substate):
|
||
STANCE CHANGE:
|
||
a. Find exit-substate transition → AddAnimationsToSequence
|
||
b. Find enter-new-stance transition → AddAnimationsToSequence
|
||
c. Update currentState
|
||
CYCLE MOTION:
|
||
a. Find exit-current-cycle transition (one-shot)
|
||
b. Find enter-new-cycle transition (sets FirstCyclic marker)
|
||
c. AddAnimationsToSequence for both
|
||
LINKED MOTION:
|
||
a. Find link animation
|
||
b. Append after current FirstCyclic marker
|
||
SUBSTATE ANIM:
|
||
a. Find substate animation
|
||
b. Append with override flag
|
||
```
|
||
|
||
`AddAnimationsToSequence` (0x523000):
|
||
- Applies linear/angular velocity from the TransitionLink to the Sequence
|
||
- Iterates the chain of AnimNode entries
|
||
- For each: `BuildTempAnimNode` → `Sequence.AppendAnimation`
|
||
|
||
---
|
||
|
||
## 6. Transition — Collision / Movement Resolution
|
||
|
||
**ACE file:** `Animation/Transition.cs` (note: namespace is Animation not Collision)
|
||
**Decompiled:** `chunk_00530000.c` (FUN_005384E0, FUN_005387C0, FUN_00538180)
|
||
|
||
### Data owned
|
||
|
||
```
|
||
Transition {
|
||
ObjectInfo ObjectInfo // what's moving (state flags: IsPlayer, IsViewer, Ethereal...)
|
||
SpherePath SpherePath // the swept sphere path from old→new position
|
||
CollisionInfo CollisionInfo // accumulates contact plane, sliding normal, hit objects
|
||
CellArray CellArray // cells the sphere path passes through
|
||
}
|
||
```
|
||
|
||
`SpherePath` tracks:
|
||
- `GlobalSphere[]` — world-space sphere(s) representing the object's collision volume
|
||
- `CheckPos` — current position being tested
|
||
- `CurPos` — resolved valid position
|
||
- `CurCell` — resolved cell
|
||
- `StepDown` — flag for step-down terrain contact
|
||
- `PlacementAllowsSliding` — whether sliding is allowed
|
||
|
||
`CollisionInfo` tracks:
|
||
- `ContactPlane` — ground/wall plane
|
||
- `SlidingNormal` — lateral deflection normal
|
||
- `CollidedWithEnvironment` — hit static geo
|
||
- `AddObject(obj, state)` — record object collision
|
||
|
||
### Key collision methods
|
||
|
||
| Method | Address | What it does |
|
||
|--------|---------|--------------|
|
||
| `FindValidPosition()` | — | Main loop: sweep sphere through cells, test polygon collision |
|
||
| `find_collisions()` | 0x5387C0 | Iterates CellArray, calls cell.FindCollisions on each |
|
||
| `collide_with_point()` | 0x538180 | Point collision handler |
|
||
| `AdjustOffset(offset)` | — | Projects movement vector along contact/sliding planes |
|
||
| `Sphere.slide_sphere()` | 0x538EB0 | Slide along a surface |
|
||
| `Sphere.land_on_sphere()` | 0x538F50 | Step down to surface |
|
||
| `Polygon.sphere_intersects_poly()` | 0x539500 | Sphere-polygon contact test |
|
||
| `Polygon.find_walkable_collision()` | 0x53A040 | Returns edge normal for walkable surface |
|
||
| `Polygon.find_time_of_collision()` | 0x539BA0/0x539DF0 | Ray-plane-polygon t (sphere/cylinder variants) |
|
||
|
||
### Transition outcomes
|
||
|
||
```
|
||
TransitionState {
|
||
Invalid = 0 // bad input
|
||
OK = 1 // no collision / passthrough
|
||
Collided = 2 // hit solid surface
|
||
Adjusted = 3 // position corrected (slide/step)
|
||
Slid = 4 // sliding along surface
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. LandCell / EnvCell / SortCell — Cell Management
|
||
|
||
### Class hierarchy
|
||
|
||
```
|
||
ObjCell (abstract base)
|
||
ID, WaterType, Pos, ObjectList, ShadowObjectList
|
||
ClipPlanes, VisibleCells, VoyeurTable
|
||
→ AddObject / RemoveObject
|
||
→ abstract FindCollisions(Transition)
|
||
→ abstract find_transit_cells(...)
|
||
|
||
SortCell : ObjCell
|
||
Building (BuildingObj — handles building collision + transit cell lookup)
|
||
→ FindCollisions: delegates to Building.find_building_collisions
|
||
|
||
LandCell : SortCell
|
||
Polygons List<Polygon> // always exactly 2 (the two terrain triangles)
|
||
InView bool
|
||
→ FindCollisions: FindEnvCollisions (terrain polygon) → base (building) → FindObjCollisions
|
||
→ FindEnvCollisions: find_terrain_poly → ValidateWalkable
|
||
|
||
EnvCell : ObjCell
|
||
CellStructure CellStruct // geometry from Environment dat
|
||
Portals List<CellPortal>
|
||
StaticObjectIDs List<uint>
|
||
StaticObjectFrames List<AFrame>
|
||
StaticObjects List<PhysicsObj>
|
||
VisibleCellIDs List<ushort>
|
||
VisibleCells Dict<uint, EnvCell>
|
||
Flags EnvCellFlags
|
||
SeenOutside bool
|
||
→ PostInit: build_visible_cells + init_static_objects
|
||
→ FindCollisions: CellStruct BSP collision
|
||
```
|
||
|
||
### ObjCell key fields
|
||
|
||
```
|
||
ID uint // cell ID: high 16 = landblock (0xXXYY), low 16 = cell index
|
||
// outdoor: 0x0001..0x0040 (64 outdoor cells per LB)
|
||
// indoor: 0x0100..0xFFFD (EnvCell IDs)
|
||
Pos Position // cell's own origin frame
|
||
ObjectList List<PhysicsObj> // entities currently in this cell
|
||
```
|
||
|
||
### Cell ID conventions
|
||
|
||
```
|
||
outdoor cell: (blockX << 24) | (blockY << 16) | cellIndex where cellIndex = 0x0001..0x0040
|
||
indoor cell: (blockX << 24) | (blockY << 16) | cellIndex where cellIndex = 0x0100..0xFFFD
|
||
special: 0xFFFF = "lost" / no cell
|
||
```
|
||
|
||
---
|
||
|
||
## 8. CLandBlock / CLandBlockStruct — Terrain
|
||
|
||
**ACE file:** `Common/LandblockStruct.cs`, `Common/Landblock.cs`
|
||
**Decompiled:** `chunk_00530000.c` (0x530690, 0x531780, 0x531D10, 0x532A50, 0x532EB0, 0x532D10)
|
||
|
||
### LandblockStruct (raw terrain data)
|
||
|
||
```
|
||
LandblockStruct {
|
||
ID uint
|
||
TransDir LandDefs.Direction
|
||
SideVertexCount, SidePolyCount, SideCellCount int
|
||
WaterType LandDefs.WaterType
|
||
Height List<byte> // 9×9 = 81 height values (0..127 → 0..63.5 meters)
|
||
Terrain List<ushort> // 8×8 = 64 terrain type codes (palCode)
|
||
VertexArray VertexArray // computed vertices (9×9 = 81 positions)
|
||
Polygons List<Polygon> // computed polygons (8×8×2 = 128 triangles)
|
||
SWtoNEcut List<bool> // per-cell split direction (64 entries)
|
||
LandCells ConcurrentDict<int, ObjCell> // 64 outdoor cells
|
||
}
|
||
```
|
||
|
||
**Key algorithms (decompiled addresses):**
|
||
|
||
| Method | Address | What it does |
|
||
|--------|---------|--------------|
|
||
| `IsSWtoNECut(x, y)` | 0x531D10 | Inner split test with 0xCCAC033 constants |
|
||
| `ConstructPolygons()` | 0x532A50 | Outer 8×8 loop using FSplitNESW formula |
|
||
| `GetCellRotation / ConstructUVs` | 0x532EB0 | PalCode computation for UV rotation |
|
||
| `unpack()` | 0x532D10 | Deserialize from dat stream |
|
||
| `get_packed_size()` | 0x531F10 | Returns 0xF4 (244 bytes) |
|
||
| `AdjustPlanes()` | 0x532440 | Normal accumulation + lighting |
|
||
| `CalcCellWater()` | 0x532290 | Water depth check |
|
||
|
||
**CRITICAL — terrain split formula:**
|
||
|
||
The CORRECT render formula (from AC2D, confirmed by ACME `ClientReference.cs`):
|
||
```cpp
|
||
bool FSplitNESW(DWORD x, DWORD y) {
|
||
DWORD dw = x * y * 0x0CCAC033 - x * 0x421BE3BD + y * 0x6C1AC587 - 0x519B8F25;
|
||
return (dw & 0x80000000) != 0;
|
||
}
|
||
// x = blockX * 8 + cellX (GLOBAL cell coordinates)
|
||
// y = blockY * 8 + cellY
|
||
// true = NE/SW split (SW corner → NE corner diagonal)
|
||
```
|
||
|
||
### Landblock (manager)
|
||
|
||
```
|
||
Landblock {
|
||
// manages the 8×8 grid of outdoor LandCells
|
||
// static object list (from LandBlockInfo dat)
|
||
// links to neighbor landblocks for cross-boundary movement
|
||
}
|
||
```
|
||
|
||
**Key methods (decompiled):**
|
||
|
||
| Method | Address | What it does |
|
||
|--------|---------|--------------|
|
||
| `Init / constructor` | 0x530690 | Initialize landblock fields |
|
||
| `release_all()` | 0x5307E0 | Free all resources |
|
||
| `init_static_objs()` | 0x531780 | Load static objects from LandBlockInfo dat |
|
||
| `release_visible_cells()` | 0x531000 | Free cell data |
|
||
| `grab_visible_cells()` | 0x5301E0 | BFS neighbor expansion |
|
||
| `add_server_object()` | 0x530650 | Add entity to landblock |
|
||
|
||
---
|
||
|
||
## 9. Position / ObjCell — Coordinate System
|
||
|
||
**ACE file:** `Common/Position.cs`
|
||
|
||
### Position
|
||
|
||
```
|
||
Position {
|
||
ObjCellID uint // which cell this position is in
|
||
Frame AFrame // origin (Vector3) + orientation (Quaternion)
|
||
}
|
||
```
|
||
|
||
### AFrame
|
||
|
||
```
|
||
AFrame {
|
||
Origin Vector3 // landblock-local XYZ (meters within landblock = 192×192m area)
|
||
Orientation Quaternion // WXYZ quaternion
|
||
|
||
LocalToGlobal(pt) // rotate point by orientation, add origin
|
||
GlobalToLocal(pt) // inverse: subtract origin, inverse-rotate
|
||
Combine(a, b) // concatenate two frames
|
||
GRotate(omega*dt) // apply angular velocity
|
||
}
|
||
```
|
||
|
||
### Coordinate conventions
|
||
|
||
```
|
||
Landblock size: 192 meters × 192 meters (8×8 cells, each 24m × 24m)
|
||
Height range: 0..127 height units → 0..63.5 meters (scale × 0.5)
|
||
Cell terrain polygon: each cell has 2 triangles (SW or NE split)
|
||
Global origin: (0,0) = SW corner of landblock grid
|
||
AC heading: 0 = facing west (-X), PI/2 = north (+Y), PI = east (+X)
|
||
Gravity: Z-down, gravity = -9.8 m/s²
|
||
```
|
||
|
||
### LandDefs (coordinate utilities) (0x5AAA30 area)
|
||
|
||
| Method | Address | What it does |
|
||
|--------|---------|--------------|
|
||
| `get_vars()` | 0x5AAA30 | Set 8 coordinate constants |
|
||
| `get_outside_lcoord()` | 0x5AABB0 | Cell ID → world coord |
|
||
| `AdjustToOutside()` | 0x5AAC70 | Normalize position to outdoor coords |
|
||
| `get_block_dir()` | 0x5AAB50 | Quadrant → Direction enum |
|
||
|
||
---
|
||
|
||
## 10. WeenieObject — Game Logic Bridge
|
||
|
||
**ACE file:** `Common/WeenieObject.cs`
|
||
|
||
### Data owned
|
||
|
||
```
|
||
WeenieObject {
|
||
ID uint
|
||
UpdateTime double
|
||
WorldObjectInfo WorldObjectInfo // weak reference to WorldObject
|
||
IsMonster bool
|
||
IsCombatPet bool
|
||
Faction1Bits FactionBits
|
||
PlayerKillerStatus PlayerKillerStatus
|
||
}
|
||
```
|
||
|
||
### Vtable callbacks (called FROM physics INTO game logic)
|
||
|
||
| Vtable offset | ACE method | Called from physics when... |
|
||
|--------------|-----------|---------------------------|
|
||
| +0x30 | `InqJumpVelocity(extent)` | Computing jump Z velocity |
|
||
| +0x34 | `InqRunRate()` | Computing movement speed for entity |
|
||
| +0x3C | `CanJump(extent)` | Checking if jump is permitted |
|
||
| +0x4C | `HandleCollisionEnd(obj)` | Object collision ended |
|
||
| +0x50 | `HandleCollisionStart(obj)` | Object collision started |
|
||
| +0x54 | `HandleMissileCollision(obj)` | Missile hit entity |
|
||
| +0x58 | `HandleEnvironmentCollision(normal)` | Hit static geometry |
|
||
|
||
### Key predicates
|
||
|
||
```
|
||
IsPlayer() — ID in 0x50000001..0x5FFFFFFF
|
||
IsCreature() — has creature WorldObject
|
||
IsMonster — creature and not combat pet
|
||
IsPK() — PlayerKillerStatus == PK
|
||
```
|
||
|
||
---
|
||
|
||
## Per-Frame Update Order
|
||
|
||
### Client entity update (static/NPC — `update_animation` path)
|
||
|
||
```
|
||
1. PhysicsEngine.update()
|
||
└─ for each active PhysicsObj:
|
||
└─ update_animation()
|
||
├─ compute deltaTime = now - UpdateTime
|
||
├─ while deltaTime > MaxQuantum (0.034s):
|
||
│ └─ UpdateAnimationInternal(MaxQuantum)
|
||
│ ├─ PartArray.Update(quantum, ref newFrame)
|
||
│ │ └─ Sequence.update_internal(frameRate, ...)
|
||
│ │ ├─ advance framePosition by rate*dt
|
||
│ │ ├─ fire frame-trigger events
|
||
│ │ └─ advance_to_next_animation on boundary
|
||
│ ├─ set_frame(newFrame) // update all parts' positions
|
||
│ └─ PartArray.HandleMovement() // check completed motions
|
||
└─ UpdateTime = now
|
||
```
|
||
|
||
### Moving entity update (player — `update_object` path)
|
||
|
||
```
|
||
1. PhysicsEngine.update()
|
||
└─ for each active PhysicsObj:
|
||
└─ update_object()
|
||
├─ compute deltaTime
|
||
├─ while deltaTime > MaxQuantum:
|
||
│ └─ UpdateObjectInternal(quantum)
|
||
│ ├─ UpdatePositionInternal(quantum, ref newFrame)
|
||
│ │ ├─ PartArray.Update(quantum, ref offsetFrame) // animation → root motion
|
||
│ │ ├─ PositionManager.AdjustOffset(offsetFrame) // sticky/constraint
|
||
│ │ ├─ build newFrame = Combine(Position.Frame, offsetFrame)
|
||
│ │ └─ UpdatePhysicsInternal(quantum, ref newFrame) // Euler integration
|
||
│ │ ├─ pos += vel*dt + 0.5*accel*dt²
|
||
│ │ ├─ calc_friction
|
||
│ │ └─ vel += accel*dt
|
||
│ ├─ transition(oldPos, newPos) // collision resolve
|
||
│ │ ├─ build SpherePath (swept sphere)
|
||
│ │ ├─ find_transit_cells → CellArray
|
||
│ │ ├─ for each cell: cell.FindCollisions(transition)
|
||
│ │ │ ├─ LandCell: terrain polygon test → ValidateWalkable
|
||
│ │ │ ├─ SortCell/BuildingObj: building BSP test
|
||
│ │ │ └─ ObjCell: obj-obj collision
|
||
│ │ └─ AdjustOffset → clamp to contact/sliding planes
|
||
│ ├─ SetPositionInternal(transit) // apply resolved pos
|
||
│ │ ├─ change_cell if needed
|
||
│ │ └─ update ShadowObjects
|
||
│ ├─ MovementManager.UseTime() // motion interp tick
|
||
│ │ └─ MotionInterp.apply_current_movement()
|
||
│ │ └─ set_local_velocity from InterpretedState
|
||
│ ├─ PartArray.HandleMovement() // completed motion check
|
||
│ ├─ PositionManager.UseTime()
|
||
│ ├─ DetectionManager.CheckDetection() (server only)
|
||
│ └─ TargetManager.HandleTargetting() (server only)
|
||
└─ UpdateTime = now
|
||
|
||
2. Rendering (acdream-specific, AFTER physics):
|
||
└─ For each PhysicsObj:
|
||
├─ PartArray.Sequence.GetCurrAnimFrame() // get current pose
|
||
├─ apply bone transforms
|
||
└─ submit to GPU
|
||
```
|
||
|
||
### Animation-only entities (static NPCs, breathing creatures)
|
||
|
||
Use `update_animation` instead of `update_object`. Same inner loop but:
|
||
- No physics integration (no velocity/gravity)
|
||
- No collision testing
|
||
- No cell change detection
|
||
|
||
---
|
||
|
||
## Entity Lifecycle
|
||
|
||
### Creation (from CreateObject network message)
|
||
|
||
```
|
||
1. Network receives CreateObject (opcode 0xF745)
|
||
2. Parse PhysicsDesc → position, state flags, scale, motion table ID
|
||
3. Parse WeenieDesc → name, type, weenie class
|
||
4. PhysicsObj.Create()
|
||
├─ PartArray.CreateSetup(owner, setupDID) // load Setup dat resource
|
||
│ ├─ load GfxObj parts (mesh + textures)
|
||
│ ├─ load sphere/cylsphere collision shapes
|
||
│ └─ set MotionTableManager (loads MotionTable dat resource)
|
||
├─ set_initial_frame(position.Frame)
|
||
├─ enter_cell(position) // attach to ObjCell
|
||
│ ├─ CurCell = cell
|
||
│ └─ cell.AddObject(this)
|
||
├─ make_moveable() if not static // allocate MovementManager
|
||
│ └─ MovementManager.Init(this, weenieObj)
|
||
│ └─ MotionInterp.Init(this, weenieObj)
|
||
├─ set_default_script() if HasDefaultScript
|
||
├─ set_default_animation() if HasDefaultAnimation
|
||
└─ StartTimer() // seed UpdateTime
|
||
```
|
||
|
||
### Per-frame updates (see above)
|
||
|
||
### Motion state change (from UpdateMotion / local input)
|
||
|
||
```
|
||
Client input / network:
|
||
DoMotion(motion, params)
|
||
└─ MovementManager.PerformMovement(RawCommand, motion, params)
|
||
└─ MotionInterp.DoMotion(motion, params)
|
||
├─ adjust_motion (speed, hold key)
|
||
├─ DoInterpretedMotion(interpretedMotion, params)
|
||
│ ├─ contact_allows_move check
|
||
│ ├─ PhysicsObj.DoInterpretedMotion()
|
||
│ │ └─ PartArray.DoInterpretedMotion()
|
||
│ │ └─ MotionTableManager.GetObjectSequence()
|
||
│ │ └─ MotionTable.get_link() → AddAnimationsToSequence()
|
||
│ │ └─ Sequence.AppendAnimation(animNode)
|
||
│ ├─ add_to_queue(contextID, motion, jump_error)
|
||
│ └─ InterpretedState.ApplyMotion(motion, params)
|
||
└─ RawState.ApplyMotion(motion, params)
|
||
```
|
||
|
||
### Position update (from server UpdatePosition / AutonomousPosition)
|
||
|
||
```
|
||
Network: UpdatePosition (0xF748) or AutonomousPosition
|
||
→ SetPosition(newPos, SetPositionFlags)
|
||
├─ Validate cell ID
|
||
├─ AdjustPosition → find correct ObjCell
|
||
├─ ForceIntoCell or transition()
|
||
└─ update_position()
|
||
├─ set all part positions
|
||
└─ sync ShadowObjects
|
||
```
|
||
|
||
### Cell transition (crossing landblock/cell boundary)
|
||
|
||
```
|
||
During transition():
|
||
if transit.SpherePath.CurCell != CurCell:
|
||
change_cell(newCell)
|
||
├─ oldCell.RemoveObject(this)
|
||
├─ CurCell = newCell
|
||
├─ newCell.AddObject(this)
|
||
└─ (if crossing landblock boundary)
|
||
├─ CurLandblock.remove_entity(this)
|
||
└─ newLandblock.add_entity(this)
|
||
calc_cross_cells() // update ShadowObjects for multi-cell presence
|
||
```
|
||
|
||
### Destruction (from DeleteObject / leave world)
|
||
|
||
```
|
||
DestroyObject()
|
||
├─ leave_cell(false) // cell.RemoveObject + clear ShadowObjects
|
||
├─ remove_shadows_from_cells()
|
||
├─ leave_world() // notify landblock
|
||
├─ exit_world() // notify ObjMaint
|
||
└─ ObjMaint.DestroyObject()
|
||
```
|
||
|
||
---
|
||
|
||
## System Dependency Matrix
|
||
|
||
| System | Calls Into | Called By |
|
||
|--------|-----------|----------|
|
||
| PhysicsEngine | PhysicsObj.update_object / update_animation | Server tick loop |
|
||
| PhysicsObj | PartArray, MovementManager, PositionManager, WeenieObject, Transition, ObjCell | PhysicsEngine |
|
||
| PartArray | Sequence, MotionTableManager, Setup | PhysicsObj |
|
||
| Sequence | AnimSequenceNode (dat AnimData), frame trigger hooks | PartArray, MotionTableManager |
|
||
| MotionTableManager | MotionTable, Sequence.AppendAnimation | PartArray, MotionInterp |
|
||
| MotionTable | dat loader (DatLoader.FileTypes.MotionTable) | MotionTableManager |
|
||
| MotionInterp | PhysicsObj.DoInterpretedMotion, WeenieObject.InqRunRate | MovementManager |
|
||
| MovementManager | MotionInterp, MoveToManager | PhysicsObj |
|
||
| WeenieObject | WorldObject (game logic layer) | PhysicsObj (callbacks) |
|
||
| Transition | ObjCell.FindCollisions, Polygon, Sphere | PhysicsObj.UpdateObjectInternal |
|
||
| ObjCell/LandCell | Polygon, LandblockStruct | Transition, PhysicsObj.enter_cell |
|
||
| EnvCell | CellStruct (BSP), Portal list, static PhysicsObjs | Transition, Landblock |
|
||
| LandblockStruct | Polygon, VertexArray, LandDefs | LandCell, Landblock |
|
||
| LandDefs | (math utilities only) | LandblockStruct, Position, ObjCell |
|
||
| Position | AFrame | PhysicsObj, ObjCell, Transition |
|
||
|
||
---
|
||
|
||
## acdream Implementation Notes
|
||
|
||
### What acdream currently has vs. what it needs
|
||
|
||
| System | Status | Gap |
|
||
|--------|--------|-----|
|
||
| Position / AFrame | Complete | None known |
|
||
| LandblockStruct | Complete (render side) | Split formula may still use physics formula — verify against AC2D formula |
|
||
| Terrain Z sampling | Broken | Needs per-triangle barycentric (not bilinear) using AC2D FSplitNESW formula |
|
||
| Sequence | Integrated (AnimationSequencer) | Need to verify boundary handling matches decompiled update_internal |
|
||
| MotionTable | Integrated (SetCycle) | Need to verify transition chain building (AddAnimationsToSequence) |
|
||
| MotionInterp | Not ported | Need to port DoMotion/DoInterpretedMotion → SetCycle/SetLinkedMotion call chain |
|
||
| Transition / Collision | Partially ported (CollisionPrimitives, PhysicsBody) | Not yet wired into per-frame update_object |
|
||
| Cell management | LandCell / EnvCell exist | No runtime cell transition logic |
|
||
| WeenieObject callbacks | Not ported | Server handles this; client side is game state queries only |
|
||
| Physics integration | Not done | Euler integration + gravity missing from client |
|
||
| Movement deduplication | Broken | Sends MoveToState every frame instead of on state change |
|
||
| AutonomousPosition | Missing | Not sent |
|
||
| Sequence counters | Zeroed | Must extract from CreateObject and echo back |
|
||
|
||
### Priority for B.3 / next phase
|
||
|
||
1. **MotionInterp port** — DoMotion → apply_current_movement → set_local_velocity: this is what drives physics velocity from animation state
|
||
2. **Terrain FSplitNESW fix** — swap to AC2D render formula, add per-triangle Z sampling
|
||
3. **update_object per-frame loop** — wire Transition into position update (already have CollisionPrimitives)
|
||
4. **Cell transition** — change_cell / calc_cross_cells when moving between cells
|
||
5. **MoveToState deduplication** — send once per state change, not per frame
|
||
6. **AutonomousPosition heartbeat** — every 1 second
|
||
7. **Sequence counters** — extract from CreateObject messages
|