Merge remote-tracking branch 'origin/master' into glass-cockpit

This commit is contained in:
Cyd
2026-07-18 20:05:58 -05:00
6 changed files with 213 additions and 59 deletions
+28 -6
View File
@@ -1,11 +1,33 @@
@echo off
rem BT411 -- join 107.202.218.169's game (internet). Your seat/mech
rem is assigned automatically by the operator console.
rem BT411 -- join 107.202.218.169's game (internet). Seat/mech assigned
rem by the operator. Keep this file next to content\ + build\.
cd /d %~dp0
if not exist "build\Release\btl4.exe" goto badpath
if not exist "content\OPERATOR.EGG" goto badpath
set BT_RELAY=107.202.218.169:1500
cd /d %~dp0content
set BT_LOG=join.log
set BT_START_INSIDE=1
set BT_DEV_GAUGES=1
cd content
..\build\Release\btl4.exe -egg OPERATOR.EGG -net 1501
echo.
echo The game has exited. If you did NOT quit on purpose,
echo the session may not be running yet or the server was
echo unreachable -- check with the operator and try again.
echo The game has exited. If you did NOT quit on purpose
echo (crash, or it never connected), send the operator the
echo file content\join.log so they can see what happened.
echo Otherwise the session may not be up yet -- try again.
pause
exit /b
:badpath
echo.
echo ==================================================
echo ERROR: this file is not in the right place.
echo.
echo It must sit in the EXTRACTED game folder, next to
echo the 'content' and 'build' folders.
echo.
echo Fix: right-click the .zip - Extract All, open the
echo extracted folder, and run this file from THERE.
echo (Do NOT run it from inside the zip.)
echo ==================================================
echo.
pause
+27 -6
View File
@@ -1,12 +1,33 @@
@echo off
rem BT411 -- join the game from the operator's OWN
rem network (LAN) -- finds the console automatically.
rem BT411 -- join the operator's LAN game (auto-discovers).
rem Joining over the INTERNET? Use join.bat instead.
cd /d %~dp0
if not exist "build\Release\btl4.exe" goto badpath
if not exist "content\OPERATOR.EGG" goto badpath
set BT_RELAY=auto
cd /d %~dp0content
set BT_LOG=join.log
set BT_START_INSIDE=1
set BT_DEV_GAUGES=1
cd content
..\build\Release\btl4.exe -egg OPERATOR.EGG -net 1501
echo.
echo The game has exited. If you did NOT quit on purpose,
echo the session may not be running yet or the server was
echo unreachable -- check with the operator and try again.
echo The game has exited. If you did NOT quit on purpose
echo (crash, or it never connected), send the operator the
echo file content\join.log so they can see what happened.
echo Otherwise the session may not be up yet -- try again.
pause
exit /b
:badpath
echo.
echo ==================================================
echo ERROR: this file is not in the right place.
echo.
echo It must sit in the EXTRACTED game folder, next to
echo the 'content' and 'build' folders.
echo.
echo Fix: right-click the .zip - Extract All, open the
echo extracted folder, and run this file from THERE.
echo (Do NOT run it from inside the zip.)
echo ==================================================
echo.
pause
+23 -5
View File
@@ -1,11 +1,29 @@
@echo off
rem BT411 -- single-player practice mission (no network,
rem no operator needed). Edit the -egg name below to try
rem other maps (ARENA1/ARENA2/DBASE/FOGDAY/...).
rem BT411 -- single-player practice (no network, no operator).
rem Edit the -egg below for other maps (ARENA2/DBASE/FOGDAY/...).
cd /d %~dp0
if not exist "build\Release\btl4.exe" goto badpath
if not exist "content\OPERATOR.EGG" goto badpath
set BT_START_INSIDE=1
cd /d %~dp0content
set BT_LOG=join.log
cd content
..\build\Release\btl4.exe -egg ARENA1.EGG
echo.
echo The game has exited. If it closed unexpectedly,
echo send the operator the file content\btl4.log.
echo send the operator content\join.log.
pause
exit /b
:badpath
echo.
echo ==================================================
echo ERROR: this file is not in the right place.
echo.
echo It must sit in the EXTRACTED game folder, next to
echo the 'content' and 'build' folders.
echo.
echo Fix: right-click the .zip - Extract All, open the
echo extracted folder, and run this file from THERE.
echo (Do NOT run it from inside the zip.)
echo ==================================================
echo.
pause
+61 -7
View File
@@ -284,6 +284,7 @@ class Relay:
self.manual_launch = manual_launch
self._reserved_tags = set(reserved_tags or ())
self.launch_requested = False # set by the operator (stdin)
self._launch_blocked_warned = False # warned once about empty seats
self.stop_requested = False # operator 'stop' (End Mission)
self.mission_length = parse_egg_mission_length(egg_path)
self.mission_started_at = None # set when RunMission #2 fires
@@ -413,6 +414,26 @@ class Relay:
return sum(1 for c in self.console_conns if c.acked)
def _check_launch_gate(self):
# BACK-TO-BACK MISSIONS (2026-07-18): a previous mission finished
# (launches_sent==2) and now all seats have RE-ACKED -- i.e. every pod
# exited, re-ran join.bat, and reconnected fresh. Reset the launch
# state so the relay (and the operator's LAUNCH button) re-arm for a
# new round -- no Stop/Start needed. The binary needs nothing: a
# rejoined pod is a brand-new process at WaitingForLaunch. (Only ever
# reached on a pod ACK, which never happens mid-mission.)
if self.launches_sent >= 2 and self._pods_ready() >= len(self.roster):
print("[relay] all seats rejoined after the last mission -- "
"re-arming for a NEW mission", flush=True)
self.launches_sent = 0
self.stop_sent = False
self.stop_requested = False
self.mission_started_at = None
self.launch_at = None
self.launch_requested = False
self._launch_blocked_warned = False
self.eggs_done_at = None
# fall through -> the normal gate re-prints WAITING FOR OPERATOR
# GLOBAL launch: arm the timer when the LAST pod has ACKED its egg
# (auto mode), or wait for the operator's 'launch' command (manual
# mode -- the operator app's Launch button writes it to our stdin).
@@ -463,18 +484,46 @@ class Relay:
if conn in self.console_conns:
self.console_conns.remove(conn)
def _log_launch_readiness(self):
# PRE-LAUNCH READINESS (2026-07-18): make the "launching short" footgun
# VISIBLE. A roster seat with no registered pod means the mission will
# STALL -- every pod waits on that empty seat's connection and the
# All-connections-completed gate never fires. Log filled vs empty
# seats so the operator sees it BEFORE the confusion.
filled, empty = [], []
for i, tag in enumerate(self.roster):
host_id = FIRST_GAME_HOST_ID + i
(filled if host_id in self.by_host else empty).append(
f"seat{i + 1}({tag})")
print(f"[relay] LAUNCH readiness: {len(filled)}/{len(self.roster)} "
f"seats filled -- {', '.join(filled) or 'NONE'}", flush=True)
if empty:
print(f"[relay] *** WARNING: {len(empty)} EMPTY seat(s): "
f"{', '.join(empty)} -- the mission will STALL (every pod "
f"waits on the empty seat). Stop, reduce the roster to the "
f"players present, and re-launch.", flush=True)
def _tick_launch(self):
# Manual mode: the operator's 'launch' arms the timer, still honouring
# the settle window (pods must reach WaitingForLaunch or the handler
# Fail()s -- the same reason the auto timer waits).
if (self.manual_launch and self.launch_requested
and self.launch_at is None and self.eggs_done_at is not None
and self.launches_sent == 0):
self.launch_at = max(time.time(),
self.eggs_done_at + LAUNCH_SETTLE_SECONDS)
wait = max(0.0, self.launch_at - time.time())
print(f"[relay] operator LAUNCH received; firing in {wait:.0f}s",
flush=True)
and self.launch_at is None and self.launches_sent == 0):
if self.eggs_done_at is not None:
self.launch_at = max(time.time(),
self.eggs_done_at + LAUNCH_SETTLE_SECONDS)
wait = max(0.0, self.launch_at - time.time())
print(f"[relay] operator LAUNCH received; firing in "
f"{wait:.0f}s", flush=True)
self._log_launch_readiness()
elif not self._launch_blocked_warned:
# Operator pressed LAUNCH but the gate is NOT ready (not every
# roster seat has ACKed its egg) -- previously this did NOTHING
# visible ("I pressed launch and nothing happened"). Say WHY.
self._launch_blocked_warned = True
print("[relay] LAUNCH pressed but NOT all seats are filled:",
flush=True)
self._log_launch_readiness()
if self.launch_at is None or self.launches_sent >= 2:
return
if time.time() < self.launch_at:
@@ -493,6 +542,11 @@ class Relay:
f"{len(self.console_conns)} pod(s)", flush=True)
if self.launches_sent == 2:
self.mission_started_at = time.time()
# CONSUME the launch: clear the timer + the request so the NEXT
# LAUNCH press is a fresh, deliberate new-mission request (else the
# back-to-back block would re-fire while this mission is running).
self.launch_at = None
self.launch_requested = False
if self.mission_length > 0:
print(f"[relay] mission clock: {self.mission_length:.0f}s "
"(StopMission at expiry)", flush=True)
+70 -33
View File
@@ -85,7 +85,12 @@ class SessionMonitor:
self.eggs = int(m.group(1))
changed = True
if self.RE_RELAY_READY.search(line):
# WAITING FOR OPERATOR = pods are ready to launch. Reset
# `launched` so the LAUNCH button re-enables -- this fires
# again for a NEW mission after the pods rejoin (back-to-back
# missions, 2026-07-18), not only the first time.
self.ready = True
self.launched = False
changed = True
m = self.RE_RELAY_REG.search(line)
if m:
@@ -755,44 +760,76 @@ class Operator(QMainWindow):
# first-come-first-served (no BT_SELF), so every player gets the SAME
# file -- who flies which mech is whoever sits down first, exactly
# like walking up to a pod.
tail = ("echo.\n"
"echo The game has exited. If you did NOT quit on purpose,\n"
"echo the session may not be running yet or the server was\n"
"echo unreachable -- check with the operator and try again.\n"
"pause\n")
# LOCATION GUARD (field report 2026-07-18: 'cannot find path specified'
# + dumps): the bat MUST sit in the extracted folder next to content\
# and build\. Running it from inside the zip (or after moving just the
# .bat) broke the relative cd with a cryptic error. Check first and
# give a clear message instead.
def guarded(header, sets, run, tail):
return ("@echo off\n" + header +
"cd /d %~dp0\n"
"if not exist \"build\\Release\\btl4.exe\" goto badpath\n"
"if not exist \"content\\" + egg_name +
"\" goto badpath\n" +
sets +
"cd content\n" + run + tail +
"exit /b\n"
":badpath\n"
"echo.\n"
"echo ==================================================\n"
"echo ERROR: this file is not in the right place.\n"
"echo.\n"
"echo It must sit in the EXTRACTED game folder, next to\n"
"echo the 'content' and 'build' folders.\n"
"echo.\n"
"echo Fix: right-click the .zip - Extract All, open the\n"
"echo extracted folder, and run this file from THERE.\n"
"echo (Do NOT run it from inside the zip.)\n"
"echo ==================================================\n"
"echo.\n"
"pause\n")
net_tail = ("echo.\n"
"echo The game has exited. If you did NOT quit on purpose\n"
"echo (crash, or it never connected), send the operator the\n"
"echo file content\\join.log so they can see what "
"happened.\n"
"echo Otherwise the session may not be up yet -- try "
"again.\n"
"pause\n")
with open(os.path.join(out_dir, "join.bat"), "w", newline="\r\n") as f:
f.write("@echo off\n"
"rem BT411 -- join %s's game (internet). Your seat/mech\n"
"rem is assigned automatically by the operator console.\n"
"set BT_RELAY=%s:%d\n"
"cd /d %%~dp0content\n"
"..\\build\\Release\\btl4.exe -egg %s -net 1501\n"
% (host, host, self.f_port.value(), egg_name) + tail)
f.write(guarded(
"rem BT411 -- join %s's game (internet). Seat/mech assigned\n"
"rem by the operator. Keep this file next to content\\ + "
"build\\.\n" % host,
# BT_START_INSIDE + BT_DEV_GAUGES: the pod is a first-person
# cockpit with MFD gauges -- match it (field report: remote
# players booted 3rd-person + no gauges because join.bat set
# neither; view/gauges are CLIENT flags the relay can't reach).
"set BT_RELAY=%s:%d\nset BT_LOG=join.log\n"
"set BT_START_INSIDE=1\nset BT_DEV_GAUGES=1\n"
% (host, self.f_port.value()),
"..\\build\\Release\\btl4.exe -egg %s -net 1501\n" % egg_name,
net_tail))
with open(os.path.join(out_dir, "join_lan.bat"), "w",
newline="\r\n") as f:
f.write("@echo off\n"
"rem BT411 -- join the game from the operator's OWN\n"
"rem network (LAN) -- finds the console automatically.\n"
"rem Joining over the INTERNET? Use join.bat instead.\n"
"set BT_RELAY=auto\n"
"cd /d %%~dp0content\n"
"..\\build\\Release\\btl4.exe -egg %s -net 1501\n"
% egg_name + tail)
# solo practice: no server involved -- the game self-launches a
# single-player mission (no -net argument = no console wait)
f.write(guarded(
"rem BT411 -- join the operator's LAN game (auto-discovers).\n"
"rem Joining over the INTERNET? Use join.bat instead.\n",
"set BT_RELAY=auto\nset BT_LOG=join.log\n"
"set BT_START_INSIDE=1\nset BT_DEV_GAUGES=1\n",
"..\\build\\Release\\btl4.exe -egg %s -net 1501\n" % egg_name,
net_tail))
with open(os.path.join(out_dir, "play_solo.bat"), "w",
newline="\r\n") as f:
f.write("@echo off\n"
"rem BT411 -- single-player practice mission (no network,\n"
"rem no operator needed). Edit the -egg name below to try\n"
"rem other maps (ARENA1/ARENA2/DBASE/FOGDAY/...).\n"
"set BT_START_INSIDE=1\n"
"cd /d %~dp0content\n"
"..\\build\\Release\\btl4.exe -egg ARENA1.EGG\n"
"echo.\n"
"echo The game has exited. If it closed unexpectedly,\n"
"echo send the operator the file content\\btl4.log.\n"
"pause\n")
f.write(guarded(
"rem BT411 -- single-player practice (no network, no "
"operator).\n"
"rem Edit the -egg below for other maps "
"(ARENA2/DBASE/FOGDAY/...).\n",
"set BT_START_INSIDE=1\nset BT_LOG=join.log\n",
"..\\build\\Release\\btl4.exe -egg ARENA1.EGG\n",
"echo.\necho The game has exited. If it closed unexpectedly,\n"
"echo send the operator content\\join.log.\npause\n"))
self.log.appendPlainText(
"exported join.bat + join_lan.bat + play_solo.bat to %s "
"(public host: %s) -- join bats are universal; the relay "
+4 -2
View File
@@ -15,10 +15,12 @@ execute_process(COMMAND git rev-parse --short HEAD
WORKING_DIRECTORY "${SRC}"
OUTPUT_VARIABLE BT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
# content/LAST.EGG is rewritten by the game at every run -- a permanent
# false-dirty; exclude it so a pristine checkout stamps clean.
# content/LAST.EGG is rewritten by the game every run and content/OPERATOR.EGG
# is overwritten by the operator app on Save -- both are runtime working files,
# not source, so exclude them (else the build stamps a false '+' dirty marker).
execute_process(COMMAND git status --porcelain --untracked-files=no
-- . ":(exclude)content/LAST.EGG"
":(exclude)content/OPERATOR.EGG"
WORKING_DIRECTORY "${SRC}"
OUTPUT_VARIABLE BT_DIRTY_RAW
ERROR_QUIET)