diff --git a/RP_L4/RPL4FE.cpp b/RP_L4/RPL4FE.cpp index b00ffdf..c6b7442 100644 --- a/RP_L4/RPL4FE.cpp +++ b/RP_L4/RPL4FE.cpp @@ -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 } } diff --git a/RP_L4/RPL4LOBBY.cpp b/RP_L4/RPL4LOBBY.cpp index eac5427..48c7289 100644 --- a/RP_L4/RPL4LOBBY.cpp +++ b/RP_L4/RPL4LOBBY.cpp @@ -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 diff --git a/RP_L4/RPL4LOBBY.h b/RP_L4/RPL4LOBBY.h index cca2129..68b92a1 100644 --- a/RP_L4/RPL4LOBBY.h +++ b/RP_L4/RPL4LOBBY.h @@ -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();