Files
RP412/RP_L4/RPL4FE.cpp
T
CydandClaude Fable 5 c66cb00a7e Network races: the local console marshals remote pods over the wire
The lobby-owner-as-console architecture, in-engine. RPL4CONSOLE gains
RPL4LocalConsole_InstallNetworkRace: the owner pod switches to network
mode and meshes like any pod (egg fed locally via FeedLocalEgg, which
now opens the ConsoleOnly state gate), while the console tick also
marshals REMOTE pods over NetTransport speaking the exact arcade
protocol - egg chunks with 5s-retry-until-ACK, 1Hz state polling,
RunMission once every pod stages at WaitingForLaunch, StopMission at
expiry (remotes first, local pod holds until their EndMission scores
land), score intake labeled with [pilots]-order names on the results
screen.

The front end builds the multi-pilot egg: RP412HOSTPODS lists member
console channels (lobby stand-in; the Steam lobby feeds the same
path), RP412HOSTPORT/RP412HOSTADDR set the owner side.

Winsock Connect now redials with a fresh socket per attempt (a refused
TCP socket is dead; the old loop reused it) bounded at 120s - needed
whenever a peer boots after the caller, which is the normal Steam
lobby launch order.

Verified on loopback: member pod in -net, owner hosting from its menu;
mesh completed both sides, 30s race, remote score collected over the
wire (host 3), local stop after the drain, results screen shows both
pilots by name in one process that returns to the menu.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:08:18 -05:00

1353 lines
38 KiB
C++

