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:
co-authored by
Claude Fable 5
parent
dbb9af2dfa
commit
43fa53d542
@@ -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())
|
||||
Reference in New Issue
Block a user