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>
This commit is contained in:
Cyd
2026-08-11 22:08:32 -05:00
co-authored by Claude Opus 5
parent ae939bcea7
commit bc56ab5d48
3 changed files with 577 additions and 166 deletions
+279 -94
View File
@@ -305,7 +305,6 @@ namespace
HBRUSH editBrush;
Logical launched;
Logical closed;
int steamAction; // 2 = join lobby (hosting stays on this page)
//
// Hosting without leaving this page. The lobby is claimed in
@@ -315,6 +314,14 @@ namespace
//
Logical hostingLive; // we own an open lobby right now
Logical hostLaunch; // LAUNCH pressed while hosting: go as host
//
// Joined without leaving this page, either. The mission column
// greys out and mirrors the host's picks, the roster shows who
// else is in, and the timer watches for the host's go.
//
Logical memberLive; // seated in somebody else's lobby
Logical memberLaunch; // the go arrived: leave as a member
Logical publishDirty; // a pick changed; republish on the timer
DWORD lastPublish; // tick of the last republish (rate limit)
RECT rosterRect; // under GAME LENGTH, where joiners list
@@ -798,7 +805,10 @@ namespace
HFONT font = (HFONT) SendMessageA(hwnd, WM_GETFONT, 0, 0);
HGDIOBJ old_font = (font != NULL) ? SelectObject(dc, font) : NULL;
SetBkMode(dc, TRANSPARENT);
SetTextColor(dc, kGreenBright);
// a disabled box (a member's view of the host's settings)
// reads dim, like every other thing on this screen that is
// not yours to change
SetTextColor(dc, IsWindowEnabled(hwnd) ? kGreenBright : kGreenDim);
RECT label = rc;
label.left += 6;
@@ -1075,11 +1085,15 @@ namespace
case GroupLength: return kLengths[index].name;
case GroupRole: return kRoles[index].name;
case GroupRecord: return kRecording[index].name;
case GroupLaunch: return "L A U N C H G A M E";
case GroupLaunch:
return (gFE != NULL && gFE->memberLive)
? "WAITING FOR THE HOST" : "L A U N C H G A M E";
case GroupSteamHost:
return (gFE != NULL && gFE->hostingLive)
? "CLOSE LOBBY" : "HOST STEAM GAME";
case GroupSteamJoin: return "JOIN STEAM GAME";
case GroupSteamJoin:
return (gFE != NULL && gFE->memberLive)
? "LEAVE LOBBY" : "JOIN STEAM GAME";
case GroupExit: return "EXIT GAME";
}
return "";
@@ -1168,6 +1182,18 @@ namespace
dead = True;
}
//
// Seated in somebody else's lobby: the host decides when
// the race goes and there is one lobby to be in, so the
// launch and host buttons read as what they are - not
// yours right now.
//
if (fe->memberLive &&
(item->group == GroupLaunch || item->group == GroupSteamHost))
{
dead = True;
}
COLORREF ink = dead ? kGreenDim : kGreenBright;
HBRUSH launch_brush = CreateSolidBrush(ink);
FrameRect(mem, &row, launch_brush);
@@ -1193,7 +1219,16 @@ namespace
continue;
}
if (selected)
//
// The scenario buttons are mission settings, and a seated
// member does not set the mission: painted flat dim, no
// selection fill, matching the greyed drop-downs beside them.
//
if (fe->memberLive && item->group == GroupScenario)
{
SetTextColor(mem, kGreenDim);
}
else if (selected)
{
HBRUSH sel_brush = CreateSolidBrush(kGreenDim);
RECT fill = row;
@@ -1217,10 +1252,11 @@ namespace
// (the pilot-name caption is drawn with the other labels above)
//
// The lobby roster, while this page is the host's room. Under
// GAME LENGTH, where the mission column runs out of settings.
// The lobby roster, while this page is anybody's room - the
// host's or a member's. Under GAME LENGTH, where the mission
// column runs out of settings.
//
if (fe->hostingLive)
if (fe->hostingLive || fe->memberLive)
{
int line_h = (fe->rosterRect.bottom - fe->rosterRect.top) / 10;
if (line_h < 16) line_h = 16;
@@ -1328,6 +1364,95 @@ namespace
}
}
//
// A seated member's mission column belongs to the host. The combos
// stay visible - they mirror what the host picks - but they answer to
// nobody at this station. Called after every LayoutMenu too, because
// layout rebuilds the combo windows from scratch.
//
void ApplyMemberLock(FEState *fe)
{
static const int mission_groups[] =
{ GroupMap, GroupTime, GroupWeather, GroupLength };
for (int i = 0; i < 4; ++i)
{
if (fe->combo[mission_groups[i]] != NULL)
{
EnableWindow(fe->combo[mission_groups[i]],
fe->memberLive ? FALSE : TRUE);
}
}
}
//
// Mirror the host's published mission into the greyed column, names
// mapped back to catalog indices. Names, because that is what the
// owner publishes and what the room screen always showed; a name
// that matches nothing (yet) leaves the box alone rather than
// guessing.
//
void ReflectLobbySetup(FEState *fe)
{
char scenario[48], map[48], time_of_day[48], weather[48], length[48];
RPL4Lobby_GetSetup(scenario, map, time_of_day, weather, length);
Logical changed = False;
// scenario travels as its KEY ("race"/"football")
for (int i = 0; i < FE_COUNT(kScenarios); ++i)
{
if (scenario[0] != '\0' &&
_stricmp(scenario, kScenarios[i].key) == 0 &&
fe->selection[GroupScenario] != i)
{
fe->selection[GroupScenario] = i;
changed = True;
}
}
// the rest travel as display names
struct { int group; const char *name; } mirror[] =
{
{ GroupMap, map }, { GroupTime, time_of_day },
{ GroupWeather, weather }, { GroupLength, length },
};
for (int m = 0; m < 4; ++m)
{
if (mirror[m].name[0] == '\0')
{
continue;
}
int count = GroupSize(mirror[m].group, fe->selection);
for (int i = 0; i < count; ++i)
{
if (_stricmp(mirror[m].name,
ItemName(mirror[m].group, i)) == 0 &&
fe->selection[mirror[m].group] != i)
{
fe->selection[mirror[m].group] = i;
changed = True;
}
}
}
if (changed)
{
//
// Rebuild rather than poke: the scenario swaps whole lists,
// and LayoutMenu already knows how to stand the menu up from
// a selection. Re-lock afterwards - the rebuild makes fresh
// combo windows.
//
RECT client;
GetClientRect(fe->menuWindow, &client);
LayoutMenu(fe, client.right, client.bottom);
ApplyMemberLock(fe);
InvalidateRect(fe->menuWindow, NULL, TRUE);
}
}
void MenuClick(FEState *fe, int x, int y)
{
for (int i = 0; i < fe->itemCount; ++i)
@@ -1370,8 +1495,9 @@ namespace
}
else if (item->group == GroupSteamHost)
{
// dead until the Steam client is there; the button says so
if (!RPL4Lobby_Available())
// dead until the Steam client is there; the button
// says so - and not the member's button at all
if (!RPL4Lobby_Available() || fe->memberLive)
{
return;
}
@@ -1392,23 +1518,67 @@ namespace
// accessors, so sync first.
//
SyncPersistFromMenu(fe);
if (RPL4Lobby_HostOpen())
switch (RPL4Lobby_HostOpen())
{
case HostOpenCreated:
fe->hostingLive = True;
fe->hostNotice[0] = '\0';
SetTimer(fe->menuWindow, kLobbyTimerId, 750, NULL);
RefreshRoster(fe);
InvalidateRect(fe->menuWindow, NULL, FALSE);
break;
case HostOpenLobbyExists:
// one host at a time, and it is not you
strcpy(fe->hostNotice,
"A LOBBY IS ALREADY OPEN - JOIN IT");
break;
default:
strcpy(fe->hostNotice, "COULD NOT OPEN A LOBBY");
break;
}
InvalidateRect(fe->menuWindow, NULL, FALSE);
}
else if (item->group == GroupSteamJoin)
{
// dead without Steam, and dead while hosting our own
if (RPL4Lobby_Available() && !fe->hostingLive)
if (!RPL4Lobby_Available() || fe->hostingLive)
{
fe->steamAction = 2;
PostMessageA(fe->menuWindow, WM_NULL, 0, 0);
return;
}
if (fe->memberLive)
{
// LEAVE LOBBY: back to a page that is all ours
RPL4Lobby_Leave();
fe->memberLive = False;
fe->rosterCount = 0;
fe->hostNotice[0] = '\0';
KillTimer(fe->menuWindow, kLobbyTimerId);
ApplyMemberLock(fe);
InvalidateRect(fe->menuWindow, NULL, TRUE);
return;
}
//
// Sit down and STAY, mirroring the host: same shape
// as hosting, from the other side of the table.
//
SyncPersistFromMenu(fe);
switch (RPL4Lobby_JoinOpen(GetParent(fe->menuWindow)))
{
case JoinOpenJoined:
fe->memberLive = True;
fe->hostNotice[0] = '\0';
SetTimer(fe->menuWindow, kLobbyTimerId, 750, NULL);
RefreshRoster(fe);
ApplyMemberLock(fe);
ReflectLobbySetup(fe);
break;
case JoinOpenNothingFound:
strcpy(fe->hostNotice, "NO OPEN LOBBY FOUND");
break;
default:
// wrong build: the dialog already explained
break;
}
InvalidateRect(fe->menuWindow, NULL, TRUE);
}
else if (item->group == GroupExit)
{
@@ -1418,10 +1588,16 @@ namespace
}
else
{
fe->selection[item->group] = item->index;
if (fe->hostingLive)
// the mission is the host's: a member's clicks on the
// scenario buttons land nowhere
if (fe->memberLive && item->group == GroupScenario)
{
fe->publishDirty = True; // members' rooms track it
return;
}
fe->selection[item->group] = item->index;
if (fe->hostingLive || fe->memberLive)
{
fe->publishDirty = True; // the roster rows track it
}
if (item->group == GroupScenario)
{
@@ -1477,7 +1653,7 @@ namespace
if (pick >= 0)
{
fe->selection[group] = pick;
if (fe->hostingLive)
if (fe->hostingLive || fe->memberLive)
{
fe->publishDirty = True;
}
@@ -1488,15 +1664,16 @@ namespace
return 0;
}
}
// a callsign being typed reaches the members' rooms too
if (fe != NULL && HIWORD(wParam) == EN_CHANGE && fe->hostingLive)
// a callsign being typed reaches the other rosters too
if (fe != NULL && HIWORD(wParam) == EN_CHANGE &&
(fe->hostingLive || fe->memberLive))
{
fe->publishDirty = True;
}
break;
//
// The lobby heartbeat, while this page is the host's room. Steam
// The lobby heartbeat, while this page is anybody's room. Steam
// only delivers member joins, leaves and data through its
// callback pump, and with no room screen running nothing else
// pumps it. Publishes ride the same beat, rate-limited, so a
@@ -1504,7 +1681,8 @@ namespace
// Steam throttles writers who cannot restrain themselves.
//
case WM_TIMER:
if (fe != NULL && wParam == kLobbyTimerId && fe->hostingLive)
if (fe != NULL && wParam == kLobbyTimerId &&
(fe->hostingLive || fe->memberLive))
{
RPL4Lobby_Pump();
RefreshRoster(fe);
@@ -1516,6 +1694,35 @@ namespace
SyncPersistFromMenu(fe);
RPL4Lobby_PublishSetup();
}
//
// The member's half: mirror what the host is picking,
// and watch for the go. A launch exits the modal loop
// like any button would; a folded lobby hands the page
// back whole, and says why it folded.
//
if (fe->memberLive)
{
ReflectLobbySetup(fe);
int poll = RPL4Lobby_MemberPoll();
if (poll == MemberPollLaunch)
{
fe->memberLaunch = True;
fe->launched = True;
PostMessageA(fe->menuWindow, WM_NULL, 0, 0);
}
else if (poll == MemberPollClosed)
{
fe->memberLive = False;
fe->rosterCount = 0;
strcpy(fe->hostNotice, "THE LOBBY CLOSED");
KillTimer(fe->menuWindow, kLobbyTimerId);
ApplyMemberLock(fe);
InvalidateRect(fe->menuWindow, NULL, TRUE);
}
}
return 0;
}
break;
@@ -2170,43 +2377,12 @@ Logical
EnsurePilotSettingsLoaded();
//---------------------------------------------------------------
// Coming back from a race while still in a lobby: members go
// straight to the room (the lobby outlives races - single binary
// payoff). The OWNER falls through to the setup menu instead -
// that page is the host's room now, hosting still live, so the
// next race is configured right where the roster shows.
// Coming back from a race while still in a lobby: EVERYONE falls
// through to the setup menu now - the page is the room, for the
// host and the members alike. The lobby outlives races (single
// binary payoff), so hosting or membership picks straight back up
// in the menu-setup block below.
//---------------------------------------------------------------
if (RPL4Lobby_InRoom() && !RPL4Lobby_IsOwner())
{
int outcome = RPL4Lobby_Room(instance, main_window);
if (outcome == LobbyRoomClosed)
{
return False;
}
if (outcome == LobbyLaunchMember)
{
//
// A member's cockpit comes from its OWN role pick, set here
// because the renderers are built before the wire egg
// arrives - the same ordering that decides the host's and
// playback's cockpits. The host honours the same pick in the
// egg, so the two agree; the one exception is the
// everybody-picked-cam room, where the host strips every
// pick to keep a racer and this machine cannot know yet.
//
Application::SetCameraStation(
RPL4FrontEnd_IsCameraRole() ? True : False);
gLastLaunchMode = FELaunchMember;
egg_path_out[0] = '\0';
return True;
}
if (outcome == LobbyLaunchHost)
{
gLastLaunchMode = FELaunchHost;
return BuildEggFromPersisted(egg_path_out, egg_path_size);
}
// LobbyRoomLeft: fall through to the setup menu
}
// NOTE: the vehicle catalog below is hand-written while RPL4.RES is
// built elsewhere, so the two can drift - this menu offered 'blkspk'
@@ -2286,25 +2462,32 @@ Logical
SendMessageA(fe.nameEdit, EM_SETLIMITTEXT, sizeof(fe.pilotName) - 2, 0);
//
// Back from a race we hosted: the lobby is still open and this
// page picks hosting straight back up - roster, heartbeat, all
// of it - so the host never sees the room screen at all.
// Back from a race while still in the lobby: the page picks its
// role straight back up - hosting or seated - roster, heartbeat,
// all of it. Nobody sees the room screen anymore.
//
if (RPL4Lobby_IsOwner())
if (RPL4Lobby_InRoom())
{
fe.hostingLive = True;
fe.hostingLive = RPL4Lobby_IsOwner();
fe.memberLive = fe.hostingLive ? False : True;
SetTimer(fe.menuWindow, kLobbyTimerId, 750, NULL);
RefreshRoster(&fe);
}
LayoutMenu(&fe, client.right, client.bottom);
if (fe.memberLive)
{
ApplyMemberLock(&fe);
ReflectLobbySetup(&fe);
}
InvalidateRect(fe.menuWindow, NULL, TRUE);
//---------------------------------------------------------------
// Modal loop until launch, lobby button, or close
// Modal loop until launch or close (lobbies come and go without
// leaving it - the timer and the buttons handle both roles)
//---------------------------------------------------------------
MSG msg;
while (!fe.launched && !fe.closed && fe.steamAction == 0)
while (!fe.launched && !fe.closed)
{
BOOL result = GetMessageA(&msg, NULL, 0, 0);
if (result <= 0)
@@ -2370,6 +2553,7 @@ Logical
Logical launched = fe.launched;
Logical closed = fe.closed;
Logical host_launch = fe.hostLaunch;
Logical member_launch = fe.memberLaunch;
DestroyWindow(fe.menuWindow);
DeleteObject(fe.textFont);
@@ -2380,10 +2564,11 @@ Logical
if (closed)
{
//
// Quitting while hosting closes the lobby too - members see
// the room fold rather than a host who never comes back.
// Quitting while in a lobby leaves it properly - the others
// see the room fold or a row vanish, not a ghost who never
// answers.
//
if (fe.hostingLive)
if (fe.hostingLive || fe.memberLive)
{
RPL4Lobby_Leave();
}
@@ -2391,6 +2576,29 @@ Logical
}
if (launched)
{
//
// The go arrived while we sat as a member: enter the race as
// a pod. The cockpit comes from this machine's OWN role pick,
// set before the renderers build - the same ordering that
// decides the host's and playback's cockpits. The host
// honours the same pick in the egg, so the two agree; the one
// exception is the everybody-picked-cam room, where the host
// strips every pick to keep a racer and this machine cannot
// know yet. RECORDING is likewise this station's own pick -
// nothing on the member path ever set it before, so a member
// who ticked it recorded nothing.
//
if (member_launch)
{
Application::SetCameraStation(
RPL4FrontEnd_IsCameraRole() ? True : False);
Application::SetRecording(
(gHavePersist && WantsRecording(gPersistSelection))
? True : False);
gLastLaunchMode = FELaunchMember;
egg_path_out[0] = '\0';
return True;
}
//
// LAUNCH while hosting goes out as the lobby's host - the
// roster is primed and the go is published. Otherwise it is
@@ -2404,31 +2612,8 @@ Logical
return BuildEggFromPersisted(egg_path_out, egg_path_size);
}
//---------------------------------------------------------------
// Steam lobby join, then back here if they leave. (Hosting no
// longer exits the menu - the page IS the host's room.)
//---------------------------------------------------------------
int outcome = RPL4Lobby_Join(instance, main_window);
if (outcome == LobbyRoomClosed)
{
return False;
}
if (outcome == LobbyLaunchMember)
{
// same as the room re-entry path: the cockpit is this
// machine's own role pick, set before the renderers build
Application::SetCameraStation(
RPL4FrontEnd_IsCameraRole() ? True : False);
gLastLaunchMode = FELaunchMember;
egg_path_out[0] = '\0';
return True;
}
if (outcome == LobbyLaunchHost)
{
gLastLaunchMode = FELaunchHost;
return BuildEggFromPersisted(egg_path_out, egg_path_size);
}
// LobbyRoomLeft: show the menu again
// neither launched nor closed cannot fall out of the loop above;
// show the menu again regardless
}
}
+257 -69
View File
@@ -13,7 +13,11 @@
Logical RPL4Lobby_Available() { return False; }
Logical RPL4Lobby_Configured() { return False; }
Logical RPL4Lobby_InRoom() { return False; }
Logical RPL4Lobby_HostOpen() { 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() { }
@@ -513,6 +517,102 @@ namespace
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)
//---------------------------------------------------------------
@@ -1009,75 +1109,17 @@ namespace
//
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)
int poll = MemberPollNow();
if (poll == MemberPollLaunch)
{
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;
outcome = LobbyLaunchMember;
break;
}
const char *go = SteamMatchmaking()->GetLobbyData(gLobby, kGoKey);
if (go != NULL && go[0] != '\0')
if (poll == MemberPollClosed)
{
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;
}
outcome = LobbyRoomLeft;
break;
}
}
@@ -1138,12 +1180,58 @@ Logical
// 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;
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;
@@ -1153,7 +1241,7 @@ int
if (!WaitForCall(15000))
{
DEBUG_STREAM << "Lobby: CreateLobby failed\n" << std::flush;
return False;
return HostOpenFailed;
}
gLobby = gCallLobby;
gInLobby = True;
@@ -1169,7 +1257,107 @@ int
PublishMemberData();
DEBUG_STREAM << "Lobby: hosting " << gLobby.ConvertToUint64()
<< " from the setup menu\n" << std::flush;
return True;
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
+41 -3
View File
@@ -55,11 +55,49 @@ Logical
// GAME goes through HostLaunch. The room screen is for members only.
//------------------------------------------------------------------------
// Create the lobby and publish our row + the mission setup. True on
// success (also true if already in one). Does not open any window.
Logical
enum RPL4LobbyHostOpen
{
HostOpenFailed = 0, // Steam said no (or is not there)
HostOpenCreated, // the lobby is ours (also: already was)
HostOpenLobbyExists // one host at a time - join the open one
};
// Create the lobby and publish our row + the mission setup. Does not
// open any window. Refuses when an RP412 lobby is already open anywhere:
// one host at a time while the flow is young.
int
RPL4Lobby_HostOpen();
enum RPL4LobbyJoinOpen
{
JoinOpenNothingFound = 0, // no open lobby, or Steam said no
JoinOpenJoined, // seated; our row is published
JoinOpenWrongBuild // bounced at the door (dialog shown)
};
// Find the open lobby and sit down in it. Does not open any window: the
// setup menu stays up, mission column greyed and mirroring the host.
int
RPL4Lobby_JoinOpen(HWND main_window);
enum RPL4LobbyMemberPoll
{
MemberPollNothing = 0, // keep waiting
MemberPollLaunch, // the go arrived; peers registered - launch
MemberPollClosed // the lobby is over (host left / mismatch)
};
// A seated member's heartbeat question: anything to act on?
int
RPL4Lobby_MemberPoll();
// The host's published mission setup as display names (each buffer at
// least 48 bytes). Empty strings before the first publish arrives.
void
RPL4Lobby_GetSetup(
char *scenario, char *map, char *time_of_day,
char *weather, char *length);
// True when we are in a lobby we own.
Logical
RPL4Lobby_IsOwner();