Files
BT411/engine/MUNGA_L4/L4STEAMNET.cpp
T
CydandClaude Fable 5 f703bb1d56 Steam: the lobby + IDENTITY-TOKEN roster -- internet MP code-complete (step 4c)
NEW gated (BT_STEAM) game/glass/btl4lobby: an ISteamMatchmaking room replacing
the arcade Site-Management screen -- host creates (lobby data btl4=1), members
join by filter, pilots publish name/mech/color, the host ENTER mints the
roster and signals GO.

THE FAKEIP LESSON (live-verified, prior-art assumption DISPROVEN): a fresh
process gets a DIFFERENT FakeIP, so menu-time fake addresses go stale by
mission time -- the first cycle timed out connecting to its own stale address.
Redesign: roster addresses are opaque ipv4-shaped TOKENS (169.254.77.N with
ports 1501/1502) minted by the host at GO, mapped to Steam IDENTITIES;
connections ride ConnectP2P(identity, channel) with console=0/game=1 virtual
ports; the map reaches mission processes via env (BT_FE_MYFAKE +
BT_FE_STEAMMAP); the GetMyAddress seam feeds the pod its own token for roster
self-match; CheckSocket reports peers by token.  L4STEAMNET reworked (no
FakeIP allocation at all); the marshal gained the Steam-wire branch.

Verified single-machine (ON/ON, live Steam): menu -> HOST STEAM LOBBY ->
room -> GO -> token map minted -> egg roster carries the token -> mission
process up with 1 roster token incl. self -> pod self-matches -> stock
console ladder -> mission RUNS (46 ticks).  Remaining: the live 2-account
cross-machine session (docs/STEAM_TEST.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 00:03:17 -05:00

635 lines
16 KiB
C++

//###########################################################################
// 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 unsigned long lastPumpTick = 0;
//
// The TOKEN TABLE (the FakeIP-stability lesson, live-verified: a fresh
// process gets a DIFFERENT FakeIP, so menu-time fake addresses go stale
// by mission time). Roster addresses are therefore opaque ipv4-shaped
// TOKENS minted by the lobby host and mapped to Steam IDENTITIES; the
// mission process receives the map via env:
// BT_FE_MYFAKE = my own token ip (joins GetMyAddress -- self-match)
// BT_FE_STEAMMAP = ip=steamid64;ip=steamid64;...
// Console channel = P2P virtual port 0 (token port 1501), game = virtual
// port 1 (token port 1502).
//
enum { TokenConsolePort = 1501, TokenGamePort = 1502 };
struct SteamToken
{
unsigned long addressBE;
unsigned long long steamID;
};
static SteamToken tokens[8];
static int tokenCount = 0;
static unsigned long myTokenBE = 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 TOKEN ipv4:port as a SOCKADDR_IN: the connection's remote
// Steam identity looked up in the token table; the port encodes the
// channel (console/game) the connection rides.
//
static void
FillPeerAddressFromIdentity(SteamConnection *connection, int channel)
{
SteamNetConnectionInfo_t info;
if (!SteamNetworkingSockets()->GetConnectionInfo(
connection->connection, &info))
{
return;
}
unsigned long long remote_id = info.m_identityRemote.GetSteamID64();
for (int t = 0; t < tokenCount; ++t)
{
if (tokens[t].steamID == remote_id)
{
connection->peerAddress.sin_family = AF_INET;
connection->peerAddress.sin_addr.s_addr = tokens[t].addressBE;
connection->peerAddress.sin_port = htons((unsigned short)
((channel == 0) ? TokenConsolePort : TokenGamePort));
return;
}
}
}
//###########################################################################
// 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;
}
FillPeerAddressFromIdentity(connection, channel);
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;
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);
//
// The token handoff (lobby -> mission process): my own token + the
// token->identity map. A lobby/menu process has neither -- fine, it
// only needs matchmaking.
//
tokenCount = 0;
myTokenBE = 0;
const char *my_token = getenv("BT_FE_MYFAKE");
if (my_token != NULL && my_token[0])
{
myTokenBE = inet_addr(my_token);
}
const char *map_text = getenv("BT_FE_STEAMMAP");
if (map_text != NULL && map_text[0])
{
char map_copy[512];
strncpy(map_copy, map_text, sizeof(map_copy) - 1);
map_copy[sizeof(map_copy) - 1] = 0;
char *cursor = strtok(map_copy, ";");
while (cursor != NULL && tokenCount < 8)
{
char *equals = strchr(cursor, '=');
if (equals != NULL)
{
*equals = 0;
tokens[tokenCount].addressBE = inet_addr(cursor);
tokens[tokenCount].steamID = _strtoui64(equals + 1, NULL, 10);
if (tokens[tokenCount].addressBE != INADDR_NONE &&
tokens[tokenCount].steamID != 0)
{
++tokenCount;
}
}
cursor = strtok(NULL, ";");
}
}
//
// Listen on both identity channels (console = 0, game = 1).
//
for (int i = 0; i < FakePortCount; ++i)
{
listenSockets[i] = sockets->CreateListenSocketP2P(i, 0, NULL);
acceptHead[i] = acceptTail[i] = 0;
if (listenSockets[i] == k_HSteamListenSocket_Invalid)
{
STEAMNET_LOG("CreateListenSocketP2P(" << i << ") failed");
}
}
steamActive = 1;
STEAMNET_LOG("up -- identity "
<< (unsigned long long)SteamUser()->GetSteamID().ConvertToUint64()
<< ", " << tokenCount << " roster token(s)"
<< (myTokenBE ? " incl. self" : ""));
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;
}
*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;
}
for (int t = 0; t < tokenCount; ++t)
{
if (tokens[t].addressBE == internet_address_be)
{
return 1;
}
}
return 0;
}
SOCKET
BTSteamNet_Connect(unsigned long internet_address_be, int remote_port)
{
if (!steamActive)
{
return INVALID_SOCKET;
}
unsigned long long steam_id = 0;
for (int t = 0; t < tokenCount; ++t)
{
if (tokens[t].addressBE == internet_address_be)
{
steam_id = tokens[t].steamID;
}
}
if (steam_id == 0)
{
return INVALID_SOCKET;
}
int channel = (remote_port == TokenConsolePort) ? 0 : 1;
SteamNetworkingIdentity identity;
identity.Clear();
identity.SetSteamID64(steam_id);
HSteamNetConnection handle =
SteamNetworkingSockets()->ConnectP2P(identity, channel, 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);
(void)identity;
//
// 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)
{
STEAMNET_LOG("connected to peer " << steam_id
<< " channel " << channel);
return PseudoSocketOf(connection);
}
if (connection->closedByPeer)
{
break;
}
Sleep(100);
}
STEAMNET_LOG("connect failed/timed out");
BTSteamNet_Close(PseudoSocketOf(connection));
return INVALID_SOCKET;
}
int
BTSteamNet_GetFakeAddressRaw(unsigned long *internet_address_be)
{
if (!steamActive || myTokenBE == 0)
{
return -1;
}
*internet_address_be = myTokenBE;
return 0;
}
unsigned long long
BTSteamNet_MySteamID()
{
if (!steamActive)
{
return 0;
}
return SteamUser()->GetSteamID().ConvertToUint64();
}