Files
Commonwealth-Online-Server/server/tests/test_player_state_integration.py
T

185 lines
6.0 KiB
Python

from __future__ import annotations
import json
import socket
import time
import pytest
from server_core import PROTOCOL_VERSION, FalloutTogetherServer
_RECV_BUFFERS: dict[socket.socket, bytes] = {}
def recv_packet(sock: socket.socket, timeout: float = 2.0) -> dict:
sock.settimeout(timeout)
data = _RECV_BUFFERS.get(sock, b"")
while b"\n" not in data:
chunk = sock.recv(4096)
if not chunk:
raise ConnectionError("socket closed before a complete packet was received")
data += chunk
line, remainder = data.split(b"\n", 1)
_RECV_BUFFERS[sock] = remainder
return json.loads(line.decode("utf-8"))
def recv_until(sock: socket.socket, packet_type: str, timeout: float = 2.0) -> dict:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
remaining = max(0.01, deadline - time.monotonic())
try:
packet = recv_packet(sock, timeout=remaining)
except socket.timeout:
break
if packet.get("type") == packet_type:
return packet
raise AssertionError(f"did not receive packet type {packet_type}")
def send_packet(sock: socket.socket, packet: dict) -> None:
sock.sendall(json.dumps(packet, separators=(",", ":")).encode() + b"\n")
def start_server() -> FalloutTogetherServer:
server = FalloutTogetherServer(host="127.0.0.1", port=0)
server.start()
deadline = time.monotonic() + 2.0
while not server.is_running() and time.monotonic() < deadline:
time.sleep(0.01)
return server
def connect_v2(server: FalloutTogetherServer) -> tuple[socket.socket, dict]:
sock = socket.create_connection(("127.0.0.1", server.port), timeout=2.0)
welcome = recv_until(sock, "welcome")
send_packet(sock, {"type": "hello", "protocolVersion": PROTOCOL_VERSION})
ready = recv_until(sock, "sessionReady")
assert ready["playerId"] == welcome["playerId"]
return sock, welcome
def player_state(**overrides) -> dict:
packet = {
"type": "playerState",
"playerId": 999999,
"characterName": "Sole Survivor",
"equippedItems": [{"slot": "rightHand", "formId": "ff"}],
"appearance": {
"version": 4,
"raceFormId": "13746",
"height": 1.0,
"headParts": ["1a2b"],
},
"actionEvents": [{"sequence": 3, "type": 3, "eventName": "fireSingle"}],
}
packet.update(overrides)
return packet
def transform(**overrides) -> dict:
packet = {
"type": "transform",
"x": 0.0,
"y": 0.0,
"z": 0.0,
"angleZ": 0.0,
"cellId": "00000001",
"worldspaceId": "0000003C",
"movementType": "normal",
}
packet.update(overrides)
return packet
def wait_for_stat(server: FalloutTogetherServer, key: str, minimum: int, timeout: float = 2.0) -> int:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
value = int(server.get_stats()[key])
if value >= minimum:
return value
time.sleep(0.01)
raise AssertionError(f"stat {key} did not reach {minimum}")
def test_player_state_relay_uses_server_owned_identity():
server = start_server()
sender, sender_welcome = connect_v2(server)
receiver, _ = connect_v2(server)
try:
send_packet(sender, player_state())
relayed = recv_until(receiver, "playerState")
assert relayed["playerId"] == sender_welcome["playerId"]
assert relayed["characterName"] == "Sole Survivor"
assert relayed["equippedItems"] == [{"slot": "rightHand", "formId": "000000FF"}]
assert relayed["appearance"]["raceFormId"] == "00013746"
assert relayed["actionEvents"][0]["sequence"] == 3
wait_for_stat(server, "playerStatePacketsReceived", 1)
wait_for_stat(server, "playerStatePacketsBroadcast", 1)
finally:
sender.close()
receiver.close()
server.stop()
def test_v2_transform_strips_reliable_player_state_fields():
server = start_server()
sender, _ = connect_v2(server)
receiver, _ = connect_v2(server)
try:
send_packet(
sender,
transform(
equippedItems=[{"slot": "rightHand", "formId": "000000FF"}],
appearance={"version": 4, "raceFormId": "00013746"},
actionEvents=[{"sequence": 3, "type": 3, "eventName": "fireSingle"}],
characterName="must-not-ride-transform",
),
)
relayed = recv_until(receiver, "transform")
assert "equippedItems" not in relayed
assert "appearance" not in relayed
assert "actionEvents" not in relayed
assert "characterName" not in relayed
finally:
sender.close()
receiver.close()
server.stop()
def test_new_v2_client_receives_cached_player_state_before_any_transform():
server = start_server()
sender, sender_welcome = connect_v2(server)
receiver = None
try:
send_packet(sender, player_state(actionEvents=[]))
wait_for_stat(server, "playerStatePacketsReceived", 1)
receiver, _ = connect_v2(server)
cached = recv_until(receiver, "playerState")
assert cached["playerId"] == sender_welcome["playerId"]
assert cached["characterName"] == "Sole Survivor"
finally:
sender.close()
if receiver is not None:
receiver.close()
server.stop()
def test_malformed_player_state_is_rejected_before_relay():
server = start_server()
sender, _ = connect_v2(server)
receiver, _ = connect_v2(server)
try:
before = server.get_stats()["packetsRejected"]
send_packet(sender, player_state(equippedItems=[{"slot": "rightHand", "formId": "not-a-form"}]))
wait_for_stat(server, "packetsRejected", before + 1)
assert server.get_stats()["playerStatePacketsReceived"] == 0
with pytest.raises(AssertionError):
recv_until(receiver, "playerState", timeout=0.2)
finally:
sender.close()
receiver.close()
server.stop()