The loadout has always survived a race - gPersistSelection is why the setup screen reopens the way you left it - but only for as long as the process lived. Closing the game was a reset, and the callsign is the one thing on that screen a player types rather than picks, so it was the one they had to type again every launch. pilot.cfg beside bindings.txt now holds both, KEY=VALUE like environ.ini, one line per group. BT411 solved this first, in fe_last.ini, and its own comment says why RP412 never grew the file: BT411 relaunches the process between missions and would otherwise forget the loadout mid-evening, while RP412 stays in one process. That made the gap invisible from inside a session and total across two. Same idea, two differences worth naming: BT411 saves only on a launch - it returns before SavePersisted when the player quits. That loses a callsign typed by somebody who then changed their mind, which is exactly the moment this feature exists for, so this writes on the way out however the menu is left: launching, stepping into a lobby, or EXIT GAME. BT411 takes the stored name as-is. A callsign here is quoted into frontend.egg, joined into a comma-separated list for the results screen, and published as Steam lobby member data, so a comma alone would split one pilot into two on the score sheet. SanitizeCallsign drops what could end a token early and is applied to what is typed as well as to what is read, so the file cannot hold what the game will not accept. Every index is range-checked on the way in, against the group's real size rather than a constant - the track list is the one that moves, since football and the death race carry different maps, so it answers for whichever scenario is selected. The track is re-checked after the whole file is read as well, because the file is parsed in the order it happens to be written and the scenario may arrive second. Written unconditionally rather than only on a change: it is a few hundred bytes, and writing every time means a value hand-edited out of range comes back corrected instead of being quietly re-rejected on every launch forever. Verified by round trip. A callsign typed and then abandoned via EXIT GAME is in the file and back in the box next launch. A file carrying Ba"d,Na#me loads as BadName; an empty one falls back to Pilot. A full loadout round-trips value for value; vehicle=999 and color=-3 come back 0 with the rest untouched; and track=9 under football falls back to 0 both when the scenario is read first and when it is read second, which is the case the second check exists for. One correction to my own test rig on the way: cross-process SetWindowText on an EDIT updates the cached caption, which an external GetWindowText then reads back happily, while leaving the control's own buffer alone - so the harness looked right and the game correctly saw the old name. WM_SETTEXT is marshalled properly and shows the truth. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2389 lines
66 KiB
C++
2389 lines
66 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";
|
|
|
|
//---------------------------------------------------------------
|
|
// 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
|
|
//---------------------------------------------------------------
|
|
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;
|
|
|
|
// 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
|
|
}
|
|
|
|
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";
|
|
}
|