#!/usr/bin/env python3 """Minimal stand-in POD: answer the 4.10 console's StateQuery so it unlocks mission-send, then capture the mission-egg delivery. Protocol (decoded from CODE/RP/MUNGA/{APPMSG,CONSOLE,NETWORK}.HPP): - Console TCP-connects to the pod (port 1501) and repeatedly sends an Application StateQueryMessage (clientID=4 App, wire messageID=3) until the pod answers, THEN it lets the operator send a mission. - The pod answers with a ConsoleApplicationStateResponseMessage (messageID=1, size 24: respondingHostID, applicationState, application). application = BTL4 = 1 (Puck runs BattleTech). - Wire is LITTLE-ENDIAN (Mac console byte-swaps via LEDWORD; confirmed by capture). Every packet = NetworkPacketHeader(16) + Receiver::Message. A few values (the "ready" applicationState code, host-ID routing, clientID) are inferred; tweak the TUNE block and re-run if the console doesn't accept the response. Once it does, send a mission from the console and this logs + reassembles the ReceiveEggFileMessage chunks (the egg). Usage: python pod_responder.py [port] """ import socket, struct, sys, time PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 1501 # ---- TUNE: values inferred from headers; adjust if the console rejects ---- RESP_CLIENT_ID = 4 # ClientID in the response header (query used 4=App) RESP_FROM_HOST = 1 # header.fromHost of our response RESP_HOST_ID = 1 # respondingHostID (echo the query's requestingHost) APP_STATE = 0 # applicationState: 0 = idle/ready (best guess) APP_ID = 1 # ApplicationID: RPL4=0, BTL4=1, BTW4=2 -> Puck=BT STATE_RESP_MSGID = 1 # ConsoleApplicationStateResponseMessageID # --------------------------------------------------------------------------- RAW = f"pod_responder_{int(time.time())}.bin" def le(*vals): return b"".join(struct.pack(" 40-byte packet def parse_hdr(buf): if len(buf) < 28: return None cid, gid, fh, ts = struct.unpack(" n: out.append(f" ... (+{len(b)-n})") return "\n".join(out) def main(): srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind(("0.0.0.0", PORT)); srv.listen(1) print(f"pod_responder: listening on 0.0.0.0:{PORT} (raw -> {RAW})") print(f" reply = StateResponse(host={RESP_HOST_ID}, state={APP_STATE}, " f"app={APP_ID}) clientID={RESP_CLIENT_ID}") raw = open(RAW, "wb") egg = bytearray() # reassembled egg egg_total = None while True: conn, addr = srv.accept() print(f"\n=== connection from {addr} ===") buf = b""; queries = 0 conn.settimeout(30) try: while True: chunk = conn.recv(4096) if not chunk: break raw.write(chunk); raw.flush(); buf += chunk # frame out concatenated packets: 16B hdr + msgLen body while len(buf) >= 28: h = parse_hdr(buf) total = 16 + h["msgLen"] if h["msgLen"] < 12 or h["msgLen"] > 2000: print(" ! bad msgLen, dumping:\n" + hexdump(buf)) buf = b""; break if len(buf) < total: break # wait for the rest pkt, buf = buf[:total], buf[total:] print(f"\n<- pkt {total}B {h}") print(hexdump(pkt)) if h["msgID"] == 3: # StateQuery queries += 1 r = state_response() conn.sendall(r) print(f"-> StateResponse ({len(r)}B) [query #{queries}]") elif h["msgLen"] == 1024: # ReceiveEggFileMessage seq, flen, tlen = struct.unpack("= flen: fn = f"received_egg_{int(time.time())}.egg" open(fn, "wb").write(egg) print(f" *** EGG COMPLETE -> {fn} ({len(egg)}B)") else: print(f" (msgID={h['msgID']} — not handled)") except socket.timeout: print("(idle 30s)") print("=== closed ===") conn.close() if __name__ == "__main__": main()