D1: relay-assigned seats -- players never need a player number
A pod launched with no BT_SELF asks the relay for a seat before joining: new control frames SEAT_REQUEST (-6) -> relay reserves the lowest roster seat not claimed or reserved (60s reservation so simultaneous joiners can't race onto one seat) -> SEAT_ASSIGN (-7, int32 hostID + NUL tag) becomes relaySelf; everything downstream (egg self-match, HELLO) runs exactly as if BT_SELF had been set. Roster full -> SEAT_FULL (-8) -> clean Fail(). A real HELLO pops the reservation; a pod drop frees the seat. Explicit BT_SELF still claims a specific seat (the operator's local launches use it). Client: L4NetworkManager::RelayRequestSeat (throwaway TCP dial to the relay game port, 10s reply window). Relay: SEAT_REQUEST branch in _handle_game_frame. Operator exporter collapsed from per-seat join_as_playerN.bat to ONE universal join.bat (+ join_lan.bat with BT_RELAY=auto) -- every player gets the same file, first come first served, the arcade walk-up-to-a-pod model. players/ regenerated. Verified: 8/8 seat stub tests (distinct seats in roster order, FULL on exhaustion, claimed+reserved stays FULL, HELLO claim accepted, duplicate HELLO refused); 2-node localhost e2e with NO BT_SELF on either pod -> both seated, full ladder, RunMission pair, UDP flowing post-launch; regression smoke: explicit-BT_SELF relay session clean with zero seat requests, classic mesh (no BT_RELAY) un-regressed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
efd440c64b
commit
f04e8019c2
@@ -184,6 +184,10 @@ ROUTE_HELLO = -2
|
||||
ROUTE_PEER_UP = -3
|
||||
ROUTE_PEER_DOWN = -4
|
||||
ROUTE_UDP_ACK = -5
|
||||
ROUTE_SEAT_REQUEST = -6 # client->relay: assign me a free roster seat
|
||||
ROUTE_SEAT_ASSIGN = -7 # relay->client: int32 hostID + NUL-terminated tag
|
||||
ROUTE_SEAT_FULL = -8 # relay->client: no free seats
|
||||
SEAT_RESERVE_SECONDS = 60.0
|
||||
HELLO_MAGIC = 0x31525442 # 'BTR1' little-endian
|
||||
FRAME_CAP = 1600 # NETWORKMANAGER_BUFFER_SIZE (NETWORK.h:89)
|
||||
FIRST_GAME_HOST_ID = 2 # FirstLegalHostID(1) == the console; pods 2..N+1
|
||||
@@ -257,6 +261,7 @@ class Relay:
|
||||
self.console_conns = []
|
||||
self.game_conns = [] # RelayGameConn (incl. unregistered)
|
||||
self.by_host = {} # hostID -> RelayGameConn
|
||||
self.seat_reservations = {} # hostID -> expiry (auto-seats)
|
||||
self.udp_endpoint = {} # hostID -> (addr, port)
|
||||
self.udp_sock = None
|
||||
self.launch_at = None
|
||||
@@ -472,6 +477,37 @@ class Relay:
|
||||
return
|
||||
|
||||
def _handle_game_frame(self, conn, route, payload):
|
||||
if conn.host_id is None and route == ROUTE_SEAT_REQUEST:
|
||||
# SERVER-ASSIGNED SEATS: hand out the lowest roster seat that is
|
||||
# neither claimed (by_host) nor reserved; reserve it briefly so
|
||||
# two simultaneous joiners can't race onto the same seat. The
|
||||
# requesting connection closes after the reply; the pod re-dials
|
||||
# and claims the seat with a normal HELLO.
|
||||
now = time.time()
|
||||
self.seat_reservations = {h: t for h, t in
|
||||
self.seat_reservations.items() if t > now}
|
||||
assigned = None
|
||||
for i, tag in enumerate(self.roster):
|
||||
host_id = FIRST_GAME_HOST_ID + i
|
||||
if host_id in self.by_host or host_id in self.seat_reservations:
|
||||
continue
|
||||
assigned = (host_id, tag)
|
||||
break
|
||||
if assigned is None:
|
||||
self._send_raw(conn, struct.pack(ENV_FMT_TCP,
|
||||
ROUTE_SEAT_FULL, 0))
|
||||
print(f"[relay] {conn.name()} seat request: ROSTER FULL",
|
||||
flush=True)
|
||||
return
|
||||
host_id, tag = assigned
|
||||
self.seat_reservations[host_id] = now + SEAT_RESERVE_SECONDS
|
||||
payload_out = struct.pack("<i", host_id) + tag.encode() + b"\0"
|
||||
self._send_raw(conn, struct.pack(ENV_FMT_TCP, ROUTE_SEAT_ASSIGN,
|
||||
len(payload_out)) + payload_out)
|
||||
print(f"[relay] {conn.name()} seat request -> assigned host "
|
||||
f"{host_id} '{tag}' (reserved {SEAT_RESERVE_SECONDS:.0f}s)",
|
||||
flush=True)
|
||||
return
|
||||
if conn.host_id is None:
|
||||
# First frame MUST be a valid HELLO.
|
||||
if route != ROUTE_HELLO or len(payload) < 8:
|
||||
@@ -491,6 +527,7 @@ class Relay:
|
||||
return
|
||||
conn.host_id = host_id
|
||||
self.by_host[host_id] = conn
|
||||
self.seat_reservations.pop(host_id, None) # claim clears reserve
|
||||
print(f"[relay] {conn.name()} REGISTERED "
|
||||
f"({len(self.by_host)}/{len(self.roster)})", flush=True)
|
||||
# PEER_UP exchange: newcomer learns everyone; everyone learns newcomer.
|
||||
|
||||
Reference in New Issue
Block a user