diff --git a/RP_L4/RPL4FE.cpp b/RP_L4/RPL4FE.cpp index 1cccdb1..b00ffdf 100644 --- a/RP_L4/RPL4FE.cpp +++ b/RP_L4/RPL4FE.cpp @@ -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, ¬ice, + 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; diff --git a/RP_L4/RPL4FE.h b/RP_L4/RPL4FE.h index 11c50c1..9f8d50c 100644 --- a/RP_L4/RPL4FE.h +++ b/RP_L4/RPL4FE.h @@ -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( diff --git a/RP_L4/RPL4LOBBY.cpp b/RP_L4/RPL4LOBBY.cpp index 2c2d98a..fd03b60 100644 --- a/RP_L4/RPL4LOBBY.cpp +++ b/RP_L4/RPL4LOBBY.cpp @@ -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 diff --git a/RP_L4/RPL4LOBBY.h b/RP_L4/RPL4LOBBY.h index aed69f3..cca2129 100644 --- a/RP_L4/RPL4LOBBY.h +++ b/RP_L4/RPL4LOBBY.h @@ -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);