From 9bec4b2050738157274c809752c5d5871ab86ee6 Mon Sep 17 00:00:00 2001 From: arcattack Date: Wed, 22 Jul 2026 19:57:03 -0500 Subject: [PATCH] READY light: pods report load-complete to the operator roster The roster showed "registered" for a still-loading pod and a ready one alike -- with multi-minute contended loads the operator couldn't tell who they'd be waiting on. When the app ladder reaches WaitingForLaunch the pod sends one empty route -10 frame on its live relay game socket (BTRelayNotifyReady; socket mirrored at HELLO); the relay announces SEAT n READY (k/m loaded) and the console GUI lights the seat bright-green with a "N pod(s) LOADED+READY" banner. Verified LIVE by the operator: blue (seated) -> teal (registered) -> green (ready) in order, twice. Also resolved the load-speed mystery: loads are ~30s uncontended; the 1-2+ minute cases were assistant background test runs competing with the user's interactive sessions on the same machine (memory note added -- no game-spawning tests while the user is testing). Co-Authored-By: Claude Opus 4.8 (1M context) --- context/multiplayer.md | 11 +++++++++ engine/MUNGA/APP.cpp | 5 ++++ engine/MUNGA_L4/L4NET.CPP | 50 ++++++++++++++++++++++++++++++++++++++- tools/btconsole.py | 10 ++++++++ tools/btoperator.py | 16 +++++++++++-- 5 files changed, 89 insertions(+), 3 deletions(-) diff --git a/context/multiplayer.md b/context/multiplayer.md index beaf8c1..2c6ef3d 100644 --- a/context/multiplayer.md +++ b/context/multiplayer.md @@ -437,6 +437,17 @@ phase). Plan: `~/.claude/plans/partitioned-snuggling-piglet.md`. from take 2's commit is UNPROVEN -- the measured fact is just that loads outlive any fixed cap; the counter design is correct under either reading. Residual: WHY loads take 1-2+ min on the operator box (vs ~30s earlier same day) is an open perf question ([loadobj] timings). +- **READY LIGHT (2026-07-22) [T2, live-verified]**: the roster showed "registered" for a + still-loading pod and a ready one alike. When the app ladder reaches WaitingForLaunch + (APP.cpp CheckLoad transition) the pod sends ONE empty route **-10** frame on its live relay + game socket (`BTRelayNotifyReady`, L4NET; socket mirrored at HELLO via + `BTRelayRememberGameSocket`); the relay prints `SEAT n READY tag='...' (k/m loaded)` and the + GUI lights the seat bright-green (`ST_READY`) with a "N pod(s) LOADED+READY" banner. Chain + verified LIVE by the operator: blue(seated) -> teal(registered) -> green(ready) in order. + ALSO the load-speed "mystery" resolved: mission loads are ~30s uncontended; the 1-2+ minute + loads happened when assistant background test runs (game instances + relay + cdb) ran + CONCURRENTLY with the user's interactive sessions on the same box -- see the memory note; + don't run game-spawning tests while the user is testing. - **ROUND RESET (2026-07-22) [T2]**: trim/prefs FINALIZE the egg for one round -- and that used to be permanent for the relay's lifetime, so a REJOIN after a mission (or crash) was served the stale finalized egg and its new callsign/mech request was recorded but never applied (the diff --git a/engine/MUNGA/APP.cpp b/engine/MUNGA/APP.cpp index 90996c1..eee79bf 100644 --- a/engine/MUNGA/APP.cpp +++ b/engine/MUNGA/APP.cpp @@ -1416,6 +1416,11 @@ void Tell("Sent ready message to ourselves\n"); } + { + // tell the operator this pod finished loading (READY light) + extern void BTRelayNotifyReady(void); + BTRelayNotifyReady(); + } { // relay launches that arrived MID-LOAD fire now (they could // not enter the queue then -- see L4APP.cpp pend counter) diff --git a/engine/MUNGA_L4/L4NET.CPP b/engine/MUNGA_L4/L4NET.CPP index 45a4f32..7fed3a3 100644 --- a/engine/MUNGA_L4/L4NET.CPP +++ b/engine/MUNGA_L4/L4NET.CPP @@ -159,8 +159,10 @@ enum RELAY_ROUTE_SEAT_REQUEST= -6, // pod->relay: assign me a free seat RELAY_ROUTE_SEAT_ASSIGN = -7, // relay->pod: int32 hostID + NUL tag RELAY_ROUTE_SEAT_FULL = -8, // relay->pod: roster full - RELAY_ROUTE_MATCHLOG = -9 // pod->relay: matchlog upload + RELAY_ROUTE_MATCHLOG = -9, // pod->relay: matchlog upload // (payload: filename NUL + file bytes) + RELAY_ROUTE_READY = -10 // pod->relay: mission load complete + // (operator's READY light; empty payload) }; #define RELAY_HELLO_MAGIC 0x31525442 // 'BTR1' little-endian // LAN auto-discovery (BT_RELAY=auto): probe broadcast on this UDP port; the @@ -180,6 +182,7 @@ struct RelayUdpEnvelope unsigned int sequence; }; void BTRelayRememberGameAddress(const SOCKADDR_IN &game_address); // fwd (defined below) +void BTRelayRememberGameSocket(SOCKET game_socket); // fwd (defined below) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // BTRelayWaitStatus -- progress text into the join.bat window during the @@ -593,6 +596,50 @@ void BTRelayRememberGameAddress(const SOCKADDR_IN &game_address) s_relayGameAddrCached = 1; } +// +// READY LIGHT (2026-07-22): the operator's roster shows "registered" for a +// still-loading pod and a ready one alike -- with 1-2 minute loads the +// operator can't tell who they'd be waiting on. When the app ladder reaches +// WaitingForLaunch (APP.cpp CheckLoadMessageHandler), the pod sends one +// empty route -10 frame on its live relay game socket; the relay announces +// SEAT n READY and the console GUI lights the seat green. Relay mode only; +// silent no-op otherwise. +// +static SOCKET s_relayGameSocketForReady = INVALID_SOCKET; + +void BTRelayRememberGameSocket(SOCKET game_socket) +{ + s_relayGameSocketForReady = game_socket; +} + +void BTRelayNotifyReady(void) +{ + static int s_ready_sent = 0; + if (s_ready_sent || s_relayGameSocketForReady == INVALID_SOCKET) + return; + s_ready_sent = 1; + RelayTcpEnvelope envelope; + envelope.route = RELAY_ROUTE_READY; + envelope.length = 0; + const char *data = (const char *)&envelope; + int done = 0, want = (int)sizeof(envelope); + unsigned long deadline = GetTickCount() + 2000UL; + while (done < want && GetTickCount() < deadline) + { + int sent = send(s_relayGameSocketForReady, data + done, want - done, 0); + if (sent > 0) + { + done += sent; + continue; + } + if (WSAGetLastError() != WSAEWOULDBLOCK) + break; + Sleep(10); + } + DEBUG_STREAM << "[launch] READY " << (done == want ? "sent" : "send FAILED") + << " to the relay" << std::endl << std::flush; +} + void BTRelayUploadMatchLog(void) { extern const char *BTMatchLogPath(); @@ -2072,6 +2119,7 @@ void L4NetworkManager::ConnectRelayGame(HostID local_host_ID) Fail("relay HELLO transmit failed\n"); } relayLocalHostID = local_host_ID; + BTRelayRememberGameSocket(relayGameSocket); // READY-light notifier DEBUG_STREAM << "[relay] game connection up, HELLO sent (hostID " << (int)local_host_ID << ")\n" << std::flush; diff --git a/tools/btconsole.py b/tools/btconsole.py index 0a069b1..881cb37 100644 --- a/tools/btconsole.py +++ b/tools/btconsole.py @@ -226,6 +226,7 @@ ROUTE_SEAT_REQUEST = -6 # client->relay: assign me a free roster seat ROUTE_SEAT_ASSIGN = -7 # relay->client: int32 hostID + NUL-terminated tag ROUTE_SEAT_FULL = -8 # relay->client: no free seats ROUTE_MATCHLOG = -9 # client->relay: matchlog upload (filename NUL + bytes) +ROUTE_READY = -10 # client->relay: mission load complete (READY light) # the 18 certified mech tags (mirror of the FE kVehicles catalog) VEHICLE_TAGS = {"blkhawk", "loki", "bhk1", "madcat", "thor", "owens", "own1", "thr1", "lok1", "mad1", "avatar", "ava1", @@ -925,6 +926,15 @@ class Relay: if other is not conn: self._send_control(other, ROUTE_PEER_UP, host_id) return + if route == ROUTE_READY: + conn.ready = True + ready_count = sum(1 for c in self.by_host.values() + if getattr(c, "ready", False)) + i = conn.host_id - FIRST_GAME_HOST_ID + tag = self.roster[i] if 0 <= i < len(self.roster) else "?" + print(f"[relay] SEAT {conn.host_id} READY tag='{tag}' " + f"({ready_count}/{len(self.by_host)} loaded)", flush=True) + return if route == ROUTE_BCAST: env = struct.pack(ENV_FMT_TCP, conn.host_id, len(payload)) for other in list(self.by_host.values()): diff --git a/tools/btoperator.py b/tools/btoperator.py index 98a1db3..e6e07ed 100644 --- a/tools/btoperator.py +++ b/tools/btoperator.py @@ -46,11 +46,12 @@ import eggmodel # noqa: E402 # --------------------------------------------------------------------------- ST_SEATED = "seated" +ST_READY = "ready" 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", - ST_SEATED: "#268bd2"} + ST_SEATED: "#268bd2", ST_READY: "#8be000"} class SessionMonitor: @@ -65,6 +66,7 @@ class SessionMonitor: RE_RELAY_SEAT = re.compile( r"SEAT (\d+) PRESENT tag='([^']*)' callsign='([^']*)' mech='([^']*)'") RE_RELAY_FREED = re.compile(r"SEAT (\d+) FREED tag='([^']*)'") + RE_RELAY_READY = re.compile(r"SEAT (\d+) READY tag='([^']*)'") RE_MESH_EGG = re.compile(r"\[([^\]]+)\] egg sent") RE_MESH_RUN = re.compile(r"\[([^\]]+)\] RunMission #(\d+) sent") @@ -112,10 +114,16 @@ class SessionMonitor: self.state[tag] = ST_IDLE self.seat_info.pop(tag, None) changed = True + m = self.RE_RELAY_READY.search(line) + if m: + tag = m.group(2) + if tag in self.state: + self.state[tag] = ST_READY + changed = True m = self.RE_RELAY_REG.search(line) if m: tag = self.host_tag(int(m.group(1))) - if tag: + if tag and self.state.get(tag) != ST_READY: self.state[tag] = ST_REGISTERED changed = True m = self.RE_RELAY_DOWN.search(line) @@ -713,11 +721,15 @@ class Operator(QMainWindow): parts.append(' %s %s' % (ST_COLORS[state], tag, state)) seated = sum(1 for s in self.monitor.state.values() if s == ST_SEATED) + ready_n = sum(1 for s in self.monitor.state.values() if s == ST_READY) if self.monitor.launched: head = "LAUNCHED — mission running" if (self.console_proc and not self.end_btn.isEnabled() and not getattr(self, "end_sent", False)): self.end_btn.setEnabled(True) + elif ready_n: + head = ("%d pod(s) LOADED+READY — LAUNCH starts them instantly" + % ready_n) elif self.monitor.ready: head = "ALL PODS READY — press LAUNCH MISSION" elif seated: