#!/usr/bin/env python """btoperator.py -- BT411 OPERATOR CONSOLE (PySide6). The system-operator station the 1995 archive lost, recreated: build missions with validated dropdowns (every value read live from BTL4.RES via eggmodel), run the console/relay, watch pods arrive, and launch games -- LAN mesh or internet relay -- plus one-click local instances for testing. python tools/btoperator.py Modes: Relay (internet) -- runs `btconsole.py --relay`; pods dial OUT to this box (players: BT_RELAY=: + BT_SELF=). Mesh (LAN legacy) -- runs the classic dial-out console against each pod's -net port (authentic cabinet topology). The egg/protocol logic stays headless in eggmodel.py / btconsole.py; this file is only UI + process management (QProcess), so the tested wire code is shared with the CLI and the test harnesses. """ import os import re import sys from PySide6.QtCore import Qt, QProcess, QProcessEnvironment from PySide6.QtGui import QColor, QFont, QPalette, QAction from PySide6.QtWidgets import ( QApplication, QCheckBox, QComboBox, QFileDialog, QFormLayout, QGroupBox, QHBoxLayout, QHeaderView, QLabel, QLineEdit, QMainWindow, QMessageBox, QPlainTextEdit, QPushButton, QSpinBox, QSplitter, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget, QStatusBar) HERE = os.path.dirname(os.path.abspath(__file__)) REPO = os.path.abspath(os.path.join(HERE, "..")) CONTENT = os.path.join(REPO, "content") DEFAULT_EXE = os.path.join(REPO, "build", "Release", "btl4.exe") DEFAULT_EGG = os.path.join(CONTENT, "OPERATOR.EGG") BTCONSOLE = os.path.join(HERE, "btconsole.py") sys.path.insert(0, HERE) import eggmodel # noqa: E402 # --------------------------------------------------------------------------- # Pilot state model (parsed live from console/relay stdout) # --------------------------------------------------------------------------- ST_IDLE, ST_EGG, ST_REGISTERED, ST_LAUNCHED = ( "waiting", "egg sent", "registered", "LAUNCHED") ST_COLORS = {ST_IDLE: "#666666", ST_EGG: "#b58900", ST_REGISTERED: "#2aa198", ST_LAUNCHED: "#33cc55"} class SessionMonitor: """Turns btconsole/relay stdout lines into per-pilot state + stats.""" RE_RELAY_EGG = re.compile(r"pod ACK from .*?\((\d+)/(\d+) ready\)") RE_RELAY_REG = re.compile(r"game\[.* host=(\d+)\] REGISTERED") RE_RELAY_RUN = re.compile(r"RunMission #(\d+) sent") RE_RELAY_STAT = re.compile(r"\[relay-stats\] (.*)") RE_RELAY_DOWN = re.compile(r"game\[.* host=(\d+)\] dropped") RE_RELAY_READY = re.compile(r"WAITING FOR OPERATOR") RE_MESH_EGG = re.compile(r"\[([^\]]+)\] egg sent") RE_MESH_RUN = re.compile(r"\[([^\]]+)\] RunMission #(\d+) sent") def __init__(self, relay_mode, tags): self.relay_mode = relay_mode self.tags = tags # roster order == hostID order self.state = {t: ST_IDLE for t in tags} self.eggs = 0 self.ready = False # manual mode: awaiting operator self.launched = False self.stats = "" def host_tag(self, host_id): i = host_id - 2 # hostIDs 2..N+1 in egg order return self.tags[i] if 0 <= i < len(self.tags) else None def feed(self, line): """Consume one log line; return True if any state changed.""" changed = False if self.relay_mode: m = self.RE_RELAY_EGG.search(line) if m: self.eggs = int(m.group(1)) changed = True if self.RE_RELAY_READY.search(line): self.ready = True changed = True m = self.RE_RELAY_REG.search(line) if m: tag = self.host_tag(int(m.group(1))) if tag: self.state[tag] = ST_REGISTERED changed = True m = self.RE_RELAY_DOWN.search(line) if m: tag = self.host_tag(int(m.group(1))) if tag and self.state.get(tag) != ST_IDLE: self.state[tag] = ST_IDLE changed = True m = self.RE_RELAY_RUN.search(line) if m and int(m.group(1)) >= 2: self.launched = True for t in self.state: if self.state[t] == ST_REGISTERED: self.state[t] = ST_LAUNCHED changed = True m = self.RE_RELAY_STAT.search(line) if m: self.stats = m.group(1) changed = True else: m = self.RE_MESH_EGG.search(line) if m: # mesh log key = host:consolePort; roster tag = host:gamePort tag = self._mesh_tag(m.group(1)) if tag: self.state[tag] = ST_EGG changed = True m = self.RE_MESH_RUN.search(line) if m and int(m.group(2)) >= 2: tag = self._mesh_tag(m.group(1)) if tag: self.state[tag] = ST_LAUNCHED self.launched = True changed = True return changed def _mesh_tag(self, console_hostport): host, _, port = console_hostport.rpartition(":") try: game = "%s:%d" % (host, int(port) + 1) except ValueError: return None return game if game in self.state else None # --------------------------------------------------------------------------- # Main window # --------------------------------------------------------------------------- class Operator(QMainWindow): COLS = ["Identity / Address", "Callsign", "Vehicle", "Color", "Badge", "Patch", "Experience", "Local"] def __init__(self): super().__init__() self.setWindowTitle("BT411 Operator Console") self.resize(1180, 760) self.values = eggmodel.ValueSets() self.egg = eggmodel.new_from_template() self.egg_path = DEFAULT_EGG self.console_proc = None self.game_procs = [] self.monitor = None self._build_ui() self._load_egg_into_ui() # ------------------------------------------------------------------ UI -- def _build_ui(self): central = QWidget() self.setCentralWidget(central) root = QVBoxLayout(central) # -- top bar: mode + egg file actions top = QHBoxLayout() top.addWidget(QLabel("Mode:")) self.mode = QComboBox() self.mode.addItems(["Relay (internet)", "Mesh (LAN legacy)"]) self.mode.currentIndexChanged.connect(self._mode_changed) top.addWidget(self.mode) top.addSpacing(24) for label, slot in (("New", self._egg_new), ("Open…", self._egg_open), ("Save", self._egg_save), ("Save As…", self._egg_save_as)): b = QPushButton(label) b.clicked.connect(slot) top.addWidget(b) self.egg_label = QLabel(os.path.basename(self.egg_path)) self.egg_label.setStyleSheet("color:#888;") top.addWidget(self.egg_label) top.addStretch(1) self.validate_btn = QPushButton("Validate") self.validate_btn.clicked.connect(self._validate_clicked) top.addWidget(self.validate_btn) root.addLayout(top) split = QSplitter(Qt.Vertical) root.addWidget(split, 1) upper = QWidget() upper_l = QHBoxLayout(upper) upper_l.setContentsMargins(0, 0, 0, 0) split.addWidget(upper) # -- mission + network forms left = QVBoxLayout() upper_l.addLayout(left, 0) mission_box = QGroupBox("Mission") form = QFormLayout(mission_box) self.f_map = QComboBox(); self.f_map.addItems(self.values.maps) self.f_time = QComboBox(); self.f_time.addItems(self.values.times) self.f_weather = QComboBox(); self.f_weather.addItems(self.values.weathers) self.f_scenario = QComboBox(); self.f_scenario.addItems(self.values.scenarios) self.f_temp = QLineEdit(); self.f_temp.setPlaceholderText("e.g. 25.0") self.f_length = QLineEdit(); self.f_length.setPlaceholderText("seconds; blank = unlimited") form.addRow("Map", self.f_map) form.addRow("Time", self.f_time) form.addRow("Weather", self.f_weather) form.addRow("Scenario", self.f_scenario) form.addRow("Temperature", self.f_temp) form.addRow("Game length", self.f_length) left.addWidget(mission_box) net_box = QGroupBox("Network") nform = QFormLayout(net_box) self.f_port = QSpinBox() self.f_port.setRange(1024, 65000) self.f_port.setValue(1500) self.f_public = QLineEdit() self.f_public.setPlaceholderText("public hostname for player scripts") self.f_manual = QCheckBox("Manual launch (wait for my Launch button)") self.f_manual.setChecked(True) nform.addRow("Console port", self.f_port) nform.addRow("Public host", self.f_public) nform.addRow(self.f_manual) left.addWidget(net_box) exe_box = QGroupBox("Local game") eform = QFormLayout(exe_box) self.f_exe = QLineEdit(DEFAULT_EXE) pick = QPushButton("…") pick.setFixedWidth(28) pick.clicked.connect(self._pick_exe) exe_row = QHBoxLayout() exe_row.addWidget(self.f_exe, 1) exe_row.addWidget(pick) eform.addRow("btl4.exe", exe_row) self.f_gauges = QCheckBox("Dev gauges"); self.f_gauges.setChecked(True) self.f_inside = QCheckBox("Start in cockpit"); self.f_inside.setChecked(True) opt_row = QHBoxLayout() opt_row.addWidget(self.f_gauges) opt_row.addWidget(self.f_inside) eform.addRow(opt_row) left.addWidget(exe_box) left.addStretch(1) # -- roster roster_box = QGroupBox("Pilot roster (hostIDs assigned in this order)") rl = QVBoxLayout(roster_box) self.table = QTableWidget(0, len(self.COLS)) self.table.setHorizontalHeaderLabels(self.COLS) self.table.horizontalHeader().setSectionResizeMode( 0, QHeaderView.Stretch) self.table.verticalHeader().setVisible(True) rl.addWidget(self.table, 1) btns = QHBoxLayout() add = QPushButton("Add pilot"); add.clicked.connect(self._add_pilot) rem = QPushButton("Remove selected"); rem.clicked.connect(self._remove_pilot) self.export_btn = QPushButton("Export player scripts…") self.export_btn.clicked.connect(self._export_scripts) btns.addWidget(add); btns.addWidget(rem) btns.addStretch(1); btns.addWidget(self.export_btn) rl.addLayout(btns) upper_l.addWidget(roster_box, 1) # -- session panel lower = QWidget() low = QVBoxLayout(lower) low.setContentsMargins(0, 0, 0, 0) split.addWidget(lower) srow = QHBoxLayout() self.start_btn = QPushButton("▶ Start session") self.start_btn.setStyleSheet("font-weight:bold;") self.start_btn.clicked.connect(self._start_session) self.stop_btn = QPushButton("■ Stop session") self.stop_btn.clicked.connect(self._stop_session) self.stop_btn.setEnabled(False) self.restart_btn = QPushButton("↻ Restart session") self.restart_btn.clicked.connect(self._restart_session) self.restart_btn.setEnabled(False) self.launch_btn = QPushButton("🚀 LAUNCH MISSION") self.launch_btn.setStyleSheet( "font-weight:bold; color:#33cc55; padding:4px 14px;") self.launch_btn.clicked.connect(self._launch_mission) self.launch_btn.setEnabled(False) self.launch_local_btn = QPushButton("Launch local instances") self.launch_local_btn.clicked.connect(self._launch_local) self.launch_local_btn.setEnabled(False) self.stop_games_btn = QPushButton("Stop games") self.stop_games_btn.clicked.connect(self._stop_games) srow.addWidget(self.start_btn) srow.addWidget(self.stop_btn) srow.addWidget(self.restart_btn) srow.addSpacing(20) srow.addWidget(self.launch_btn) srow.addSpacing(20) srow.addWidget(self.launch_local_btn) srow.addWidget(self.stop_games_btn) srow.addStretch(1) self.stats_label = QLabel("") self.stats_label.setStyleSheet("color:#7a9;") srow.addWidget(self.stats_label) low.addLayout(srow) self.pod_status = QLabel("session not running") self.pod_status.setTextFormat(Qt.RichText) low.addWidget(self.pod_status) self.log = QPlainTextEdit() self.log.setReadOnly(True) self.log.setMaximumBlockCount(2000) self.log.setFont(QFont("Consolas", 9)) low.addWidget(self.log, 1) split.setSizes([440, 300]) self.setStatusBar(QStatusBar()) self._status("ready — values loaded live from BTL4.RES: " f"{len(self.values.maps)} maps, " f"{len(self.values.vehicles)} vehicles, " f"{len(self.values.colors)} colors") def _status(self, text): self.statusBar().showMessage(text) # -------------------------------------------------------------- roster -- def _combo(self, items, current): c = QComboBox() c.addItems(items) if current in items: c.setCurrentText(current) elif current: c.insertItem(0, current) c.setCurrentIndex(0) return c def _add_pilot_row(self, pilot, local=False): r = self.table.rowCount() self.table.insertRow(r) addr = QTableWidgetItem(pilot.get("address", "")) if self.mode.currentIndex() == 0: # relay tags are auto-managed addr.setFlags(addr.flags() & ~Qt.ItemIsEditable) self.table.setItem(r, 0, addr) # callsign: free text, becomes the name BITMAPS in the egg on save # (the 1995 console's job -- see eggmodel.set_callsigns) self.table.setItem(r, 1, QTableWidgetItem( pilot.get("name") or "PLAYER%d" % (r + 1))) self.table.setCellWidget(r, 2, self._combo( self.values.vehicles, pilot.get("vehicle") or "bhk1")) self.table.setCellWidget(r, 3, self._combo( self.values.colors, pilot.get("color") or "White")) self.table.setCellWidget(r, 4, self._combo( self.values.badges, pilot.get("badge") or "VGL")) self.table.setCellWidget(r, 5, self._combo( self.values.patches, pilot.get("patch") or "Yellow")) self.table.setCellWidget(r, 6, self._combo( self.values.experiences, pilot.get("experience") or "expert")) chk = QCheckBox() chk.setChecked(local) wrap = QWidget() wl = QHBoxLayout(wrap) wl.setContentsMargins(0, 0, 0, 0) wl.setAlignment(Qt.AlignCenter) wl.addWidget(chk) self.table.setCellWidget(r, 7, wrap) def _row_pilot(self, r, template): """Read one roster row back into a pilot dict.""" p = dict(template) p["address"] = self.table.item(r, 0).text().strip() p["name"] = self._row_callsign(r) p["vehicle"] = self.table.cellWidget(r, 2).currentText() p["color"] = self.table.cellWidget(r, 3).currentText() p["badge"] = self.table.cellWidget(r, 4).currentText() p["patch"] = self.table.cellWidget(r, 5).currentText() p["experience"] = self.table.cellWidget(r, 6).currentText() p["bitmapindex"] = str(r + 1) return p def _row_callsign(self, r): item = self.table.item(r, 1) text = item.text().strip() if item else "" return text or "PLAYER%d" % (r + 1) def _row_local(self, r): wrap = self.table.cellWidget(r, 7) return wrap.findChild(QCheckBox).isChecked() if wrap else False def _pilot_template(self): pilots = self.egg.pilots() if pilots: t = dict(pilots[0]) else: t = {"hostType": "0", "advancedDamage": "1", "loadzones": "1", "name": "Pilot", "experience": "expert", "badge": "VGL", "patch": "Yellow", "role": "Role::Default", "dropzone": "one", "vehicle": "bhk1", "vehicleValue": "1000", "color": "White"} t.pop("address", None) return t def _add_pilot(self): n = self.table.rowCount() + 1 if self.mode.currentIndex() == 0: address = eggmodel.relay_tags(n)[-1] else: address = "192.168.1.%d:1502" % (99 + n) 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)) self._renumber_relay_tags() def _remove_pilot(self): r = self.table.currentRow() if r >= 0: self.table.removeRow(r) self._renumber_relay_tags() def _renumber_relay_tags(self): if self.mode.currentIndex() != 0: return tags = eggmodel.relay_tags(self.table.rowCount()) for r, tag in enumerate(tags): self.table.item(r, 0).setText(tag) def _mode_changed(self, _index): relay = self.mode.currentIndex() == 0 for r in range(self.table.rowCount()): item = self.table.item(r, 0) flags = item.flags() item.setFlags((flags & ~Qt.ItemIsEditable) if relay else (flags | Qt.ItemIsEditable)) if relay: self._renumber_relay_tags() self._status("mode: %s" % self.mode.currentText()) # ------------------------------------------------------------ egg file -- def _load_egg_into_ui(self): m = self.egg.mission() for combo, key in ((self.f_map, "map"), (self.f_time, "time"), (self.f_weather, "weather"), (self.f_scenario, "scenario")): if m.get(key): combo.setCurrentText(m[key]) self.f_temp.setText(m.get("temperature") or "") 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) self._renumber_relay_tags() self.egg_label.setText(os.path.basename(self.egg_path)) def _collect_egg(self): """UI -> self.egg (returns validation problems).""" self.egg.set_mission( map=self.f_map.currentText(), time=self.f_time.currentText(), weather=self.f_weather.currentText(), scenario=self.f_scenario.currentText(), temperature=self.f_temp.text().strip(), length=self.f_length.text().strip()) template = self._pilot_template() pilots = [self._row_pilot(r, template) for r in range(self.table.rowCount())] self.egg.set_pilots(pilots) # rasterize the callsigns into the egg's name-bitmap pages (what the # 1995 console did when the operator typed a callsign) self.egg.set_callsigns(eggmodel.make_callsigns( [self._row_callsign(r) for r in range(self.table.rowCount())])) return self.egg.validate(self.values) def _egg_new(self): self.egg = eggmodel.new_from_template() self.egg_path = DEFAULT_EGG self._load_egg_into_ui() self._status("new mission from template") def _egg_open(self): path, _ = QFileDialog.getOpenFileName( self, "Open mission egg", CONTENT, "Egg files (*.EGG *.egg)") if path: self.egg = eggmodel.EggDoc.load(path) self.egg_path = path self._load_egg_into_ui() self._status("opened %s" % path) def _egg_save(self): problems = self._collect_egg() if problems and not self._confirm_problems(problems, "Save anyway?"): return False self.egg.save(self.egg_path) self.egg_label.setText(os.path.basename(self.egg_path)) self._status("saved %s" % self.egg_path) return True def _egg_save_as(self): path, _ = QFileDialog.getSaveFileName( self, "Save mission egg", self.egg_path, "Egg files (*.EGG *.egg)") if path: self.egg_path = path self._egg_save() def _validate_clicked(self): problems = self._collect_egg() if problems: QMessageBox.warning(self, "Validation", "\n".join("• %s" % p for p in problems)) else: QMessageBox.information(self, "Validation", "Mission is valid and launchable.") def _confirm_problems(self, problems, question): return QMessageBox.question( self, "Validation problems", "\n".join("• %s" % p for p in problems) + "\n\n" + question, QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes # ------------------------------------------------------------- session -- def _start_session(self): problems = self._collect_egg() if problems: QMessageBox.warning(self, "Cannot start", "\n".join(problems)) return self.egg.save(self.egg_path) relay = self.mode.currentIndex() == 0 tags = [p["address"] for p in self.egg.pilots()] self.monitor = SessionMonitor(relay, tags) args = [BTCONSOLE] if relay: args += ["--relay", str(self.f_port.value()), self.egg_path] if self.f_manual.isChecked(): args += ["--manual-launch"] else: # mesh: console dials each pod's CONSOLE port (= game port - 1) args += [self.egg_path] for tag in tags: host, _, port = tag.rpartition(":") args.append("%s:%d" % (host, int(port) - 1)) self.console_proc = QProcess(self) self.console_proc.setProcessChannelMode(QProcess.MergedChannels) self.console_proc.readyReadStandardOutput.connect(self._console_output) self.console_proc.finished.connect(self._console_finished) self.console_proc.start(sys.executable, ["-u"] + args) self.start_btn.setEnabled(False) self.stop_btn.setEnabled(True) self.restart_btn.setEnabled(True) self.launch_local_btn.setEnabled(True) self.launch_btn.setEnabled(False) self.log.appendPlainText("== session started (%s%s) ==" % ("relay" if relay else "mesh", ", manual launch" if relay and self.f_manual.isChecked() else "")) self._refresh_pod_status() def _stop_session(self): if self.console_proc: self.console_proc.kill() self.console_proc = None self._stop_games() self.start_btn.setEnabled(True) self.stop_btn.setEnabled(False) self.restart_btn.setEnabled(False) self.launch_btn.setEnabled(False) self.launch_local_btn.setEnabled(False) self.pod_status.setText("session not running") self.log.appendPlainText("== session stopped ==") def _restart_session(self): """Next round: same mission/roster, fresh session. Players just re-run their join script.""" self.log.appendPlainText("== restarting session ==") self._stop_session() self._start_session() def _launch_mission(self): """Manual launch gate: send the relay its 'launch' command.""" if self.console_proc: self.console_proc.write(b"launch\n") self.launch_btn.setEnabled(False) self.log.appendPlainText(">> operator LAUNCH sent") def _console_finished(self): self.log.appendPlainText("== console/relay process exited ==") self.start_btn.setEnabled(True) self.stop_btn.setEnabled(False) def _console_output(self): if not self.console_proc: return data = bytes(self.console_proc.readAllStandardOutput()).decode( "utf-8", "replace") for line in data.splitlines(): if line.strip(): self.log.appendPlainText(line) if self.monitor and self.monitor.feed(line): self._refresh_pod_status() def _refresh_pod_status(self): if not self.monitor: return parts = [] for tag, state in self.monitor.state.items(): parts.append(' %s %s' % (ST_COLORS[state], tag, state)) if self.monitor.launched: head = "LAUNCHED — mission running" elif self.monitor.ready: head = "ALL PODS READY — press LAUNCH MISSION" elif self.monitor.relay_mode: head = "eggs delivered: %d/%d" % (self.monitor.eggs, len(self.monitor.tags)) else: head = "mesh console driving pods" self.pod_status.setText("%s    %s" % (head, "   ".join(parts))) self.launch_btn.setEnabled( self.monitor.ready and not self.monitor.launched and self.console_proc is not None) if self.monitor.stats: self.stats_label.setText(self.monitor.stats) # -------------------------------------------------------- local launch -- def _pick_exe(self): path, _ = QFileDialog.getOpenFileName( self, "Locate btl4.exe", os.path.dirname(self.f_exe.text()), "Executable (*.exe)") if path: self.f_exe.setText(path) def _launch_local(self): exe = self.f_exe.text() if not os.path.isfile(exe): QMessageBox.warning(self, "Launch", "btl4.exe not found:\n" + exe) return relay = self.mode.currentIndex() == 0 pilots = self.egg.pilots() launched = 0 for r in range(self.table.rowCount()): if not self._row_local(r) or r >= len(pilots): continue tag = pilots[r]["address"] env = QProcessEnvironment.systemEnvironment() if self.f_gauges.isChecked(): env.insert("BT_DEV_GAUGES", "1") if self.f_inside.isChecked(): env.insert("BT_START_INSIDE", "1") if relay: env.insert("BT_RELAY", "127.0.0.1:%d" % self.f_port.value()) env.insert("BT_SELF", tag) net_port = 1501 else: host, _, port = tag.rpartition(":") net_port = int(port) - 1 env.insert("BT_LOG", "operator_%d.log" % (r + 1)) proc = QProcess(self) proc.setWorkingDirectory(CONTENT) proc.setProcessEnvironment(env) proc.start(exe, ["-egg", os.path.basename(self.egg_path) if os.path.dirname(self.egg_path) == CONTENT else self.egg_path, "-net", str(net_port)]) self.game_procs.append(proc) launched += 1 self.log.appendPlainText("launched %d local instance(s)" % launched) def _stop_games(self): for proc in self.game_procs: if proc.state() != QProcess.NotRunning: proc.kill() self.game_procs = [] # ------------------------------------------------------ player scripts -- def _export_scripts(self): problems = self._collect_egg() if problems and not self._confirm_problems(problems, "Export anyway?"): return host = self.f_public.text().strip() or "YOUR.RELAY.HOST" out_dir = QFileDialog.getExistingDirectory( self, "Export player launch scripts to…", REPO) if not out_dir: return egg_name = os.path.basename(self.egg_path) # ONE universal script per transport: the RELAY assigns seats # 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. 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)) 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 LAN game. Finds the operator\n" "rem console AND your seat automatically.\n" "set BT_RELAY=auto\n" "cd /d %%~dp0content\n" "..\\build\\Release\\btl4.exe -egg %s -net 1501\n" % egg_name) self.log.appendPlainText( "exported join.bat + join_lan.bat to %s (public host: %s) -- " "one file for everyone; the relay assigns seats" % (out_dir, host)) # ------------------------------------------------------------- closing -- def closeEvent(self, event): self._stop_session() event.accept() def dark_palette(app): app.setStyle("Fusion") p = QPalette() bg, panel, text = QColor(37, 37, 40), QColor(28, 28, 30), QColor(220, 220, 220) p.setColor(QPalette.Window, bg) p.setColor(QPalette.WindowText, text) p.setColor(QPalette.Base, panel) p.setColor(QPalette.AlternateBase, bg) p.setColor(QPalette.Text, text) p.setColor(QPalette.Button, bg) p.setColor(QPalette.ButtonText, text) p.setColor(QPalette.Highlight, QColor(42, 130, 218)) p.setColor(QPalette.HighlightedText, Qt.white) p.setColor(QPalette.ToolTipBase, panel) p.setColor(QPalette.ToolTipText, text) p.setColor(QPalette.PlaceholderText, QColor(120, 120, 120)) app.setPalette(p) def main(): app = QApplication(sys.argv) dark_palette(app) win = Operator() win.show() return app.exec() if __name__ == "__main__": sys.exit(main())