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>
This commit is contained in:
Cyd
2026-07-18 00:03:17 -05:00
co-authored by Claude Fable 5
parent 48ede2f7d7
commit f703bb1d56
11 changed files with 875 additions and 124 deletions
+14
View File
@@ -368,6 +368,20 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
SetEnvironmentVariableA("BT_FE_EGG", NULL);
sprintf(fe_arguments, "-net %d -platform glass", fe_spec.consolePort);
break;
#ifdef BT_STEAM
case BTFeLaunchJoinSteam:
SetEnvironmentVariableA("BT_FE_EGG", NULL);
SetEnvironmentVariableA("BT_STEAM_NET", "1");
SetEnvironmentVariableA("BT_FE_MYFAKE", fe_spec.steamMyToken);
SetEnvironmentVariableA("BT_FE_STEAMMAP", fe_spec.steamMap);
sprintf(fe_arguments, "-net %d -platform glass", fe_spec.consolePort);
break;
case BTFeLaunchHostSteam:
SetEnvironmentVariableA("BT_STEAM_NET", "1");
SetEnvironmentVariableA("BT_FE_MYFAKE", fe_spec.steamMyToken);
SetEnvironmentVariableA("BT_FE_STEAMMAP", fe_spec.steamMap);
// falls into the marshal-armed default
#endif
default: // Solo / HostLan: the marshal armed via env handoff
SetEnvironmentVariableA("BT_FE_EGG", fe_spec.eggPath);
SetEnvironmentVariableA("BT_FE_PODS", fe_spec.podList);
+108 -12
View File
@@ -68,6 +68,56 @@ struct MarshalState
static MarshalState marshalState;
//
// The Steam-wire branch (BT_STEAM): a pod whose console address is a
// FakeIP is reached through the engine's Steam transport (munga_engine)
// instead of raw Winsock -- same wire bytes, different carrier.
//
#ifdef BT_STEAM
extern int BTSteamNet_IsFakeAddress(unsigned long internet_address_be);
extern SOCKET BTSteamNet_Connect(unsigned long internet_address_be, int remote_port);
extern int BTSteamNet_Owns(SOCKET wire_socket);
extern int BTSteamNet_Send(SOCKET wire_socket, const char *data, int length);
extern int BTSteamNet_Recv(SOCKET wire_socket, char *buffer, int capacity);
extern void BTSteamNet_Close(SOCKET wire_socket);
#endif
static int
MarshalIsSteam(SOCKET s)
{
#ifdef BT_STEAM
return BTSteamNet_Owns(s);
#else
(void)s;
return 0;
#endif
}
static int
MarshalSend(SOCKET s, const char *data, int length)
{
#ifdef BT_STEAM
if (BTSteamNet_Owns(s))
{
return BTSteamNet_Send(s, data, length);
}
#endif
return send(s, data, length, 0);
}
static void
MarshalClose(SOCKET s)
{
#ifdef BT_STEAM
if (BTSteamNet_Owns(s))
{
BTSteamNet_Close(s);
return;
}
#endif
closesocket(s);
}
//###########################################################################
void
@@ -199,7 +249,7 @@ static int
PutInt32(packet + 32, wire_length);
PutInt32(packet + 36, this_length);
memcpy(packet + 40, wire + off, this_length);
if (send(s, (const char *)packet, sizeof(packet), 0) != sizeof(packet))
if (MarshalSend(s, (const char *)packet, sizeof(packet)) != sizeof(packet))
{
return -1;
}
@@ -222,7 +272,7 @@ static int
PutInt32(packet + 16, 12);
PutInt32(packet + 20, message_id);
PutInt32(packet + 24, ReliableFlag);
return (send(s, (const char *)packet, sizeof(packet), 0) == sizeof(packet))
return (MarshalSend(s, (const char *)packet, sizeof(packet)) == sizeof(packet))
? 0 : -1;
}
@@ -233,6 +283,24 @@ static int
static SOCKET
ConnectWithRetry(const char *host, int port, int attempts)
{
#ifdef BT_STEAM
{
unsigned long address_be = inet_addr(host);
if (address_be != INADDR_NONE && BTSteamNet_IsFakeAddress(address_be))
{
for (int attempt = 0; attempt < attempts; ++attempt)
{
SOCKET s = BTSteamNet_Connect(address_be, port);
if (s != INVALID_SOCKET)
{
return s;
}
Sleep(1000);
}
return INVALID_SOCKET;
}
}
#endif
for (int attempt = 0; attempt < attempts; ++attempt)
{
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
@@ -348,24 +416,52 @@ static DWORD WINAPI
char drain[4096];
while (GetTickCount() < mission_end)
{
//
// select() only understands REAL sockets; Steam pseudo-sockets
// drain nonblocking on their own pass.
//
fd_set readable;
FD_ZERO(&readable);
int real_count = 0;
for (int p = 0; p < pod_count; ++p)
{
FD_SET(pods[p], &readable);
}
timeval tv = { 1, 0 };
int ready = select(0, &readable, NULL, NULL, &tv);
if (ready > 0)
{
for (int p = 0; p < pod_count; ++p)
if (!MarshalIsSteam(pods[p]))
{
if (FD_ISSET(pods[p], &readable))
FD_SET(pods[p], &readable);
++real_count;
}
}
if (real_count > 0)
{
timeval tv = { 1, 0 };
int ready = select(0, &readable, NULL, NULL, &tv);
if (ready > 0)
{
for (int p = 0; p < pod_count; ++p)
{
recv(pods[p], drain, sizeof(drain), 0);
if (!MarshalIsSteam(pods[p]) &&
FD_ISSET(pods[p], &readable))
{
recv(pods[p], drain, sizeof(drain), 0);
}
}
}
}
else
{
Sleep(1000);
}
#ifdef BT_STEAM
for (int p = 0; p < pod_count; ++p)
{
if (MarshalIsSteam(pods[p]))
{
while (BTSteamNet_Recv(pods[p], drain, sizeof(drain)) > 0)
{
}
}
}
#endif
}
MarshalLog("mission clock expired -- StopMission to all pods");
@@ -376,7 +472,7 @@ static DWORD WINAPI
Sleep(MissionEndGraceSeconds * 1000);
for (int p = 0; p < pod_count; ++p)
{
closesocket(pods[p]);
MarshalClose(pods[p]);
}
MarshalLog("relaunching the menu");
+67 -2
View File
@@ -4,6 +4,9 @@
//###########################################################################
#include "btl4fe.hpp"
#ifdef BT_STEAM
#include "btl4lobby.hpp"
#endif
#include <windows.h>
#include <stdio.h>
@@ -25,7 +28,11 @@ static const char *kVehicles[] = { "bhk1", "madcat", "ava1" };
static const char *kColors[] = { "White", "Black", "Crimson" };
static const char *kModes[] =
{ "SOLO MISSION (networked)", "RAW SOLO (-egg, endless)",
"HOST LAN", "JOIN LAN" };
"HOST LAN", "JOIN LAN",
#ifdef BT_STEAM
"HOST STEAM LOBBY", "JOIN STEAM LOBBY",
#endif
};
static const int kPorts[] = { 1501, 1601, 1701, 1801 };
#define COUNT(a) ((int)(sizeof(a)/sizeof((a)[0])))
@@ -720,10 +727,68 @@ int
}
break;
default: // JOIN LAN: plain -net pod; the host's marshal feeds us
case 3: // JOIN LAN: plain -net pod; the host's marshal feeds us
spec->mode = BTFeLaunchJoinLan;
spec->eggPath[0] = 0; // no egg written; host provides
return 0;
#ifdef BT_STEAM
case 4: // HOST STEAM LOBBY: the room fills the roster (self first)
{
BTLobbyRoster roster;
if (BTLobby_HostAndRoom(self.name, self.vehicle, self.color,
&roster, spec->steamMyToken, sizeof(spec->steamMyToken),
spec->steamMap, sizeof(spec->steamMap)) != 0 ||
roster.memberCount == 0)
{
return 1; // cancelled / Steam unavailable
}
spec->mode = BTFeLaunchHostSteam;
//
// Roster -> egg pilots (FAKE game addresses; self is pilot 1)
// + the marshal pod list (self over TCP loopback, members at
// their FAKE console addresses over the Steam wire).
//
mission.pilotCount = 0;
sprintf(spec->podList, "127.0.0.1:%d", console_port);
for (int m = 0; m < roster.memberCount && m < 8; ++m)
{
const BTLobbyMember &member = roster.members[m];
BTFePilot &pilot = mission.pilots[mission.pilotCount++];
strncpy(pilot.name, member.name, sizeof(pilot.name) - 1);
strncpy(pilot.vehicle,
member.vehicle[0] ? member.vehicle : kVehicles[0],
sizeof(pilot.vehicle) - 1);
strncpy(pilot.color,
member.color[0] ? member.color : kColors[0],
sizeof(pilot.color) - 1);
sprintf(pilot.address, "%s:%d",
member.fakeAddress, member.gamePort);
if (!member.isSelf)
{
char entry[64];
sprintf(entry, ",%s:%d",
member.fakeAddress, member.consolePort);
strcat(spec->podList, entry);
}
}
}
break;
case 5: // JOIN STEAM LOBBY: wait for GO, then a Steam-wire pod
if (BTLobby_JoinAndWait(self.name, self.vehicle, self.color,
spec->steamMyToken, sizeof(spec->steamMyToken),
spec->steamMap, sizeof(spec->steamMap)) != 0)
{
return 1;
}
spec->mode = BTFeLaunchJoinSteam;
spec->eggPath[0] = 0;
return 0;
#endif
default:
return 1;
}
if (BTFeMission_WriteEgg(&mission, spec->eggPath) != 0)
+5 -1
View File
@@ -47,7 +47,9 @@ enum BTFeLaunchMode
BTFeLaunchSolo, // networked 1-pod mission (console marshal + self)
BTFeLaunchRawSolo, // plain -egg endless solo (renderer work)
BTFeLaunchHostLan, // marshal feeds self + remote pods
BTFeLaunchJoinLan // plain -net pod; a remote host's marshal feeds us
BTFeLaunchJoinLan, // plain -net pod; a remote host's marshal feeds us
BTFeLaunchHostSteam, // (BT_STEAM) lobby host: marshal over the Steam wire
BTFeLaunchJoinSteam // (BT_STEAM) lobby member: -net pod on the Steam wire
};
struct BTFeLaunchSpec
@@ -57,6 +59,8 @@ struct BTFeLaunchSpec
int consolePort; // this instance's -net port
char podList[256]; // marshal targets (host modes)
int missionSeconds;
char steamMyToken[48]; // (BT_STEAM) my roster token ip
char steamMap[512]; // (BT_STEAM) ip=steamid64;...
};
//
+440
View File
@@ -0,0 +1,440 @@
//###########################################################################
// btl4lobby -- the Steam lobby (BT_STEAM only; compiled only when the gate
// is on -- see CMakeLists.txt). Design: btl4lobby.hpp.
//###########################################################################
#include "btl4lobby.hpp"
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#pragma pack(push, 8)
#include "steam/steam_api.h"
#include "steam/isteammatchmaking.h"
#pragma pack(pop)
//
// The engine-side transport surface (munga_engine).
//
extern int BTSteamNet_Install();
extern int BTSteamNet_Active();
extern unsigned long long BTSteamNet_MySteamID();
static FILE *lobbyLogFile = NULL;
static void
LobbyLog(const char *format, ...)
{
if (lobbyLogFile == NULL)
{
lobbyLogFile = fopen("marshal.log", "at"); // shares the marshal's file
if (lobbyLogFile == NULL)
{
return;
}
}
va_list arguments;
va_start(arguments, format);
fprintf(lobbyLogFile, "[lobby] ");
vfprintf(lobbyLogFile, format, arguments);
fprintf(lobbyLogFile, "\n");
fflush(lobbyLogFile);
va_end(arguments);
}
#include <stdarg.h>
//###########################################################################
// Synchronous Steam call-result helper (manual polling -- no callback
// template machinery in this C-style TU).
//###########################################################################
static int
WaitApiCall(SteamAPICall_t call, void *result, int result_size,
int expected_callback_id, int timeout_ms)
{
ISteamUtils *utils = SteamUtils();
for (int waited = 0; waited < timeout_ms; waited += 50)
{
SteamAPI_RunCallbacks();
bool failed = false;
if (utils->IsAPICallCompleted(call, &failed))
{
if (failed)
{
return -1;
}
return utils->GetAPICallResult(call, result, result_size,
expected_callback_id, &failed) && !failed ? 0 : -1;
}
Sleep(50);
}
return -1;
}
//###########################################################################
// Member data
//###########################################################################
static CSteamID currentLobby;
static void
PublishSelf(const char *pilot_name, const char *vehicle, const char *color)
{
//
// Identity is implicit (the member's SteamID); the roster TOKENS are
// minted by the host at GO time -- only the pilot identity publishes.
//
ISteamMatchmaking *matchmaking = SteamMatchmaking();
matchmaking->SetLobbyMemberData(currentLobby, "nm", pilot_name);
matchmaking->SetLobbyMemberData(currentLobby, "vh", vehicle);
matchmaking->SetLobbyMemberData(currentLobby, "cl", color);
}
static int
ReadMember(CSteamID lobby, int index, BTLobbyMember *out)
{
ISteamMatchmaking *matchmaking = SteamMatchmaking();
CSteamID member = matchmaking->GetLobbyMemberByIndex(lobby, index);
const char *published_name = matchmaking->GetLobbyMemberData(lobby, member, "nm");
if (published_name == NULL || published_name[0] == 0)
{
return -1; // not published yet
}
memset(out, 0, sizeof(*out));
out->steamID = member.ConvertToUint64();
out->consolePort = 1501; // token ports (see L4STEAMNET token table)
out->gamePort = 1502;
strncpy(out->name,
matchmaking->GetLobbyMemberData(lobby, member, "nm"),
sizeof(out->name) - 1);
strncpy(out->vehicle,
matchmaking->GetLobbyMemberData(lobby, member, "vh"),
sizeof(out->vehicle) - 1);
strncpy(out->color,
matchmaking->GetLobbyMemberData(lobby, member, "cl"),
sizeof(out->color) - 1);
out->isSelf = (member == SteamUser()->GetSteamID());
if (out->name[0] == 0)
{
strcpy(out->name, "Pilot");
}
return 0;
}
//###########################################################################
// The room screen -- the FE's green-on-black style; lists members, host
// ENTER = GO, ESC = cancel.
//###########################################################################
static int roomIsHost = 0;
static int roomResult = 0; // 0 pending, 1 go, -1 cancel
static void
PaintRoom(HWND window)
{
PAINTSTRUCT paint;
HDC dc = BeginPaint(window, &paint);
RECT client;
GetClientRect(window, &client);
HBRUSH background = CreateSolidBrush(RGB(4, 12, 4));
FillRect(dc, &client, background);
DeleteObject(background);
SetBkMode(dc, TRANSPARENT);
HFONT font = CreateFontW(-16, 0, 0, 0, FW_NORMAL, 0, 0, 0,
DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, FIXED_PITCH, L"Consolas");
HFONT old_font = (HFONT)SelectObject(dc, font);
SetTextColor(dc, RGB(90, 255, 90));
WCHAR line[160];
int y = 16;
wsprintfW(line, L"STEAM LOBBY -- %s",
roomIsHost ? L"HOSTING (ENTER = launch, ESC = cancel)"
: L"WAITING FOR THE HOST (ESC = leave)");
RECT row = { 24, y, client.right - 24, y + 24 };
DrawTextW(dc, line, -1, &row, DT_LEFT | DT_TOP | DT_SINGLELINE);
y += 40;
ISteamMatchmaking *matchmaking = SteamMatchmaking();
int count = matchmaking->GetNumLobbyMembers(currentLobby);
for (int i = 0; i < count && i < 8; ++i)
{
BTLobbyMember member;
WCHAR name[120];
if (ReadMember(currentLobby, i, &member) == 0)
{
int n = 0;
for (const char *s = member.name; *s && n < 60; ++s) name[n++] = (WCHAR)*s;
name[n] = 0;
wsprintfW(line, L" %d. %s%s", i + 1, name,
member.isSelf ? L" (you)" : L"");
}
else
{
wsprintfW(line, L" %d. (joining...)", i + 1);
}
RECT member_row = { 24, y, client.right - 24, y + 24 };
DrawTextW(dc, line, -1, &member_row, DT_LEFT | DT_TOP | DT_SINGLELINE);
y += 26;
}
SelectObject(dc, old_font);
DeleteObject(font);
EndPaint(window, &paint);
}
static LRESULT CALLBACK
RoomWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam)
{
switch (message)
{
case WM_PAINT:
PaintRoom(window);
return 0;
case WM_TIMER:
SteamAPI_RunCallbacks();
//
// Members leave when the host signals GO.
//
if (!roomIsHost)
{
const char *go = SteamMatchmaking()->GetLobbyData(currentLobby, "btl4go");
if (go != NULL && go[0] == '1')
{
roomResult = 1;
}
}
InvalidateRect(window, NULL, FALSE);
return 0;
case WM_KEYDOWN:
if (wparam == VK_RETURN && roomIsHost)
{
roomResult = 1;
}
else if (wparam == VK_ESCAPE)
{
roomResult = -1;
}
return 0;
case WM_CLOSE:
roomResult = -1;
return 0;
}
return DefWindowProcW(window, message, wparam, lparam);
}
static int
RunRoom(int is_host)
{
roomIsHost = is_host;
roomResult = 0;
WNDCLASSW window_class;
memset(&window_class, 0, sizeof(window_class));
window_class.lpfnWndProc = RoomWndProc;
window_class.hInstance = GetModuleHandleW(NULL);
window_class.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW);
window_class.lpszClassName = L"BTLobbyRoomWnd";
RegisterClassW(&window_class);
HWND window = CreateWindowW(
L"BTLobbyRoomWnd", L"BattleTech - Steam Lobby",
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU,
160, 160, 560, 360,
NULL, NULL, GetModuleHandleW(NULL), NULL);
if (window == NULL)
{
return -1;
}
SetTimer(window, 1, 250, NULL);
ShowWindow(window, SW_SHOW);
SetForegroundWindow(window);
MSG message;
while (roomResult == 0 && GetMessageW(&message, NULL, 0, 0))
{
TranslateMessage(&message);
DispatchMessageW(&message);
}
KillTimer(window, 1);
DestroyWindow(window);
return (roomResult == 1) ? 0 : 1;
}
//###########################################################################
// Host / member entries
//###########################################################################
int
BTLobby_HostAndRoom(
const char *pilot_name, const char *vehicle, const char *color,
BTLobbyRoster *roster_out,
char *my_token_out, int my_token_capacity,
char *steam_map_out, int steam_map_capacity)
{
memset(roster_out, 0, sizeof(*roster_out));
if (BTSteamNet_Install() != 0 || !BTSteamNet_Active())
{
LobbyLog("host: Steam transport unavailable");
return -1;
}
SteamAPICall_t call = SteamMatchmaking()->CreateLobby(k_ELobbyTypePublic, 8);
LobbyCreated_t created;
memset(&created, 0, sizeof(created));
if (WaitApiCall(call, &created, sizeof(created),
LobbyCreated_t::k_iCallback, 15000) != 0 ||
created.m_eResult != k_EResultOK)
{
LobbyLog("host: CreateLobby failed");
return -1;
}
currentLobby = created.m_ulSteamIDLobby;
SteamMatchmaking()->SetLobbyData(currentLobby, "btl4", "1");
PublishSelf(pilot_name, vehicle, color);
LobbyLog("host: lobby up (%llu)", (unsigned long long)created.m_ulSteamIDLobby);
if (RunRoom(1) != 0)
{
SteamMatchmaking()->LeaveLobby(currentLobby);
return 1;
}
//
// GO: snapshot the roster SELF FIRST (the egg's pilot order defines
// the connect topology; the host must be pilot 1), MINT the tokens,
// publish the token map, then signal.
//
ISteamMatchmaking *matchmaking = SteamMatchmaking();
int count = matchmaking->GetNumLobbyMembers(currentLobby);
for (int pass = 0; pass < 2; ++pass)
{
for (int i = 0; i < count && roster_out->memberCount < 8; ++i)
{
BTLobbyMember member;
if (ReadMember(currentLobby, i, &member) != 0)
{
continue;
}
if ((pass == 0) == (member.isSelf != 0))
{
roster_out->members[roster_out->memberCount++] = member;
}
}
}
steam_map_out[0] = 0;
my_token_out[0] = 0;
for (int m = 0; m < roster_out->memberCount; ++m)
{
BTLobbyMember &member = roster_out->members[m];
sprintf(member.fakeAddress, "169.254.77.%d", m + 1);
char entry[96];
sprintf(entry, "%s%s=%llu", (m > 0) ? ";" : "",
member.fakeAddress, member.steamID);
if ((int)(strlen(steam_map_out) + strlen(entry) + 1)
< steam_map_capacity)
{
strcat(steam_map_out, entry);
}
if (member.isSelf)
{
strncpy(my_token_out, member.fakeAddress, my_token_capacity - 1);
my_token_out[my_token_capacity - 1] = 0;
}
}
matchmaking->SetLobbyData(currentLobby, "btl4map", steam_map_out);
matchmaking->SetLobbyData(currentLobby, "btl4go", "1");
SteamAPI_RunCallbacks();
LobbyLog("host: GO with %d member(s), map [%s]",
roster_out->memberCount, steam_map_out);
//
// The lobby will not survive the relaunch (documented); leave now so
// members do not see a ghost host.
//
Sleep(1500); // let the GO propagate
SteamMatchmaking()->LeaveLobby(currentLobby);
return 0;
}
int
BTLobby_JoinAndWait(
const char *pilot_name, const char *vehicle, const char *color,
char *my_token_out, int my_token_capacity,
char *steam_map_out, int steam_map_capacity)
{
if (BTSteamNet_Install() != 0 || !BTSteamNet_Active())
{
LobbyLog("join: Steam transport unavailable");
return -1;
}
ISteamMatchmaking *matchmaking = SteamMatchmaking();
matchmaking->AddRequestLobbyListStringFilter(
"btl4", "1", k_ELobbyComparisonEqual);
SteamAPICall_t call = matchmaking->RequestLobbyList();
LobbyMatchList_t match_list;
memset(&match_list, 0, sizeof(match_list));
if (WaitApiCall(call, &match_list, sizeof(match_list),
LobbyMatchList_t::k_iCallback, 15000) != 0 ||
match_list.m_nLobbiesMatching == 0)
{
LobbyLog("join: no btl4 lobby found");
return -1;
}
CSteamID lobby = matchmaking->GetLobbyByIndex(0);
call = matchmaking->JoinLobby(lobby);
LobbyEnter_t entered;
memset(&entered, 0, sizeof(entered));
if (WaitApiCall(call, &entered, sizeof(entered),
LobbyEnter_t::k_iCallback, 15000) != 0 ||
entered.m_EChatRoomEnterResponse != k_EChatRoomEnterResponseSuccess)
{
LobbyLog("join: JoinLobby failed");
return -1;
}
currentLobby = lobby;
PublishSelf(pilot_name, vehicle, color);
LobbyLog("join: in lobby, waiting for GO");
int result = RunRoom(0);
if (result == 0)
{
//
// GO: read the token map, find my own token by my SteamID.
//
const char *map_text = matchmaking->GetLobbyData(currentLobby, "btl4map");
steam_map_out[0] = 0;
my_token_out[0] = 0;
if (map_text != NULL)
{
strncpy(steam_map_out, map_text, steam_map_capacity - 1);
steam_map_out[steam_map_capacity - 1] = 0;
char needle[32];
sprintf(needle, "=%llu", BTSteamNet_MySteamID());
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)
{
char *hit = strstr(cursor, needle);
if (hit != NULL && hit[strlen(needle)] == 0)
{
*hit = 0;
strncpy(my_token_out, cursor, my_token_capacity - 1);
my_token_out[my_token_capacity - 1] = 0;
}
cursor = strtok(NULL, ";");
}
}
LobbyLog("join: my token [%s], map [%s]", my_token_out, steam_map_out);
if (my_token_out[0] == 0)
{
result = -1; // the host's map is missing us
}
}
SteamMatchmaking()->LeaveLobby(currentLobby);
return result;
}
+61
View File
@@ -0,0 +1,61 @@
#pragma once
//###########################################################################
//
// btl4lobby -- the Steam lobby (BT_STEAM only): an ISteamMatchmaking room
// replacing the arcade Site-Management screen for internet MP testing.
//
// The MENU process runs the lobby (the Steam transport is installed there
// first, so each member's FakeIP is known): members publish their fake
// address/ports + pilot identity as lobby member data; the host's room
// screen lists members and its ENTER writes the GO signal; everyone then
// relaunches into the mission -- the host with the marshal armed against
// the members' fake CONSOLE addresses, members as plain -net pods with
// BT_STEAM_NET=1 waiting to be fed over the Steam wire.
//
// Known limitation (documented): the lobby object does not survive the
// per-mission process relaunch -- after a mission everyone re-joins from
// the menu. FakeIP stability across the immediate relaunch is assumed
// (Valve persists assignments per app+account); the live 2-account test
// is the step's exit criterion (docs/STEAM_TEST.md).
//
//###########################################################################
struct BTLobbyMember
{
char name[32];
char fakeAddress[48]; // the minted roster TOKEN ip
int consolePort; // token console port (1501)
int gamePort; // token game port (1502)
char vehicle[16];
char color[16];
int isSelf;
unsigned long long steamID;
};
struct BTLobbyRoster
{
int memberCount;
BTLobbyMember members[8];
};
//
// Host: create the lobby, run the room screen, on ENTER write GO and fill
// the roster (self first). Returns 0 = launch, nonzero = cancelled.
//
int
BTLobby_HostAndRoom(
const char *pilot_name, const char *vehicle, const char *color,
BTLobbyRoster *roster_out,
char *my_token_out, int my_token_capacity,
char *steam_map_out, int steam_map_capacity);
//
// Member: find + join a lobby, run the room screen, return when the host
// signals GO. Returns 0 = launch, nonzero = cancelled / none found.
//
int
BTLobby_JoinAndWait(
const char *pilot_name, const char *vehicle, const char *color,
char *my_token_out, int my_token_capacity,
char *steam_map_out, int steam_map_capacity);