Files
RP412/RP_L4/RPL4FE.cpp
T
CydandClaude Fable 5 4b25f89e6f Football: every player picks their own team and position
Closes the gap left by the football commit. Members publish their
picks as lobby member data (tm/ps) alongside their loadout, and the
owner publishes the scenario (sc) so members know when the picks
matter. The lobby room turns into a team sheet: each member row shows
their team and position, and MY TEAM / MY POSITION buttons cycle the
local pick and republish it live, so everyone watches the sides fill
out before the host launches.

The egg builder honors explicit picks and only fills gaps:
- pilots who chose a team get it; the rest are spread across the
  host team and one other so the sides stay balanced
- a team with no volunteer runner promotes its first UNPICKED member,
  never overriding someone who chose crusher or blocker
- if two pilots on a team both claim runner, the first keeps it and
  the second lines up
- colors stay derived: runner in the team runner color, everyone else
  in the team color

Verified: a three-pilot egg puts the host on crusher in team color
with an unpicked teammate promoted to runner in runner color, the
other side gets its own runner, and the teams/pilots blocks group
correctly; the lobby room cycles picks and repaints the roster; both
scenarios still pass their single-player regressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 19:59:42 -05:00

1952 lines
53 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" },
};
// 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" },
};
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,
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;
HFONT textFont;
HFONT titleFont;
HBRUSH editBrush;
Logical launched;
Logical closed;
int steamAction; // 1 = host lobby, 2 = join lobby
};
FEState *gFE = 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";
// [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
//---------------------------------------------------------------
void AddGroupItems(
FEState *fe, int group, int count,
int x, int *y, int row_h, int width)
{
for (int i = 0; i < count; ++i)
{
FEItem *item = &fe->items[fe->itemCount++];
item->group = group;
item->index = i;
item->rect.left = x;
item->rect.top = *y;
item->rect.right = x + width;
item->rect.bottom = *y + row_h;
*y += row_h;
}
*y += row_h; // gap below the group
}
void LayoutMenu(FEState *fe, int client_w, int client_h)
{
fe->itemCount = 0;
int row_h = client_h / 36;
if (row_h < 18) row_h = 18;
if (row_h > 30) row_h = 30;
int col_w = client_w / 5;
int col1 = client_w / 14;
int col2 = col1 + col_w + client_w / 28;
int col3 = col2 + col_w + client_w / 28;
int top = client_h / 7;
int y = top;
AddGroupItems(fe, GroupScenario, FE_COUNT(kScenarios), col1, &y, row_h, col_w);
int map_count;
ActiveMaps(fe->selection, &map_count);
AddGroupItems(fe, GroupMap, map_count, col1, &y, row_h, col_w);
AddGroupItems(fe, GroupTime, FE_COUNT(kTimes), col1, &y, row_h, col_w);
AddGroupItems(fe, GroupWeather, FE_COUNT(kWeather), col1, &y, row_h, col_w);
AddGroupItems(fe, GroupLength, FE_COUNT(kLengths), col1, &y, row_h, col_w);
y = top;
AddGroupItems(fe, GroupVehicle, FE_COUNT(kVehicles), col2, &y, row_h, col_w);
y = top + 2 * row_h; // leave room for the name edit + header
if (IsFootball(fe->selection))
{
AddGroupItems(fe, GroupTeam, FE_COUNT(kTeams), col3, &y, row_h, col_w);
AddGroupItems(fe, GroupPosition, FE_COUNT(kPositions), col3, &y, row_h, col_w);
}
else
{
AddGroupItems(fe, GroupColor, FE_COUNT(kColors), col3, &y, row_h, col_w);
AddGroupItems(fe, GroupBadge, FE_COUNT(kBadges), col3, &y, row_h, col_w);
}
// launch button
FEItem *launch = &fe->items[fe->itemCount++];
launch->group = GroupLaunch;
launch->index = 0;
launch->rect.left = col3;
launch->rect.top = client_h - 3 * row_h;
launch->rect.right = col3 + col_w;
launch->rect.bottom = client_h - row_h;
// Steam lobby buttons (only when the Steam wire is live)
if (RPL4Lobby_Available())
{
FEItem *host = &fe->items[fe->itemCount++];
host->group = GroupSteamHost;
host->index = 0;
host->rect.left = col3;
host->rect.top = client_h - 6 * row_h;
host->rect.right = col3 + 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 = col3;
join->rect.top = client_h - (9 * row_h) / 2;
join->rect.right = col3 + col_w;
join->rect.bottom = client_h - (7 * row_h) / 2;
}
// pilot name edit sits at the top of column 3
if (fe->nameEdit != NULL)
{
MoveWindow(fe->nameEdit,
col3, top - row_h / 4, col_w, row_h, TRUE);
}
}
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 int *gItemNameSelection = NULL;
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";
}
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);
// group headers (drawn above each group's first item)
int previous_group = -1;
for (int i = 0; i < fe->itemCount; ++i)
{
const FEItem *item = &fe->items[i];
if (item->group != previous_group && item->group < GroupLaunch)
{
previous_group = item->group;
RECT header = item->rect;
header.top -= (item->rect.bottom - item->rect.top);
header.bottom = item->rect.top;
SetTextColor(mem, kGreenBright);
DrawTextA(mem, GroupTitle(item->group), -1, &header,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
}
Logical selected =
(item->group >= GroupLaunch) ||
(fe->selection[item->group] == item->index);
RECT row = item->rect;
if (item->group >= GroupLaunch)
{
HBRUSH launch_brush = CreateSolidBrush(kGreenBright);
FrameRect(mem, &row, launch_brush);
DeleteObject(launch_brush);
SetTextColor(mem, kGreenBright);
DrawTextA(mem, ItemName(item->group, item->index), -1, &row,
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;
DrawTextA(mem, ItemName(item->group, item->index), -1, &text,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
}
// pilot-name header
if (fe->nameEdit != NULL)
{
RECT edit_rect;
GetWindowRect(fe->nameEdit, &edit_rect);
POINT corner = { edit_rect.left, edit_rect.top };
ScreenToClient(fe->menuWindow, &corner);
RECT header;
header.left = corner.x;
header.right = corner.x + 300;
header.bottom = corner.y;
header.top = corner.y - 26;
SetTextColor(mem, kGreenBright);
DrawTextA(mem, "PILOT NAME", -1, &header,
DT_LEFT | 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(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)
{
fe->steamAction = (item->group == GroupSteamHost) ? 1 : 2;
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_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;
//---------------------------------------------------------------
// 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
}
for (;;)
{
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
}
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 loadout whichever way we leave the menu - the
// lobby publishes it as member data, launches build from it
//---------------------------------------------------------------
if (!fe.closed)
{
GetWindowTextA(fe.nameEdit, fe.pilotName, sizeof(fe.pilotName) - 1);
if (fe.pilotName[0] == '\0')
{
strcpy(fe.pilotName, "Pilot");
}
strcpy(gLastPilotName, fe.pilotName);
memcpy(gPersistSelection, fe.selection, sizeof(gPersistSelection));
gHavePersist = True;
}
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);
}
//########################################################################
// 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;
}
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";
}