,noframe takes the title bar away, and with it the only way out of the game. The console screen now offers its own, bottom left: half width and diagonally opposite LAUNCH GAME, because it is the one button on that screen you cannot undo and it should not sit next to the one everybody is aiming for. It goes through the same door as closing the window - fe.closed, so RPL4FrontEnd_Run returns False and the race loop breaks - rather than opening a second shutdown path. Two things had to move for it to make sense. The saved placement now loads in RPL4.CPP, right after the main window is shown, instead of only when SVGA16 builds the cockpit. That was not until a mission started, so the console screen came up at the default rect with its title bar still on and the window only jumped to the saved placement once a race began - which, for a flag whose whole purpose is to take the title bar off, meant it did nothing on the screen you land on. RPL4.CPP now owns the main window's registration outright and SVGA16's branches only reload; the reload after CockpitShellProc goes on still matters, since its WM_SIZE is what re-fits the canvas. That in turn made the exploded view's position-only registration incoherent - the startup load had already applied the size - so the game window is simply position and size everywhere now. The earlier reasoning that its exploded size IS the -res render size does not hold: the back buffer stretches to the window in either view, exactly as it does for the cockpit. WM_EXITSIZEMOVE moves from the cockpit subclass to RPL4.CPP's own WndProc, which the subclass chains to anyway. In its old home it only existed once a cockpit had been built, so dragging the window on the console screen - the obvious moment to put it where you want it - saved nothing. There is also a save on the way out of WinMain, for a session that never started a race and so never ran SVGA16's teardown save. Verified: on a bare-framed window the console screen comes up at the saved 1280x760 with client == window rect, EXIT GAME ends the process with code 0, and a screenshot shows it clear of the column content. A console-only session dragged to 333,222 900x640 wrote that on the drag, kept it through the exit, and came back to exactly it on relaunch - without a race anywhere in the round trip. The noframe and cockpit round trips still pass unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2124 lines
58 KiB
C++
2124 lines
58 KiB
C++
#include "..\munga_l4\mungal4.h"
|
|
#pragma hdrstop
|
|
|
|
#include "rpl4fe.h"
|
|
#include "rpl4console.h"
|
|
#include "rpl4lobby.h"
|
|
|
|
#include <stdio.h>
|
|
#include <string>
|
|
|
|
//########################################################################
|
|
// The Death Race catalog (from TeslaConsole's RedPlanet/RPConfig.xml;
|
|
// 'headmf' is excluded for the race scenario there).
|
|
//########################################################################
|
|
|
|
namespace
|
|
{
|
|
struct CatalogEntry
|
|
{
|
|
const char *key;
|
|
const char *name;
|
|
};
|
|
|
|
const CatalogEntry kMaps[] =
|
|
{
|
|
{ "wise", "Wiseguy's Wake" },
|
|
{ "lyzlane", "Lyz's Lane" },
|
|
{ "yip", "Yip's Yahoorama" },
|
|
{ "blade", "Blade's Edge" },
|
|
{ "otto", "Berserker's Bahnzai" },
|
|
{ "frstrm", "Firestorm's Fury" },
|
|
{ "burnt", "Burnt Cookies" },
|
|
{ "brewers", "Brewer's Bane" },
|
|
{ "pain", "Paingod's Passage" },
|
|
{ "headoff", "Freezemoon's Freeway" },
|
|
};
|
|
|
|
// Football excludes pain/headoff and adds the football build of
|
|
// Freezemoon's Freeway (RPConfig.xml per-scenario invalid lists).
|
|
const CatalogEntry kFootballMaps[] =
|
|
{
|
|
{ "wise", "Wiseguy's Wake" },
|
|
{ "lyzlane", "Lyz's Lane" },
|
|
{ "yip", "Yip's Yahoorama" },
|
|
{ "blade", "Blade's Edge" },
|
|
{ "otto", "Berserker's Bahnzai" },
|
|
{ "frstrm", "Firestorm's Fury" },
|
|
{ "burnt", "Burnt Cookies" },
|
|
{ "brewers", "Brewer's Bane" },
|
|
{ "headmf", "Freezemoon's Freeway" },
|
|
};
|
|
|
|
const CatalogEntry kScenarios[] =
|
|
{
|
|
{ "race", "Death Race" },
|
|
{ "football", "Football" },
|
|
};
|
|
|
|
// Two-color teams: crushers/blockers wear the team color, the
|
|
// runner wears the runner color (that is how you spot the ball).
|
|
struct TeamEntry
|
|
{
|
|
const char *key;
|
|
const char *name;
|
|
const char *teamColor;
|
|
const char *runnerColor;
|
|
};
|
|
|
|
const TeamEntry kTeams[] =
|
|
{
|
|
{ "Red/Pink", "Red / Pink", "Red", "Pink" },
|
|
{ "Blue/Aqua", "Blue / Aqua", "Blue", "Aqua" },
|
|
{ "Green/Yellow", "Green / Yellow", "Green", "Yellow" },
|
|
{ "Black/Purple", "Black / Purple", "Black", "Purple" },
|
|
};
|
|
|
|
const CatalogEntry kPositions[] =
|
|
{
|
|
{ "runner", "Runner" },
|
|
{ "crusher", "Crusher" },
|
|
{ "blocker", "Blocker" },
|
|
};
|
|
|
|
const CatalogEntry kVehicles[] =
|
|
{
|
|
{ "quark", "Quark" },
|
|
{ "lepton", "Lepton" },
|
|
{ "proton", "Proton" },
|
|
{ "bug", "Bug" },
|
|
{ "speck", "Speck" },
|
|
{ "blkspk", "Black Speck" },
|
|
{ "chigger", "Chigger" },
|
|
{ "flea", "Flea" },
|
|
{ "roach", "Roach" },
|
|
{ "skeeter", "Skeeter" },
|
|
{ "wasp", "Wasp" },
|
|
{ "barge", "Barge" },
|
|
{ "broccoli", "Screaming Broccoli" },
|
|
{ "burro", "Burro" },
|
|
{ "mule", "Mule" },
|
|
{ "bttlbrg", "Battle Barge" },
|
|
{ "gator", "Gator" },
|
|
{ "grunt", "Grunt" },
|
|
{ "vole", "Vole" },
|
|
{ "bull", "Bull" },
|
|
{ "tarntula", "Tarantula" },
|
|
{ "mantis", "Mantis" },
|
|
{ "ripper", "Ripper" },
|
|
{ "roadblk", "Road Block" },
|
|
{ "spitter", "Spitter" },
|
|
{ "puck", "Armadillo" },
|
|
{ "dragon", "Dragon" },
|
|
};
|
|
|
|
const CatalogEntry kColors[] =
|
|
{
|
|
{ "Red", "Red" }, { "Blue", "Blue" }, { "Green", "Green" },
|
|
{ "White", "White" }, { "Pink", "Pink" }, { "Aqua", "Aqua" },
|
|
{ "Yellow", "Yellow" }, { "Purple", "Purple" }, { "Black", "Black" },
|
|
};
|
|
|
|
const CatalogEntry kBadges[] =
|
|
{
|
|
{ "None", "None" }, { "Celtic", "Celtic" }, { "Desert", "Desert" },
|
|
{ "Flames", "Flames" }, { "Giraffe", "Giraffe" }, { "Hawaii", "Hawaii" },
|
|
{ "Korean", "Korean Camo" }, { "Lightning", "Lightning" },
|
|
{ "Razzle Dazzle", "Razzle Dazzle" }, { "Rising Sun", "Rising Sun" },
|
|
{ "Tiger Stripe", "Tiger Stripes" },
|
|
};
|
|
|
|
const CatalogEntry kTimes[] =
|
|
{
|
|
{ "day", "Day" }, { "night", "Night" },
|
|
};
|
|
|
|
const CatalogEntry kWeather[] =
|
|
{
|
|
{ "clear", "Clear" }, { "fog", "Light Fog" }, { "soup", "Heavy Fog" },
|
|
};
|
|
|
|
struct LengthEntry
|
|
{
|
|
int seconds; // 0 = endless (length line omitted)
|
|
const char *name;
|
|
};
|
|
|
|
const LengthEntry kLengths[] =
|
|
{
|
|
{ 0, "Endless" },
|
|
{ 180, "3:00" },
|
|
{ 300, "5:00" },
|
|
{ 480, "8:00" },
|
|
{ 600, "10:00" },
|
|
};
|
|
|
|
// Standalone pilot address, as used by TEST.EGG: single-user mode
|
|
// adopts the first pilot entry without resolving it.
|
|
const char kLocalPilotAddress[] = "200.0.0.255";
|
|
|
|
#define FE_COUNT(t) ((int)(sizeof(t)/sizeof((t)[0])))
|
|
|
|
//---------------------------------------------------------------
|
|
// Menu model
|
|
//---------------------------------------------------------------
|
|
enum FEGroup
|
|
{
|
|
GroupMap = 0,
|
|
GroupVehicle,
|
|
GroupColor,
|
|
GroupBadge,
|
|
GroupTime,
|
|
GroupWeather,
|
|
GroupLength,
|
|
GroupScenario,
|
|
GroupTeam, // football only
|
|
GroupPosition, // football only
|
|
GroupLaunch,
|
|
GroupSteamHost, // buttons, not selections (Steam builds only)
|
|
GroupSteamJoin,
|
|
GroupExit,
|
|
GroupCount
|
|
};
|
|
|
|
Logical IsFootball(const int *selection)
|
|
{
|
|
return selection[GroupScenario] == 1;
|
|
}
|
|
|
|
const CatalogEntry *ActiveMaps(const int *selection, int *count)
|
|
{
|
|
if (IsFootball(selection))
|
|
{
|
|
*count = (int)(sizeof(kFootballMaps) / sizeof(kFootballMaps[0]));
|
|
return kFootballMaps;
|
|
}
|
|
*count = (int)(sizeof(kMaps) / sizeof(kMaps[0]));
|
|
return kMaps;
|
|
}
|
|
|
|
struct FEItem
|
|
{
|
|
int group;
|
|
int index;
|
|
RECT rect;
|
|
};
|
|
|
|
struct FEState
|
|
{
|
|
int selection[GroupCount];
|
|
char pilotName[24];
|
|
|
|
FEItem items[128];
|
|
int itemCount;
|
|
|
|
HWND menuWindow;
|
|
HWND nameEdit;
|
|
HFONT textFont;
|
|
HFONT titleFont;
|
|
HBRUSH editBrush;
|
|
Logical launched;
|
|
Logical closed;
|
|
int steamAction; // 1 = host lobby, 2 = join lobby
|
|
};
|
|
|
|
FEState *gFE = NULL;
|
|
int gLastMissionSeconds = 0;
|
|
|
|
// carried across races so cycling back to the menu keeps the
|
|
// player's loadout (and names the results screen's score rows)
|
|
int gPersistSelection[GroupCount];
|
|
Logical gHavePersist = False;
|
|
char gLastPilotName[24] = "Pilot";
|
|
|
|
// [pilots]-order names of the last launched race (owner first),
|
|
// comma separated - the network console labels results with them
|
|
char gLastPilotNamesCsv[256] = "";
|
|
|
|
//---------------------------------------------------------------
|
|
// Multiplayer host mode (lobby stand-in): RP412HOSTPODS lists the
|
|
// member pods' console channels ("ip[:port]" comma separated) and
|
|
// the egg gains one pilot per member. RP412HOSTPORT is the
|
|
// owner's console port (default 1501; game port is +1) and
|
|
// RP412HOSTADDR the owner's mesh address (default 127.0.0.1 -
|
|
// the Steam lobby supplies the FakeIP instead).
|
|
//---------------------------------------------------------------
|
|
enum { maxExtraPilots = 8 };
|
|
struct ExtraPilot
|
|
{
|
|
char address[64]; // mesh (game port) address for [pilots]
|
|
char name[32];
|
|
char vehicle[24];
|
|
char color[16];
|
|
char badge[24];
|
|
char team[32]; // football pick, "" = assign for them
|
|
char position[16];
|
|
};
|
|
|
|
// lobby-fed override (real personas + loadouts); empty = env parsing
|
|
FEHostedPilot gHostedPilots[maxExtraPilots];
|
|
int gHostedPilotCount = 0;
|
|
char gHostedOwnerAddress[64] = "";
|
|
|
|
int CollectExtraPilots(const FEState *fe, ExtraPilot *extras, char *owner_address)
|
|
{
|
|
if (gHostedPilotCount > 0)
|
|
{
|
|
sprintf(owner_address, "%s:1502", gHostedOwnerAddress);
|
|
for (int i = 0; i < gHostedPilotCount; ++i)
|
|
{
|
|
strcpy(extras[i].address, gHostedPilots[i].address);
|
|
strcpy(extras[i].name,
|
|
gHostedPilots[i].name[0] ? gHostedPilots[i].name : "Pilot");
|
|
strcpy(extras[i].vehicle,
|
|
gHostedPilots[i].vehicle[0] ? gHostedPilots[i].vehicle : "speck");
|
|
strcpy(extras[i].color,
|
|
gHostedPilots[i].color[0] ? gHostedPilots[i].color : "Red");
|
|
strcpy(extras[i].badge,
|
|
gHostedPilots[i].badge[0] ? gHostedPilots[i].badge : "None");
|
|
strcpy(extras[i].team, gHostedPilots[i].team);
|
|
strcpy(extras[i].position, gHostedPilots[i].position);
|
|
}
|
|
return gHostedPilotCount;
|
|
}
|
|
|
|
const char *host_pods = getenv("RP412HOSTPODS");
|
|
if (host_pods == NULL || host_pods[0] == '\0')
|
|
{
|
|
return -1; // single player: standalone address
|
|
}
|
|
|
|
int host_port = 1501;
|
|
const char *port_override = getenv("RP412HOSTPORT");
|
|
if (port_override != NULL && atoi(port_override) > 0)
|
|
{
|
|
host_port = atoi(port_override);
|
|
}
|
|
const char *host_address = getenv("RP412HOSTADDR");
|
|
if (host_address == NULL || host_address[0] == '\0')
|
|
{
|
|
host_address = "127.0.0.1";
|
|
}
|
|
sprintf(owner_address, "%s:%d", host_address, host_port + 1);
|
|
|
|
int extra_count = 0;
|
|
const char *cursor = host_pods;
|
|
while (*cursor != '\0' && extra_count < maxExtraPilots)
|
|
{
|
|
char entry[64];
|
|
int length = 0;
|
|
while (cursor[length] != '\0' && cursor[length] != ',' &&
|
|
length < (int) sizeof(entry) - 1)
|
|
{
|
|
entry[length] = cursor[length];
|
|
++length;
|
|
}
|
|
entry[length] = '\0';
|
|
cursor += length;
|
|
if (*cursor == ',')
|
|
{
|
|
++cursor;
|
|
}
|
|
|
|
int console_port = 1501;
|
|
char *colon = strrchr(entry, ':');
|
|
if (colon != NULL)
|
|
{
|
|
*colon = '\0';
|
|
console_port = atoi(colon + 1);
|
|
}
|
|
|
|
ExtraPilot *pilot = &extras[extra_count];
|
|
memset(pilot, 0, sizeof(*pilot));
|
|
sprintf(pilot->address, "%s:%d", entry, console_port + 1);
|
|
sprintf(pilot->name, "PILOT %d", extra_count + 2);
|
|
strcpy(pilot->vehicle, kVehicles[
|
|
(fe->selection[GroupVehicle] + extra_count + 1) % FE_COUNT(kVehicles)].key);
|
|
strcpy(pilot->color, kColors[
|
|
(fe->selection[GroupColor] + extra_count + 1) % FE_COUNT(kColors)].key);
|
|
strcpy(pilot->badge, "None");
|
|
++extra_count;
|
|
}
|
|
return extra_count;
|
|
}
|
|
|
|
const COLORREF kGreenBright = RGB(64, 255, 64);
|
|
const COLORREF kGreenDim = RGB(24, 140, 24);
|
|
const COLORREF kBlack = RGB(0, 0, 0);
|
|
|
|
//---------------------------------------------------------------
|
|
// Layout: three columns of lists + the launch button
|
|
//---------------------------------------------------------------
|
|
void AddGroupItems(
|
|
FEState *fe, int group, int count,
|
|
int x, int *y, int row_h, int width)
|
|
{
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
FEItem *item = &fe->items[fe->itemCount++];
|
|
item->group = group;
|
|
item->index = i;
|
|
item->rect.left = x;
|
|
item->rect.top = *y;
|
|
item->rect.right = x + width;
|
|
item->rect.bottom = *y + row_h;
|
|
*y += row_h;
|
|
}
|
|
*y += row_h; // gap below the group
|
|
}
|
|
|
|
void LayoutMenu(FEState *fe, int client_w, int client_h)
|
|
{
|
|
fe->itemCount = 0;
|
|
|
|
int row_h = client_h / 36;
|
|
if (row_h < 18) row_h = 18;
|
|
if (row_h > 30) row_h = 30;
|
|
|
|
int col_w = client_w / 5;
|
|
int col1 = client_w / 14;
|
|
int col2 = col1 + col_w + client_w / 28;
|
|
int col3 = col2 + col_w + client_w / 28;
|
|
int top = client_h / 7;
|
|
|
|
int y = top;
|
|
AddGroupItems(fe, GroupScenario, FE_COUNT(kScenarios), col1, &y, row_h, col_w);
|
|
int map_count;
|
|
ActiveMaps(fe->selection, &map_count);
|
|
AddGroupItems(fe, GroupMap, map_count, col1, &y, row_h, col_w);
|
|
AddGroupItems(fe, GroupTime, FE_COUNT(kTimes), col1, &y, row_h, col_w);
|
|
AddGroupItems(fe, GroupWeather, FE_COUNT(kWeather), col1, &y, row_h, col_w);
|
|
AddGroupItems(fe, GroupLength, FE_COUNT(kLengths), col1, &y, row_h, col_w);
|
|
|
|
y = top;
|
|
AddGroupItems(fe, GroupVehicle, FE_COUNT(kVehicles), col2, &y, row_h, col_w);
|
|
|
|
y = top + 2 * row_h; // leave room for the name edit + header
|
|
if (IsFootball(fe->selection))
|
|
{
|
|
AddGroupItems(fe, GroupTeam, FE_COUNT(kTeams), col3, &y, row_h, col_w);
|
|
AddGroupItems(fe, GroupPosition, FE_COUNT(kPositions), col3, &y, row_h, col_w);
|
|
}
|
|
else
|
|
{
|
|
AddGroupItems(fe, GroupColor, FE_COUNT(kColors), col3, &y, row_h, col_w);
|
|
AddGroupItems(fe, GroupBadge, FE_COUNT(kBadges), col3, &y, row_h, col_w);
|
|
}
|
|
|
|
// launch button
|
|
FEItem *launch = &fe->items[fe->itemCount++];
|
|
launch->group = GroupLaunch;
|
|
launch->index = 0;
|
|
launch->rect.left = col3;
|
|
launch->rect.top = client_h - 3 * row_h;
|
|
launch->rect.right = col3 + col_w;
|
|
launch->rect.bottom = client_h - row_h;
|
|
|
|
// Steam lobby buttons, offered whenever environ.ini asked for Steam.
|
|
// Painting greys them out and says so if the wire never came up.
|
|
if (RPL4Lobby_Configured())
|
|
{
|
|
FEItem *host = &fe->items[fe->itemCount++];
|
|
host->group = GroupSteamHost;
|
|
host->index = 0;
|
|
host->rect.left = 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;
|
|
}
|
|
|
|
//
|
|
// Exit, bottom left. Diagonally opposite LAUNCH on purpose: it is
|
|
// the one button on this screen you cannot undo, so it does not go
|
|
// next to the one people are aiming for. Half width for the same
|
|
// reason - it is a way out, not a peer of LAUNCH.
|
|
//
|
|
// The window frame used to be the way out, and mfd_layout.cfg's
|
|
// ,noframe takes it away, so the screen has to offer its own.
|
|
//
|
|
FEItem *quit = &fe->items[fe->itemCount++];
|
|
quit->group = GroupExit;
|
|
quit->index = 0;
|
|
quit->rect.left = col1;
|
|
quit->rect.top = client_h - 2 * row_h;
|
|
quit->rect.right = col1 + col_w / 2;
|
|
quit->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 GroupScenario: return "SCENARIO";
|
|
case GroupMap: return "TRACK";
|
|
case GroupVehicle: return "VEHICLE";
|
|
case GroupColor: return "COLOR";
|
|
case GroupBadge: return "BADGE";
|
|
case GroupTeam: return "TEAM";
|
|
case GroupPosition: return "POSITION";
|
|
case GroupTime: return "TIME OF DAY";
|
|
case GroupWeather: return "WEATHER";
|
|
case GroupLength: return "GAME LENGTH";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// the map rows need the active scenario's list
|
|
const int *gItemNameSelection = NULL;
|
|
|
|
const char *ItemName(int group, int index)
|
|
{
|
|
switch (group)
|
|
{
|
|
case GroupScenario: return kScenarios[index].name;
|
|
case GroupMap:
|
|
if (gItemNameSelection != NULL && IsFootball(gItemNameSelection))
|
|
{
|
|
return kFootballMaps[index].name;
|
|
}
|
|
return kMaps[index].name;
|
|
case GroupVehicle: return kVehicles[index].name;
|
|
case GroupColor: return kColors[index].name;
|
|
case GroupBadge: return kBadges[index].name;
|
|
case GroupTeam: return kTeams[index].name;
|
|
case GroupPosition: return kPositions[index].name;
|
|
case GroupTime: return kTimes[index].name;
|
|
case GroupWeather: return kWeather[index].name;
|
|
case GroupLength: return kLengths[index].name;
|
|
case GroupLaunch: return "L A U N C H G A M E";
|
|
case GroupSteamHost: return "HOST STEAM GAME";
|
|
case GroupSteamJoin: return "JOIN STEAM GAME";
|
|
case GroupExit: return "EXIT GAME";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Painting (double buffered, pod green on black)
|
|
//---------------------------------------------------------------
|
|
void PaintMenu(FEState *fe)
|
|
{
|
|
PAINTSTRUCT ps;
|
|
HDC hdc = BeginPaint(fe->menuWindow, &ps);
|
|
|
|
RECT client;
|
|
GetClientRect(fe->menuWindow, &client);
|
|
|
|
HDC mem = CreateCompatibleDC(hdc);
|
|
HBITMAP surface = CreateCompatibleBitmap(hdc, client.right, client.bottom);
|
|
HBITMAP old_surface = (HBITMAP) SelectObject(mem, surface);
|
|
|
|
FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH));
|
|
SetBkMode(mem, TRANSPARENT);
|
|
|
|
// title
|
|
gItemNameSelection = fe->selection;
|
|
SelectObject(mem, fe->titleFont);
|
|
SetTextColor(mem, kGreenBright);
|
|
RECT title = client;
|
|
title.top = client.bottom / 28;
|
|
title.bottom = title.top + client.bottom / 10;
|
|
DrawTextA(mem,
|
|
IsFootball(fe->selection)
|
|
? "RED PLANET - MARTIAN FOOTBALL"
|
|
: "RED PLANET - MARTIAN DEATH RACE",
|
|
-1, &title, DT_CENTER | DT_TOP | DT_SINGLELINE);
|
|
|
|
SelectObject(mem, fe->textFont);
|
|
|
|
// 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)
|
|
{
|
|
//
|
|
// The lobby buttons are offered whenever environ.ini asked
|
|
// for Steam, but they only work once the client is actually
|
|
// there. Dim them and say why rather than leaving them out -
|
|
// two buttons quietly missing looks like a broken build.
|
|
//
|
|
Logical steam_button =
|
|
(item->group == GroupSteamHost || item->group == GroupSteamJoin);
|
|
Logical dead = steam_button && !RPL4Lobby_Available();
|
|
|
|
COLORREF ink = dead ? kGreenDim : kGreenBright;
|
|
HBRUSH launch_brush = CreateSolidBrush(ink);
|
|
FrameRect(mem, &row, launch_brush);
|
|
DeleteObject(launch_brush);
|
|
SetTextColor(mem, ink);
|
|
DrawTextA(mem, ItemName(item->group, item->index), -1, &row,
|
|
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
|
|
|
//
|
|
// The reason, on its own line above the pair. It does not
|
|
// fit inside a button - they are about sixteen characters
|
|
// wide and this is more than twice that.
|
|
//
|
|
if (dead && item->group == GroupSteamHost)
|
|
{
|
|
RECT notice = row;
|
|
notice.bottom = row.top;
|
|
notice.top = row.top - (row.bottom - row.top);
|
|
SetTextColor(mem, kGreenDim);
|
|
DrawTextA(mem, "STEAM NOT RUNNING", -1, ¬ice,
|
|
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)
|
|
{
|
|
// dead until the Steam client is there; the button says so
|
|
if (RPL4Lobby_Available())
|
|
{
|
|
fe->steamAction = (item->group == GroupSteamHost) ? 1 : 2;
|
|
PostMessageA(fe->menuWindow, WM_NULL, 0, 0);
|
|
}
|
|
}
|
|
else if (item->group == GroupExit)
|
|
{
|
|
// the same door closing the window goes through
|
|
fe->closed = True;
|
|
PostMessageA(fe->menuWindow, WM_NULL, 0, 0);
|
|
}
|
|
else
|
|
{
|
|
fe->selection[item->group] = item->index;
|
|
if (item->group == GroupScenario)
|
|
{
|
|
// the scenario swaps the map list and the
|
|
// color/badge vs team/position columns
|
|
int map_count;
|
|
ActiveMaps(fe->selection, &map_count);
|
|
if (fe->selection[GroupMap] >= map_count)
|
|
{
|
|
fe->selection[GroupMap] = 0;
|
|
}
|
|
RECT client;
|
|
GetClientRect(fe->menuWindow, &client);
|
|
LayoutMenu(fe, client.right, client.bottom);
|
|
}
|
|
InvalidateRect(fe->menuWindow, NULL, FALSE);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
LRESULT CALLBACK
|
|
FrontEndWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
FEState *fe = gFE;
|
|
|
|
switch (message)
|
|
{
|
|
case WM_PAINT:
|
|
if (fe != NULL)
|
|
{
|
|
PaintMenu(fe);
|
|
return 0;
|
|
}
|
|
break;
|
|
|
|
case WM_LBUTTONDOWN:
|
|
if (fe != NULL)
|
|
{
|
|
MenuClick(fe, (int)(short) LOWORD(lParam), (int)(short) HIWORD(lParam));
|
|
return 0;
|
|
}
|
|
break;
|
|
|
|
case WM_CTLCOLOREDIT:
|
|
if (fe != NULL)
|
|
{
|
|
SetTextColor((HDC) wParam, kGreenBright);
|
|
SetBkColor((HDC) wParam, kBlack);
|
|
return (LRESULT) fe->editBrush;
|
|
}
|
|
break;
|
|
|
|
case WM_ERASEBKGND:
|
|
return 1;
|
|
}
|
|
return DefWindowProcA(hwnd, message, wParam, lParam);
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Plasma name bitmaps: white text auto-fit into WxH, thresholded
|
|
// to 1bpp hex rows - the C++ twin of the console's PlasmaBitmaps.
|
|
//---------------------------------------------------------------
|
|
void AppendNameBitmap(std::string &egg, int width, int height, const char *name)
|
|
{
|
|
BITMAPINFO info;
|
|
memset(&info, 0, sizeof(info));
|
|
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
|
info.bmiHeader.biWidth = width;
|
|
info.bmiHeader.biHeight = -height; // top-down
|
|
info.bmiHeader.biPlanes = 1;
|
|
info.bmiHeader.biBitCount = 32;
|
|
info.bmiHeader.biCompression = BI_RGB;
|
|
|
|
void *bits = NULL;
|
|
HDC dc = CreateCompatibleDC(NULL);
|
|
HBITMAP bitmap = CreateDIBSection(dc, &info, DIB_RGB_COLORS, &bits, NULL, 0);
|
|
HBITMAP old_bitmap = (HBITMAP) SelectObject(dc, bitmap);
|
|
|
|
memset(bits, 0, width * height * 4);
|
|
SetBkMode(dc, TRANSPARENT);
|
|
SetTextColor(dc, RGB(255, 255, 255));
|
|
|
|
// shrink the font until the name fits (console: 24pt downward)
|
|
int point_size = 24;
|
|
for (;;)
|
|
{
|
|
HFONT font = CreateFontA(
|
|
-MulDiv(point_size, GetDeviceCaps(dc, LOGPIXELSY), 72),
|
|
0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
|
|
ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
|
|
NONANTIALIASED_QUALITY, DEFAULT_PITCH | FF_DONTCARE,
|
|
"Microsoft Sans Serif");
|
|
HFONT old_font = (HFONT) SelectObject(dc, font);
|
|
|
|
SIZE extent;
|
|
GetTextExtentPoint32A(dc, name, (int) strlen(name), &extent);
|
|
if ((extent.cx <= width && extent.cy <= height) || point_size <= 1)
|
|
{
|
|
RECT rect = { 0, 0, width, height };
|
|
DrawTextA(dc, name, -1, &rect,
|
|
DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
|
|
SelectObject(dc, old_font);
|
|
DeleteObject(font);
|
|
break;
|
|
}
|
|
SelectObject(dc, old_font);
|
|
DeleteObject(font);
|
|
point_size = (point_size <= 2) ? (point_size / 2) : (point_size - 2);
|
|
}
|
|
|
|
GdiFlush();
|
|
const unsigned long *pixels = (const unsigned long *) bits;
|
|
char hex[8];
|
|
for (int row = 0; row < height; ++row)
|
|
{
|
|
egg += "bitmap=";
|
|
for (int byte_index = 0; byte_index < width / 8; ++byte_index)
|
|
{
|
|
unsigned char packed = 0;
|
|
for (int bit = 0; bit < 8; ++bit)
|
|
{
|
|
unsigned long pixel = pixels[row * width + byte_index * 8 + bit];
|
|
if ((pixel & 0xFF) >= 128) // white text on black
|
|
{
|
|
packed |= (unsigned char)(128 >> bit);
|
|
}
|
|
}
|
|
sprintf(hex, "%02X", packed);
|
|
egg += hex;
|
|
}
|
|
egg += "\n";
|
|
}
|
|
sprintf(hex, "%d", width);
|
|
egg += "x="; egg += hex; egg += "\n";
|
|
sprintf(hex, "%d", height);
|
|
egg += "y="; egg += hex; egg += "\n";
|
|
|
|
SelectObject(dc, old_bitmap);
|
|
DeleteObject(bitmap);
|
|
DeleteDC(dc);
|
|
}
|
|
|
|
// The 1st-4th place plasma graphics, verbatim from the console
|
|
// (RPMission.AppendOrdinals) / TEST.EGG.
|
|
extern const char *kOrdinalsBlock;
|
|
|
|
//---------------------------------------------------------------
|
|
// Egg assembly (console RPMission.ToEggString / RPFootballMission).
|
|
// Single player uses the standalone address; a hosted network game
|
|
// lists the owner first plus one pilot per member pod. Football
|
|
// adds team=/position= per pilot and the [teams] blocks; the
|
|
// owner's team/position picks seed a deterministic assignment
|
|
// across two teams (each team's first member without a runner
|
|
// becomes one; the rest alternate crusher/blocker; the runner
|
|
// wears the team's runner color, everyone else the team color).
|
|
//---------------------------------------------------------------
|
|
void BuildEggText(const FEState *fe, std::string &egg)
|
|
{
|
|
char line[256];
|
|
Logical football = IsFootball(fe->selection);
|
|
|
|
ExtraPilot extras[maxExtraPilots];
|
|
char owner_address[64];
|
|
strcpy(owner_address, kLocalPilotAddress);
|
|
int extra_count = CollectExtraPilots(fe, extras, owner_address);
|
|
if (extra_count < 0)
|
|
{
|
|
extra_count = 0; // single player
|
|
}
|
|
|
|
//
|
|
// One flat pilot table: the owner then the extras
|
|
//
|
|
struct PilotEntry
|
|
{
|
|
const char *address;
|
|
const char *name;
|
|
const char *vehicle;
|
|
const char *color;
|
|
const char *badge;
|
|
int team; // kTeams index, -1 outside football
|
|
const char *position;
|
|
Logical chosePosition; // picked it themselves
|
|
};
|
|
PilotEntry pilots[maxExtraPilots + 1];
|
|
int pilot_count = 0;
|
|
|
|
PilotEntry *owner = &pilots[pilot_count++];
|
|
owner->address = owner_address;
|
|
owner->name = fe->pilotName;
|
|
owner->vehicle = kVehicles[fe->selection[GroupVehicle]].key;
|
|
owner->color = kColors[fe->selection[GroupColor]].key;
|
|
owner->badge = kBadges[fe->selection[GroupBadge]].key;
|
|
owner->team = -1;
|
|
owner->position = NULL;
|
|
owner->chosePosition = False;
|
|
for (int p = 0; p < extra_count; ++p)
|
|
{
|
|
PilotEntry *pilot = &pilots[pilot_count++];
|
|
pilot->address = extras[p].address;
|
|
pilot->name = extras[p].name;
|
|
pilot->vehicle = extras[p].vehicle;
|
|
pilot->color = extras[p].color;
|
|
pilot->badge = extras[p].badge;
|
|
pilot->team = -1;
|
|
pilot->position = NULL;
|
|
pilot->chosePosition = False;
|
|
}
|
|
|
|
//
|
|
// Football: every pilot's own team/position pick is honored;
|
|
// unset picks get filled in, then each team is normalized to
|
|
// exactly one runner (the console's rule).
|
|
//
|
|
if (football)
|
|
{
|
|
//
|
|
// 1. Teams: the host's pick, then each member's, then
|
|
// alternate whoever did not choose across the two
|
|
// most-used teams so the sides stay balanced.
|
|
//
|
|
owner->team = fe->selection[GroupTeam];
|
|
for (int p = 1; p < pilot_count; ++p)
|
|
{
|
|
for (int t = 0; t < FE_COUNT(kTeams); ++t)
|
|
{
|
|
if (strcmp(extras[p - 1].team, kTeams[t].key) == 0)
|
|
{
|
|
pilots[p].team = t;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
int fallback_team = (owner->team + 1) % FE_COUNT(kTeams);
|
|
int unassigned = 0;
|
|
for (int p = 1; p < pilot_count; ++p)
|
|
{
|
|
if (pilots[p].team < 0)
|
|
{
|
|
pilots[p].team = (unassigned++ % 2) ? owner->team : fallback_team;
|
|
}
|
|
}
|
|
|
|
//
|
|
// 2. Positions: honor each pick, default the rest to the
|
|
// line, then give every team exactly one runner.
|
|
//
|
|
for (int p = 1; p < pilot_count; ++p)
|
|
{
|
|
const char *pick = extras[p - 1].position;
|
|
for (int i = 0; i < FE_COUNT(kPositions); ++i)
|
|
{
|
|
if (strcmp(pick, kPositions[i].key) == 0)
|
|
{
|
|
pilots[p].position = kPositions[i].key;
|
|
pilots[p].chosePosition = True;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
owner->position = kPositions[fe->selection[GroupPosition]].key;
|
|
owner->chosePosition = True;
|
|
|
|
for (int t = 0; t < FE_COUNT(kTeams); ++t)
|
|
{
|
|
int members = 0, runner = -1, spare = -1, line = 0;
|
|
for (int p = 0; p < pilot_count; ++p)
|
|
{
|
|
if (pilots[p].team != t)
|
|
{
|
|
continue;
|
|
}
|
|
++members;
|
|
if (spare < 0 && !pilots[p].chosePosition)
|
|
{
|
|
spare = p; // nobody's plans to disturb
|
|
}
|
|
if (pilots[p].position != NULL &&
|
|
strcmp(pilots[p].position, "runner") == 0)
|
|
{
|
|
if (runner < 0)
|
|
{
|
|
runner = p; // first runner keeps the ball
|
|
}
|
|
else
|
|
{
|
|
// two pilots claimed it - the later one lines up
|
|
pilots[p].position = NULL;
|
|
pilots[p].chosePosition = False;
|
|
}
|
|
}
|
|
}
|
|
if (members == 0)
|
|
{
|
|
continue;
|
|
}
|
|
if (runner < 0 && spare >= 0)
|
|
{
|
|
// no volunteer: the first pilot without a pick of
|
|
// their own runs (explicit picks are never overridden)
|
|
pilots[spare].position = "runner";
|
|
runner = spare;
|
|
}
|
|
for (int p = 0; p < pilot_count; ++p)
|
|
{
|
|
if (pilots[p].team == t && p != runner &&
|
|
pilots[p].position == NULL)
|
|
{
|
|
pilots[p].position = (line++ % 2) ? "crusher" : "blocker";
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// 3. Colors follow team and position (runner wears the
|
|
// team's runner color - that is how you spot the ball)
|
|
//
|
|
for (int p = 0; p < pilot_count; ++p)
|
|
{
|
|
const TeamEntry *team = &kTeams[pilots[p].team];
|
|
pilots[p].color =
|
|
(strcmp(pilots[p].position, "runner") == 0)
|
|
? team->runnerColor : team->teamColor;
|
|
pilots[p].badge = "None";
|
|
}
|
|
}
|
|
|
|
//
|
|
// [mission]
|
|
//
|
|
int map_count;
|
|
const CatalogEntry *maps = ActiveMaps(fe->selection, &map_count);
|
|
|
|
egg += "[mission]\n";
|
|
egg += "adventure=Red Planet\n";
|
|
sprintf(line, "map=%s\n", maps[fe->selection[GroupMap]].key);
|
|
egg += line;
|
|
sprintf(line, "scenario=%s\n", kScenarios[fe->selection[GroupScenario]].key);
|
|
egg += line;
|
|
sprintf(line, "time=%s\n", kTimes[fe->selection[GroupTime]].key);
|
|
egg += line;
|
|
sprintf(line, "weather=%s\n", kWeather[fe->selection[GroupWeather]].key);
|
|
egg += line;
|
|
egg += "temperature=0\n";
|
|
egg += "compression=0\n";
|
|
int seconds = kLengths[fe->selection[GroupLength]].seconds;
|
|
if (seconds > 0)
|
|
{
|
|
sprintf(line, "length=%d\n", seconds);
|
|
egg += line;
|
|
}
|
|
|
|
//
|
|
// [pilots] + per-pilot sections
|
|
//
|
|
egg += "[pilots]\n";
|
|
for (int p = 0; p < pilot_count; ++p)
|
|
{
|
|
sprintf(line, "pilot=%s\n", pilots[p].address);
|
|
egg += line;
|
|
}
|
|
|
|
for (int p = 0; p < pilot_count; ++p)
|
|
{
|
|
sprintf(line, "[%s]\n", pilots[p].address);
|
|
egg += line;
|
|
egg += "hostType=0\n";
|
|
egg += "dropzone=one\n";
|
|
sprintf(line, "name=%s\n", pilots[p].name);
|
|
egg += line;
|
|
sprintf(line, "bitmapindex=%d\n", p + 1);
|
|
egg += line;
|
|
egg += "loadzones=1\n";
|
|
sprintf(line, "vehicle=%s\n", pilots[p].vehicle);
|
|
egg += line;
|
|
sprintf(line, "color=%s\n", pilots[p].color);
|
|
egg += line;
|
|
sprintf(line, "badge=%s\n", pilots[p].badge);
|
|
egg += line;
|
|
if (football)
|
|
{
|
|
sprintf(line, "team=team::%s\n", kTeams[pilots[p].team].key);
|
|
egg += line;
|
|
sprintf(line, "position=%s\n", pilots[p].position);
|
|
egg += line;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Football team blocks (console RPFootballMission layout)
|
|
//
|
|
if (football)
|
|
{
|
|
std::string teams_list = "[teams]\n";
|
|
std::string team_sections, pilots_sections;
|
|
std::string teambitmap_list = "[teambitmap]\n";
|
|
std::string teambitmap_sections;
|
|
int bitmap_index = 1;
|
|
for (int side = 0; side < FE_COUNT(kTeams); ++side)
|
|
{
|
|
Logical team_present = False;
|
|
for (int p = 0; p < pilot_count; ++p)
|
|
{
|
|
if (pilots[p].team == side)
|
|
{
|
|
team_present = True;
|
|
}
|
|
}
|
|
if (!team_present)
|
|
{
|
|
continue;
|
|
}
|
|
const char *team_key = kTeams[side].key;
|
|
sprintf(line, "team=team::%s\n", team_key);
|
|
teams_list += line;
|
|
sprintf(line, "[team::%s]\nbitmapindex=%d\npilots=pilots::%s\n",
|
|
team_key, bitmap_index++, team_key);
|
|
team_sections += line;
|
|
sprintf(line, "[pilots::%s]\n", team_key);
|
|
pilots_sections += line;
|
|
for (int p = 0; p < pilot_count; ++p)
|
|
{
|
|
if (pilots[p].team == side)
|
|
{
|
|
sprintf(line, "pilot=%s\n", pilots[p].address);
|
|
pilots_sections += line;
|
|
}
|
|
}
|
|
sprintf(line, "bitmap=BitMap::Small::team::%s\n", team_key);
|
|
teambitmap_list += line;
|
|
sprintf(line, "[BitMap::Small::team::%s]\n", team_key);
|
|
teambitmap_sections += line;
|
|
AppendNameBitmap(teambitmap_sections, 64, 16, team_key);
|
|
teambitmap_sections += "width=4\n";
|
|
}
|
|
egg += teams_list;
|
|
egg += team_sections;
|
|
egg += pilots_sections;
|
|
egg += teambitmap_list;
|
|
egg += teambitmap_sections;
|
|
}
|
|
|
|
//
|
|
// Name bitmaps + ordinals
|
|
//
|
|
egg += "[largebitmap]\n";
|
|
for (int p = 0; p < pilot_count; ++p)
|
|
{
|
|
sprintf(line, "bitmap=BitMap::Large::%s\n", pilots[p].name);
|
|
egg += line;
|
|
}
|
|
egg += "[smallbitmap]\n";
|
|
for (int p = 0; p < pilot_count; ++p)
|
|
{
|
|
sprintf(line, "bitmap=BitMap::Small::%s\n", pilots[p].name);
|
|
egg += line;
|
|
}
|
|
for (int p = 0; p < pilot_count; ++p)
|
|
{
|
|
sprintf(line, "[BitMap::Large::%s]\n", pilots[p].name);
|
|
egg += line;
|
|
AppendNameBitmap(egg, 128, 32, pilots[p].name);
|
|
egg += "width=8\n";
|
|
sprintf(line, "[BitMap::Small::%s]\n", pilots[p].name);
|
|
egg += line;
|
|
AppendNameBitmap(egg, 64, 16, pilots[p].name);
|
|
egg += "width=4\n";
|
|
}
|
|
|
|
egg += kOrdinalsBlock;
|
|
|
|
// [pilots]-order names for the network console's results
|
|
strcpy(gLastPilotNamesCsv, fe->pilotName);
|
|
for (int p = 1; p < pilot_count; ++p)
|
|
{
|
|
if (strlen(gLastPilotNamesCsv) + strlen(pilots[p].name) + 2 <
|
|
sizeof(gLastPilotNamesCsv))
|
|
{
|
|
strcat(gLastPilotNamesCsv, ",");
|
|
strcat(gLastPilotNamesCsv, pilots[p].name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//########################################################################
|
|
//############################ RPL4FrontEnd_Run ##########################
|
|
//########################################################################
|
|
|
|
static int gLastLaunchMode = FELaunchSingle;
|
|
|
|
//---------------------------------------------------------------
|
|
// Build frontend.egg from the persisted loadout (the menu persists
|
|
// on every exit; lobby launches can happen without a live menu)
|
|
//---------------------------------------------------------------
|
|
static Logical
|
|
BuildEggFromPersisted(char *egg_path_out, int egg_path_size)
|
|
{
|
|
FEState fe;
|
|
memset(&fe, 0, sizeof(fe));
|
|
strcpy(fe.pilotName, gLastPilotName);
|
|
if (gHavePersist)
|
|
{
|
|
memcpy(fe.selection, gPersistSelection, sizeof(fe.selection));
|
|
}
|
|
else
|
|
{
|
|
fe.selection[GroupLength] = 2; // 5:00
|
|
}
|
|
gLastMissionSeconds = kLengths[fe.selection[GroupLength]].seconds;
|
|
|
|
std::string egg;
|
|
egg.reserve(16384);
|
|
BuildEggText(&fe, egg);
|
|
|
|
strncpy(egg_path_out, "frontend.egg", egg_path_size - 1);
|
|
egg_path_out[egg_path_size - 1] = '\0';
|
|
|
|
FILE *file = fopen(egg_path_out, "wb");
|
|
if (file == NULL)
|
|
{
|
|
DEBUG_STREAM << "FrontEnd: could not write " << egg_path_out
|
|
<< "\n" << std::flush;
|
|
return False;
|
|
}
|
|
fwrite(egg.data(), 1, egg.size(), file);
|
|
fclose(file);
|
|
DEBUG_STREAM << "FrontEnd: wrote " << egg_path_out << " ("
|
|
<< (int) egg.size() << " bytes)\n" << std::flush;
|
|
return True;
|
|
}
|
|
|
|
Logical
|
|
RPL4FrontEnd_Run(
|
|
HINSTANCE instance,
|
|
HWND main_window,
|
|
char *egg_path_out,
|
|
int egg_path_size
|
|
)
|
|
{
|
|
gLastLaunchMode = FELaunchSingle;
|
|
|
|
//---------------------------------------------------------------
|
|
// Coming back from a race while still in a lobby: straight to
|
|
// the room (the lobby outlives races - single binary payoff)
|
|
//---------------------------------------------------------------
|
|
if (RPL4Lobby_InRoom())
|
|
{
|
|
int outcome = RPL4Lobby_Room(instance, main_window);
|
|
if (outcome == LobbyRoomClosed)
|
|
{
|
|
return False;
|
|
}
|
|
if (outcome == LobbyLaunchMember)
|
|
{
|
|
gLastLaunchMode = FELaunchMember;
|
|
egg_path_out[0] = '\0';
|
|
return True;
|
|
}
|
|
if (outcome == LobbyLaunchHost)
|
|
{
|
|
gLastLaunchMode = FELaunchHost;
|
|
return BuildEggFromPersisted(egg_path_out, egg_path_size);
|
|
}
|
|
// LobbyRoomLeft: fall through to the setup menu
|
|
}
|
|
|
|
for (;;)
|
|
{
|
|
FEState fe;
|
|
memset(&fe, 0, sizeof(fe));
|
|
strcpy(fe.pilotName, gLastPilotName);
|
|
if (gHavePersist)
|
|
{
|
|
memcpy(fe.selection, gPersistSelection, sizeof(fe.selection));
|
|
}
|
|
else
|
|
{
|
|
fe.selection[GroupLength] = 2; // 5:00
|
|
}
|
|
gFE = &fe;
|
|
|
|
//---------------------------------------------------------------
|
|
// Window class + fonts
|
|
//---------------------------------------------------------------
|
|
static Logical class_registered = False;
|
|
if (!class_registered)
|
|
{
|
|
class_registered = True;
|
|
WNDCLASSA window_class;
|
|
memset(&window_class, 0, sizeof(window_class));
|
|
window_class.style = CS_HREDRAW | CS_VREDRAW;
|
|
window_class.lpfnWndProc = FrontEndWndProc;
|
|
window_class.hInstance = instance;
|
|
window_class.hCursor = LoadCursor(NULL, IDC_ARROW);
|
|
window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
|
|
window_class.lpszClassName = "RPFrontEnd";
|
|
RegisterClassA(&window_class);
|
|
}
|
|
|
|
RECT client;
|
|
GetClientRect(main_window, &client);
|
|
|
|
fe.menuWindow = CreateWindowExA(
|
|
0, "RPFrontEnd", "",
|
|
WS_CHILD | WS_VISIBLE,
|
|
0, 0, client.right, client.bottom,
|
|
main_window, NULL, instance, NULL);
|
|
if (fe.menuWindow == NULL)
|
|
{
|
|
gFE = NULL;
|
|
return False;
|
|
}
|
|
|
|
int text_h = client.bottom / 40;
|
|
if (text_h < 16) text_h = 16;
|
|
if (text_h > 24) text_h = 24;
|
|
fe.textFont = CreateFontA(-text_h, 0, 0, 0, FW_NORMAL,
|
|
FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS,
|
|
CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN,
|
|
"Consolas");
|
|
fe.titleFont = CreateFontA(-(text_h * 2), 0, 0, 0, FW_BOLD,
|
|
FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS,
|
|
CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN,
|
|
"Consolas");
|
|
fe.editBrush = CreateSolidBrush(kBlack);
|
|
|
|
fe.nameEdit = CreateWindowExA(
|
|
0, "EDIT", fe.pilotName,
|
|
WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
|
|
0, 0, 10, 10,
|
|
fe.menuWindow, NULL, instance, NULL);
|
|
SendMessageA(fe.nameEdit, WM_SETFONT, (WPARAM) fe.textFont, TRUE);
|
|
SendMessageA(fe.nameEdit, EM_SETLIMITTEXT, sizeof(fe.pilotName) - 2, 0);
|
|
|
|
LayoutMenu(&fe, client.right, client.bottom);
|
|
InvalidateRect(fe.menuWindow, NULL, TRUE);
|
|
|
|
//---------------------------------------------------------------
|
|
// Modal loop until launch, lobby button, or close
|
|
//---------------------------------------------------------------
|
|
MSG msg;
|
|
while (!fe.launched && !fe.closed && fe.steamAction == 0)
|
|
{
|
|
BOOL result = GetMessageA(&msg, NULL, 0, 0);
|
|
if (result <= 0)
|
|
{
|
|
fe.closed = True;
|
|
break;
|
|
}
|
|
if (msg.message == WM_QUIT ||
|
|
(msg.message == WM_SYSCOMMAND && msg.wParam == SC_CLOSE))
|
|
{
|
|
fe.closed = True;
|
|
}
|
|
// closing the main window ends the front end too
|
|
if (msg.message == WM_CLOSE && msg.hwnd == main_window)
|
|
{
|
|
fe.closed = True;
|
|
}
|
|
TranslateMessage(&msg);
|
|
DispatchMessageA(&msg);
|
|
if (!IsWindow(main_window))
|
|
{
|
|
fe.closed = True;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Harvest the loadout whichever way we leave the menu - the
|
|
// lobby publishes it as member data, launches build from it
|
|
//---------------------------------------------------------------
|
|
if (!fe.closed)
|
|
{
|
|
GetWindowTextA(fe.nameEdit, fe.pilotName, sizeof(fe.pilotName) - 1);
|
|
if (fe.pilotName[0] == '\0')
|
|
{
|
|
strcpy(fe.pilotName, "Pilot");
|
|
}
|
|
strcpy(gLastPilotName, fe.pilotName);
|
|
memcpy(gPersistSelection, fe.selection, sizeof(gPersistSelection));
|
|
gHavePersist = True;
|
|
}
|
|
|
|
Logical launched = fe.launched;
|
|
Logical closed = fe.closed;
|
|
int steam_action = fe.steamAction;
|
|
|
|
DestroyWindow(fe.menuWindow);
|
|
DeleteObject(fe.textFont);
|
|
DeleteObject(fe.titleFont);
|
|
DeleteObject(fe.editBrush);
|
|
gFE = NULL;
|
|
|
|
if (closed)
|
|
{
|
|
return False;
|
|
}
|
|
if (launched)
|
|
{
|
|
// plain launch: single player, or a LAN-hosted race when
|
|
// RP412HOSTPODS is set in the environment
|
|
return BuildEggFromPersisted(egg_path_out, egg_path_size);
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Steam lobby (host or join), then back here if they leave
|
|
//---------------------------------------------------------------
|
|
int outcome = (steam_action == 1)
|
|
? RPL4Lobby_Host(instance, main_window)
|
|
: RPL4Lobby_Join(instance, main_window);
|
|
if (outcome == LobbyRoomClosed)
|
|
{
|
|
return False;
|
|
}
|
|
if (outcome == LobbyLaunchMember)
|
|
{
|
|
gLastLaunchMode = FELaunchMember;
|
|
egg_path_out[0] = '\0';
|
|
return True;
|
|
}
|
|
if (outcome == LobbyLaunchHost)
|
|
{
|
|
gLastLaunchMode = FELaunchHost;
|
|
return BuildEggFromPersisted(egg_path_out, egg_path_size);
|
|
}
|
|
// LobbyRoomLeft: show the menu again
|
|
}
|
|
}
|
|
|
|
int
|
|
RPL4FrontEnd_LastMissionSeconds()
|
|
{
|
|
return gLastMissionSeconds;
|
|
}
|
|
|
|
const char *
|
|
RPL4FrontEnd_LastPilotNames()
|
|
{
|
|
return gLastPilotNamesCsv;
|
|
}
|
|
|
|
int
|
|
RPL4FrontEnd_LastLaunchMode()
|
|
{
|
|
return gLastLaunchMode;
|
|
}
|
|
|
|
void
|
|
RPL4FrontEnd_GetLoadout(char *vehicle, char *color, char *badge)
|
|
{
|
|
int v = gHavePersist ? gPersistSelection[GroupVehicle] : 0;
|
|
int c = gHavePersist ? gPersistSelection[GroupColor] : 0;
|
|
int b = gHavePersist ? gPersistSelection[GroupBadge] : 0;
|
|
strcpy(vehicle, kVehicles[v].key);
|
|
strcpy(color, kColors[c].key);
|
|
strcpy(badge, kBadges[b].key);
|
|
}
|
|
|
|
//########################################################################
|
|
// The host's mission setup, as display names.
|
|
//
|
|
// The lobby shows everyone what they are about to fly into, and only the
|
|
// owner knows it - so the owner publishes these into lobby data and the
|
|
// members read them back. Names rather than catalog keys: the map lists
|
|
// differ between race and football, so resolving here means the room
|
|
// never has to know which scenario's table a key came from.
|
|
//########################################################################
|
|
|
|
const char *
|
|
RPL4FrontEnd_SelectedMapName()
|
|
{
|
|
if (!gHavePersist)
|
|
{
|
|
return kMaps[0].name;
|
|
}
|
|
|
|
int count = 0;
|
|
const CatalogEntry *maps = ActiveMaps(gPersistSelection, &count);
|
|
int index = gPersistSelection[GroupMap];
|
|
if (index < 0 || index >= count)
|
|
{
|
|
index = 0;
|
|
}
|
|
return maps[index].name;
|
|
}
|
|
|
|
const char *
|
|
RPL4FrontEnd_SelectedTimeName()
|
|
{
|
|
int index = gHavePersist ? gPersistSelection[GroupTime] : 0;
|
|
if (index < 0 || index >= FE_COUNT(kTimes))
|
|
{
|
|
index = 0;
|
|
}
|
|
return kTimes[index].name;
|
|
}
|
|
|
|
const char *
|
|
RPL4FrontEnd_SelectedWeatherName()
|
|
{
|
|
int index = gHavePersist ? gPersistSelection[GroupWeather] : 0;
|
|
if (index < 0 || index >= FE_COUNT(kWeather))
|
|
{
|
|
index = 0;
|
|
}
|
|
return kWeather[index].name;
|
|
}
|
|
|
|
const char *
|
|
RPL4FrontEnd_SelectedLengthName()
|
|
{
|
|
// the menu defaults this one to 5:00, not to entry zero
|
|
int index = gHavePersist ? gPersistSelection[GroupLength] : 2;
|
|
if (index < 0 || index >= FE_COUNT(kLengths))
|
|
{
|
|
index = 0;
|
|
}
|
|
return kLengths[index].name;
|
|
}
|
|
|
|
//########################################################################
|
|
// Catalog key -> display name. Member data travels as keys, so the room
|
|
// needs these to show "Battle Barge" where the wire says "bttlbrg".
|
|
//########################################################################
|
|
|
|
namespace
|
|
{
|
|
const char *
|
|
NameForKey(const CatalogEntry *table, int count, const char *key)
|
|
{
|
|
if (key == NULL || key[0] == '\0')
|
|
{
|
|
return "";
|
|
}
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
if (strcmp(table[i].key, key) == 0)
|
|
{
|
|
return table[i].name;
|
|
}
|
|
}
|
|
return key; // unknown to this build: show it raw rather than blank
|
|
}
|
|
}
|
|
|
|
const char *
|
|
RPL4FrontEnd_VehicleNameForKey(const char *key)
|
|
{
|
|
return NameForKey(kVehicles, FE_COUNT(kVehicles), key);
|
|
}
|
|
|
|
const char *
|
|
RPL4FrontEnd_PositionNameForKey(const char *key)
|
|
{
|
|
return NameForKey(kPositions, FE_COUNT(kPositions), key);
|
|
}
|
|
|
|
const char *
|
|
RPL4FrontEnd_TeamNameForKey(const char *key)
|
|
{
|
|
if (key == NULL || key[0] == '\0')
|
|
{
|
|
return "";
|
|
}
|
|
for (int i = 0; i < FE_COUNT(kTeams); ++i)
|
|
{
|
|
if (strcmp(kTeams[i].key, key) == 0)
|
|
{
|
|
return kTeams[i].name;
|
|
}
|
|
}
|
|
return key;
|
|
}
|
|
|
|
//########################################################################
|
|
// Football team / position picks (the lobby publishes and cycles these)
|
|
//########################################################################
|
|
|
|
int
|
|
RPL4FrontEnd_TeamCount()
|
|
{
|
|
return FE_COUNT(kTeams);
|
|
}
|
|
|
|
const char *
|
|
RPL4FrontEnd_TeamName(int index)
|
|
{
|
|
if (index < 0 || index >= FE_COUNT(kTeams))
|
|
{
|
|
return "";
|
|
}
|
|
return kTeams[index].name;
|
|
}
|
|
|
|
const char *
|
|
RPL4FrontEnd_TeamKey(int index)
|
|
{
|
|
if (index < 0 || index >= FE_COUNT(kTeams))
|
|
{
|
|
return "";
|
|
}
|
|
return kTeams[index].key;
|
|
}
|
|
|
|
int
|
|
RPL4FrontEnd_PositionCount()
|
|
{
|
|
return FE_COUNT(kPositions);
|
|
}
|
|
|
|
const char *
|
|
RPL4FrontEnd_PositionName(int index)
|
|
{
|
|
if (index < 0 || index >= FE_COUNT(kPositions))
|
|
{
|
|
return "";
|
|
}
|
|
return kPositions[index].name;
|
|
}
|
|
|
|
const char *
|
|
RPL4FrontEnd_PositionKey(int index)
|
|
{
|
|
if (index < 0 || index >= FE_COUNT(kPositions))
|
|
{
|
|
return "";
|
|
}
|
|
return kPositions[index].key;
|
|
}
|
|
|
|
int
|
|
RPL4FrontEnd_GetTeamIndex()
|
|
{
|
|
return gHavePersist ? gPersistSelection[GroupTeam] : 0;
|
|
}
|
|
|
|
void
|
|
RPL4FrontEnd_SetTeamIndex(int index)
|
|
{
|
|
if (index < 0 || index >= FE_COUNT(kTeams))
|
|
{
|
|
return;
|
|
}
|
|
gPersistSelection[GroupTeam] = index;
|
|
gHavePersist = True;
|
|
}
|
|
|
|
int
|
|
RPL4FrontEnd_GetPositionIndex()
|
|
{
|
|
return gHavePersist ? gPersistSelection[GroupPosition] : 0;
|
|
}
|
|
|
|
void
|
|
RPL4FrontEnd_SetPositionIndex(int index)
|
|
{
|
|
if (index < 0 || index >= FE_COUNT(kPositions))
|
|
{
|
|
return;
|
|
}
|
|
gPersistSelection[GroupPosition] = index;
|
|
gHavePersist = True;
|
|
}
|
|
|
|
Logical
|
|
RPL4FrontEnd_IsFootballSelected()
|
|
{
|
|
return gHavePersist && IsFootball(gPersistSelection);
|
|
}
|
|
|
|
void
|
|
RPL4FrontEnd_SetHostedPilots(
|
|
const char *owner_address,
|
|
const FEHostedPilot *pilots,
|
|
int count
|
|
)
|
|
{
|
|
if (count > maxExtraPilots)
|
|
{
|
|
count = maxExtraPilots;
|
|
}
|
|
gHostedPilotCount = (pilots != NULL) ? count : 0;
|
|
for (int i = 0; i < gHostedPilotCount; ++i)
|
|
{
|
|
gHostedPilots[i] = pilots[i];
|
|
}
|
|
strncpy(gHostedOwnerAddress,
|
|
(owner_address != NULL) ? owner_address : "",
|
|
sizeof(gHostedOwnerAddress) - 1);
|
|
gHostedOwnerAddress[sizeof(gHostedOwnerAddress) - 1] = '\0';
|
|
}
|
|
|
|
//########################################################################
|
|
//######################## RPL4FrontEnd_ShowResults ######################
|
|
//########################################################################
|
|
|
|
namespace
|
|
{
|
|
struct ResultRow
|
|
{
|
|
char name[32];
|
|
int score;
|
|
};
|
|
|
|
struct ResultsState
|
|
{
|
|
HWND window;
|
|
HFONT textFont;
|
|
HFONT titleFont;
|
|
ResultRow rows[16];
|
|
int rowCount;
|
|
RECT continueRect;
|
|
Logical dismissed;
|
|
Logical closed;
|
|
};
|
|
|
|
ResultsState *gRS = NULL;
|
|
|
|
const char *PlaceName(int place)
|
|
{
|
|
static const char *kPlaces[] = { "1ST", "2ND", "3RD", "4TH" };
|
|
static char other[8];
|
|
if (place < 4)
|
|
{
|
|
return kPlaces[place];
|
|
}
|
|
sprintf(other, "%dTH", place + 1);
|
|
return other;
|
|
}
|
|
|
|
void PaintResults(ResultsState *rs)
|
|
{
|
|
PAINTSTRUCT ps;
|
|
HDC hdc = BeginPaint(rs->window, &ps);
|
|
|
|
RECT client;
|
|
GetClientRect(rs->window, &client);
|
|
|
|
HDC mem = CreateCompatibleDC(hdc);
|
|
HBITMAP surface = CreateCompatibleBitmap(hdc, client.right, client.bottom);
|
|
HBITMAP old_surface = (HBITMAP) SelectObject(mem, surface);
|
|
|
|
FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH));
|
|
SetBkMode(mem, TRANSPARENT);
|
|
|
|
SelectObject(mem, rs->titleFont);
|
|
SetTextColor(mem, kGreenBright);
|
|
RECT title = client;
|
|
title.top = client.bottom / 28;
|
|
title.bottom = title.top + client.bottom / 10;
|
|
DrawTextA(mem, "RACE RESULTS", -1, &title,
|
|
DT_CENTER | DT_TOP | DT_SINGLELINE);
|
|
|
|
SelectObject(mem, rs->textFont);
|
|
int row_h = client.bottom / 16;
|
|
int top = client.bottom / 4;
|
|
int left = client.right / 3;
|
|
int right = 2 * client.right / 3;
|
|
char text[48];
|
|
for (int i = 0; i < rs->rowCount; ++i)
|
|
{
|
|
RECT row;
|
|
row.left = left;
|
|
row.right = right;
|
|
row.top = top + i * row_h;
|
|
row.bottom = row.top + row_h;
|
|
|
|
SetTextColor(mem, (i == 0) ? kGreenBright : kGreenDim);
|
|
DrawTextA(mem, PlaceName(i), -1, &row,
|
|
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
|
|
|
RECT name = row;
|
|
name.left += (right - left) / 5;
|
|
DrawTextA(mem, rs->rows[i].name, -1, &name,
|
|
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
|
|
|
sprintf(text, "%d", rs->rows[i].score);
|
|
DrawTextA(mem, text, -1, &row,
|
|
DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
|
|
}
|
|
|
|
HBRUSH frame_brush = CreateSolidBrush(kGreenBright);
|
|
FrameRect(mem, &rs->continueRect, frame_brush);
|
|
DeleteObject(frame_brush);
|
|
SetTextColor(mem, kGreenBright);
|
|
DrawTextA(mem, "C O N T I N U E", -1, &rs->continueRect,
|
|
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
|
|
|
BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);
|
|
|
|
SelectObject(mem, old_surface);
|
|
DeleteObject(surface);
|
|
DeleteDC(mem);
|
|
EndPaint(rs->window, &ps);
|
|
}
|
|
|
|
LRESULT CALLBACK
|
|
ResultsWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
ResultsState *rs = gRS;
|
|
|
|
switch (message)
|
|
{
|
|
case WM_PAINT:
|
|
if (rs != NULL)
|
|
{
|
|
PaintResults(rs);
|
|
return 0;
|
|
}
|
|
break;
|
|
|
|
case WM_LBUTTONDOWN:
|
|
if (rs != NULL)
|
|
{
|
|
POINT point = { (int)(short) LOWORD(lParam), (int)(short) HIWORD(lParam) };
|
|
if (PtInRect(&rs->continueRect, point))
|
|
{
|
|
rs->dismissed = True;
|
|
PostMessageA(rs->window, WM_NULL, 0, 0);
|
|
}
|
|
return 0;
|
|
}
|
|
break;
|
|
|
|
case WM_ERASEBKGND:
|
|
return 1;
|
|
}
|
|
return DefWindowProcA(hwnd, message, wParam, lParam);
|
|
}
|
|
}
|
|
|
|
Logical
|
|
RPL4FrontEnd_ShowResults(
|
|
HINSTANCE instance,
|
|
HWND main_window
|
|
)
|
|
{
|
|
int result_count = RPL4LocalConsole_ResultCount();
|
|
if (result_count <= 0)
|
|
{
|
|
return True; // nothing to show (first boot)
|
|
}
|
|
if (!IsWindow(main_window))
|
|
{
|
|
return False;
|
|
}
|
|
|
|
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";
|
|
}
|