Net: the Steam transport -- FakeIP + SDR behind the wire seam (step 4b)

NEW gated (BT_STEAM) TU L4STEAMNET: ISteamNetworkingSockets with FakeIP --
pseudo-SOCKET handles (0x5EA0xxxx) behind the L4NET BTNet* seam; the engine
TCP byte stream rides reliable-NoNagle Steam messages re-assembled into
per-connection rings (nonblocking recv semantics preserved: empty ->
WSAEWOULDBLOCK, peer-closed -> 0); two FakeIP ports mirror the arcade
console/game channel convention; listen sockets + accept queues via the
global status-changed callback; graceful degrade to Winsock at every step.
Seam completions in L4NET.CPP: CheckSocket reports a pseudo-socket peer as
its FAKE ipv4:port (the lobby egg roster matches unchanged); OpenConnection
TCP_OPEN delegates FakeIP targets to BTSteamNet_Connect.  WinMain installs
on BT_STEAM_NET=1 (lobby launch path / by hand).  CMake: gated SDK include/
lib/dll-copy.  Both pumps (SteamAPI_RunCallbacks + sockets RunCallbacks)
drive it -- the FakeIP result rides the GENERAL dispatch (first attempt with
only the sockets pump timed out).

Verified live (ON/ON build, Steam client running): SteamAPI_Init OK,
steam_appid.txt(480) auto-written, FakeIP allocated
(169.254.36.58, console 54464 / game 54465); without BT_STEAM_NET or
without Steam the game stays pure Winsock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-17 23:47:47 -05:00
co-authored by Claude Fable 5
parent e79e8faeed
commit 48ede2f7d7
6 changed files with 701 additions and 20 deletions
+13
View File
@@ -371,6 +371,19 @@ target_link_libraries(btl4 PRIVATE
# UNRESOLVED tolerates the dead offline-tool factory in mech3.cpp. See docs.
target_link_options(btl4 PRIVATE /FORCE)
if(BT_STEAM)
set(STEAMWORKS "${CMAKE_SOURCE_DIR}/extern/steamworks_sdk_164")
target_sources(munga_engine PRIVATE "engine/MUNGA_L4/L4STEAMNET.cpp")
target_include_directories(munga_engine PRIVATE "${STEAMWORKS}/public")
target_include_directories(btl4 PRIVATE "${STEAMWORKS}/public")
target_link_libraries(btl4 PRIVATE
"${STEAMWORKS}/redistributable_bin/steam_api.lib")
add_custom_command(TARGET btl4 POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${STEAMWORKS}/redistributable_bin/steam_api.dll"
"$<TARGET_FILE_DIR:btl4>")
endif()
# Copy the OpenAL runtime DLL next to the built exe. (libsndfile is gone: its
# repo DLL was a no-op STUB -- L4AUDRES now loads the soundbank WAVs with an
# in-tree RIFF/PCM reader, no external dependency.)
+4 -20
View File
@@ -1,8 +1,8 @@
[mission]
adventure=BattleTech
map=cavern
map=grass
scenario=freeforall
time=night
time=day
weather=clear
temperature=27
length=600
@@ -156,9 +156,8 @@ x=128
y=32
width=8
[pilots]
pilot=127.0.0.1:1502
pilot=127.0.0.1:1602
[127.0.0.1:1502]
pilot=200.0.0.96
[200.0.0.96]
hostType=0
advancedDamage=1
loadzones=1
@@ -172,20 +171,6 @@ dropzone=one
vehicle=bhk1
vehicleValue=1000
color=White
[127.0.0.1:1602]
hostType=0
advancedDamage=1
loadzones=1
name=Boreas
bitmapindex=2
experience=expert
badge=VGL
patch=Yellow
role=Role::Default
dropzone=one
vehicle=ava1
vehicleValue=1000
color=Red
[largebitmap]
bitmap=BitMap::Large::Aeolus
[BitMap::Large::Aeolus]
@@ -242,4 +227,3 @@ width=4
model=dfltrole
[Role::NoReturn]
model=noretun
+22
View File
@@ -2760,6 +2760,17 @@ bool L4NetworkManager::CheckSocket(SOCKET socket, SOCKADDR_IN *remoteEndpoint)
{
if (remoteEndpoint)
{
#ifdef BT_STEAM
//
// A Steam pseudo-socket's peer is its FAKE ipv4:port -- the same
// address the lobby-built egg carries in [pilots], so the swap/
// match logic downstream works unchanged.
//
if (BTSteamNet_Owns(socket))
{
return BTSteamNet_PeerAddress(socket, remoteEndpoint) != 0;
}
#endif
int size = sizeof(SOCKADDR_IN);
memset(remoteEndpoint, 0, size);
if(!getpeername(socket, (sockaddr*)remoteEndpoint, &size))
@@ -2818,6 +2829,17 @@ SOCKET L4NetworkManager::OpenConnection(
if(connection_type == NETNUB_TCP_OPEN)
{
#ifdef BT_STEAM
//
// A FakeIP target travels the Steam wire (SDR); the returned
// pseudo-SOCKET flows through the same L4Host bookkeeping.
//
if (BTSteamNet_IsFakeAddress((unsigned long)internet_address))
{
return BTSteamNet_Connect(
(unsigned long)internet_address, remote_port);
}
#endif
DEBUG_STREAM << "Opening connection to " << inet_ntoa(*(in_addr*)&internet_address) << ":" << remote_port << "...\n" << std::flush;
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+584
View File
@@ -0,0 +1,584 @@
//###########################################################################
// L4STEAMNET -- the Steam internet transport (BT_STEAM only; this TU is
// only in the build when the gate is on -- see CMakeLists.txt).
// Design: L4STEAMNET.h.
//###########################################################################
#include "l4steamnet.h"
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#pragma pack(push, 8)
#include "steam/steam_api.h"
#include "steam/isteamnetworkingsockets.h"
#include "steam/isteamnetworkingutils.h"
#include "steam/steamnetworkingfakeip.h"
#pragma pack(pop)
#include <iostream>
#define STEAMNET_LOG(x) \
do { std::cout << "[steamnet] " << x << "\n" << std::flush; } while (0)
//###########################################################################
// State
//###########################################################################
enum
{
MaxConnections = 16,
RingCapacity = 256 * 1024, // per-connection reassembly ring
PseudoSocketBase = 0x5EA00000,
AcceptQueueSize = 8,
FakePortCount = 2 // [0] console, [1] game
};
struct SteamConnection
{
int inUse;
HSteamNetConnection connection;
SOCKADDR_IN peerAddress; // the peer's FAKE ipv4:port
int connected; // handshake complete
int closedByPeer;
unsigned char *ring;
int ringHead, ringTail; // [tail, head) valid
};
static int steamActive = 0;
static SteamConnection connections[MaxConnections];
static HSteamListenSocket listenSockets[FakePortCount];
static SOCKET acceptQueue[FakePortCount][AcceptQueueSize];
static int acceptHead[FakePortCount], acceptTail[FakePortCount];
static SteamNetworkingFakeIPResult_t myFakeIP;
static unsigned long lastPumpTick = 0;
static SteamConnection *
ConnectionOf(SOCKET wire_socket)
{
unsigned long value = (unsigned long)wire_socket;
if ((value & 0xFFFF0000) != PseudoSocketBase)
{
return NULL;
}
int index = (int)(value & 0xFFFF);
if (index < 0 || index >= MaxConnections || !connections[index].inUse)
{
return NULL;
}
return &connections[index];
}
static SOCKET
PseudoSocketOf(SteamConnection *connection)
{
return (SOCKET)(PseudoSocketBase | (int)(connection - connections));
}
static SteamConnection *
AllocateConnection(HSteamNetConnection handle)
{
for (int i = 0; i < MaxConnections; ++i)
{
if (!connections[i].inUse)
{
SteamConnection &c = connections[i];
memset(&c.peerAddress, 0, sizeof(c.peerAddress));
c.inUse = 1;
c.connection = handle;
c.connected = 0;
c.closedByPeer = 0;
if (c.ring == NULL)
{
c.ring = (unsigned char *)malloc(RingCapacity);
}
c.ringHead = c.ringTail = 0;
return &c;
}
}
return NULL;
}
//
// The peer's fake ipv4:port as a SOCKADDR_IN (network byte order), from
// the connection info. The GAME port convention on the wire is the
// arcade's: whatever port the peer's fake address carries.
//
static void
FillPeerAddress(SteamConnection *connection)
{
SteamNetConnectionInfo_t info;
if (SteamNetworkingSockets()->GetConnectionInfo(
connection->connection, &info) &&
info.m_addrRemote.IsFakeIP())
{
connection->peerAddress.sin_family = AF_INET;
connection->peerAddress.sin_addr.s_addr =
htonl(info.m_addrRemote.GetIPv4());
connection->peerAddress.sin_port = htons(info.m_addrRemote.m_port);
}
}
//###########################################################################
// The status-changed callback (invoked from RunCallbacks on our thread).
//###########################################################################
static void __cdecl
OnConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t *status)
{
ISteamNetworkingSockets *sockets = SteamNetworkingSockets();
switch (status->m_info.m_eState)
{
case k_ESteamNetworkingConnectionState_Connecting:
//
// Incoming connection on one of our listen sockets: accept it and
// queue the pseudo-socket for BTNetAccept.
//
if (status->m_eOldState == k_ESteamNetworkingConnectionState_None &&
status->m_info.m_hListenSocket != k_HSteamListenSocket_Invalid)
{
int channel = -1;
for (int i = 0; i < FakePortCount; ++i)
{
if (listenSockets[i] == status->m_info.m_hListenSocket)
{
channel = i;
}
}
if (channel < 0)
{
sockets->CloseConnection(status->m_hConn, 0, "unknown listener", false);
return;
}
int next = (acceptHead[channel] + 1) % AcceptQueueSize;
if (next == acceptTail[channel])
{
sockets->CloseConnection(status->m_hConn, 0, "accept queue full", false);
return;
}
if (sockets->AcceptConnection(status->m_hConn) != k_EResultOK)
{
sockets->CloseConnection(status->m_hConn, 0, "accept failed", false);
return;
}
SteamConnection *connection = AllocateConnection(status->m_hConn);
if (connection == NULL)
{
sockets->CloseConnection(status->m_hConn, 0, "table full", false);
return;
}
acceptQueue[channel][acceptHead[channel]] = PseudoSocketOf(connection);
acceptHead[channel] = next;
STEAMNET_LOG("incoming connection queued on channel " << channel);
}
break;
case k_ESteamNetworkingConnectionState_Connected:
for (int i = 0; i < MaxConnections; ++i)
{
if (connections[i].inUse &&
connections[i].connection == status->m_hConn)
{
connections[i].connected = 1;
FillPeerAddress(&connections[i]);
STEAMNET_LOG("connection " << i << " up");
}
}
break;
case k_ESteamNetworkingConnectionState_ClosedByPeer:
case k_ESteamNetworkingConnectionState_ProblemDetectedLocally:
for (int i = 0; i < MaxConnections; ++i)
{
if (connections[i].inUse &&
connections[i].connection == status->m_hConn)
{
connections[i].closedByPeer = 1;
STEAMNET_LOG("connection " << i << " closed ("
<< status->m_info.m_szEndDebug << ")");
}
}
break;
default:
break;
}
}
//###########################################################################
// The pump: Steam callbacks + draining every connection's messages into
// its ring. Called lazily from the seam entries (the game thread), rate-
// limited to once per millisecond tick.
//###########################################################################
void
BTSteamNet_Pump()
{
if (!steamActive)
{
return;
}
unsigned long now = GetTickCount();
if (now == lastPumpTick)
{
return;
}
lastPumpTick = now;
//
// Both pumps: the general Steam dispatch (FakeIP results, auth) AND
// the networking-sockets callbacks (connection status).
//
SteamAPI_RunCallbacks();
ISteamNetworkingSockets *sockets = SteamNetworkingSockets();
sockets->RunCallbacks();
for (int i = 0; i < MaxConnections; ++i)
{
SteamConnection &c = connections[i];
if (!c.inUse || c.connection == k_HSteamNetConnection_Invalid)
{
continue;
}
for (;;)
{
SteamNetworkingMessage_t *messages[16];
int received = sockets->ReceiveMessagesOnConnection(
c.connection, messages, 16);
if (received <= 0)
{
break;
}
for (int m = 0; m < received; ++m)
{
int length = (int)messages[m]->m_cbSize;
const unsigned char *data =
(const unsigned char *)messages[m]->m_pData;
int space = RingCapacity - 1 -
((c.ringHead - c.ringTail + RingCapacity) % RingCapacity);
if (length > space)
{
STEAMNET_LOG("ring overflow on connection " << i
<< " -- dropping " << length << " bytes");
}
else
{
for (int b = 0; b < length; ++b)
{
c.ring[c.ringHead] = data[b];
c.ringHead = (c.ringHead + 1) % RingCapacity;
}
}
messages[m]->Release();
}
}
}
}
//###########################################################################
// Install
//###########################################################################
int
BTSteamNet_Install()
{
if (steamActive)
{
return 0;
}
//
// The 480 (Spacewar) test id if no appid file exists -- dev tooling.
//
FILE *appid = fopen("steam_appid.txt", "rt");
if (appid == NULL)
{
appid = fopen("steam_appid.txt", "wt");
if (appid != NULL)
{
fputs("480\n", appid);
}
}
if (appid != NULL)
{
fclose(appid);
}
if (!SteamAPI_Init())
{
STEAMNET_LOG("SteamAPI_Init failed (Steam not running / no login) "
"-- staying on Winsock");
return -1;
}
ISteamNetworkingSockets *sockets = SteamNetworkingSockets();
SteamNetworkingUtils()->InitRelayNetworkAccess();
SteamNetworkingUtils()->SetGlobalCallback_SteamNetConnectionStatusChanged(
OnConnectionStatusChanged);
//
// FakeIP: one allocation carrying both channel ports.
//
memset(&myFakeIP, 0, sizeof(myFakeIP));
sockets->BeginAsyncRequestFakeIP(FakePortCount);
for (int wait = 0; wait < 200; ++wait) // <= 20 s
{
SteamAPI_RunCallbacks(); // FakeIP result rides the general dispatch
sockets->RunCallbacks();
sockets->GetFakeIP(0, &myFakeIP); // void: fills the struct
if (myFakeIP.m_eResult == k_EResultOK && myFakeIP.m_unIP != 0)
{
break;
}
Sleep(100);
}
if (myFakeIP.m_unIP == 0)
{
STEAMNET_LOG("FakeIP allocation timed out -- staying on Winsock");
SteamAPI_Shutdown();
return -1;
}
//
// Listen on both fake ports.
//
for (int i = 0; i < FakePortCount; ++i)
{
listenSockets[i] = sockets->CreateListenSocketP2PFakeIP(i, 0, NULL);
acceptHead[i] = acceptTail[i] = 0;
if (listenSockets[i] == k_HSteamListenSocket_Invalid)
{
STEAMNET_LOG("CreateListenSocketP2PFakeIP(" << i << ") failed");
}
}
steamActive = 1;
char dotted[32];
SteamNetworkingIPAddr address;
address.SetIPv4(myFakeIP.m_unIP, myFakeIP.m_unPorts[0]);
address.ToString(dotted, sizeof(dotted), false);
STEAMNET_LOG("up -- FakeIP " << dotted
<< " console-port " << myFakeIP.m_unPorts[0]
<< " game-port " << myFakeIP.m_unPorts[1]);
return 0;
}
int
BTSteamNet_Active()
{
return steamActive;
}
//###########################################################################
// The wire-seam surface
//###########################################################################
int
BTSteamNet_Owns(SOCKET wire_socket)
{
return steamActive && ConnectionOf(wire_socket) != NULL;
}
int
BTSteamNet_Send(SOCKET wire_socket, const char *data, int length)
{
BTSteamNet_Pump();
SteamConnection *connection = ConnectionOf(wire_socket);
if (connection == NULL || connection->closedByPeer)
{
WSASetLastError(WSAECONNRESET);
return SOCKET_ERROR;
}
EResult result = SteamNetworkingSockets()->SendMessageToConnection(
connection->connection, data, (uint32)length,
k_nSteamNetworkingSend_ReliableNoNagle, NULL);
if (result != k_EResultOK)
{
WSASetLastError(WSAEWOULDBLOCK);
return SOCKET_ERROR;
}
return length;
}
int
BTSteamNet_Recv(SOCKET wire_socket, char *buffer, int capacity)
{
BTSteamNet_Pump();
SteamConnection *connection = ConnectionOf(wire_socket);
if (connection == NULL)
{
WSASetLastError(WSAENOTSOCK);
return SOCKET_ERROR;
}
int available =
(connection->ringHead - connection->ringTail + RingCapacity)
% RingCapacity;
if (available == 0)
{
if (connection->closedByPeer)
{
return 0; // the engine's disconnect path
}
WSASetLastError(WSAEWOULDBLOCK); // nonblocking no-data semantics
return SOCKET_ERROR;
}
int count = (available < capacity) ? available : capacity;
for (int b = 0; b < count; ++b)
{
buffer[b] = (char)connection->ring[connection->ringTail];
connection->ringTail = (connection->ringTail + 1) % RingCapacity;
}
return count;
}
SOCKET
BTSteamNet_Accept(SOCKET listener_socket)
{
(void)listener_socket;
if (!steamActive)
{
return INVALID_SOCKET;
}
BTSteamNet_Pump();
//
// The engine's console/game listeners map onto our two fake-port
// channels; either TCP listener polling through BTNetAccept also
// offers whatever Steam has queued. Console arrivals are only
// meaningful pre-mission, game arrivals during the mesh -- the
// engine's own swap logic sorts them by peer address.
//
for (int channel = 0; channel < FakePortCount; ++channel)
{
if (acceptTail[channel] != acceptHead[channel])
{
SOCKET wire_socket = acceptQueue[channel][acceptTail[channel]];
acceptTail[channel] = (acceptTail[channel] + 1) % AcceptQueueSize;
return wire_socket;
}
}
return INVALID_SOCKET;
}
void
BTSteamNet_Close(SOCKET wire_socket)
{
SteamConnection *connection = ConnectionOf(wire_socket);
if (connection == NULL)
{
return;
}
SteamNetworkingSockets()->CloseConnection(
connection->connection, 0, "closed", true);
connection->inUse = 0;
connection->connection = k_HSteamNetConnection_Invalid;
}
int
BTSteamNet_PeerAddress(SOCKET wire_socket, SOCKADDR_IN *out)
{
SteamConnection *connection = ConnectionOf(wire_socket);
if (connection == NULL)
{
return 0;
}
if (connection->peerAddress.sin_family == 0)
{
FillPeerAddress(connection);
}
*out = connection->peerAddress;
return connection->peerAddress.sin_family != 0;
}
//###########################################################################
// Connection establishment + the lobby surface
//###########################################################################
int
BTSteamNet_IsFakeAddress(unsigned long internet_address_be)
{
if (!steamActive)
{
return 0;
}
SteamNetworkingIPAddr address;
address.SetIPv4(ntohl(internet_address_be), 0);
return address.IsFakeIP();
}
SOCKET
BTSteamNet_Connect(unsigned long internet_address_be, int remote_port)
{
if (!steamActive)
{
return INVALID_SOCKET;
}
SteamNetworkingIPAddr address;
address.SetIPv4(ntohl(internet_address_be), (uint16)remote_port);
HSteamNetConnection handle =
SteamNetworkingSockets()->ConnectByIPAddress(address, 0, NULL);
if (handle == k_HSteamNetConnection_Invalid)
{
return INVALID_SOCKET;
}
SteamConnection *connection = AllocateConnection(handle);
if (connection == NULL)
{
SteamNetworkingSockets()->CloseConnection(handle, 0, "table full", false);
return INVALID_SOCKET;
}
connection->peerAddress.sin_family = AF_INET;
connection->peerAddress.sin_addr.s_addr = internet_address_be;
connection->peerAddress.sin_port = htons((unsigned short)remote_port);
//
// The engine's OpenConnection is synchronous (blocking connect); wait
// out the SDR handshake here, bounded.
//
for (int wait = 0; wait < 300; ++wait) // <= 30 s
{
BTSteamNet_Pump();
if (connection->connected)
{
char dotted[48];
address.ToString(dotted, sizeof(dotted), true);
STEAMNET_LOG("connected to " << dotted);
return PseudoSocketOf(connection);
}
if (connection->closedByPeer)
{
break;
}
Sleep(100);
}
STEAMNET_LOG("connect failed/timed out");
BTSteamNet_Close(PseudoSocketOf(connection));
return INVALID_SOCKET;
}
int
BTSteamNet_GetFakeAddress(char *dotted_out, int capacity,
int *console_port, int *game_port)
{
if (!steamActive || myFakeIP.m_unIP == 0)
{
return -1;
}
SteamNetworkingIPAddr address;
address.SetIPv4(myFakeIP.m_unIP, 0);
char text[48];
address.ToString(text, sizeof(text), false);
strncpy(dotted_out, text, capacity - 1);
dotted_out[capacity - 1] = 0;
if (console_port != NULL)
{
*console_port = myFakeIP.m_unPorts[0];
}
if (game_port != NULL)
{
*game_port = myFakeIP.m_unPorts[1];
}
return 0;
}
+66
View File
@@ -0,0 +1,66 @@
#pragma once
//###########################################################################
//
// L4STEAMNET -- the Steam internet transport (BT_STEAM only).
//
// ISteamNetworkingSockets with FakeIP + SDR behind the L4NET.CPP wire
// seam: connections carry the engine's exact TCP byte stream as reliable
// Steam messages, re-assembled into per-connection rings so the engine's
// nonblocking recv semantics hold (empty -> WSAEWOULDBLOCK, peer-closed
// -> 0). Handles are pseudo-SOCKETs (0x5EA0xxxx) so L4Host and every
// call site above the seam are identical on both wires; the peer address
// reported through CheckSocket is the peer's FAKE IPv4:port, so the
// engine's roster matching works against a lobby-built egg whose
// [pilots] carry FakeIPs.
//
// Install: env BT_STEAM_NET=1 (set by the lobby launch path) -- needs the
// Steam client running and content\steam_appid.txt (written with the 480
// Spacewar test id if absent). Everything degrades gracefully: no Steam
// -> inactive -> pure Winsock.
//
// Ports: FakeIP port index 0 = the CONSOLE channel, 1 = the GAME channel
// (mirroring the arcade console-port / game-port=console+1 convention).
//
//###########################################################################
#include <winsock2.h>
int
BTSteamNet_Install(); // 0 = up (FakeIP allocated)
int
BTSteamNet_Active();
void
BTSteamNet_Pump(); // callbacks + rx drain (game thread)
//
// The wire-seam surface (BTNet* wrappers in L4NET.CPP + the marshal).
//
int
BTSteamNet_Owns(SOCKET wire_socket);
int
BTSteamNet_Send(SOCKET wire_socket, const char *data, int length);
int
BTSteamNet_Recv(SOCKET wire_socket, char *buffer, int capacity);
SOCKET
BTSteamNet_Accept(SOCKET listener_socket);
void
BTSteamNet_Close(SOCKET wire_socket);
int
BTSteamNet_PeerAddress(SOCKET wire_socket, SOCKADDR_IN *out);
//
// Connection establishment (the OpenConnection TCP_OPEN delegate + the
// marshal): True when the IPv4 is a Steam FakeIP.
//
int
BTSteamNet_IsFakeAddress(unsigned long internet_address_be);
SOCKET
BTSteamNet_Connect(unsigned long internet_address_be, int remote_port);
//
// The lobby surface: our own fake address, dotted, plus its two ports.
//
int
BTSteamNet_GetFakeAddress(char *dotted_out, int capacity,
int *console_port, int *game_port);
+12
View File
@@ -378,6 +378,18 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
}
BTFE_RelaunchSelfAndExit(fe_arguments); // never returns
}
#ifdef BT_STEAM
//
// The Steam transport (step 4b): BT_STEAM_NET=1 (set by the lobby
// launch path, or by hand for testing) brings up FakeIP + SDR
// before the engine opens any connection. Degrades to Winsock.
//
if (getenv("BT_STEAM_NET") != NULL && *getenv("BT_STEAM_NET") != '0')
{
extern int BTSteamNet_Install();
BTSteamNet_Install();
}
#endif
//
// A marshal-armed mission process: start the LocalConsole worker
// (it retries until our own console listener is up).