//===========================================================================// // btl4lobby.cpp -- BT412 Steam lobby (see btl4lobby.hpp). // // Ported from RP412 RPL4LOBBY: the ISteamMatchmaking room, member loadout // exchange (FakeIP + ports + persona + mech/color/badge), the launch roster, // and the post-race score sheet. Compiled to stubs without BT412_STEAM. //===========================================================================// #define WIN32_LEAN_AND_MEAN #define _WIN32_WINNT 0x0500 #define WINVER 0x0500 #include #include #include "btl4lobby.hpp" #ifndef BT412_STEAM //---------------------------------------------------------------------------// // No Steam SDK: the lobby is unavailable; single-player + LAN -net unchanged. //---------------------------------------------------------------------------// int BTLobby_Available() { return 0; } int BTLobby_InRoom() { return 0; } int BTLobby_Host(void *, void *) { return LobbyRoomLeft; } int BTLobby_Join(void *, void *) { return LobbyRoomLeft; } int BTLobby_Room(void *, void *) { return LobbyRoomLeft; } void BTLobby_PushRaceResults() { } void BTLobby_PullRaceResults() { } #else #include "btl4fe.hpp" // BTFrontEnd_GetLoadout / SetHostedPilots / FEHostedPilot #include "btl4console.hpp" // result API for the score sheet #include "l4steamtransport.hpp" // SteamNetTransport_* (via the fwd shim) #pragma pack(push, 8) #include "steam/steam_api.h" #pragma pack(pop) #include #include // The marshal result API (kills/deaths per roster slot; slot 0 = local). extern int BTLocalConsole_ResultCount(); extern int BTLocalConsole_GetResult(int index, int *kills, int *deaths); extern void BTLocalConsole_ClearResults(); extern void BTLocalConsole_InjectResult(int slot, int kills, int deaths, const char *name); extern const char *BTLocalConsole_GetResultName(int slot); // (The remote-pod network-race marshal -- collating every pod's scores over the // wire -- is deferred; the owner publishes its local snapshot for now.) namespace { enum { kMaxLobbyMembers = 8 }; const char kLobbyTagKey[] = "bt412"; const char kGoKey[] = "go"; const char kResultsKey[] = "res"; CSteamID gLobby; bool gInLobby = false; int gLastGoNonce = 0; int gShownResultsNonce = 0; // async call-result plumbing bool gCallDone = false, gCallOK = false; CSteamID gCallLobby; class LobbyCalls { public: CCallResult createResult; CCallResult listResult; CCallResult enterResult; void OnCreated(LobbyCreated_t *r, bool io) { gCallOK = !io && r->m_eResult == k_EResultOK; gCallLobby = CSteamID(r->m_ulSteamIDLobby); gCallDone = true; } void OnList(LobbyMatchList_t *r, bool io) { gCallOK = !io && r->m_nLobbiesMatching > 0; if (gCallOK) gCallLobby = SteamMatchmaking()->GetLobbyByIndex(0); gCallDone = true; } void OnEnter(LobbyEnter_t *r, bool io) { gCallOK = !io && r->m_EChatRoomEnterResponse == k_EChatRoomEnterResponseSuccess; gCallLobby = CSteamID(r->m_ulSteamIDLobby); gCallDone = true; } }; LobbyCalls gCalls; bool WaitForCall(int timeout_ms) { DWORD deadline = GetTickCount() + timeout_ms; while (!gCallDone) { SteamAPI_RunCallbacks(); if ((LONG)(GetTickCount() - deadline) >= 0) return false; Sleep(50); } return gCallOK; } void SanitizeName(const char *in, char *out, int out_size) { int n = 0; for (int i = 0; in[i] && 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]; BTFrontEnd_GetLoadout(vehicle, color, badge); SteamMatchmaking()->SetLobbyMemberData(gLobby, "vh", vehicle); SteamMatchmaking()->SetLobbyMemberData(gLobby, "cl", color); SteamMatchmaking()->SetLobbyMemberData(gLobby, "bd", badge); } bool IsOwner() { return gInLobby && SteamMatchmaking()->GetLobbyOwner(gLobby) == SteamUser()->GetSteamID(); } struct MemberInfo { CSteamID id; char ip[32]; int consolePort, gamePort; char name[32], vehicle[24], color[16], badge[24]; bool published; }; int CollectMembers(MemberInfo *members) { int count = SteamMatchmaking()->GetNumLobbyMembers(gLobby); if (count > kMaxLobbyMembers) count = kMaxLobbyMembers; for (int i = 0; i < count; ++i) { MemberInfo *m = &members[i]; memset(m, 0, sizeof(*m)); m->id = SteamMatchmaking()->GetLobbyMemberByIndex(gLobby, i); strncpy(m->ip, SteamMatchmaking()->GetLobbyMemberData(gLobby, m->id, "ip"), sizeof(m->ip)-1); m->consolePort = atoi(SteamMatchmaking()->GetLobbyMemberData(gLobby, m->id, "cp")); m->gamePort = atoi(SteamMatchmaking()->GetLobbyMemberData(gLobby, m->id, "gp")); strncpy(m->name, SteamMatchmaking()->GetLobbyMemberData(gLobby, m->id, "nm"), sizeof(m->name)-1); strncpy(m->vehicle, SteamMatchmaking()->GetLobbyMemberData(gLobby, m->id, "vh"), sizeof(m->vehicle)-1); strncpy(m->color, SteamMatchmaking()->GetLobbyMemberData(gLobby, m->id, "cl"), sizeof(m->color)-1); strncpy(m->badge, SteamMatchmaking()->GetLobbyMemberData(gLobby, m->id, "bd"), sizeof(m->badge)-1); m->published = m->ip[0] && m->consolePort > 0 && m->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 the hosted-race env (BT412HOSTPODS/ADDR/PORT) + the // hosted-pilot loadouts the front end feeds into the egg. void PrimeHostedRace(MemberInfo *members, int count) { static char pods_env[512], addr_env[64], 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 *p = &pilots[pilot_count++]; memset(p, 0, sizeof(*p)); sprintf(p->address, "%s:1502", members[i].ip); strncpy(p->name, members[i].name, sizeof(p->name)-1); strncpy(p->vehicle, members[i].vehicle, sizeof(p->vehicle)-1); strncpy(p->color, members[i].color, sizeof(p->color)-1); strncpy(p->badge, members[i].badge, sizeof(p->badge)-1); if (pods[0]) strcat(pods, ","); strcat(pods, members[i].ip); strcat(pods, ":1501"); } BTFrontEnd_SetHostedPilots(SteamNetTransport_GetFakeAddressString(), pilots, pilot_count); sprintf(pods_env, "BT412HOSTPODS=%s", pods); putenv(pods_env); sprintf(addr_env, "BT412HOSTADDR=%s", SteamNetTransport_GetFakeAddressString()); putenv(addr_env); strcpy(port_env, "BT412HOSTPORT=1501"); putenv(port_env); } //-----------------------------------------------------------------------// // The room screen (green-on-black, like the menu). //-----------------------------------------------------------------------// const COLORREF kGreenBright = RGB(64,255,64), kGreenDim = RGB(24,140,24); struct RoomState { HWND window; HFONT textFont, titleFont; RECT launchRect, leaveRect; bool launchClicked, leaveClicked, closed; MemberInfo members[kMaxLobbyMembers]; int memberCount; }; RoomState *gRoom = NULL; void PaintRoom(RoomState *room) { PAINTSTRUCT ps; HDC hdc = BeginPaint(room->window, &ps); RECT cl; GetClientRect(room->window, &cl); HDC mem = CreateCompatibleDC(hdc); HBITMAP surf = CreateCompatibleBitmap(hdc, cl.right, cl.bottom); HBITMAP old = (HBITMAP) SelectObject(mem, surf); FillRect(mem, &cl, (HBRUSH) GetStockObject(BLACK_BRUSH)); SetBkMode(mem, TRANSPARENT); SelectObject(mem, room->titleFont); SetTextColor(mem, kGreenBright); RECT title = cl; title.top = cl.bottom/28; title.bottom = title.top + cl.bottom/10; DrawTextA(mem, "STEAM RACE LOBBY", -1, &title, DT_CENTER|DT_TOP|DT_SINGLELINE); SelectObject(mem, room->textFont); int row_h = cl.bottom/16, top = cl.bottom/4, left = cl.right/4, right = 3*cl.right/4; char text[128]; for (int i = 0; i < room->memberCount; ++i) { MemberInfo *m = &room->members[i]; RECT row; row.left=left; row.right=right; row.top=top+i*row_h; row.bottom=row.top+row_h; bool owner_row = SteamMatchmaking()->GetLobbyOwner(gLobby) == m->id; SetTextColor(mem, m->published ? kGreenBright : kGreenDim); sprintf(text, "%s%s", m->name[0]?m->name:"(joining...)", owner_row?" [HOST]":""); DrawTextA(mem, text, -1, &row, DT_LEFT|DT_VCENTER|DT_SINGLELINE); if (m->vehicle[0]) { sprintf(text, "%s / %s", m->vehicle, m->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, cl.right, cl.bottom, mem, 0,0, SRCCOPY); SelectObject(mem, old); DeleteObject(surf); DeleteDC(mem); EndPaint(room->window, &ps); } LRESULT CALLBACK RoomWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { RoomState *room = gRoom; switch (msg) { case WM_PAINT: if (room) { PaintRoom(room); return 0; } break; case WM_LBUTTONDOWN: if (room) { POINT pt = {(int)(short)LOWORD(lp),(int)(short)HIWORD(lp)}; if (IsOwner() && PtInRect(&room->launchRect, pt)) room->launchClicked = true; else if (PtInRect(&room->leaveRect, pt)) room->leaveClicked = true; return 0; } break; case WM_ERASEBKGND: return 1; } return DefWindowProcA(hwnd, msg, wp, lp); } int RunRoom(HINSTANCE instance, HWND main_window) { static bool reg = false; if (!reg) { reg = true; WNDCLASSA wc; memset(&wc,0,sizeof(wc)); wc.style=CS_HREDRAW|CS_VREDRAW; wc.lpfnWndProc=RoomWndProc; wc.hInstance=instance; wc.hCursor=LoadCursor(NULL,IDC_ARROW); wc.hbrBackground=(HBRUSH)GetStockObject(BLACK_BRUSH); wc.lpszClassName="BTLobbyRoom"; RegisterClassA(&wc); } int W = 1000, H = 720; int sx = (GetSystemMetrics(SM_CXSCREEN)-W)/2, sy = (GetSystemMetrics(SM_CYSCREEN)-H)/2; RoomState room; memset(&room, 0, sizeof(room)); room.window = CreateWindowExA(0, "BTLobbyRoom", "BattleTech", WS_OVERLAPPEDWINDOW|WS_VISIBLE, sx<0?0:sx, sy<0?0:sy, W, H, NULL, NULL, instance, NULL); if (room.window == NULL) return LobbyRoomClosed; RECT cl; GetClientRect(room.window, &cl); int th = cl.bottom/40; if (th<16) th=16; if (th>24) th=24; room.textFont = CreateFontA(-(th*3/2),0,0,0,FW_NORMAL,0,0,0,ANSI_CHARSET,OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,ANTIALIASED_QUALITY,DEFAULT_PITCH|FF_MODERN,"Consolas"); room.titleFont = CreateFontA(-(th*2),0,0,0,FW_BOLD,0,0,0,ANSI_CHARSET,OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,ANTIALIASED_QUALITY,DEFAULT_PITCH|FF_MODERN,"Consolas"); int row_h = cl.bottom/36; if (row_h<18) row_h=18; if (row_h>30) row_h=30; int col_w = cl.right/4; room.launchRect.left=(cl.right-col_w)/2; room.launchRect.right=room.launchRect.left+col_w; room.launchRect.top=cl.bottom-6*row_h; room.launchRect.bottom=room.launchRect.top+2*row_h; room.leaveRect.left=(cl.right-col_w/2)/2; room.leaveRect.right=room.leaveRect.left+col_w/2; room.leaveRect.top=cl.bottom-3*row_h; room.leaveRect.bottom=cl.bottom-row_h; gRoom = &room; PublishMemberData(); SetForegroundWindow(room.window); 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 (room.closed) { outcome = LobbyRoomClosed; break; } if (room.leaveClicked) { SteamMatchmaking()->LeaveLobby(gLobby); gInLobby = false; BTFrontEnd_SetHostedPilots(NULL, NULL, 0); static char clr[] = "BT412HOSTPODS="; putenv(clr); outcome = LobbyRoomLeft; break; } if (room.launchClicked) { room.launchClicked = false; room.memberCount = CollectMembers(room.members); bool all_pub = true; for (int i = 0; i < room.memberCount; ++i) if (!room.members[i].published) all_pub = false; if (all_pub && room.memberCount >= 1) { ++gLastGoNonce; char go[900]; sprintf(go, "%d:", gLastGoNonce); for (int i = 0; i < room.memberCount; ++i) { char e[96]; sprintf(e, "%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(e) < sizeof(go)) strcat(go, e); } SteamMatchmaking()->SetLobbyData(gLobby, kGoKey, go); RegisterRoster(room.members, room.memberCount); PrimeHostedRace(room.members, room.memberCount); outcome = LobbyLaunchHost; break; } } if (!IsOwner()) { const char *go = SteamMatchmaking()->GetLobbyData(gLobby, kGoKey); if (go && go[0]) { int nonce = atoi(go); if (nonce > gLastGoNonce) { gLastGoNonce = nonce; SteamNetTransport_SetEnginePorts(1501); const char *c = strchr(go, ':'); c = c ? c+1 : go; while (*c) { char ip[32]; int cpp=0, gpp=0; unsigned __int64 sid=0; int n=0; while (*c && *c!='|' && n<(int)sizeof(ip)-1) ip[n++]=*c++; ip[n]='\0'; if (*c=='|') { cpp=atoi(++c); while (*c && *c!='|') ++c; } if (*c=='|') { gpp=atoi(++c); while (*c && *c!='|' && *c!=';') ++c; } if (*c=='|') sid=_strtoui64(++c, NULL, 10); while (*c && *c!=';') ++c; if (*c==';') ++c; if (ip[0] && cpp>0 && gpp>0) SteamNetTransport_RegisterPeer(ip, cpp, gpp, sid); } outcome = LobbyLaunchMember; break; } } } 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; } } // namespace //===========================================================================// // Public API //===========================================================================// int BTLobby_Available() { return SteamNetTransport_GetFakeAddressString()[0] != '\0'; } int BTLobby_InRoom() { return gInLobby ? 1 : 0; } int BTLobby_Host(void *instance, void *main_window) { gCallDone = false; SteamAPICall_t call = SteamMatchmaking()->CreateLobby(k_ELobbyTypePublic, kMaxLobbyMembers); gCalls.createResult.Set(call, &gCalls, &LobbyCalls::OnCreated); if (!WaitForCall(15000)) return LobbyRoomLeft; gLobby = gCallLobby; gInLobby = true; gLastGoNonce = 0; SteamMatchmaking()->SetLobbyData(gLobby, kLobbyTagKey, "1"); SteamMatchmaking()->SetLobbyData(gLobby, kGoKey, ""); return RunRoom((HINSTANCE)instance, (HWND)main_window); } int BTLobby_Join(void *instance, void *main_window) { gCallDone = false; SteamMatchmaking()->AddRequestLobbyListStringFilter(kLobbyTagKey, "1", k_ELobbyComparisonEqual); SteamMatchmaking()->AddRequestLobbyListDistanceFilter(k_ELobbyDistanceFilterWorldwide); SteamAPICall_t call = SteamMatchmaking()->RequestLobbyList(); gCalls.listResult.Set(call, &gCalls, &LobbyCalls::OnList); if (!WaitForCall(15000)) return LobbyRoomLeft; CSteamID target = gCallLobby; gCallDone = false; call = SteamMatchmaking()->JoinLobby(target); gCalls.enterResult.Set(call, &gCalls, &LobbyCalls::OnEnter); if (!WaitForCall(15000)) return LobbyRoomLeft; gLobby = gCallLobby; gInLobby = true; const char *go = SteamMatchmaking()->GetLobbyData(gLobby, kGoKey); gLastGoNonce = go ? atoi(go) : 0; return RunRoom((HINSTANCE)instance, (HWND)main_window); } int BTLobby_Room(void *instance, void *main_window) { if (!gInLobby) return LobbyRoomLeft; return RunRoom((HINSTANCE)instance, (HWND)main_window); } void BTLobby_PushRaceResults() { if (!gInLobby || !IsOwner() || BTLocalConsole_ResultCount() == 0) return; SteamAPI_RunCallbacks(); char sheet[700]; sprintf(sheet, "%d:", gLastGoNonce); for (int i = 0; i < BTLocalConsole_ResultCount(); ++i) { int kills = 0, deaths = 0; BTLocalConsole_GetResult(i, &kills, &deaths); const char *name = BTLocalConsole_GetResultName(i); char e[96]; sprintf(e, "%d|%d|%d|%.30s;", i, kills, deaths, name ? name : ""); if (strlen(sheet)+strlen(e) < sizeof(sheet)) strcat(sheet, e); } SteamMatchmaking()->SetLobbyData(gLobby, kResultsKey, sheet); gShownResultsNonce = gLastGoNonce; } void BTLobby_PullRaceResults() { if (!gInLobby || IsOwner() || gLastGoNonce == gShownResultsNonce) return; const char *sheet = NULL; DWORD deadline = GetTickCount() + 8000; for (;;) { SteamAPI_RunCallbacks(); sheet = SteamMatchmaking()->GetLobbyData(gLobby, kResultsKey); if (sheet && sheet[0] && atoi(sheet) == gLastGoNonce) break; if ((LONG)(GetTickCount() - deadline) >= 0) return; Sleep(100); } gShownResultsNonce = gLastGoNonce; BTLocalConsole_ClearResults(); const char *c = strchr(sheet, ':'); c = c ? c+1 : sheet; while (*c) { int slot = atoi(c), kills = 0, deaths = 0; char name[32] = ""; while (*c && *c!='|' && *c!=';') ++c; if (*c=='|') { kills=atoi(++c); while (*c && *c!='|' && *c!=';') ++c; } if (*c=='|') { deaths=atoi(++c); while (*c && *c!='|' && *c!=';') ++c; } if (*c=='|') { ++c; int n=0; while (*c && *c!=';' && n<(int)sizeof(name)-1) name[n++]=*c++; name[n]='\0'; } while (*c && *c!=';') ++c; if (*c==';') ++c; BTLocalConsole_InjectResult(slot, kills, deaths, name); } } #endif // BT412_STEAM