A window that answers while it waits

Launching a network race connects to each machine in turn and retries
while one is not listening yet, because they finish loading at different
moments. That part is deliberate and stays. What was not deliberate is
that the wait slept without pumping messages, so Windows saw a process
that had stopped answering and painted the whole thing "Not responding" -
for up to two minutes, with no indication of which peer was missing, how
long remained, or any way out. It runs before the engine block, so there
is no render loop keeping the window alive either.

Three changes, both transports:

Pump while waiting. Every sleep on the connect path goes through
NetTransport_PumpAndSleep, so the window keeps painting and can be moved.
It is re-entrancy guarded, because dispatching a message can run
application code that reaches a connect of its own, and nested pumping
would deliver messages twice and let an inner wait swallow the escape
meant for the outer one.

Shorten the deadline. Two minutes suited the arcade, where a pod that was
still booting would always answer eventually on a LAN with nothing else to
go wrong. Over the internet a machine silent for twenty seconds is not
coming. RP412CONNECTWAIT, 2 to 300, default 20, documented in environ.ini.

Say what is happening. The title bar names the peer and counts down, and
ESC gives up at once - the title being the one surface guaranteed to exist
this early, since there is no renderer yet to draw a progress screen with.
The cancel latch is cleared when a connect sequence begins so that an
escape pressed during one race cannot cancel the next.

