Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a24de03e6 | ||
|
|
e4025c2aec | ||
|
|
43cf086ea4 | ||
|
|
abd720dd6e |
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+196
-19
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user