fake_client: track players & handle disconnects

Add in-memory remote player state tracking and disconnect handling to fake_client.py: validate and store transform packets by playerId (position, angle, movementType, cellId, worldspaceId, clientTime, serverTime, lastReceivedLocalTime), print readable per-player summaries, and remove players on disconnect packets. Also add warning/log helpers and route packets to the new handlers. Update server/README.md and docs/dev-log.md to document the new behavior, test results, and next steps (clientTime verification and disconnect broadcasting).
This commit is contained in:
2026-05-31 19:19:18 +12:00
parent 32ed2aeedb
commit 848c36cd1e
3 changed files with 181 additions and 3 deletions
+63
View File
@@ -354,3 +354,66 @@ Player position: X=2048.00, Y=2048.00, Z=0.00, AngleZ=0.00
- Track remote players by `playerId`. - Track remote players by `playerId`.
- Store position, rotation, cellId, worldspaceId, movementType, and last update time. - Store position, rotation, cellId, worldspaceId, movementType, and last update time.
- Print a readable remote player state table. - Print a readable remote player state table.
## 2026-05-31
### What Changed
- Updated `fake_client.py` so it stores remote player state by `playerId`.
- Remote player state now tracks position, angle, movement type, cell ID, worldspace ID, server time, and local receive time.
- Tested the full relay flow from Fallout 4 plugin to server to fake client.
### What Worked
- The server received transform packets from the Fallout 4 plugin.
- The server assigned the Fallout 4 plugin a player ID.
- The server broadcast transform packets to the fake client.
- The fake client received transform packets.
- The fake client updated remote player state for player 2.
- Cell changes and worldspace metadata were preserved.
### What Broke
- Nothing recorded.
### Notes
- `clientTime` currently appears as `None` in fake client output.
- This should be investigated before building the Fallout-side receive loop.
- The fake client now models the kind of remote player state table that the eventual Fallout 4 receiving client will need.
### Next Steps
- Fix or verify `clientTime` in outgoing plugin transform packets.
- Add disconnect packet broadcasting so fake clients can remove disconnected remote players.
- Then begin planning a plugin receive loop for server messages.
## 2026-05-31
### What Changed
- Fixed `clientTime` in transform packets.
- Confirmed the Fallout 4 plugin sends `clientTime`.
- Confirmed the server preserves and broadcasts `clientTime`.
- Confirmed the fake client receives and stores `clientTime`.
### What Worked
- `clientTime` now appears in server output.
- `clientTime` now appears in fake client remote player state.
- `serverTime` is still added by the server.
- Remote player state now has both client-side and server-side timing data.
### What Broke
- Nothing recorded.
### Notes
- The packet timing fields are now suitable for later interpolation experiments.
- The next lifecycle feature should be disconnect broadcasting.
### Next Steps
- Add disconnect packets when a client leaves.
- Have fake clients remove remote players when disconnect packets are received.
+4 -2
View File
@@ -49,10 +49,12 @@ Expected result:
- The server broadcasts those transform packets to connected clients except the - The server broadcasts those transform packets to connected clients except the
sender. sender.
- `fake_client.py` receives and prints the broadcast transform packets. - `fake_client.py` receives and prints the broadcast transform packets.
- `fake_client.py` stores the latest remote player state for each `playerId`
in memory and prints a readable state summary after each transform update.
The fake client exists so broadcast behavior can be tested before coordinating a The fake client exists so broadcast behavior can be tested before coordinating a
second Fallout 4/F4SE instance. It is only a receiver for now and does not send second Fallout 4/F4SE instance. It stores remote player transform state only; it
movement. does not spawn remote actors or send movement.
## Current Server Goals ## Current Server Goals
+113
View File
@@ -2,24 +2,137 @@ from __future__ import annotations
import json import json
import socket import socket
import time
from typing import Any from typing import Any
HOST = "127.0.0.1" HOST = "127.0.0.1"
PORT = 7777 PORT = 7777
remote_players: dict[int, dict[str, Any]] = {}
def log(message: str) -> None: def log(message: str) -> None:
print(message, flush=True) print(message, flush=True)
def warn(message: str) -> None:
log(f"Warning: {message}")
def update_remote_player_state(packet: dict[str, Any]) -> None:
required_fields = (
"playerId",
"x",
"y",
"z",
"angleZ",
"movementType",
"cellId",
"worldspaceId",
)
missing_fields = [field_name for field_name in required_fields if field_name not in packet]
if missing_fields:
warn(f"Ignoring transform packet missing {', '.join(missing_fields)}: {packet}")
return
try:
player_id = int(packet["playerId"])
x = float(packet["x"])
y = float(packet["y"])
z = float(packet["z"])
angle_z = float(packet["angleZ"])
except (TypeError, ValueError) as error:
warn(f"Ignoring transform packet with invalid numeric fields: {error}: {packet}")
return
remote_players[player_id] = {
"playerId": player_id,
"x": x,
"y": y,
"z": z,
"angleZ": angle_z,
"movementType": packet["movementType"],
"cellId": packet["cellId"],
"worldspaceId": packet["worldspaceId"],
"clientTime": packet.get("clientTime"),
"serverTime": packet.get("serverTime"),
"lastReceivedLocalTime": time.time(),
}
log(f"Updated remote player {player_id}")
print_remote_player(remote_players[player_id])
def print_remote_player(player: dict[str, Any]) -> None:
client_time = player["clientTime"] if player["clientTime"] is not None else "missing from packet"
server_time = player["serverTime"] if player["serverTime"] is not None else "missing from packet"
log(
"\n".join(
[
f"Position: X={player['x']:.2f}, Y={player['y']:.2f}, Z={player['z']:.2f}",
f"AngleZ: {player['angleZ']:.2f}",
f"Movement Type: {player['movementType']}",
f"Cell: {player['cellId']}",
f"Worldspace: {player['worldspaceId']}",
f"Client Time: {client_time}",
f"Last Server Time: {server_time}",
f"Last Received Local Time: {player['lastReceivedLocalTime']}",
]
)
)
def print_remote_players() -> None:
if not remote_players:
log("Known remote players: none")
return
log("Known remote players:")
for player_id in sorted(remote_players):
player = remote_players[player_id]
log(
f"- Player {player_id}: "
f"pos=({player['x']:.2f}, {player['y']:.2f}, {player['z']:.2f}), "
f"angleZ={player['angleZ']:.2f}, movementType={player['movementType']}, "
f"cell={player['cellId']}, worldspace={player['worldspaceId']}, "
f"serverTime={player['serverTime']}"
)
def handle_disconnect_packet(packet: dict[str, Any]) -> None:
player_id = packet.get("playerId")
if player_id is None:
warn(f"Ignoring disconnect packet without playerId: {packet}")
return
try:
player_id = int(player_id)
except (TypeError, ValueError):
warn(f"Ignoring disconnect packet with invalid playerId: {packet}")
return
removed_player = remote_players.pop(player_id, None)
if removed_player is None:
log(f"Disconnect packet for unknown remote player {player_id}")
return
log(f"Removed remote player {player_id}")
print_remote_players()
def handle_packet(packet: dict[str, Any]) -> None: def handle_packet(packet: dict[str, Any]) -> None:
packet_type = packet.get("type") packet_type = packet.get("type")
if packet_type == "welcome": if packet_type == "welcome":
log(f"Welcome packet: {packet}") log(f"Welcome packet: {packet}")
elif packet_type == "transform": elif packet_type == "transform":
log(f"Received transform: {packet}") log(f"Received transform: {packet}")
update_remote_player_state(packet)
elif packet_type == "disconnect":
handle_disconnect_packet(packet)
else: else:
# TODO: Remove remote players when the server broadcasts disconnect packets.
log(f"Received packet: {packet}") log(f"Received packet: {packet}")