Files
RP412/RP_L4/RPL4LOBBY.cpp
T
CydandClaude Fable 5 4b25f89e6f Football: every player picks their own team and position
Closes the gap left by the football commit. Members publish their
picks as lobby member data (tm/ps) alongside their loadout, and the
owner publishes the scenario (sc) so members know when the picks
matter. The lobby room turns into a team sheet: each member row shows
their team and position, and MY TEAM / MY POSITION buttons cycle the
local pick and republish it live, so everyone watches the sides fill
out before the host launches.

The egg builder honors explicit picks and only fills gaps:
- pilots who chose a team get it; the rest are spread across the
  host team and one other so the sides stay balanced
- a team with no volunteer runner promotes its first UNPICKED member,
  never overriding someone who chose crusher or blocker
- if two pilots on a team both claim runner, the first keeps it and
  the second lines up
- colors stay derived: runner in the team runner color, everyone else
  in the team color

Verified: a three-pilot egg puts the host on crusher in team color
with an unpicked teammate promoted to runner in runner color, the
other side gets its own runner, and the teams/pilots blocks group
correctly; the lobby room cycles picks and repaints the roster; both
scenarios still pass their single-player regressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 19:59:42 -05:00

919 lines
26 KiB
C++

#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 <stdio.h>
//########################################################################
// 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";
const char kScenarioKey[] = "sc";
// async call-result plumbing
Logical gCallDone = False;
Logical gCallOK = False;
CSteamID gCallLobby;
class LobbyCalls
{
public:
CCallResult<LobbyCalls, LobbyCreated_t> createResult;
CCallResult<LobbyCalls, LobbyMatchList_t> listResult;
CCallResult<LobbyCalls, LobbyEnter_t> 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");
}
}
Logical IsOwner()
{
return gInLobby &&
SteamMatchmaking()->GetLobbyOwner(gLobby) == SteamUser()->GetSteamID();
}
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);
// football: this member's own team and position pick
SteamMatchmaking()->SetLobbyMemberData(gLobby, "tm",
RPL4FrontEnd_TeamKey(RPL4FrontEnd_GetTeamIndex()));
SteamMatchmaking()->SetLobbyMemberData(gLobby, "ps",
RPL4FrontEnd_PositionKey(RPL4FrontEnd_GetPositionIndex()));
// the owner also publishes the scenario, so members know
// whether their team/position pick matters
if (IsOwner())
{
SteamMatchmaking()->SetLobbyData(gLobby, kScenarioKey,
RPL4FrontEnd_IsFootballSelected() ? "football" : "race");
}
}
Logical FootballLobby()
{
if (!gInLobby)
{
return False;
}
const char *scenario = SteamMatchmaking()->GetLobbyData(gLobby, kScenarioKey);
return (scenario != NULL && strcmp(scenario, "football") == 0);
}
//---------------------------------------------------------------
// 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];
char team[32]; // football pick
char position[16];
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);
strncpy(member->team,
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "tm"),
sizeof(member->team) - 1);
strncpy(member->position,
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "ps"),
sizeof(member->position) - 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);
strncpy(pilot->team, members[i].team, sizeof(pilot->team) - 1);
strncpy(pilot->position, members[i].position, sizeof(pilot->position) - 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;
RECT teamRect; // football: cycle my team
RECT positionRect; // football: cycle my position
Logical launchClicked;
Logical leaveClicked;
Logical pickChanged;
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,
FootballLobby() ? "STEAM FOOTBALL LOBBY" : "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 (FootballLobby())
{
// football: the roster is a team sheet
if (member->team[0] != '\0')
{
const char *position_name = "?";
for (int i = 0; i < RPL4FrontEnd_PositionCount(); ++i)
{
if (strcmp(member->position, RPL4FrontEnd_PositionKey(i)) == 0)
{
position_name = RPL4FrontEnd_PositionName(i);
break;
}
}
sprintf(text, "%s - %s", member->team, position_name);
DrawTextA(mem, text, -1, &row, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
}
}
else 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);
//
// Football: my own team and position, click to cycle
//
if (FootballLobby())
{
FrameRect(mem, &room->teamRect, frame);
SetTextColor(mem, kGreenBright);
sprintf(text, "MY TEAM: %s",
RPL4FrontEnd_TeamName(RPL4FrontEnd_GetTeamIndex()));
DrawTextA(mem, text, -1, &room->teamRect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
FrameRect(mem, &room->positionRect, frame);
sprintf(text, "MY POSITION: %s",
RPL4FrontEnd_PositionName(RPL4FrontEnd_GetPositionIndex()));
DrawTextA(mem, text, -1, &room->positionRect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
if (IsOwner())
{
FrameRect(mem, &room->launchRect, frame);
SetTextColor(mem, kGreenBright);
DrawTextA(mem, "L A U N C H G A M 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;
}
else if (FootballLobby() && PtInRect(&room->teamRect, point))
{
RPL4FrontEnd_SetTeamIndex(
(RPL4FrontEnd_GetTeamIndex() + 1) % RPL4FrontEnd_TeamCount());
room->pickChanged = True;
}
else if (FootballLobby() && PtInRect(&room->positionRect, point))
{
RPL4FrontEnd_SetPositionIndex(
(RPL4FrontEnd_GetPositionIndex() + 1) % RPL4FrontEnd_PositionCount());
room->pickChanged = 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;
// football pick buttons, above the launch button
room.teamRect.left = (client.right - col_w) / 2;
room.teamRect.right = room.teamRect.left + col_w;
room.teamRect.top = client.bottom - 12 * row_h;
room.teamRect.bottom = room.teamRect.top + 2 * row_h;
room.positionRect.left = room.teamRect.left;
room.positionRect.right = room.teamRect.right;
room.positionRect.top = client.bottom - 9 * row_h;
room.positionRect.bottom = room.positionRect.top + 2 * 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;
}
//
// A pick changed: republish so everyone's roster updates
//
if (room.pickChanged)
{
room.pickChanged = False;
PublishMemberData();
room.memberCount = CollectMembers(room.members);
InvalidateRect(room.window, NULL, FALSE);
RedrawWindow(room.window, NULL, NULL, RDW_UPDATENOW);
}
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