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
+21
View File
@@ -48,6 +48,22 @@ ClientSnapshotProvider = Callable[[], list[dict[str, Any]]]
WalkTargetProvider = Callable[[int], dict[str, Any] | None]
def map_movement_speed_to_graph_speed(movement_speed: float, *, is_sprinting: bool = False) -> float:
"""Match plugin MapMovementSpeedToGraphSpeed.
FO4 graph "Speed" shares the same world-units/sec scale as movementSpeed, so the
mapping is ~1:1 (clamped). Confirmed from local-player graph debug: run movementSpeed
~306 lines up with graph Speed ~296-373.
"""
if movement_speed < 1.0:
return 0.0
graph_speed = movement_speed
if is_sprinting:
graph_speed = max(graph_speed, 350.0)
return min(graph_speed, 500.0)
class FakePlayerClient:
def __init__(
self,
@@ -537,6 +553,11 @@ class FakePlayerClient:
"weaponDrawn": False,
"movementSpeed": movement_speed,
}
if is_moving and movement_speed >= 1.0:
packet["animationGraphSpeed"] = round(
map_movement_speed_to_graph_speed(movement_speed),
1,
)
encoded = json.dumps(packet, separators=(",", ":")).encode("utf-8") + b"\n"
with self._send_lock:
connection.sendall(encoded)