diff --git a/RP_L4/RPL4FE.cpp b/RP_L4/RPL4FE.cpp index 9be2abf..f8643f3 100644 --- a/RP_L4/RPL4FE.cpp +++ b/RP_L4/RPL4FE.cpp @@ -1380,6 +1380,122 @@ void strcpy(badge, kBadges[b].key); } +//######################################################################## +// The host's mission setup, as display names. +// +// The lobby shows everyone what they are about to fly into, and only the +// owner knows it - so the owner publishes these into lobby data and the +// members read them back. Names rather than catalog keys: the map lists +// differ between race and football, so resolving here means the room +// never has to know which scenario's table a key came from. +//######################################################################## + +const char * + RPL4FrontEnd_SelectedMapName() +{ + if (!gHavePersist) + { + return kMaps[0].name; + } + + int count = 0; + const CatalogEntry *maps = ActiveMaps(gPersistSelection, &count); + int index = gPersistSelection[GroupMap]; + if (index < 0 || index >= count) + { + index = 0; + } + return maps[index].name; +} + +const char * + RPL4FrontEnd_SelectedTimeName() +{ + int index = gHavePersist ? gPersistSelection[GroupTime] : 0; + if (index < 0 || index >= FE_COUNT(kTimes)) + { + index = 0; + } + return kTimes[index].name; +} + +const char * + RPL4FrontEnd_SelectedWeatherName() +{ + int index = gHavePersist ? gPersistSelection[GroupWeather] : 0; + if (index < 0 || index >= FE_COUNT(kWeather)) + { + index = 0; + } + return kWeather[index].name; +} + +const char * + RPL4FrontEnd_SelectedLengthName() +{ + // the menu defaults this one to 5:00, not to entry zero + int index = gHavePersist ? gPersistSelection[GroupLength] : 2; + if (index < 0 || index >= FE_COUNT(kLengths)) + { + index = 0; + } + return kLengths[index].name; +} + +//######################################################################## +// Catalog key -> display name. Member data travels as keys, so the room +// needs these to show "Battle Barge" where the wire says "bttlbrg". +//######################################################################## + +namespace +{ + const char * + NameForKey(const CatalogEntry *table, int count, const char *key) + { + if (key == NULL || key[0] == '\0') + { + return ""; + } + for (int i = 0; i < count; ++i) + { + if (strcmp(table[i].key, key) == 0) + { + return table[i].name; + } + } + return key; // unknown to this build: show it raw rather than blank + } +} + +const char * + RPL4FrontEnd_VehicleNameForKey(const char *key) +{ + return NameForKey(kVehicles, FE_COUNT(kVehicles), key); +} + +const char * + RPL4FrontEnd_PositionNameForKey(const char *key) +{ + return NameForKey(kPositions, FE_COUNT(kPositions), key); +} + +const char * + RPL4FrontEnd_TeamNameForKey(const char *key) +{ + if (key == NULL || key[0] == '\0') + { + return ""; + } + for (int i = 0; i < FE_COUNT(kTeams); ++i) + { + if (strcmp(kTeams[i].key, key) == 0) + { + return kTeams[i].name; + } + } + return key; +} + //######################################################################## // Football team / position picks (the lobby publishes and cycles these) //######################################################################## diff --git a/RP_L4/RPL4FE.h b/RP_L4/RPL4FE.h index fb30715..b6bf02c 100644 --- a/RP_L4/RPL4FE.h +++ b/RP_L4/RPL4FE.h @@ -50,6 +50,33 @@ int void RPL4FrontEnd_GetLoadout(char *vehicle, char *color, char *badge); +//--------------------------------------------------------------- +// This player's mission setup as display names. Only the host's +// picks decide the mission, so the lobby owner publishes these for +// everyone else to read - names rather than keys, because the map +// catalog differs between race and football and resolving it here +// saves the room from knowing which table a key belongs to. +//--------------------------------------------------------------- +const char * + RPL4FrontEnd_SelectedMapName(); +const char * + RPL4FrontEnd_SelectedTimeName(); +const char * + RPL4FrontEnd_SelectedWeatherName(); +const char * + RPL4FrontEnd_SelectedLengthName(); + +//--------------------------------------------------------------- +// Catalog key -> display name, for the keys that travel as lobby +// member data. Unknown keys come back unchanged rather than blank. +//--------------------------------------------------------------- +const char * + RPL4FrontEnd_VehicleNameForKey(const char *key); +const char * + RPL4FrontEnd_PositionNameForKey(const char *key); +const char * + RPL4FrontEnd_TeamNameForKey(const char *key); + //--------------------------------------------------------------- // Football team / position: the catalogs, and this player's picks. // The lobby publishes the picks as member data and lets members diff --git a/RP_L4/RPL4LOBBY.cpp b/RP_L4/RPL4LOBBY.cpp index 575b70d..b79e916 100644 --- a/RP_L4/RPL4LOBBY.cpp +++ b/RP_L4/RPL4LOBBY.cpp @@ -47,6 +47,12 @@ namespace const char kResultsKey[] = "res"; const char kScenarioKey[] = "sc"; + // the owner's mission setup, shown to everyone in the room + const char kMapKey[] = "mp"; + const char kTimeKey[] = "td"; + const char kWeatherKey[] = "wx"; + const char kLengthKey[] = "ln"; + // async call-result plumbing Logical gCallDone = False; Logical gCallOK = False; @@ -154,12 +160,24 @@ namespace SteamMatchmaking()->SetLobbyMemberData(gLobby, "ps", RPL4FrontEnd_PositionKey(RPL4FrontEnd_GetPositionIndex())); - // the owner also publishes the scenario, so members know - // whether their team/position pick matters + //--------------------------------------------------------------- + // Only the owner's menu decides the mission, so the owner also + // publishes what it picked: the scenario (members need it to know + // whether their team/position pick matters) and the setup + // everyone is about to fly into. + //--------------------------------------------------------------- if (IsOwner()) { SteamMatchmaking()->SetLobbyData(gLobby, kScenarioKey, RPL4FrontEnd_IsFootballSelected() ? "football" : "race"); + SteamMatchmaking()->SetLobbyData(gLobby, kMapKey, + RPL4FrontEnd_SelectedMapName()); + SteamMatchmaking()->SetLobbyData(gLobby, kTimeKey, + RPL4FrontEnd_SelectedTimeName()); + SteamMatchmaking()->SetLobbyData(gLobby, kWeatherKey, + RPL4FrontEnd_SelectedWeatherName()); + SteamMatchmaking()->SetLobbyData(gLobby, kLengthKey, + RPL4FrontEnd_SelectedLengthName()); } } @@ -173,6 +191,21 @@ namespace return (scenario != NULL && strcmp(scenario, "football") == 0); } + //------------------------------------------------------------------- + // Lobby data read back as a string, empty when the owner has not + // published it yet (a member can be in the room a beat before the + // owner's first publish lands). + //------------------------------------------------------------------- + const char *LobbyText(const char *key) + { + if (!gInLobby) + { + return ""; + } + const char *text = SteamMatchmaking()->GetLobbyData(gLobby, key); + return (text != NULL) ? text : ""; + } + //--------------------------------------------------------------- // Launch: roster into lobby data, peers into the transport, and // the hosted-race path primed on the owner @@ -347,10 +380,106 @@ namespace SelectObject(mem, room->textFont); int row_h = client.bottom / 16; - int top = client.bottom / 4; - int left = client.right / 4; - int right = 3 * client.right / 4; + // wider than the old middle-half: the right column now carries + // full catalog names, and in football three of them + int left = client.right / 6; + int right = client.right - client.right / 6; char text[128]; + + //--------------------------------------------------------------- + // The host's setup, under the title: the track on its own line, + // then the conditions. Everyone flies the owner's picks, so this + // is the one thing in the room nobody can see for themselves. + // + // Drawn full width rather than the roster's middle half, because + // track names run long ("Berserker's Bahnzai"), and the roster + // then starts below whatever this took - two extra lines above a + // fixed roster top would have run into the first player. + //--------------------------------------------------------------- + int top = client.bottom / 4; + + //--------------------------------------------------------------- + // The buttons are laid out upward from the bottom edge, so the + // setup lines are only affordable if the roster and its hint + // still clear the topmost one. Work out the room first and take + // as many lines as fit - the map alone is worth more than the + // conditions, so it goes first and drops last. + //--------------------------------------------------------------- + int ceiling = client.bottom; + if (FootballLobby() && room->teamRect.top > 0) + { + ceiling = room->teamRect.top; + } + else if (IsOwner() && room->launchRect.top > 0) + { + ceiling = room->launchRect.top; + } + else if (room->leaveRect.top > 0) + { + ceiling = room->leaveRect.top; + } + + int setup_top = title.bottom + row_h / 4; + // the fixed block below: four roster slots, a gap, and the hint, + // plus the half row this block leaves above the roster + int block = kMaxLobbyMembers * row_h + row_h / 2 + row_h; + int setup_lines = + (ceiling - block - setup_top - row_h / 2) / row_h; + if (setup_lines > 2) + { + setup_lines = 2; + } + + const char *map_name = LobbyText(kMapKey); + if (map_name[0] != '\0' && setup_lines > 0) + { + RECT setup; + setup.left = client.right / 12; + setup.right = client.right - client.right / 12; + setup.top = setup_top; + setup.bottom = setup.top + row_h; + + SetTextColor(mem, kGreenBright); + DrawTextA(mem, map_name, -1, &setup, + DT_CENTER | DT_VCENTER | DT_SINGLELINE); + + //----------------------------------------------------------- + // Conditions on the line below. Game length is the host's + // pick too, and the one people ask about first. + //----------------------------------------------------------- + const char *time_name = LobbyText(kTimeKey); + const char *weather_name = LobbyText(kWeatherKey); + const char *length_name = LobbyText(kLengthKey); + text[0] = '\0'; + if (time_name[0] != '\0') + { + strcat(text, time_name); + } + if (weather_name[0] != '\0') + { + if (text[0] != '\0') strcat(text, " - "); + strcat(text, weather_name); + } + if (length_name[0] != '\0') + { + if (text[0] != '\0') strcat(text, " - "); + strcat(text, length_name); + } + if (text[0] != '\0' && setup_lines > 1) + { + setup.top = setup.bottom; + setup.bottom = setup.top + row_h; + SetTextColor(mem, kGreenDim); + DrawTextA(mem, text, -1, &setup, + DT_CENTER | DT_VCENTER | DT_SINGLELINE); + } + + if (setup.bottom + row_h / 2 > top) + { + top = setup.bottom + row_h / 2; + } + } + for (int i = 0; i < room->memberCount; ++i) { MemberInfo *member = &room->members[i]; @@ -367,27 +496,29 @@ namespace is_owner_row ? " [HOST]" : ""); DrawTextA(mem, text, -1, &row, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + //----------------------------------------------------------- + // The right column is what this player brings. Football is a + // team sheet - team colors and position - but the VTV still + // decides how they play it, so name it there too. Keys + // travel on the wire; the catalogs turn them back into + // names, so nobody reads "bttlbrg". + //----------------------------------------------------------- if (FootballLobby()) { - // football: the roster is a team sheet if (member->team[0] != '\0') { - const char *position_name = "?"; - for (int i = 0; i < RPL4FrontEnd_PositionCount(); ++i) - { - if (strcmp(member->position, RPL4FrontEnd_PositionKey(i)) == 0) - { - position_name = RPL4FrontEnd_PositionName(i); - break; - } - } - sprintf(text, "%s - %s", member->team, position_name); + sprintf(text, "%s - %s - %s", + RPL4FrontEnd_VehicleNameForKey(member->vehicle), + RPL4FrontEnd_TeamNameForKey(member->team), + RPL4FrontEnd_PositionNameForKey(member->position)); DrawTextA(mem, text, -1, &row, DT_RIGHT | DT_VCENTER | DT_SINGLELINE); } } else if (member->vehicle[0] != '\0') { - sprintf(text, "%s / %s", member->vehicle, member->color); + sprintf(text, "%s - %s", + RPL4FrontEnd_VehicleNameForKey(member->vehicle), + member->color); DrawTextA(mem, text, -1, &row, DT_RIGHT | DT_VCENTER | DT_SINGLELINE); } }