The setup page is the host's room

Two player requests from the six-player night, one cause between them:
the lobby treated hosting as somewhere you GO, and a role you could only
have by being the machine that went there.

Hosting no longer leaves the setup page. HOST STEAM GAME claims the lobby
and stays put: the button becomes CLOSE LOBBY, joiners list under GAME
LENGTH as they arrive (callsign, vehicle, a cam tag, and WRONG BUILD where
launch would refuse - the reasons launch might say no, standing on the
same screen as the launch button), and the track, weather and length stay
changeable the whole time, republished to the members' rooms as they
change. LAUNCH GAME launches the whole room from right there, and after
the race the host lands back on the same page, lobby still open, roster
still live. The room screen still exists - for members, whose flow is
untouched.

Mechanically: RPL4Lobby_Host's create-then-run-the-room split into
HostOpen (create, publish, return) plus Pump/PublishSetup/RosterLines/
HostLaunch/Leave, with the owner-launch and leave blocks extracted from
the room's message loop so both screens call the same code. The menu gets
a timer that pumps Steam callbacks - nothing else pumps them with no room
running - and republishes dirty picks at most every 1.5s, because Steam
throttles chatty writers. One wrinkle: the lobby publishes through the
RPL4FrontEnd_* accessors, which read globals the menu only wrote on the
way OUT, so publishing from a live menu syncs them first or every publish
would carry the previous visit's picks.

And a member's Live Cam pick now works. It was host-only by design - the
pick published as member data and the room displayed it, but the egg
builder never read it, so a member who chose Live Cam raced their default
loadout instead (the "default Quark - red" Cyd was listed as). The pick
now rides the whole chain - lobby row, hosted-pilot table, egg entry as
hostType=1 / vehicle=camera - and the member machine sets its own cockpit
from its own pick at launch, before the renderers build, exactly as the
host and playback already did. One rule holds it together: SOMEBODY has
to race. If every pilot picks cam, every pick is stripped and the log
says so - a grid of cameras has nothing to point at, and is also the
shape known to hang the map load.

