4 Commits
Author SHA1 Message Date
CydandClaude Fable 5 3a24de03e6 Release 4.12.4
Version string, zip name and README. Cut as its own release rather than
replacing 4.12.3's asset again: the three test machines need to be able
to tell builds apart from what the log and the README say, and a binary
that reports 4.12.3 while carrying the lobby work defeats exactly that.

This one is about the lobby. Both rooms show the host's mission setup,
the football team sheet names each player's VTV, and a lobby holds eight
players rather than four.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 15:38:24 -05:00
CydandClaude Fable 5 e4025c2aec Lobbies hold eight, and the room lays itself out to fit them
The lobby capped at four, which is half a grid. The pod hall raced eight
and the egg builder already carries the owner plus maxExtraPilots, so
eight members fit with room to spare. CreateLobby now asks Steam for
eight and the roster arrays follow.

Checked what eight has to travel through before raising it: the go
roster is 43 bytes a member against an 800 byte buffer, the score sheet
41 against 600, and the pods list 21 against 512, so all three hold
eight with margin. The member side parses the go string with a cursor
rather than a fixed array, and RegisterRoster walks the count it is
given, so neither needed touching.

The layout is the part that could not just be renumbered. Rows were a
fixed sixteenth of the window, and eight of those ran off the bottom of
every window we support - the buttons are laid out upward from the
bottom edge while the roster runs down from the top, so the two meet in
the middle. The room now measures the band between the title and the
topmost button and sizes its rows to it, counted in half rows as

  setup lines + gap + eight rows + gap + hint

so the row height falls out of the space actually available. It never
grows past the old sixteenth, so a two player lobby does not get circus
sized rows, and when the band cannot give eight rows the room the font
needs, the block is drawn in a font that does fit rather than letting
rows overlap.

The minimum row is the font cell plus two, not the cell plus an eighth.
That sounds like a detail and is not: at 1920x1080 the generous version
missed by one pixel and paid for it by dropping the conditions line out
of every football lobby, which is the one thing in the room nobody can
see for themselves.

Verified live on hosted lobbies at 1904x1041, and modelled across every
window size from 3440x1440 down to 800x480 in race and football, host
and member. Race keeps both setup lines everywhere; football, which
carries four stacked buttons, keeps both down to 1080 and drops to one
below that.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 15:17:33 -05:00
CydandClaude Fable 5 43cf086ea4 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>
2026-07-25 15:08:10 -05:00
CydandClaude Fable 5 abd720dd6e Ship the controls map as a page in the dist
The published artifact source now lives in docs/, and pack-dist emits it
as CONTROLS.html beside the plain-text CONTROLS.txt so players get the
diagrams without the repo.

The artifact source is a fragment - the publisher supplies the document
shell - so it is wrapped here with a doctype, charset and viewport.
Without those a browser opens it in quirks mode and renders the
typography as mojibake. Written without a BOM so the charset declaration
is the only thing speaking.

