Sync from GitHub main #1

Open
nomad wants to merge 145 commits from sync/from-github into main
Showing only changes of commit 5ad59d871c - Show all commits
+135 -1
View File
@@ -38,6 +38,21 @@ def recv_until(sock: socket.socket, packet_type: str, timeout: float = 2.0) -> d
raise AssertionError(f"did not receive packet type {packet_type}") raise AssertionError(f"did not receive packet type {packet_type}")
def recv_authority(
sock: socket.socket,
cell: str,
world: str = "0000003C",
timeout: float = 2.0,
) -> dict:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
remaining = max(0.01, deadline - time.monotonic())
packet = recv_until(sock, "npcAuthority", timeout=remaining)
if packet.get("authorityCellId") == cell and packet.get("authorityWorldspaceId", "") == world:
return packet
raise AssertionError(f"did not receive npcAuthority for {cell}/{world}")
def assert_no_packet_type(sock: socket.socket, packet_type: str, timeout: float = 0.2) -> None: def assert_no_packet_type(sock: socket.socket, packet_type: str, timeout: float = 0.2) -> None:
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
recv_until(sock, packet_type, timeout=timeout) recv_until(sock, packet_type, timeout=timeout)
@@ -76,6 +91,45 @@ def transform(
} }
def npc_state(
cell: str,
epoch: int,
x: float,
world: str = "0000003C",
source_form_id: str = "000000AA",
) -> dict:
return {
"type": "npcState",
"authorityEpoch": epoch,
"authorityCellId": cell,
"authorityWorldspaceId": world,
"npcs": [
{
"npcId": 1,
"baseFormId": "0000000F",
"sourceFormId": source_form_id,
"x": x,
"y": 0.0,
"z": 0.0,
"angleZ": 0.0,
"cellId": cell,
"worldspaceId": world,
"isDead": False,
}
],
}
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 start_server(max_players: int = 16) -> FalloutTogetherServer: def start_server(max_players: int = 16) -> FalloutTogetherServer:
server = FalloutTogetherServer(host="127.0.0.1", port=0, max_players=max_players) server = FalloutTogetherServer(host="127.0.0.1", port=0, max_players=max_players)
server.start() server.start()
@@ -179,6 +233,86 @@ def test_transform_interest_filters_distant_peer():
server.stop() server.stop()
def test_scoped_npc_authorities_publish_independently():
server = start_server()
a, a_welcome = connect(server)
hello(a)
b, b_welcome = connect(server)
hello(b)
send_packet(a, transform("00000010", 0.0, 0.0))
authority_a = recv_authority(a, "00000010")
assert authority_a["authorityPlayerId"] == a_welcome["playerId"]
send_packet(b, transform("00000020", 25000.0, 0.0))
authority_b = recv_authority(b, "00000020")
assert authority_b["authorityPlayerId"] == b_welcome["playerId"]
send_packet(a, npc_state("00000010", authority_a["authorityEpoch"], 0.0))
send_packet(b, npc_state("00000020", authority_b["authorityEpoch"], 25000.0, source_form_id="000000AB"))
assert wait_for_stat(server, "npcStatePacketsReceived", 2) == 2
assert server.get_stats()["npcAuthorityRejects"] == 0
a.close()
b.close()
server.stop()
def test_stale_npc_authority_epoch_cannot_publish_after_handoff():
server = start_server()
a, a_welcome = connect(server)
hello(a)
b, b_welcome = connect(server)
hello(b)
send_packet(a, transform("00000010", 0.0, 0.0))
initial_a = recv_authority(a, "00000010")
initial_b = recv_authority(b, "00000010")
assert initial_a["authorityPlayerId"] == a_welcome["playerId"]
assert initial_b["authorityEpoch"] == initial_a["authorityEpoch"]
send_packet(b, transform("00000010", 64.0, 0.0))
current_b = recv_authority(b, "00000010")
assert current_b["authorityPlayerId"] == a_welcome["playerId"]
assert current_b["authorityEpoch"] == initial_a["authorityEpoch"]
send_packet(a, npc_state("00000010", initial_a["authorityEpoch"], 0.0))
wait_for_stat(server, "npcStatePacketsReceived", 1)
send_packet(a, transform("00000020", 0.0, 0.0, movement_type="cell_change"))
handoff = recv_authority(b, "00000010")
assert handoff["authorityPlayerId"] == b_welcome["playerId"]
assert handoff["authorityEpoch"] > initial_a["authorityEpoch"]
send_packet(a, npc_state("00000010", initial_a["authorityEpoch"], 0.0))
wait_for_stat(server, "npcAuthorityRejects", 1)
assert server.get_stats()["npcStatePacketsReceived"] == 1
send_packet(b, npc_state("00000010", handoff["authorityEpoch"], 64.0, source_form_id="000000AB"))
wait_for_stat(server, "npcStatePacketsReceived", 2)
a.close()
b.close()
server.stop()
def test_scoped_npc_packet_rejects_entry_outside_authority_scope():
server = start_server()
sock, _ = connect(server)
hello(sock)
send_packet(sock, transform("00000010", 0.0, 0.0))
authority = recv_authority(sock, "00000010")
packet = npc_state("00000010", authority["authorityEpoch"], 0.0)
packet["npcs"][0]["cellId"] = "00000011"
send_packet(sock, packet)
wait_for_stat(server, "npcAuthorityRejects", 1)
assert server.get_stats()["npcStatePacketsReceived"] == 0
sock.close()
server.stop()
def test_rejected_normal_teleport_is_corrected_and_never_relayed(): def test_rejected_normal_teleport_is_corrected_and_never_relayed():
server = start_server() server = start_server()
a, a_welcome = connect(server) a, a_welcome = connect(server)
@@ -267,4 +401,4 @@ def test_oversized_unterminated_packet_closes_session():
ended = recv_until(sock, "sessionEnded") ended = recv_until(sock, "sessionEnded")
assert ended["code"] == "packet_too_large" assert ended["code"] == "packet_too_large"
sock.close() sock.close()
server.stop() server.stop()