Files
CydandClaude Fable 5 5892af318e Steam lobby: host, join, room, and launch into a marshaled race
RPL4LOBBY implements the multiplayer front door on ISteamMatchmaking.
The setup menu grows HOST STEAM RACE / JOIN STEAM RACE buttons when
the Steam wire is live; hosting creates a tagged public lobby, joining
finds one. Every member publishes FakeIP + fake ports + persona +
loadout as member data; the room screen lists members (host marked)
and gives the owner a launch button.

Launching writes a nonced go-roster into lobby data. Each pod
registers every peer with the Steam transport (two-port peer table:
engine console/game ports map to Steam fake ports on connect) and
enters the race: the owner through the hosted-race path - it builds
the multi-pilot egg from real personas and loadouts and its console
marshals everyone - and members as network pods that boot straight
into WaitingForEgg for the owner to feed over the wire.

The lobby outlives races: members loop back through WinMain into the
room (no local console needed - MissionCompleted is waived for member
races), and the owner returns to the room after its results screen.
Leaving the lobby clears the hosted-race priming.

Verified on this box: menu buttons appear under RP412STEAM=1, hosting
creates a lobby on the Steam backend, the room runs and leaves back to
the menu; single-player cycling and the LAN hosted race both still
pass. Full three-account mesh test is next, on real hardware.

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

1537 lines
43 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" },
};
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,
GroupSteamHost, // buttons, not selections (Steam builds only)
GroupSteamJoin,
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;
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];
};
// 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");
}
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];
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, 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;
// 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 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";
case GroupSteamHost: return "HOST STEAM RACE";
case GroupSteamJoin: return "JOIN STEAM RACE";
}
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 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;
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;
sprintf(line, "badge=%s\n", extras[p].badge);
egg += line;
}
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 ##########################
//########################################################################
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);
}
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";
}