//########################################################################### // btl4lobby -- the Steam lobby (BT_STEAM only; compiled only when the gate // is on -- see CMakeLists.txt). Design: btl4lobby.hpp. //########################################################################### #include "btl4lobby.hpp" #include #include #include #include #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 //########################################################################### // 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; }