Show the host mission setup in both lobbies, and the VTV in football
The room told you who was in it and nothing about what you were about to fly into. Only the owner's menu decides the mission, so nobody else could see the track or the conditions until the race had already started. The owner now publishes its picks as lobby data (track, time of day, weather, game length) and the room draws them under the title: the track on its own line, the conditions under it. Published as display names rather than catalog keys, because the map list differs between race and football and resolving it at the source saves the room from knowing which table a key came from. Football also names the VTV now. The team sheet was team and position only, but the vehicle still decides how someone plays the position, and it was already travelling as member data, just never drawn. Both roster rows now run keys back through the catalogs, so nobody reads bttlbrg where Battle Barge belongs, and the rows widened from the middle half to two thirds to carry three fields. The setup lines are only taken if they fit. Buttons are laid out upward from the bottom edge while the roster runs down from the top, so on a short window the two extra lines would have pushed the roster into MY TEAM. The room measures the gap first and takes two lines, one, or none. Checked against every window size from 3440x1440 down to 640x400 in both scenarios. Verified live on a hosted lobby. Race shows the track, the conditions line, and Battle Barge / Red. Football shows the same setup with Battle Barge, Blue / Aqua, Crusher, tracking the MY TEAM and MY POSITION buttons as they cycle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
//########################################################################
|
||||
|
||||
@@ -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
|
||||
|
||||
+148
-17
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user