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>
1537 lines
43 KiB
C++
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";
|
|
}
|