Picking Live Cam and then hosting still showed the host in the room with a VTV and a colour, because the room row draws whatever loadout the member published and nothing on the wire said the host had given up its grid slot. The lobby now publishes a cam key with the rest of the member data, carries it in MemberInfo, and the owner's row reads LIVE CAM where a loadout would go. The loadout itself still goes out unchanged. Live Cam is a role, not a vehicle, so vh/cl/bd keep carrying what was picked and switching back to Racer finds it all still there. Only the owner's row shows it. The host is the one that writes the egg, so the host's pick is the only one acted on; a member who set Live Cam is still going to race, and its row goes on saying so. Painting every cam=1 row as LIVE CAM would have the room screen lying about the grid. Letting members spectate too is a real feature - the same hostType=1 on their egg entry, plus a guard that one racer is left - but it is not this change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1203 lines
36 KiB
C++
1203 lines
36 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_Configured() { 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
|
|
{
|
|
// A full grid is eight pods, as the pod hall ran it. The egg builder
|
|
// carries the owner plus maxExtraPilots (8), so eight members fit
|
|
// with room to spare.
|
|
enum { kMaxLobbyMembers = 8 };
|
|
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";
|
|
|
|
//-------------------------------------------------------------------
|
|
// Simulation protocol revision. Bump this whenever a change makes
|
|
// two builds simulate the same mission differently - it is not the
|
|
// wire format alone. Map entity ownership is dealt by advancing a
|
|
// shared cursor once per map entity, so anything that changes which
|
|
// entities are dealt at all silently desynchronizes who owns what.
|
|
//
|
|
// 2 - doorframes became local Hermit clockwork and are no longer
|
|
// dealt, which shifts every subsequent map entity's owner
|
|
// 1 - the 3-machine verified Steam build
|
|
//-------------------------------------------------------------------
|
|
const char kNetRevision[] = "2";
|
|
const char kNetRevKey[] = "nr";
|
|
|
|
// this member picked Live Cam rather than a grid slot
|
|
const char kCamKey[] = "cam";
|
|
|
|
// the owner's mission setup, shown to everyone in the room
|
|
const char kMapKey[] = "mp";
|
|
const char kTimeKey[] = "td";
|
|
const char kWeatherKey[] = "wx";
|
|
const char kLengthKey[] = "ln";
|
|
|
|
// 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);
|
|
|
|
//
|
|
// Live Cam. The loadout above still goes out unchanged - the pick
|
|
// is a role, not a vehicle, and it is only acted on for the host,
|
|
// so a member's own row must go on showing what it will really
|
|
// fly. The room screen reads this against the owner's row.
|
|
//
|
|
SteamMatchmaking()->SetLobbyMemberData(gLobby, kCamKey,
|
|
RPL4FrontEnd_IsCameraRole() ? "1" : "0");
|
|
|
|
// 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()));
|
|
|
|
// what this build simulates like, so a mismatched room cannot launch
|
|
SteamMatchmaking()->SetLobbyMemberData(gLobby, kNetRevKey, kNetRevision);
|
|
|
|
//---------------------------------------------------------------
|
|
// Only the owner's menu decides the mission, so the owner also
|
|
// publishes what it picked: the scenario (members need it to know
|
|
// whether their team/position pick matters) and the setup
|
|
// everyone is about to fly into.
|
|
//---------------------------------------------------------------
|
|
if (IsOwner())
|
|
{
|
|
// members check this before they act on the owner's go
|
|
SteamMatchmaking()->SetLobbyData(gLobby, kNetRevKey, kNetRevision);
|
|
SteamMatchmaking()->SetLobbyData(gLobby, kScenarioKey,
|
|
RPL4FrontEnd_IsFootballSelected() ? "football" : "race");
|
|
SteamMatchmaking()->SetLobbyData(gLobby, kMapKey,
|
|
RPL4FrontEnd_SelectedMapName());
|
|
SteamMatchmaking()->SetLobbyData(gLobby, kTimeKey,
|
|
RPL4FrontEnd_SelectedTimeName());
|
|
SteamMatchmaking()->SetLobbyData(gLobby, kWeatherKey,
|
|
RPL4FrontEnd_SelectedWeatherName());
|
|
SteamMatchmaking()->SetLobbyData(gLobby, kLengthKey,
|
|
RPL4FrontEnd_SelectedLengthName());
|
|
}
|
|
}
|
|
|
|
Logical FootballLobby()
|
|
{
|
|
if (!gInLobby)
|
|
{
|
|
return False;
|
|
}
|
|
const char *scenario = SteamMatchmaking()->GetLobbyData(gLobby, kScenarioKey);
|
|
return (scenario != NULL && strcmp(scenario, "football") == 0);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// Lobby data read back as a string, empty when the owner has not
|
|
// published it yet (a member can be in the room a beat before the
|
|
// owner's first publish lands).
|
|
//-------------------------------------------------------------------
|
|
const char *LobbyText(const char *key)
|
|
{
|
|
if (!gInLobby)
|
|
{
|
|
return "";
|
|
}
|
|
const char *text = SteamMatchmaking()->GetLobbyData(gLobby, key);
|
|
return (text != NULL) ? text : "";
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// 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];
|
|
char netRev[8]; // simulation protocol revision
|
|
Logical camera; // picked Live Cam (acted on for the host)
|
|
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);
|
|
strncpy(member->netRev,
|
|
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, kNetRevKey),
|
|
sizeof(member->netRev) - 1);
|
|
member->camera = (atoi(
|
|
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, kCamKey)) != 0)
|
|
? True : False;
|
|
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);
|
|
// wider than the old middle-half: the right column now carries
|
|
// full catalog names, and in football three of them
|
|
int left = client.right / 6;
|
|
int right = client.right - client.right / 6;
|
|
char text[128];
|
|
|
|
//---------------------------------------------------------------
|
|
// The buttons are laid out upward from the bottom edge while the
|
|
// roster runs down from the top, so everything between the title
|
|
// and the topmost button has to share one band.
|
|
//---------------------------------------------------------------
|
|
int ceiling = client.bottom;
|
|
if (FootballLobby() && room->teamRect.top > 0)
|
|
{
|
|
ceiling = room->teamRect.top;
|
|
}
|
|
else if (IsOwner() && room->launchRect.top > 0)
|
|
{
|
|
ceiling = room->launchRect.top;
|
|
}
|
|
else if (room->leaveRect.top > 0)
|
|
{
|
|
ceiling = room->leaveRect.top;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Size the rows to that band rather than to a fixed fraction of
|
|
// the window: a full lobby is eight players, and eight rows at a
|
|
// sixteenth each would run off the bottom of every window we
|
|
// support. Counted in half rows, the band holds
|
|
//
|
|
// setup lines + gap + kMaxLobbyMembers rows + gap + hint
|
|
//
|
|
// so the row height falls out of the space actually available.
|
|
// It never grows past the old sixteenth (a two-player lobby
|
|
// should not have circus-sized rows) and never shrinks below the
|
|
// font, which is what would actually break - overlapping text.
|
|
//---------------------------------------------------------------
|
|
// tmHeight already includes the font's own leading, so a row that
|
|
// tall cannot clip or collide - asking for more than that just
|
|
// costs setup lines the room would rather keep.
|
|
TEXTMETRICA metrics;
|
|
GetTextMetricsA(mem, &metrics);
|
|
int min_row = metrics.tmHeight + 2;
|
|
int max_row = client.bottom / 16;
|
|
|
|
int setup_top = title.bottom + client.bottom / 64;
|
|
int band = ceiling - setup_top;
|
|
|
|
int setup_lines = 2;
|
|
int row_h = 0;
|
|
for (;;)
|
|
{
|
|
// halves: setup, half gap, roster, half gap, hint
|
|
int halves = 2 * setup_lines + 1 + 2 * kMaxLobbyMembers + 1 + 2;
|
|
row_h = (2 * band) / halves;
|
|
if (row_h >= min_row || setup_lines == 0)
|
|
{
|
|
break;
|
|
}
|
|
--setup_lines; // buy room back from the setup lines
|
|
}
|
|
if (row_h > max_row) row_h = max_row;
|
|
if (row_h < 12) row_h = 12;
|
|
|
|
//---------------------------------------------------------------
|
|
// On a window too short to give eight rows the room the standard
|
|
// font wants, draw this block smaller rather than letting the
|
|
// rows overlap each other or run under the buttons. Nobody plays
|
|
// at that size, but a squeezed roster still has to be readable.
|
|
//---------------------------------------------------------------
|
|
HFONT squeezed = NULL;
|
|
HFONT previous = NULL;
|
|
if (row_h < min_row)
|
|
{
|
|
squeezed = CreateFontA(-(row_h * 2 / 3), 0, 0, 0, FW_NORMAL,
|
|
FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_TT_PRECIS,
|
|
CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
|
|
FIXED_PITCH | FF_MODERN, "Consolas");
|
|
if (squeezed != NULL)
|
|
{
|
|
previous = (HFONT) SelectObject(mem, squeezed);
|
|
}
|
|
}
|
|
|
|
int top = setup_top;
|
|
|
|
const char *map_name = LobbyText(kMapKey);
|
|
if (map_name[0] != '\0' && setup_lines > 0)
|
|
{
|
|
RECT setup;
|
|
setup.left = client.right / 12;
|
|
setup.right = client.right - client.right / 12;
|
|
setup.top = setup_top;
|
|
setup.bottom = setup.top + row_h;
|
|
|
|
SetTextColor(mem, kGreenBright);
|
|
DrawTextA(mem, map_name, -1, &setup,
|
|
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
|
|
|
//-----------------------------------------------------------
|
|
// Conditions on the line below. Game length is the host's
|
|
// pick too, and the one people ask about first.
|
|
//-----------------------------------------------------------
|
|
const char *time_name = LobbyText(kTimeKey);
|
|
const char *weather_name = LobbyText(kWeatherKey);
|
|
const char *length_name = LobbyText(kLengthKey);
|
|
text[0] = '\0';
|
|
if (time_name[0] != '\0')
|
|
{
|
|
strcat(text, time_name);
|
|
}
|
|
if (weather_name[0] != '\0')
|
|
{
|
|
if (text[0] != '\0') strcat(text, " - ");
|
|
strcat(text, weather_name);
|
|
}
|
|
if (length_name[0] != '\0')
|
|
{
|
|
if (text[0] != '\0') strcat(text, " - ");
|
|
strcat(text, length_name);
|
|
}
|
|
if (text[0] != '\0' && setup_lines > 1)
|
|
{
|
|
setup.top = setup.bottom;
|
|
setup.bottom = setup.top + row_h;
|
|
SetTextColor(mem, kGreenDim);
|
|
DrawTextA(mem, text, -1, &setup,
|
|
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
|
}
|
|
|
|
if (setup.bottom + row_h / 2 > top)
|
|
{
|
|
top = setup.bottom + row_h / 2;
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
//-----------------------------------------------------------
|
|
// The right column is what this player brings. Football is a
|
|
// team sheet - team colors and position - but the VTV still
|
|
// decides how they play it, so name it there too. Keys
|
|
// travel on the wire; the catalogs turn them back into
|
|
// names, so nobody reads "bttlbrg".
|
|
//-----------------------------------------------------------
|
|
//
|
|
// A Live Cam host brings no vehicle, so its row says the role
|
|
// instead of a loadout. Only the owner's row: the host writes
|
|
// the egg, so only the host's pick is acted on, and a member
|
|
// who set Live Cam is still going to race - saying otherwise
|
|
// here would be the room screen lying about the grid.
|
|
//
|
|
if (member->camera && is_owner_row)
|
|
{
|
|
DrawTextA(mem, "LIVE CAM", -1, &row,
|
|
DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
|
|
}
|
|
else if (FootballLobby())
|
|
{
|
|
if (member->team[0] != '\0')
|
|
{
|
|
sprintf(text, "%s - %s - %s",
|
|
RPL4FrontEnd_VehicleNameForKey(member->vehicle),
|
|
RPL4FrontEnd_TeamNameForKey(member->team),
|
|
RPL4FrontEnd_PositionNameForKey(member->position));
|
|
DrawTextA(mem, text, -1, &row, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
|
|
}
|
|
}
|
|
else if (member->vehicle[0] != '\0')
|
|
{
|
|
sprintf(text, "%s - %s",
|
|
RPL4FrontEnd_VehicleNameForKey(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);
|
|
|
|
// back to the room font for the buttons, which have their own room
|
|
if (squeezed != NULL)
|
|
{
|
|
SelectObject(mem, previous);
|
|
DeleteObject(squeezed);
|
|
}
|
|
|
|
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;
|
|
Logical all_same_build = True;
|
|
for (int i = 0; i < room.memberCount; ++i)
|
|
{
|
|
if (!room.members[i].published)
|
|
{
|
|
all_published = False;
|
|
}
|
|
if (strcmp(room.members[i].netRev, kNetRevision) != 0)
|
|
{
|
|
all_same_build = False;
|
|
DEBUG_STREAM << "Lobby: " << room.members[i].name
|
|
<< " simulates like rev '" << room.members[i].netRev
|
|
<< "', we are rev '" << kNetRevision << "'\n" << std::flush;
|
|
}
|
|
}
|
|
if (all_published && all_same_build && 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())
|
|
{
|
|
//
|
|
// A room whose owner simulates differently than we do would
|
|
// desynchronize silently rather than fail, so sit the race out
|
|
// instead of flying into it.
|
|
//
|
|
const char *owner_rev = LobbyText(kNetRevKey);
|
|
if (owner_rev[0] != '\0' &&
|
|
strcmp(owner_rev, kNetRevision) != 0)
|
|
{
|
|
DEBUG_STREAM << "Lobby: owner simulates like rev '"
|
|
<< owner_rev << "', we are rev '" << kNetRevision
|
|
<< "' - not launching\n" << std::flush;
|
|
outcome = LobbyRoomLeft;
|
|
SteamMatchmaking()->LeaveLobby(gLobby);
|
|
gInLobby = False;
|
|
break;
|
|
}
|
|
|
|
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';
|
|
}
|
|
|
|
//
|
|
// The environ.ini switch, read the same way RPL4.CPP reads it to decide
|
|
// whether to install the transport at all. Says nothing about whether the
|
|
// Steam client was actually there.
|
|
//
|
|
Logical
|
|
RPL4Lobby_Configured()
|
|
{
|
|
const char *steam_switch = getenv("RP412STEAM");
|
|
return (steam_switch != NULL && atoi(steam_switch) != 0) ? True : False;
|
|
}
|
|
|
|
Logical
|
|
RPL4Lobby_InRoom()
|
|
{
|
|
return gInLobby;
|
|
}
|
|
|
|
//
|
|
// Host and Join are the two places that reach Steam without the
|
|
// transport having got there first, so they carry their own guard.
|
|
// steam_api.dll is delay-loaded and calling into it when it is absent
|
|
// raises the helper's fatal exception - the menu already greys these on
|
|
// Available(), but a dead DLL must not depend on the UI for safety.
|
|
//
|
|
int
|
|
RPL4Lobby_Host(HINSTANCE instance, HWND main_window)
|
|
{
|
|
if (!SteamNetTransport_ClientLibraryPresent())
|
|
{
|
|
return LobbyRoomLeft;
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (!SteamNetTransport_ClientLibraryPresent())
|
|
{
|
|
return LobbyRoomLeft;
|
|
}
|
|
|
|
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
|