#!/usr/bin/env python """btconsole.py -- minimal Tesla/WinTesla CONSOLE emulator (the operator station). The pod-side engine (MUNGA_L4/L4NET.CPP) boots `-net ` into ConsoleOnly state: it listens on TCP and waits for a console to connect and stream the mission egg. The real console software is absent from every archive, so this tool speaks the console's wire protocol: packet := NetworkPacketHeader + NetworkManager::ReceiveEggFileMessage off 0 int32 clientID = 0 (NetworkClient::NetworkManagerClientID) off 4 int32 gameID = 0 (NETWORK.cpp:121 -- always 0) off 8 int32 fromHost = 1 (FirstLegalHostID == the console) off 12 int32 timeStamp = 0 (Time; unused by the egg handler) off 16 uint32 messageLength = 1024 (sizeof(ReceiveEggFileMessage)) off 20 int32 messageID = 3 (NetworkManager::ReceiveEggFileMessageID) off 24 uint32 messageFlags = 1 (Receiver::Message::ReliableFlag) off 28 int32 sequenceNumber (0..n-1; -1 is the solo-local path only) off 32 int32 notationFileLength (total egg bytes) off 36 int32 thisMessageLength (bytes used in this chunk, <= 1000) off 40 char notationData[1000] total 1040 bytes per chunk (constants verified from the port build via `btl4.exe` BT_NET_PROBE=1) The pod reassembles chunks sequentially (L4NET.CPP:773-816: seq 0 allocates, append until notationFileLength reached), sets NormalState, CreateMission -> StartConnecting (the egg's [pilots] list forms the pod mesh), and sends an AcknowledgeEggFile back on this socket. The console must STAY CONNECTED (a disconnect trips the pod's console-loss path, which also closes the game listener -- an engine bug). Usage: python btconsole.py [ ...] Example (two instances on one box, -net 1501 / -net 1601): python btconsole.py MP.EGG 127.0.0.1:1501 127.0.0.1:1601 """ import socket import struct import sys import threading import time CHUNK = 1000 PKT_FMT_HDR = "LaunchingMission (dispatches the # player MissionStarting/translocation), then LaunchingMission->RunningMission. # Sending it early (state==CreatingMission) hits the handler's Fail() -- hence # the settle delay. Constants from BT_NET_PROBE=1. APPLICATION_CLIENT_ID = 4 # NetworkClient::ApplicationClientID RUN_MISSION_MESSAGE_ID = 5 # Application::RunMissionMessageID LAUNCH_SETTLE_SECONDS = 20.0 # egg -> first launch (pods must reach WaitingForLaunch) LAUNCH_STEP_SECONDS = 4.0 # first launch -> second (Launching -> Running) def run_mission_packet(): pkt = struct.pack(PKT_FMT_HDR, APPLICATION_CLIENT_ID, 0, CONSOLE_HOST_ID, 0) pkt += struct.pack("= launch_at: s.sendall(run_mission_packet()) launches_sent += 1 launch_at = time.time() + LAUNCH_STEP_SECONDS print(f"[{name}] RunMission #{launches_sent} sent") try: data = s.recv(4096) if not data: print(f"[{name}] pod closed the console socket", flush=True) return # Expect the AcknowledgeEggFile (and possibly other console traffic). if len(data) >= 24: (clid, gid, fh, ts) = struct.unpack_from("console packet: clientID={clid} fromHost={fh} " f"msgID={mid} len={mlen} ({len(data)} bytes)") else: print(f"[{name}] pod->console {len(data)} bytes") except socket.timeout: continue except OSError as e: # Previously UNHANDLED: a reset/abort here killed the thread silently # (buffered stdout lost) and the console process exited -- leaving the # pods holding dead console sockets. Log it and keep the thread alive # (idle) so the process + diagnosis survive. print(f"[{name}] console socket error: {e!r} -- idling", flush=True) while True: time.sleep(60) def main(): if len(sys.argv) < 3: print(__doc__) return 2 egg_bytes = egg_wire_bytes(open(sys.argv[1], "rb").read()) pods = sys.argv[2:] threads = [threading.Thread(target=serve_pod, args=(p, egg_bytes), daemon=True) for p in pods] for t in threads: t.start() try: while any(t.is_alive() for t in threads): time.sleep(0.5) except KeyboardInterrupt: print("console shutting down") return 0 if __name__ == "__main__": sys.exit(main())