Fix seat-collision: reserve the operator's LOCAL seats from auto-assign

FIELD BUG (2026-07-18): operator's local instance crashed + the mission
never launched.  Root cause (confirmed by code inspection + stub test,
NOT the earlier mis-read of stale logs): the relay assigns a no-BT_SELF
joiner 'the lowest free seat'.  The operator's local instance uses an
EXPLICIT seat (BT_SELF) but takes ~15s to boot; a remote join.bat
requests a seat immediately and gets assigned the operator's seat 1
first.  The operator's later HELLO for seat 1 -> 'already registered'
-> dropped ('closed by relay') -> its seat sits empty -> the
All-connections-completed gate never fires -> game never starts.

Fix: the operator's LOCAL seats are RESERVED.  btoperator passes the
Local roster tags as ; the
relay excludes those host_ids from seat ASSIGNMENT (an explicit HELLO
still claims them).  Verified by stub test: a racing joiner is assigned
host 3 (seat 1 skipped), the operator's HELLO for the reserved seat 1
registers fine.

Also fixed the UX bug that produced the EARLIER 4-window mess: Local
now defaults checked only on the operator's own seat (row 0), not every
relay roster row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-18 18:25:19 -05:00
co-authored by Claude Fable 5
parent d26d0375ae
commit 196112f2dc
2 changed files with 43 additions and 7 deletions
+18 -3
View File
@@ -418,7 +418,10 @@ class Operator(QMainWindow):
p = self._pilot_template()
p["address"] = address
p.pop("name", None) # new row gets the PLAYERn default callsign
self._add_pilot_row(p, local=(self.mode.currentIndex() == 0))
# a NEWLY-added pilot is a remote joiner by default (Local only on the
# operator's own seat, normally row 0 which the egg load already sets)
self._add_pilot_row(p, local=(self.mode.currentIndex() == 0
and self.table.rowCount() == 0))
self._renumber_relay_tags()
def _remove_pilot(self):
@@ -458,8 +461,12 @@ class Operator(QMainWindow):
self.f_length.setText(m.get("length") or "")
self.table.setRowCount(0)
relay = self.mode.currentIndex() == 0
for p in self.egg.pilots():
self._add_pilot_row(p, local=relay)
# LOCAL default: ONLY the operator's own seat (row 0) -- NOT every row.
# (Checking Local on all rows made "Launch local instances" spawn one
# full game client PER seat on this machine, which collided and
# crashed -- field report 2026-07-18.) Remote joiners stay unchecked.
for i, p in enumerate(self.egg.pilots()):
self._add_pilot_row(p, local=(relay and i == 0))
self._renumber_relay_tags()
self.egg_label.setText(os.path.basename(self.egg_path))
@@ -545,6 +552,14 @@ class Operator(QMainWindow):
args += ["--relay", str(self.f_port.value()), self.egg_path]
if self.f_manual.isChecked():
args += ["--manual-launch"]
# RESERVE the operator's LOCAL seats so a remote join.bat can't be
# assigned one before the (slow-to-boot) local instance claims it
# -- the seat-collision that dropped the operator's pod + stalled
# the launch (field report 2026-07-18).
local_tags = [tags[r] for r in range(self.table.rowCount())
if self._row_local(r) and r < len(tags)]
if local_tags:
args += ["--reserve", ",".join(local_tags)]
else:
# mesh: console dials each pod's CONSOLE port (= game port - 1)
args += [self.egg_path]