Sync from GitHub main #1
@@ -0,0 +1,60 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
MAX_SEQUENCE = 0xFFFFFFFF
|
||||||
|
_HALF_RANGE = 0x80000000
|
||||||
|
|
||||||
|
|
||||||
|
def next_sequence(current: int) -> int:
|
||||||
|
if not isinstance(current, int) or isinstance(current, bool) or not 0 <= current <= MAX_SEQUENCE:
|
||||||
|
raise ValueError("snapshot sequence must be an unsigned 32-bit integer")
|
||||||
|
value = (current + 1) & MAX_SEQUENCE
|
||||||
|
return 1 if value == 0 else value
|
||||||
|
|
||||||
|
|
||||||
|
def is_newer(candidate: int, baseline: int) -> bool:
|
||||||
|
if not isinstance(candidate, int) or isinstance(candidate, bool):
|
||||||
|
return False
|
||||||
|
if not isinstance(baseline, int) or isinstance(baseline, bool):
|
||||||
|
return False
|
||||||
|
if not 0 <= candidate <= MAX_SEQUENCE or not 0 <= baseline <= MAX_SEQUENCE:
|
||||||
|
return False
|
||||||
|
if candidate == 0:
|
||||||
|
return False
|
||||||
|
if baseline == 0:
|
||||||
|
return True
|
||||||
|
delta = (candidate - baseline) & MAX_SEQUENCE
|
||||||
|
return 0 < delta < _HALF_RANGE
|
||||||
|
|
||||||
|
|
||||||
|
class SequenceCounter:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._value = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current(self) -> int:
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
def advance(self) -> int:
|
||||||
|
self._value = next_sequence(self._value)
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self._value = 0
|
||||||
|
|
||||||
|
|
||||||
|
class SequenceWindow:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._last_accepted = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_accepted(self) -> int:
|
||||||
|
return self._last_accepted
|
||||||
|
|
||||||
|
def accept(self, sequence: int) -> bool:
|
||||||
|
if not is_newer(sequence, self._last_accepted):
|
||||||
|
return False
|
||||||
|
self._last_accepted = sequence
|
||||||
|
return True
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self._last_accepted = 0
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from snapshot_sequence import MAX_SEQUENCE, SequenceCounter, SequenceWindow, is_newer, next_sequence
|
||||||
|
|
||||||
|
|
||||||
|
def test_next_sequence_skips_zero_after_wrap():
|
||||||
|
assert next_sequence(0) == 1
|
||||||
|
assert next_sequence(1) == 2
|
||||||
|
assert next_sequence(MAX_SEQUENCE) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_newer_is_wrap_safe():
|
||||||
|
assert is_newer(1, 0)
|
||||||
|
assert is_newer(2, 1)
|
||||||
|
assert not is_newer(1, 1)
|
||||||
|
assert not is_newer(1, 2)
|
||||||
|
assert is_newer(1, MAX_SEQUENCE)
|
||||||
|
assert not is_newer(MAX_SEQUENCE, 1)
|
||||||
|
assert not is_newer(0, MAX_SEQUENCE)
|
||||||
|
|
||||||
|
|
||||||
|
def test_counter_and_window_enforce_latest_wins():
|
||||||
|
counter = SequenceCounter()
|
||||||
|
assert counter.current == 0
|
||||||
|
assert counter.advance() == 1
|
||||||
|
assert counter.advance() == 2
|
||||||
|
counter.reset()
|
||||||
|
assert counter.advance() == 1
|
||||||
|
|
||||||
|
window = SequenceWindow()
|
||||||
|
assert window.accept(1)
|
||||||
|
assert not window.accept(1)
|
||||||
|
assert window.accept(2)
|
||||||
|
assert not window.accept(1)
|
||||||
|
window.reset()
|
||||||
|
assert window.accept(MAX_SEQUENCE)
|
||||||
|
assert window.accept(1)
|
||||||
|
assert not window.accept(MAX_SEQUENCE)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_sequence_inputs_are_rejected():
|
||||||
|
assert not is_newer(-1, 0)
|
||||||
|
assert not is_newer(1, -1)
|
||||||
|
assert not is_newer(True, 0)
|
||||||
|
assert not is_newer(1, MAX_SEQUENCE + 1)
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
from transport_policy import Delivery, delivery_for_packet_type, is_snapshot_packet
|
from transport_policy import (
|
||||||
|
Delivery,
|
||||||
|
delivery_for_packet_type,
|
||||||
|
is_snapshot_packet,
|
||||||
|
requires_application_sequence,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_snapshot_packets_use_unreliable_sequenced_delivery():
|
def test_snapshot_packets_use_unreliable_sequenced_delivery():
|
||||||
@@ -6,6 +11,8 @@ def test_snapshot_packets_use_unreliable_sequenced_delivery():
|
|||||||
assert delivery_for_packet_type("npcState") is Delivery.UNRELIABLE_SEQUENCED
|
assert delivery_for_packet_type("npcState") is Delivery.UNRELIABLE_SEQUENCED
|
||||||
assert is_snapshot_packet("transform")
|
assert is_snapshot_packet("transform")
|
||||||
assert is_snapshot_packet("npcState")
|
assert is_snapshot_packet("npcState")
|
||||||
|
assert requires_application_sequence("transform")
|
||||||
|
assert requires_application_sequence("npcState")
|
||||||
|
|
||||||
|
|
||||||
def test_gameplay_and_control_packets_use_reliable_ordered_delivery():
|
def test_gameplay_and_control_packets_use_reliable_ordered_delivery():
|
||||||
@@ -25,7 +32,9 @@ def test_gameplay_and_control_packets_use_reliable_ordered_delivery():
|
|||||||
for packet_type in reliable_types:
|
for packet_type in reliable_types:
|
||||||
assert delivery_for_packet_type(packet_type) is Delivery.RELIABLE_ORDERED
|
assert delivery_for_packet_type(packet_type) is Delivery.RELIABLE_ORDERED
|
||||||
assert not is_snapshot_packet(packet_type)
|
assert not is_snapshot_packet(packet_type)
|
||||||
|
assert not requires_application_sequence(packet_type)
|
||||||
|
|
||||||
|
|
||||||
def test_unknown_packet_types_never_default_to_unreliable():
|
def test_unknown_packet_types_never_default_to_unreliable():
|
||||||
assert delivery_for_packet_type("futureControlPacket") is Delivery.RELIABLE_ORDERED
|
assert delivery_for_packet_type("futureControlPacket") is Delivery.RELIABLE_ORDERED
|
||||||
|
assert not requires_application_sequence("futureControlPacket")
|
||||||
|
|||||||
@@ -26,3 +26,13 @@ def delivery_for_packet_type(packet_type: str) -> Delivery:
|
|||||||
|
|
||||||
def is_snapshot_packet(packet_type: str) -> bool:
|
def is_snapshot_packet(packet_type: str) -> bool:
|
||||||
return delivery_for_packet_type(packet_type) is Delivery.UNRELIABLE_SEQUENCED
|
return delivery_for_packet_type(packet_type) is Delivery.UNRELIABLE_SEQUENCED
|
||||||
|
|
||||||
|
|
||||||
|
def requires_application_sequence(packet_type: str) -> bool:
|
||||||
|
"""Return whether the protocol must reject stale copies after reordering.
|
||||||
|
|
||||||
|
GameNetworkingSockets can deliver unreliable messages without retransmit,
|
||||||
|
but the application still owns latest-wins snapshot semantics. Sequence
|
||||||
|
numbers are therefore required for every unreliable snapshot family.
|
||||||
|
"""
|
||||||
|
return is_snapshot_packet(packet_type)
|
||||||
|
|||||||
Reference in New Issue
Block a user