diff --git a/MUNGA_L4/L4NETTRANSPORT.cpp b/MUNGA_L4/L4NETTRANSPORT.cpp index 166d421..7fa4db9 100644 --- a/MUNGA_L4/L4NETTRANSPORT.cpp +++ b/MUNGA_L4/L4NETTRANSPORT.cpp @@ -2,8 +2,138 @@ #pragma hdrstop #include "l4nettransport.h" +#include "..\munga\appmgr.h" #include +//######################################################################## +// 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; + } + } } } diff --git a/MUNGA_L4/L4NETTRANSPORT.h b/MUNGA_L4/L4NETTRANSPORT.h index 16827e7..705aa73 100644 --- a/MUNGA_L4/L4NETTRANSPORT.h +++ b/MUNGA_L4/L4NETTRANSPORT.h @@ -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(); diff --git a/MUNGA_L4/L4STEAMTRANSPORT.cpp b/MUNGA_L4/L4STEAMTRANSPORT.cpp index ff10c5e..77bb641 100644 --- a/MUNGA_L4/L4STEAMTRANSPORT.cpp +++ b/MUNGA_L4/L4STEAMTRANSPORT.cpp @@ -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; + } + } } } diff --git a/RP_L4/RPL4CONSOLE.cpp b/RP_L4/RPL4CONSOLE.cpp index 8377db9..d270f9a 100644 --- a/RP_L4/RPL4CONSOLE.cpp +++ b/RP_L4/RPL4CONSOLE.cpp @@ -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) { diff --git a/RP_L4/RPL4ENVIRON.cpp b/RP_L4/RPL4ENVIRON.cpp index c32ad24..b7e8833 100644 --- a/RP_L4/RPL4ENVIRON.cpp +++ b/RP_L4/RPL4ENVIRON.cpp @@ -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"