From b04fca815463debd1aaccc369b22c61bd4e8006a Mon Sep 17 00:00:00 2001 From: Nomads_Reach <144523850+NomadsReach@users.noreply.github.com> Date: Sat, 15 Aug 2026 23:49:31 -0400 Subject: [PATCH] Prevent stale player action replay --- server/server_core.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/server/server_core.py b/server/server_core.py index 28f733c..bce703d 100644 --- a/server/server_core.py +++ b/server/server_core.py @@ -769,8 +769,16 @@ class FalloutTogetherServer: return False normalized["playerId"] = client.player_id normalized["serverTime"] = time.time() + durable_fields = ("equippedItems", "appearance", "characterName") with self._lock: - self._last_player_state_by_player_id[client.player_id] = dict(normalized) + if any(field in normalized for field in durable_fields): + cached = dict(self._last_player_state_by_player_id.get(client.player_id, {})) + cached.update({"type": "playerState", "playerId": client.player_id, "serverTime": normalized["serverTime"]}) + for field in durable_fields: + if field in normalized: + cached[field] = normalized[field] + cached.pop("actionEvents", None) + self._last_player_state_by_player_id[client.player_id] = cached self._stats["playerStatePacketsReceived"] += 1 self._broadcast_player_state(client, normalized) return True @@ -1177,7 +1185,29 @@ class FalloutTogetherServer: self._disconnect_client(recipient) def _broadcast_player_state(self, sender: ClientSession, packet: dict[str, Any]) -> None: - successful, failed = self._broadcast_to_active(packet, exclude=sender, v2_only=True) + with self._lock: + recipients = [ + client + for client in self._active_clients_locked() + if client.connection != sender.connection and client.protocol_version >= PROTOCOL_VERSION + ] + successful = 0 + failed: list[ClientSession] = [] + durable_fields = ("equippedItems", "appearance", "characterName") + sender_scope_known = _scope_from_state(sender.last_transform) is not None + for recipient in recipients: + relay_packet = dict(packet) + if "actionEvents" in relay_packet: + recipient_scope_known = _scope_from_state(recipient.last_transform) is not None + if not sender_scope_known or not recipient_scope_known or not states_share_interest(sender.last_transform, recipient.last_transform): + relay_packet.pop("actionEvents", None) + if "actionEvents" not in relay_packet and not any(field in relay_packet for field in durable_fields): + continue + try: + self._send_packet(recipient, relay_packet, broadcast=True) + successful += 1 + except (OSError, ValueError): + failed.append(recipient) with self._lock: self._stats["playerStatePacketsBroadcast"] += successful for client in failed: