Merge remote-tracking branch 'origin/master' into glass-cockpit
This commit is contained in:
+28
-6
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 "
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user