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
37
CLAUDE.md
37
CLAUDE.md
|
|
@ -2,24 +2,35 @@
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
Build **acdream**, a modern open-source C# .NET 10 Asheron's Call client. The
|
Build **acdream**, a modern open-source C# .NET 10 Asheron's Call client.
|
||||||
end state is a working client that:
|
A faithful port of the retail AC client's behavior to modern C# + Silk.NET,
|
||||||
|
with a plugin API the original never had.
|
||||||
|
|
||||||
- Loads the retail AC dat files and renders the world (terrain, static meshes,
|
**The code is modern. The behavior is retail.**
|
||||||
dynamic entities, characters)
|
|
||||||
- Connects to an ACE server and plays as a character
|
|
||||||
- Exposes a **first-class plugin API** so players can write native scripts and
|
|
||||||
macros to automate gameplay — this is a core architectural requirement, not
|
|
||||||
a bolt-on
|
|
||||||
|
|
||||||
The codebase is organized by phase. Current phase state lives in memory
|
Every AC-specific algorithm is ported from the decompiled retail client
|
||||||
(`memory/project_phase_*_state.md`), current phase plans live in `docs/plans/`,
|
(`docs/research/decompiled/`, 22,225 functions, 688K lines of C). The code
|
||||||
and the long-term vision lives in `memory/project_acdream.md`.
|
around those algorithms is modern C# with clean architecture. The plugin API
|
||||||
|
exposes game state through well-defined interfaces.
|
||||||
|
|
||||||
|
**Architecture:** `docs/architecture/acdream-architecture.md` is the
|
||||||
|
single source of truth for how the client is structured. All work must
|
||||||
|
align with this document. When the architecture doc and reality diverge,
|
||||||
|
update one or the other — never leave them out of sync.
|
||||||
|
|
||||||
|
**Execution phases:** R1→R8 in the architecture doc. Each phase has clear
|
||||||
|
goals, test criteria, and builds on the previous. Don't skip phases.
|
||||||
|
|
||||||
|
The codebase is organized by layer (see architecture doc). Current phase
|
||||||
|
state lives in memory (`memory/project_*.md`), plans in `docs/plans/`,
|
||||||
|
research in `docs/research/`.
|
||||||
|
|
||||||
## How to operate
|
## How to operate
|
||||||
|
|
||||||
**You are the lead engineer on this project at all times. Stop as little as
|
**You are the lead engineer AND architect on this project at all times.**
|
||||||
possible.** Drive work autonomously and continuously through full phases and
|
You own the architecture (`docs/architecture/acdream-architecture.md`),
|
||||||
|
the execution plan (phases R1–R8), the development workflow, and all
|
||||||
|
technical decisions. Stop as little as possible. Drive work autonomously and continuously through full phases and
|
||||||
across commit boundaries. Do not stop mid-phase for routine progress check-ins,
|
across commit boundaries. Do not stop mid-phase for routine progress check-ins,
|
||||||
permission asks on low-stakes design calls, or "should I continue?" confirmations.
|
permission asks on low-stakes design calls, or "should I continue?" confirmations.
|
||||||
The user has repeatedly authorized direct-to-main commits, multi-commit sessions,
|
The user has repeatedly authorized direct-to-main commits, multi-commit sessions,
|
||||||
|
|
|
||||||
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