#include "..\munga_l4\mungal4.h" #pragma hdrstop #include "rpl4lobby.h" #ifndef RP412_STEAM //######################################################################## // No Steam SDK in this build: the lobby is unavailable, everything // else (single player, LAN -net races) works as always. //######################################################################## Logical RPL4Lobby_Available() { return False; } Logical RPL4Lobby_InRoom() { return False; } int RPL4Lobby_Host(HINSTANCE, HWND) { return LobbyRoomLeft; } int RPL4Lobby_Join(HINSTANCE, HWND) { return LobbyRoomLeft; } int RPL4Lobby_Room(HINSTANCE, HWND) { return LobbyRoomLeft; } void RPL4Lobby_PushRaceResults() { } void RPL4Lobby_PullRaceResults() { } #else #include "rpl4fe.h" #include "rpl4console.h" #include "..\munga_l4\l4steamtransport.h" #pragma pack(push, 8) #include "steam/steam_api.h" #pragma pack(pop) #include //######################################################################## // Lobby state - one lobby at a time, alive across races //######################################################################## namespace { enum { kMaxLobbyMembers = 4 }; const char kLobbyTagKey[] = "rp412"; const char kGoKey[] = "go"; CSteamID gLobby; Logical gInLobby = False; int gLastGoNonce = 0; // launches we already answered int gShownResultsNonce = 0; // score sheets we already displayed const char kResultsKey[] = "res"; // async call-result plumbing Logical gCallDone = False; Logical gCallOK = False; CSteamID gCallLobby; class LobbyCalls { public: CCallResult createResult; CCallResult listResult; CCallResult enterResult; void OnCreated(LobbyCreated_t *result, bool io_failure) { gCallOK = !io_failure && result->m_eResult == k_EResultOK; gCallLobby = CSteamID(result->m_ulSteamIDLobby); gCallDone = True; } void OnList(LobbyMatchList_t *result, bool io_failure) { gCallOK = !io_failure && result->m_nLobbiesMatching > 0; if (gCallOK) { gCallLobby = SteamMatchmaking()->GetLobbyByIndex(0); } gCallDone = True; } void OnEnter(LobbyEnter_t *result, bool io_failure) { gCallOK = !io_failure && result->m_EChatRoomEnterResponse == k_EChatRoomEnterResponseSuccess; gCallLobby = CSteamID(result->m_ulSteamIDLobby); gCallDone = True; } }; LobbyCalls gCalls; Logical WaitForCall(int timeout_ms) { DWORD deadline = GetTickCount() + timeout_ms; while (!gCallDone) { SteamAPI_RunCallbacks(); if ((LONG)(GetTickCount() - deadline) >= 0) { return False; } Sleep(50); } return gCallOK; } //--------------------------------------------------------------- // Member data: what every pod publishes on entering the room //--------------------------------------------------------------- void SanitizeName(const char *in, char *out, int out_size) { int n = 0; for (int i = 0; in[i] != '\0' && n < out_size - 1; ++i) { char c = in[i]; if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == ' ' || c == '-' || c == '_') { out[n++] = c; } } out[n] = '\0'; if (n == 0) { strcpy(out, "Pilot"); } } void PublishMemberData() { char value[64]; SteamMatchmaking()->SetLobbyMemberData(gLobby, "ip", SteamNetTransport_GetFakeAddressString()); sprintf(value, "%d", SteamNetTransport_GetFakeConsolePort()); SteamMatchmaking()->SetLobbyMemberData(gLobby, "cp", value); sprintf(value, "%d", SteamNetTransport_GetFakeGamePort()); SteamMatchmaking()->SetLobbyMemberData(gLobby, "gp", value); char name[32]; SanitizeName(SteamFriends()->GetPersonaName(), name, sizeof(name)); SteamMatchmaking()->SetLobbyMemberData(gLobby, "nm", name); char vehicle[24], color[16], badge[24]; RPL4FrontEnd_GetLoadout(vehicle, color, badge); SteamMatchmaking()->SetLobbyMemberData(gLobby, "vh", vehicle); SteamMatchmaking()->SetLobbyMemberData(gLobby, "cl", color); SteamMatchmaking()->SetLobbyMemberData(gLobby, "bd", badge); } Logical IsOwner() { return gInLobby && SteamMatchmaking()->GetLobbyOwner(gLobby) == SteamUser()->GetSteamID(); } //--------------------------------------------------------------- // Launch: roster into lobby data, peers into the transport, and // the hosted-race path primed on the owner //--------------------------------------------------------------- struct MemberInfo { CSteamID id; char ip[32]; int consolePort; int gamePort; char name[32]; char vehicle[24]; char color[16]; char badge[24]; Logical published; }; int CollectMembers(MemberInfo *members) { int count = SteamMatchmaking()->GetNumLobbyMembers(gLobby); if (count > kMaxLobbyMembers) { count = kMaxLobbyMembers; } for (int i = 0; i < count; ++i) { MemberInfo *member = &members[i]; memset(member, 0, sizeof(*member)); member->id = SteamMatchmaking()->GetLobbyMemberByIndex(gLobby, i); strncpy(member->ip, SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "ip"), sizeof(member->ip) - 1); member->consolePort = atoi( SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "cp")); member->gamePort = atoi( SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "gp")); strncpy(member->name, SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "nm"), sizeof(member->name) - 1); strncpy(member->vehicle, SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "vh"), sizeof(member->vehicle) - 1); strncpy(member->color, SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "cl"), sizeof(member->color) - 1); strncpy(member->badge, SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "bd"), sizeof(member->badge) - 1); member->published = member->ip[0] != '\0' && member->consolePort > 0 && member->gamePort > 0; } return count; } void RegisterRoster(MemberInfo *members, int count) { SteamNetTransport_SetEnginePorts(1501); for (int i = 0; i < count; ++i) { if (members[i].published) { SteamNetTransport_RegisterPeer( members[i].ip, members[i].consolePort, members[i].gamePort, members[i].id.ConvertToUint64()); } } } // Owner side: prime RPL4FE + WinMain's hosted-race path void PrimeHostedRace(MemberInfo *members, int count) { static char pods_env[512]; static char addr_env[64]; static char port_env[32]; CSteamID self = SteamUser()->GetSteamID(); FEHostedPilot pilots[kMaxLobbyMembers]; int pilot_count = 0; char pods[512] = ""; for (int i = 0; i < count; ++i) { if (members[i].id == self || !members[i].published) { continue; } FEHostedPilot *pilot = &pilots[pilot_count++]; memset(pilot, 0, sizeof(*pilot)); sprintf(pilot->address, "%s:1502", members[i].ip); strncpy(pilot->name, members[i].name, sizeof(pilot->name) - 1); strncpy(pilot->vehicle, members[i].vehicle, sizeof(pilot->vehicle) - 1); strncpy(pilot->color, members[i].color, sizeof(pilot->color) - 1); strncpy(pilot->badge, members[i].badge, sizeof(pilot->badge) - 1); if (pods[0] != '\0') { strcat(pods, ","); } strcat(pods, members[i].ip); strcat(pods, ":1501"); } RPL4FrontEnd_SetHostedPilots( SteamNetTransport_GetFakeAddressString(), pilots, pilot_count); sprintf(pods_env, "RP412HOSTPODS=%s", pods); putenv(pods_env); sprintf(addr_env, "RP412HOSTADDR=%s", SteamNetTransport_GetFakeAddressString()); putenv(addr_env); strcpy(port_env, "RP412HOSTPORT=1501"); putenv(port_env); } //--------------------------------------------------------------- // The room screen (front-end style: green on black) //--------------------------------------------------------------- const COLORREF kGreenBright = RGB(64, 255, 64); const COLORREF kGreenDim = RGB(24, 140, 24); struct RoomState { HWND window; HFONT textFont; HFONT titleFont; RECT launchRect; // owner only RECT leaveRect; Logical launchClicked; Logical leaveClicked; Logical closed; MemberInfo members[kMaxLobbyMembers]; int memberCount; }; RoomState *gRoom = NULL; void PaintRoom(RoomState *room) { PAINTSTRUCT ps; HDC hdc = BeginPaint(room->window, &ps); RECT client; GetClientRect(room->window, &client); HDC mem = CreateCompatibleDC(hdc); HBITMAP surface = CreateCompatibleBitmap(hdc, client.right, client.bottom); HBITMAP old_surface = (HBITMAP) SelectObject(mem, surface); FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH)); SetBkMode(mem, TRANSPARENT); SelectObject(mem, room->titleFont); SetTextColor(mem, kGreenBright); RECT title = client; title.top = client.bottom / 28; title.bottom = title.top + client.bottom / 10; DrawTextA(mem, "STEAM RACE LOBBY", -1, &title, DT_CENTER | DT_TOP | DT_SINGLELINE); SelectObject(mem, room->textFont); int row_h = client.bottom / 16; int top = client.bottom / 4; int left = client.right / 4; int right = 3 * client.right / 4; char text[128]; for (int i = 0; i < room->memberCount; ++i) { MemberInfo *member = &room->members[i]; RECT row; row.left = left; row.right = right; row.top = top + i * row_h; row.bottom = row.top + row_h; Logical is_owner_row = SteamMatchmaking()->GetLobbyOwner(gLobby) == member->id; SetTextColor(mem, member->published ? kGreenBright : kGreenDim); sprintf(text, "%s%s", member->name[0] ? member->name : "(joining...)", is_owner_row ? " [HOST]" : ""); DrawTextA(mem, text, -1, &row, DT_LEFT | DT_VCENTER | DT_SINGLELINE); if (member->vehicle[0] != '\0') { sprintf(text, "%s / %s", member->vehicle, member->color); DrawTextA(mem, text, -1, &row, DT_RIGHT | DT_VCENTER | DT_SINGLELINE); } } SetTextColor(mem, kGreenDim); RECT hint; hint.left = left; hint.right = right; hint.top = top + kMaxLobbyMembers * row_h + row_h / 2; hint.bottom = hint.top + row_h; DrawTextA(mem, IsOwner() ? "You are the console: launch when everyone is in." : "Waiting for the host to launch...", -1, &hint, DT_CENTER | DT_VCENTER | DT_SINGLELINE); HBRUSH frame = CreateSolidBrush(kGreenBright); if (IsOwner()) { FrameRect(mem, &room->launchRect, frame); SetTextColor(mem, kGreenBright); DrawTextA(mem, "L A U N C H R A C E", -1, &room->launchRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); } FrameRect(mem, &room->leaveRect, frame); SetTextColor(mem, kGreenBright); DrawTextA(mem, "LEAVE LOBBY", -1, &room->leaveRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); DeleteObject(frame); BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY); SelectObject(mem, old_surface); DeleteObject(surface); DeleteDC(mem); EndPaint(room->window, &ps); } LRESULT CALLBACK RoomWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { RoomState *room = gRoom; switch (message) { case WM_PAINT: if (room != NULL) { PaintRoom(room); return 0; } break; case WM_LBUTTONDOWN: if (room != NULL) { POINT point = { (int)(short) LOWORD(lParam), (int)(short) HIWORD(lParam) }; if (IsOwner() && PtInRect(&room->launchRect, point)) { room->launchClicked = True; } else if (PtInRect(&room->leaveRect, point)) { room->leaveClicked = True; } return 0; } break; case WM_ERASEBKGND: return 1; } return DefWindowProcA(hwnd, message, wParam, lParam); } //--------------------------------------------------------------- // The room loop: pump Steam + Windows, watch for launch/leave //--------------------------------------------------------------- int RunRoom(HINSTANCE instance, HWND main_window) { static Logical class_registered = False; if (!class_registered) { class_registered = True; WNDCLASSA window_class; memset(&window_class, 0, sizeof(window_class)); window_class.style = CS_HREDRAW | CS_VREDRAW; window_class.lpfnWndProc = RoomWndProc; window_class.hInstance = instance; window_class.hCursor = LoadCursor(NULL, IDC_ARROW); window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH); window_class.lpszClassName = "RPLobby"; RegisterClassA(&window_class); } RECT client; GetClientRect(main_window, &client); RoomState room; memset(&room, 0, sizeof(room)); room.window = CreateWindowExA( 0, "RPLobby", "", WS_CHILD | WS_VISIBLE, 0, 0, client.right, client.bottom, main_window, NULL, instance, NULL); if (room.window == NULL) { return LobbyRoomClosed; } int text_h = client.bottom / 40; if (text_h < 16) text_h = 16; if (text_h > 24) text_h = 24; room.textFont = CreateFontA(-(text_h * 3 / 2), 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN, "Consolas"); room.titleFont = CreateFontA(-(text_h * 2), 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN, "Consolas"); int row_h = client.bottom / 36; if (row_h < 18) row_h = 18; if (row_h > 30) row_h = 30; int col_w = client.right / 4; room.launchRect.left = (client.right - col_w) / 2; room.launchRect.right = room.launchRect.left + col_w; room.launchRect.top = client.bottom - 6 * row_h; room.launchRect.bottom = room.launchRect.top + 2 * row_h; room.leaveRect.left = (client.right - col_w / 2) / 2; room.leaveRect.right = room.leaveRect.left + col_w / 2; room.leaveRect.top = client.bottom - 3 * row_h; room.leaveRect.bottom = client.bottom - row_h; gRoom = &room; PublishMemberData(); int outcome = -1; DWORD last_refresh = 0; while (outcome < 0) { MSG msg; while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) { room.closed = True; } TranslateMessage(&msg); DispatchMessageA(&msg); } SteamAPI_RunCallbacks(); if (!IsWindow(main_window) || room.closed) { outcome = LobbyRoomClosed; break; } if (room.leaveClicked) { SteamMatchmaking()->LeaveLobby(gLobby); gInLobby = False; // plain menu launches must not inherit lobby hosting RPL4FrontEnd_SetHostedPilots(NULL, NULL, 0); static char clear_env[] = "RP412HOSTPODS="; putenv(clear_env); outcome = LobbyRoomLeft; break; } // // Owner launch: roster into lobby data + local prime // if (room.launchClicked) { room.launchClicked = False; room.memberCount = CollectMembers(room.members); Logical all_published = True; for (int i = 0; i < room.memberCount; ++i) { if (!room.members[i].published) { all_published = False; } } if (all_published && room.memberCount >= 1) { ++gLastGoNonce; char go[800]; sprintf(go, "%d:", gLastGoNonce); for (int i = 0; i < room.memberCount; ++i) { char entry[96]; sprintf(entry, "%s|%d|%d|%I64u;", room.members[i].ip, room.members[i].consolePort, room.members[i].gamePort, room.members[i].id.ConvertToUint64()); if (strlen(go) + strlen(entry) < sizeof(go)) { strcat(go, entry); } } SteamMatchmaking()->SetLobbyData(gLobby, kGoKey, go); RegisterRoster(room.members, room.memberCount); PrimeHostedRace(room.members, room.memberCount); outcome = LobbyLaunchHost; break; } DEBUG_STREAM << "Lobby: not everyone is ready yet\n" << std::flush; } // // Member: watch for the owner's go signal // if (!IsOwner()) { const char *go = SteamMatchmaking()->GetLobbyData(gLobby, kGoKey); if (go != NULL && go[0] != '\0') { int nonce = atoi(go); if (nonce > gLastGoNonce) { gLastGoNonce = nonce; // register every rostered peer with the transport SteamNetTransport_SetEnginePorts(1501); const char *cursor = strchr(go, ':'); cursor = (cursor != NULL) ? cursor + 1 : go; while (*cursor != '\0') { char ip[32]; int console_port = 0, game_port = 0; unsigned __int64 steam_id = 0; int n = 0; while (cursor[n] != '\0' && cursor[n] != '|' && n < (int) sizeof(ip) - 1) { ip[n] = cursor[n]; ++n; } ip[n] = '\0'; cursor += n; if (*cursor == '|') { console_port = atoi(++cursor); while (*cursor != '\0' && *cursor != '|') ++cursor; } if (*cursor == '|') { game_port = atoi(++cursor); while (*cursor != '\0' && *cursor != '|' && *cursor != ';') ++cursor; } if (*cursor == '|') { steam_id = _strtoui64(++cursor, NULL, 10); } while (*cursor != '\0' && *cursor != ';') ++cursor; if (*cursor == ';') ++cursor; if (ip[0] != '\0' && console_port > 0 && game_port > 0) { SteamNetTransport_RegisterPeer( ip, console_port, game_port, steam_id); } } outcome = LobbyLaunchMember; break; } } } // // Refresh the member list once a second // DWORD now = GetTickCount(); if ((LONG)(now - last_refresh) >= 1000) { last_refresh = now; room.memberCount = CollectMembers(room.members); InvalidateRect(room.window, NULL, FALSE); RedrawWindow(room.window, NULL, NULL, RDW_UPDATENOW); } Sleep(50); } DestroyWindow(room.window); DeleteObject(room.textFont); DeleteObject(room.titleFont); gRoom = NULL; return outcome; } } //######################################################################## // Public API //######################################################################## Logical RPL4Lobby_Available() { return SteamNetTransport_GetFakeAddressString()[0] != '\0'; } Logical RPL4Lobby_InRoom() { return gInLobby; } int RPL4Lobby_Host(HINSTANCE instance, HWND main_window) { gCallDone = False; SteamAPICall_t call = SteamMatchmaking()->CreateLobby(k_ELobbyTypePublic, kMaxLobbyMembers); gCalls.createResult.Set(call, &gCalls, &LobbyCalls::OnCreated); if (!WaitForCall(15000)) { DEBUG_STREAM << "Lobby: CreateLobby failed\n" << std::flush; return LobbyRoomLeft; } gLobby = gCallLobby; gInLobby = True; gLastGoNonce = 0; SteamMatchmaking()->SetLobbyData(gLobby, kLobbyTagKey, "1"); SteamMatchmaking()->SetLobbyData(gLobby, kGoKey, ""); DEBUG_STREAM << "Lobby: hosting " << gLobby.ConvertToUint64() << "\n" << std::flush; return RunRoom(instance, main_window); } int RPL4Lobby_Join(HINSTANCE instance, HWND main_window) { gCallDone = False; SteamMatchmaking()->AddRequestLobbyListStringFilter( kLobbyTagKey, "1", k_ELobbyComparisonEqual); // the default lobby search is distance-filtered (roughly same // region) - RP412 races are worldwide SteamMatchmaking()->AddRequestLobbyListDistanceFilter( k_ELobbyDistanceFilterWorldwide); SteamAPICall_t call = SteamMatchmaking()->RequestLobbyList(); gCalls.listResult.Set(call, &gCalls, &LobbyCalls::OnList); if (!WaitForCall(15000)) { DEBUG_STREAM << "Lobby: no open race lobby found\n" << std::flush; return LobbyRoomLeft; } CSteamID target = gCallLobby; gCallDone = False; call = SteamMatchmaking()->JoinLobby(target); gCalls.enterResult.Set(call, &gCalls, &LobbyCalls::OnEnter); if (!WaitForCall(15000)) { DEBUG_STREAM << "Lobby: join failed\n" << std::flush; return LobbyRoomLeft; } gLobby = gCallLobby; gInLobby = True; // answer only launches newer than anything already in the lobby const char *go = SteamMatchmaking()->GetLobbyData(gLobby, kGoKey); gLastGoNonce = (go != NULL) ? atoi(go) : 0; DEBUG_STREAM << "Lobby: joined " << gLobby.ConvertToUint64() << "\n" << std::flush; return RunRoom(instance, main_window); } int RPL4Lobby_Room(HINSTANCE instance, HWND main_window) { if (!gInLobby) { return LobbyRoomLeft; } return RunRoom(instance, main_window); } void RPL4Lobby_PushRaceResults() { if (!gInLobby || !IsOwner() || RPL4LocalConsole_ResultCount() == 0) { return; } SteamAPI_RunCallbacks(); char sheet[600]; sprintf(sheet, "%d:", gLastGoNonce); for (int i = 0; i < RPL4LocalConsole_ResultCount(); ++i) { int host_ID, score; if (!RPL4LocalConsole_GetResult(i, &host_ID, &score)) { continue; } const char *name = RPL4LocalConsole_GetResultName(host_ID); char entry[80]; sprintf(entry, "%d|%d|%.30s;", host_ID, score, (name != NULL) ? name : ""); if (strlen(sheet) + strlen(entry) < sizeof(sheet)) { strcat(sheet, entry); } } SteamMatchmaking()->SetLobbyData(gLobby, kResultsKey, sheet); gShownResultsNonce = gLastGoNonce; // the owner sees its own screen DEBUG_STREAM << "Lobby: results published (" << sheet << ")\n" << std::flush; } void RPL4Lobby_PullRaceResults() { if (!gInLobby || IsOwner() || gLastGoNonce == gShownResultsNonce) { return; } // // The owner publishes right after its race teardown - ours may // finish first, so give the sheet a moment to arrive // const char *sheet = NULL; DWORD deadline = GetTickCount() + 8 * 1000; for (;;) { SteamAPI_RunCallbacks(); sheet = SteamMatchmaking()->GetLobbyData(gLobby, kResultsKey); if (sheet != NULL && sheet[0] != '\0' && atoi(sheet) == gLastGoNonce) { break; } if ((LONG)(GetTickCount() - deadline) >= 0) { return; // no sheet for this race - skip the screen } Sleep(100); } gShownResultsNonce = gLastGoNonce; RPL4LocalConsole_ClearResults(); const char *cursor = strchr(sheet, ':'); cursor = (cursor != NULL) ? cursor + 1 : sheet; while (*cursor != '\0') { int host_ID = atoi(cursor); int score = 0; char name[32] = ""; while (*cursor != '\0' && *cursor != '|' && *cursor != ';') ++cursor; if (*cursor == '|') { score = atoi(++cursor); while (*cursor != '\0' && *cursor != '|' && *cursor != ';') ++cursor; } if (*cursor == '|') { ++cursor; int n = 0; while (cursor[n] != '\0' && cursor[n] != ';' && n < (int) sizeof(name) - 1) { name[n] = cursor[n]; ++n; } name[n] = '\0'; cursor += n; } while (*cursor != '\0' && *cursor != ';') ++cursor; if (*cursor == ';') ++cursor; if (host_ID > 0) { RPL4LocalConsole_InjectResult(host_ID, score, name); } } DEBUG_STREAM << "Lobby: results pulled for race " << gLastGoNonce << "\n" << std::flush; } #endif // RP412_STEAM