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>
1016 lines
28 KiB
C++
1016 lines
28 KiB
C++
#include "mungal4.h"
|
|
#pragma hdrstop
|
|
|
|
#include "l4steamtransport.h"
|
|
|
|
//########################################################################
|
|
// Is steam_api.dll actually here?
|
|
//
|
|
// Defined outside the RP412_STEAM guard on purpose: callers ask without
|
|
// caring how the build was configured, and a build without the SDK
|
|
// truthfully has no client library. See the header for why every Steam
|
|
// path has to come through here first.
|
|
//
|
|
// The answer is cached because it is asked on menu paint, and because a
|
|
// DLL that appeared halfway through a session is not a case worth
|
|
// supporting - the delay-load helper would have bound the first miss
|
|
// anyway.
|
|
//########################################################################
|
|
Logical
|
|
SteamNetTransport_ClientLibraryPresent()
|
|
{
|
|
#ifdef RP412_STEAM
|
|
static int present = -1;
|
|
|
|
if (present < 0)
|
|
{
|
|
present = (LoadLibraryA("steam_api.dll") != NULL) ? 1 : 0;
|
|
|
|
if (!present)
|
|
{
|
|
DEBUG_STREAM << "Steam: steam_api.dll not found beside the exe - "
|
|
<< "Steam features off, staying on TCP\n" << std::flush;
|
|
}
|
|
}
|
|
return present ? True : False;
|
|
#else
|
|
return False;
|
|
#endif
|
|
}
|
|
|
|
#ifdef RP412_STEAM
|
|
|
|
#include "l4nettransport.h"
|
|
|
|
// The engine builds with /Zp1; Valve's ABI expects default packing.
|
|
// Callback structs carry their own pack pragmas, but everything else
|
|
// must see the platform default.
|
|
#pragma pack(push, 8)
|
|
#include "steam/steam_api.h"
|
|
#include "steam/isteamnetworkingsockets.h"
|
|
#include "steam/isteamnetworkingutils.h"
|
|
#include "steam/steamnetworkingfakeip.h"
|
|
#pragma pack(pop)
|
|
|
|
//########################################################################
|
|
// SteamNetTransport - see l4steamtransport.h for the design. The
|
|
// engine is single threaded and so is this: every entry point pumps
|
|
// SteamAPI_RunCallbacks, which is where the status-changed callback
|
|
// fires (accepting incoming connections and marking drops).
|
|
//########################################################################
|
|
|
|
namespace
|
|
{
|
|
enum
|
|
{
|
|
maxListeners = 4,
|
|
maxConnections = 32,
|
|
maxPeers = 16,
|
|
maxPending = 8,
|
|
leftoverSize = 4096
|
|
};
|
|
|
|
struct ListenerRecord
|
|
{
|
|
HSteamListenSocket handle;
|
|
int fakePortIndex;
|
|
unsigned short enginePort; // host order
|
|
HSteamNetConnection pending[maxPending];
|
|
int pendingCount;
|
|
// The engine closes listeners with TCP semantics (accepted
|
|
// connections survive). Steam's CloseListenSocket kills the
|
|
// accepted connections ungracefully - so an engine close only
|
|
// marks the listener here; the real close waits for Cleanup.
|
|
Logical closed;
|
|
};
|
|
|
|
struct ConnectionRecord
|
|
{
|
|
HSteamNetConnection handle;
|
|
unsigned long remoteIP; // network order
|
|
unsigned short remoteEnginePort; // network order
|
|
Logical connected;
|
|
Logical dead;
|
|
char leftover[leftoverSize];
|
|
int leftoverCount;
|
|
};
|
|
|
|
struct PeerRecord
|
|
{
|
|
unsigned long fakeIP; // host order
|
|
unsigned short fakeConsolePort; // host order
|
|
unsigned short fakeGamePort; // host order
|
|
unsigned __int64 steamID; // identity -> global FakeIP on accept
|
|
};
|
|
|
|
// engine-side port convention (lobby default; game port is +1)
|
|
unsigned short gEngineConsolePort = 1501;
|
|
|
|
Logical gSteamReady = False;
|
|
unsigned long gLocalFakeIP = 0; // host order
|
|
unsigned short gLocalFakePorts[2] = { 0, 0 };
|
|
char gLocalFakeAddressString[32] = "";
|
|
|
|
// engine ports in the order Listen sees them: [0] console, [1] game
|
|
unsigned short gEnginePorts[2] = { 0, 0 };
|
|
int gEnginePortCount = 0;
|
|
|
|
ListenerRecord gListeners[maxListeners];
|
|
int gListenerCount = 0;
|
|
ConnectionRecord gConnections[maxConnections];
|
|
int gConnectionCount = 0;
|
|
PeerRecord gPeers[maxPeers];
|
|
int gPeerCount = 0;
|
|
|
|
ListenerRecord *
|
|
FindListener(HSteamListenSocket handle)
|
|
{
|
|
for (int i = 0; i < gListenerCount; ++i)
|
|
{
|
|
if (gListeners[i].handle == handle)
|
|
{
|
|
return &gListeners[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
ConnectionRecord *
|
|
FindConnection(HSteamNetConnection handle)
|
|
{
|
|
for (int i = 0; i < gConnectionCount; ++i)
|
|
{
|
|
if (gConnections[i].handle == handle)
|
|
{
|
|
return &gConnections[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
ConnectionRecord *
|
|
AddConnection(
|
|
HSteamNetConnection handle,
|
|
unsigned long remote_ip_net,
|
|
unsigned short remote_engine_port_net
|
|
)
|
|
{
|
|
if (gConnectionCount >= maxConnections)
|
|
{
|
|
return NULL;
|
|
}
|
|
ConnectionRecord *record = &gConnections[gConnectionCount++];
|
|
memset(record, 0, sizeof(*record));
|
|
record->handle = handle;
|
|
record->remoteIP = remote_ip_net;
|
|
record->remoteEnginePort = remote_engine_port_net;
|
|
return record;
|
|
}
|
|
|
|
void
|
|
RemoveConnection(HSteamNetConnection handle)
|
|
{
|
|
for (int i = 0; i < gConnectionCount; ++i)
|
|
{
|
|
if (gConnections[i].handle == handle)
|
|
{
|
|
gConnections[i] = gConnections[gConnectionCount - 1];
|
|
--gConnectionCount;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// engine port in a SOCKADDR_IN -> the peer's matching fake port
|
|
unsigned short
|
|
LookupPeerFakePort(unsigned long fake_ip_host, unsigned short engine_port)
|
|
{
|
|
for (int i = 0; i < gPeerCount; ++i)
|
|
{
|
|
if (gPeers[i].fakeIP == fake_ip_host)
|
|
{
|
|
return (engine_port == gEngineConsolePort)
|
|
? gPeers[i].fakeConsolePort
|
|
: gPeers[i].fakeGamePort;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Status-changed handler (fires inside SteamAPI_RunCallbacks on
|
|
// the game thread): accept incoming, queue connected, mark drops.
|
|
// Registered through the CCallback dispatcher below - the
|
|
// config-value function pointer is NOT dispatched by the steam_api
|
|
// flavor of the library (Valve's own example uses STEAM_CALLBACK).
|
|
//---------------------------------------------------------------
|
|
void
|
|
OnConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t *status)
|
|
{
|
|
switch (status->m_info.m_eState)
|
|
{
|
|
case k_ESteamNetworkingConnectionState_Connecting:
|
|
if (status->m_info.m_hListenSocket != k_HSteamListenSocket_Invalid)
|
|
{
|
|
// no more callers once the engine closed the listener
|
|
ListenerRecord *listener = FindListener(status->m_info.m_hListenSocket);
|
|
if (listener == NULL || listener->closed)
|
|
{
|
|
DEBUG_STREAM << "SteamNetTransport: incoming ["
|
|
<< status->m_info.m_szConnectionDescription
|
|
<< "] on a closed listener - rejecting\n" << std::flush;
|
|
SteamNetworkingSockets()->CloseConnection(
|
|
status->m_hConn, 0, "listener closed", false);
|
|
break;
|
|
}
|
|
|
|
// incoming: accept immediately, queue it when Connected
|
|
DEBUG_STREAM << "SteamNetTransport: incoming ["
|
|
<< status->m_info.m_szConnectionDescription
|
|
<< "] - accepting\n" << std::flush;
|
|
EResult accepted = SteamNetworkingSockets()->AcceptConnection(status->m_hConn);
|
|
if (accepted != k_EResultOK)
|
|
{
|
|
DEBUG_STREAM << "SteamNetTransport: AcceptConnection failed (EResult "
|
|
<< (int) accepted << ")\n" << std::flush;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case k_ESteamNetworkingConnectionState_Connected:
|
|
DEBUG_STREAM << "SteamNetTransport: connected ["
|
|
<< status->m_info.m_szConnectionDescription << "]\n" << std::flush;
|
|
if (status->m_info.m_hListenSocket != k_HSteamListenSocket_Invalid)
|
|
{
|
|
ListenerRecord *listener = FindListener(status->m_info.m_hListenSocket);
|
|
if (listener != NULL && listener->pendingCount < maxPending)
|
|
{
|
|
listener->pending[listener->pendingCount++] = status->m_hConn;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ConnectionRecord *record = FindConnection(status->m_hConn);
|
|
if (record != NULL)
|
|
{
|
|
record->connected = True;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case k_ESteamNetworkingConnectionState_ProblemDetectedLocally:
|
|
case k_ESteamNetworkingConnectionState_ClosedByPeer:
|
|
{
|
|
DEBUG_STREAM << "SteamNetTransport: dropped ["
|
|
<< status->m_info.m_szConnectionDescription
|
|
<< "] end reason " << status->m_info.m_eEndReason
|
|
<< ": " << status->m_info.m_szEndDebug << "\n" << std::flush;
|
|
ConnectionRecord *record = FindConnection(status->m_hConn);
|
|
if (record != NULL)
|
|
{
|
|
record->dead = True;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// The callback listener: constructed after SteamAPI_Init so the
|
|
// CCallback registration lands in a live callback manager
|
|
//---------------------------------------------------------------
|
|
class SteamTransportCallbacks
|
|
{
|
|
public:
|
|
STEAM_CALLBACK(SteamTransportCallbacks, OnStatusChanged,
|
|
SteamNetConnectionStatusChangedCallback_t);
|
|
};
|
|
|
|
void SteamTransportCallbacks::OnStatusChanged(
|
|
SteamNetConnectionStatusChangedCallback_t *status)
|
|
{
|
|
OnConnectionStatusChanged(status);
|
|
}
|
|
|
|
SteamTransportCallbacks *gCallbacks = NULL;
|
|
|
|
//---------------------------------------------------------------
|
|
// The transport
|
|
//---------------------------------------------------------------
|
|
class SteamNetTransport:
|
|
public NetTransport
|
|
{
|
|
public:
|
|
Logical
|
|
Startup()
|
|
{
|
|
// Install() already brought Steam up; the network manager
|
|
// just confirms the transport is live
|
|
return gSteamReady;
|
|
}
|
|
|
|
void
|
|
Cleanup()
|
|
{
|
|
// mission teardown (single-binary race loop): drop the
|
|
// wire but keep the Steam API up - the lobby lives on
|
|
for (int i = 0; i < gConnectionCount; ++i)
|
|
{
|
|
SteamNetworkingSockets()->CloseConnection(
|
|
gConnections[i].handle, 0, "mission teardown", true);
|
|
}
|
|
gConnectionCount = 0;
|
|
for (int j = 0; j < gListenerCount; ++j)
|
|
{
|
|
SteamNetworkingSockets()->CloseListenSocket(gListeners[j].handle);
|
|
}
|
|
gListenerCount = 0;
|
|
gEnginePortCount = 0;
|
|
}
|
|
|
|
int
|
|
GetLocalAddresses(
|
|
unsigned long *addresses,
|
|
int max_count
|
|
)
|
|
{
|
|
if (!gSteamReady || max_count < 1)
|
|
{
|
|
return 0;
|
|
}
|
|
addresses[0] = htonl(gLocalFakeIP);
|
|
return 1;
|
|
}
|
|
|
|
Logical
|
|
Resolve(
|
|
const char *host_name,
|
|
SOCKADDR_IN *address
|
|
)
|
|
{
|
|
memset(address, 0, sizeof(SOCKADDR_IN));
|
|
address->sin_family = AF_INET;
|
|
|
|
SteamNetworkingIPAddr parsed;
|
|
if (!parsed.ParseString(host_name))
|
|
{
|
|
return False;
|
|
}
|
|
address->sin_addr.S_un.S_addr = htonl(parsed.GetIPv4());
|
|
address->sin_port = htons(parsed.m_port); // 0 when absent
|
|
return True;
|
|
}
|
|
|
|
Connection
|
|
Connect(
|
|
const SOCKADDR_IN *remote,
|
|
int local_port
|
|
)
|
|
{
|
|
unsigned long remote_fake_ip = ntohl(remote->sin_addr.S_un.S_addr);
|
|
unsigned short fake_port =
|
|
LookupPeerFakePort(remote_fake_ip, ntohs(remote->sin_port));
|
|
if (fake_port == 0)
|
|
{
|
|
DEBUG_STREAM << "SteamNetTransport: no registered peer for "
|
|
<< inet_ntoa(remote->sin_addr) << " - lobby did not feed it\n" << std::flush;
|
|
return InvalidConnection;
|
|
}
|
|
|
|
DEBUG_STREAM << "SteamNetTransport: connecting to "
|
|
<< inet_ntoa(remote->sin_addr) << ":" << ntohs(remote->sin_port)
|
|
<< " (fake port " << fake_port << ")...\n" << std::flush;
|
|
|
|
SteamNetworkingIPAddr target;
|
|
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.
|
|
//
|
|
// 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;
|
|
HSteamNetConnection handle =
|
|
SteamNetworkingSockets()->ConnectByIPAddress(target, 0, NULL);
|
|
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);
|
|
|
|
//
|
|
// Poll the connection state directly (the callback also
|
|
// runs, for the log and the accept queues)
|
|
//
|
|
ESteamNetworkingConnectionState state =
|
|
k_ESteamNetworkingConnectionState_Connecting;
|
|
while ((LONG)(GetTickCount() - deadline) < 0)
|
|
{
|
|
SteamAPI_RunCallbacks();
|
|
|
|
SteamNetConnectionInfo_t info;
|
|
if (!SteamNetworkingSockets()->GetConnectionInfo(handle, &info))
|
|
{
|
|
state = k_ESteamNetworkingConnectionState_Dead;
|
|
break;
|
|
}
|
|
state = info.m_eState;
|
|
if (state == k_ESteamNetworkingConnectionState_Connected ||
|
|
state == k_ESteamNetworkingConnectionState_ProblemDetectedLocally ||
|
|
state == k_ESteamNetworkingConnectionState_ClosedByPeer ||
|
|
state == k_ESteamNetworkingConnectionState_None)
|
|
{
|
|
break;
|
|
}
|
|
|
|
//
|
|
// 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)
|
|
{
|
|
ConnectionRecord *record = FindConnection(handle);
|
|
if (record != NULL)
|
|
{
|
|
record->connected = True;
|
|
}
|
|
DEBUG_STREAM << "SteamNetTransport: connect succeeded (attempt "
|
|
<< attempt << ")\n" << std::flush;
|
|
NetTransport_SetWaitProgress(NULL);
|
|
return (Connection) handle;
|
|
}
|
|
|
|
// attempt failed - drop it and retry until the deadline
|
|
DEBUG_STREAM << "SteamNetTransport: attempt " << attempt
|
|
<< " ended in state " << (int) state << "\n" << std::flush;
|
|
SteamNetworkingSockets()->CloseConnection(handle, 0, "retry", false);
|
|
RemoveConnection(handle);
|
|
if ((LONG)(GetTickCount() - deadline) >= 0)
|
|
{
|
|
DEBUG_STREAM << "SteamNetTransport: connect timed out after "
|
|
<< wait_seconds << "s (RP412CONNECTWAIT)\n" << std::flush;
|
|
NetTransport_SetWaitProgress(NULL);
|
|
return InvalidConnection;
|
|
}
|
|
|
|
//
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Connection
|
|
Listen(
|
|
int local_port,
|
|
int backlog
|
|
)
|
|
{
|
|
if (gListenerCount >= maxListeners)
|
|
{
|
|
return InvalidConnection;
|
|
}
|
|
|
|
// a logically-closed listener for this engine port reopens
|
|
// (the Steam socket outlives engine closes; see Close)
|
|
for (int existing = 0; existing < gListenerCount; ++existing)
|
|
{
|
|
if (gListeners[existing].enginePort == (unsigned short) local_port)
|
|
{
|
|
gListeners[existing].closed = False;
|
|
gListeners[existing].pendingCount = 0;
|
|
DEBUG_STREAM << "SteamNetTransport: reopened listener on engine port "
|
|
<< local_port << "\n" << std::flush;
|
|
return (Connection) gListeners[existing].handle;
|
|
}
|
|
}
|
|
|
|
// engine port -> fake port index, in order of appearance:
|
|
// the console channel always listens first, the mesh second
|
|
int index = -1;
|
|
for (int i = 0; i < gEnginePortCount; ++i)
|
|
{
|
|
if (gEnginePorts[i] == (unsigned short) local_port)
|
|
{
|
|
index = i;
|
|
}
|
|
}
|
|
if (index < 0)
|
|
{
|
|
if (gEnginePortCount >= 2)
|
|
{
|
|
DEBUG_STREAM << "SteamNetTransport: only two fake ports allocated!\n" << std::flush;
|
|
return InvalidConnection;
|
|
}
|
|
index = gEnginePortCount;
|
|
gEnginePorts[gEnginePortCount++] = (unsigned short) local_port;
|
|
}
|
|
|
|
DEBUG_STREAM << "SteamNetTransport: listening on engine port "
|
|
<< local_port << " (fake port " << gLocalFakePorts[index]
|
|
<< ")...\n" << std::flush;
|
|
|
|
HSteamListenSocket handle =
|
|
SteamNetworkingSockets()->CreateListenSocketP2PFakeIP(index, 0, NULL);
|
|
if (handle == k_HSteamListenSocket_Invalid)
|
|
{
|
|
return InvalidConnection;
|
|
}
|
|
|
|
ListenerRecord *listener = &gListeners[gListenerCount++];
|
|
memset(listener, 0, sizeof(*listener));
|
|
listener->handle = handle;
|
|
listener->fakePortIndex = index;
|
|
listener->enginePort = (unsigned short) local_port;
|
|
return (Connection) handle;
|
|
}
|
|
|
|
Connection
|
|
Accept(Connection listener_handle)
|
|
{
|
|
SteamAPI_RunCallbacks();
|
|
|
|
ListenerRecord *listener = FindListener((HSteamListenSocket) listener_handle);
|
|
if (listener == NULL || listener->closed || listener->pendingCount == 0)
|
|
{
|
|
return InvalidConnection;
|
|
}
|
|
|
|
HSteamNetConnection handle = listener->pending[0];
|
|
for (int i = 1; i < listener->pendingCount; ++i)
|
|
{
|
|
listener->pending[i - 1] = listener->pending[i];
|
|
}
|
|
--listener->pendingCount;
|
|
|
|
//
|
|
// The remote engine port is the same engine port we accept
|
|
// on (all pods share the -net convention under Steam). The
|
|
// caller's address: Steam reports incoming connections
|
|
// under a locally-allocated ALIAS FakeIP, so resolve the
|
|
// caller's identity through the peer table to the GLOBAL
|
|
// FakeIP the egg's [pilots] list promised - the engine's
|
|
// mesh identity checks compare against that.
|
|
//
|
|
unsigned long remote_ip_net = 0;
|
|
SteamNetConnectionInfo_t info;
|
|
if (SteamNetworkingSockets()->GetConnectionInfo(handle, &info))
|
|
{
|
|
unsigned __int64 caller_id = info.m_identityRemote.GetSteamID64();
|
|
for (int p = 0; p < gPeerCount; ++p)
|
|
{
|
|
if (gPeers[p].steamID != 0 && gPeers[p].steamID == caller_id)
|
|
{
|
|
remote_ip_net = htonl(gPeers[p].fakeIP);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (remote_ip_net == 0)
|
|
{
|
|
// unregistered caller: fall back to the alias address
|
|
SteamNetworkingIPAddr remote_fake;
|
|
if (SteamNetworkingSockets()->GetRemoteFakeIPForConnection(
|
|
handle, &remote_fake) == k_EResultOK)
|
|
{
|
|
remote_ip_net = htonl(remote_fake.GetIPv4());
|
|
}
|
|
}
|
|
ConnectionRecord *record = AddConnection(
|
|
handle, remote_ip_net, htons(listener->enginePort));
|
|
if (record != NULL)
|
|
{
|
|
record->connected = True;
|
|
}
|
|
return (Connection) handle;
|
|
}
|
|
|
|
void
|
|
Close(Connection connection)
|
|
{
|
|
ListenerRecord *listener = FindListener((HSteamListenSocket) connection);
|
|
if (listener != NULL)
|
|
{
|
|
//
|
|
// TCP semantics: accepted connections must survive a
|
|
// listener close. Steam's CloseListenSocket kills them
|
|
// ungracefully, so only mark it closed (rejecting any
|
|
// new callers) and destroy it for real in Cleanup.
|
|
//
|
|
listener->closed = True;
|
|
for (int i = 0; i < listener->pendingCount; ++i)
|
|
{
|
|
SteamNetworkingSockets()->CloseConnection(
|
|
listener->pending[i], 0, "listener closed", false);
|
|
}
|
|
listener->pendingCount = 0;
|
|
return;
|
|
}
|
|
SteamNetworkingSockets()->CloseConnection(
|
|
(HSteamNetConnection) connection, 0, "closed", true);
|
|
RemoveConnection((HSteamNetConnection) connection);
|
|
}
|
|
|
|
int
|
|
Send(
|
|
Connection connection,
|
|
const void *data,
|
|
int size
|
|
)
|
|
{
|
|
EResult result = SteamNetworkingSockets()->SendMessageToConnection(
|
|
(HSteamNetConnection) connection, data, (uint32) size,
|
|
k_nSteamNetworkingSend_Reliable, NULL);
|
|
return (result == k_EResultOK) ? size : -1;
|
|
}
|
|
|
|
int
|
|
Receive(
|
|
Connection connection,
|
|
void *buffer,
|
|
int size
|
|
)
|
|
{
|
|
SteamAPI_RunCallbacks();
|
|
|
|
ConnectionRecord *record = FindConnection((HSteamNetConnection) connection);
|
|
if (record == NULL)
|
|
{
|
|
return ReceiveNoData;
|
|
}
|
|
|
|
char *out = (char *) buffer;
|
|
int copied = 0;
|
|
|
|
// leftover bytes from a message that outsized the last call
|
|
if (record->leftoverCount > 0)
|
|
{
|
|
int take = (record->leftoverCount < size) ? record->leftoverCount : size;
|
|
memcpy(out, record->leftover, take);
|
|
memmove(record->leftover, record->leftover + take,
|
|
record->leftoverCount - take);
|
|
record->leftoverCount -= take;
|
|
out += take;
|
|
copied += take;
|
|
}
|
|
|
|
// drain messages while they fit; stash any partial tail
|
|
while (copied < size && record->leftoverCount == 0)
|
|
{
|
|
SteamNetworkingMessage_t *message = NULL;
|
|
int count = SteamNetworkingSockets()->ReceiveMessagesOnConnection(
|
|
record->handle, &message, 1);
|
|
if (count <= 0)
|
|
{
|
|
break;
|
|
}
|
|
int room = size - copied;
|
|
int take = ((int) message->m_cbSize < room) ? (int) message->m_cbSize : room;
|
|
memcpy(out, message->m_pData, take);
|
|
out += take;
|
|
copied += take;
|
|
|
|
int rest = (int) message->m_cbSize - take;
|
|
if (rest > 0)
|
|
{
|
|
if (rest > leftoverSize)
|
|
{
|
|
rest = leftoverSize; // cannot happen: engine packets < 2K
|
|
}
|
|
memcpy(record->leftover, (const char *) message->m_pData + take, rest);
|
|
record->leftoverCount = rest;
|
|
}
|
|
message->Release();
|
|
}
|
|
|
|
if (copied > 0)
|
|
{
|
|
return copied;
|
|
}
|
|
if (record->dead)
|
|
{
|
|
return ReceiveDisconnected;
|
|
}
|
|
return ReceiveNoData;
|
|
}
|
|
|
|
Logical
|
|
GetRemoteAddress(
|
|
Connection connection,
|
|
SOCKADDR_IN *address
|
|
)
|
|
{
|
|
memset(address, 0, sizeof(SOCKADDR_IN));
|
|
ConnectionRecord *record = FindConnection((HSteamNetConnection) connection);
|
|
if (record == NULL || !record->connected)
|
|
{
|
|
return False;
|
|
}
|
|
address->sin_family = AF_INET;
|
|
address->sin_addr.S_un.S_addr = record->remoteIP;
|
|
address->sin_port = record->remoteEnginePort;
|
|
return True;
|
|
}
|
|
};
|
|
|
|
SteamNetTransport gSteamTransport;
|
|
}
|
|
|
|
//########################################################################
|
|
// Install: bring Steam up, get our FakeIP identity, take over the wire
|
|
//########################################################################
|
|
|
|
Logical
|
|
SteamNetTransport_Install()
|
|
{
|
|
if (gSteamReady)
|
|
{
|
|
return True;
|
|
}
|
|
|
|
//
|
|
// Before ANY Steam symbol: steam_api.dll is delay-loaded, so calling
|
|
// into it when it is missing raises the helper's fatal exception
|
|
// rather than failing. This is the gate that makes the DLL optional.
|
|
//
|
|
if (!SteamNetTransport_ClientLibraryPresent())
|
|
{
|
|
return False;
|
|
}
|
|
|
|
if (!SteamAPI_Init())
|
|
{
|
|
DEBUG_STREAM << "SteamNetTransport: SteamAPI_Init failed "
|
|
<< "(Steam not running, or no steam_appid.txt) - staying on TCP\n" << std::flush;
|
|
return False;
|
|
}
|
|
|
|
// connection status arrives through the CCallback dispatcher
|
|
if (gCallbacks == NULL)
|
|
{
|
|
gCallbacks = new SteamTransportCallbacks;
|
|
}
|
|
|
|
SteamNetworkingUtils()->InitRelayNetworkAccess();
|
|
|
|
//
|
|
// Mission load stalls the game thread for 10-30s with nothing
|
|
// pumping - Steam's default 10s connected-timeout would shear
|
|
// every connection mid-load (seen live: end reason 4001 with rx
|
|
// ages right at load duration). TCP never timed out idle arcade
|
|
// links; 90s keeps that spirit.
|
|
//
|
|
SteamNetworkingUtils()->SetGlobalConfigValueInt32(
|
|
k_ESteamNetworkingConfig_TimeoutConnected, 90 * 1000);
|
|
|
|
if (!SteamNetworkingSockets()->BeginAsyncRequestFakeIP(2))
|
|
{
|
|
DEBUG_STREAM << "SteamNetTransport: BeginAsyncRequestFakeIP failed - staying on TCP\n" << std::flush;
|
|
return False;
|
|
}
|
|
|
|
// FakeIP allocation is async: pump until it lands (or give up)
|
|
SteamNetworkingFakeIPResult_t fake;
|
|
memset(&fake, 0, sizeof(fake));
|
|
DWORD deadline = GetTickCount() + 20 * 1000;
|
|
for (;;)
|
|
{
|
|
SteamAPI_RunCallbacks();
|
|
SteamNetworkingSockets()->GetFakeIP(0, &fake);
|
|
if (fake.m_eResult == k_EResultOK)
|
|
{
|
|
break;
|
|
}
|
|
if (fake.m_eResult != k_EResultBusy && fake.m_eResult != k_EResultNoMatch)
|
|
{
|
|
DEBUG_STREAM << "SteamNetTransport: FakeIP allocation failed (EResult "
|
|
<< (int) fake.m_eResult << ") - staying on TCP\n" << std::flush;
|
|
return False;
|
|
}
|
|
if ((LONG)(GetTickCount() - deadline) >= 0)
|
|
{
|
|
DEBUG_STREAM << "SteamNetTransport: FakeIP allocation timed out - staying on TCP\n" << std::flush;
|
|
return False;
|
|
}
|
|
Sleep(50);
|
|
}
|
|
|
|
gLocalFakeIP = fake.m_unIP;
|
|
gLocalFakePorts[0] = fake.m_unPorts[0];
|
|
gLocalFakePorts[1] = fake.m_unPorts[1];
|
|
|
|
SteamNetworkingIPAddr mine;
|
|
mine.Clear();
|
|
mine.SetIPv4(gLocalFakeIP, 0);
|
|
mine.ToString(gLocalFakeAddressString, sizeof(gLocalFakeAddressString), false);
|
|
|
|
DEBUG_STREAM << "SteamNetTransport: up as " << gLocalFakeAddressString
|
|
<< " (fake ports " << gLocalFakePorts[0] << " console, "
|
|
<< gLocalFakePorts[1] << " game)\n" << std::flush;
|
|
|
|
gSteamReady = True;
|
|
NetTransport_Set(&gSteamTransport);
|
|
|
|
//
|
|
// dev: RP412STEAMSELFTEST=1 loops a connection back to ourselves,
|
|
// proving the listen/accept/connect/send/receive machinery (and
|
|
// the status-callback dispatch) without a second machine
|
|
//
|
|
const char *self_test = getenv("RP412STEAMSELFTEST");
|
|
if (self_test != NULL && atoi(self_test) != 0)
|
|
{
|
|
DEBUG_STREAM << "SteamNetTransport: SELF TEST starting\n" << std::flush;
|
|
SteamNetTransport_RegisterPeer(
|
|
gLocalFakeAddressString, gLocalFakePorts[0], gLocalFakePorts[1],
|
|
SteamUser()->GetSteamID().ConvertToUint64());
|
|
|
|
NetTransport *transport = &gSteamTransport;
|
|
NetTransport::Connection listener = transport->Listen(1501, 1);
|
|
|
|
SOCKADDR_IN self_address;
|
|
transport->Resolve(gLocalFakeAddressString, &self_address);
|
|
self_address.sin_port = htons(1501);
|
|
NetTransport::Connection outgoing = transport->Connect(&self_address, 0);
|
|
|
|
NetTransport::Connection incoming = NetTransport::InvalidConnection;
|
|
DWORD accept_deadline = GetTickCount() + 10 * 1000;
|
|
while (incoming == NetTransport::InvalidConnection &&
|
|
(LONG)(GetTickCount() - accept_deadline) < 0)
|
|
{
|
|
incoming = transport->Accept(listener);
|
|
Sleep(50);
|
|
}
|
|
|
|
Logical ok = False;
|
|
Logical survives_close = False;
|
|
if (outgoing != NetTransport::InvalidConnection &&
|
|
incoming != NetTransport::InvalidConnection)
|
|
{
|
|
transport->Send(outgoing, "PING", 4);
|
|
char buffer[16];
|
|
DWORD recv_deadline = GetTickCount() + 5 * 1000;
|
|
while ((LONG)(GetTickCount() - recv_deadline) < 0)
|
|
{
|
|
if (transport->Receive(incoming, buffer, sizeof(buffer)) == 4)
|
|
{
|
|
ok = (memcmp(buffer, "PING", 4) == 0);
|
|
break;
|
|
}
|
|
Sleep(25);
|
|
}
|
|
|
|
//
|
|
// The arcade engine closes a listener right after
|
|
// accepting - the accepted connection MUST survive
|
|
// (Steam kills children on CloseListenSocket; the
|
|
// transport defers the real close to Cleanup)
|
|
//
|
|
transport->Close(listener);
|
|
transport->Send(incoming, "PONG", 4);
|
|
recv_deadline = GetTickCount() + 5 * 1000;
|
|
while ((LONG)(GetTickCount() - recv_deadline) < 0)
|
|
{
|
|
if (transport->Receive(outgoing, buffer, sizeof(buffer)) == 4)
|
|
{
|
|
survives_close = (memcmp(buffer, "PONG", 4) == 0);
|
|
break;
|
|
}
|
|
Sleep(25);
|
|
}
|
|
}
|
|
DEBUG_STREAM << "SteamNetTransport: SELF TEST "
|
|
<< ((ok && survives_close) ? "PASSED" : "FAILED")
|
|
<< " (out " << (outgoing != NetTransport::InvalidConnection)
|
|
<< ", in " << (incoming != NetTransport::InvalidConnection)
|
|
<< ", ping " << ok
|
|
<< ", survives listener close " << survives_close
|
|
<< ")\n" << std::flush;
|
|
|
|
// leave the session pristine for the real race
|
|
gSteamTransport.Cleanup();
|
|
gPeerCount = 0;
|
|
}
|
|
return True;
|
|
}
|
|
|
|
const char *
|
|
SteamNetTransport_GetFakeAddressString()
|
|
{
|
|
return gLocalFakeAddressString;
|
|
}
|
|
|
|
int
|
|
SteamNetTransport_GetFakeConsolePort()
|
|
{
|
|
return gLocalFakePorts[0];
|
|
}
|
|
|
|
int
|
|
SteamNetTransport_GetFakeGamePort()
|
|
{
|
|
return gLocalFakePorts[1];
|
|
}
|
|
|
|
void
|
|
SteamNetTransport_SetEnginePorts(int console_port)
|
|
{
|
|
gEngineConsolePort = (unsigned short) console_port;
|
|
}
|
|
|
|
void
|
|
SteamNetTransport_RegisterPeer(
|
|
const char *fake_ip,
|
|
int fake_console_port,
|
|
int fake_game_port,
|
|
unsigned __int64 steam_id
|
|
)
|
|
{
|
|
SteamNetworkingIPAddr parsed;
|
|
if (!parsed.ParseString(fake_ip))
|
|
{
|
|
return;
|
|
}
|
|
for (int i = 0; i < gPeerCount; ++i)
|
|
{
|
|
if (gPeers[i].fakeIP == parsed.GetIPv4())
|
|
{
|
|
gPeers[i].fakeConsolePort = (unsigned short) fake_console_port;
|
|
gPeers[i].fakeGamePort = (unsigned short) fake_game_port;
|
|
gPeers[i].steamID = steam_id;
|
|
return;
|
|
}
|
|
}
|
|
if (gPeerCount >= maxPeers)
|
|
{
|
|
return;
|
|
}
|
|
gPeers[gPeerCount].fakeIP = parsed.GetIPv4();
|
|
gPeers[gPeerCount].fakeConsolePort = (unsigned short) fake_console_port;
|
|
gPeers[gPeerCount].fakeGamePort = (unsigned short) fake_game_port;
|
|
gPeers[gPeerCount].steamID = steam_id;
|
|
++gPeerCount;
|
|
DEBUG_STREAM << "SteamNetTransport: peer registered: " << fake_ip
|
|
<< " (console " << fake_console_port << ", game "
|
|
<< fake_game_port << ", id " << steam_id << ")\n" << std::flush;
|
|
}
|
|
|
|
#endif // RP412_STEAM
|