Operator console app: PySide6 station + validated egg model (the lost 1995 console, recreated)

The operator station the archive never preserved: build missions with
authoritative dropdowns, run the console/relay, watch pods arrive, launch --
LAN mesh or internet relay -- plus one-click local instances for testing.

tools/eggmodel.py (headless, importable, self-tested 14/14):
- Order-preserving egg parse/emit: the editor manages [mission]/[pilots]/
  per-pilot pages; bitmap rasters, ordinals and role pages survive untouched.
- EVERY value set read LIVE from content/BTL4.RES (the resscan parser,
  importable): maps = MakeMessageStream(14) INTERSECT ExistanceBoxStream(26)
  (the game aborts unless both exist); colors/badges/patches from the
  VehicleTable resource (type 25, NUL-line NotationFile text with ;-comments);
  vehicles = ModelList names, known-good mechs (bhk1, madcat) surfaced first.
- validate(): the full required-key ruleset from MISSION.cpp/btl4mssn.cpp
  (missing map/time/weather/scenario, per-pilot hostType/vehicle/dropzone/
  color/patch/badge/experience/role, role page model=, illegal values,
  duplicate addresses) -- kills the hand-edited-egg crash class, incl. the
  color=Red-vs-Crimson gotcha that bit the dev eggs.

tools/btoperator.py (PySide6, dark Fusion theme):
- Mission form + pilot roster table (all dropdowns from eggmodel), relay-tag
  auto-numbering, add/remove pilots, egg New/Open/Save/Validate.
- Mode switch: Relay (internet; runs btconsole --relay) / Mesh (LAN legacy;
  dial-out console at address-port minus 1 per the +1 rule).
- Session panel: QProcess drives btconsole.py; stdout parsed live into
  per-pilot state lights (waiting/egg/registered/LAUNCHED) + relay stats.
- Local launcher: spawns btl4.exe per roster row with BT_RELAY/BT_SELF (or
  mesh -net port), BT_DEV_GAUGES/BT_START_INSIDE toggles.
- Export player scripts: per-pilot join_as_playerN.bat with the public
  relay hostname (a remote player's whole setup = run one .bat).
- Wire/protocol code stays in btconsole.py/eggmodel.py -- the GUI is only
  UI + process management; CLI + test harnesses share the same tested core.

Verified: eggmodel self-test 14/14; scripted end-to-end
(scratchpad/operator_e2e.py) drives the real window through its own methods --
start relay session -> launch 2 local instances -> monitor observes the
mission LAUNCH (user-confirmed live on screen: both pods in-mission).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-18 10:57:30 -05:00
co-authored by Claude Fable 5
parent dbb9af2dfa
commit 43fa53d542
4 changed files with 1403 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python
"""operator_e2e.py -- scripted end-to-end test of the operator console.
Drives the real Operator window through its own methods on a timer:
start a RELAY session -> launch 2 local btl4 instances -> wait for the
mission LAUNCH to be observed by the session monitor -> report + shut down.
Exercises the exact QProcess paths the buttons use (no synthetic clicks).
Exit 0 = launched; 1 = timed out.
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "tools"))
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication, QMessageBox
import btoperator
# never block on dialogs in scripted mode
QMessageBox.warning = staticmethod(lambda *a, **k: print("[e2e] WARN dialog:", a[2] if len(a) > 2 else a))
QMessageBox.information = staticmethod(lambda *a, **k: None)
TIMEOUT_S = 120
result = {"ok": False}
def main():
app = QApplication(sys.argv)
btoperator.dark_palette(app)
win = btoperator.Operator()
win.show()
def step_start():
print("[e2e] starting relay session")
win._start_session()
assert win.console_proc is not None, "session did not start"
QTimer.singleShot(3000, step_launch)
def step_launch():
print("[e2e] launching local instances")
win._launch_local()
assert len(win.game_procs) == 2, \
"expected 2 local instances, got %d" % len(win.game_procs)
poll.start(2000)
ticks = {"n": 0}
def step_poll():
ticks["n"] += 1
m = win.monitor
states = dict(m.state) if m else {}
print("[e2e] t=%ds states=%s launched=%s" %
(ticks["n"] * 2, states, m.launched if m else "?"))
if m and m.launched:
result["ok"] = True
status = win.pod_status.text().encode("ascii", "replace").decode()
print("[e2e] LAUNCH observed -- pod status: %s" % status)
finish()
elif ticks["n"] * 2 >= TIMEOUT_S:
print("[e2e] TIMEOUT")
finish()
def finish():
poll.stop()
win._stop_session()
QTimer.singleShot(500, app.quit)
poll = QTimer()
poll.timeout.connect(step_poll)
QTimer.singleShot(1000, step_start)
app.exec()
print("[e2e] %s" % ("PASS" if result["ok"] else "FAIL"))
return 0 if result["ok"] else 1
if __name__ == "__main__":
sys.exit(main())