Files
RP412/RP_L4/RPL4LOBBY.cpp
T
CydandClaude Opus 5 bc56ab5d48 Joining stays on the page too, and there is one host at a time
The other half of the setup-page-as-room work, both halves Cyd's ask.

JOIN STEAM GAME now sits down where it stands. The mission column -
scenario buttons and the track, time, weather and length drop-downs -
greys out and MIRRORS the host: the owner's published setup is mapped
from display names back to catalog indices on every heartbeat, so a
member watches the host change the track from inside their own setup
screen. The right column stays theirs - callsign, role, vehicle, colour,
badge, recording - and republishes as they change it, so the host's
roster tracks them the same way. LAUNCH GAME reads WAITING FOR THE HOST
and answers to nobody; the go arrives through the same timer, registers
the peers, and leaves the menu as a member launch. JOIN becomes LEAVE
LOBBY while seated. The room screen is now unused by both roles - kept
compiled, but nobody's flow reaches it.

A member's launch also now sets the RECORDING flag from its own pick.
Nothing on the member path ever set it before - the same
published-but-never-acted-on seam as the member cam - so a member who
ticked RECORDING recorded nothing, all the way back to the feature
landing.

One host at a time, while the flow is young: HOST STEAM GAME first runs
the same worldwide search joining uses, and if an RP412 lobby is already
open anywhere it refuses with A LOBBY IS ALREADY OPEN - JOIN IT rather
than opening a second room nobody can tell from the first.
Check-then-create is not airtight against two people clicking in the
same breath, but for a playtest group on voice it is the rule that was
asked for. If the host leaves, ownership would migrate silently and turn
some member's page into a host's - the member poll treats inheriting
ownership as THE LOBBY CLOSED and steps out instead.

The old modal exits are gone with it: steamAction, the post-loop
RPL4Lobby_Join call, and the member's room re-entry path. The menu's
modal loop now ends only at launch or close, whoever you are.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 22:08:32 -05:00

