Ground-truth audit of acdream's animation pipeline against retail
decompile (chunk_*.c), cross-referenced line-by-line with our code.
Previous audit relied on ACE and got wiring claims wrong (said our
PlayAction path was orphaned when it's wired via OnLiveMotionUpdated).
Findings:
- PerformMovement dispatcher (FUN_00529a90) matches our MotionInterpreter.
- apply_current_movement cycle priority (FUN_00529210) matches our
OnLiveMotionUpdated sequencer path.
- Commands list → PlayAction wiring matches retail.
- Falling / Jump / Dead substate routing matches.
- Frame-timing epsilon + negative-speed playback matches.
The agent's "hit-react missing" claim turned out to be wrong: the
referenced FUN_0048d760 call passes 32-bit IDs shaped like MotionCommand
values but user-confirmed retail shows NO body animation on damage, so
vtable +0x9c is almost certainly emit-effect / play-sound / spawn-
particle — not a motion play. Not an animation gap.
Open follow-up: CreateObject initial Commands list is parsed but not
replayed when the entity hydrates (minor; rare case).
Not a follow-up: on-hit combat feedback (particles, damage numbers).
That's a separate feature, not an animation pipeline concern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Sprint 2 research for indoor transitions. Documents:
- ACE EnvCell.find_transit_cells: sphere-plane + BSP containment
- ACE SortCell/BuildingObj.check_building_transit: outdoor→indoor
- PortalSide semantics and portal polygon plane testing
- Gap analysis: acdream needs CellBSP, BldPortal list, VisibleCells
Key finding: full accuracy requires CellBSP (physics BSP from dat)
for sphere_intersects_cell. Current PortalPlane.IsCrossing is a valid
approximation. ACME's AABB PointInCell is an intermediate option.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Complete pseudocode translation of the retail AC client's animation
system, extracted from chunk_00520000.c. Covers:
- Sequence::update_internal (1021 bytes, the core frame advance loop)
- Sequence::advance_to_next_animation (node transitions)
- Sequence::append_animation (queue management)
- MotionTableManager::PerformMovement (1878 bytes, full state machine)
- AddAnimationsToSequence (transition link → sequence nodes)
- GetStartFramePosition / GetEndFramePosition (reverse playback support)
- AdjustNodeSpeed (negative speed = swapped start/end frames)
Key findings:
- framePosition is a 64-bit DOUBLE, not float
- Negative speedScale swaps startFrame↔endFrame at the node level
- update_internal handles both forward and reverse in one loop
- Frame triggers fire at every integer boundary crossing
- The keyframe slerp lives in the renderer, not the sequencer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ROOT CAUSE FIX for missing left-side animations.
The AC client's MotionTable has NO cycles for TurnLeft (0x000E),
SideStepLeft (0x0010), or WalkBackward (0x0006). The real client
calls adjust_motion() which remaps these to their right-side
equivalents with NEGATIVE speed before looking up the cycle. Then
multiply_framerate() swaps LowFrame↔HighFrame so the animation
plays backward.
Source: ACE MotionInterp.cs:394-428, decompiled FUN_005267E0.
Changes:
- AnimationSequencer.SetCycle: adds adjust_motion block that remaps
left→right with speed *= -1 (TurnLeft, SideStepLeft) or
speed *= -0.65 (WalkBackward = BackwardsFactor)
- LoadAnimNode: when framerate < 0, swaps Low↔High (matching the
decompiled multiply_framerate)
- GameWindow.UpdatePlayerAnimation: passes original animCommand to
SetCycle (sequencer handles remapping internally), keeps legacy
fallback for non-sequencer entities
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Complete decompilation of the retail Asheron's Call client using
Ghidra 12.0.4 + pyghidra headless. 22,225 of 22,226 functions
successfully decompiled in 75 seconds.
Output: docs/research/decompiled/ (54 files, 688,567 lines of C)
Key findings already identified:
- CLandBlockStruct::ConstructPolygons at chunk_00530000.c:2270
(split direction formula with 0x0CCAC033 constants)
- Motion command handlers at chunk_00510000.c (0x45000005 etc)
- Motion interpreter at chunk_00520000.c
- Portal space UI at chunk_004D0000.c and chunk_00560000.c
Next: identify CPhysicsObj, CMotionInterp, collision, and movement
functions by cross-referencing against ACE's C# port.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Exhaustive analysis of two working AC clients revealing three critical
findings that reshape acdream's movement system:
1. Server-authoritative Z: neither AC2D nor holtburger computes local
terrain Z for the player. AC2D sends keys, receives position. Holtburger
dead-reckons for smoothing but the server overrides.
2. Terrain split formula mismatch: AC2D and ACViewer's render path use
0x0CCAC033-based FSplitNESW; WorldBuilder (our source) uses a different
214614067-based physics formula. Our terrain mesh triangulation doesn't
match the real AC client's, causing Z mismatches on slopes.
3. Movement deduplication: MoveToState sent once per state change, not per
frame. AutonomousPosition heartbeat every 1 second.
Also adds AC2D to CLAUDE.md reference repos section.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>