Files
BT411/game/glass/btl4lobby.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

441 lines
12 KiB
C++

//###########################################################################
// 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;
}