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>
This commit is contained in:
parent
a722c29759
commit
adf626367e
3 changed files with 1199 additions and 13 deletions
343
docs/architecture/acdream-architecture.md
Normal file
343
docs/architecture/acdream-architecture.md
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
# acdream — Comprehensive Architecture Plan
|
||||
|
||||
## Vision
|
||||
|
||||
A modern C# .NET 10 Asheron's Call client that:
|
||||
- **Behaves identically to the retail client** — same physics, same
|
||||
animations, same terrain, same collision, same network protocol
|
||||
- **Looks identical to the retail client** — same meshes, same textures,
|
||||
same lighting, same blending, rendered via modern Silk.NET OpenGL
|
||||
- **Adds a plugin API** the retail client never had — native C# plugins
|
||||
+ Lua macro scripting for player automation
|
||||
- **Is NOT a 1:1 C++ port** — uses modern C# patterns (composition over
|
||||
inheritance, interfaces, dependency injection) while matching retail
|
||||
behavior exactly
|
||||
|
||||
## Guiding Principle
|
||||
|
||||
**The code is modern. The behavior is retail.**
|
||||
|
||||
Every AC-specific algorithm is ported faithfully from the decompiled retail
|
||||
client (docs/research/decompiled/, 688K lines). The code AROUND those
|
||||
algorithms is modern C# with clean architecture. The plugin API exposes
|
||||
game state through well-defined interfaces that the retail client never had.
|
||||
|
||||
---
|
||||
|
||||
## Layer Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ LAYER 5: Plugin API │
|
||||
│ IGameState, IEvents, IActions, IPacketPipeline, IOverlay │
|
||||
│ Plugin host (ALC), Lua macro engine (MoonSharp) │
|
||||
│ ► acdream-unique — not in retail client │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ LAYER 4: Game Objects │
|
||||
│ GameEntity (one per world object) │
|
||||
│ ├── PhysicsBody (ported from decompiled) │
|
||||
│ ├── AnimSequencer (ported from decompiled) │
|
||||
│ ├── CellTracker (ported from decompiled) │
|
||||
│ ├── AppearanceState (ObjDesc: palettes, textures, parts)│
|
||||
│ └── MotionState (ported from decompiled) │
|
||||
│ ► behavior matches retail, code is modern C# composition │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ LAYER 3: World Systems │
|
||||
│ TerrainSystem (heightmap, blending, scenery) │
|
||||
│ CellSystem (LandCells, EnvCells, portals, BSP) │
|
||||
│ StreamingSystem (background loading, LOD, frustum cull) │
|
||||
│ ► behavior matches retail, streaming is acdream-unique │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ LAYER 2: Network │
|
||||
│ WorldSession (ISAAC, fragments, game messages) │
|
||||
│ MessageRouter (opcode dispatch, sequence tracking) │
|
||||
│ ► wire-format identical to retail │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ LAYER 1: Renderer │
|
||||
│ Silk.NET OpenGL 4.3 core profile │
|
||||
│ TerrainRenderer, StaticMeshRenderer, TextureCache │
|
||||
│ Shaders (terrain blending, mesh lighting, translucency) │
|
||||
│ ► completely different from retail (D3D7), same visual │
|
||||
│ output │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ LAYER 0: Platform │
|
||||
│ .NET 10, Silk.NET window/input, DatReaderWriter │
|
||||
│ ► acdream-unique infrastructure │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure (target)
|
||||
|
||||
```
|
||||
src/
|
||||
AcDream.Core/ Layer 2-4: no GL, no Silk.NET, pure logic
|
||||
Physics/
|
||||
PhysicsBody.cs ← ported from decompiled (done)
|
||||
CollisionPrimitives.cs ← ported from decompiled (done)
|
||||
MotionInterpreter.cs ← ported from decompiled (done)
|
||||
AnimationSequencer.cs ← ported from decompiled (done)
|
||||
CellBsp.cs ← TODO: port from decompiled
|
||||
Transition.cs ← TODO: port from decompiled
|
||||
TerrainSurface.cs ← verified against ACME (done)
|
||||
World/
|
||||
GameEntity.cs ← TODO: unified entity (replaces scattered state)
|
||||
WorldState.cs ← TODO: owns all entities
|
||||
CellTracker.cs ← TODO: per-entity cell management
|
||||
SceneryGenerator.cs ← verified against decompiled (done)
|
||||
LandblockLoader.cs ← done
|
||||
Terrain/
|
||||
LandblockMesh.cs ← verified against ACME (done)
|
||||
TerrainBlending.cs ← verified against ACME (done)
|
||||
Meshing/
|
||||
GfxObjMesh.cs ← cross-checked against ACME (done)
|
||||
SetupMesh.cs ← cross-checked (done)
|
||||
Textures/
|
||||
SurfaceDecoder.cs ← done
|
||||
Dat/
|
||||
MotionResolver.cs ← done (move here from Meshing/)
|
||||
|
||||
AcDream.Core.Net/ Layer 2: networking
|
||||
WorldSession.cs ← done (wire-compatible with ACE)
|
||||
NetClient.cs ← done
|
||||
Messages/ ← done (CreateObject, MoveToState, etc.)
|
||||
|
||||
AcDream.Plugin.Abstractions/ Layer 5: plugin interfaces
|
||||
IAcDreamPlugin.cs ← done
|
||||
IPluginHost.cs ← done
|
||||
IGameState.cs ← done
|
||||
IEvents.cs ← done
|
||||
|
||||
AcDream.App/ Layer 1 + Layer 4 wiring
|
||||
Rendering/
|
||||
GameWindow.cs ← TODO: thin down to GL calls only
|
||||
TerrainRenderer.cs ← done
|
||||
StaticMeshRenderer.cs ← done
|
||||
TextureCache.cs ← done
|
||||
ChaseCamera.cs ← done
|
||||
FlyCamera.cs ← done
|
||||
Streaming/
|
||||
StreamingController.cs ← done
|
||||
GpuWorldState.cs ← done
|
||||
Input/
|
||||
PlayerMovementController.cs ← done (uses ported physics)
|
||||
Plugins/
|
||||
AppPluginHost.cs ← done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GameEntity: The Unified Entity (TODO — the big refactor)
|
||||
|
||||
Currently, entity state is scattered across:
|
||||
- `WorldEntity` (position, rotation, mesh refs)
|
||||
- `AnimatedEntity` (animation frame, setup, sequencer)
|
||||
- `_entitiesByServerGuid` dict (server GUID lookup)
|
||||
- `GpuWorldState._loaded[lb].Entities` (per-landblock lists)
|
||||
- `_playerController` (player-specific movement)
|
||||
|
||||
This should become ONE class:
|
||||
|
||||
```csharp
|
||||
public sealed class GameEntity
|
||||
{
|
||||
// Identity
|
||||
public uint ServerGuid { get; }
|
||||
public uint SetupId { get; }
|
||||
public string? Name { get; }
|
||||
|
||||
// Spatial (ported from CPhysicsObj)
|
||||
public PhysicsBody Physics { get; } // position, velocity, gravity
|
||||
public CellTracker Cell { get; } // which cell we're in
|
||||
|
||||
// Appearance (ported from CPartArray)
|
||||
public AnimationSequencer Animation { get; } // frame playback
|
||||
public AppearanceState Appearance { get; } // ObjDesc overrides
|
||||
|
||||
// Motion (ported from CMotionInterp)
|
||||
public MotionInterpreter Motion { get; } // walk/run/turn state
|
||||
|
||||
// Render output (consumed by StaticMeshRenderer)
|
||||
public IReadOnlyList<MeshRef> MeshRefs { get; }
|
||||
|
||||
// Per-frame update (matches retail update_object)
|
||||
public void Update(float dt)
|
||||
{
|
||||
Motion.ApplyCurrentMovement(); // set velocity from motion state
|
||||
Physics.UpdateObject(dt); // integrate position
|
||||
// TODO: Transition.FindValidPosition // collision resolve
|
||||
Cell.UpdateCell(Physics.Position); // check cell transitions
|
||||
Animation.Advance(dt); // advance animation frames
|
||||
RebuildMeshRefs(); // compute per-part transforms
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Every entity in the world — player, NPC, monster, lifestone, door, chest —
|
||||
is a `GameEntity`. The renderer iterates them and draws. The plugin API
|
||||
exposes them as `WorldEntitySnapshot`. GameWindow becomes thin.
|
||||
|
||||
---
|
||||
|
||||
## Per-Frame Update Order (matches retail)
|
||||
|
||||
```
|
||||
1. Network tick
|
||||
└── Drain inbound queue → process CreateObject, UpdateMotion,
|
||||
UpdatePosition, PlayerTeleport → create/update GameEntities
|
||||
|
||||
2. Streaming tick
|
||||
└── Compute observer position → load/unload landblocks →
|
||||
create terrain + scenery GameEntities
|
||||
|
||||
3. Input tick (player mode only)
|
||||
└── Read WASD/mouse → MotionInterpreter.DoMotion →
|
||||
send MoveToState/AutonomousPosition to server
|
||||
|
||||
4. Entity tick (ALL entities, 30Hz fixed step)
|
||||
└── For each GameEntity: entity.Update(dt)
|
||||
This runs: motion → physics → collision → cell → animation
|
||||
|
||||
5. Render tick
|
||||
└── For each GameEntity: read MeshRefs, draw
|
||||
TerrainRenderer.Draw, StaticMeshRenderer.Draw
|
||||
(frustum cull, translucency pass, etc.)
|
||||
|
||||
6. Plugin tick
|
||||
└── Fire IEvents, drain IActions queue
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Execution Plan: How to Get There
|
||||
|
||||
### Phase R1: GameEntity Refactor (the foundation)
|
||||
**Goal:** Replace the scattered entity state with unified GameEntity.
|
||||
|
||||
1. Create `GameEntity` class in `AcDream.Core/World/`
|
||||
2. Move `AnimatedEntity` fields into `GameEntity.Animation`
|
||||
3. Move `WorldEntity` fields into `GameEntity.Physics` + position
|
||||
4. Move `_entitiesByServerGuid` into `WorldState`
|
||||
5. Move animation tick from `GameWindow.TickAnimations` into `GameEntity.Update`
|
||||
6. GameWindow.OnRender reads `GameEntity.MeshRefs` instead of `WorldEntity.MeshRefs`
|
||||
|
||||
**Test:** Everything looks the same as before. No visual change.
|
||||
|
||||
### Phase R2: Thin GameWindow
|
||||
**Goal:** GameWindow does only GL calls + input dispatch.
|
||||
|
||||
1. Extract entity creation from `OnLiveEntitySpawned` into `WorldState.SpawnEntity`
|
||||
2. Extract motion updates from `OnLiveMotionUpdated` into `WorldState.UpdateMotion`
|
||||
3. Extract player movement from the giant OnUpdate block into `PlayerController`
|
||||
4. GameWindow.OnUpdate calls: network.Tick → streaming.Tick → input.Tick → worldState.Tick → render
|
||||
|
||||
**Test:** Everything works the same. GameWindow.cs drops from 2000+ to ~500 lines.
|
||||
|
||||
### Phase R3: CellBSP + Wall Collision
|
||||
**Goal:** Entities can't walk through walls.
|
||||
|
||||
1. Port CellBSP from decompiled code (sphere_intersects_cell)
|
||||
2. Port Transition.FindValidPosition (swept sphere collision)
|
||||
3. Wire into GameEntity.Update between physics and cell tracking
|
||||
4. Indoor transitions become correct (wall stops you, doorway lets you through)
|
||||
|
||||
**Test:** Walk into building wall → stopped. Walk through doorway → enter.
|
||||
|
||||
### Phase R4: Complete Animation State Machine
|
||||
**Goal:** Every animation works for every entity type.
|
||||
|
||||
1. Port full MotionInterp.PerformMovement from decompiled (all 5 movement types)
|
||||
2. Port Links table resolution for smooth transitions
|
||||
3. Port idle modifiers (fidgets)
|
||||
4. Jump animation (wire jump motion command through the pipeline)
|
||||
|
||||
**Test:** All entity types animate correctly. Transitions are smooth.
|
||||
|
||||
### Phase R5: Lighting from Retail
|
||||
**Goal:** Sun, ambient, per-vertex lighting match retail.
|
||||
|
||||
1. Port AdjustPlanes (FUN_00532440) — face normals + per-vertex lighting
|
||||
2. Extract global lighting constants from decompiled DAT addresses
|
||||
3. Replace hardcoded shader constants with ported values
|
||||
|
||||
**Test:** Side-by-side with retail client shows matching lighting.
|
||||
|
||||
### Phase R6: Server Compliance
|
||||
**Goal:** ACE accepts all movement, no rubber-banding.
|
||||
|
||||
1. Server-authoritative Z (trust server position, local is cosmetic)
|
||||
2. Proper MoveToState with full RawMotionState packing
|
||||
3. Keepalive ping (5s idle)
|
||||
4. Graceful session management
|
||||
|
||||
**Test:** Walk around, other clients see smooth movement. No ACE errors.
|
||||
|
||||
### Phase R7: Interaction
|
||||
**Goal:** Click NPCs, open doors, pick up items, chat.
|
||||
|
||||
1. Use/UseWithTarget game actions
|
||||
2. Door open animation (server sends UpdateMotion → animate)
|
||||
3. Chat send/receive
|
||||
4. Basic inventory (pickup/drop)
|
||||
|
||||
**Test:** Open a door, talk to an NPC, send a chat message.
|
||||
|
||||
### Phase R8: Plugin API Completion
|
||||
**Goal:** Plugins can observe and control everything.
|
||||
|
||||
1. IGameState exposes all GameEntity fields
|
||||
2. IEvents fires for all world changes
|
||||
3. IActions covers: Move, Cast, Use, Say, Pickup, Drop
|
||||
4. IPacketPipeline hooks all 4 stages
|
||||
5. Lua macro engine (MoonSharp) ships as a built-in plugin
|
||||
|
||||
**Test:** A Lua script auto-loots gems. A C# plugin displays an overlay.
|
||||
|
||||
---
|
||||
|
||||
## Development Workflow (mandatory for ALL work)
|
||||
|
||||
```
|
||||
For every AC-specific behavior:
|
||||
|
||||
1. DECOMPILE → Find the function in docs/research/decompiled/
|
||||
2. CROSS-CHECK → Verify against ACE + ACME + holtburger
|
||||
3. PSEUDOCODE → Translate to readable pseudocode
|
||||
4. PORT → Faithful C# translation
|
||||
5. TEST → Conformance test against decompiled golden values
|
||||
6. INTEGRATE → Surgical wiring into the existing system
|
||||
7. VERIFY → Visual + functional test
|
||||
```
|
||||
|
||||
For acdream-specific code (renderer, plugin API, streaming):
|
||||
- Design for clean interfaces
|
||||
- Test independently
|
||||
- No AC-specific magic — those live in the ported layer
|
||||
|
||||
---
|
||||
|
||||
## Reference Hierarchy
|
||||
|
||||
| Domain | Primary Oracle | Secondary |
|
||||
|--------|---------------|-----------|
|
||||
| Physics/collision | Decompiled acclient.exe | ACE Physics/ |
|
||||
| Animation | Decompiled + ACE Animation/ | — |
|
||||
| Terrain | ACME ClientReference.cs | Decompiled |
|
||||
| Rendering | WorldBuilder (Silk.NET) | ACViewer |
|
||||
| Protocol | holtburger | AC2D |
|
||||
| Server behavior | ACE | — |
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
The client is "done" when:
|
||||
1. You can log in to an ACE server
|
||||
2. Walk around the entire world (streaming loads new areas)
|
||||
3. Enter and exit buildings through doorways
|
||||
4. See all NPCs, monsters, and players animated correctly
|
||||
5. Open doors, talk to NPCs, pick up items
|
||||
6. Send and receive chat
|
||||
7. A Lua macro can automate gameplay
|
||||
8. Side-by-side with the retail client, the world looks the same
|
||||
832
docs/research/acclient_architecture_map.md
Normal file
832
docs/research/acclient_architecture_map.md
Normal file
|
|
@ -0,0 +1,832 @@
|
|||
# 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue