Add basic movement state to transform packets
Add optional movement-state fields (isMoving, isSprinting, isSneaking, isJumping, weaponDrawn, movementSpeed) to transform packets and wire them end-to-end. Plugin changes: extend F4TNetworking API and RemotePlayerState, derive movement speed/jump state on the game-thread, validate values, include fields when formatting transform JSON, and add throttled remote movement-state logging. main.cpp adds sampling, speed/vertical calculations, jump hold logic, and change-detection to avoid extra sends. Networking parsing (F4TNetworking.cpp) reads optional booleans/floats safely, preserves backwards compatibility, and clears movement-state logs on disconnect. Proxy controller includes a TODO note for future animation use. Server and docs: update protocol and packet docs, dev-log, server README, and fake_client.py to parse/display optional fields safely. Also add several .cursor rule files for coding, documentation, protocol, project overview, and testing guidance. This milestone prepares the data model for later animation/behavior work while keeping existing relay behavior unchanged.
This commit is contained in:
@@ -9,6 +9,11 @@ packets, assigns incrementing `playerId` values, sends `welcome` packets, adds
|
||||
to every other connected client, and broadcasts disconnect packets when clients
|
||||
disconnect.
|
||||
|
||||
Transform packets may include optional movement-state fields such as
|
||||
`isMoving`, `isSprinting`, `isSneaking`, `isJumping`, `weaponDrawn`, and
|
||||
`movementSpeed`. The server preserves these fields automatically because it
|
||||
broadcasts the original transform packet after adding server-owned fields.
|
||||
|
||||
This is still a local prototype. It does not make Fallout 4 multiplayer
|
||||
playable yet.
|
||||
|
||||
@@ -51,6 +56,7 @@ Expected behavior:
|
||||
- The server receives transforms from the Fallout 4 plugin.
|
||||
- The server adds `playerId` and `serverTime` to each transform packet before
|
||||
broadcast.
|
||||
- The server preserves optional movement-state fields on transform packets.
|
||||
- The server broadcasts those transform packets to connected clients except the
|
||||
sender.
|
||||
- `fake_client.py` receives broadcast transform packets.
|
||||
@@ -72,6 +78,7 @@ does not spawn remote actors or send movement.
|
||||
- Send welcome packets
|
||||
- Receive transform packets
|
||||
- Add server timestamps
|
||||
- Preserve optional transform fields
|
||||
- Broadcast transforms to other connected clients
|
||||
- Broadcast disconnect packets
|
||||
- Handle disconnect cleanup
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import socket
|
||||
import time
|
||||
from typing import Any
|
||||
@@ -20,6 +21,25 @@ def warn(message: str) -> None:
|
||||
log(f"Warning: {message}")
|
||||
|
||||
|
||||
def get_optional_bool(packet: dict[str, Any], field_name: str, default: bool = False) -> bool:
|
||||
value = packet.get(field_name, default)
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return default
|
||||
|
||||
|
||||
def get_optional_float(packet: dict[str, Any], field_name: str, default: float = 0.0) -> float:
|
||||
value = packet.get(field_name, default)
|
||||
try:
|
||||
parsed_value = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
if not math.isfinite(parsed_value) or parsed_value < 0.0:
|
||||
return default
|
||||
return parsed_value
|
||||
|
||||
|
||||
def update_remote_player_state(packet: dict[str, Any]) -> None:
|
||||
required_fields = (
|
||||
"type",
|
||||
@@ -56,6 +76,12 @@ def update_remote_player_state(packet: dict[str, Any]) -> None:
|
||||
"worldspaceId": packet.get("worldspaceId", ""),
|
||||
"clientTime": packet.get("clientTime", 0.0),
|
||||
"serverTime": packet.get("serverTime", 0.0),
|
||||
"isMoving": get_optional_bool(packet, "isMoving"),
|
||||
"isSprinting": get_optional_bool(packet, "isSprinting"),
|
||||
"isSneaking": get_optional_bool(packet, "isSneaking"),
|
||||
"isJumping": get_optional_bool(packet, "isJumping"),
|
||||
"weaponDrawn": get_optional_bool(packet, "weaponDrawn"),
|
||||
"movementSpeed": get_optional_float(packet, "movementSpeed"),
|
||||
"lastReceivedLocalTime": time.time(),
|
||||
}
|
||||
|
||||
@@ -73,6 +99,12 @@ def print_remote_player(player: dict[str, Any]) -> None:
|
||||
f"Position: X={player['x']:.2f}, Y={player['y']:.2f}, Z={player['z']:.2f}",
|
||||
f"AngleZ: {player['angleZ']:.2f}",
|
||||
f"Movement Type: {player['movementType']}",
|
||||
(
|
||||
"Movement State: "
|
||||
f"moving={player['isMoving']}, sprinting={player['isSprinting']}, "
|
||||
f"sneaking={player['isSneaking']}, jumping={player['isJumping']}, "
|
||||
f"weaponDrawn={player['weaponDrawn']}, speed={player['movementSpeed']:.1f}"
|
||||
),
|
||||
f"Cell: {player['cellId']}",
|
||||
f"Worldspace: {player['worldspaceId']}",
|
||||
f"Client Time: {client_time}",
|
||||
@@ -95,6 +127,9 @@ def print_remote_players() -> None:
|
||||
f"- Player {player_id}: "
|
||||
f"pos=({player['x']:.2f}, {player['y']:.2f}, {player['z']:.2f}), "
|
||||
f"angleZ={player['angleZ']:.2f}, movementType={player['movementType']}, "
|
||||
f"moving={player['isMoving']}, sprinting={player['isSprinting']}, "
|
||||
f"sneaking={player['isSneaking']}, jumping={player['isJumping']}, "
|
||||
f"weaponDrawn={player['weaponDrawn']}, speed={player['movementSpeed']:.1f}, "
|
||||
f"cell={player['cellId']}, worldspace={player['worldspaceId']}, "
|
||||
f"serverTime={player['serverTime']}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user