The map itself gains a section for the mouse: the button banks reach
under the glass, and the six displays are the players to size and the
radar to place. Verified the five radar-placement diagrams against the
layout they describe - box 16:9, radar 16.7x39.3% of it, centred flush
to the bottom edge, flush in either bottom corner, and 30/30 vertically
when halfway up a side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 14:39:17 -05:00
7 changed files with 1902 additions and 24 deletions
+9
View File
@@ -41,6 +41,15 @@ radar moved out of the middle of the road (`environ.ini`). Each display's
button bank now reaches under its glass, so the picture itself is the press
target rather than a sliver at the edge.
[v4.12.4](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.4)
is about the lobby. Both rooms now show the host's mission setup — the track,
then time of day, weather and game length — since everyone flies the owner's
picks and it was the one thing in the room nobody could see for themselves.
The football team sheet names each player's VTV beside their team colours and
position. And a lobby holds **eight** players rather than four, a full grid as
the pod hall ran it, with the room sizing its roster to whatever space the
window gives it.
## Playing
Grab the release zip (or run `pack-dist.ps1` on a build). Single player:
+1 -1
View File
@@ -177,7 +177,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
fclose(file);
}
DEBUG_STREAM << "Red Planet 4.12.3" << std::endl << std::flush;
DEBUG_STREAM << "Red Planet 4.12.4" << std::endl << std::flush;
DEBUG_STREAM << "L4CONTROLS=" << getenv("L4CONTROLS") << std::endl << std::flush;
#ifdef RP412_STEAM
+116
View File
@@ -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)
//########################################################################
+27
View File
@@ -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
+196 -19
View File
@@ -36,7 +36,10 @@ void RPL4Lobby_PullRaceResults() { }
namespace
{
enum { kMaxLobbyMembers = 4 };
// A full grid is eight pods, as the pod hall ran it. The egg builder
// carries the owner plus maxExtraPilots (8), so eight members fit
// with room to spare.
enum { kMaxLobbyMembers = 8 };
const char kLobbyTagKey[] = "rp412";
const char kGoKey[] = "go";
@@ -47,6 +50,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 +163,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 +194,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
@@ -346,11 +382,143 @@ namespace
-1, &title, DT_CENTER | DT_TOP | DT_SINGLELINE);
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 buttons are laid out upward from the bottom edge while the
// roster runs down from the top, so everything between the title
// and the topmost button has to share one band.
//---------------------------------------------------------------
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;
}
//---------------------------------------------------------------
// Size the rows to that band rather than to a fixed fraction of
// the window: a full lobby is eight players, and eight rows at a
// sixteenth each would run off the bottom of every window we
// support. Counted in half rows, the band holds
//
// setup lines + gap + kMaxLobbyMembers rows + gap + hint
//
// so the row height falls out of the space actually available.
// It never grows past the old sixteenth (a two-player lobby
// should not have circus-sized rows) and never shrinks below the
// font, which is what would actually break - overlapping text.
//---------------------------------------------------------------
// tmHeight already includes the font's own leading, so a row that
// tall cannot clip or collide - asking for more than that just
// costs setup lines the room would rather keep.
TEXTMETRICA metrics;
GetTextMetricsA(mem, &metrics);
int min_row = metrics.tmHeight + 2;
int max_row = client.bottom / 16;
int setup_top = title.bottom + client.bottom / 64;
int band = ceiling - setup_top;
int setup_lines = 2;
int row_h = 0;
for (;;)
{
// halves: setup, half gap, roster, half gap, hint
int halves = 2 * setup_lines + 1 + 2 * kMaxLobbyMembers + 1 + 2;
row_h = (2 * band) / halves;
if (row_h >= min_row || setup_lines == 0)
{
break;
}
--setup_lines; // buy room back from the setup lines
}
if (row_h > max_row) row_h = max_row;
if (row_h < 12) row_h = 12;
//---------------------------------------------------------------
// On a window too short to give eight rows the room the standard
// font wants, draw this block smaller rather than letting the
// rows overlap each other or run under the buttons. Nobody plays
// at that size, but a squeezed roster still has to be readable.
//---------------------------------------------------------------
HFONT squeezed = NULL;
HFONT previous = NULL;
if (row_h < min_row)
{
squeezed = CreateFontA(-(row_h * 2 / 3), 0, 0, 0, FW_NORMAL,
FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_TT_PRECIS,
CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
FIXED_PITCH | FF_MODERN, "Consolas");
if (squeezed != NULL)
{
previous = (HFONT) SelectObject(mem, squeezed);
}
}
int top = setup_top;
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 +535,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);
}
}
@@ -403,6 +573,13 @@ namespace
: "Waiting for the host to launch...",
-1, &hint, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
// back to the room font for the buttons, which have their own room
if (squeezed != NULL)
{
SelectObject(mem, previous);
DeleteObject(squeezed);
}
HBRUSH frame = CreateSolidBrush(kGreenBright);
//
File diff suppressed because one or more lines are too long
+29 -4
View File
@@ -76,6 +76,28 @@ foreach ($pair in @(
}
Set-Content -Path "$dist\CONTROLS.txt" -Encoding ascii -Value $controls
# The same map as a page, for anyone who would rather look at the
# diagrams than read them. Its source is the published artifact, which is
# a fragment - the publisher supplies the document shell - so wrap it to
# stand alone: without a doctype the browser drops into quirks mode, and
# without a charset the typography arrives as mojibake. Written without a
# BOM so the charset declaration is the only thing speaking.
$controlsPage = Get-Content (Join-Path $root 'docs\rp412-controls.html') -Raw -Encoding UTF8
$page = @"
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
$controlsPage
</body>
</html>
"@
[System.IO.File]::WriteAllText(
"$dist\CONTROLS.html", $page, (New-Object System.Text.UTF8Encoding $false))
# --- OpenAL runtime --------------------------------------------------------
# The exe links OpenAL32.dll (32-bit). Prefer shipping the already-installed
# runtime beside the exe; oalinst.exe covers machines where that misses.
@@ -305,7 +327,7 @@ start rpl4opt.exe -fit
"@
Set-Content -Path "$dist\README.txt" -Encoding ascii -Value @"
Red Planet 4.12.3
Red Planet 4.12.4
=================
Run start-fullscreen.bat for borderless over the whole monitor, or
@@ -352,8 +374,11 @@ pod's real button banks: click them with the mouse, and they light up
as the game commands their lamps.
environ.ini is self-documenting: every option ships in the file with
a comment (Steam networking, keyboard lighting, stick inversion, LAN
hosting, developer keys, and more). CONTROLS.txt has the full controls
map with pad and keyboard diagrams.
hosting, developer keys, display scaling and radar placement, and more).
CONTROLS.html is the full controls map - open it in a browser for the
pad, keyboard and pod-panel diagrams. CONTROLS.txt is the same thing as
plain text.
Known prototype notes: pods race untextured (the player1-8 skins come
from the presets system, not shipped data), and text drawn on the plasma
@@ -367,7 +392,7 @@ $size = (Get-ChildItem $dist -Recurse | Measure-Object Length -Sum).Sum
Write-Host ("dist ready: {0:N1} MB" -f ($size / 1MB))
if ($Zip) {
$zipPath = Join-Path $root 'RedPlanet-4.12.3.zip'
$zipPath = Join-Path $root 'RedPlanet-4.12.4.zip'
Write-Host "zipping to $zipPath..."
# Everything lives under a single RP412\ folder inside the zip, so