Sync from GitHub main #1
+94
-5
@@ -41,6 +41,18 @@ MAX_PACKETS_PER_SECOND = 120
|
||||
MAX_CONNECT_ATTEMPTS = 8
|
||||
CONNECT_ATTEMPT_WINDOW_SECONDS = 10.0
|
||||
EXTERIOR_INTEREST_RADIUS = 8192.0
|
||||
MAX_NORMAL_MOVEMENT_SPEED = 2500.0
|
||||
MOVEMENT_GRACE_DISTANCE = 512.0
|
||||
MAX_MOVEMENT_VALIDATION_ELAPSED_SECONDS = 5.0
|
||||
MOVEMENT_TRANSITION_TYPES = frozenset({
|
||||
"teleport",
|
||||
"cell_change",
|
||||
"worldspace_change",
|
||||
"load",
|
||||
"spawn",
|
||||
"fast_travel",
|
||||
})
|
||||
ALLOWED_MOVEMENT_TYPES = MOVEMENT_TRANSITION_TYPES | {"normal"}
|
||||
|
||||
_LOG_LEVELS = {
|
||||
"debug": 10,
|
||||
@@ -228,6 +240,8 @@ class FalloutTogetherServer:
|
||||
"transformPacketsReceived": 0,
|
||||
"transformPacketsBroadcast": 0,
|
||||
"transformPacketsInterestFiltered": 0,
|
||||
"movementPacketsRejected": 0,
|
||||
"movementCorrectionsSent": 0,
|
||||
"worldStatePacketsReceived": 0,
|
||||
"worldStatePacketsBroadcast": 0,
|
||||
"npcStatePacketsReceived": 0,
|
||||
@@ -589,7 +603,7 @@ class FalloutTogetherServer:
|
||||
"serverName": self.server_name,
|
||||
"serverDescription": self.server_description,
|
||||
"protocolVersion": PROTOCOL_VERSION,
|
||||
"capabilities": ["interest-v1", "hello-v2", "bounded-framing", "rate-limit-v1"],
|
||||
"capabilities": ["interest-v1", "hello-v2", "bounded-framing", "rate-limit-v1", "movement-correction-v1"],
|
||||
},
|
||||
)
|
||||
buffer = b""
|
||||
@@ -705,9 +719,21 @@ class FalloutTogetherServer:
|
||||
if normalized is None:
|
||||
self._reject_packet(client, "Malformed transform")
|
||||
return False
|
||||
accepted_monotonic = time.monotonic()
|
||||
movement_valid, movement_reason = self._validate_transform_movement(
|
||||
client,
|
||||
normalized,
|
||||
accepted_monotonic,
|
||||
)
|
||||
if not movement_valid:
|
||||
with self._lock:
|
||||
self._stats["movementPacketsRejected"] += 1
|
||||
self._reject_packet(client, movement_reason)
|
||||
self._send_position_correction(client, movement_reason)
|
||||
return False
|
||||
normalized["playerId"] = client.player_id
|
||||
normalized["serverTime"] = time.time()
|
||||
client.record_transform(normalized)
|
||||
client.record_transform(normalized, accepted_monotonic)
|
||||
with self._lock:
|
||||
self._stats["transformPacketsReceived"] += 1
|
||||
self._broadcast_transform(client, normalized)
|
||||
@@ -789,6 +815,10 @@ class FalloutTogetherServer:
|
||||
if not _is_hex_form_id(worldspace_id, allow_empty=True, allow_zero=True):
|
||||
return None
|
||||
|
||||
movement_type = packet.get("movementType", "normal")
|
||||
if not isinstance(movement_type, str) or movement_type not in ALLOWED_MOVEMENT_TYPES:
|
||||
return None
|
||||
|
||||
normalized = dict(packet)
|
||||
normalized["x"] = float(packet["x"])
|
||||
normalized["y"] = float(packet["y"])
|
||||
@@ -796,9 +826,7 @@ class FalloutTogetherServer:
|
||||
normalized["angleZ"] = float(packet["angleZ"])
|
||||
normalized["cellId"] = _normalize_hex_form_id(packet["cellId"])
|
||||
normalized["worldspaceId"] = _normalize_hex_form_id(worldspace_id) if worldspace_id else ""
|
||||
|
||||
movement_type = packet.get("movementType", "normal")
|
||||
normalized["movementType"] = str(movement_type)[:32] if isinstance(movement_type, str) else "normal"
|
||||
normalized["movementType"] = movement_type
|
||||
for field_name in ("movementSpeed", "animationGraphSpeed"):
|
||||
if field_name in packet:
|
||||
value = packet[field_name]
|
||||
@@ -826,6 +854,67 @@ class FalloutTogetherServer:
|
||||
normalized["actionEvents"] = _normalize_action_events(packet["actionEvents"])
|
||||
return normalized
|
||||
|
||||
def _validate_transform_movement(
|
||||
self,
|
||||
client: ClientSession,
|
||||
packet: dict[str, Any],
|
||||
now_monotonic: float,
|
||||
) -> tuple[bool, str]:
|
||||
previous, previous_monotonic = client.get_last_transform_anchor()
|
||||
if previous is None or previous_monotonic is None:
|
||||
return True, "first transform"
|
||||
|
||||
movement_type = packet.get("movementType", "normal")
|
||||
if movement_type in MOVEMENT_TRANSITION_TYPES:
|
||||
return True, f"explicit {movement_type} transition"
|
||||
|
||||
previous_cell = previous.get("cellId", "")
|
||||
current_cell = packet.get("cellId", "")
|
||||
previous_world = previous.get("worldspaceId", "")
|
||||
current_world = packet.get("worldspaceId", "")
|
||||
if previous_cell != current_cell or previous_world != current_world:
|
||||
return False, "scope changed without an explicit movement transition"
|
||||
|
||||
elapsed = now_monotonic - previous_monotonic
|
||||
if not math.isfinite(elapsed) or elapsed < 0.0:
|
||||
elapsed = 0.0
|
||||
elapsed = min(elapsed, MAX_MOVEMENT_VALIDATION_ELAPSED_SECONDS)
|
||||
|
||||
dx = packet["x"] - previous["x"]
|
||||
dy = packet["y"] - previous["y"]
|
||||
dz = packet["z"] - previous["z"]
|
||||
distance = math.sqrt((dx * dx) + (dy * dy) + (dz * dz))
|
||||
allowed_distance = MOVEMENT_GRACE_DISTANCE + (MAX_NORMAL_MOVEMENT_SPEED * elapsed)
|
||||
if not math.isfinite(distance) or distance > allowed_distance:
|
||||
return (
|
||||
False,
|
||||
f"normal movement exceeded server envelope: distance={distance:.1f}, allowed={allowed_distance:.1f}, elapsed={elapsed:.3f}s",
|
||||
)
|
||||
return True, "normal movement accepted"
|
||||
|
||||
def _send_position_correction(self, client: ClientSession, reason: str) -> None:
|
||||
previous, _previous_monotonic = client.get_last_transform_anchor()
|
||||
if previous is None:
|
||||
return
|
||||
packet: dict[str, Any] = {
|
||||
"type": "positionCorrection",
|
||||
"reason": reason[:160],
|
||||
"x": previous["x"],
|
||||
"y": previous["y"],
|
||||
"z": previous["z"],
|
||||
"angleZ": previous["angleZ"],
|
||||
"cellId": previous["cellId"],
|
||||
"worldspaceId": previous.get("worldspaceId", ""),
|
||||
"serverTime": time.time(),
|
||||
}
|
||||
try:
|
||||
self._send_packet(client, packet)
|
||||
except (OSError, ValueError):
|
||||
self._disconnect_client(client)
|
||||
return
|
||||
with self._lock:
|
||||
self._stats["movementCorrectionsSent"] += 1
|
||||
|
||||
def _normalize_world_state_packet(self, packet: dict[str, Any]) -> dict[str, Any] | None:
|
||||
normalized = dict(packet)
|
||||
if "gameHour" in packet:
|
||||
|
||||
Reference in New Issue
Block a user