A door's position was an integrated countdown owned by whichever machine the map-entity round-robin happened to deal it to. That left doors one one-way-latency behind on every other machine, re-acquired at each state change; drifting permanently on any frame hitch over a second, which the old code dropped outright rather than clamping; and frozen mid-cycle, collision volumes included, when their owning peer left, since ownership transfer is not implemented. Doors are clockwork with no inputs, and door/VTV physics is already local pointer access - VTV::ProcessCollision reads door->currentVelocity off the local object and the crush test is local VTV state - so a door does not need an owner at all. Door::SlideDoor is now a phase function of Application::GetMissionElapsed(), anchored so phase zero reproduces the original DefaultState entry: fully open, starting to close. Every host builds its own doorframe out of the map stream as a HermitInstance, the instance kind DynamicEntityCreation does not broadcast, so nothing is sent, nothing is received, and a peer leaving takes no doors with it. Verified against a copy of the old integrator at 25fps with the real 10s travel / 3s dead timings: identical 26s cycle, a constant one-frame offset, and no drift across a 3s stall that leaves the old code permanently 3 seconds out of phase. Also fixes a latent bug found on the way: UpdateManager iterates the dynamic master socket, which holds Independant and Hermit instances as well as masters, and handed all of them to EntityUpdateReplicants, which asserts MasterInstance. Doorframes no longer consume a slot in the map-entity ownership cursor, which shifts who owns every map entity dealt after them, so this cannot share a session with an older build. The lobby publishes a simulation revision and refuses to launch a mixed room. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1158 lines
34 KiB
C++
1158 lines
34 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";
|
|
|
|
// 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);
|
|
|
|
// 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 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->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".
|
|
//-----------------------------------------------------------
|
|
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;
|
|
}
|
|
|
|
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
|