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:
+279
-94
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user