Steam lobby: host, join, room, and launch into a marshaled race

RPL4LOBBY implements the multiplayer front door on ISteamMatchmaking.
The setup menu grows HOST STEAM RACE / JOIN STEAM RACE buttons when
the Steam wire is live; hosting creates a tagged public lobby, joining
finds one. Every member publishes FakeIP + fake ports + persona +
loadout as member data; the room screen lists members (host marked)
and gives the owner a launch button.

Launching writes a nonced go-roster into lobby data. Each pod
registers every peer with the Steam transport (two-port peer table:
engine console/game ports map to Steam fake ports on connect) and
enters the race: the owner through the hosted-race path - it builds
the multi-pilot egg from real personas and loadouts and its console
marshals everyone - and members as network pods that boot straight
into WaitingForEgg for the owner to feed over the wire.

The lobby outlives races: members loop back through WinMain into the
room (no local console needed - MissionCompleted is waived for member
races), and the owner returns to the room after its results screen.
Leaving the lobby clears the hosted-race priming.

Verified on this box: menu buttons appear under RP412STEAM=1, hosting
creates a lobby on the Steam backend, the room runs and leaves back to
the menu; single-player cycling and the LAN hosted race both still
pass. Full three-account mesh test is next, on real hardware.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-12 21:26:31 -05:00
co-authored by Claude Fable 5
parent c66cb00a7e
commit 5892af318e
8 changed files with 1191 additions and 172 deletions
+44 -12
View File
@@ -58,9 +58,13 @@ namespace
struct PeerRecord
{
unsigned long fakeIP; // host order
unsigned short fakeConsolePort; // host order
unsigned short fakeGamePort; // host order
};
// 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 };
@@ -136,14 +140,17 @@ namespace
}
}
// engine port in a SOCKADDR_IN -> the peer's matching fake port
unsigned short
LookupPeerFakeGamePort(unsigned long fake_ip_host)
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 gPeers[i].fakeGamePort;
return (engine_port == gEngineConsolePort)
? gPeers[i].fakeConsolePort
: gPeers[i].fakeGamePort;
}
}
return 0;
@@ -272,8 +279,9 @@ namespace
)
{
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)
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;
@@ -281,8 +289,8 @@ namespace
}
DEBUG_STREAM << "SteamNetTransport: connecting to "
<< inet_ntoa(remote->sin_addr) << " (fake port "
<< fake_game_port << ")...\n" << std::flush;
<< inet_ntoa(remote->sin_addr) << ":" << ntohs(remote->sin_port)
<< " (fake port " << fake_port << ")...\n" << std::flush;
SteamNetworkingConfigValue_t option;
option.SetPtr(
@@ -291,7 +299,7 @@ namespace
SteamNetworkingIPAddr target;
target.Clear();
target.SetIPv4(remote_fake_ip, fake_game_port);
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
@@ -620,32 +628,56 @@ const char *
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
)
{
if (gPeerCount >= maxPeers)
{
return;
}
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;
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;
++gPeerCount;
DEBUG_STREAM << "SteamNetTransport: peer registered: " << fake_ip
<< " game port " << fake_game_port << "\n" << std::flush;
<< " (console " << fake_console_port << ", game "
<< fake_game_port << ")\n" << std::flush;
}
#endif // RP412_STEAM
+13 -3
View File
@@ -68,15 +68,25 @@ Logical
const char *
SteamNetTransport_GetFakeAddressString();
// Our Steam-assigned fake game port (lobby member data).
// Our Steam-assigned fake ports (lobby member data): the console
// channel and the game mesh.
int
SteamNetTransport_GetFakeConsolePort();
int
SteamNetTransport_GetFakeGamePort();
// The lobby feeds every member's FakeIP + fake game port here before
// StartConnecting runs.
// The engine-side port convention for this session (default 1501;
// game port is +1). Connect() maps an engine port in a SOCKADDR_IN to
// the peer's matching Steam fake port.
void
SteamNetTransport_SetEnginePorts(int console_port);
// The lobby feeds every member's FakeIP + fake ports here before
// anything dials out. Registering is idempotent per IP.
void
SteamNetTransport_RegisterPeer(
const char *fake_ip,
int fake_console_port,
int fake_game_port
);