Files
RP412/MUNGA_L4/L4STEAMTRANSPORT.cpp
T
CydandClaude Fable 5 ff6ec8c56a SteamNetTransport: the Steam wire is implemented and live
Steamworks SDK 1.64 vendored at extern/steamworks_sdk_164 (headers +
win32 redistributables only; .gitignore trims the rest). Both projects
build with RP412_STEAM; activation stays behind the RP412STEAM=1
environment switch, so plain desktop runs never touch Steam.

L4STEAMTRANSPORT.cpp implements NetTransport on ISteamNetworkingSockets
with FakeIP: SteamNetTransport_Install brings up SteamAPI, relay
network access, and a two-port FakeIP identity (fake port 0 = console
channel, 1 = game mesh), then swaps the process wire; any failure logs
the reason and the game carries on over TCP. Addressing keeps the
engine untouched: all pods share the -net port convention, eggs carry
fakeip:engineport, and the transport alone translates engine ports to
Steam fake ports via the lobby-fed peer table (RegisterPeer). Connect
mirrors the TCP retry-while-refused loop; Receive normalizes message
lanes back into the stream semantics CheckBuffers expects.

Runtime verified on this box: RP412STEAM=1 under AppID 480 came up as
169.254.59.52 (fake ports 32256/32257); without Steam credentials it
falls back to TCP cleanly; default boot logs no Steam lines at all.
steam_api.dll ships in the dist.

Next: the lobby layer (ISteamMatchmaking member data -> RegisterPeer +
egg build + RPL4CONSOLE marshal), which needs a second account to test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 20:34:07 -05:00

652 lines
16 KiB
C++

#include "mungal4.h"
#pragma hdrstop
#include "l4steamtransport.h"
#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;
};
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 fakeGamePort; // host order
};
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;
}
}
}
unsigned short
LookupPeerFakeGamePort(unsigned long fake_ip_host)
{
for (int i = 0; i < gPeerCount; ++i)
{
if (gPeers[i].fakeIP == fake_ip_host)
{
return gPeers[i].fakeGamePort;
}
}
return 0;
}
//---------------------------------------------------------------
// Status-changed callback (fires inside SteamAPI_RunCallbacks on
// the game thread): accept incoming, queue connected, mark drops
//---------------------------------------------------------------
void __cdecl
OnConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t *status)
{
switch (status->m_info.m_eState)
{
case k_ESteamNetworkingConnectionState_Connecting:
if (status->m_info.m_hListenSocket != k_HSteamListenSocket_Invalid)
{
// incoming: accept immediately, queue it when Connected
SteamNetworkingSockets()->AcceptConnection(status->m_hConn);
}
break;
case k_ESteamNetworkingConnectionState_Connected:
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:
{
ConnectionRecord *record = FindConnection(status->m_hConn);
if (record != NULL)
{
record->dead = True;
}
}
break;
}
}
//---------------------------------------------------------------
// 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_game_port = LookupPeerFakeGamePort(remote_fake_ip);
if (fake_game_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) << " (fake port "
<< fake_game_port << ")...\n" << std::flush;
SteamNetworkingConfigValue_t option;
option.SetPtr(
k_ESteamNetworkingConfig_Callback_ConnectionStatusChanged,
(void *) &OnConnectionStatusChanged);
SteamNetworkingIPAddr target;
target.Clear();
target.SetIPv4(remote_fake_ip, fake_game_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;
for (;;)
{
HSteamNetConnection handle =
SteamNetworkingSockets()->ConnectByIPAddress(target, 1, &option);
if (handle == k_HSteamNetConnection_Invalid)
{
return InvalidConnection;
}
ConnectionRecord *record =
AddConnection(handle, remote->sin_addr.S_un.S_addr, remote->sin_port);
while (!record->connected && !record->dead &&
(LONG)(GetTickCount() - deadline) < 0)
{
SteamAPI_RunCallbacks();
Sleep(25);
}
if (record->connected)
{
return (Connection) handle;
}
// attempt failed - drop it and retry until the deadline
SteamNetworkingSockets()->CloseConnection(handle, 0, "retry", false);
RemoveConnection(handle);
if ((LONG)(GetTickCount() - deadline) >= 0)
{
DEBUG_STREAM << "SteamNetTransport: connect timed out\n" << std::flush;
return InvalidConnection;
}
Sleep(250);
}
}
Connection
Listen(
int local_port,
int backlog
)
{
if (gListenerCount >= maxListeners)
{
return InvalidConnection;
}
// 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;
SteamNetworkingConfigValue_t option;
option.SetPtr(
k_ESteamNetworkingConfig_Callback_ConnectionStatusChanged,
(void *) &OnConnectionStatusChanged);
HSteamListenSocket handle =
SteamNetworkingSockets()->CreateListenSocketP2PFakeIP(index, 1, &option);
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->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
unsigned long remote_ip_net = 0;
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)
{
SteamNetworkingSockets()->CloseListenSocket(listener->handle);
*listener = gListeners[gListenerCount - 1];
--gListenerCount;
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;
}
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;
}
SteamNetworkingUtils()->InitRelayNetworkAccess();
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);
return True;
}
const char *
SteamNetTransport_GetFakeAddressString()
{
return gLocalFakeAddressString;
}
int
SteamNetTransport_GetFakeGamePort()
{
return gLocalFakePorts[1];
}
void
SteamNetTransport_RegisterPeer(
const char *fake_ip,
int fake_game_port
)
{
if (gPeerCount >= maxPeers)
{
return;
}
SteamNetworkingIPAddr parsed;
if (!parsed.ParseString(fake_ip))
{
return;
}
gPeers[gPeerCount].fakeIP = parsed.GetIPv4();
gPeers[gPeerCount].fakeGamePort = (unsigned short) fake_game_port;
++gPeerCount;
DEBUG_STREAM << "SteamNetTransport: peer registered: " << fake_ip
<< " game port " << fake_game_port << "\n" << std::flush;
}
#endif // RP412_STEAM