Sync from GitHub main #1
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user