Connection audit: every connect/disconnect scenario handled + verified

Systematic actor-death x lifecycle-phase sweep (operator request after
the WaitingForEgg zombie).  New handling, all live-verified:

1. PRE-EGG console-pad loss (the real zombie phase): before the egg the
   pod's only link is the console pad -- the game socket doesn't exist,
   so the earlier RelayGameDown hook could never fire (the verification
   run itself caught this).  Detection now lives in
   HostDisconnectedMessageHandler ConsoleHostType (relayMode +
   pre-scene -> BTRelayRejoinNow); mesh keeps 1995 behavior.
2. MID-MISSION relay loss: the STOP is relay-sent (1995: the console
   owned the clock), so a pod losing its relay mid-match played a
   peer-less FOREVER-mission.  RelayGameDown (scene presented) now
   posts StopMissionMessage at +15s -> normal end -> lobby relaunch.
3. POST-RELEASE pod death stalled the round (survivors wait forever on
   the dead peer's connection gate): relay _abort_round -> route -11
   REJOIN to survivors -> everyone re-seats in seconds (reset-first
   ordering makes the drop cascade re-trigger-proof).
4. BTRelayRejoinNow carries BT_SEAT_CLAIM (mirrored tag): without it
   the rejoiner's own reclaim-hold blocked assignment -> ROSTER FULL
   loop for the 90s window (found by verification round 2).
5. Beacon NAT keepalive (SIO_KEEPALIVE_VALS 60s/10s) so long
   between-rounds waits can't be silently unseated.
6. BT_LOG_APPEND=1 preserves the log across lobby-loop generations
   (truncation erased round-1 verification evidence).

Verified non-issues: relay pods never bind the -net listener (no
double-launch collision on the internet path); mesh bind-fail exits
cleanly.  Full matrix + evidence: context/multiplayer.md;
scratchpad/audit_verify.sh is the repeatable rig.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-23 09:23:23 -05:00
co-authored by Claude Opus 4.8
parent 56ca2f5c13
commit c8cba8c764
4 changed files with 194 additions and 2 deletions
+127 -1
View File
@@ -17,6 +17,7 @@
#pragma hdrstop
#include "l4app.h"
#include "..\munga\APPMSG.h" // StopMissionMessage (mid-mission relay-loss fallback)
#include "l4host.h"
#include "l4net.h"
#include "..\munga\mission.h"
@@ -161,8 +162,12 @@ enum
RELAY_ROUTE_SEAT_FULL = -8, // relay->pod: roster full
RELAY_ROUTE_MATCHLOG = -9, // pod->relay: matchlog upload
// (payload: filename NUL + file bytes)
RELAY_ROUTE_READY = -10 // pod->relay: mission load complete
RELAY_ROUTE_READY = -10, // pod->relay: mission load complete
// (operator's READY light; empty payload)
RELAY_ROUTE_REJOIN = -11 // relay->pod: abandon this round and
// relaunch into the join wait (a peer
// died after egg release; the stalled
// round cannot complete)
};
#define RELAY_HELLO_MAGIC 0x31525442 // 'BTR1' little-endian
// LAN auto-discovery (BT_RELAY=auto): probe broadcast on this UDP port; the
@@ -627,6 +632,39 @@ const char *BTRelaySelfTag(void)
return s_relaySelfTag;
}
//
// BTRelayRejoinNow -- relaunch this pod into the patient join wait (the
// lobby-loop mechanism): used pre-mission when the relay dies OR when the
// relay orders a round abort (REJOIN). Strips the exe token off the full
// command line; never returns.
//
void BTRelayRejoinNow(void)
{
// carry the seat identity so the relaunched generation reclaims its
// EXACT seat immediately (without it, its own reclaim-hold blocks
// generic assignment for the whole 90s window -- verified live)
if (s_relaySelfTag[0] != 0)
SetEnvironmentVariableA("BT_SEAT_CLAIM", s_relaySelfTag);
const char *full_command = GetCommandLineA();
const char *arguments = full_command;
if (arguments != NULL)
{
if (*arguments == '"')
{
++arguments;
while (*arguments != 0 && *arguments != '"') ++arguments;
if (*arguments == '"') ++arguments;
}
else
{
while (*arguments != 0 && *arguments != ' ') ++arguments;
}
while (*arguments == ' ') ++arguments;
}
extern void BTFE_RelaunchSelfAndExit(const char *);
BTFE_RelaunchSelfAndExit(arguments != NULL ? arguments : "");
}
void BTRelayNotifyReady(void)
{
static int s_ready_sent = 0;
@@ -1647,6 +1685,24 @@ void L4NetworkManager::HostDisconnectedMessageHandler(HostDisconnectedMessage* H
break;
case ConsoleHostType:
{
//
// PRE-MISSION console loss in RELAY mode (connection audit
// 2026-07-23): before the egg arrives the pod's ONLY link is
// this console pad -- the game socket does not exist yet, so
// RelayGameDown can never catch an operator Stop Session here
// (the original WaitingForEgg zombie). Rejoin the patient
// walk-up; mesh mode keeps the 1995 console-less behavior.
//
{
extern Logical gBTSceneHasPresented;
if (relayMode && !gBTSceneHasPresented)
{
DEBUG_STREAM << "[relay] console pad lost pre-mission -- "
"relaunching into the join wait" << std::endl << std::flush;
extern void BTRelayRejoinNow(void);
BTRelayRejoinNow(); // never returns
}
}
//
// Knock down the number of consoles online
//
@@ -1881,6 +1937,18 @@ Logical L4NetworkManager::RelayRequestSeat()
}
if (got_seat)
{
// NAT KEEPALIVE (connection audit 2026-07-23): the beacon can sit
// idle for many minutes between rounds; a NAT/firewall idling the
// TCP out would silently free the seat. 60s probes keep the
// mapping alive (struct defined inline -- mstcpip.h equivalent).
{
struct { u_long onoff, keepalivetime, keepaliveinterval; }
keepalive = { 1, 60000, 10000 };
DWORD bytes_returned = 0;
WSAIoctl(seat_socket, 0x98000004 /*SIO_KEEPALIVE_VALS*/,
&keepalive, sizeof(keepalive), NULL, 0,
&bytes_returned, NULL, NULL);
}
// PRESENCE BEACON (2026-07-22, "launch with whoever connects"):
// KEEP the seat socket open for the process lifetime. The relay
// reads a live beacon as "this player is seated": the operator
@@ -2364,6 +2432,46 @@ void L4NetworkManager::RelayGameDown(const char *why)
relayPadTail = 0;
udpUp = False;
//
// PRE-MISSION relay death = the operator stopped/restarted the session
// while this pod was still waiting for its egg (field 2026-07-23: END
// MISSION -> lobby-loop relaunch -> STOP SESSION killed the relay ->
// the pod idled at WaitingForEgg on dead sockets FOREVER, and its stale
// -net listener then blocked any replacement -- the "no longer
// registering" zombie). The arcade-correct move: go back to the patient
// walk-up -- relaunch self into the join wait (the lobby-loop mechanism);
// the fresh process re-dials until the operator's next session appears.
// Mid-match (scene presented) keeps the existing peer-less behavior; the
// post-mission path has its own relaunch.
//
{
extern Logical gBTSceneHasPresented;
if (!gBTSceneHasPresented)
{
DEBUG_STREAM << "[relay] pre-mission relay loss -- relaunching "
"into the join wait" << std::endl << std::flush;
extern void BTRelayRejoinNow(void);
BTRelayRejoinNow(); // never returns
}
else
{
// MID-MISSION relay loss (connection audit 2026-07-23): the
// mission STOP is relay-sent (the 1995 console owned the clock),
// so a pod that loses its relay mid-match would otherwise play a
// peer-less FOREVER-mission. End it gracefully in 15s -- the
// normal stop path runs (fade, matchlog kept for manual send,
// lobby relaunch back to the patient join wait), so the player
// flows into the operator's next session automatically.
DEBUG_STREAM << "[relay] mid-mission relay loss -- ending the "
"mission in 15s (no console to end it)" << std::endl << std::flush;
Application::StopMissionMessage stop_message(0);
Time when = Now();
when += 15.0f;
application->Post(HighEventPriority, application,
&stop_message, when);
}
}
HostManager::RemoteHostIterator remote_hosts(application->GetHostManager());
Host *host;
while ((host = remote_hosts.ReadAndNext()) != NULL)
@@ -2457,6 +2565,24 @@ Logical L4NetworkManager::CheckRelay(NetworkPacket *network_packet)
client->ReceiveNetworkPacket(NULL, &peer_down);
}
}
else if (route == RELAY_ROUTE_REJOIN)
{
// ROUND ABORT (connection audit 2026-07-23): a peer died
// after egg release -- the engine's connection gate would
// wait on it forever, so the relay bounces everyone back to
// their seats for a fresh (re-trimmed) round. Mid-mission
// this cannot arrive (the relay only aborts pre-launch);
// guard anyway.
extern Logical gBTSceneHasPresented;
if (!gBTSceneHasPresented)
{
DEBUG_STREAM << "[relay] round ABORT from the operator "
"(a peer dropped mid-load) -- rejoining"
<< std::endl << std::flush;
extern void BTRelayRejoinNow(void);
BTRelayRejoinNow(); // never returns
}
}
else if (route >= FirstLegalHostID)
{
is_game_frame = True;