Add proxy puppet hook and graph speed sync

Introduce a puppet vtable hook to preserve networked locomotion Speed and fix proxy cadence. Added F4TProxyPuppet (header + impl) which installs an Actor::Update hook that writes the desired graph Speed and calls UpdateNoAI for proxy actors so the engine does not recompute/zero Speed from controller velocity. Publish active proxy FormIDs and per-proxy desired speeds so the hook targets only proxy actors.

Wire animationGraphSpeed through networking: parse/send a new "animationGraphSpeed" field and store it on RemotePlayerState. Proxy animation sync now prefers the authoritative animationGraphSpeed when present, otherwise maps movementSpeed ~1:1 to graph Speed (with sprint floor and clamping). Added graph variables speedSampled/speedDamped alongside Speed and updated snapshot population and diagnostics. Removed character-controller velocity injection (was causing physics/animation conflicts) and adjusted proxy movement logic to rely on graph variables plus kinematic SetPosition.

Also: Publish puppet diagnostics from the controller, add sender-side local graph speed read diagnostic, update fake_player to compute and send animationGraphSpeed, and append detailed dev-log entries describing the root cause, diagnostics, and fixes. Build and runtime diagnostics added to verify hook behavior and cadence changes.
This commit is contained in:
2026-06-04 20:18:34 +12:00
parent 2aab83087f
commit abef899505
11 changed files with 658 additions and 89 deletions
+2 -1
View File
@@ -24,5 +24,6 @@ namespace F4T::Networking
bool a_isSneaking = false,
bool a_isJumping = false,
bool a_weaponDrawn = false,
float a_movementSpeed = 0.0F);
float a_movementSpeed = 0.0F,
float a_animationGraphSpeed = -1.0F);
}
+6
View File
@@ -22,9 +22,15 @@ namespace F4T::ProxyAnimationSync
{
inline constexpr const char* kSpeed = "Speed";
inline constexpr const char* kSpeedLower = "speed";
inline constexpr const char* kSpeedSampled = "speedSampled";
inline constexpr const char* kSpeedDamped = "speedDamped";
inline constexpr const char* kIsSprinting = "IsSprinting";
}
// Maps network movementSpeed (units/sec) to graph Speed (~0-105). Used when the sender
// does not provide animationGraphSpeed.
float MapMovementSpeedToGraphSpeed(float a_movementSpeed, bool a_isSprinting);
struct ProxyAppliedAnimationState
{
bool hasAppliedGraphState{ false };
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <cstdint>
#include <vector>
namespace F4T::ProxyPuppet
{
// Installs the Actor vtable hook that stops the engine from recomputing the
// locomotion graph Speed/Direction for networked proxy actors.
//
// Without this, Fallout 4 derives the graph "Speed" from the proxy's actual
// character-controller velocity every frame. Because proxies move via
// SetPosition (near-zero controller velocity), the engine overwrites whatever
// Speed we drive over the network, so every proxy plays a single fixed walk
// pace regardless of the remote player's real speed. This mirrors
// TiltedEvolution's puppet model (HookActorProcess), but is surgically scoped
// to just the Speed/Direction feedback write instead of the whole AI update.
//
// Safe to call repeatedly; the hook is installed at most once.
bool Install();
// Publishes the set of FormIDs that currently back active proxy actors. Called
// from the game-thread proxy controller each tick. Thread-safe.
void SetActiveProxyFormIDs(std::vector<std::uint32_t> a_formIDs);
// Records the locomotion graph Speed the proxy should hold. The Update hook
// re-asserts this immediately after the engine's per-frame actor update (which
// otherwise resets Speed to 0 on a SetPosition-driven actor). Negative speed
// means "no override" (idle). Thread-safe.
void SetProxyDesiredSpeed(std::uint32_t a_formID, float a_speed);
// Thread-safe membership test used by the vtable hook to gate proxies from
// real NPCs (which must keep their engine-driven locomotion).
bool IsProxyFormID(std::uint32_t a_formID);
// Diagnostics: total hook invocations (all actors) and proxy feedback skips
// since process start. Used to confirm the hook actually intercepts proxies.
void GetHookStats(std::uint64_t& a_outTotalCalls, std::uint64_t& a_outProxySkips);
}
+2
View File
@@ -40,6 +40,8 @@ namespace F4T::RemotePlayerState
bool isJumping{ false };
bool weaponDrawn{ false };
float movementSpeed{ 0.0F };
// Authoritative Havok graph Speed from sender when available (>= 0). -1 means unset.
float animationGraphSpeed{ -1.0F };
std::chrono::steady_clock::time_point lastReceivedLocalTime{};
// Phase 2: Actor state and action events (TiltedEvolution alignment)