Broadcast disconnects; update fake client
Implement server-side disconnect lifecycle and update client handling. Added disconnect_client and broadcast_disconnect in server.py to remove clients, close sockets, and notify remaining clients with a disconnect packet; broadcast_transform now uses disconnect_client for failed recipients and logs delivery counts. Updated fake_client.py to handle disconnect packets, remove remote players from its in-memory table, and improve logging. Documentation (docs/dev-log.md and server/README.md) updated to record the change and next steps.
This commit is contained in:
@@ -417,3 +417,36 @@ Player position: X=2048.00, Y=2048.00, Z=0.00, AngleZ=0.00
|
|||||||
|
|
||||||
- Add disconnect packets when a client leaves.
|
- Add disconnect packets when a client leaves.
|
||||||
- Have fake clients remove remote players when disconnect packets are received.
|
- Have fake clients remove remote players when disconnect packets are received.
|
||||||
|
|
||||||
|
## 2026-05-31
|
||||||
|
|
||||||
|
### What Changed
|
||||||
|
|
||||||
|
- Added server-side disconnect packet broadcasting.
|
||||||
|
- Updated the fake client to handle disconnect packets.
|
||||||
|
- Fake client now removes disconnected remote players from its remote player state table.
|
||||||
|
- Tested disconnect lifecycle using the Fallout 4 plugin and fake client.
|
||||||
|
|
||||||
|
### What Worked
|
||||||
|
|
||||||
|
- Server detected the Fallout 4 plugin disconnecting.
|
||||||
|
- Server removed the disconnected client.
|
||||||
|
- Server broadcast a disconnect packet to remaining clients.
|
||||||
|
- Fake client received the disconnect packet.
|
||||||
|
- Fake client removed remote player 2 from its state table.
|
||||||
|
- Fake client correctly reported that no remote players were currently tracked.
|
||||||
|
|
||||||
|
### What Broke
|
||||||
|
|
||||||
|
- Nothing recorded.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- The fake client reported `WinError 10054` after the server was stopped manually. This is expected during shutdown and is not a protocol issue.
|
||||||
|
- The basic networking lifecycle now works: connect, welcome, transform, disconnect, cleanup.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- Add protocol documentation for welcome, transform, and disconnect packets.
|
||||||
|
- Begin planning a Fallout 4 plugin receive loop.
|
||||||
|
- Store remote player state inside the plugin, without spawning actors yet.
|
||||||
@@ -51,6 +51,10 @@ Expected result:
|
|||||||
- `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`
|
- `fake_client.py` stores the latest remote player state for each `playerId`
|
||||||
in memory and prints a readable state summary after each transform update.
|
in memory and prints a readable state summary after each transform update.
|
||||||
|
- When a client disconnects, the server removes that client and broadcasts a
|
||||||
|
`disconnect` packet with the departed `playerId` to the remaining clients.
|
||||||
|
- `fake_client.py` removes disconnected players from its in-memory
|
||||||
|
`remote_players` table and prints the remaining tracked players.
|
||||||
|
|
||||||
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 stores remote player transform state only; it
|
second Fallout 4/F4SE instance. It stores remote player transform state only; it
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ def print_remote_player(player: dict[str, Any]) -> None:
|
|||||||
|
|
||||||
def print_remote_players() -> None:
|
def print_remote_players() -> None:
|
||||||
if not remote_players:
|
if not remote_players:
|
||||||
log("Known remote players: none")
|
log("No remote players currently tracked.")
|
||||||
return
|
return
|
||||||
|
|
||||||
log("Known remote players:")
|
log("Known remote players:")
|
||||||
@@ -115,10 +115,10 @@ def handle_disconnect_packet(packet: dict[str, Any]) -> None:
|
|||||||
|
|
||||||
removed_player = remote_players.pop(player_id, None)
|
removed_player = remote_players.pop(player_id, None)
|
||||||
if removed_player is None:
|
if removed_player is None:
|
||||||
log(f"Disconnect packet for unknown remote player {player_id}")
|
log(f"Received disconnect for unknown remote player {player_id}.")
|
||||||
return
|
return
|
||||||
|
|
||||||
log(f"Removed remote player {player_id}")
|
log(f"Removed remote player {player_id} after disconnect.")
|
||||||
print_remote_players()
|
print_remote_players()
|
||||||
|
|
||||||
|
|
||||||
@@ -132,7 +132,6 @@ def handle_packet(packet: dict[str, Any]) -> None:
|
|||||||
elif packet_type == "disconnect":
|
elif packet_type == "disconnect":
|
||||||
handle_disconnect_packet(packet)
|
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}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+43
-9
@@ -45,9 +45,9 @@ def assign_client(connection: socket.socket, address: tuple[str, int]) -> Client
|
|||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
||||||
def remove_client(client: Client) -> None:
|
def remove_client(client: Client) -> bool:
|
||||||
with clients_lock:
|
with clients_lock:
|
||||||
clients.pop(client.connection, None)
|
return clients.pop(client.connection, None) is not None
|
||||||
|
|
||||||
|
|
||||||
def send_packet(connection: socket.socket, packet: dict[str, Any]) -> None:
|
def send_packet(connection: socket.socket, packet: dict[str, Any]) -> None:
|
||||||
@@ -55,6 +55,19 @@ def send_packet(connection: socket.socket, packet: dict[str, Any]) -> None:
|
|||||||
connection.sendall(encoded)
|
connection.sendall(encoded)
|
||||||
|
|
||||||
|
|
||||||
|
def disconnect_client(client: Client) -> None:
|
||||||
|
if not remove_client(client):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
client.connection.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
log(f"Client disconnected: {client.label} (player {client.player_id})")
|
||||||
|
broadcast_disconnect(client)
|
||||||
|
|
||||||
|
|
||||||
def handle_client(connection: socket.socket, address: tuple[str, int]) -> None:
|
def handle_client(connection: socket.socket, address: tuple[str, int]) -> None:
|
||||||
client = assign_client(connection, address)
|
client = assign_client(connection, address)
|
||||||
log(f"Client connected: {client.label} (player {client.player_id})")
|
log(f"Client connected: {client.label} (player {client.player_id})")
|
||||||
@@ -86,8 +99,7 @@ def handle_client(connection: socket.socket, address: tuple[str, int]) -> None:
|
|||||||
except OSError as error:
|
except OSError as error:
|
||||||
log(f"Client connection error: {client.label} (player {client.player_id}): {error}")
|
log(f"Client connection error: {client.label} (player {client.player_id}): {error}")
|
||||||
finally:
|
finally:
|
||||||
remove_client(client)
|
disconnect_client(client)
|
||||||
log(f"Client disconnected: {client.label} (player {client.player_id})")
|
|
||||||
|
|
||||||
|
|
||||||
def handle_line(client: Client, line: str) -> None:
|
def handle_line(client: Client, line: str) -> None:
|
||||||
@@ -128,15 +140,37 @@ def broadcast_transform(sender: Client, packet: dict[str, Any]) -> None:
|
|||||||
failed_recipients.append(recipient)
|
failed_recipients.append(recipient)
|
||||||
|
|
||||||
for recipient in failed_recipients:
|
for recipient in failed_recipients:
|
||||||
remove_client(recipient)
|
disconnect_client(recipient)
|
||||||
try:
|
|
||||||
recipient.connection.close()
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
log(f"Broadcast transform from player {sender.player_id} to {len(recipients) - len(failed_recipients)} other client(s)")
|
log(f"Broadcast transform from player {sender.player_id} to {len(recipients) - len(failed_recipients)} other client(s)")
|
||||||
|
|
||||||
|
|
||||||
|
def broadcast_disconnect(disconnected_client: Client) -> None:
|
||||||
|
packet = {
|
||||||
|
"type": "disconnect",
|
||||||
|
"playerId": disconnected_client.player_id,
|
||||||
|
"serverTime": time.time(),
|
||||||
|
}
|
||||||
|
|
||||||
|
with clients_lock:
|
||||||
|
recipients = list(clients.values())
|
||||||
|
|
||||||
|
failed_recipients: list[Client] = []
|
||||||
|
for recipient in recipients:
|
||||||
|
try:
|
||||||
|
send_packet(recipient.connection, packet)
|
||||||
|
except OSError:
|
||||||
|
failed_recipients.append(recipient)
|
||||||
|
|
||||||
|
for recipient in failed_recipients:
|
||||||
|
disconnect_client(recipient)
|
||||||
|
|
||||||
|
log(
|
||||||
|
f"Broadcast disconnect for player {disconnected_client.player_id} "
|
||||||
|
f"to {len(recipients) - len(failed_recipients)} other client(s)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def print_packet(client: Client, packet: dict[str, Any]) -> None:
|
def print_packet(client: Client, packet: dict[str, Any]) -> None:
|
||||||
if packet.get("type") != "transform":
|
if packet.get("type") != "transform":
|
||||||
log(f"Packet from {client.label}: {packet}")
|
log(f"Packet from {client.label}: {packet}")
|
||||||
|
|||||||
Reference in New Issue
Block a user