Files
BT411/game/glass/btl4lobby.cpp
T
CydandClaude Fable 5 8cf2cc1f07 FE: the RP412 menu + lobby-room pattern; flat single-exe dist (per Cyd)
Menu (btl4fe, modeled on RP412 RP_L4/RPL4FE.cpp): full catalogs as
always-visible clickable LISTS in three columns with friendly names --
8 maps, the canonical 8 certified mechs, colors, times, weather, lengths,
modes -- bright/dim green with a selection chevron, double-buffered paint,
real EDIT controls for pilot name + LAN peers (green-on-black via
WM_CTLCOLOREDIT), framed LAUNCH / HOST STEAM / JOIN STEAM buttons, and the
loadout persisted across the per-mission relaunch (fe_last.ini, gitignored
implicitly by *.ini? no -- plain file beside the eggs).  ENTER/ESC kept.

Lobby room (RP412 RPL4LOBBY look): pilot roster with loadouts (mech, color),
framed L A U N C H  M I S S I O N (owner) + LEAVE LOBBY buttons, clickable.

Dist: FLAT layout (the RP412 convention) -- ONE btl4.exe entry point at the
folder root beside the content tree (the shape TeslaConsole manages), DLLs +
launchers at root, content flattened from git-tracked files.  Verified: the
staged flat tree boots the Mission Console zero-arg from its own root; the
click-driven menu launches a full marshaled mission (row click -> length=60
in the egg, LAUNCH click -> ladder -> 41+ ticks, fe_last.ini written).

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

480 lines
13 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 RECT roomLaunchRect = { 140, 250, 420, 296 };
static RECT roomLeaveRect = { 140, 306, 300, 336 };
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));
//
// The RP412 room look (RP_L4/RPL4LOBBY.cpp): pilot roster with
// loadouts, a framed LAUNCH button for the owner, LEAVE for all.
//
WCHAR line[160];
int y = 16;
wsprintfW(line, L"STEAM LOBBY -- %s",
roomIsHost ? L"YOU ARE HOSTING" : L"WAITING FOR THE HOST");
RECT row = { 24, y, client.right - 24, y + 26 };
DrawTextW(dc, line, -1, &row, DT_LEFT | DT_TOP | DT_SINGLELINE);
y += 44;
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. %-16s %hs, %hs%s", i + 1, name,
member.vehicle[0] ? member.vehicle : "mech",
member.color[0] ? member.color : "-",
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;
}
//
// Framed buttons (hit rects shared with the WndProc).
//
HBRUSH frame = CreateSolidBrush(RGB(90, 255, 90));
if (roomIsHost)
{
FrameRect(dc, &roomLaunchRect, frame);
DrawTextW(dc, L"L A U N C H M I S S I O N", -1, &roomLaunchRect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
FrameRect(dc, &roomLeaveRect, frame);
DrawTextW(dc, L"LEAVE LOBBY", -1, &roomLeaveRect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
DeleteObject(frame);
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_LBUTTONDOWN:
{
int x = (int)(short)LOWORD(lparam);
int y = (int)(short)HIWORD(lparam);
if (roomIsHost &&
x >= roomLaunchRect.left && x < roomLaunchRect.right &&
y >= roomLaunchRect.top && y < roomLaunchRect.bottom)
{
roomResult = 1;
}
else if (x >= roomLeaveRect.left && x < roomLeaveRect.right &&
y >= roomLeaveRect.top && y < roomLeaveRect.bottom)
{
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;
}