The playtest launch failures while a member had cam picked are not
directly explained - no logs were kept from those attempts - but the
half-implemented state they ran in (flag published, egg ignoring it) is
exactly the seam this closes, and the failure cannot recur in that form:
the pick is now either honoured everywhere or stripped everywhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-11 21:34:12 -05:00
co-authored by Claude Opus 5
parent e2bcb29a53
commit 47817ca6f5
4 changed files with 592 additions and 81 deletions
+350 -17
View File
@@ -305,7 +305,22 @@ namespace
HBRUSH editBrush;
Logical launched;
Logical closed;
int steamAction; // 1 = host lobby, 2 = join lobby
int steamAction; // 2 = join lobby (hosting stays on this page)
//
// Hosting without leaving this page. The lobby is claimed in
// place, the roster paints under GAME LENGTH, and LAUNCH GAME
// launches the room. Members still get the room screen; the
// host's room IS the setup menu.
//
Logical hostingLive; // we own an open lobby right now
Logical hostLaunch; // LAUNCH pressed while hosting: go as host
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
char roster[8][48];
int rosterCount;
char hostNotice[64]; // why LAUNCH refused, drawn above it
};
FEState *gFE = NULL;
@@ -580,6 +595,7 @@ namespace
char badge[24];
char team[32]; // football pick, "" = assign for them
char position[16];
Logical camera; // this member picked Live Cam
};
// lobby-fed override (real personas + loadouts); empty = env parsing
@@ -605,6 +621,7 @@ namespace
gHostedPilots[i].badge[0] ? gHostedPilots[i].badge : "None");
strcpy(extras[i].team, gHostedPilots[i].team);
strcpy(extras[i].position, gHostedPilots[i].position);
extras[i].camera = gHostedPilots[i].camera;
}
return gHostedPilotCount;
}
@@ -899,6 +916,16 @@ namespace
}
//
// The lobby roster, under GAME LENGTH: whoever joins while this
// page is hosting is listed here, in the column whose bottom half
// was always empty. Display only - no clicks land in it.
//
fe->rosterRect.left = margin;
fe->rosterRect.right = margin + col_w;
fe->rosterRect.top = left_y + row_h / 2;
fe->rosterRect.bottom = client_h - 3 * row_h;
// launch button
FEItem *launch = &fe->items[fe->itemCount++];
launch->group = GroupLaunch;
@@ -1049,7 +1076,9 @@ namespace
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 GroupSteamHost: return "HOST STEAM GAME";
case GroupSteamHost:
return (gFE != NULL && gFE->hostingLive)
? "CLOSE LOBBY" : "HOST STEAM GAME";
case GroupSteamJoin: return "JOIN STEAM GAME";
case GroupExit: return "EXIT GAME";
}
@@ -1132,6 +1161,13 @@ namespace
(item->group == GroupSteamHost || item->group == GroupSteamJoin);
Logical dead = steam_button && !RPL4Lobby_Available();
// one lobby at a time: no joining somebody else's while
// hosting our own
if (item->group == GroupSteamJoin && fe->hostingLive)
{
dead = True;
}
COLORREF ink = dead ? kGreenDim : kGreenBright;
HBRUSH launch_brush = CreateSolidBrush(ink);
FrameRect(mem, &row, launch_brush);
@@ -1180,6 +1216,63 @@ 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.
//
if (fe->hostingLive)
{
int line_h = (fe->rosterRect.bottom - fe->rosterRect.top) / 10;
if (line_h < 16) line_h = 16;
RECT line = fe->rosterRect;
line.bottom = line.top + line_h;
char header[48];
sprintf(header, "LOBBY OPEN - %d IN",
(fe->rosterCount > 0) ? fe->rosterCount : 1);
SetTextColor(mem, kGreenBright);
DrawTextA(mem, header, -1, &line,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
for (int r = 0; r < fe->rosterCount; ++r)
{
line.top += line_h;
line.bottom += line_h;
if (line.bottom > fe->rosterRect.bottom)
{
break;
}
SetTextColor(mem,
(strstr(fe->roster[r], "WRONG BUILD") != NULL)
? kGreenDim : kGreenBright);
DrawTextA(mem, fe->roster[r], -1, &line,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
}
}
//
// Why LAUNCH refused, above the launch button - same slot the
// STEAM NOT RUNNING notice uses, same reasoning: it does not fit
// in the button.
//
if (fe->hostNotice[0] != '\0')
{
for (int i = 0; i < fe->itemCount; ++i)
{
if (fe->items[i].group == GroupLaunch)
{
RECT notice = fe->items[i].rect;
notice.bottom = notice.top;
notice.top -= (fe->items[i].rect.bottom - fe->items[i].rect.top) / 2;
SetTextColor(mem, kGreenDim);
DrawTextA(mem, fe->hostNotice, -1, &notice,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
break;
}
}
}
BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);
SelectObject(mem, old_surface);
@@ -1188,6 +1281,53 @@ namespace
EndPaint(fe->menuWindow, &ps);
}
enum { kLobbyTimerId = 7 };
//
// The lobby publishes through the RPL4FrontEnd_* accessors, and those
// read the persisted globals - which the menu writes only on the way
// out. Hosting from the menu publishes while the menu is still up, so
// the globals must be brought current first or every publish would
// send the PREVIOUS visit's picks.
//
void SyncPersistFromMenu(FEState *fe)
{
if (fe->nameEdit != NULL)
{
char name[24];
name[0] = '\0';
GetWindowTextA(fe->nameEdit, name, sizeof(name) - 1);
SanitizeCallsign(name, sizeof(name));
if (name[0] != '\0')
{
strcpy(gLastPilotName, name);
}
}
memcpy(gPersistSelection, fe->selection, sizeof(gPersistSelection));
gHavePersist = True;
}
void RefreshRoster(FEState *fe)
{
char lines[8][48];
int count = RPL4Lobby_RosterLines(lines, 8);
Logical changed = (count != fe->rosterCount) ? True : False;
for (int i = 0; !changed && i < count; ++i)
{
if (strcmp(lines[i], fe->roster[i]) != 0)
{
changed = True;
}
}
if (changed)
{
memcpy(fe->roster, lines, sizeof(lines));
fe->rosterCount = count;
InvalidateRect(fe->menuWindow, NULL, FALSE);
}
}
void MenuClick(FEState *fe, int x, int y)
{
for (int i = 0; i < fe->itemCount; ++i)
@@ -1198,17 +1338,75 @@ namespace
{
if (item->group == GroupLaunch)
{
//
// Hosting: LAUNCH launches the whole room, from right
// here. The go must carry the latest picks, so publish
// once more before it; a refusal names itself above
// the button rather than in a log nobody has open.
//
if (fe->hostingLive)
{
SyncPersistFromMenu(fe);
RPL4Lobby_PublishSetup();
if (RPL4Lobby_HostLaunch())
{
fe->hostNotice[0] = '\0';
fe->hostLaunch = True;
fe->launched = True;
PostMessageA(fe->menuWindow, WM_NULL, 0, 0);
}
else
{
strcpy(fe->hostNotice,
"NOT EVERYONE IS READY - SEE THE ROSTER");
InvalidateRect(fe->menuWindow, NULL, FALSE);
}
return;
}
fe->launched = True;
// wake the modal loop (a click delivered via
// SendMessage never passes through the queue)
PostMessageA(fe->menuWindow, WM_NULL, 0, 0);
}
else if (item->group == GroupSteamHost || item->group == GroupSteamJoin)
else if (item->group == GroupSteamHost)
{
// dead until the Steam client is there; the button says so
if (RPL4Lobby_Available())
if (!RPL4Lobby_Available())
{
fe->steamAction = (item->group == GroupSteamHost) ? 1 : 2;
return;
}
if (fe->hostingLive)
{
// CLOSE LOBBY: everyone's room tells them
RPL4Lobby_Leave();
fe->hostingLive = False;
fe->rosterCount = 0;
fe->hostNotice[0] = '\0';
KillTimer(fe->menuWindow, kLobbyTimerId);
InvalidateRect(fe->menuWindow, NULL, FALSE);
return;
}
//
// Claim the lobby and STAY: the page is the host's
// room now. The publish inside HostOpen reads the
// accessors, so sync first.
//
SyncPersistFromMenu(fe);
if (RPL4Lobby_HostOpen())
{
fe->hostingLive = True;
fe->hostNotice[0] = '\0';
SetTimer(fe->menuWindow, kLobbyTimerId, 750, NULL);
RefreshRoster(fe);
InvalidateRect(fe->menuWindow, NULL, FALSE);
}
}
else if (item->group == GroupSteamJoin)
{
// dead without Steam, and dead while hosting our own
if (RPL4Lobby_Available() && !fe->hostingLive)
{
fe->steamAction = 2;
PostMessageA(fe->menuWindow, WM_NULL, 0, 0);
}
}
@@ -1221,6 +1419,10 @@ namespace
else
{
fe->selection[item->group] = item->index;
if (fe->hostingLive)
{
fe->publishDirty = True; // members' rooms track it
}
if (item->group == GroupScenario)
{
// the scenario swaps the map list and the
@@ -1275,6 +1477,10 @@ namespace
if (pick >= 0)
{
fe->selection[group] = pick;
if (fe->hostingLive)
{
fe->publishDirty = True;
}
}
// the closed box is ours to redraw now
InvalidateRect(fe->combo[group], NULL, FALSE);
@@ -1282,6 +1488,36 @@ namespace
return 0;
}
}
// a callsign being typed reaches the members' rooms too
if (fe != NULL && HIWORD(wParam) == EN_CHANGE && fe->hostingLive)
{
fe->publishDirty = True;
}
break;
//
// The lobby heartbeat, while this page is the host'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
// burst of clicking costs one SetLobbyData volley, not ten -
// Steam throttles writers who cannot restrain themselves.
//
case WM_TIMER:
if (fe != NULL && wParam == kLobbyTimerId && fe->hostingLive)
{
RPL4Lobby_Pump();
RefreshRoster(fe);
if (fe->publishDirty &&
(GetTickCount() - fe->lastPublish) > 1500)
{
fe->publishDirty = False;
fe->lastPublish = GetTickCount();
SyncPersistFromMenu(fe);
RPL4Lobby_PublishSetup();
}
return 0;
}
break;
//
@@ -1461,6 +1697,7 @@ namespace
const char *vehicle;
const char *color;
const char *badge;
Logical camera; // rides as hostType=1 / vehicle=camera
int team; // kTeams index, -1 outside football
const char *position;
Logical chosePosition; // picked it themselves
@@ -1474,6 +1711,7 @@ namespace
owner->vehicle = kVehicles[fe->selection[GroupVehicle]].key;
owner->color = kColors[fe->selection[GroupColor]].key;
owner->badge = kBadges[fe->selection[GroupBadge]].key;
owner->camera = False; // resolved below with everyone else's
owner->team = -1;
owner->position = NULL;
owner->chosePosition = False;
@@ -1485,11 +1723,50 @@ namespace
pilot->vehicle = extras[p].vehicle;
pilot->color = extras[p].color;
pilot->badge = extras[p].badge;
//
// A member's Live Cam pick rides its lobby row. Race only:
// football normalizes everyone into a team with a runner,
// and a camera has no place on either side of that.
//
pilot->camera = (!football && extras[p].camera) ? True : False;
pilot->team = -1;
pilot->position = NULL;
pilot->chosePosition = False;
}
//
// The owner's own pick, and then the rule that makes any set of
// picks launchable: SOMEBODY has to race. A camera needs pods to
// point at - a grid of nothing but cameras is also the shape
// known to hang the map load - so if honouring every pick would
// leave no racer, every pick is stripped and the log says so.
// The lobby roster shows who picked cam, so this is visible
// before launch, not a surprise after it.
//
owner->camera = (WantsCamera(fe->selection) && extra_count > 0)
? True : False;
{
int racer_count = 0;
for (int p = 0; p < pilot_count; ++p)
{
if (!pilots[p].camera)
{
++racer_count;
}
}
if (racer_count == 0)
{
for (int p = 0; p < pilot_count; ++p)
{
pilots[p].camera = False;
}
DEBUG_STREAM << "FE: every pilot picked Live Cam - somebody"
<< " has to race, so nobody is a camera this time\n"
<< std::flush;
}
}
//
// Football: every pilot's own team/position pick is honored;
// unset picks get filled in, then each team is normalized to
@@ -1659,7 +1936,12 @@ namespace
// else on the line stays - dropzone and colour cost nothing and
// the egg readers still expect them.
//
Logical owner_is_camera = WantsCamera(fe->selection) && extra_count > 0;
//
// The owner's camera pick was resolved into pilots[0] with
// everyone else's, up where the table was built - including the
// somebody-must-race strip. This is what remains true of it here.
//
Logical owner_is_camera = pilots[0].camera;
if (WantsCamera(fe->selection) && extra_count == 0)
{
DEBUG_STREAM << "FE: Live Cam ignored - no other pods in this race\n"
@@ -1696,7 +1978,13 @@ namespace
for (int p = 0; p < pilot_count; ++p)
{
Logical camera_entry = (p == 0 && owner_is_camera);
//
// Any pilot's camera pick now, not just the owner's - a
// member who chose Live Cam in its own menu rides as
// hostType=1 / vehicle=camera, and its machine sets its own
// cockpit up from the same pick at launch.
//
Logical camera_entry = pilots[p].camera;
sprintf(line, "[%s]\n", pilots[p].address);
egg += line;
@@ -1882,10 +2170,13 @@ Logical
EnsurePilotSettingsLoaded();
//---------------------------------------------------------------
// Coming back from a race while still in a lobby: straight to
// the room (the lobby outlives races - single binary payoff)
// 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.
//---------------------------------------------------------------
if (RPL4Lobby_InRoom())
if (RPL4Lobby_InRoom() && !RPL4Lobby_IsOwner())
{
int outcome = RPL4Lobby_Room(instance, main_window);
if (outcome == LobbyRoomClosed)
@@ -1894,6 +2185,17 @@ Logical
}
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;
@@ -1983,6 +2285,18 @@ Logical
SendMessageA(fe.nameEdit, WM_SETFONT, (WPARAM) fe.textFont, TRUE);
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.
//
if (RPL4Lobby_IsOwner())
{
fe.hostingLive = True;
SetTimer(fe.menuWindow, kLobbyTimerId, 750, NULL);
RefreshRoster(&fe);
}
LayoutMenu(&fe, client.right, client.bottom);
InvalidateRect(fe.menuWindow, NULL, TRUE);
@@ -2055,7 +2369,7 @@ Logical
Logical launched = fe.launched;
Logical closed = fe.closed;
int steam_action = fe.steamAction;
Logical host_launch = fe.hostLaunch;
DestroyWindow(fe.menuWindow);
DeleteObject(fe.textFont);
@@ -2065,27 +2379,46 @@ Logical
if (closed)
{
//
// Quitting while hosting closes the lobby too - members see
// the room fold rather than a host who never comes back.
//
if (fe.hostingLive)
{
RPL4Lobby_Leave();
}
return False;
}
if (launched)
{
// plain launch: single player, or a LAN-hosted race when
// RP412HOSTPODS is set in the environment
//
// LAUNCH while hosting goes out as the lobby's host - the
// roster is primed and the go is published. Otherwise it is
// the plain launch: single player, or a LAN-hosted race when
// RP412HOSTPODS is set in the environment.
//
if (host_launch)
{
gLastLaunchMode = FELaunchHost;
}
return BuildEggFromPersisted(egg_path_out, egg_path_size);
}
//---------------------------------------------------------------
// Steam lobby (host or join), then back here if they leave
// Steam lobby join, then back here if they leave. (Hosting no
// longer exits the menu - the page IS the host's room.)
//---------------------------------------------------------------
int outcome = (steam_action == 1)
? RPL4Lobby_Host(instance, main_window)
: RPL4Lobby_Join(instance, main_window);
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;
+5
View File
@@ -142,6 +142,11 @@ struct FEHostedPilot
// football: the member's own picks, empty = assign one for them
char team[32]; // team key ("Red/Pink", ...)
char position[16]; // "runner" / "crusher" / "blocker"
// the member picked Live Cam: hostType=1, vehicle=camera in the egg.
// Somebody still has to race - the egg builder strips every cam pick
// if honouring them would leave nothing to point a camera at.
Logical camera;
};
void
RPL4FrontEnd_SetHostedPilots(
+195 -62
View File
@@ -13,7 +13,13 @@
Logical RPL4Lobby_Available() { return False; }
Logical RPL4Lobby_Configured() { return False; }
Logical RPL4Lobby_InRoom() { return False; }
int RPL4Lobby_Host(HINSTANCE, HWND) { return LobbyRoomLeft; }
Logical RPL4Lobby_HostOpen() { return False; }
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() { }
@@ -398,6 +404,7 @@ namespace
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')
{
@@ -418,6 +425,94 @@ namespace
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 room screen (front-end style: green on black)
//---------------------------------------------------------------
@@ -890,12 +985,7 @@ namespace
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);
LeaveLobbyNow();
outcome = LobbyRoomLeft;
break;
}
@@ -906,59 +996,11 @@ namespace
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 (OwnerLaunchNow())
{
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;
}
//
// The exact build too. An empty string is an older
// build that predates the key and cannot be trusted to
// match either.
//
if (strcmp(room.members[i].build, RP412_VERSION) != 0)
{
all_same_build = False;
DEBUG_STREAM << "Lobby: " << room.members[i].name
<< " is build '"
<< (room.members[i].build[0] ? room.members[i].build : "(older)")
<< "', we are '" << RP412_VERSION << "'\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;
}
//
@@ -1096,11 +1138,11 @@ Logical
// Available(), but a dead DLL must not depend on the UI for safety.
//
int
RPL4Lobby_Host(HINSTANCE instance, HWND main_window)
RPL4Lobby_HostOpen()
{
if (!SteamNetTransport_ClientLibraryPresent())
if (!SteamNetTransport_ClientLibraryPresent() || gInLobby)
{
return LobbyRoomLeft;
return gInLobby;
}
gCallDone = False;
@@ -1110,15 +1152,106 @@ int
if (!WaitForCall(15000))
{
DEBUG_STREAM << "Lobby: CreateLobby failed\n" << std::flush;
return LobbyRoomLeft;
return False;
}
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);
//
// 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 True;
}
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...)";
const char *note = "";
if (strcmp(members[i].build, RP412_VERSION) != 0)
{
note = " WRONG BUILD";
}
else if (members[i].camera)
{
note = " cam";
}
else if (!members[i].published)
{
note = " ...";
}
_snprintf(lines[written], 47, "%-14s %s%s",
name, members[i].vehicle, note);
lines[written][47] = '\0';
++written;
}
return written;
}
Logical
RPL4Lobby_HostLaunch()
{
if (!gInLobby || !IsOwner())
{
return False;
}
return OwnerLaunchNow();
}
void
RPL4Lobby_Leave()
{
LeaveLobbyNow();
}
int
+42 -2
View File
@@ -46,9 +46,49 @@ Logical
Logical
RPL4Lobby_InRoom();
// Create a lobby / find-and-join one, then run the room screen.
//------------------------------------------------------------------------
// Hosting, from the setup menu.
//
// The host never leaves the configuration page: HostOpen claims the lobby
// and returns immediately, the menu keeps showing (and changing) the
// mission, joiners appear under GAME LENGTH via RosterLines, and LAUNCH
// 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
RPL4Lobby_HostOpen();
// True when we are in a lobby we own.
Logical
RPL4Lobby_IsOwner();
// Run the Steam callbacks once - the menu's timer calls this so member
// joins/leaves and their data show up while no room screen is pumping.
void
RPL4Lobby_Pump();
// Republish our member row and (as owner) the mission setup. Call after
// the host changes anything on the menu, so members' rooms track it.
void
RPL4Lobby_PublishSetup();
// The roster as display lines ("callsign vehicle [cam|WRONG BUILD]").
// Returns the number written, 0 when not in a lobby.
int
RPL4Lobby_Host(HINSTANCE instance, HWND main_window);
RPL4Lobby_RosterLines(char lines[][48], int max_lines);
// The owner's go: verify everyone, publish the roster, prime the hosted
// race. False (and a log line saying who) when the room is not ready.
Logical
RPL4Lobby_HostLaunch();
// Leave the lobby and clear the hosted-race environment.
void
RPL4Lobby_Leave();
// Find-and-join a lobby, then run the room screen (members).
int
RPL4Lobby_Join(HINSTANCE instance, HWND main_window);