#include "..\munga_l4\mungal4.h"
#pragma hdrstop
#include "rpl4fe.h"
#include "rpl4console.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" },
};
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,
GroupLaunch,
GroupCount
};
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;
};
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];
const char *vehicle;
const char *color;
};
int CollectExtraPilots(const FEState *fe, ExtraPilot *extras, char *owner_address)
{
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];
sprintf(pilot->address, "%s:%d", entry, console_port + 1);
sprintf(pilot->name, "PILOT %d", extra_count + 2);
pilot->vehicle = kVehicles[
(fe->selection[GroupVehicle] + extra_count + 1) % FE_COUNT(kVehicles)].key;
pilot->color = kColors[
(fe->selection[GroupColor] + extra_count + 1) % FE_COUNT(kColors)].key;
++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, GroupMap, FE_COUNT(kMaps), col1, &y, row_h, col_w);
int y_after_maps = y;
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
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;
// 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 GroupMap: return "TRACK";
case GroupVehicle: return "VEHICLE";
case GroupColor: return "COLOR";
case GroupBadge: return "BADGE";
case GroupTime: return "TIME OF DAY";
case GroupWeather: return "WEATHER";
case GroupLength: return "RACE LENGTH";
}
return "";
}
const char *ItemName(int group, int index)
{
switch (group)
{
case GroupMap: return kMaps[index].name;
case GroupVehicle: return kVehicles[index].name;
case GroupColor: return kColors[index].name;
case GroupBadge: return kBadges[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 R A C E";
}
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
SelectObject(mem, fe->titleFont);
SetTextColor(mem, kGreenBright);
RECT title = client;
title.top = client.bottom / 28;
title.bottom = title.top + client.bottom / 10;
DrawTextA(mem, "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
{
fe->selection[item->group] = item->index;
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, race scenario).
// Single player uses the standalone address; a hosted network
// race lists the owner first plus one pilot per member pod.
//---------------------------------------------------------------
void BuildEggText(const FEState *fe, std::string &egg)
{
char line[256];
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
}
egg += "[mission]\n";
egg += "adventure=Red Planet\n";
sprintf(line, "map=%s\n", kMaps[fe->selection[GroupMap]].key);
egg += line;
egg += "scenario=race\n";
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;
}
egg += "[pilots]\n";
sprintf(line, "pilot=%s\n", owner_address);
egg += line;
for (int p = 0; p < extra_count; ++p)
{
sprintf(line, "pilot=%s\n", extras[p].address);
egg += line;
}
sprintf(line, "[%s]\n", owner_address);
egg += line;
egg += "hostType=0\n";
egg += "dropzone=one\n";
sprintf(line, "name=%s\n", fe->pilotName);
egg += line;
egg += "bitmapindex=1\n";
egg += "loadzones=1\n";
sprintf(line, "vehicle=%s\n", kVehicles[fe->selection[GroupVehicle]].key);
egg += line;
sprintf(line, "color=%s\n", kColors[fe->selection[GroupColor]].key);
egg += line;
sprintf(line, "badge=%s\n", kBadges[fe->selection[GroupBadge]].key);
egg += line;
for (int p = 0; p < extra_count; ++p)
{
sprintf(line, "[%s]\n", extras[p].address);
egg += line;
egg += "hostType=0\n";
egg += "dropzone=one\n";
sprintf(line, "name=%s\n", extras[p].name);
egg += line;
sprintf(line, "bitmapindex=%d\n", p + 2);
egg += line;
egg += "loadzones=1\n";
sprintf(line, "vehicle=%s\n", extras[p].vehicle);
egg += line;
sprintf(line, "color=%s\n", extras[p].color);
egg += line;
egg += "badge=None\n";
}
egg += "[largebitmap]\n";
sprintf(line, "bitmap=BitMap::Large::%s\n", fe->pilotName);
egg += line;
for (int p = 0; p < extra_count; ++p)
{
sprintf(line, "bitmap=BitMap::Large::%s\n", extras[p].name);
egg += line;
}
egg += "[smallbitmap]\n";
sprintf(line, "bitmap=BitMap::Small::%s\n", fe->pilotName);
egg += line;
for (int p = 0; p < extra_count; ++p)
{
sprintf(line, "bitmap=BitMap::Small::%s\n", extras[p].name);
egg += line;
}
sprintf(line, "[BitMap::Large::%s]\n", fe->pilotName);
egg += line;
AppendNameBitmap(egg, 128, 32, fe->pilotName);
egg += "width=8\n";
sprintf(line, "[BitMap::Small::%s]\n", fe->pilotName);
egg += line;
AppendNameBitmap(egg, 64, 16, fe->pilotName);
egg += "width=4\n";
for (int p = 0; p < extra_count; ++p)
{
sprintf(line, "[BitMap::Large::%s]\n", extras[p].name);
egg += line;
AppendNameBitmap(egg, 128, 32, extras[p].name);
egg += "width=8\n";
sprintf(line, "[BitMap::Small::%s]\n", extras[p].name);
egg += line;
AppendNameBitmap(egg, 64, 16, extras[p].name);
egg += "width=4\n";
}
egg += kOrdinalsBlock;
// [pilots]-order names for the network console's results
strcpy(gLastPilotNamesCsv, fe->pilotName);
for (int p = 0; p < extra_count; ++p)
{
if (strlen(gLastPilotNamesCsv) + strlen(extras[p].name) + 2 <
sizeof(gLastPilotNamesCsv))
{
strcat(gLastPilotNamesCsv, ",");
strcat(gLastPilotNamesCsv, extras[p].name);
}
}
}
}
//########################################################################
//############################ RPL4FrontEnd_Run ##########################
//########################################################################
Logical
RPL4FrontEnd_Run(
HINSTANCE instance,
HWND main_window,
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
}
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 the player launches or closes the window
//---------------------------------------------------------------
MSG msg;
while (!fe.launched && !fe.closed)
{
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;
}
}
Logical launched = fe.launched;
if (launched)
{
gLastMissionSeconds = kLengths[fe.selection[GroupLength]].seconds;
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;
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)
{
fwrite(egg.data(), 1, egg.size(), file);
fclose(file);
DEBUG_STREAM << "FrontEnd: wrote " << egg_path_out << " ("
<< (int) egg.size() << " bytes)\n" << std::flush;
}
else
{
DEBUG_STREAM << "FrontEnd: could not write " << egg_path_out
<< "\n" << std::flush;
launched = False;
}
}
DestroyWindow(fe.menuWindow);
DeleteObject(fe.textFont);
DeleteObject(fe.titleFont);
DeleteObject(fe.editBrush);
gFE = NULL;
return launched;
}
int
RPL4FrontEnd_LastMissionSeconds()
{
return gLastMissionSeconds;
}
const char *
RPL4FrontEnd_LastPilotNames()
{
return gLastPilotNamesCsv;
}
//########################################################################
//######################## 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";
}