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:
+64
-1
@@ -353,4 +353,67 @@ Player position: X=2048.00, Y=2048.00, Z=0.00, AngleZ=0.00
|
||||
- Make `fake_client.py` store remote player state instead of only printing packets.
|
||||
- Track remote players by `playerId`.
|
||||
- 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
@@ -49,10 +49,12 @@ Expected result:
|
||||
- The server broadcasts those transform packets to connected clients except the
|
||||
sender.
|
||||
- `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
|
||||
second Fallout 4/F4SE instance. It is only a receiver for now and does not send
|
||||
movement.
|
||||
second Fallout 4/F4SE instance. It stores remote player transform state only; it
|
||||
does not spawn remote actors or send movement.
|
||||
|
||||
## Current Server Goals
|
||||
|
||||
|
||||
@@ -2,24 +2,137 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import socket
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
|
||||
HOST = "127.0.0.1"
|
||||
PORT = 7777
|
||||
|
||||
remote_players: dict[int, dict[str, Any]] = {}
|
||||
|
||||
|
||||
def log(message: str) -> None:
|
||||
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:
|
||||
packet_type = packet.get("type")
|
||||
if packet_type == "welcome":
|
||||
log(f"Welcome packet: {packet}")
|
||||
elif packet_type == "transform":
|
||||
log(f"Received transform: {packet}")
|
||||
update_remote_player_state(packet)
|
||||
elif packet_type == "disconnect":
|
||||
handle_disconnect_packet(packet)
|
||||
else:
|
||||
# TODO: Remove remote players when the server broadcasts disconnect packets.
|
||||
log(f"Received packet: {packet}")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user