1644 lines
49 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_HostOpen() { return HostOpenFailed; }
int RPL4Lobby_JoinOpen(HWND) { return JoinOpenNothingFound; }
int RPL4Lobby_MemberPoll() { return MemberPollClosed; }
void RPL4Lobby_GetSetup(char *a, char *b, char *c, char *d, char *e)
{ a[0] = b[0] = c[0] = d[0] = e[0] = '\0'; }
Logical RPL4Lobby_IsOwner() { return False; }
void RPL4Lobby_Pump() { }
void RPL4Lobby_PublishSetup() { }
int RPL4Lobby_RosterLines(char [][48], int) { return 0; }
Logical RPL4Lobby_HostLaunch() { return False; }
void RPL4Lobby_Leave() { }
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 "rpl4build.h" // generated: RP412_VERSION, for the build guard
#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 exact build, so a room cannot mix them.
//
// kNetRevision above is hand-maintained and only bumped when someone
// decides a change alters the simulation - which means two DIFFERENT
// builds normally carry the same revision and will happily race each
// other. That is fine when the difference really is cosmetic and
// disastrous when the judgement was wrong, and it is not a judgement
// anyone should have to make correctly every time. The patch number is
// the repository's commit count, so this compares the actual binary.
//
// Both guards stay: the revision still refuses a mix that is known to
// simulate differently even between builds that agree here, which
// matters for anyone hand-editing a version.
//
const char kBuildKey[] = "bld";
// 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);
//
// The name in the room is the CALLSIGN from the setup screen, not
// the Steam persona. Publishing the persona meant the box a player
// types their name into had no effect on anything they could see -
// and because it never came from a file, wiping the install
// directory did not shake it loose either.
//
// The persona is kept as the fallback for somebody who has never
// set a callsign: better to appear as yourself than as "Pilot".
//
char name[32];
const char *callsign = RPL4FrontEnd_Callsign();
if (callsign != NULL && callsign[0] != '\0' &&
strcmp(callsign, "Pilot") != 0)
{
SanitizeName(callsign, name, sizeof(name));
}
else
{
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);
// and which build it actually IS - see kBuildKey
SteamMatchmaking()->SetLobbyMemberData(gLobby, kBuildKey, RP412_VERSION);
//---------------------------------------------------------------
// 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 these before they act on the owner's go, and
// a joiner checks the build before it even sits down
SteamMatchmaking()->SetLobbyData(gLobby, kNetRevKey, kNetRevision);
SteamMatchmaking()->SetLobbyData(gLobby, kBuildKey, RP412_VERSION);
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
char build[24]; // the exact build, see kBuildKey
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);
strncpy(member->build,
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, kBuildKey),
sizeof(member->build) - 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);
pilot->camera = members[i].camera;
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);
}
//---------------------------------------------------------------
// Leaving and launching, callable from either screen.
//
// These began as blocks inside the room's message loop, which meant
// only the room could do either - and the host no longer lives in
// the room: hosting is claimed from the setup menu, where the track
// is still changeable and the roster shows under GAME LENGTH. The
// room keeps calling them; the menu calls them too.
//---------------------------------------------------------------
void LeaveLobbyNow()
{
if (!gInLobby)
{
return;
}
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);
}
//
// The owner's go: verify the room, publish the roster, prime the
// hosted-race path. Answers False when somebody has not published or
// is the wrong build - the caller says so however it says things.
//
Logical OwnerLaunchNow()
{
MemberInfo members[kMaxLobbyMembers];
int member_count = CollectMembers(members);
Logical all_published = True;
Logical all_same_build = True;
for (int i = 0; i < member_count; ++i)
{
if (!members[i].published)
{
all_published = False;
}
if (strcmp(members[i].netRev, kNetRevision) != 0)
{
all_same_build = False;
DEBUG_STREAM << "Lobby: " << members[i].name
<< " simulates like rev '" << members[i].netRev
<< "', we are rev '" << kNetRevision << "'\n" << std::flush;
}
//
// The exact build too. An empty string is an older build that
// predates the key and cannot be trusted to match either.
//
if (strcmp(members[i].build, RP412_VERSION) != 0)
{
all_same_build = False;
DEBUG_STREAM << "Lobby: " << members[i].name
<< " is build '"
<< (members[i].build[0] ? members[i].build : "(older)")
<< "', we are '" << RP412_VERSION << "'\n" << std::flush;
}
}
if (!all_published || !all_same_build || member_count < 1)
{
DEBUG_STREAM << "Lobby: not everyone is ready yet\n" << std::flush;
return False;
}
++gLastGoNonce;
char go[800];
sprintf(go, "%d:", gLastGoNonce);
for (int i = 0; i < member_count; ++i)
{
char entry[96];
sprintf(entry, "%s|%d|%d|%I64u;", members[i].ip,
members[i].consolePort, members[i].gamePort,
members[i].id.ConvertToUint64());
if (strlen(go) + strlen(entry) < sizeof(go))
{
strcat(go, entry);
}
}
SteamMatchmaking()->SetLobbyData(gLobby, kGoKey, go);
RegisterRoster(members, member_count);
PrimeHostedRace(members, member_count);
return True;
}
//
// The member's side of the same coin: has anything happened that this
// station must act on? Answers in the header's RPL4LobbyMemberPoll
// values - nothing / launch (peers already registered) / closed (we
// have already left).
//
int MemberPollNow()
{
if (!gInLobby)
{
return MemberPollClosed;
}
//
// The host leaving hands Steam's lobby ownership to somebody
// else - which would silently turn a joiner's page into a host's.
// For now there is one host and it is whoever opened the lobby:
// if ownership lands on us, the host is gone, so fold the room.
//
if (IsOwner())
{
DEBUG_STREAM << "Lobby: the host left - closing\n" << std::flush;
LeaveLobbyNow();
return MemberPollClosed;
}
//
// 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;
LeaveLobbyNow();
return MemberPollClosed;
}
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);
}
}
return MemberPollLaunch;
}
}
return MemberPollNothing;
}
//---------------------------------------------------------------
// 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 build the room cannot race with is worth saying before
// anything about loadouts. A joiner is now turned away at the
// door, so this should only ever show for a member who was
// already seated when the guard arrived - but the launch check
// still refuses on it, and a host is owed the reason.
//
if (member->published && member->build[0] != '\0' &&
strcmp(member->build, RP412_VERSION) != 0)
{
sprintf(text, "BUILD %s", member->build);
DrawTextA(mem, text, -1, &row,
DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
}
//
// A Live Cam brings no vehicle, so the row says the role where
// the loadout would go - ANY row now, since the egg honours a
// member's pick the same as the host's. Football first, though:
// there are no cameras in football (the egg builder strips the
// pick), so a football room shows the team sheet and saying
// LIVE CAM there would be the room lying about the grid.
//
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->camera)
{
DrawTextA(mem, "LIVE CAM", -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)
{
LeaveLobbyNow();
outcome = LobbyRoomLeft;
break;
}
//
// Owner launch: roster into lobby data + local prime
//
if (room.launchClicked)
{
room.launchClicked = False;
if (OwnerLaunchNow())
{
outcome = LobbyLaunchHost;
break;
}
}
//
// Member: watch for the owner's go signal
//
if (!IsOwner())
{
int poll = MemberPollNow();
if (poll == MemberPollLaunch)
{
outcome = LobbyLaunchMember;
break;
}
if (poll == MemberPollClosed)
{
outcome = LobbyRoomLeft;
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.
//
namespace
{
//
// The one worldwide search, shared by joining and by the one-host
// rule: is there an open RP412 lobby right now?
//
Logical FindOpenLobby(CSteamID *found)
{
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))
{
return False;
}
*found = gCallLobby;
return True;
}
}
int
RPL4Lobby_HostOpen()
{
if (!SteamNetTransport_ClientLibraryPresent() || gInLobby)
{
return gInLobby ? HostOpenCreated : HostOpenFailed;
}
//
// One host at a time, while the whole flow is young: if an RP412
// lobby is already open anywhere, this button does not open a second
// one - the caller says join it instead. Check-then-create is not
// airtight against two people clicking in the same breath, but for a
// playtest group coordinating over voice it is the rule they asked
// for.
//
{
CSteamID existing;
if (FindOpenLobby(&existing))
{
DEBUG_STREAM << "Lobby: " << existing.ConvertToUint64()
<< " is already open - one host at a time, join it instead\n"
<< std::flush;
return HostOpenLobbyExists;
}
}
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 HostOpenFailed;
}
gLobby = gCallLobby;
gInLobby = True;
gLastGoNonce = 0;
SteamMatchmaking()->SetLobbyData(gLobby, kLobbyTagKey, "1");
SteamMatchmaking()->SetLobbyData(gLobby, kGoKey, "");
//
// The owner's row and the mission setup, published immediately: a
// joiner can arrive seconds from now, and what they see first is
// whatever is on the books.
//
PublishMemberData();
DEBUG_STREAM << "Lobby: hosting " << gLobby.ConvertToUint64()
<< " from the setup menu\n" << std::flush;
return HostOpenCreated;
}
//
// Joining, the same shape as hosting: sit down in the lobby and return -
// the setup menu stays up, the mission column greys out and mirrors the
// host's picks, and the go arrives through RPL4Lobby_MemberPoll.
//
int
RPL4Lobby_JoinOpen(HWND main_window)
{
if (!SteamNetTransport_ClientLibraryPresent() || gInLobby)
{
return gInLobby ? JoinOpenJoined : JoinOpenNothingFound;
}
CSteamID target;
if (!FindOpenLobby(&target))
{
DEBUG_STREAM << "Lobby: no open race lobby found\n" << std::flush;
return JoinOpenNothingFound;
}
gCallDone = False;
SteamAPICall_t call = SteamMatchmaking()->JoinLobby(target);
gCalls.enterResult.Set(call, &gCalls, &LobbyCalls::OnEnter);
if (!WaitForCall(15000))
{
DEBUG_STREAM << "Lobby: join failed\n" << std::flush;
return JoinOpenNothingFound;
}
gLobby = gCallLobby;
gInLobby = True;
//
// Refuse a room running a different build, and refuse it HERE rather
// than at launch - the same door guard the room flow has always had,
// word for word. See the room version for why it is loud.
//
{
const char *host_build = SteamMatchmaking()->GetLobbyData(gLobby, kBuildKey);
if (host_build == NULL || strcmp(host_build, RP412_VERSION) != 0)
{
DEBUG_STREAM << "Lobby: room is build '"
<< ((host_build != NULL && host_build[0]) ? host_build : "(older)")
<< "', we are '" << RP412_VERSION
<< "' - leaving, everyone must run the same build\n" << std::flush;
SteamMatchmaking()->LeaveLobby(gLobby);
gInLobby = False;
char said[256];
sprintf(said,
"That race is running Red Planet %s.\n"
"You are running %s.\n\n"
"Everyone in a race has to be on the same build - the\n"
"simulation has to agree exactly. Swap to a matching\n"
"build and join again.",
(host_build != NULL && host_build[0]) ? host_build : "an older build",
RP412_VERSION);
MessageBoxA(main_window, said, "Different build",
MB_OK | MB_ICONINFORMATION);
return JoinOpenWrongBuild;
}
}
// answer only launches newer than anything already in the lobby
const char *go = SteamMatchmaking()->GetLobbyData(gLobby, kGoKey);
gLastGoNonce = (go != NULL) ? atoi(go) : 0;
// our row, visible to the host's roster immediately
PublishMemberData();
DEBUG_STREAM << "Lobby: joined " << gLobby.ConvertToUint64()
<< " from the setup menu\n" << std::flush;
return JoinOpenJoined;
}
int
RPL4Lobby_MemberPoll()
{
if (!gInLobby)
{
return MemberPollClosed;
}
return MemberPollNow();
}
//
// The host's published mission setup, as display names, for the greyed
// mission column to mirror. Empty strings before the first publish lands.
//
void
RPL4Lobby_GetSetup(
char *scenario, char *map, char *time_of_day,
char *weather, char *length)
{
strncpy(scenario, LobbyText(kScenarioKey), 47); scenario[47] = '\0';
strncpy(map, LobbyText(kMapKey), 47); map[47] = '\0';
strncpy(time_of_day, LobbyText(kTimeKey), 47); time_of_day[47] = '\0';
strncpy(weather, LobbyText(kWeatherKey), 47); weather[47] = '\0';
strncpy(length, LobbyText(kLengthKey), 47); length[47] = '\0';
}
Logical
RPL4Lobby_IsOwner()
{
return gInLobby && IsOwner();
}
void
RPL4Lobby_Pump()
{
if (SteamNetTransport_ClientLibraryPresent())
{
SteamAPI_RunCallbacks();
}
}
void
RPL4Lobby_PublishSetup()
{
if (gInLobby)
{
PublishMemberData();
}
}
//
// The roster, as display lines for the setup menu: callsign plus what it
// is bringing, a cam tag for a member who picked Live Cam, and a build
// warning where launch would refuse - so the host can see WHY the room is
// not ready, standing in the same screen the launch button is on.
//
int
RPL4Lobby_RosterLines(char lines[][48], int max_lines)
{
if (!gInLobby || max_lines <= 0)
{
return 0;
}
MemberInfo members[kMaxLobbyMembers];
int count = CollectMembers(members);
int written = 0;
for (int i = 0; i < count && written < max_lines; ++i)
{
const char *name = members[i].name[0] ? members[i].name : "(joining...)";
//
// What they are bringing. A camera brings no vehicle - the pick
// replaces it outright, in the egg and so on this line.
//
const char *bring =
members[i].camera ? "LIVE CAM" : members[i].vehicle;
const char *note = "";
if (strcmp(members[i].build, RP412_VERSION) != 0)
{
note = " WRONG BUILD";
}
else if (!members[i].published)
{
note = " ...";
}
_snprintf(lines[written], 47, "%-14s %s%s", name, bring, note);
lines[written][47] = '\0';
++written;
}
return written;
}
Logical
RPL4Lobby_HostLaunch()
{
if (!gInLobby || !IsOwner())
{
return False;
}
return OwnerLaunchNow();
}
void
RPL4Lobby_Leave()
{
LeaveLobbyNow();
}
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;
//
// Refuse a room running a different build, and refuse it HERE rather
// than at launch. The owner's launch check would catch it, but only
// after everyone has picked a loadout and pressed go, and all it can do
// then is silently decline to start - which reads as the host's button
// being broken. Far better to say so on the way in.
//
// The owner publishes its build as lobby data, so this costs one read.
// An empty string means a host older than the key, which is equally a
// mismatch: it cannot be trusted to be this build.
//
{
const char *host_build = SteamMatchmaking()->GetLobbyData(gLobby, kBuildKey);
if (host_build == NULL || strcmp(host_build, RP412_VERSION) != 0)
{
DEBUG_STREAM << "Lobby: room is build '"
<< ((host_build != NULL && host_build[0]) ? host_build : "(older)")
<< "', we are '" << RP412_VERSION
<< "' - leaving, everyone must run the same build\n" << std::flush;
SteamMatchmaking()->LeaveLobby(gLobby);
gInLobby = False;
//
// Say so on screen, not just in the log. A player who is bounced
// out of a room with no explanation will try again, and again,
// and then report that joining is broken - which is precisely
// the afternoon this guard exists to prevent.
//
char said[256];
sprintf(said,
"That race is running Red Planet %s.\n"
"You are running %s.\n\n"
"Everyone in a race has to be on the same build - the\n"
"simulation has to agree exactly. Swap to a matching\n"
"build and join again.",
(host_build != NULL && host_build[0]) ? host_build : "an older build",
RP412_VERSION);
MessageBoxA(main_window, said, "Different build",
MB_OK | MB_ICONINFORMATION);
return LobbyRoomLeft;
}
}
// 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