The Winsock path is only partly fixed and the code now says so: connect()
there is still blocking, since the socket is only made nonblocking after
it succeeds, so an unreachable host - filtered rather than refused - still
sits in the OS SYN retry for around twenty seconds. Fixing that needs
FIONBIO before connect() and a select() on our own timeout. Left for when
LAN play comes up; a Steam host goes through SteamNetTransport::Connect,
which is fully covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-11 11:36:46 -05:00
co-authored by Claude Opus 5
parent b0b40559d5
commit 94e1cf2cf0
5 changed files with 314 additions and 8 deletions
+180 -2
View File
@@ -2,8 +2,138 @@
#pragma hdrstop
#include "l4nettransport.h"
#include "..\munga\appmgr.h"
#include <Ws2tcpip.h>
//########################################################################
// Waiting for a peer without hanging the window
//########################################################################
namespace
{
Logical gInWait = False; // re-entrancy guard
Logical gWaitCancelled = False;
char gOriginalTitle[256] = "";
Logical gTitleSaved = False;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// How long Connect keeps redialling.
//
// It was two minutes, which was right for what it was written for: an
// arcade console redialling a pod that is still booting and WILL answer,
// on a LAN with nothing else to go wrong. Over Steam the same two minutes
// is a host staring at a dead window, because a peer that has not answered
// in twenty seconds is not coming - it never launched, or it is behind
// something the relay cannot cross.
//
int
NetTransport_ConnectWaitSeconds()
{
static int cached = -1;
if (cached < 0)
{
const char *setting = getenv("RP412CONNECTWAIT");
cached = (setting != NULL) ? atoi(setting) : 20;
if (cached < 2) { cached = 2; }
if (cached > 300) { cached = 300; }
}
return cached;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
NetTransport_SetWaitProgress(const char *text)
{
if (ghWnd == 0)
{
return;
}
if (!gTitleSaved)
{
GetWindowTextA(ghWnd, gOriginalTitle, sizeof(gOriginalTitle) - 1);
gTitleSaved = True;
}
//
// The title bar because it is the one surface guaranteed to exist
// here: this runs before the engine block, so there is no renderer to
// draw a progress screen with yet.
//
SetWindowTextA(ghWnd, (text != NULL) ? text : gOriginalTitle);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
NetWaitResult
NetTransport_PumpAndSleep(int milliseconds)
{
//
// Dispatching a message can run application code, and that code can
// reach a connect of its own. Nested pumping would then deliver the
// same messages twice and let an inner wait consume the escape meant
// for the outer one, so an inner call just sleeps.
//
if (gInWait)
{
Sleep(milliseconds);
return NetWaitContinue;
}
gInWait = True;
NetWaitResult result = NetWaitContinue;
MSG message;
while (PeekMessage(&message, NULL, 0, 0, PM_REMOVE))
{
if (message.message == WM_QUIT)
{
//
// Put it back: the message loop that owns the shutdown has
// to see this, not us.
//
PostQuitMessage((int) message.wParam);
result = NetWaitQuit;
break;
}
if (message.message == WM_KEYDOWN && message.wParam == VK_ESCAPE)
{
gWaitCancelled = True;
}
TranslateMessage(&message);
DispatchMessage(&message);
}
if (result == NetWaitContinue && gWaitCancelled)
{
result = NetWaitCancelled;
}
if (result == NetWaitContinue)
{
Sleep(milliseconds);
}
gInWait = False;
return result;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Cleared when a connect sequence starts, so an escape pressed during one
// race does not cancel the next one.
//
void
NetTransport_ClearWaitCancel()
{
gWaitCancelled = False;
}
//########################################################################
// WinsockNetTransport - the TCP wire the arcade always used, moved
// verbatim out of L4NET.CPP behind the NetTransport seam. Behavior
@@ -117,7 +247,28 @@ namespace
// the peer may not be listening yet. A refused TCP socket
// is dead - every attempt needs a fresh one.
//
DWORD deadline = GetTickCount() + 120 * 1000;
//
// NOTE the connect() below is still BLOCKING - the socket only
// goes nonblocking once it has succeeded - so an unreachable
// host (filtered rather than refused) sits in the OS SYN retry
// for around twenty seconds with nothing we can do about it.
// Pumping between redials fixes the refused case, which is the
// common one; the unreachable case needs the socket made
// nonblocking before connect() and a select() on our own
// timeout. Not done here because this is the LAN/direct path -
// a Steam host goes through SteamNetTransport::Connect.
//
DWORD deadline =
GetTickCount() + (DWORD) NetTransport_ConnectWaitSeconds() * 1000;
char progress[256];
sprintf(
progress,
"Red Planet - connecting to %s ...",
inet_ntoa(remote->sin_addr)
);
NetTransport_SetWaitProgress(progress);
for (;;)
{
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
@@ -164,6 +315,7 @@ namespace
closesocket(sock);
return InvalidConnection;
}
NetTransport_SetWaitProgress(NULL);
return (Connection) sock;
}
@@ -174,9 +326,35 @@ namespace
{
DEBUG_STREAM << "ERROR: connect() failed with "
<< wsa_error << "!\n" << std::flush;
NetTransport_SetWaitProgress(NULL);
return InvalidConnection;
}
Sleep(250); // peer not up yet - redial
//
// Peer not up yet - redial, answering the window meanwhile.
//
DWORD left = (DWORD)((LONG)(deadline - GetTickCount()) / 1000);
sprintf(
progress,
"Red Planet - connecting to %s ... %us left, ESC to cancel",
inet_ntoa(remote->sin_addr),
(unsigned) left
);
NetTransport_SetWaitProgress(progress);
for (int slept = 0; slept < 250; slept += 50)
{
NetWaitResult wait = NetTransport_PumpAndSleep(50);
if (wait != NetWaitContinue)
{
DEBUG_STREAM << "Connect "
<< ((wait == NetWaitCancelled) ? "cancelled" : "abandoned, quitting")
<< " by the user\n" << std::flush;
NetTransport_SetWaitProgress(NULL);
return InvalidConnection;
}
}
}
}
+41
View File
@@ -124,3 +124,44 @@ NetTransport *
NetTransport_Get();
void
NetTransport_Set(NetTransport *transport);
//########################################################################
// Waiting for a peer without hanging the window.
//
// Connect retries until the peer answers, because the egg-ACK ordering
// means it may still be booting - that part is deliberate. What was not
// deliberate is that the wait slept without pumping messages, so Windows
// saw a process that had stopped answering and painted the whole thing
// "Not responding" for up to two minutes. It happens before the engine
// block, so there is no render loop keeping the window alive either.
//
// Sleep through this instead. It pumps, so the window keeps drawing and
// can be moved; it watches for a cancel; and it is re-entrancy guarded,
// because dispatching a message can run application code that reaches a
// connect of its own.
//########################################################################
enum NetWaitResult
{
NetWaitContinue, // carry on waiting
NetWaitCancelled, // the user pressed escape
NetWaitQuit // WM_QUIT arrived; the caller must unwind
};
NetWaitResult
NetTransport_PumpAndSleep(int milliseconds);
// How long Connect keeps redialling, in seconds. RP412CONNECTWAIT
// overrides; see the definition for why the default came down from 120.
int
NetTransport_ConnectWaitSeconds();
// Progress, shown in the window title while a connect is outstanding.
// Pass NULL to put the original title back.
void
NetTransport_SetWaitProgress(const char *text);
// Forget a previous escape, so one race's cancel cannot cancel the next.
// Call before starting a connect sequence.
void
NetTransport_ClearWaitCancel();
+71 -6
View File
@@ -385,10 +385,29 @@ namespace
target.Clear();
target.SetIPv4(remote_fake_ip, fake_port);
// mirror the TCP retry-while-refused loop, bounded: the
// egg-ACK ordering means the peer may not be listening yet
DWORD deadline = GetTickCount() + 120 * 1000;
//
// Mirror the TCP retry-while-refused loop, bounded: the
// egg-ACK ordering means the peer may not be listening yet.
//
// Every wait below goes through NetTransport_PumpAndSleep, so
// the window keeps painting and answering instead of being
// declared "Not responding" for the whole attempt. There is no
// render loop yet at this point - this runs before the engine
// block - so nothing else is keeping it alive.
//
DWORD wait_seconds = (DWORD) NetTransport_ConnectWaitSeconds();
DWORD started = GetTickCount();
DWORD deadline = started + wait_seconds * 1000;
int attempt = 0;
char progress[256];
sprintf(
progress,
"Red Planet - connecting to %s ...",
inet_ntoa(remote->sin_addr)
);
NetTransport_SetWaitProgress(progress);
for (;;)
{
++attempt;
@@ -397,6 +416,7 @@ namespace
if (handle == k_HSteamNetConnection_Invalid)
{
DEBUG_STREAM << "SteamNetTransport: ConnectByIPAddress refused the call\n" << std::flush;
NetTransport_SetWaitProgress(NULL);
return InvalidConnection;
}
AddConnection(handle, remote->sin_addr.S_un.S_addr, remote->sin_port);
@@ -425,7 +445,34 @@ namespace
{
break;
}
Sleep(25);
//
// Count down out loud. Two minutes of a silent frozen
// window gave a host nothing to act on - not which peer
// was missing, not how long was left, not a way out.
//
DWORD left = (DWORD)((LONG)(deadline - GetTickCount()) / 1000);
sprintf(
progress,
"Red Planet - connecting to %s ... %us left, ESC to cancel",
inet_ntoa(remote->sin_addr),
(unsigned) left
);
NetTransport_SetWaitProgress(progress);
NetWaitResult wait = NetTransport_PumpAndSleep(25);
if (wait != NetWaitContinue)
{
DEBUG_STREAM << "SteamNetTransport: connect "
<< ((wait == NetWaitCancelled) ? "cancelled" : "abandoned, quitting")
<< " after " << ((GetTickCount() - started) / 1000)
<< "s\n" << std::flush;
SteamNetworkingSockets()->CloseConnection(handle, 0, "cancelled", false);
RemoveConnection(handle);
NetTransport_SetWaitProgress(NULL);
return InvalidConnection;
}
}
if (state == k_ESteamNetworkingConnectionState_Connected)
{
@@ -436,6 +483,7 @@ namespace
}
DEBUG_STREAM << "SteamNetTransport: connect succeeded (attempt "
<< attempt << ")\n" << std::flush;
NetTransport_SetWaitProgress(NULL);
return (Connection) handle;
}
@@ -446,10 +494,27 @@ namespace
RemoveConnection(handle);
if ((LONG)(GetTickCount() - deadline) >= 0)
{
DEBUG_STREAM << "SteamNetTransport: connect timed out\n" << std::flush;
DEBUG_STREAM << "SteamNetTransport: connect timed out after "
<< wait_seconds << "s (RP412CONNECTWAIT)\n" << std::flush;
NetTransport_SetWaitProgress(NULL);
return InvalidConnection;
}
Sleep(1000);
//
// A second between redials, still answering the window.
//
for (int slept = 0; slept < 1000; slept += 50)
{
NetWaitResult wait = NetTransport_PumpAndSleep(50);
if (wait != NetWaitContinue)
{
DEBUG_STREAM << "SteamNetTransport: connect "
<< ((wait == NetWaitCancelled) ? "cancelled" : "abandoned, quitting")
<< " between attempts\n" << std::flush;
NetTransport_SetWaitProgress(NULL);
return InvalidConnection;
}
}
}
}
+6
View File
@@ -687,6 +687,12 @@ Logical
// booting; runs before the engine block so nothing is waiting.
//
NetTransport_Get()->Startup();
//
// An escape pressed during the last race must not cancel this one.
//
NetTransport_ClearWaitCancel();
const char *cursor = remote_pod_list;
while (*cursor != '\0' && gRemotePodCount < maxRemotePods)
{
+16
View File
@@ -321,6 +321,22 @@ namespace
"# the old arrival-time behaviour if you want to compare.\n"
"#RP412NETCLOCK=0\n"
"\n"
"# How long to keep redialling a player who has not answered yet, in\n"
"# seconds. 2 to 300, default 20.\n"
"#\n"
"# Launching a race connects to each machine in turn, and retries while\n"
"# one is not listening yet, because they finish loading at different\n"
"# moments. The wait used to be two minutes, which suited the arcade: a\n"
"# pod that was still booting would always answer eventually, on a LAN\n"
"# with nothing else to go wrong. Over the internet a machine that has\n"
"# been silent for twenty seconds is not coming - it never launched, or\n"
"# something between you cannot be crossed - so the default is twenty.\n"
"#\n"
"# The window stays alive throughout and the title bar counts down, and\n"
"# ESC gives up immediately. Raise this if you play with someone whose\n"
"# machine loads very slowly.\n"
"RP412CONNECTWAIT=20\n"
"\n"
"# ---- Test harness -----------------------------------------------------------\n"
"\n"
"# The knobs that make a run repeatable and measurable. All are off\n"