Files
RP412/RP_L4/RPL4FE.cpp
T
CydandClaude Opus 5 b3ed7bc141 The drop-down boxes stop being Windows-coloured
CBS_OWNERDRAWFIXED only hands over the item area, so while the list rows
came out green on black, the closed box kept the system's frame and drop
arrow - a white/grey Windows control sitting in the middle of a black panel.

The closed box is painted here now: black field, dim green border, bright
green text, and a plain green triangle instead of a themed button. The
control keeps doing everything else, including dropping its list, so this is
a subclass over WM_PAINT rather than a reimplementation.

Still system-drawn: the scrollbar inside a dropped list, which only appears
on the two lists longer than twelve rows - vehicle and track.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 09:55:10 -05:00

2680 lines
75 KiB
C++

#include "..\munga_l4\mungal4.h"
#pragma hdrstop
#include "rpl4fe.h"
#include "rpl4console.h"
#include "rpl4lobby.h"
#include <stdio.h>
#include <string>
//########################################################################
// The Death Race catalog (from TeslaConsole's RedPlanet/RPConfig.xml;
// 'headmf' is excluded for the race scenario there).
//########################################################################
namespace
{
struct CatalogEntry
{
const char *key;
const char *name;
};
const CatalogEntry kMaps[] =
{
{ "wise", "Wiseguy's Wake" },
{ "lyzlane", "Lyz's Lane" },
{ "yip", "Yip's Yahoorama" },
{ "blade", "Blade's Edge" },
{ "otto", "Berserker's Bahnzai" },
{ "frstrm", "Firestorm's Fury" },
{ "burnt", "Burnt Cookies" },
{ "brewers", "Brewer's Bane" },
{ "pain", "Paingod's Passage" },
{ "headoff", "Freezemoon's Freeway" },
// Maps that came with the promoted resource file. The console
// config brackets their names in dashes, which is how it marks
// the ones that are not original arcade tracks.
{ "arena", "-- Arena (Small) --" },
{ "demoderb", "-- Arena (Large) --" },
{ "donut", "-- Donut --" },
{ "tourdemars", "-- Tour De Mars --" },
{ "trough", "-- Ares' Armpits --" },
{ "wiserguys", "-- Wiserguy's --" },
{ "rpweekend2014", "-- Zaxxis --" },
};
// Football excludes pain/headoff and adds the football build of
// Freezemoon's Freeway (RPConfig.xml per-scenario invalid lists).
const CatalogEntry kFootballMaps[] =
{
{ "wise", "Wiseguy's Wake" },
{ "lyzlane", "Lyz's Lane" },
{ "yip", "Yip's Yahoorama" },
{ "blade", "Blade's Edge" },
{ "otto", "Berserker's Bahnzai" },
{ "frstrm", "Firestorm's Fury" },
{ "burnt", "Burnt Cookies" },
{ "brewers", "Brewer's Bane" },
{ "headmf", "Freezemoon's Freeway" },
};
const CatalogEntry kScenarios[] =
{
{ "race", "Death Race" },
{ "football", "Football" },
};
// Two-color teams: crushers/blockers wear the team color, the
// runner wears the runner color (that is how you spot the ball).
struct TeamEntry
{
const char *key;
const char *name;
const char *teamColor;
const char *runnerColor;
};
const TeamEntry kTeams[] =
{
{ "Red/Pink", "Red / Pink", "Red", "Pink" },
{ "Blue/Aqua", "Blue / Aqua", "Blue", "Aqua" },
{ "Green/Yellow", "Green / Yellow", "Green", "Yellow" },
{ "Black/Purple", "Black / Purple", "Black", "Purple" },
};
const CatalogEntry kPositions[] =
{
{ "runner", "Runner" },
{ "crusher", "Crusher" },
{ "blocker", "Blocker" },
};
const CatalogEntry kVehicles[] =
{
{ "quark", "Quark" },
{ "lepton", "Lepton" },
{ "proton", "Proton" },
{ "bug", "Bug" },
{ "speck", "Speck" },
{ "blkspk", "Black Speck" },
{ "chigger", "Chigger" },
{ "flea", "Flea" },
{ "roach", "Roach" },
{ "skeeter", "Skeeter" },
{ "wasp", "Wasp" },
{ "barge", "Barge" },
{ "broccoli", "Screaming Broccoli" },
{ "burro", "Burro" },
{ "mule", "Mule" },
{ "bttlbrg", "Battle Barge" },
{ "gator", "Gator" },
{ "grunt", "Grunt" },
{ "vole", "Vole" },
{ "bull", "Bull" },
{ "tarntula", "Tarantula" },
{ "mantis", "Mantis" },
{ "ripper", "Ripper" },
{ "roadblk", "Road Block" },
{ "spitter", "Spitter" },
{ "puck", "Armadillo" },
{ "dragon", "Dragon" },
// Restored with the promoted resource file: cut from the .bld
// after 4.10 shipped, back now that RPL4.RES carries them again.
// Names from the console's own RPConfig.xml.
{ "dark", "Blacker Puck" },
{ "blktrn", "Black Tarantula" },
{ "neut", "Neutrino" },
// Community variants that came with the same archive.
{ "blkrpuck", "Blacker Armadillo" },
{ "blkrbroc", "Blacker Broccoli" },
{ "blkrdragon", "Blacker Dragon" },
{ "blkrlepton", "Blacker Lepton" },
{ "blkrquark", "Blacker Quark" },
{ "blkrspk", "Blacker Speck" },
{ "blkrtran", "Blacker Tarantula" },
{ "blkrwasp", "Blacker Wasp" },
};
const CatalogEntry kColors[] =
{
{ "Red", "Red" }, { "Blue", "Blue" }, { "Green", "Green" },
{ "White", "White" }, { "Pink", "Pink" }, { "Aqua", "Aqua" },
{ "Yellow", "Yellow" }, { "Purple", "Purple" }, { "Black", "Black" },
};
const CatalogEntry kBadges[] =
{
{ "None", "None" }, { "Celtic", "Celtic" }, { "Desert", "Desert" },
{ "Flames", "Flames" }, { "Giraffe", "Giraffe" }, { "Hawaii", "Hawaii" },
{ "Korean", "Korean Camo" }, { "Lightning", "Lightning" },
{ "Razzle Dazzle", "Razzle Dazzle" }, { "Rising Sun", "Rising Sun" },
{ "Tiger Stripe", "Tiger Stripes" },
};
const CatalogEntry kTimes[] =
{
{ "day", "Day" }, { "night", "Night" },
};
const CatalogEntry kWeather[] =
{
{ "clear", "Clear" }, { "fog", "Light Fog" }, { "soup", "Heavy Fog" },
};
struct LengthEntry
{
int seconds; // 0 = endless (length line omitted)
const char *name;
};
const LengthEntry kLengths[] =
{
{ 0, "Endless" },
{ 180, "3:00" },
{ 300, "5:00" },
{ 480, "8:00" },
{ 600, "10:00" },
};
// Standalone pilot address, as used by TEST.EGG: single-user mode
// adopts the first pilot entry without resolving it.
const char kLocalPilotAddress[] = "200.0.0.255";
#define FE_COUNT(t) ((int)(sizeof(t)/sizeof((t)[0])))
//---------------------------------------------------------------
// Menu model
//---------------------------------------------------------------
enum FEGroup
{
GroupMap = 0,
GroupVehicle,
GroupColor,
GroupBadge,
GroupTime,
GroupWeather,
GroupLength,
GroupScenario,
GroupTeam, // football only
GroupPosition, // football only
GroupLaunch,
GroupSteamHost, // buttons, not selections (Steam builds only)
GroupSteamJoin,
GroupExit,
GroupCount
};
Logical IsFootball(const int *selection)
{
return selection[GroupScenario] == 1;
}
const CatalogEntry *ActiveMaps(const int *selection, int *count)
{
if (IsFootball(selection))
{
*count = (int)(sizeof(kFootballMaps) / sizeof(kFootballMaps[0]));
return kFootballMaps;
}
*count = (int)(sizeof(kMaps) / sizeof(kMaps[0]));
return kMaps;
}
struct FEItem
{
int group;
int index;
RECT rect;
};
struct FEState
{
int selection[GroupCount];
char pilotName[24];
FEItem items[128];
int itemCount;
HWND menuWindow;
HWND nameEdit;
// Every list group is a drop-down. They are owner-drawn so they
// keep the green-on-black panel look instead of arriving in
// system colours, and their labels are painted by PaintMenu.
HWND combo[GroupCount];
RECT comboLabel[GroupCount];
HFONT textFont;
HFONT titleFont;
HBRUSH editBrush;
Logical launched;
Logical closed;
int steamAction; // 1 = host lobby, 2 = join lobby
};
FEState *gFE = NULL;
// ItemName() reads this to answer with the right track list.
const int *gItemNameSelection = NULL;
int gLastMissionSeconds = 0;
// carried across races so cycling back to the menu keeps the
// player's loadout (and names the results screen's score rows)
int gPersistSelection[GroupCount];
Logical gHavePersist = False;
char gLastPilotName[24] = "Pilot";
//---------------------------------------------------------------
// What the player set last time, in pilot.cfg beside bindings.txt:
// the callsign they typed and the loadout they picked.
//
// The loadout has always survived a race - gPersistSelection below
// is why the menu reopens the way you left it - but only for as
// long as the process lives. BT411 keeps the same things in
// fe_last.ini and had to, since it relaunches between missions;
// RP412 stayed in one process and so never needed a file. Closing
// the game was still a reset, which is what this fixes.
//
// KEY=VALUE like environ.ini, one line per group.
//---------------------------------------------------------------
const char kPilotFileName[] = "pilot.cfg";
//
// Stable file keys, independent of the on-screen headings - those
// carry spaces ("TIME OF DAY") and are free to be reworded.
//
struct GroupKey
{
int group;
const char *key;
};
const GroupKey kGroupKeys[] =
{
{ GroupScenario, "scenario" }, { GroupMap, "track" },
{ GroupVehicle, "vehicle" }, { GroupColor, "color" },
{ GroupBadge, "badge" }, { GroupTeam, "team" },
{ GroupPosition, "position" }, { GroupTime, "time" },
{ GroupWeather, "weather" }, { GroupLength, "length" },
};
//
// How many rows a group offers, so a stale or hand-edited index
// cannot select past the end of a list. The track list is the one
// that moves - football and the death race carry different maps -
// so it answers for whichever scenario is selected.
//
int GroupSize(int group, const int *selection)
{
switch (group)
{
case GroupScenario: return FE_COUNT(kScenarios);
case GroupMap:
{
int count = 0;
ActiveMaps(selection, &count);
return count;
}
case GroupVehicle: return FE_COUNT(kVehicles);
case GroupColor: return FE_COUNT(kColors);
case GroupBadge: return FE_COUNT(kBadges);
case GroupTeam: return FE_COUNT(kTeams);
case GroupPosition: return FE_COUNT(kPositions);
case GroupTime: return FE_COUNT(kTimes);
case GroupWeather: return FE_COUNT(kWeather);
case GroupLength: return FE_COUNT(kLengths);
}
return 0;
}
//
// A callsign is quoted into frontend.egg, joined into a
// comma-separated list for the results screen, and published as
// Steam lobby member data. Anything that could end a token early
// therefore has to go - a comma alone would split one pilot into
// two on the score sheet. Applied to what is typed as well as to
// what is read back, so the file cannot hold what the game will
// not accept.
//
void SanitizeCallsign(char *name, int size)
{
char clean[64];
int out = 0;
for (int i = 0; name[i] != '\0' && out < (int) sizeof(clean) - 1; ++i)
{
unsigned char c = (unsigned char) name[i];
if (c < 32 || c > 126) continue; // controls, high bytes
if (c == ',' || c == '"') continue; // egg and CSV delimiters
if (c == '#' || c == ';') continue; // pilot.cfg comment marks
clean[out++] = (char) c;
}
clean[out] = '\0';
char *start = clean;
while (*start == ' ' || *start == '\t')
{
++start;
}
int end = (int) strlen(start);
while (end > 0 && (start[end - 1] == ' ' || start[end - 1] == '\t'))
{
start[--end] = '\0';
}
if (start[0] == '\0')
{
strcpy(start, "Pilot");
}
strncpy(name, start, size - 1);
name[size - 1] = '\0';
}
void SavePilotSettings(const char *name, const int *selection)
{
FILE *file = fopen(kPilotFileName, "wt");
if (file == NULL)
{
DEBUG_STREAM << "FrontEnd: could not write " << kPilotFileName
<< "\n" << std::flush;
return;
}
fputs("# RP412 pilot settings, written by the game on the way out of\n"
"# the setup screen. Delete this file to start over.\n", file);
fprintf(file, "callsign=%s\n", name);
if (selection != NULL)
{
for (int i = 0; i < FE_COUNT(kGroupKeys); ++i)
{
fprintf(file, "%s=%d\n", kGroupKeys[i].key,
selection[kGroupKeys[i].group]);
}
}
fclose(file);
DEBUG_STREAM << "FrontEnd: saved callsign \"" << name << "\""
<< ((selection != NULL) ? " and loadout" : "")
<< " to " << kPilotFileName << "\n" << std::flush;
}
//
// Read once per run. Absent or unreadable simply leaves the
// built-in defaults in place - a missing file is a first run, not
// an error, and every value is range-checked so a hand-edited or
// out-of-date file cannot select past the end of a list.
//
void EnsurePilotSettingsLoaded()
{
static Logical loaded = False;
if (loaded)
{
return;
}
loaded = True;
FILE *file = fopen(kPilotFileName, "rt");
if (file == NULL)
{
return;
}
int selection[GroupCount];
memset(selection, 0, sizeof(selection));
selection[GroupLength] = 2; // 5:00, as the menu defaults
Logical have_loadout = False;
char line[256];
while (fgets(line, sizeof(line), file) != NULL)
{
char *cursor = line;
while (*cursor == ' ' || *cursor == '\t')
{
++cursor;
}
if (*cursor == '#' || *cursor == ';')
{
continue;
}
char *equals = strchr(cursor, '=');
if (equals == NULL)
{
continue;
}
*equals = '\0';
char *key = cursor;
char *value = equals + 1;
while (*value == ' ' || *value == '\t')
{
++value;
}
char *newline = strpbrk(value, "\r\n");
if (newline != NULL)
{
*newline = '\0';
}
if (_stricmp(key, "callsign") == 0)
{
char candidate[24];
strncpy(candidate, value, sizeof(candidate) - 1);
candidate[sizeof(candidate) - 1] = '\0';
SanitizeCallsign(candidate, sizeof(candidate));
strcpy(gLastPilotName, candidate);
continue;
}
for (int i = 0; i < FE_COUNT(kGroupKeys); ++i)
{
if (_stricmp(key, kGroupKeys[i].key) != 0)
{
continue;
}
int index = atoi(value);
if (index >= 0 && index < GroupSize(kGroupKeys[i].group, selection))
{
selection[kGroupKeys[i].group] = index;
have_loadout = True;
}
break;
}
}
fclose(file);
//
// The track list belongs to the scenario, and the file is read in
// whatever order it was written, so re-check the track once the
// scenario is settled - the same clamp the menu applies when the
// scenario is switched by hand.
//
if (have_loadout)
{
int map_count = 0;
ActiveMaps(selection, &map_count);
if (selection[GroupMap] >= map_count)
{
selection[GroupMap] = 0;
}
memcpy(gPersistSelection, selection, sizeof(gPersistSelection));
gHavePersist = True;
}
DEBUG_STREAM << "FrontEnd: callsign \"" << gLastPilotName << "\""
<< (have_loadout ? " and loadout" : "")
<< " from " << kPilotFileName << "\n" << std::flush;
}
// [pilots]-order names of the last launched race (owner first),
// comma separated - the network console labels results with them
char gLastPilotNamesCsv[256] = "";
//---------------------------------------------------------------
// Multiplayer host mode (lobby stand-in): RP412HOSTPODS lists the
// member pods' console channels ("ip[:port]" comma separated) and
// the egg gains one pilot per member. RP412HOSTPORT is the
// owner's console port (default 1501; game port is +1) and
// RP412HOSTADDR the owner's mesh address (default 127.0.0.1 -
// the Steam lobby supplies the FakeIP instead).
//---------------------------------------------------------------
enum { maxExtraPilots = 8 };
struct ExtraPilot
{
char address[64]; // mesh (game port) address for [pilots]
char name[32];
char vehicle[24];
char color[16];
char badge[24];
char team[32]; // football pick, "" = assign for them
char position[16];
};
// lobby-fed override (real personas + loadouts); empty = env parsing
FEHostedPilot gHostedPilots[maxExtraPilots];
int gHostedPilotCount = 0;
char gHostedOwnerAddress[64] = "";
int CollectExtraPilots(const FEState *fe, ExtraPilot *extras, char *owner_address)
{
if (gHostedPilotCount > 0)
{
sprintf(owner_address, "%s:1502", gHostedOwnerAddress);
for (int i = 0; i < gHostedPilotCount; ++i)
{
strcpy(extras[i].address, gHostedPilots[i].address);
strcpy(extras[i].name,
gHostedPilots[i].name[0] ? gHostedPilots[i].name : "Pilot");
strcpy(extras[i].vehicle,
gHostedPilots[i].vehicle[0] ? gHostedPilots[i].vehicle : "speck");
strcpy(extras[i].color,
gHostedPilots[i].color[0] ? gHostedPilots[i].color : "Red");
strcpy(extras[i].badge,
gHostedPilots[i].badge[0] ? gHostedPilots[i].badge : "None");
strcpy(extras[i].team, gHostedPilots[i].team);
strcpy(extras[i].position, gHostedPilots[i].position);
}
return gHostedPilotCount;
}
const char *host_pods = getenv("RP412HOSTPODS");
if (host_pods == NULL || host_pods[0] == '\0')
{
return -1; // single player: standalone address
}
int host_port = 1501;
const char *port_override = getenv("RP412HOSTPORT");
if (port_override != NULL && atoi(port_override) > 0)
{
host_port = atoi(port_override);
}
const char *host_address = getenv("RP412HOSTADDR");
if (host_address == NULL || host_address[0] == '\0')
{
host_address = "127.0.0.1";
}
sprintf(owner_address, "%s:%d", host_address, host_port + 1);
int extra_count = 0;
const char *cursor = host_pods;
while (*cursor != '\0' && extra_count < maxExtraPilots)
{
char entry[64];
int length = 0;
while (cursor[length] != '\0' && cursor[length] != ',' &&
length < (int) sizeof(entry) - 1)
{
entry[length] = cursor[length];
++length;
}
entry[length] = '\0';
cursor += length;
if (*cursor == ',')
{
++cursor;
}
int console_port = 1501;
char *colon = strrchr(entry, ':');
if (colon != NULL)
{
*colon = '\0';
console_port = atoi(colon + 1);
}
ExtraPilot *pilot = &extras[extra_count];
memset(pilot, 0, sizeof(*pilot));
sprintf(pilot->address, "%s:%d", entry, console_port + 1);
sprintf(pilot->name, "PILOT %d", extra_count + 2);
strcpy(pilot->vehicle, kVehicles[
(fe->selection[GroupVehicle] + extra_count + 1) % FE_COUNT(kVehicles)].key);
strcpy(pilot->color, kColors[
(fe->selection[GroupColor] + extra_count + 1) % FE_COUNT(kColors)].key);
strcpy(pilot->badge, "None");
++extra_count;
}
return extra_count;
}
const COLORREF kGreenBright = RGB(64, 255, 64);
const COLORREF kGreenDim = RGB(24, 140, 24);
const COLORREF kBlack = RGB(0, 0, 0);
//---------------------------------------------------------------
// Layout: three columns of lists + the launch button
//---------------------------------------------------------------
//---------------------------------------------------------------
// The groups that became drop-downs, in the order they are laid
// out. Scenario is deliberately NOT one of them: it decides what
// the others contain, so it stays visible as a pair of buttons.
//---------------------------------------------------------------
int ComboGroups(const int *selection, int *groups)
{
int n = 0;
groups[n++] = GroupMap;
groups[n++] = GroupTime;
groups[n++] = GroupWeather;
groups[n++] = GroupLength;
groups[n++] = GroupVehicle;
if (IsFootball(selection))
{
groups[n++] = GroupTeam;
groups[n++] = GroupPosition;
}
else
{
groups[n++] = GroupColor;
groups[n++] = GroupBadge;
}
return n;
}
// The combo id is its group, so CBN_SELCHANGE says which list moved.
const int kComboIdBase = 100;
//---------------------------------------------------------------
// CBS_OWNERDRAWFIXED only hands us the item area: the frame around
// the closed box and its drop arrow are still drawn by the system,
// which lands a white/grey Windows control in the middle of a black
// panel. So the closed box is painted here instead - black field,
// dim green border, bright green text and arrow - and the control
// keeps doing everything else, including the list it drops.
//---------------------------------------------------------------
WNDPROC gComboProc = NULL;
LRESULT CALLBACK ComboSubclassProc(
HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
if (message == WM_ERASEBKGND)
{
return 1;
}
if (message == WM_PAINT)
{
PAINTSTRUCT ps;
HDC dc = BeginPaint(hwnd, &ps);
RECT rc;
GetClientRect(hwnd, &rc);
HBRUSH field = CreateSolidBrush(kBlack);
FillRect(dc, &rc, field);
DeleteObject(field);
HBRUSH edge = CreateSolidBrush(kGreenDim);
FrameRect(dc, &rc, edge);
DeleteObject(edge);
int arrow_w = rc.bottom - rc.top;
char text[128];
text[0] = '\0';
int sel = (int) SendMessageA(hwnd, CB_GETCURSEL, 0, 0);
if (sel >= 0)
{
SendMessageA(hwnd, CB_GETLBTEXT, sel, (LPARAM) text);
}
HFONT font = (HFONT) SendMessageA(hwnd, WM_GETFONT, 0, 0);
HGDIOBJ old_font = (font != NULL) ? SelectObject(dc, font) : NULL;
SetBkMode(dc, TRANSPARENT);
SetTextColor(dc, kGreenBright);
RECT label = rc;
label.left += 6;
label.right -= arrow_w;
DrawTextA(dc, text, -1, &label,
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
// the arrow, a plain triangle rather than a themed button
int cx = rc.right - arrow_w / 2;
int cy = (rc.top + rc.bottom) / 2;
int r = arrow_w / 6;
POINT tri[3];
tri[0].x = cx - r; tri[0].y = cy - r / 2;
tri[1].x = cx + r; tri[1].y = cy - r / 2;
tri[2].x = cx; tri[2].y = cy + r;
HBRUSH tip = CreateSolidBrush(kGreenBright);
HGDIOBJ old_brush = SelectObject(dc, tip);
HGDIOBJ old_pen = SelectObject(dc, GetStockObject(NULL_PEN));
Polygon(dc, tri, 3);
SelectObject(dc, old_pen);
SelectObject(dc, old_brush);
DeleteObject(tip);
if (old_font != NULL) SelectObject(dc, old_font);
EndPaint(hwnd, &ps);
return 0;
}
return CallWindowProcA(gComboProc, hwnd, message, wParam, lParam);
}
void DestroyCombos(FEState *fe)
{
for (int g = 0; g < GroupCount; ++g)
{
if (fe->combo[g] != NULL)
{
DestroyWindow(fe->combo[g]);
fe->combo[g] = NULL;
}
}
}
// defined below, next to the catalogs it reads
const char *ItemName(int group, int index);
void LayoutMenu(FEState *fe, int client_w, int client_h)
{
fe->itemCount = 0;
gItemNameSelection = fe->selection;
int row_h = client_h / 36;
if (row_h < 18) row_h = 18;
if (row_h > 30) row_h = 30;
int top = client_h / 7;
//---------------------------------------------------------------
// Every list is a drop-down, so the menu no longer grows with the
// content: eight controls instead of ninety-odd rows. Two columns
// of label-over-box, settings on the left and loadout on the right,
// with the scenario left as visible buttons because it decides what
// the other lists contain.
//---------------------------------------------------------------
int margin = client_w / 14;
int gap = client_w / 20;
int col_w = (client_w - 2 * margin - gap) / 2;
if (col_w < 1) col_w = 1;
int col2 = margin + col_w + gap;
int block_h = row_h * 5 / 2; // label, box, breathing room
// scenario: buttons, top of the left column
int y = top;
fe->comboLabel[GroupScenario].left = margin;
fe->comboLabel[GroupScenario].right = margin + col_w;
fe->comboLabel[GroupScenario].top = y;
fe->comboLabel[GroupScenario].bottom = y + row_h;
y += row_h;
for (int i = 0; i < FE_COUNT(kScenarios); ++i)
{
FEItem *item = &fe->items[fe->itemCount++];
item->group = GroupScenario;
item->index = i;
item->rect.left = margin + i * (col_w / 2);
item->rect.top = y;
item->rect.right = item->rect.left + col_w / 2 - row_h / 3;
item->rect.bottom = y + row_h;
}
y += row_h + row_h / 2;
int groups[GroupCount];
int group_count = ComboGroups(fe->selection, groups);
// left column takes the mission settings, right the loadout; the
// split is where the vehicle list starts. The pilot name heads
// the loadout column - it is who you are, so it reads before
// what you are driving.
int left_y = y;
int right_y = top;
int name_y = right_y;
right_y += block_h;
for (int g = 0; g < group_count; ++g)
{
int group = groups[g];
Logical left = (group == GroupMap || group == GroupTime ||
group == GroupWeather || group == GroupLength);
int x = left ? margin : col2;
int *slot = left ? &left_y : &right_y;
fe->comboLabel[group].left = x;
fe->comboLabel[group].right = x + col_w;
fe->comboLabel[group].top = *slot;
fe->comboLabel[group].bottom = *slot + row_h;
*slot += block_h;
}
// launch button
FEItem *launch = &fe->items[fe->itemCount++];
launch->group = GroupLaunch;
launch->index = 0;
launch->rect.left = col2;
launch->rect.top = client_h - 3 * row_h;
launch->rect.right = col2 + col_w;
launch->rect.bottom = client_h - row_h;
// Steam lobby buttons, offered whenever environ.ini asked for Steam.
// Painting greys them out and says so if the wire never came up.
if (RPL4Lobby_Configured())
{
FEItem *host = &fe->items[fe->itemCount++];
host->group = GroupSteamHost;
host->index = 0;
host->rect.left = col2;
host->rect.top = client_h - 6 * row_h;
host->rect.right = col2 + col_w;
host->rect.bottom = client_h - 5 * row_h;
FEItem *join = &fe->items[fe->itemCount++];
join->group = GroupSteamJoin;
join->index = 0;
join->rect.left = col2;
join->rect.top = client_h - (9 * row_h) / 2;
join->rect.right = col2 + col_w;
join->rect.bottom = client_h - (7 * row_h) / 2;
}
//
// Exit, bottom left. Diagonally opposite LAUNCH on purpose: it is
// the one button on this screen you cannot undo, so it does not go
// next to the one people are aiming for. Half width for the same
// reason - it is a way out, not a peer of LAUNCH.
//
// The window frame used to be the way out, and mfd_layout.cfg's
// ,noframe takes it away, so the screen has to offer its own.
//
FEItem *quit = &fe->items[fe->itemCount++];
quit->group = GroupExit;
quit->index = 0;
quit->rect.left = margin;
quit->rect.top = client_h - 2 * row_h;
quit->rect.right = margin + col_w / 2;
quit->rect.bottom = client_h - row_h;
// pilot name, under the loadout column
fe->comboLabel[GroupLaunch].left = col2; // reused: name label
fe->comboLabel[GroupLaunch].right = col2 + col_w;
fe->comboLabel[GroupLaunch].top = name_y;
fe->comboLabel[GroupLaunch].bottom = name_y + row_h;
if (fe->nameEdit != NULL)
{
MoveWindow(fe->nameEdit, col2, name_y + row_h, col_w, row_h, TRUE);
}
//---------------------------------------------------------------
// The drop-downs themselves. Rebuilt rather than moved, because
// the scenario swaps two of them outright (team/position for
// colour/badge) and reshuffles the track list.
//---------------------------------------------------------------
DestroyCombos(fe);
for (int g = 0; g < group_count; ++g)
{
int group = groups[g];
const RECT &label = fe->comboLabel[group];
int count = GroupSize(group, fe->selection);
// The height given at creation is the DROPPED height - what the
// closed box shows is the item height - so ask for enough to
// show a dozen rows without a scrollbar where the list is short.
int drop_rows = (count < 12) ? count : 12;
HWND box = CreateWindowExA(
0, "COMBOBOX", "",
WS_CHILD | WS_VISIBLE | WS_VSCROLL |
CBS_DROPDOWNLIST | CBS_OWNERDRAWFIXED | CBS_HASSTRINGS,
label.left, label.top + row_h,
label.right - label.left, row_h + drop_rows * row_h,
fe->menuWindow, (HMENU)(INT_PTR)(kComboIdBase + group),
(HINSTANCE) GetWindowLongPtr(fe->menuWindow, GWLP_HINSTANCE), NULL);
if (box == NULL)
{
continue;
}
SendMessageA(box, WM_SETFONT, (WPARAM) fe->textFont, TRUE);
SendMessageA(box, CB_SETITEMHEIGHT, (WPARAM) -1, row_h);
SendMessageA(box, CB_SETITEMHEIGHT, 0, row_h);
// paint the closed box ourselves - see ComboSubclassProc
WNDPROC previous = (WNDPROC) SetWindowLongPtrA(
box, GWLP_WNDPROC, (LONG_PTR) ComboSubclassProc);
if (gComboProc == NULL)
{
gComboProc = previous;
}
for (int i = 0; i < count; ++i)
{
SendMessageA(box, CB_ADDSTRING, 0, (LPARAM) ItemName(group, i));
}
if (fe->selection[group] >= count) fe->selection[group] = 0;
SendMessageA(box, CB_SETCURSEL, fe->selection[group], 0);
fe->combo[group] = box;
}
}
const char *GroupTitle(int group)
{
switch (group)
{
case GroupScenario: return "SCENARIO";
case GroupMap: return "TRACK";
case GroupVehicle: return "VEHICLE";
case GroupColor: return "COLOR";
case GroupBadge: return "BADGE";
case GroupTeam: return "TEAM";
case GroupPosition: return "POSITION";
case GroupTime: return "TIME OF DAY";
case GroupWeather: return "WEATHER";
case GroupLength: return "GAME LENGTH";
}
return "";
}
// the map rows need the active scenario's list
const char *ItemName(int group, int index)
{
switch (group)
{
case GroupScenario: return kScenarios[index].name;
case GroupMap:
if (gItemNameSelection != NULL && IsFootball(gItemNameSelection))
{
return kFootballMaps[index].name;
}
return kMaps[index].name;
case GroupVehicle: return kVehicles[index].name;
case GroupColor: return kColors[index].name;
case GroupBadge: return kBadges[index].name;
case GroupTeam: return kTeams[index].name;
case GroupPosition: return kPositions[index].name;
case GroupTime: return kTimes[index].name;
case GroupWeather: return kWeather[index].name;
case GroupLength: return kLengths[index].name;
case GroupLaunch: return "L A U N C H G A M E";
case GroupSteamHost: return "HOST STEAM GAME";
case GroupSteamJoin: return "JOIN STEAM GAME";
case GroupExit: return "EXIT GAME";
}
return "";
}
//---------------------------------------------------------------
// Painting (double buffered, pod green on black)
//---------------------------------------------------------------
void PaintMenu(FEState *fe)
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(fe->menuWindow, &ps);
RECT client;
GetClientRect(fe->menuWindow, &client);
HDC mem = CreateCompatibleDC(hdc);
HBITMAP surface = CreateCompatibleBitmap(hdc, client.right, client.bottom);
HBITMAP old_surface = (HBITMAP) SelectObject(mem, surface);
FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH));
SetBkMode(mem, TRANSPARENT);
// title
gItemNameSelection = fe->selection;
SelectObject(mem, fe->titleFont);
SetTextColor(mem, kGreenBright);
RECT title = client;
title.top = client.bottom / 28;
title.bottom = title.top + client.bottom / 10;
DrawTextA(mem,
IsFootball(fe->selection)
? "RED PLANET - MARTIAN FOOTBALL"
: "RED PLANET - MARTIAN DEATH RACE",
-1, &title, DT_CENTER | DT_TOP | DT_SINGLELINE);
SelectObject(mem, fe->textFont);
// Labels: one above each drop-down, one above the scenario
// buttons, one above the pilot name box. The drop-downs draw
// themselves (WM_DRAWITEM), so all that is left here is their
// captions.
SetTextColor(mem, kGreenBright);
for (int g = 0; g < GroupCount; ++g)
{
Logical labelled = (g == GroupScenario) || (fe->combo[g] != NULL);
if (!labelled && g != GroupLaunch)
{
continue;
}
RECT label = fe->comboLabel[g];
if (label.right <= label.left)
{
continue;
}
const char *caption = (g == GroupLaunch) ? "PILOT NAME" : GroupTitle(g);
DrawTextA(mem, caption, -1, &label,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
}
for (int i = 0; i < fe->itemCount; ++i)
{
const FEItem *item = &fe->items[i];
Logical selected =
(item->group >= GroupLaunch) ||
(fe->selection[item->group] == item->index);
RECT row = item->rect;
if (item->group >= GroupLaunch)
{
//
// The lobby buttons are offered whenever environ.ini asked
// for Steam, but they only work once the client is actually
// there. Dim them and say why rather than leaving them out -
// two buttons quietly missing looks like a broken build.
//
Logical steam_button =
(item->group == GroupSteamHost || item->group == GroupSteamJoin);
Logical dead = steam_button && !RPL4Lobby_Available();
COLORREF ink = dead ? kGreenDim : kGreenBright;
HBRUSH launch_brush = CreateSolidBrush(ink);
FrameRect(mem, &row, launch_brush);
DeleteObject(launch_brush);
SetTextColor(mem, ink);
DrawTextA(mem, ItemName(item->group, item->index), -1, &row,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
//
// The reason, on its own line above the pair. It does not
// fit inside a button - they are about sixteen characters
// wide and this is more than twice that.
//
if (dead && item->group == GroupSteamHost)
{
RECT notice = row;
notice.bottom = row.top;
notice.top = row.top - (row.bottom - row.top);
SetTextColor(mem, kGreenDim);
DrawTextA(mem, "STEAM NOT RUNNING", -1, &notice,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
continue;
}
if (selected)
{
HBRUSH sel_brush = CreateSolidBrush(kGreenDim);
RECT fill = row;
FillRect(mem, &fill, sel_brush);
DeleteObject(sel_brush);
SetTextColor(mem, kGreenBright);
}
else
{
SetTextColor(mem, kGreenDim);
}
RECT text = row;
text.left += 6;
// On a narrow window the longest names (Screaming Broccoli,
// Blacker Tarantula) outrun their column. Ellipsis rather than
// a glyph sliced down the middle.
DrawTextA(mem, ItemName(item->group, item->index), -1, &text,
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
}
// (the pilot-name caption is drawn with the other labels above)
BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);
SelectObject(mem, old_surface);
DeleteObject(surface);
DeleteDC(mem);
EndPaint(fe->menuWindow, &ps);
}
void MenuClick(FEState *fe, int x, int y)
{
for (int i = 0; i < fe->itemCount; ++i)
{
const FEItem *item = &fe->items[i];
if (x >= item->rect.left && x < item->rect.right &&
y >= item->rect.top && y < item->rect.bottom)
{
if (item->group == GroupLaunch)
{
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)
{
// dead until the Steam client is there; the button says so
if (RPL4Lobby_Available())
{
fe->steamAction = (item->group == GroupSteamHost) ? 1 : 2;
PostMessageA(fe->menuWindow, WM_NULL, 0, 0);
}
}
else if (item->group == GroupExit)
{
// the same door closing the window goes through
fe->closed = True;
PostMessageA(fe->menuWindow, WM_NULL, 0, 0);
}
else
{
fe->selection[item->group] = item->index;
if (item->group == GroupScenario)
{
// the scenario swaps the map list and the
// color/badge vs team/position columns
int map_count;
ActiveMaps(fe->selection, &map_count);
if (fe->selection[GroupMap] >= map_count)
{
fe->selection[GroupMap] = 0;
}
RECT client;
GetClientRect(fe->menuWindow, &client);
LayoutMenu(fe, client.right, client.bottom);
}
InvalidateRect(fe->menuWindow, NULL, FALSE);
}
return;
}
}
}
LRESULT CALLBACK
FrontEndWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
FEState *fe = gFE;
switch (message)
{
case WM_PAINT:
if (fe != NULL)
{
PaintMenu(fe);
return 0;
}
break;
case WM_LBUTTONDOWN:
if (fe != NULL)
{
MenuClick(fe, (int)(short) LOWORD(lParam), (int)(short) HIWORD(lParam));
return 0;
}
break;
case WM_COMMAND:
if (fe != NULL && HIWORD(wParam) == CBN_SELCHANGE)
{
int group = LOWORD(wParam) - kComboIdBase;
if (group >= 0 && group < GroupCount && fe->combo[group] != NULL)
{
int pick = (int) SendMessageA(fe->combo[group], CB_GETCURSEL, 0, 0);
if (pick >= 0)
{
fe->selection[group] = pick;
}
// the closed box is ours to redraw now
InvalidateRect(fe->combo[group], NULL, FALSE);
InvalidateRect(fe->menuWindow, NULL, FALSE);
return 0;
}
}
break;
//
// The drop-downs are owner-drawn so they read as part of the
// panel rather than as system widgets: green on black, and the
// highlight inverted rather than tinted.
//
case WM_DRAWITEM:
if (fe != NULL)
{
DRAWITEMSTRUCT *di = (DRAWITEMSTRUCT *) lParam;
if (di->CtlType == ODT_COMBOBOX && (int) di->itemID >= 0)
{
Logical hot =
((di->itemState & (ODS_SELECTED | ODS_COMBOBOXEDIT)) == ODS_SELECTED);
HBRUSH back = CreateSolidBrush(hot ? kGreenDim : kBlack);
FillRect(di->hDC, &di->rcItem, back);
DeleteObject(back);
char text[128];
text[0] = '\0';
SendMessageA(di->hwndItem, CB_GETLBTEXT, di->itemID, (LPARAM) text);
RECT label = di->rcItem;
label.left += 6;
SetBkMode(di->hDC, TRANSPARENT);
SetTextColor(di->hDC, hot ? kBlack : kGreenBright);
DrawTextA(di->hDC, text, -1, &label,
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
return TRUE;
}
}
break;
case WM_CTLCOLORLISTBOX:
if (fe != NULL)
{
SetTextColor((HDC) wParam, kGreenBright);
SetBkColor((HDC) wParam, kBlack);
return (LRESULT) fe->editBrush;
}
break;
case WM_CTLCOLOREDIT:
if (fe != NULL)
{
SetTextColor((HDC) wParam, kGreenBright);
SetBkColor((HDC) wParam, kBlack);
return (LRESULT) fe->editBrush;
}
break;
case WM_ERASEBKGND:
return 1;
}
return DefWindowProcA(hwnd, message, wParam, lParam);
}
//---------------------------------------------------------------
// Plasma name bitmaps: white text auto-fit into WxH, thresholded
// to 1bpp hex rows - the C++ twin of the console's PlasmaBitmaps.
//---------------------------------------------------------------
void AppendNameBitmap(std::string &egg, int width, int height, const char *name)
{
BITMAPINFO info;
memset(&info, 0, sizeof(info));
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
info.bmiHeader.biWidth = width;
info.bmiHeader.biHeight = -height; // top-down
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = 32;
info.bmiHeader.biCompression = BI_RGB;
void *bits = NULL;
HDC dc = CreateCompatibleDC(NULL);
HBITMAP bitmap = CreateDIBSection(dc, &info, DIB_RGB_COLORS, &bits, NULL, 0);
HBITMAP old_bitmap = (HBITMAP) SelectObject(dc, bitmap);
memset(bits, 0, width * height * 4);
SetBkMode(dc, TRANSPARENT);
SetTextColor(dc, RGB(255, 255, 255));
// shrink the font until the name fits (console: 24pt downward)
int point_size = 24;
for (;;)
{
HFONT font = CreateFontA(
-MulDiv(point_size, GetDeviceCaps(dc, LOGPIXELSY), 72),
0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
NONANTIALIASED_QUALITY, DEFAULT_PITCH | FF_DONTCARE,
"Microsoft Sans Serif");
HFONT old_font = (HFONT) SelectObject(dc, font);
SIZE extent;
GetTextExtentPoint32A(dc, name, (int) strlen(name), &extent);
if ((extent.cx <= width && extent.cy <= height) || point_size <= 1)
{
RECT rect = { 0, 0, width, height };
DrawTextA(dc, name, -1, &rect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
SelectObject(dc, old_font);
DeleteObject(font);
break;
}
SelectObject(dc, old_font);
DeleteObject(font);
point_size = (point_size <= 2) ? (point_size / 2) : (point_size - 2);
}
GdiFlush();
const unsigned long *pixels = (const unsigned long *) bits;
char hex[8];
for (int row = 0; row < height; ++row)
{
egg += "bitmap=";
for (int byte_index = 0; byte_index < width / 8; ++byte_index)
{
unsigned char packed = 0;
for (int bit = 0; bit < 8; ++bit)
{
unsigned long pixel = pixels[row * width + byte_index * 8 + bit];
if ((pixel & 0xFF) >= 128) // white text on black
{
packed |= (unsigned char)(128 >> bit);
}
}
sprintf(hex, "%02X", packed);
egg += hex;
}
egg += "\n";
}
sprintf(hex, "%d", width);
egg += "x="; egg += hex; egg += "\n";
sprintf(hex, "%d", height);
egg += "y="; egg += hex; egg += "\n";
SelectObject(dc, old_bitmap);
DeleteObject(bitmap);
DeleteDC(dc);
}
// The 1st-4th place plasma graphics, verbatim from the console
// (RPMission.AppendOrdinals) / TEST.EGG.
extern const char *kOrdinalsBlock;
//---------------------------------------------------------------
// Egg assembly (console RPMission.ToEggString / RPFootballMission).
// Single player uses the standalone address; a hosted network game
// lists the owner first plus one pilot per member pod. Football
// adds team=/position= per pilot and the [teams] blocks; the
// owner's team/position picks seed a deterministic assignment
// across two teams (each team's first member without a runner
// becomes one; the rest alternate crusher/blocker; the runner
// wears the team's runner color, everyone else the team color).
//---------------------------------------------------------------
void BuildEggText(const FEState *fe, std::string &egg)
{
char line[256];
Logical football = IsFootball(fe->selection);
ExtraPilot extras[maxExtraPilots];
char owner_address[64];
strcpy(owner_address, kLocalPilotAddress);
int extra_count = CollectExtraPilots(fe, extras, owner_address);
if (extra_count < 0)
{
extra_count = 0; // single player
}
//
// One flat pilot table: the owner then the extras
//
struct PilotEntry
{
const char *address;
const char *name;
const char *vehicle;
const char *color;
const char *badge;
int team; // kTeams index, -1 outside football
const char *position;
Logical chosePosition; // picked it themselves
};
PilotEntry pilots[maxExtraPilots + 1];
int pilot_count = 0;
PilotEntry *owner = &pilots[pilot_count++];
owner->address = owner_address;
owner->name = fe->pilotName;
owner->vehicle = kVehicles[fe->selection[GroupVehicle]].key;
owner->color = kColors[fe->selection[GroupColor]].key;
owner->badge = kBadges[fe->selection[GroupBadge]].key;
owner->team = -1;
owner->position = NULL;
owner->chosePosition = False;
for (int p = 0; p < extra_count; ++p)
{
PilotEntry *pilot = &pilots[pilot_count++];
pilot->address = extras[p].address;
pilot->name = extras[p].name;
pilot->vehicle = extras[p].vehicle;
pilot->color = extras[p].color;
pilot->badge = extras[p].badge;
pilot->team = -1;
pilot->position = NULL;
pilot->chosePosition = False;
}
//
// Football: every pilot's own team/position pick is honored;
// unset picks get filled in, then each team is normalized to
// exactly one runner (the console's rule).
//
if (football)
{
//
// 1. Teams: the host's pick, then each member's, then
// alternate whoever did not choose across the two
// most-used teams so the sides stay balanced.
//
owner->team = fe->selection[GroupTeam];
for (int p = 1; p < pilot_count; ++p)
{
for (int t = 0; t < FE_COUNT(kTeams); ++t)
{
if (strcmp(extras[p - 1].team, kTeams[t].key) == 0)
{
pilots[p].team = t;
break;
}
}
}
int fallback_team = (owner->team + 1) % FE_COUNT(kTeams);
int unassigned = 0;
for (int p = 1; p < pilot_count; ++p)
{
if (pilots[p].team < 0)
{
pilots[p].team = (unassigned++ % 2) ? owner->team : fallback_team;
}
}
//
// 2. Positions: honor each pick, default the rest to the
// line, then give every team exactly one runner.
//
for (int p = 1; p < pilot_count; ++p)
{
const char *pick = extras[p - 1].position;
for (int i = 0; i < FE_COUNT(kPositions); ++i)
{
if (strcmp(pick, kPositions[i].key) == 0)
{
pilots[p].position = kPositions[i].key;
pilots[p].chosePosition = True;
break;
}
}
}
owner->position = kPositions[fe->selection[GroupPosition]].key;
owner->chosePosition = True;
for (int t = 0; t < FE_COUNT(kTeams); ++t)
{
int members = 0, runner = -1, spare = -1, line = 0;
for (int p = 0; p < pilot_count; ++p)
{
if (pilots[p].team != t)
{
continue;
}
++members;
if (spare < 0 && !pilots[p].chosePosition)
{
spare = p; // nobody's plans to disturb
}
if (pilots[p].position != NULL &&
strcmp(pilots[p].position, "runner") == 0)
{
if (runner < 0)
{
runner = p; // first runner keeps the ball
}
else
{
// two pilots claimed it - the later one lines up
pilots[p].position = NULL;
pilots[p].chosePosition = False;
}
}
}
if (members == 0)
{
continue;
}
if (runner < 0 && spare >= 0)
{
// no volunteer: the first pilot without a pick of
// their own runs (explicit picks are never overridden)
pilots[spare].position = "runner";
runner = spare;
}
for (int p = 0; p < pilot_count; ++p)
{
if (pilots[p].team == t && p != runner &&
pilots[p].position == NULL)
{
pilots[p].position = (line++ % 2) ? "crusher" : "blocker";
}
}
}
//
// 3. Colors follow team and position (runner wears the
// team's runner color - that is how you spot the ball)
//
for (int p = 0; p < pilot_count; ++p)
{
const TeamEntry *team = &kTeams[pilots[p].team];
pilots[p].color =
(strcmp(pilots[p].position, "runner") == 0)
? team->runnerColor : team->teamColor;
pilots[p].badge = "None";
}
}
//
// [mission]
//
int map_count;
const CatalogEntry *maps = ActiveMaps(fe->selection, &map_count);
egg += "[mission]\n";
egg += "adventure=Red Planet\n";
sprintf(line, "map=%s\n", maps[fe->selection[GroupMap]].key);
egg += line;
sprintf(line, "scenario=%s\n", kScenarios[fe->selection[GroupScenario]].key);
egg += line;
sprintf(line, "time=%s\n", kTimes[fe->selection[GroupTime]].key);
egg += line;
sprintf(line, "weather=%s\n", kWeather[fe->selection[GroupWeather]].key);
egg += line;
egg += "temperature=0\n";
egg += "compression=0\n";
int seconds = kLengths[fe->selection[GroupLength]].seconds;
if (seconds > 0)
{
sprintf(line, "length=%d\n", seconds);
egg += line;
}
//
// [pilots] + per-pilot sections
//
egg += "[pilots]\n";
for (int p = 0; p < pilot_count; ++p)
{
sprintf(line, "pilot=%s\n", pilots[p].address);
egg += line;
}
for (int p = 0; p < pilot_count; ++p)
{
sprintf(line, "[%s]\n", pilots[p].address);
egg += line;
egg += "hostType=0\n";
egg += "dropzone=one\n";
sprintf(line, "name=%s\n", pilots[p].name);
egg += line;
sprintf(line, "bitmapindex=%d\n", p + 1);
egg += line;
egg += "loadzones=1\n";
sprintf(line, "vehicle=%s\n", pilots[p].vehicle);
egg += line;
sprintf(line, "color=%s\n", pilots[p].color);
egg += line;
sprintf(line, "badge=%s\n", pilots[p].badge);
egg += line;
if (football)
{
sprintf(line, "team=team::%s\n", kTeams[pilots[p].team].key);
egg += line;
sprintf(line, "position=%s\n", pilots[p].position);
egg += line;
}
}
//
// Football team blocks (console RPFootballMission layout)
//
if (football)
{
std::string teams_list = "[teams]\n";
std::string team_sections, pilots_sections;
std::string teambitmap_list = "[teambitmap]\n";
std::string teambitmap_sections;
int bitmap_index = 1;
for (int side = 0; side < FE_COUNT(kTeams); ++side)
{
Logical team_present = False;
for (int p = 0; p < pilot_count; ++p)
{
if (pilots[p].team == side)
{
team_present = True;
}
}
if (!team_present)
{
continue;
}
const char *team_key = kTeams[side].key;
sprintf(line, "team=team::%s\n", team_key);
teams_list += line;
sprintf(line, "[team::%s]\nbitmapindex=%d\npilots=pilots::%s\n",
team_key, bitmap_index++, team_key);
team_sections += line;
sprintf(line, "[pilots::%s]\n", team_key);
pilots_sections += line;
for (int p = 0; p < pilot_count; ++p)
{
if (pilots[p].team == side)
{
sprintf(line, "pilot=%s\n", pilots[p].address);
pilots_sections += line;
}
}
sprintf(line, "bitmap=BitMap::Small::team::%s\n", team_key);
teambitmap_list += line;
sprintf(line, "[BitMap::Small::team::%s]\n", team_key);
teambitmap_sections += line;
AppendNameBitmap(teambitmap_sections, 64, 16, team_key);
teambitmap_sections += "width=4\n";
}
egg += teams_list;
egg += team_sections;
egg += pilots_sections;
egg += teambitmap_list;
egg += teambitmap_sections;
}
//
// Name bitmaps + ordinals
//
egg += "[largebitmap]\n";
for (int p = 0; p < pilot_count; ++p)
{
sprintf(line, "bitmap=BitMap::Large::%s\n", pilots[p].name);
egg += line;
}
egg += "[smallbitmap]\n";
for (int p = 0; p < pilot_count; ++p)
{
sprintf(line, "bitmap=BitMap::Small::%s\n", pilots[p].name);
egg += line;
}
for (int p = 0; p < pilot_count; ++p)
{
sprintf(line, "[BitMap::Large::%s]\n", pilots[p].name);
egg += line;
AppendNameBitmap(egg, 128, 32, pilots[p].name);
egg += "width=8\n";
sprintf(line, "[BitMap::Small::%s]\n", pilots[p].name);
egg += line;
AppendNameBitmap(egg, 64, 16, pilots[p].name);
egg += "width=4\n";
}
egg += kOrdinalsBlock;
// [pilots]-order names for the network console's results
strcpy(gLastPilotNamesCsv, fe->pilotName);
for (int p = 1; p < pilot_count; ++p)
{
if (strlen(gLastPilotNamesCsv) + strlen(pilots[p].name) + 2 <
sizeof(gLastPilotNamesCsv))
{
strcat(gLastPilotNamesCsv, ",");
strcat(gLastPilotNamesCsv, pilots[p].name);
}
}
}
}
//########################################################################
//############################ RPL4FrontEnd_Run ##########################
//########################################################################
static int gLastLaunchMode = FELaunchSingle;
//---------------------------------------------------------------
// Build frontend.egg from the persisted loadout (the menu persists
// on every exit; lobby launches can happen without a live menu)
//---------------------------------------------------------------
static Logical
BuildEggFromPersisted(char *egg_path_out, int egg_path_size)
{
FEState fe;
memset(&fe, 0, sizeof(fe));
strcpy(fe.pilotName, gLastPilotName);
if (gHavePersist)
{
memcpy(fe.selection, gPersistSelection, sizeof(fe.selection));
}
else
{
fe.selection[GroupLength] = 2; // 5:00
}
gLastMissionSeconds = kLengths[fe.selection[GroupLength]].seconds;
std::string egg;
egg.reserve(16384);
BuildEggText(&fe, egg);
strncpy(egg_path_out, "frontend.egg", egg_path_size - 1);
egg_path_out[egg_path_size - 1] = '\0';
FILE *file = fopen(egg_path_out, "wb");
if (file == NULL)
{
DEBUG_STREAM << "FrontEnd: could not write " << egg_path_out
<< "\n" << std::flush;
return False;
}
fwrite(egg.data(), 1, egg.size(), file);
fclose(file);
DEBUG_STREAM << "FrontEnd: wrote " << egg_path_out << " ("
<< (int) egg.size() << " bytes)\n" << std::flush;
return True;
}
Logical
RPL4FrontEnd_Run(
HINSTANCE instance,
HWND main_window,
char *egg_path_out,
int egg_path_size
)
{
gLastLaunchMode = FELaunchSingle;
// before the lobby branch below: a member rejoining a room publishes
// the callsign as member data without the menu ever opening
EnsurePilotSettingsLoaded();
//---------------------------------------------------------------
// Coming back from a race while still in a lobby: straight to
// the room (the lobby outlives races - single binary payoff)
//---------------------------------------------------------------
if (RPL4Lobby_InRoom())
{
int outcome = RPL4Lobby_Room(instance, main_window);
if (outcome == LobbyRoomClosed)
{
return False;
}
if (outcome == LobbyLaunchMember)
{
gLastLaunchMode = FELaunchMember;
egg_path_out[0] = '\0';
return True;
}
if (outcome == LobbyLaunchHost)
{
gLastLaunchMode = FELaunchHost;
return BuildEggFromPersisted(egg_path_out, egg_path_size);
}
// LobbyRoomLeft: fall through to the setup menu
}
// NOTE: the vehicle catalog below is hand-written while RPL4.RES is
// built elsewhere, so the two can drift - this menu offered 'blkspk'
// for a while before any vehicle resource backed it. Validating here
// does NOT work: the front end runs before the resource file is
// opened, so GetResourceFile() has nothing to search yet. If this is
// worth guarding, do it offline against the built RPL4.RES (see
// tools/resbuild) rather than at menu time.
for (;;)
{
FEState fe;
memset(&fe, 0, sizeof(fe));
EnsurePilotSettingsLoaded();
strcpy(fe.pilotName, gLastPilotName);
if (gHavePersist)
{
memcpy(fe.selection, gPersistSelection, sizeof(fe.selection));
}
else
{
fe.selection[GroupLength] = 2; // 5:00
}
gFE = &fe;
//---------------------------------------------------------------
// Window class + fonts
//---------------------------------------------------------------
static Logical class_registered = False;
if (!class_registered)
{
class_registered = True;
WNDCLASSA window_class;
memset(&window_class, 0, sizeof(window_class));
window_class.style = CS_HREDRAW | CS_VREDRAW;
window_class.lpfnWndProc = FrontEndWndProc;
window_class.hInstance = instance;
window_class.hCursor = LoadCursor(NULL, IDC_ARROW);
window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
window_class.lpszClassName = "RPFrontEnd";
RegisterClassA(&window_class);
}
RECT client;
GetClientRect(main_window, &client);
fe.menuWindow = CreateWindowExA(
0, "RPFrontEnd", "",
WS_CHILD | WS_VISIBLE,
0, 0, client.right, client.bottom,
main_window, NULL, instance, NULL);
if (fe.menuWindow == NULL)
{
gFE = NULL;
return False;
}
int text_h = client.bottom / 40;
if (text_h < 16) text_h = 16;
if (text_h > 24) text_h = 24;
fe.textFont = CreateFontA(-text_h, 0, 0, 0, FW_NORMAL,
FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN,
"Consolas");
fe.titleFont = CreateFontA(-(text_h * 2), 0, 0, 0, FW_BOLD,
FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN,
"Consolas");
fe.editBrush = CreateSolidBrush(kBlack);
fe.nameEdit = CreateWindowExA(
0, "EDIT", fe.pilotName,
WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
0, 0, 10, 10,
fe.menuWindow, NULL, instance, NULL);
SendMessageA(fe.nameEdit, WM_SETFONT, (WPARAM) fe.textFont, TRUE);
SendMessageA(fe.nameEdit, EM_SETLIMITTEXT, sizeof(fe.pilotName) - 2, 0);
LayoutMenu(&fe, client.right, client.bottom);
InvalidateRect(fe.menuWindow, NULL, TRUE);
//---------------------------------------------------------------
// Modal loop until launch, lobby button, or close
//---------------------------------------------------------------
MSG msg;
while (!fe.launched && !fe.closed && fe.steamAction == 0)
{
BOOL result = GetMessageA(&msg, NULL, 0, 0);
if (result <= 0)
{
fe.closed = True;
break;
}
if (msg.message == WM_QUIT ||
(msg.message == WM_SYSCOMMAND && msg.wParam == SC_CLOSE))
{
fe.closed = True;
}
// closing the main window ends the front end too
if (msg.message == WM_CLOSE && msg.hwnd == main_window)
{
fe.closed = True;
}
TranslateMessage(&msg);
DispatchMessageA(&msg);
if (!IsWindow(main_window))
{
fe.closed = True;
}
}
//---------------------------------------------------------------
// Harvest the callsign whichever way we leave the menu, INCLUDING
// a close: typing a name and then quitting is how somebody sets it
// for next time, and losing it there would be the one case that
// makes the whole thing feel unreliable. The loadout still only
// persists on a real exit - it is picked, not typed, and the menu
// reopens with it visible anyway.
//---------------------------------------------------------------
if (fe.nameEdit != NULL)
{
fe.pilotName[0] = '\0';
GetWindowTextA(fe.nameEdit, fe.pilotName, sizeof(fe.pilotName) - 1);
SanitizeCallsign(fe.pilotName, sizeof(fe.pilotName));
}
else
{
strcpy(fe.pilotName, gLastPilotName);
}
strcpy(gLastPilotName, fe.pilotName);
memcpy(gPersistSelection, fe.selection, sizeof(gPersistSelection));
gHavePersist = True;
//
// Written on the way out however the player leaves - launching,
// stepping into a lobby, or quitting. BT411 saves only on a
// launch, which loses a callsign typed by somebody who then
// changed their mind, and that is the one moment this feature
// exists for.
//
// Unconditionally, rather than only when something changed: the
// file is a few hundred bytes, and writing it every time means a
// value that was hand-edited out of range comes back corrected
// instead of being quietly re-rejected on every launch forever.
//
SavePilotSettings(gLastPilotName, gPersistSelection);
Logical launched = fe.launched;
Logical closed = fe.closed;
int steam_action = fe.steamAction;
DestroyWindow(fe.menuWindow);
DeleteObject(fe.textFont);
DeleteObject(fe.titleFont);
DeleteObject(fe.editBrush);
gFE = NULL;
if (closed)
{
return False;
}
if (launched)
{
// plain launch: single player, or a LAN-hosted race when
// RP412HOSTPODS is set in the environment
return BuildEggFromPersisted(egg_path_out, egg_path_size);
}
//---------------------------------------------------------------
// Steam lobby (host or join), then back here if they leave
//---------------------------------------------------------------
int outcome = (steam_action == 1)
? RPL4Lobby_Host(instance, main_window)
: RPL4Lobby_Join(instance, main_window);
if (outcome == LobbyRoomClosed)
{
return False;
}
if (outcome == LobbyLaunchMember)
{
gLastLaunchMode = FELaunchMember;
egg_path_out[0] = '\0';
return True;
}
if (outcome == LobbyLaunchHost)
{
gLastLaunchMode = FELaunchHost;
return BuildEggFromPersisted(egg_path_out, egg_path_size);
}
// LobbyRoomLeft: show the menu again
}
}
int
RPL4FrontEnd_LastMissionSeconds()
{
return gLastMissionSeconds;
}
const char *
RPL4FrontEnd_LastPilotNames()
{
return gLastPilotNamesCsv;
}
int
RPL4FrontEnd_LastLaunchMode()
{
return gLastLaunchMode;
}
void
RPL4FrontEnd_GetLoadout(char *vehicle, char *color, char *badge)
{
int v = gHavePersist ? gPersistSelection[GroupVehicle] : 0;
int c = gHavePersist ? gPersistSelection[GroupColor] : 0;
int b = gHavePersist ? gPersistSelection[GroupBadge] : 0;
strcpy(vehicle, kVehicles[v].key);
strcpy(color, kColors[c].key);
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)
//########################################################################
int
RPL4FrontEnd_TeamCount()
{
return FE_COUNT(kTeams);
}
const char *
RPL4FrontEnd_TeamName(int index)
{
if (index < 0 || index >= FE_COUNT(kTeams))
{
return "";
}
return kTeams[index].name;
}
const char *
RPL4FrontEnd_TeamKey(int index)
{
if (index < 0 || index >= FE_COUNT(kTeams))
{
return "";
}
return kTeams[index].key;
}
int
RPL4FrontEnd_PositionCount()
{
return FE_COUNT(kPositions);
}
const char *
RPL4FrontEnd_PositionName(int index)
{
if (index < 0 || index >= FE_COUNT(kPositions))
{
return "";
}
return kPositions[index].name;
}
const char *
RPL4FrontEnd_PositionKey(int index)
{
if (index < 0 || index >= FE_COUNT(kPositions))
{
return "";
}
return kPositions[index].key;
}
int
RPL4FrontEnd_GetTeamIndex()
{
return gHavePersist ? gPersistSelection[GroupTeam] : 0;
}
void
RPL4FrontEnd_SetTeamIndex(int index)
{
if (index < 0 || index >= FE_COUNT(kTeams))
{
return;
}
gPersistSelection[GroupTeam] = index;
gHavePersist = True;
}
int
RPL4FrontEnd_GetPositionIndex()
{
return gHavePersist ? gPersistSelection[GroupPosition] : 0;
}
void
RPL4FrontEnd_SetPositionIndex(int index)
{
if (index < 0 || index >= FE_COUNT(kPositions))
{
return;
}
gPersistSelection[GroupPosition] = index;
gHavePersist = True;
}
Logical
RPL4FrontEnd_IsFootballSelected()
{
return gHavePersist && IsFootball(gPersistSelection);
}
void
RPL4FrontEnd_SetHostedPilots(
const char *owner_address,
const FEHostedPilot *pilots,
int count
)
{
if (count > maxExtraPilots)
{
count = maxExtraPilots;
}
gHostedPilotCount = (pilots != NULL) ? count : 0;
for (int i = 0; i < gHostedPilotCount; ++i)
{
gHostedPilots[i] = pilots[i];
}
strncpy(gHostedOwnerAddress,
(owner_address != NULL) ? owner_address : "",
sizeof(gHostedOwnerAddress) - 1);
gHostedOwnerAddress[sizeof(gHostedOwnerAddress) - 1] = '\0';
}
//########################################################################
//######################## RPL4FrontEnd_ShowResults ######################
//########################################################################
namespace
{
struct ResultRow
{
char name[32];
int score;
};
struct ResultsState
{
HWND window;
HFONT textFont;
HFONT titleFont;
ResultRow rows[16];
int rowCount;
RECT continueRect;
Logical dismissed;
Logical closed;
};
ResultsState *gRS = NULL;
const char *PlaceName(int place)
{
static const char *kPlaces[] = { "1ST", "2ND", "3RD", "4TH" };
static char other[8];
if (place < 4)
{
return kPlaces[place];
}
sprintf(other, "%dTH", place + 1);
return other;
}
void PaintResults(ResultsState *rs)
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(rs->window, &ps);
RECT client;
GetClientRect(rs->window, &client);
HDC mem = CreateCompatibleDC(hdc);
HBITMAP surface = CreateCompatibleBitmap(hdc, client.right, client.bottom);
HBITMAP old_surface = (HBITMAP) SelectObject(mem, surface);
FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH));
SetBkMode(mem, TRANSPARENT);
SelectObject(mem, rs->titleFont);
SetTextColor(mem, kGreenBright);
RECT title = client;
title.top = client.bottom / 28;
title.bottom = title.top + client.bottom / 10;
DrawTextA(mem, "RACE RESULTS", -1, &title,
DT_CENTER | DT_TOP | DT_SINGLELINE);
SelectObject(mem, rs->textFont);
int row_h = client.bottom / 16;
int top = client.bottom / 4;
int left = client.right / 3;
int right = 2 * client.right / 3;
char text[48];
for (int i = 0; i < rs->rowCount; ++i)
{
RECT row;
row.left = left;
row.right = right;
row.top = top + i * row_h;
row.bottom = row.top + row_h;
SetTextColor(mem, (i == 0) ? kGreenBright : kGreenDim);
DrawTextA(mem, PlaceName(i), -1, &row,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
RECT name = row;
name.left += (right - left) / 5;
DrawTextA(mem, rs->rows[i].name, -1, &name,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
sprintf(text, "%d", rs->rows[i].score);
DrawTextA(mem, text, -1, &row,
DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
}
HBRUSH frame_brush = CreateSolidBrush(kGreenBright);
FrameRect(mem, &rs->continueRect, frame_brush);
DeleteObject(frame_brush);
SetTextColor(mem, kGreenBright);
DrawTextA(mem, "C O N T I N U E", -1, &rs->continueRect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);
SelectObject(mem, old_surface);
DeleteObject(surface);
DeleteDC(mem);
EndPaint(rs->window, &ps);
}
LRESULT CALLBACK
ResultsWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
ResultsState *rs = gRS;
switch (message)
{
case WM_PAINT:
if (rs != NULL)
{
PaintResults(rs);
return 0;
}
break;
case WM_LBUTTONDOWN:
if (rs != NULL)
{
POINT point = { (int)(short) LOWORD(lParam), (int)(short) HIWORD(lParam) };
if (PtInRect(&rs->continueRect, point))
{
rs->dismissed = True;
PostMessageA(rs->window, WM_NULL, 0, 0);
}
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
}
return DefWindowProcA(hwnd, message, wParam, lParam);
}
}
Logical
RPL4FrontEnd_ShowResults(
HINSTANCE instance,
HWND main_window
)
{
int result_count = RPL4LocalConsole_ResultCount();
if (result_count <= 0)
{
return True; // nothing to show (first boot)
}
if (!IsWindow(main_window))
{
return False;
}
// this screen labels a row with the pilot's own callsign, and in the
// -egg and lobby paths it can be the first screen of the session
EnsurePilotSettingsLoaded();
ResultsState rs;
memset(&rs, 0, sizeof(rs));
//---------------------------------------------------------------
// Intake, then sort descending by score. Single-player results
// carry the pilot's own name; with more pods the roster will map
// host IDs to Steam personas (until then: pod number).
//---------------------------------------------------------------
for (int i = 0; i < result_count && rs.rowCount < 16; ++i)
{
int host_ID, score;
if (!RPL4LocalConsole_GetResult(i, &host_ID, &score))
{
continue;
}
ResultRow *row = &rs.rows[rs.rowCount++];
const char *pilot_name = RPL4LocalConsole_GetResultName(host_ID);
if (pilot_name != NULL)
{
sprintf(row->name, "%.30s", pilot_name);
}
else if (result_count == 1)
{
sprintf(row->name, "%s", gLastPilotName);
}
else
{
sprintf(row->name, "POD %d", host_ID);
}
row->score = score;
}
for (int i = 1; i < rs.rowCount; ++i)
{
ResultRow key = rs.rows[i];
int j = i - 1;
while (j >= 0 && rs.rows[j].score < key.score)
{
rs.rows[j + 1] = rs.rows[j];
--j;
}
rs.rows[j + 1] = key;
}
static Logical class_registered = False;
if (!class_registered)
{
class_registered = True;
WNDCLASSA window_class;
memset(&window_class, 0, sizeof(window_class));
window_class.style = CS_HREDRAW | CS_VREDRAW;
window_class.lpfnWndProc = ResultsWndProc;
window_class.hInstance = instance;
window_class.hCursor = LoadCursor(NULL, IDC_ARROW);
window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
window_class.lpszClassName = "RPResults";
RegisterClassA(&window_class);
}
RECT client;
GetClientRect(main_window, &client);
rs.window = CreateWindowExA(
0, "RPResults", "",
WS_CHILD | WS_VISIBLE,
0, 0, client.right, client.bottom,
main_window, NULL, instance, NULL);
if (rs.window == NULL)
{
return False;
}
int text_h = client.bottom / 40;
if (text_h < 16) text_h = 16;
if (text_h > 24) text_h = 24;
rs.textFont = CreateFontA(-(text_h * 3 / 2), 0, 0, 0, FW_NORMAL,
FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN,
"Consolas");
rs.titleFont = CreateFontA(-(text_h * 2), 0, 0, 0, FW_BOLD,
FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN,
"Consolas");
int row_h = client.bottom / 36;
if (row_h < 18) row_h = 18;
if (row_h > 30) row_h = 30;
int col_w = client.right / 5;
rs.continueRect.left = (client.right - col_w) / 2;
rs.continueRect.right = rs.continueRect.left + col_w;
rs.continueRect.top = client.bottom - 3 * row_h;
rs.continueRect.bottom = client.bottom - row_h;
gRS = &rs;
SetFocus(rs.window);
InvalidateRect(rs.window, NULL, TRUE);
//---------------------------------------------------------------
// Modal until CONTINUE (or any key) - same shape as the menu loop
//---------------------------------------------------------------
MSG msg;
while (!rs.dismissed && !rs.closed)
{
BOOL result = GetMessageA(&msg, NULL, 0, 0);
if (result <= 0)
{
rs.closed = True;
break;
}
if (msg.message == WM_QUIT ||
(msg.message == WM_SYSCOMMAND && msg.wParam == SC_CLOSE))
{
rs.closed = True;
}
if (msg.message == WM_CLOSE && msg.hwnd == main_window)
{
rs.closed = True;
}
if (msg.message == WM_KEYDOWN &&
(msg.wParam == VK_RETURN || msg.wParam == VK_SPACE ||
msg.wParam == VK_ESCAPE))
{
rs.dismissed = True;
}
TranslateMessage(&msg);
DispatchMessageA(&msg);
if (!IsWindow(main_window))
{
rs.closed = True;
}
}
DestroyWindow(rs.window);
DeleteObject(rs.textFont);
DeleteObject(rs.titleFont);
gRS = NULL;
return !rs.closed;
}
//########################################################################
// Ordinal plasma graphics (verbatim from the console / TEST.EGG)
//########################################################################
namespace
{
const char *kOrdinalsBlock =
"[ordinals]\n"
"bitmap=Ordinal::BitMap::1\n"
"bitmap=Ordinal::BitMap::2\n"
"bitmap=Ordinal::BitMap::3\n"
"bitmap=Ordinal::BitMap::4\n"
"[Ordinal::BitMap::1]\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=000038000003C000001FF00000000F00\n"
"bitmap=0000F8000003C000003FF80000000F00\n"
"bitmap=0001F8000003C00000707C0000000F00\n"
"bitmap=0001F8000003C00000603C0000000F00\n"
"bitmap=00007801FC0FF00000003C3DF807FF00\n"
"bitmap=00007803FE0FF00000003C3FFC0FFF00\n"
"bitmap=00007803C703C00000003C3E3C1F0F00\n"
"bitmap=000078078303C0000000783C1E1E0F00\n"
"bitmap=000078078003C0000000783C1E1E0F00\n"
"bitmap=00007807C003C0000000F03C1E1E0F00\n"
"bitmap=00007807F003C0000001E03C1E1E0F00\n"
"bitmap=00007803FC03C0000003C03C1E1E0F00\n"
"bitmap=00007801FE03C0000007803C1E1E0F00\n"
"bitmap=000078007F03C000000F003C1E1E0F00\n"
"bitmap=000078001F03C000001E003C1E1E0F00\n"
"bitmap=000078000F03C000003C003C1E1E0F00\n"
"bitmap=000078060F03C0000078003C1E1E0F00\n"
"bitmap=000078071E03E0000078003C1E1F1F00\n"
"bitmap=00007803FE01F000007FFC3C1E0FFF00\n"
"bitmap=00007801FC00F000007FFC3C1E07EF00\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"x=128\n"
"y=32\n"
"width=8\n"
"[Ordinal::BitMap::2]\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=001FFF0000003C0000003C03C0780000\n"
"bitmap=001FFF0000003C0000007C03C0780000\n"
"bitmap=00000E0000003C000000FC03C0780000\n"
"bitmap=00003C0000003C000001BC03C0780000\n"
"bitmap=0000700F3E1FFC000003BC0FF07BF000\n"
"bitmap=0001E00F7E3FFC0000073C0FF07FF800\n"
"bitmap=0003800FFE7C3C0000063C03C07C7800\n"
"bitmap=0007F80FFE783C00000C3C03C0783C00\n"
"bitmap=0007FE0F80783C0000183C03C0783C00\n"
"bitmap=00001E0F00783C0000383C03C0783C00\n"
"bitmap=00000F0F00783C0000703C03C0783C00\n"
"bitmap=00000F0F00783C00007FFF03C0783C00\n"
"bitmap=00000F0F00783C00007FFF03C0783C00\n"
"bitmap=00000F0F00783C0000003C03C0783C00\n"
"bitmap=00000F0F00783C0000003C03C0783C00\n"
"bitmap=00000F0F00783C0000003C03C0783C00\n"
"bitmap=00180F0F00783C0000003C03C0783C00\n"
"bitmap=001C1F0F007C7C0000003C03E0783C00\n"
"bitmap=000FFE0F003FFC0000003C01F0783C00\n"
"bitmap=0007FC0F001FBC0000003C00F0783C00\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"x=128\n"
"y=32\n"
"width=8\n"
"[Ordinal::BitMap::3]\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=001FFF03C07800000000FC03C0780000\n"
"bitmap=001FFF03C07800000003FC03C0780000\n"
"bitmap=001E0003C07800000007C003C0780000\n"
"bitmap=001E0003C0780000000F0003C0780000\n"
"bitmap=001E000FF07BF000000F000FF07BF000\n"
"bitmap=001E000FF07FF800001E000FF07FF800\n"
"bitmap=001E0003C07C7800001E0003C07C7800\n"
"bitmap=001FFC03C0783C00001EFC03C0783C00\n"
"bitmap=001FFE03C0783C00001FFE03C0783C00\n"
"bitmap=00001F03C0783C00001F1F03C0783C00\n"
"bitmap=00000F03C0783C00001E0F03C0783C00\n"
"bitmap=00000F03C0783C00001E0F03C0783C00\n"
"bitmap=00000F03C0783C00001E0F03C0783C00\n"
"bitmap=00000F03C0783C00001E0F03C0783C00\n"
"bitmap=00000F03C0783C00001E0F03C0783C00\n"
"bitmap=00000F03C0783C00001E0F03C0783C00\n"
"bitmap=00180F03C0783C00001E0F03C0783C00\n"
"bitmap=001C1F03E0783C00001F1F03E0783C00\n"
"bitmap=000FFE01F0783C00000FFE01F0783C00\n"
"bitmap=0007FC00F0783C000007FC00F0783C00\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"x=128\n"
"y=32\n"
"width=8\n"
"[Ordinal::BitMap::4]\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=001FFF03C07800000007FC03C0780000\n"
"bitmap=001FFF03C0780000000FFE03C0780000\n"
"bitmap=00000F03C0780000001F1F03C0780000\n"
"bitmap=00000F03C0780000001E0F03C0780000\n"
"bitmap=00000F0FF07BF000001E0F0FF07BF000\n"
"bitmap=00001F0FF07FF800001E0F0FF07FF800\n"
"bitmap=00001E03C07C7800001E0F03C07C7800\n"
"bitmap=00003E03C0783C00001E0F03C0783C00\n"
"bitmap=00003C03C0783C00000F1E03C0783C00\n"
"bitmap=00003C03C0783C000007FC03C0783C00\n"
"bitmap=00007803C0783C000007FC03C0783C00\n"
"bitmap=00007803C0783C00000F1E03C0783C00\n"
"bitmap=00007803C0783C00001E0F03C0783C00\n"
"bitmap=0000F003C0783C00001E0F03C0783C00\n"
"bitmap=0000F003C0783C00001E0F03C0783C00\n"
"bitmap=0000F003C0783C00001E0F03C0783C00\n"
"bitmap=0000F003C0783C00001E0F03C0783C00\n"
"bitmap=0000F003E0783C00000F1E03E0783C00\n"
"bitmap=0000F001F0783C00000FFE01F0783C00\n"
"bitmap=0000F000F0783C000007FC00F0783C00\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"x=128\n"
"y=32\n"
"width=8\n";
}