Add detailed design and implementation scaffolding for TiltedEvolution-style animation synchronization. New documentation: animation-architecture-alignment.md, animation-sync-analysis.md, f4-animation-descriptor.md, phase1-3-completion-report.md and updates to dev-log.md describing Phase 1-3 progress. Plugin: introduce F4AnimationDescriptor (plugin/include/F4AnimationDescriptor.h, plugin/src/F4AnimationDescriptor.cpp) and extend remote state handling (plugin/include/F4TRemotePlayerState.h). Integrate protocol/state extensions and refactor points: plugin/src/F4TNetworking.cpp now parses optional actorStateFlags and actionEvents; proxy controller and animation sync files (F4TProxyActorController.cpp, F4TProxyAnimationSync.cpp) updated to support descriptor-based bulk variable snapshots and actor state replication. Server/tools: update server/dev_server_app.py and server/fake_client.py to handle and display the new optional fields. These changes are additive and backward-compatible and set up phases for action capture, dynamic proxy spawn, and action-replay integration.
391 lines
18 KiB
Markdown
391 lines
18 KiB
Markdown
# Animation Architecture Alignment: TiltedEvolution → F4T
|
|
|
|
## Executive Summary
|
|
|
|
This document maps TiltedEvolution's proven multiplayer animation and state synchronization architecture onto Fallout 4 Together's codebase. It identifies which concepts are applicable to FO4, what needs adaptation, and FO4-specific constraints.
|
|
|
|
**Key Finding:** TiltedEvolution's architecture is fundamentally compatible with FO4, but FO4 has stricter constraints around AI, velocity, and action replay mechanics.
|
|
|
|
---
|
|
|
|
## High-Level Architecture Comparison
|
|
|
|
### TiltedEvolution (Skyrim SE) - The Gold Standard
|
|
|
|
```
|
|
Local Player Remote Server Other Clients
|
|
| | |
|
|
[Capture] - Save transform, - relay - [Receive]
|
|
+ graph variables | + parse JSON
|
|
+ actor state flags | + update state
|
|
+ action events [Broadcast]
|
|
| |
|
|
[Send] -------- JSON line ------> |
|
|
~100ms intervals |
|
|
|
|
|
[Remote Actor Control]
|
|
|
|
|
[InterpolationSystem]
|
|
- Position lerp
|
|
- Direction lerp
|
|
- Graph variable load
|
|
|
|
|
[AnimationSystem]
|
|
- Action replay queue
|
|
- Actor state apply
|
|
- Discrete animation trigger
|
|
|
|
|
[Result: Smooth movement +
|
|
playing correct animations]
|
|
```
|
|
|
|
### F4T Current (Pre-Refactor) - SetPosition Only
|
|
|
|
```
|
|
Local Player Remote Server Other Clients
|
|
| | |
|
|
[Capture] - movement state relay [Receive]
|
|
+ position | + parse JSON
|
|
+ flags (move/sprint) [Broadcast] + update RemotePlayerState
|
|
+ NO actor state |
|
|
+ NO actions |
|
|
| [Proxy Actor Control]
|
|
[Send] -------- JSON ---------> |
|
|
|
|
|
[ProxyActorController]
|
|
- GetOrCreateSlot()
|
|
- ApplyRuntimeProxyTransform()
|
|
|
|
|
v
|
|
[SetPosition() only]
|
|
[+ Move(0, {0,0,0}, false)]
|
|
[+ SetLinearVelocityImpl()]
|
|
|
|
|
[Result: Smooth movement
|
|
but NO ANIMATIONS
|
|
(velocity doesn't trigger anim system)]
|
|
```
|
|
|
|
---
|
|
|
|
## Component Mapping
|
|
|
|
### 1. Capture & Serialization
|
|
|
|
| Concept | TiltedEvolution | F4T Current | F4T After Phase 2 |
|
|
|---------|-----------------|-------------|-------------------|
|
|
| **Local state capture** | `CharacterService::RunLocalUpdates()` | `main.cpp:PollLocalPlayerTransform()` | Same + add `PollLocalPlayerActionState()` |
|
|
| **What's captured** | Position, rotation, direction, **animation variables snapshot**, **actor state flags**, **action events** | Position, rotation, direction, movement flags, speed | Same as Tilted (extended) |
|
|
| **Capture interval** | 100 ms | ~50-100 ms | ~100 ms |
|
|
| **Storage** | `Movement { position, Variables, Direction, ... }` | `RemotePlayerState { x, y, z, angleZ, movementType, cellId, ... }` | Extend with `actorStateFlags1/2`, `ActionEvent[]` |
|
|
|
|
**Action:** Phase 2.3 adds action capture to `main.cpp`; Phase 2.1 extends struct.
|
|
|
|
### 2. Network Transmission
|
|
|
|
| Concept | TiltedEvolution | F4T Current | F4T After Phase 2 |
|
|
|---------|-----------------|-------------|-------------------|
|
|
| **Packet type** | `ClientReferencesMoveRequest` (C++ struct) | JSON: `{"type":"transform", "x",...}` | Extend JSON with `"actorStateFlags1"`, `"actionEvents":[...]` |
|
|
| **Serialization** | Binary (optimized for network) | JSON (text over TCP) | JSON (text over TCP, extended) |
|
|
| **Frequency** | ~10 Hz (adaptive) | ~10 Hz (throttled) | ~10 Hz (throttled) |
|
|
| **Thread** | Game thread sends | Game thread sends | Game thread sends |
|
|
|
|
**Action:** Phase 2.2 updates JSON parsing in `F4TNetworking.cpp`.
|
|
|
|
### 3. Remote State Storage
|
|
|
|
| Concept | TiltedEvolution | F4T Current | F4T After Phase 2 |
|
|
|---------|-----------------|-------------|-------------------|
|
|
| **Store** | `std::unordered_map<uint32 playerId, Character>` in CharacterService | `g_remotePlayers` map + mutex | Same + extend struct |
|
|
| **Thread** | Network thread writes, game thread reads (snapshot copy) | Network thread writes, game thread reads (snapshot copy) | Same (thread-safe boundary maintained) |
|
|
| **Per-player data** | Multiple Character instances (one per remote) | One RemotePlayerState per remote | Extend with flags + action queue |
|
|
|
|
**Action:** Phase 2.1 modifies struct; thread safety unchanged.
|
|
|
|
### 4. Proxy Actor Spawning & Lifecycle
|
|
|
|
| Concept | TiltedEvolution | F4T Current | F4T After Phase 4 |
|
|
|---------|-----------------|-------------|-------------------|
|
|
| **Spawn method** | `Actor::PlaceAtMe()` on demand | Pre-placed pool in CK | Dynamic `PlaceAtMe()` (like Tilted) |
|
|
| **Lifecycle** | Create on connect, destroy on disconnect | Pool reuse; hold out of range when inactive | Pool reuse (same as current) |
|
|
| **Count limit** | ~256 (depends on memory) | 4 (pre-placed limit) | 4-8 (dynamic pool size) |
|
|
| **AI** | Suppressed (`SetRemote(true)`) | Suppressed (neutralize) | Suppressed (neutralize) |
|
|
| **Puppet mode** | Yes (movement external) | Yes (external via SetPosition) | Yes (external via SetPosition + velocity) |
|
|
|
|
**Action:** Phase 4.1 replaces pool init with dynamic spawn logic.
|
|
|
|
### 5. Movement & Interpolation
|
|
|
|
| Concept | TiltedEvolution | F4T Current | F4T After Phase 4 |
|
|
|---------|-----------------|-------------|-------------------|
|
|
| **Position sync** | `ForcePosition()` (Skyrim API) | `SetPosition()` (FO4 API) | Same (SetPosition works) |
|
|
| **Rotation sync** | Lerp + direct set | `SetHeading()` (snap) | `SetHeading()` + lerp (Phase 4.3 later) |
|
|
| **Velocity** | Natural from AI controller + character controller velocity | Manual injection via `Move()` + `SetLinearVelocityImpl()` | Same (proven approach) |
|
|
| **Interpolation** | `InterpolationSystem` lerps transform + graph vars | Lerp position, snap rotation | Enhance rotation lerp (Phase 7+) |
|
|
|
|
**Action:** Phase 4.2 verifies velocity injection works on dynamic proxies.
|
|
|
|
### 6. Animation Variable Sync
|
|
|
|
| Concept | TiltedEvolution | F4T Current | F4T After Phase 5 |
|
|
|---------|-----------------|-------------|-------------------|
|
|
| **Method** | Descriptor-based bulk write (by index) | String-based per-variable write | Descriptor-based bulk write (Phase 3 + 5) |
|
|
| **Variables** | `Speed`, `Direction`, `IsSprinting`, `IsSneaking`, + others | `Speed` (string fallback), `IsSprinting` | Same minimal set + indexed |
|
|
| **Frequency** | Once per action + continuous during interpolation | Every frame | Every frame (indexed, faster) |
|
|
| **Thread safety** | `BSScopedLock` on graph manager | No explicit locking | Add locking (Phase 5) |
|
|
| **Variable persistence** | Snapshot approach (graph state saved/loaded) | Continuous writes | Hybrid: snapshot for actions, continuous for locomotion |
|
|
|
|
**Action:** Phase 3 builds descriptor class; Phase 5 refactors sync to use it.
|
|
|
|
### 7. Action Replay System
|
|
|
|
| Concept | TiltedEvolution | F4T Current | F4T After Phase 5 |
|
|
|---------|-----------------|-------------|-------------------|
|
|
| **Capture** | Hook `ActorMediator::PerformAction()` → queue `ActionEvent` | **Not implemented** | Add `PerformAction` hook (Phase 2.3) |
|
|
| **Storage** | `RemoteAnimationComponent::TimePoints` (event queue per remote) | **No action queue** | `RemoteActionComponent` per proxy (Phase 5.1) |
|
|
| **Replay** | `AnimationSystem::Update()` pops + replays actions in order | **No action replay** | `ApplyRemoteAction()` call in slot update (Phase 5.2) |
|
|
| **Content** | `{ Type, EventName, Variables, State1, State2, IdleForm, Target, ... }` | **N/A** | Minimal set: `{ Type, EventName, Variables, State1, State2 }` |
|
|
| **Effect** | Discrete animations (combat idles, emotes, equipping) play deterministically | **Proxies stand still** | Combat idles + transitions animate |
|
|
|
|
**Action:** Phases 2.3, 5.1, 5.2 implement action pipeline.
|
|
|
|
### 8. Actor State Flags
|
|
|
|
| Concept | TiltedEvolution | F4T Current | F4T After Phase 2 |
|
|
|---------|-----------------|-------------|-------------------|
|
|
| **Capture** | `actor→actorState.flags1/flags2` on local player | **Not captured** | Add to state capture (Phase 2.3) |
|
|
| **Network** | Packed in `ActionEvent.State1/State2` | **No** | Add to transform packet JSON |
|
|
| **Apply** | Before action replay: `actor→actorState.flags1 = remote.State1` | **No** | Apply before animation sync (Phase 4.3) |
|
|
| **Purpose** | Signal animation graph FSM (combat mode, sneaking, etc.) | **No effect** | Enable graph-driven state transitions |
|
|
|
|
**Action:** Phase 2.1 adds fields; Phase 2.3 captures; Phase 4.3 applies.
|
|
|
|
---
|
|
|
|
## Data Flow Diagrams
|
|
|
|
### TiltedEvolution Animation Sync (Reference)
|
|
|
|
```
|
|
PerformAction hook (local)
|
|
↓
|
|
Capture: {Type, EventName, Variables, State1, State2, Idle}
|
|
↓
|
|
Network: Send ActionEvent + Movement (animation vars)
|
|
↓
|
|
Remote receives (network thread)
|
|
↓
|
|
AnimationSystem::Update() (game thread)
|
|
↓
|
|
For each action in queue:
|
|
- actor→actorState.flags1/2 = State1/State2
|
|
- actor→LoadAnimationVariables(Variables)
|
|
- ActorMediator::ForceAction(ActionData)
|
|
↓
|
|
Result: Correct animation plays
|
|
```
|
|
|
|
### F4T Current Animation Sync (Broken)
|
|
|
|
```
|
|
PollLocalPlayerTransform (local)
|
|
↓
|
|
Capture: {position, angle, moving, sprinting}
|
|
↓
|
|
Network: Send transform (JSON)
|
|
↓
|
|
Remote receives (network thread)
|
|
↓
|
|
ProxyActorController::Update() (game thread)
|
|
↓
|
|
ApplyRuntimeProxyTransform()
|
|
- SetPosition()
|
|
- Move(0, {0,0,0}, false) → get controller
|
|
- SetLinearVelocityImpl(calc_velocity)
|
|
↓
|
|
ApplyProxyAnimationFromRemoteState()
|
|
- TrySetGraphFloat("Speed", value) ← writes by name, slow
|
|
- TrySetGraphBool("IsSprinting", value)
|
|
↓
|
|
Result: Movement works, but animations NEVER play
|
|
(velocity is set, but graph doesn't react)
|
|
```
|
|
|
|
### F4T After Phase 5 (Proposed)
|
|
|
|
```
|
|
Local Player
|
|
├─ PollLocalPlayerTransform()
|
|
│ └─ Capture: {position, angle, velocity, movementFlags}
|
|
│
|
|
├─ PollLocalPlayerActionState() [NEW - Phase 2.3]
|
|
│ └─ Capture: {actor→actorState, PerformAction events}
|
|
│
|
|
└─ SendTransformPacket() [EXTENDED - Phase 2.2]
|
|
└─ JSON: {position, angle, actorStateFlags1/2, actionEvents}
|
|
|
|
Network Thread
|
|
└─ HandleTransformPacket()
|
|
└─ UpdateRemotePlayer() [EXTENDED - Phase 2.1]
|
|
└─ RemotePlayerState: {position, flags, action queue}
|
|
|
|
Game Thread (ProxyActorController::Update)
|
|
├─ GetRemotePlayerSnapshot()
|
|
│
|
|
├─ For each remote:
|
|
│ ├─ GetOrCreateSlot()
|
|
│ ├─ RestoreSlotProxyForRemotePlayer() [First frame]
|
|
│ │ └─ Snap transform
|
|
│ │
|
|
│ └─ UpdateRuntimeSlotForRemotePlayer() [Ongoing]
|
|
│ ├─ ApplyRuntimeProxyTransform() [Phase 4]
|
|
│ │ ├─ SetPosition()
|
|
│ │ ├─ Move(0, {0,0,0}, false)
|
|
│ │ └─ SetLinearVelocityImpl()
|
|
│ │
|
|
│ ├─ ApplyRemoteActionState() [NEW - Phase 4.3]
|
|
│ │ └─ actor→actorState.flags1/2 = remote.flags
|
|
│ │
|
|
│ ├─ ReplayRemoteActions() [NEW - Phase 5.2]
|
|
│ │ └─ For each action:
|
|
│ │ ├─ actor→LoadAnimationVariablesFromDescriptor()
|
|
│ │ └─ ForceProxyAction()
|
|
│ │
|
|
│ └─ ApplyProxyAnimationFromRemoteState() [REFACTORED - Phase 5.3]
|
|
│ └─ LoadAnimationVariablesFromDescriptor() [indexed, bulk]
|
|
|
|
Result
|
|
├─ ✅ Smooth movement
|
|
├─ ✅ Character controller velocity set
|
|
├─ ✅ Locomotion animations (idle/walk/run/sprint)
|
|
├─ ✅ Action animations (combat, equips)
|
|
├─ ✅ Actor state affects graph FSM
|
|
└─ ✅ Performance: O(1) indexed access, not O(n) string lookup
|
|
```
|
|
|
|
---
|
|
|
|
## FO4-Specific Adaptations & Constraints
|
|
|
|
### 1. Character Controller Velocity
|
|
|
|
| Aspect | Skyrim SE | FO4 | F4T Approach |
|
|
|--------|-----------|-----|------------|
|
|
| **API exposure** | High (character controller accessible) | Lower (not directly exposed) | Use `Actor::Move()` return value to get controller |
|
|
| **Velocity injection** | Direct via API | Via Move() + SetLinearVelocityImpl() | Same as current proven approach |
|
|
| **Animation system watching velocity** | Yes | Yes (verified in dev-log) | Confirmed working; use same method |
|
|
|
|
**Note:** F4T already verified this works (see dev-log 2026-06-03). Use same velocity injection.
|
|
|
|
### 2. Actor State Flags
|
|
|
|
| Aspect | Skyrim SE | FO4 | F4T Approach |
|
|
|--------|-----------|-----|------------|
|
|
| **Flags struct** | `ActorState` | `ActorState` | Same struct + same flags |
|
|
| **Graph dependency** | Yes (FSM gated by flags) | Yes (FSM gated by flags) | Apply before animation sync |
|
|
| **Combat flag** | `inCombat` | `inCombat` | Same |
|
|
| **Sneak flag** | `sneaking` | `sneaking` | Same |
|
|
|
|
**Note:** F4T tried `SetSneaking()` API directly (disabled in code). Better approach: sync `actor→actorState.flags1/2` bitfields. More reliable than API calls.
|
|
|
|
### 3. Action Replay
|
|
|
|
| Aspect | Skyrim SE | FO4 | F4T Approach |
|
|
|--------|-----------|-----|------------|
|
|
| **API** | `ActorMediator::ForceAction()` | `ActorMediator::ForceAction()` | Same |
|
|
| **Availability** | Well-exposed in common headers | Exposed in CommonLibF4 | Use same |
|
|
| **Idle forms** | Rich idle system with conditions | Rich idle system with conditions | Capture + replay |
|
|
| **Combat actions** | Power attacks, spells, shouts | Power attacks, guns, grenades | Defer to Phase 2+ |
|
|
|
|
**Note:** CommonLibF4 likely has equivalent `ActorMediator` and `ForceAction()` (verify in Phase 1.2 testing).
|
|
|
|
### 4. Animation Graph Manager
|
|
|
|
| Aspect | Skyrim SE | FO4 | F4T Approach |
|
|
|--------|-----------|-----|------------|
|
|
| **Manager lifecycle** | Stable per actor | Stable per actor | Same assumptions |
|
|
| **Variable cache** | Accessible via `GetBSAnimationGraph()` | Accessible via `GetAnimationGraphManagerImpl()` | Use CommonLibF4 API (current code works) |
|
|
| **Thread safety** | Requires `BSScopedLock` | Requires `BSScopedLock` | Add explicit locking (Phase 5) |
|
|
| **Descriptor key** | Hash of graph type name | Hash of graph type name | Build descriptor once, cache |
|
|
|
|
**Note:** F4T already uses the CommonLibF4 API successfully. No changes needed, just add locking.
|
|
|
|
### 5. Proxy Puppet Suppression
|
|
|
|
| Aspect | Skyrim SE | FO4 | F4T Approach |
|
|
|--------|-----------|-----|------------|
|
|
| **AI blocking** | `SetRemote(true)` | No direct equivalent | Current: `NeutralizeProxyActor()` + flags (kept for Phase 4) |
|
|
| **Movement blocking** | External `ForcePosition()` | External `SetPosition()` | Same |
|
|
| **Action suppression** | Hook `PerformAction` return 0 | Need to verify hook exists | Assume possible; test in Phase 1 |
|
|
|
|
**Note:** F4T currently suppresses AI via actor flags/hostile reset. Tilted uses a `Remote` flag. Both approaches work; F4T's is compatible.
|
|
|
|
---
|
|
|
|
## Phase-by-Phase Integration Points
|
|
|
|
### Phase 1: Research
|
|
- Verify FO4 variable indices match assumptions
|
|
- Confirm `ActorMediator::ForceAction()` exists in CommonLibF4
|
|
- Document any differences from Skyrim model
|
|
|
|
### Phase 2: Protocol & State
|
|
- Extend JSON packets with `actorStateFlags1/2`, `actionEvents`
|
|
- Add action capture in `PerformAction` hook (or equivalent)
|
|
- Maintain backward compatibility
|
|
|
|
### Phase 3: Descriptors
|
|
- Build `F4AnimationDescriptor` using discovered indices
|
|
- Implement bulk read/write methods
|
|
- Verify thread safety with `BSScopedLock`
|
|
|
|
### Phase 4: Spawn & Velocity
|
|
- Switch to dynamic spawn (proof of concept: single proxy first)
|
|
- Verify velocity injection works on dynamic actors
|
|
- Apply actor state flags before animation sync
|
|
|
|
### Phase 5: Action Replay
|
|
- Implement `RemoteActionComponent` action queue
|
|
- Refactor animation sync to use descriptors
|
|
- Ensure action replay doesn't conflict with continuous locomotion
|
|
|
|
### Phase 6-7: Testing & Polish
|
|
- Verify animations play correctly
|
|
- Performance benchmark descriptor vs string-based
|
|
- Document FO4-specific adaptations
|
|
|
|
---
|
|
|
|
## Success Criteria for Phase 1
|
|
|
|
- [ ] Document all float/bool/int variables for humanoid graph (variable names, likely indices)
|
|
- [ ] Confirm TiltedEvolution concepts apply to FO4 (actor state, action replay, descriptors)
|
|
- [ ] Verify `Actor::Move()` + `SetLinearVelocityImpl()` works on dynamic proxies
|
|
- [ ] No major architectural blockers identified
|
|
- [ ] Produce `animation-architecture-alignment.md` (this document)
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
### TiltedEvolution Source
|
|
|
|
- [AnimationSystem.cpp](https://github.com/tiltedphoques/TiltedEvolution/blob/dev/Code/client/Systems/AnimationSystem.cpp) - Action replay
|
|
- [CharacterService.cpp](https://github.com/tiltedphoques/TiltedEvolution/blob/dev/Code/client/Services/Generic/CharacterService.cpp) - State capture
|
|
- [AnimationGraphDescriptor_Master_Behavior.cpp](https://github.com/tiltedphoques/TiltedEvolution/blob/dev/Code/encoding/Structs/Skyrim/AnimationGraphDescriptor_Master_Behavior.cpp) - Variable indices
|
|
|
|
### F4T Source
|
|
|
|
- `plugin/src/main.cpp` - Local transform capture
|
|
- `plugin/src/F4TNetworking.cpp` - JSON serialization
|
|
- `plugin/src/F4TProxyAnimationSync.cpp` - Current animation sync (to be refactored)
|
|
- `plugin/src/F4TProxyActorController.cpp` - Proxy lifecycle + movement
|
|
|
|
### CommonLibF4 Headers
|
|
|
|
- `RE/H/ActorState.h` - Actor state flags
|
|
- `RE/H/BSAnimationGraphManager.h` - Graph manager
|
|
- `RE/H/IAnimationGraphManagerHolder.h` - Graph holder interface
|
|
- `RE/H/Actor.h` - Actor class (Move, SetPosition, etc.)
|
|
|