CBS_OWNERDRAWFIXED only hands over the item area, so while the list rows came out green on black, the closed box kept the system's frame and drop arrow - a white/grey Windows control sitting in the middle of a black panel. The closed box is painted here now: black field, dim green border, bright green text, and a plain green triangle instead of a themed button. The control keeps doing everything else, including dropping its list, so this is a subclass over WM_PAINT rather than a reimplementation. Still system-drawn: the scrollbar inside a dropped list, which only appears on the two lists longer than twelve rows - vehicle and track. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2680 lines
75 KiB
C++
2680 lines
75 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" },
|
|
|
|
// Maps that came with the promoted resource file. The console
|
|
// config brackets their names in dashes, which is how it marks
|
|
// the ones that are not original arcade tracks.
|
|
{ "arena", "-- Arena (Small) --" },
|
|
{ "demoderb", "-- Arena (Large) --" },
|
|
{ "donut", "-- Donut --" },
|
|
{ "tourdemars", "-- Tour De Mars --" },
|
|
{ "trough", "-- Ares' Armpits --" },
|
|
{ "wiserguys", "-- Wiserguy's --" },
|
|
{ "rpweekend2014", "-- Zaxxis --" },
|
|
};
|
|
|
|
// 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" },
|
|
|
|
// Restored with the promoted resource file: cut from the .bld
|
|
// after 4.10 shipped, back now that RPL4.RES carries them again.
|
|
// Names from the console's own RPConfig.xml.
|
|
{ "dark", "Blacker Puck" },
|
|
{ "blktrn", "Black Tarantula" },
|
|
{ "neut", "Neutrino" },
|
|
|
|
// Community variants that came with the same archive.
|
|
{ "blkrpuck", "Blacker Armadillo" },
|
|
{ "blkrbroc", "Blacker Broccoli" },
|
|
{ "blkrdragon", "Blacker Dragon" },
|
|
{ "blkrlepton", "Blacker Lepton" },
|
|
{ "blkrquark", "Blacker Quark" },
|
|
{ "blkrspk", "Blacker Speck" },
|
|
{ "blkrtran", "Blacker Tarantula" },
|
|
{ "blkrwasp", "Blacker Wasp" },
|
|
};
|
|
|
|
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;
|
|
|
|
// Every list group is a drop-down. They are owner-drawn so they
|
|
// keep the green-on-black panel look instead of arriving in
|
|
// system colours, and their labels are painted by PaintMenu.
|
|
HWND combo[GroupCount];
|
|
RECT comboLabel[GroupCount];
|
|
|
|
HFONT textFont;
|
|
HFONT titleFont;
|
|
HBRUSH editBrush;
|
|
Logical launched;
|
|
Logical closed;
|
|
int steamAction; // 1 = host lobby, 2 = join lobby
|
|
};
|
|
|
|
FEState *gFE = NULL;
|
|
|
|
// ItemName() reads this to answer with the right track list.
|
|
const int *gItemNameSelection = 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";
|
|
|
|
//---------------------------------------------------------------
|
|
// What the player set last time, in pilot.cfg beside bindings.txt:
|
|
// the callsign they typed and the loadout they picked.
|
|
//
|
|
// The loadout has always survived a race - gPersistSelection below
|
|
// is why the menu reopens the way you left it - but only for as
|
|
// long as the process lives. BT411 keeps the same things in
|
|
// fe_last.ini and had to, since it relaunches between missions;
|
|
// RP412 stayed in one process and so never needed a file. Closing
|
|
// the game was still a reset, which is what this fixes.
|
|
//
|
|
// KEY=VALUE like environ.ini, one line per group.
|
|
//---------------------------------------------------------------
|
|
const char kPilotFileName[] = "pilot.cfg";
|
|
|
|
//
|
|
// Stable file keys, independent of the on-screen headings - those
|
|
// carry spaces ("TIME OF DAY") and are free to be reworded.
|
|
//
|
|
struct GroupKey
|
|
{
|
|
int group;
|
|
const char *key;
|
|
};
|
|
const GroupKey kGroupKeys[] =
|
|
{
|
|
{ GroupScenario, "scenario" }, { GroupMap, "track" },
|
|
{ GroupVehicle, "vehicle" }, { GroupColor, "color" },
|
|
{ GroupBadge, "badge" }, { GroupTeam, "team" },
|
|
{ GroupPosition, "position" }, { GroupTime, "time" },
|
|
{ GroupWeather, "weather" }, { GroupLength, "length" },
|
|
};
|
|
|
|
//
|
|
// How many rows a group offers, so a stale or hand-edited index
|
|
// cannot select past the end of a list. The track list is the one
|
|
// that moves - football and the death race carry different maps -
|
|
// so it answers for whichever scenario is selected.
|
|
//
|
|
int GroupSize(int group, const int *selection)
|
|
{
|
|
switch (group)
|
|
{
|
|
case GroupScenario: return FE_COUNT(kScenarios);
|
|
case GroupMap:
|
|
{
|
|
int count = 0;
|
|
ActiveMaps(selection, &count);
|
|
return count;
|
|
}
|
|
case GroupVehicle: return FE_COUNT(kVehicles);
|
|
case GroupColor: return FE_COUNT(kColors);
|
|
case GroupBadge: return FE_COUNT(kBadges);
|
|
case GroupTeam: return FE_COUNT(kTeams);
|
|
case GroupPosition: return FE_COUNT(kPositions);
|
|
case GroupTime: return FE_COUNT(kTimes);
|
|
case GroupWeather: return FE_COUNT(kWeather);
|
|
case GroupLength: return FE_COUNT(kLengths);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
//
|
|
// A callsign is quoted into frontend.egg, joined into a
|
|
// comma-separated list for the results screen, and published as
|
|
// Steam lobby member data. Anything that could end a token early
|
|
// therefore has to go - a comma alone would split one pilot into
|
|
// two on the score sheet. Applied to what is typed as well as to
|
|
// what is read back, so the file cannot hold what the game will
|
|
// not accept.
|
|
//
|
|
void SanitizeCallsign(char *name, int size)
|
|
{
|
|
char clean[64];
|
|
int out = 0;
|
|
for (int i = 0; name[i] != '\0' && out < (int) sizeof(clean) - 1; ++i)
|
|
{
|
|
unsigned char c = (unsigned char) name[i];
|
|
if (c < 32 || c > 126) continue; // controls, high bytes
|
|
if (c == ',' || c == '"') continue; // egg and CSV delimiters
|
|
if (c == '#' || c == ';') continue; // pilot.cfg comment marks
|
|
clean[out++] = (char) c;
|
|
}
|
|
clean[out] = '\0';
|
|
|
|
char *start = clean;
|
|
while (*start == ' ' || *start == '\t')
|
|
{
|
|
++start;
|
|
}
|
|
int end = (int) strlen(start);
|
|
while (end > 0 && (start[end - 1] == ' ' || start[end - 1] == '\t'))
|
|
{
|
|
start[--end] = '\0';
|
|
}
|
|
|
|
if (start[0] == '\0')
|
|
{
|
|
strcpy(start, "Pilot");
|
|
}
|
|
strncpy(name, start, size - 1);
|
|
name[size - 1] = '\0';
|
|
}
|
|
|
|
void SavePilotSettings(const char *name, const int *selection)
|
|
{
|
|
FILE *file = fopen(kPilotFileName, "wt");
|
|
if (file == NULL)
|
|
{
|
|
DEBUG_STREAM << "FrontEnd: could not write " << kPilotFileName
|
|
<< "\n" << std::flush;
|
|
return;
|
|
}
|
|
fputs("# RP412 pilot settings, written by the game on the way out of\n"
|
|
"# the setup screen. Delete this file to start over.\n", file);
|
|
fprintf(file, "callsign=%s\n", name);
|
|
if (selection != NULL)
|
|
{
|
|
for (int i = 0; i < FE_COUNT(kGroupKeys); ++i)
|
|
{
|
|
fprintf(file, "%s=%d\n", kGroupKeys[i].key,
|
|
selection[kGroupKeys[i].group]);
|
|
}
|
|
}
|
|
fclose(file);
|
|
DEBUG_STREAM << "FrontEnd: saved callsign \"" << name << "\""
|
|
<< ((selection != NULL) ? " and loadout" : "")
|
|
<< " to " << kPilotFileName << "\n" << std::flush;
|
|
}
|
|
|
|
//
|
|
// Read once per run. Absent or unreadable simply leaves the
|
|
// built-in defaults in place - a missing file is a first run, not
|
|
// an error, and every value is range-checked so a hand-edited or
|
|
// out-of-date file cannot select past the end of a list.
|
|
//
|
|
void EnsurePilotSettingsLoaded()
|
|
{
|
|
static Logical loaded = False;
|
|
if (loaded)
|
|
{
|
|
return;
|
|
}
|
|
loaded = True;
|
|
|
|
FILE *file = fopen(kPilotFileName, "rt");
|
|
if (file == NULL)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int selection[GroupCount];
|
|
memset(selection, 0, sizeof(selection));
|
|
selection[GroupLength] = 2; // 5:00, as the menu defaults
|
|
Logical have_loadout = False;
|
|
|
|
char line[256];
|
|
while (fgets(line, sizeof(line), file) != NULL)
|
|
{
|
|
char *cursor = line;
|
|
while (*cursor == ' ' || *cursor == '\t')
|
|
{
|
|
++cursor;
|
|
}
|
|
if (*cursor == '#' || *cursor == ';')
|
|
{
|
|
continue;
|
|
}
|
|
char *equals = strchr(cursor, '=');
|
|
if (equals == NULL)
|
|
{
|
|
continue;
|
|
}
|
|
*equals = '\0';
|
|
char *key = cursor;
|
|
char *value = equals + 1;
|
|
while (*value == ' ' || *value == '\t')
|
|
{
|
|
++value;
|
|
}
|
|
char *newline = strpbrk(value, "\r\n");
|
|
if (newline != NULL)
|
|
{
|
|
*newline = '\0';
|
|
}
|
|
|
|
if (_stricmp(key, "callsign") == 0)
|
|
{
|
|
char candidate[24];
|
|
strncpy(candidate, value, sizeof(candidate) - 1);
|
|
candidate[sizeof(candidate) - 1] = '\0';
|
|
SanitizeCallsign(candidate, sizeof(candidate));
|
|
strcpy(gLastPilotName, candidate);
|
|
continue;
|
|
}
|
|
for (int i = 0; i < FE_COUNT(kGroupKeys); ++i)
|
|
{
|
|
if (_stricmp(key, kGroupKeys[i].key) != 0)
|
|
{
|
|
continue;
|
|
}
|
|
int index = atoi(value);
|
|
if (index >= 0 && index < GroupSize(kGroupKeys[i].group, selection))
|
|
{
|
|
selection[kGroupKeys[i].group] = index;
|
|
have_loadout = True;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
fclose(file);
|
|
|
|
//
|
|
// The track list belongs to the scenario, and the file is read in
|
|
// whatever order it was written, so re-check the track once the
|
|
// scenario is settled - the same clamp the menu applies when the
|
|
// scenario is switched by hand.
|
|
//
|
|
if (have_loadout)
|
|
{
|
|
int map_count = 0;
|
|
ActiveMaps(selection, &map_count);
|
|
if (selection[GroupMap] >= map_count)
|
|
{
|
|
selection[GroupMap] = 0;
|
|
}
|
|
memcpy(gPersistSelection, selection, sizeof(gPersistSelection));
|
|
gHavePersist = True;
|
|
}
|
|
|
|
DEBUG_STREAM << "FrontEnd: callsign \"" << gLastPilotName << "\""
|
|
<< (have_loadout ? " and loadout" : "")
|
|
<< " from " << kPilotFileName << "\n" << std::flush;
|
|
}
|
|
|
|
// [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
|
|
//---------------------------------------------------------------
|
|
|
|
//---------------------------------------------------------------
|
|
// The groups that became drop-downs, in the order they are laid
|
|
// out. Scenario is deliberately NOT one of them: it decides what
|
|
// the others contain, so it stays visible as a pair of buttons.
|
|
//---------------------------------------------------------------
|
|
int ComboGroups(const int *selection, int *groups)
|
|
{
|
|
int n = 0;
|
|
groups[n++] = GroupMap;
|
|
groups[n++] = GroupTime;
|
|
groups[n++] = GroupWeather;
|
|
groups[n++] = GroupLength;
|
|
groups[n++] = GroupVehicle;
|
|
if (IsFootball(selection))
|
|
{
|
|
groups[n++] = GroupTeam;
|
|
groups[n++] = GroupPosition;
|
|
}
|
|
else
|
|
{
|
|
groups[n++] = GroupColor;
|
|
groups[n++] = GroupBadge;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
// The combo id is its group, so CBN_SELCHANGE says which list moved.
|
|
const int kComboIdBase = 100;
|
|
|
|
//---------------------------------------------------------------
|
|
// CBS_OWNERDRAWFIXED only hands us the item area: the frame around
|
|
// the closed box and its drop arrow are still drawn by the system,
|
|
// which lands a white/grey Windows control in the middle of a black
|
|
// panel. So the closed box is painted here instead - black field,
|
|
// dim green border, bright green text and arrow - and the control
|
|
// keeps doing everything else, including the list it drops.
|
|
//---------------------------------------------------------------
|
|
WNDPROC gComboProc = NULL;
|
|
|
|
LRESULT CALLBACK ComboSubclassProc(
|
|
HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
if (message == WM_ERASEBKGND)
|
|
{
|
|
return 1;
|
|
}
|
|
if (message == WM_PAINT)
|
|
{
|
|
PAINTSTRUCT ps;
|
|
HDC dc = BeginPaint(hwnd, &ps);
|
|
|
|
RECT rc;
|
|
GetClientRect(hwnd, &rc);
|
|
|
|
HBRUSH field = CreateSolidBrush(kBlack);
|
|
FillRect(dc, &rc, field);
|
|
DeleteObject(field);
|
|
|
|
HBRUSH edge = CreateSolidBrush(kGreenDim);
|
|
FrameRect(dc, &rc, edge);
|
|
DeleteObject(edge);
|
|
|
|
int arrow_w = rc.bottom - rc.top;
|
|
|
|
char text[128];
|
|
text[0] = '\0';
|
|
int sel = (int) SendMessageA(hwnd, CB_GETCURSEL, 0, 0);
|
|
if (sel >= 0)
|
|
{
|
|
SendMessageA(hwnd, CB_GETLBTEXT, sel, (LPARAM) text);
|
|
}
|
|
|
|
HFONT font = (HFONT) SendMessageA(hwnd, WM_GETFONT, 0, 0);
|
|
HGDIOBJ old_font = (font != NULL) ? SelectObject(dc, font) : NULL;
|
|
SetBkMode(dc, TRANSPARENT);
|
|
SetTextColor(dc, kGreenBright);
|
|
|
|
RECT label = rc;
|
|
label.left += 6;
|
|
label.right -= arrow_w;
|
|
DrawTextA(dc, text, -1, &label,
|
|
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
|
|
|
// the arrow, a plain triangle rather than a themed button
|
|
int cx = rc.right - arrow_w / 2;
|
|
int cy = (rc.top + rc.bottom) / 2;
|
|
int r = arrow_w / 6;
|
|
POINT tri[3];
|
|
tri[0].x = cx - r; tri[0].y = cy - r / 2;
|
|
tri[1].x = cx + r; tri[1].y = cy - r / 2;
|
|
tri[2].x = cx; tri[2].y = cy + r;
|
|
HBRUSH tip = CreateSolidBrush(kGreenBright);
|
|
HGDIOBJ old_brush = SelectObject(dc, tip);
|
|
HGDIOBJ old_pen = SelectObject(dc, GetStockObject(NULL_PEN));
|
|
Polygon(dc, tri, 3);
|
|
SelectObject(dc, old_pen);
|
|
SelectObject(dc, old_brush);
|
|
DeleteObject(tip);
|
|
|
|
if (old_font != NULL) SelectObject(dc, old_font);
|
|
EndPaint(hwnd, &ps);
|
|
return 0;
|
|
}
|
|
return CallWindowProcA(gComboProc, hwnd, message, wParam, lParam);
|
|
}
|
|
|
|
void DestroyCombos(FEState *fe)
|
|
{
|
|
for (int g = 0; g < GroupCount; ++g)
|
|
{
|
|
if (fe->combo[g] != NULL)
|
|
{
|
|
DestroyWindow(fe->combo[g]);
|
|
fe->combo[g] = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
// defined below, next to the catalogs it reads
|
|
const char *ItemName(int group, int index);
|
|
|
|
void LayoutMenu(FEState *fe, int client_w, int client_h)
|
|
{
|
|
fe->itemCount = 0;
|
|
gItemNameSelection = fe->selection;
|
|
|
|
int row_h = client_h / 36;
|
|
if (row_h < 18) row_h = 18;
|
|
if (row_h > 30) row_h = 30;
|
|
|
|
int top = client_h / 7;
|
|
|
|
//---------------------------------------------------------------
|
|
// Every list is a drop-down, so the menu no longer grows with the
|
|
// content: eight controls instead of ninety-odd rows. Two columns
|
|
// of label-over-box, settings on the left and loadout on the right,
|
|
// with the scenario left as visible buttons because it decides what
|
|
// the other lists contain.
|
|
//---------------------------------------------------------------
|
|
int margin = client_w / 14;
|
|
int gap = client_w / 20;
|
|
int col_w = (client_w - 2 * margin - gap) / 2;
|
|
if (col_w < 1) col_w = 1;
|
|
int col2 = margin + col_w + gap;
|
|
|
|
int block_h = row_h * 5 / 2; // label, box, breathing room
|
|
|
|
// scenario: buttons, top of the left column
|
|
int y = top;
|
|
fe->comboLabel[GroupScenario].left = margin;
|
|
fe->comboLabel[GroupScenario].right = margin + col_w;
|
|
fe->comboLabel[GroupScenario].top = y;
|
|
fe->comboLabel[GroupScenario].bottom = y + row_h;
|
|
y += row_h;
|
|
for (int i = 0; i < FE_COUNT(kScenarios); ++i)
|
|
{
|
|
FEItem *item = &fe->items[fe->itemCount++];
|
|
item->group = GroupScenario;
|
|
item->index = i;
|
|
item->rect.left = margin + i * (col_w / 2);
|
|
item->rect.top = y;
|
|
item->rect.right = item->rect.left + col_w / 2 - row_h / 3;
|
|
item->rect.bottom = y + row_h;
|
|
}
|
|
y += row_h + row_h / 2;
|
|
|
|
int groups[GroupCount];
|
|
int group_count = ComboGroups(fe->selection, groups);
|
|
|
|
// left column takes the mission settings, right the loadout; the
|
|
// split is where the vehicle list starts. The pilot name heads
|
|
// the loadout column - it is who you are, so it reads before
|
|
// what you are driving.
|
|
int left_y = y;
|
|
int right_y = top;
|
|
int name_y = right_y;
|
|
right_y += block_h;
|
|
for (int g = 0; g < group_count; ++g)
|
|
{
|
|
int group = groups[g];
|
|
Logical left = (group == GroupMap || group == GroupTime ||
|
|
group == GroupWeather || group == GroupLength);
|
|
int x = left ? margin : col2;
|
|
int *slot = left ? &left_y : &right_y;
|
|
|
|
fe->comboLabel[group].left = x;
|
|
fe->comboLabel[group].right = x + col_w;
|
|
fe->comboLabel[group].top = *slot;
|
|
fe->comboLabel[group].bottom = *slot + row_h;
|
|
*slot += block_h;
|
|
}
|
|
|
|
|
|
// launch button
|
|
FEItem *launch = &fe->items[fe->itemCount++];
|
|
launch->group = GroupLaunch;
|
|
launch->index = 0;
|
|
launch->rect.left = col2;
|
|
launch->rect.top = client_h - 3 * row_h;
|
|
launch->rect.right = col2 + 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 = col2;
|
|
host->rect.top = client_h - 6 * row_h;
|
|
host->rect.right = col2 + 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 = col2;
|
|
join->rect.top = client_h - (9 * row_h) / 2;
|
|
join->rect.right = col2 + 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 = margin;
|
|
quit->rect.top = client_h - 2 * row_h;
|
|
quit->rect.right = margin + col_w / 2;
|
|
quit->rect.bottom = client_h - row_h;
|
|
|
|
// pilot name, under the loadout column
|
|
fe->comboLabel[GroupLaunch].left = col2; // reused: name label
|
|
fe->comboLabel[GroupLaunch].right = col2 + col_w;
|
|
fe->comboLabel[GroupLaunch].top = name_y;
|
|
fe->comboLabel[GroupLaunch].bottom = name_y + row_h;
|
|
if (fe->nameEdit != NULL)
|
|
{
|
|
MoveWindow(fe->nameEdit, col2, name_y + row_h, col_w, row_h, TRUE);
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// The drop-downs themselves. Rebuilt rather than moved, because
|
|
// the scenario swaps two of them outright (team/position for
|
|
// colour/badge) and reshuffles the track list.
|
|
//---------------------------------------------------------------
|
|
DestroyCombos(fe);
|
|
for (int g = 0; g < group_count; ++g)
|
|
{
|
|
int group = groups[g];
|
|
const RECT &label = fe->comboLabel[group];
|
|
int count = GroupSize(group, fe->selection);
|
|
|
|
// The height given at creation is the DROPPED height - what the
|
|
// closed box shows is the item height - so ask for enough to
|
|
// show a dozen rows without a scrollbar where the list is short.
|
|
int drop_rows = (count < 12) ? count : 12;
|
|
HWND box = CreateWindowExA(
|
|
0, "COMBOBOX", "",
|
|
WS_CHILD | WS_VISIBLE | WS_VSCROLL |
|
|
CBS_DROPDOWNLIST | CBS_OWNERDRAWFIXED | CBS_HASSTRINGS,
|
|
label.left, label.top + row_h,
|
|
label.right - label.left, row_h + drop_rows * row_h,
|
|
fe->menuWindow, (HMENU)(INT_PTR)(kComboIdBase + group),
|
|
(HINSTANCE) GetWindowLongPtr(fe->menuWindow, GWLP_HINSTANCE), NULL);
|
|
if (box == NULL)
|
|
{
|
|
continue;
|
|
}
|
|
SendMessageA(box, WM_SETFONT, (WPARAM) fe->textFont, TRUE);
|
|
SendMessageA(box, CB_SETITEMHEIGHT, (WPARAM) -1, row_h);
|
|
SendMessageA(box, CB_SETITEMHEIGHT, 0, row_h);
|
|
|
|
// paint the closed box ourselves - see ComboSubclassProc
|
|
WNDPROC previous = (WNDPROC) SetWindowLongPtrA(
|
|
box, GWLP_WNDPROC, (LONG_PTR) ComboSubclassProc);
|
|
if (gComboProc == NULL)
|
|
{
|
|
gComboProc = previous;
|
|
}
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
SendMessageA(box, CB_ADDSTRING, 0, (LPARAM) ItemName(group, i));
|
|
}
|
|
if (fe->selection[group] >= count) fe->selection[group] = 0;
|
|
SendMessageA(box, CB_SETCURSEL, fe->selection[group], 0);
|
|
fe->combo[group] = box;
|
|
}
|
|
}
|
|
|
|
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 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);
|
|
|
|
// Labels: one above each drop-down, one above the scenario
|
|
// buttons, one above the pilot name box. The drop-downs draw
|
|
// themselves (WM_DRAWITEM), so all that is left here is their
|
|
// captions.
|
|
SetTextColor(mem, kGreenBright);
|
|
for (int g = 0; g < GroupCount; ++g)
|
|
{
|
|
Logical labelled = (g == GroupScenario) || (fe->combo[g] != NULL);
|
|
if (!labelled && g != GroupLaunch)
|
|
{
|
|
continue;
|
|
}
|
|
RECT label = fe->comboLabel[g];
|
|
if (label.right <= label.left)
|
|
{
|
|
continue;
|
|
}
|
|
const char *caption = (g == GroupLaunch) ? "PILOT NAME" : GroupTitle(g);
|
|
DrawTextA(mem, caption, -1, &label,
|
|
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
|
}
|
|
|
|
for (int i = 0; i < fe->itemCount; ++i)
|
|
{
|
|
const FEItem *item = &fe->items[i];
|
|
|
|
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;
|
|
// On a narrow window the longest names (Screaming Broccoli,
|
|
// Blacker Tarantula) outrun their column. Ellipsis rather than
|
|
// a glyph sliced down the middle.
|
|
DrawTextA(mem, ItemName(item->group, item->index), -1, &text,
|
|
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
|
}
|
|
|
|
// (the pilot-name caption is drawn with the other labels above)
|
|
|
|
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_COMMAND:
|
|
if (fe != NULL && HIWORD(wParam) == CBN_SELCHANGE)
|
|
{
|
|
int group = LOWORD(wParam) - kComboIdBase;
|
|
if (group >= 0 && group < GroupCount && fe->combo[group] != NULL)
|
|
{
|
|
int pick = (int) SendMessageA(fe->combo[group], CB_GETCURSEL, 0, 0);
|
|
if (pick >= 0)
|
|
{
|
|
fe->selection[group] = pick;
|
|
}
|
|
// the closed box is ours to redraw now
|
|
InvalidateRect(fe->combo[group], NULL, FALSE);
|
|
InvalidateRect(fe->menuWindow, NULL, FALSE);
|
|
return 0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
//
|
|
// The drop-downs are owner-drawn so they read as part of the
|
|
// panel rather than as system widgets: green on black, and the
|
|
// highlight inverted rather than tinted.
|
|
//
|
|
case WM_DRAWITEM:
|
|
if (fe != NULL)
|
|
{
|
|
DRAWITEMSTRUCT *di = (DRAWITEMSTRUCT *) lParam;
|
|
if (di->CtlType == ODT_COMBOBOX && (int) di->itemID >= 0)
|
|
{
|
|
Logical hot =
|
|
((di->itemState & (ODS_SELECTED | ODS_COMBOBOXEDIT)) == ODS_SELECTED);
|
|
HBRUSH back = CreateSolidBrush(hot ? kGreenDim : kBlack);
|
|
FillRect(di->hDC, &di->rcItem, back);
|
|
DeleteObject(back);
|
|
|
|
char text[128];
|
|
text[0] = '\0';
|
|
SendMessageA(di->hwndItem, CB_GETLBTEXT, di->itemID, (LPARAM) text);
|
|
RECT label = di->rcItem;
|
|
label.left += 6;
|
|
SetBkMode(di->hDC, TRANSPARENT);
|
|
SetTextColor(di->hDC, hot ? kBlack : kGreenBright);
|
|
DrawTextA(di->hDC, text, -1, &label,
|
|
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
|
return TRUE;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case WM_CTLCOLORLISTBOX:
|
|
if (fe != NULL)
|
|
{
|
|
SetTextColor((HDC) wParam, kGreenBright);
|
|
SetBkColor((HDC) wParam, kBlack);
|
|
return (LRESULT) fe->editBrush;
|
|
}
|
|
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;
|
|
|
|
// before the lobby branch below: a member rejoining a room publishes
|
|
// the callsign as member data without the menu ever opening
|
|
EnsurePilotSettingsLoaded();
|
|
|
|
//---------------------------------------------------------------
|
|
// 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
|
|
}
|
|
|
|
// NOTE: the vehicle catalog below is hand-written while RPL4.RES is
|
|
// built elsewhere, so the two can drift - this menu offered 'blkspk'
|
|
// for a while before any vehicle resource backed it. Validating here
|
|
// does NOT work: the front end runs before the resource file is
|
|
// opened, so GetResourceFile() has nothing to search yet. If this is
|
|
// worth guarding, do it offline against the built RPL4.RES (see
|
|
// tools/resbuild) rather than at menu time.
|
|
|
|
for (;;)
|
|
{
|
|
FEState fe;
|
|
memset(&fe, 0, sizeof(fe));
|
|
EnsurePilotSettingsLoaded();
|
|
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 callsign whichever way we leave the menu, INCLUDING
|
|
// a close: typing a name and then quitting is how somebody sets it
|
|
// for next time, and losing it there would be the one case that
|
|
// makes the whole thing feel unreliable. The loadout still only
|
|
// persists on a real exit - it is picked, not typed, and the menu
|
|
// reopens with it visible anyway.
|
|
//---------------------------------------------------------------
|
|
if (fe.nameEdit != NULL)
|
|
{
|
|
fe.pilotName[0] = '\0';
|
|
GetWindowTextA(fe.nameEdit, fe.pilotName, sizeof(fe.pilotName) - 1);
|
|
SanitizeCallsign(fe.pilotName, sizeof(fe.pilotName));
|
|
}
|
|
else
|
|
{
|
|
strcpy(fe.pilotName, gLastPilotName);
|
|
}
|
|
|
|
strcpy(gLastPilotName, fe.pilotName);
|
|
memcpy(gPersistSelection, fe.selection, sizeof(gPersistSelection));
|
|
gHavePersist = True;
|
|
|
|
//
|
|
// Written on the way out however the player leaves - launching,
|
|
// stepping into a lobby, or quitting. BT411 saves only on a
|
|
// launch, which loses a callsign typed by somebody who then
|
|
// changed their mind, and that is the one moment this feature
|
|
// exists for.
|
|
//
|
|
// Unconditionally, rather than only when something changed: the
|
|
// file is a few hundred bytes, and writing it every time means a
|
|
// value that was hand-edited out of range comes back corrected
|
|
// instead of being quietly re-rejected on every launch forever.
|
|
//
|
|
SavePilotSettings(gLastPilotName, gPersistSelection);
|
|
|
|
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;
|
|
}
|
|
|
|
// this screen labels a row with the pilot's own callsign, and in the
|
|
// -egg and lobby paths it can be the first screen of the session
|
|
EnsurePilotSettingsLoaded();
|
|
|
|
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";
|
|
}
|