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:
co-authored by
Claude Opus 4.8
parent
56ca2f5c13
commit
c8cba8c764
+127
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user