"Race" is Red Planet's term (it's a racing game) and rode in with the
RPL4LOBBY port. In BattleTech the gameplay unit is a MISSION. Sweep it out
of the front-end / lobby / marshal code + the roadmap and steamification
digest: rename the public API BTLobby_Push/PullRaceResults ->
Push/PullMissionResults, the internal PrimeHostedRace -> PrimeHostedMission,
the deferred marshal InstallNetworkRace -> InstallNetworkMission, and the
user-facing strings ("STEAM MISSION LOBBY", "L A U N C H M I S S I O N",
"HOST/JOIN STEAM MISSION"). Comments follow. No behavior change; both gates
(default + BT412_STEAM) still build + link clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
847 lines
35 KiB
C++
847 lines
35 KiB
C++
//===========================================================================//
|
|
// btl4fe.cpp -- BT412 in-game front end: local mission-egg builder.
|
|
//
|
|
// See btl4fe.hpp. This module is self-contained (no engine types) so it can
|
|
// build the egg text before the mission machinery exists; it only writes a
|
|
// NotationFile the standard -egg path then loads.
|
|
//===========================================================================//
|
|
// WIN32_LEAN_AND_MEAN keeps <windows.h> from pulling the legacy winsock.h;
|
|
// the engine chain (via l4app.hpp) brings winsock2.h, and the two collide.
|
|
// GDI (used below for the plasma name bitmaps) is unaffected. Same preamble
|
|
// as game/btl4main.cpp.
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#define _WIN32_WINNT 0x0500
|
|
#define WINVER 0x0500
|
|
|
|
#include <windows.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "btl4fe.hpp"
|
|
#include "btl4lobby.hpp" // BTLobby_Host/Join + LobbyOutcome
|
|
#include "l4app.hpp" // L4Application::SetEggNotationFileName
|
|
|
|
// Mission length (seconds) chosen on the last LAUNCH; WinMain arms the marshal.
|
|
static int gLastMissionSeconds = 300;
|
|
static char gLastPilotName[24] = "Pilot";
|
|
static char gLastVehicle[16] = "bhk1";
|
|
static char gLastColor[16] = "White";
|
|
static char gLastBadge[8] = "VGL";
|
|
|
|
// Lobby-fed hosted pilots (the owner's egg gains one [pilots] entry each).
|
|
static FEHostedPilot gHostedPilots[8];
|
|
static int gHostedPilotCount = 0;
|
|
static char gHostedOwnerAddress[64] = "";
|
|
|
|
// Launch mode from the last LAUNCH: 0 single, 1 host (lobby), 2 member (lobby).
|
|
static int gLastLaunchMode = 0;
|
|
|
|
void BTFrontEnd_GetLoadout(char *vehicle, char *color, char *badge)
|
|
{
|
|
strcpy(vehicle, gLastVehicle);
|
|
strcpy(color, gLastColor);
|
|
strcpy(badge, gLastBadge);
|
|
}
|
|
|
|
void BTFrontEnd_SetHostedPilots(const char *owner_address,
|
|
const FEHostedPilot *pilots, int count)
|
|
{
|
|
if (owner_address == 0 || count <= 0) { gHostedPilotCount = 0; gHostedOwnerAddress[0] = '\0'; return; }
|
|
strncpy(gHostedOwnerAddress, owner_address, sizeof(gHostedOwnerAddress) - 1);
|
|
if (count > 8) count = 8;
|
|
for (int i = 0; i < count; ++i) gHostedPilots[i] = pilots[i];
|
|
gHostedPilotCount = count;
|
|
}
|
|
|
|
int BTFrontEnd_LastLaunchMode() { return gLastLaunchMode; }
|
|
|
|
//---------------------------------------------------------------------------//
|
|
// Catalogs -- the BattleTech content the console's RPConfig.xml named.
|
|
// Maps: the 8 in BTL4.RES. Mechs: the base 8 + the known variants.
|
|
//---------------------------------------------------------------------------//
|
|
const char *const kBTMaps[] =
|
|
{
|
|
"cavern", "grass", "rav", "polar3", "polar4", "arena1", "arena2", "dbase", 0
|
|
};
|
|
|
|
const char *const kBTMechs[] =
|
|
{
|
|
"avatar", "bhk1", "loki", "madcat", "owens", "sunder", "thor", "vulture",
|
|
"ava1", "lok1", "lok2", "mad1", "mad2", "own1", "snd1", "thr1", "vul1",
|
|
"blkhawk", 0
|
|
};
|
|
|
|
const char *const kBTColors[] =
|
|
{
|
|
"White", "Red", "Blue", "Green", "Yellow", "Orange", "Purple", "Cyan", 0
|
|
};
|
|
|
|
int BTCatalogCount(const char *const *list)
|
|
{
|
|
int n = 0;
|
|
while (list[n] != 0) ++n;
|
|
return n;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------//
|
|
// The four rank-ordinal plasma bitmaps (1st..4th place), 128x32, MSB-first
|
|
// hex, 4 pixels per nibble. These are static content -- the same graphics
|
|
// the console shipped in every egg's [ordinals] block (verified against
|
|
// content/MP.EGG). The pilot NAME bitmaps are GDI-rendered per mission.
|
|
//---------------------------------------------------------------------------//
|
|
namespace
|
|
{
|
|
const char *const kOrdinal[4][32] =
|
|
{
|
|
{ // 1st
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"000038000003C000001FF00000000F00","0000F8000003C000003FF80000000F00",
|
|
"0001F8000003C00000707C0000000F00","0001F8000003C00000603C0000000F00",
|
|
"00007801FC0FF00000003C3DF807FF00","00007803FE0FF00000003C3FFC0FFF00",
|
|
"00007803C703C00000003C3E3C1F0F00","000078078303C0000000783C1E1E0F00",
|
|
"000078078003C0000000783C1E1E0F00","00007807C003C0000000F03C1E1E0F00",
|
|
"00007807F003C0000001E03C1E1E0F00","00007803FC03C0000003C03C1E1E0F00",
|
|
"00007801FE03C0000007803C1E1E0F00","000078007F03C000000F003C1E1E0F00",
|
|
"000078001F03C000001E003C1E1E0F00","000078000F03C000003C003C1E1E0F00",
|
|
"000078060F03C0000078003C1E1E0F00","000078071E03E0000078003C1E1F1F00",
|
|
"00007803FE01F000007FFC3C1E0FFF00","00007801FC00F000007FFC3C1E07EF00",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
},
|
|
{ // 2nd
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"001FFF0000003C0000003C03C0780000","001FFF0000003C0000007C03C0780000",
|
|
"00000E0000003C000000FC03C0780000","00003C0000003C000001BC03C0780000",
|
|
"0000700F3E1FFC000003BC0FF07BF000","0001E00F7E3FFC0000073C0FF07FF800",
|
|
"0003800FFE7C3C0000063C03C07C7800","0007F80FFE783C00000C3C03C0783C00",
|
|
"0007FE0F80783C0000183C03C0783C00","00001E0F00783C0000383C03C0783C00",
|
|
"00000F0F00783C0000703C03C0783C00","00000F0F00783C00007FFF03C0783C00",
|
|
"00000F0F00783C00007FFF03C0783C00","00000F0F00783C0000003C03C0783C00",
|
|
"00000F0F00783C0000003C03C0783C00","00000F0F00783C0000003C03C0783C00",
|
|
"00180F0F00783C0000003C03C0783C00","001C1F0F007C7C0000003C03E0783C00",
|
|
"000FFE0F003FFC0000003C01F0783C00","0007FC0F001FBC0000003C00F0783C00",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
},
|
|
{ // 3rd
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"001FFF03C07800000000FC03C0780000","001FFF03C07800000003FC03C0780000",
|
|
"001E0003C07800000007C003C0780000","001E0003C0780000000F0003C0780000",
|
|
"001E000FF07BF000000F000FF07BF000","001E000FF07FF800001E000FF07FF800",
|
|
"001E0003C07C7800001E0003C07C7800","001FFC03C0783C00001EFC03C0783C00",
|
|
"001FFE03C0783C00001FFE03C0783C00","00001F03C0783C00001F1F03C0783C00",
|
|
"00000F03C0783C00001E0F03C0783C00","00000F03C0783C00001E0F03C0783C00",
|
|
"00000F03C0783C00001E0F03C0783C00","00000F03C0783C00001E0F03C0783C00",
|
|
"00000F03C0783C00001E0F03C0783C00","00000F03C0783C00001E0F03C0783C00",
|
|
"00180F03C0783C00001E0F03C0783C00","001C1F03E0783C00001F1F03E0783C00",
|
|
"000FFE01F0783C00000FFE01F0783C00","0007FC00F0783C000007FC00F0783C00",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
},
|
|
{ // 4th
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"001FFF03C07800000007FC03C0780000","001FFF03C0780000000FFE03C0780000",
|
|
"00000F03C0780000001F1F03C0780000","00000F03C0780000001E0F03C0780000",
|
|
"00000F0FF07BF000001E0F0FF07BF000","00001F0FF07FF800001E0F0FF07FF800",
|
|
"00001E03C07C7800001E0F03C07C7800","00003E03C0783C00001E0F03C0783C00",
|
|
"00003C03C0783C00000F1E03C0783C00","00003C03C0783C000007FC03C0783C00",
|
|
"00007803C0783C000007FC03C0783C00","00007803C0783C00000F1E03C0783C00",
|
|
"00007803C0783C00001E0F03C0783C00","0000F003C0783C00001E0F03C0783C00",
|
|
"0000F003C0783C00001E0F03C0783C00","0000F003C0783C00001E0F03C0783C00",
|
|
"0000F003C0783C00001E0F03C0783C00","0000F003E0783C00000F1E03E0783C00",
|
|
"0000F001F0783C00000FFE01F0783C00","0000F000F0783C000007FC00F0783C00",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
"00000000000000000000000000000000","00000000000000000000000000000000",
|
|
},
|
|
};
|
|
|
|
//-------------------------------------------------------------------//
|
|
// GDI-render `text` centered into a monochrome plasma bitmap and emit
|
|
// it as `rows` lines of `width/4` hex chars, MSB-first (the exact
|
|
// [BitMap] format). width/height are the plasma glyph box (128x32 for
|
|
// the large name, 64x16 for the small). A top-down 1bpp DIB packs bit
|
|
// 7 of byte 0 as the leftmost pixel -- the same bit order as the egg
|
|
// hex -- so the readback is a direct nibble-to-hex conversion.
|
|
//-------------------------------------------------------------------//
|
|
void RenderNameBitmap(const char *text, int width, int height, FILE *out)
|
|
{
|
|
HDC screen = GetDC(NULL);
|
|
HDC dc = CreateCompatibleDC(screen);
|
|
ReleaseDC(NULL, screen);
|
|
|
|
// Top-down monochrome DIB: negative height, biBitCount 1.
|
|
struct { BITMAPINFOHEADER h; RGBQUAD c[2]; } bi;
|
|
memset(&bi, 0, sizeof(bi));
|
|
bi.h.biSize = sizeof(BITMAPINFOHEADER);
|
|
bi.h.biWidth = width;
|
|
bi.h.biHeight = -height; // top-down
|
|
bi.h.biPlanes = 1;
|
|
bi.h.biBitCount = 1;
|
|
bi.h.biCompression = BI_RGB;
|
|
bi.c[0].rgbRed = bi.c[0].rgbGreen = bi.c[0].rgbBlue = 0; // index 0 = off
|
|
bi.c[1].rgbRed = bi.c[1].rgbGreen = bi.c[1].rgbBlue = 255; // index 1 = lit
|
|
|
|
void *bits = 0;
|
|
HBITMAP dib = CreateDIBSection(dc, (BITMAPINFO *)&bi, DIB_RGB_COLORS, &bits, NULL, 0);
|
|
HGDIOBJ oldBmp = SelectObject(dc, dib);
|
|
|
|
RECT rect = { 0, 0, width, height };
|
|
SetBkColor(dc, RGB(0, 0, 0));
|
|
SetTextColor(dc, RGB(255, 255, 255));
|
|
ExtTextOutA(dc, 0, 0, ETO_OPAQUE, &rect, "", 0, NULL); // clear to off
|
|
|
|
// A small readable font that fits the glyph box height.
|
|
HFONT font = CreateFontA(
|
|
height >= 32 ? 24 : 12, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE,
|
|
ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
|
|
NONANTIALIASED_QUALITY, FF_DONTCARE | DEFAULT_PITCH, "Arial");
|
|
HGDIOBJ oldFont = SelectObject(dc, font);
|
|
SetBkMode(dc, TRANSPARENT);
|
|
|
|
SIZE sz;
|
|
GetTextExtentPoint32A(dc, text, (int)strlen(text), &sz);
|
|
int tx = (width - sz.cx) / 2; if (tx < 0) tx = 0;
|
|
int ty = (height - sz.cy) / 2; if (ty < 0) ty = 0;
|
|
TextOutA(dc, tx, ty, text, (int)strlen(text));
|
|
GdiFlush();
|
|
|
|
// Emit: each DIB row is width bits, padded to a 4-byte (32-bit)
|
|
// boundary. The egg wants width/4 hex chars per row (no padding),
|
|
// so read the first width bits and repack.
|
|
const int strideBytes = ((width + 31) / 32) * 4;
|
|
const unsigned char *base = (const unsigned char *)bits;
|
|
for (int y = 0; y < height; ++y)
|
|
{
|
|
const unsigned char *row = base + (size_t)y * strideBytes;
|
|
fprintf(out, "bitmap=");
|
|
for (int nib = 0; nib < width / 4; ++nib)
|
|
{
|
|
int value = 0;
|
|
for (int b = 0; b < 4; ++b)
|
|
{
|
|
int px = nib * 4 + b;
|
|
int byte = px >> 3;
|
|
int bit = 7 - (px & 7); // MSB-first
|
|
if (row[byte] & (1 << bit))
|
|
value |= (1 << (3 - b));
|
|
}
|
|
fputc("0123456789ABCDEF"[value], out);
|
|
}
|
|
fputc('\n', out);
|
|
}
|
|
|
|
SelectObject(dc, oldFont);
|
|
DeleteObject(font);
|
|
SelectObject(dc, oldBmp);
|
|
DeleteObject(dib);
|
|
DeleteDC(dc);
|
|
}
|
|
|
|
void WriteOrdinalBlock(FILE *out, int index /*1..4*/)
|
|
{
|
|
fprintf(out, "[Ordinal::BitMap::%d]\n", index);
|
|
for (int r = 0; r < 32; ++r)
|
|
fprintf(out, "bitmap=%s\n", kOrdinal[index - 1][r]);
|
|
fprintf(out, "x=128\ny=32\nwidth=8\n");
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------//
|
|
// Defaults: a solo mission on grass, one local pilot in a Black Hawk.
|
|
//---------------------------------------------------------------------------//
|
|
void BTFeMission_Default(BTFeMission *mission)
|
|
{
|
|
memset(mission, 0, sizeof(*mission));
|
|
strcpy(mission->map, "grass");
|
|
strcpy(mission->scenario, "freeforall");
|
|
strcpy(mission->timeOfDay, "day");
|
|
strcpy(mission->weather, "clear");
|
|
mission->temperature = 27;
|
|
mission->lengthSeconds = 600;
|
|
mission->pilotCount = 1;
|
|
|
|
BTFePilot *p = &mission->pilots[0];
|
|
strcpy(p->address, "200.0.0.96"); // solo local (matches DEV.EGG)
|
|
strcpy(p->name, "Pilot");
|
|
strcpy(p->vehicle, "bhk1");
|
|
strcpy(p->color, "White");
|
|
strcpy(p->badge, "VGL");
|
|
strcpy(p->patch, "Yellow");
|
|
strcpy(p->dropzone, "one");
|
|
p->bitmapindex = 1;
|
|
p->advancedDamage = 1;
|
|
p->loadzones = 1;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------//
|
|
// Write the egg.
|
|
//---------------------------------------------------------------------------//
|
|
int BTFeMission_WriteEgg(const BTFeMission *mission, const char *path)
|
|
{
|
|
FILE *out = 0;
|
|
if (fopen_s(&out, path, "wb") != 0 || out == 0)
|
|
return 0;
|
|
|
|
// [mission]
|
|
fprintf(out, "[mission]\n");
|
|
fprintf(out, "adventure=BattleTech\n");
|
|
fprintf(out, "map=%s\n", mission->map);
|
|
fprintf(out, "scenario=%s\n", mission->scenario);
|
|
fprintf(out, "time=%s\n", mission->timeOfDay);
|
|
fprintf(out, "weather=%s\n", mission->weather);
|
|
fprintf(out, "temperature=%d\n", mission->temperature);
|
|
fprintf(out, "length=%d\n", mission->lengthSeconds);
|
|
|
|
// [ordinals] -- the four rank-place plasma graphics.
|
|
fprintf(out, "[ordinals]\n");
|
|
for (int i = 1; i <= 4; ++i)
|
|
fprintf(out, "bitmap=Ordinal::BitMap::%d\n", i);
|
|
for (int i = 1; i <= 4; ++i)
|
|
WriteOrdinalBlock(out, i);
|
|
|
|
// [pilots] -- the ordered mesh roster.
|
|
fprintf(out, "[pilots]\n");
|
|
for (int i = 0; i < mission->pilotCount; ++i)
|
|
fprintf(out, "pilot=%s\n", mission->pilots[i].address);
|
|
|
|
// Per-pilot loadout sections.
|
|
for (int i = 0; i < mission->pilotCount; ++i)
|
|
{
|
|
const BTFePilot *p = &mission->pilots[i];
|
|
fprintf(out, "[%s]\n", p->address);
|
|
fprintf(out, "hostType=0\n");
|
|
fprintf(out, "advancedDamage=%d\n", p->advancedDamage);
|
|
fprintf(out, "loadzones=%d\n", p->loadzones);
|
|
fprintf(out, "name=%s\n", p->name);
|
|
fprintf(out, "bitmapindex=%d\n", p->bitmapindex);
|
|
fprintf(out, "experience=expert\n");
|
|
fprintf(out, "badge=%s\n", p->badge);
|
|
fprintf(out, "patch=%s\n", p->patch);
|
|
fprintf(out, "role=Role::Default\n");
|
|
fprintf(out, "dropzone=%s\n", p->dropzone);
|
|
fprintf(out, "vehicle=%s\n", p->vehicle);
|
|
fprintf(out, "vehicleValue=1000\n");
|
|
fprintf(out, "color=%s\n", p->color);
|
|
}
|
|
|
|
// [largebitmap] / [smallbitmap] -- the plasma name readouts, one per
|
|
// pilot, GDI-rendered from the pilot name.
|
|
fprintf(out, "[largebitmap]\n");
|
|
for (int i = 0; i < mission->pilotCount; ++i)
|
|
fprintf(out, "bitmap=BitMap::Large::%s\n", mission->pilots[i].name);
|
|
for (int i = 0; i < mission->pilotCount; ++i)
|
|
{
|
|
fprintf(out, "[BitMap::Large::%s]\n", mission->pilots[i].name);
|
|
RenderNameBitmap(mission->pilots[i].name, 128, 32, out);
|
|
fprintf(out, "x=128\ny=32\nwidth=8\n");
|
|
}
|
|
|
|
fprintf(out, "[smallbitmap]\n");
|
|
for (int i = 0; i < mission->pilotCount; ++i)
|
|
fprintf(out, "bitmap=BitMap::Small::%s\n", mission->pilots[i].name);
|
|
for (int i = 0; i < mission->pilotCount; ++i)
|
|
{
|
|
fprintf(out, "[BitMap::Small::%s]\n", mission->pilots[i].name);
|
|
RenderNameBitmap(mission->pilots[i].name, 64, 16, out);
|
|
fprintf(out, "x=64\ny=16\nwidth=4\n");
|
|
}
|
|
|
|
// Role models (referenced by each pilot's role= field).
|
|
fprintf(out, "[Role::Default]\n");
|
|
fprintf(out, "model=dfltrole\n");
|
|
fprintf(out, "[Role::NoReturn]\n");
|
|
fprintf(out, "model=noretun\n");
|
|
|
|
fclose(out);
|
|
return 1;
|
|
}
|
|
|
|
//===========================================================================//
|
|
// THE MINICONSOLE MENU -- the in-game mission setup that replaces the operator
|
|
// console. A GDI-painted green-on-black terminal (RP412 RPL4FE pattern): the
|
|
// catalogs render as clickable columns, a pilot-name edit box, and a LAUNCH
|
|
// button; clicking LAUNCH fills a BTFeMission from the selections and writes
|
|
// the egg. Runs in its own top-level window before the mission (front-end
|
|
// mode), so no D3D is up yet.
|
|
//===========================================================================//
|
|
namespace {
|
|
|
|
struct MenuEntry { const char *key; const char *name; };
|
|
|
|
const MenuEntry kMenuMaps[] = {
|
|
{ "grass", "Grasslands" }, { "cavern", "Cavern" }, { "rav", "Ravine" },
|
|
{ "polar3", "Polar III" }, { "polar4", "Polar IV" }, { "arena1", "Arena I" },
|
|
{ "arena2", "Arena II" }, { "dbase", "Database" },
|
|
};
|
|
const MenuEntry kMenuMechs[] = {
|
|
{ "bhk1", "Black Hawk" }, { "avatar", "Avatar" }, { "loki", "Loki" },
|
|
{ "madcat", "Mad Cat" }, { "owens", "Owens" }, { "sunder", "Sunder" },
|
|
{ "thor", "Thor" }, { "vulture","Vulture" },
|
|
};
|
|
const MenuEntry kMenuColors[] = {
|
|
{ "White","White" }, { "Red","Red" }, { "Blue","Blue" }, { "Green","Green" },
|
|
{ "Yellow","Yellow" },{ "Orange","Orange" },{ "Purple","Purple" },{ "Cyan","Cyan" },
|
|
};
|
|
const MenuEntry kMenuTimes[] = { { "day","Day" }, { "night","Night" } };
|
|
const MenuEntry kMenuWeather[] = { { "clear","Clear" }, { "fog","Light Fog" }, { "soup","Heavy Fog" } };
|
|
struct LenEntry { int seconds; const char *name; };
|
|
const LenEntry kMenuLengths[] = { { 180,"3:00" }, { 300,"5:00" }, { 600,"10:00" }, { 900,"15:00" }, { 0,"Endless" } };
|
|
|
|
#define MENU_COUNT(t) ((int)(sizeof(t)/sizeof((t)[0])))
|
|
|
|
enum MenuGroup { GMap=0, GMech, GColor, GTime, GWeather, GLength, GLaunch, GHost, GJoin, GCount };
|
|
|
|
const char *GroupTitle(int g) {
|
|
switch (g) {
|
|
case GMap: return "MAP"; case GMech: return "MECH"; case GColor: return "COLOR";
|
|
case GTime: return "TIME"; case GWeather: return "WEATHER"; case GLength: return "LENGTH";
|
|
}
|
|
return "";
|
|
}
|
|
int GroupSize(int g) {
|
|
switch (g) {
|
|
case GMap: return MENU_COUNT(kMenuMaps); case GMech: return MENU_COUNT(kMenuMechs);
|
|
case GColor: return MENU_COUNT(kMenuColors); case GTime: return MENU_COUNT(kMenuTimes);
|
|
case GWeather: return MENU_COUNT(kMenuWeather); case GLength: return MENU_COUNT(kMenuLengths);
|
|
}
|
|
return 0;
|
|
}
|
|
const char *ItemName(int g, int i) {
|
|
switch (g) {
|
|
case GMap: return kMenuMaps[i].name; case GMech: return kMenuMechs[i].name;
|
|
case GColor: return kMenuColors[i].name; case GTime: return kMenuTimes[i].name;
|
|
case GWeather: return kMenuWeather[i].name; case GLength: return kMenuLengths[i].name;
|
|
case GLaunch: return "L A U N C H";
|
|
case GHost: return "HOST STEAM MISSION";
|
|
case GJoin: return "JOIN STEAM MISSION";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
struct MenuItem { int group, index; RECT rect; };
|
|
struct MenuState {
|
|
int selection[GCount];
|
|
MenuItem items[128];
|
|
int itemCount;
|
|
HWND window, nameEdit;
|
|
HFONT textFont, titleFont;
|
|
HBRUSH editBrush;
|
|
int launched, closed;
|
|
int steamAction; // 0 none, 1 host, 2 join
|
|
};
|
|
MenuState *gMenu = NULL;
|
|
|
|
const COLORREF kGreenBright = RGB(64, 255, 64);
|
|
const COLORREF kGreenDim = RGB(24, 140, 24);
|
|
const COLORREF kBlack = RGB(0, 0, 0);
|
|
|
|
void AddGroupItems(MenuState *m, int group, int x, int *y, int row_h, int col_w) {
|
|
int n = GroupSize(group);
|
|
for (int i = 0; i < n; ++i) {
|
|
MenuItem *it = &m->items[m->itemCount++];
|
|
it->group = group; it->index = i;
|
|
it->rect.left = x; it->rect.top = *y;
|
|
it->rect.right = x + col_w; it->rect.bottom = *y + row_h;
|
|
*y += row_h;
|
|
}
|
|
*y += row_h; // gap + header room for the next group
|
|
}
|
|
|
|
void LayoutMenu(MenuState *m, int cw, int ch) {
|
|
m->itemCount = 0;
|
|
int row_h = ch / 26; if (row_h < 20) row_h = 20; if (row_h > 34) row_h = 34;
|
|
int col_w = cw / 5;
|
|
int col1 = cw / 12;
|
|
int col2 = col1 + col_w + cw / 24;
|
|
int col3 = col2 + col_w + cw / 24;
|
|
int top = ch / 7;
|
|
|
|
int y = top; AddGroupItems(m, GMap, col1, &y, row_h, col_w);
|
|
AddGroupItems(m, GTime, col1, &y, row_h, col_w);
|
|
AddGroupItems(m, GWeather, col1, &y, row_h, col_w);
|
|
AddGroupItems(m, GLength, col1, &y, row_h, col_w);
|
|
y = top; AddGroupItems(m, GMech, col2, &y, row_h, col_w);
|
|
y = top + 2 * row_h; AddGroupItems(m, GColor, col3, &y, row_h, col_w);
|
|
|
|
MenuItem *launch = &m->items[m->itemCount++];
|
|
launch->group = GLaunch; launch->index = 0;
|
|
launch->rect.left = col3; launch->rect.top = ch - 3 * row_h;
|
|
launch->rect.right = col3 + col_w; launch->rect.bottom = ch - row_h;
|
|
|
|
// Steam HOST / JOIN buttons (only when the Steam transport is up).
|
|
// (BTLobby_Available is declared globally in btl4lobby.hpp -- a block-scope
|
|
// `extern` HERE would bind it to this anonymous namespace instead.)
|
|
if (BTLobby_Available()) {
|
|
MenuItem *host = &m->items[m->itemCount++];
|
|
host->group = GHost; host->index = 0;
|
|
host->rect.left = col3; host->rect.top = ch - 6 * row_h;
|
|
host->rect.right = col3 + col_w; host->rect.bottom = ch - 5 * row_h;
|
|
MenuItem *join = &m->items[m->itemCount++];
|
|
join->group = GJoin; join->index = 0;
|
|
join->rect.left = col3; join->rect.top = ch - (9 * row_h) / 2;
|
|
join->rect.right = col3 + col_w; join->rect.bottom = ch - (7 * row_h) / 2;
|
|
}
|
|
|
|
if (m->nameEdit != NULL)
|
|
MoveWindow(m->nameEdit, col3, top - row_h / 4, col_w, row_h, TRUE);
|
|
}
|
|
|
|
void PaintMenu(MenuState *m) {
|
|
PAINTSTRUCT ps;
|
|
HDC hdc = BeginPaint(m->window, &ps);
|
|
RECT client; GetClientRect(m->window, &client);
|
|
HDC mem = CreateCompatibleDC(hdc);
|
|
HBITMAP surf = CreateCompatibleBitmap(hdc, client.right, client.bottom);
|
|
HBITMAP oldSurf = (HBITMAP) SelectObject(mem, surf);
|
|
FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH));
|
|
SetBkMode(mem, TRANSPARENT);
|
|
|
|
SelectObject(mem, m->titleFont);
|
|
SetTextColor(mem, kGreenBright);
|
|
RECT title = client; title.top = client.bottom / 26; title.bottom = title.top + client.bottom / 9;
|
|
DrawTextA(mem, "B A T T L E T E C H - MISSION SETUP", -1, &title, DT_CENTER | DT_TOP | DT_SINGLELINE);
|
|
|
|
SelectObject(mem, m->textFont);
|
|
int prevGroup = -1;
|
|
for (int i = 0; i < m->itemCount; ++i) {
|
|
const MenuItem *it = &m->items[i];
|
|
if (it->group != prevGroup && it->group < GLaunch) {
|
|
prevGroup = it->group;
|
|
RECT h = it->rect; h.top -= (it->rect.bottom - it->rect.top); h.bottom = it->rect.top;
|
|
SetTextColor(mem, kGreenBright);
|
|
DrawTextA(mem, GroupTitle(it->group), -1, &h, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
|
}
|
|
RECT row = it->rect;
|
|
if (it->group >= GLaunch) { // LAUNCH / HOST / JOIN -- framed buttons
|
|
HBRUSH b = CreateSolidBrush(kGreenBright); FrameRect(mem, &row, b); DeleteObject(b);
|
|
SetTextColor(mem, kGreenBright);
|
|
DrawTextA(mem, ItemName(it->group, it->index), -1, &row, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
|
continue;
|
|
}
|
|
int selected = (m->selection[it->group] == it->index);
|
|
if (selected) {
|
|
HBRUSH b = CreateSolidBrush(kGreenDim); FillRect(mem, &row, b); DeleteObject(b);
|
|
SetTextColor(mem, kGreenBright);
|
|
} else SetTextColor(mem, kGreenDim);
|
|
RECT t = row; t.left += 8;
|
|
DrawTextA(mem, ItemName(it->group, it->index), -1, &t, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
|
}
|
|
// pilot-name header
|
|
if (m->nameEdit != NULL) {
|
|
RECT er; GetWindowRect(m->nameEdit, &er);
|
|
POINT c = { er.left, er.top }; ScreenToClient(m->window, &c);
|
|
RECT h; h.left = c.x; h.right = c.x + 300; h.bottom = c.y; h.top = c.y - 26;
|
|
SetTextColor(mem, kGreenBright);
|
|
DrawTextA(mem, "PILOT NAME", -1, &h, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
|
}
|
|
BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);
|
|
SelectObject(mem, oldSurf); DeleteObject(surf); DeleteDC(mem);
|
|
EndPaint(m->window, &ps);
|
|
}
|
|
|
|
void MenuClick(MenuState *m, int x, int y) {
|
|
for (int i = 0; i < m->itemCount; ++i) {
|
|
const MenuItem *it = &m->items[i];
|
|
if (x >= it->rect.left && x < it->rect.right && y >= it->rect.top && y < it->rect.bottom) {
|
|
if (it->group == GLaunch) {
|
|
m->launched = 1; PostMessageA(m->window, WM_NULL, 0, 0);
|
|
} else if (it->group == GHost || it->group == GJoin) {
|
|
m->steamAction = (it->group == GHost) ? 1 : 2;
|
|
PostMessageA(m->window, WM_NULL, 0, 0);
|
|
} else {
|
|
m->selection[it->group] = it->index;
|
|
InvalidateRect(m->window, NULL, FALSE);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
LRESULT CALLBACK MenuWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|
MenuState *m = gMenu;
|
|
switch (msg) {
|
|
case WM_PAINT: if (m) { PaintMenu(m); return 0; } break;
|
|
case WM_LBUTTONDOWN: if (m) { MenuClick(m, (int)(short)LOWORD(lp), (int)(short)HIWORD(lp)); return 0; } break;
|
|
case WM_CTLCOLOREDIT:
|
|
if (m) { SetTextColor((HDC)wp, kGreenBright); SetBkColor((HDC)wp, kBlack); return (LRESULT)m->editBrush; }
|
|
break;
|
|
case WM_CLOSE: if (m) m->closed = 1; return 0;
|
|
case WM_DESTROY: return 0;
|
|
}
|
|
return DefWindowProcA(hwnd, msg, wp, lp);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
//---------------------------------------------------------------------------//
|
|
// Run the menu; on LAUNCH build the egg + point the engine at it. Returns 1
|
|
// if a mission was launched, 0 if the player closed the menu (quit).
|
|
//---------------------------------------------------------------------------//
|
|
int BTFrontEnd_Run()
|
|
{
|
|
// BT_FE_AUTOLAUNCH: skip the interactive menu and launch the default
|
|
// loadout straight away (quick-launch / deterministic marshal testing).
|
|
if (getenv("BT_FE_AUTOLAUNCH") != NULL) {
|
|
BTFeMission mission; BTFeMission_Default(&mission);
|
|
const char *egg = "frontend.egg";
|
|
if (!BTFeMission_WriteEgg(&mission, egg)) return 0;
|
|
L4Application::SetEggNotationFileName(egg);
|
|
gLastMissionSeconds = mission.lengthSeconds;
|
|
return 1;
|
|
}
|
|
|
|
HINSTANCE instance = GetModuleHandleA(NULL);
|
|
|
|
MenuState m; memset(&m, 0, sizeof(m));
|
|
m.selection[GLength] = 1; // default 5:00
|
|
gMenu = &m;
|
|
|
|
static int classReg = 0;
|
|
if (!classReg) {
|
|
classReg = 1;
|
|
WNDCLASSA wc; memset(&wc, 0, sizeof(wc));
|
|
wc.style = CS_HREDRAW | CS_VREDRAW;
|
|
wc.lpfnWndProc = MenuWndProc;
|
|
wc.hInstance = instance;
|
|
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
|
|
wc.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
|
|
wc.lpszClassName = "BTFrontEnd";
|
|
RegisterClassA(&wc);
|
|
}
|
|
|
|
int W = 1000, H = 720;
|
|
int sx = (GetSystemMetrics(SM_CXSCREEN) - W) / 2;
|
|
int sy = (GetSystemMetrics(SM_CYSCREEN) - H) / 2;
|
|
m.window = CreateWindowExA(0, "BTFrontEnd", "BattleTech",
|
|
WS_OVERLAPPEDWINDOW | WS_VISIBLE, sx < 0 ? 0 : sx, sy < 0 ? 0 : sy, W, H,
|
|
NULL, NULL, instance, NULL);
|
|
if (m.window == NULL) { gMenu = NULL; return 0; }
|
|
|
|
RECT client; GetClientRect(m.window, &client);
|
|
int text_h = client.bottom / 32; if (text_h < 16) text_h = 16; if (text_h > 26) text_h = 26;
|
|
m.textFont = CreateFontA(-text_h, 0,0,0, FW_NORMAL, 0,0,0, ANSI_CHARSET,
|
|
OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY, DEFAULT_PITCH | FF_MODERN, "Consolas");
|
|
m.titleFont = CreateFontA(-(text_h * 2), 0,0,0, FW_BOLD, 0,0,0, ANSI_CHARSET,
|
|
OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY, DEFAULT_PITCH | FF_MODERN, "Consolas");
|
|
m.editBrush = CreateSolidBrush(kBlack);
|
|
m.nameEdit = CreateWindowExA(0, "EDIT", "Pilot",
|
|
WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, 0,0,10,10, m.window, NULL, instance, NULL);
|
|
SendMessageA(m.nameEdit, WM_SETFONT, (WPARAM) m.textFont, TRUE);
|
|
SendMessageA(m.nameEdit, EM_SETLIMITTEXT, 22, 0);
|
|
|
|
LayoutMenu(&m, client.right, client.bottom);
|
|
InvalidateRect(m.window, NULL, TRUE);
|
|
SetForegroundWindow(m.window);
|
|
|
|
MSG msg;
|
|
while (!m.launched && !m.closed && !m.steamAction) {
|
|
BOOL r = GetMessageA(&msg, NULL, 0, 0);
|
|
if (r <= 0) { m.closed = 1; break; }
|
|
// Enter = LAUNCH, Escape = quit -- intercepted at the loop level so
|
|
// they fire whichever child (menu or name box) has focus.
|
|
if (msg.message == WM_KEYDOWN) {
|
|
if (msg.wParam == VK_RETURN) { m.launched = 1; continue; }
|
|
if (msg.wParam == VK_ESCAPE) { m.closed = 1; continue; }
|
|
}
|
|
if (msg.message == WM_QUIT) m.closed = 1;
|
|
TranslateMessage(&msg);
|
|
DispatchMessageA(&msg);
|
|
}
|
|
|
|
// harvest the loadout
|
|
char pilotName[24] = "Pilot";
|
|
if (!m.closed) {
|
|
GetWindowTextA(m.nameEdit, pilotName, sizeof(pilotName) - 1);
|
|
if (pilotName[0] == '\0') strcpy(pilotName, "Pilot");
|
|
}
|
|
int launched = m.launched, closed = m.closed, steamAction = m.steamAction;
|
|
int selMap = m.selection[GMap], selMech = m.selection[GMech], selColor = m.selection[GColor];
|
|
int selTime = m.selection[GTime], selWx = m.selection[GWeather], selLen = m.selection[GLength];
|
|
|
|
// Remember the loadout so the lobby can publish it as member data.
|
|
strncpy(gLastPilotName, pilotName, sizeof(gLastPilotName) - 1);
|
|
strncpy(gLastVehicle, kMenuMechs[selMech].key, sizeof(gLastVehicle) - 1);
|
|
strncpy(gLastColor, kMenuColors[selColor].key, sizeof(gLastColor) - 1);
|
|
|
|
DestroyWindow(m.window);
|
|
DeleteObject(m.textFont); DeleteObject(m.titleFont); DeleteObject(m.editBrush);
|
|
gMenu = NULL;
|
|
|
|
if (closed)
|
|
return 0;
|
|
|
|
// Steam HOST / JOIN: run the lobby room; act on its outcome.
|
|
if (steamAction) {
|
|
// BTLobby_Host/Join are declared in btl4lobby.hpp (global scope).
|
|
int outcome = (steamAction == 1) ? BTLobby_Host(instance, 0) : BTLobby_Join(instance, 0);
|
|
if (outcome == LobbyRoomClosed) return 0;
|
|
if (outcome == LobbyLaunchMember) { gLastLaunchMode = 2; return 1; } // -net pod (no egg)
|
|
if (outcome == LobbyLaunchHost) { gLastLaunchMode = 1; } // build the egg below
|
|
else return BTFrontEnd_Run(); // left the room -> menu again
|
|
}
|
|
else {
|
|
if (!launched) return 0;
|
|
gLastLaunchMode = 0;
|
|
}
|
|
|
|
// Build the mission from the selections.
|
|
BTFeMission mission;
|
|
BTFeMission_Default(&mission);
|
|
strcpy(mission.map, kMenuMaps[selMap].key);
|
|
strcpy(mission.timeOfDay, kMenuTimes[selTime].key);
|
|
strcpy(mission.weather, kMenuWeather[selWx].key);
|
|
mission.lengthSeconds = kMenuLengths[selLen].seconds;
|
|
strcpy(mission.pilots[0].name, pilotName);
|
|
|
|
// Host (lobby): append every lobby member as a [pilots] entry so the
|
|
// owner's egg carries the whole mesh (drop zones one..eight round-robin).
|
|
if (gHostedPilotCount > 0) {
|
|
static const char *const dz[8] = { "one","two","three","four","five","six","seven","eight" };
|
|
for (int i = 0; i < gHostedPilotCount && mission.pilotCount < 8; ++i) {
|
|
BTFePilot *p = &mission.pilots[mission.pilotCount];
|
|
memset(p, 0, sizeof(*p));
|
|
strncpy(p->address, gHostedPilots[i].address, sizeof(p->address)-1);
|
|
strncpy(p->name, gHostedPilots[i].name, sizeof(p->name)-1);
|
|
strncpy(p->vehicle, gHostedPilots[i].vehicle[0]?gHostedPilots[i].vehicle:"bhk1", sizeof(p->vehicle)-1);
|
|
strncpy(p->color, gHostedPilots[i].color[0]?gHostedPilots[i].color:"Red", sizeof(p->color)-1);
|
|
strcpy(p->badge, "VGL"); strcpy(p->patch, "Yellow");
|
|
strncpy(p->dropzone, dz[mission.pilotCount % 8], sizeof(p->dropzone)-1);
|
|
p->bitmapindex = mission.pilotCount + 1; p->advancedDamage = 1; p->loadzones = 1;
|
|
mission.pilotCount++;
|
|
}
|
|
// the owner is pilot 0; its mesh address is the FakeIP:1502
|
|
if (gHostedOwnerAddress[0])
|
|
sprintf(mission.pilots[0].address, "%s:1502", gHostedOwnerAddress);
|
|
}
|
|
strcpy(mission.pilots[0].vehicle, kMenuMechs[selMech].key);
|
|
strcpy(mission.pilots[0].color, kMenuColors[selColor].key);
|
|
|
|
const char *egg_path = "frontend.egg";
|
|
if (!BTFeMission_WriteEgg(&mission, egg_path))
|
|
return 0;
|
|
L4Application::SetEggNotationFileName(egg_path);
|
|
gLastMissionSeconds = mission.lengthSeconds;
|
|
return 1;
|
|
}
|
|
|
|
int BTFrontEnd_LastMissionSeconds()
|
|
{
|
|
return gLastMissionSeconds;
|
|
}
|
|
|
|
//===========================================================================//
|
|
// THE RESULTS SCREEN -- shown after a marshaled mission, before the relaunch.
|
|
// A GDI green-on-black scoreboard reading the marshal's snapshotted scores
|
|
// (kills/deaths per roster slot; slot 0 = the local pilot, named from the
|
|
// menu). Dismissed by any key/click or after a timeout.
|
|
//===========================================================================//
|
|
namespace { HWND gResultsWin = NULL; int gResultsDone = 0; }
|
|
|
|
extern int BTLocalConsole_ResultCount();
|
|
extern int BTLocalConsole_GetResult(int index, int *kills, int *deaths);
|
|
extern const char *BTLocalConsole_GetResultName(int index);
|
|
|
|
namespace {
|
|
LRESULT CALLBACK ResultsWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|
switch (msg) {
|
|
case WM_PAINT: {
|
|
PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps);
|
|
RECT cl; GetClientRect(hwnd, &cl);
|
|
HDC mem = CreateCompatibleDC(hdc);
|
|
HBITMAP surf = CreateCompatibleBitmap(hdc, cl.right, cl.bottom);
|
|
HBITMAP old = (HBITMAP) SelectObject(mem, surf);
|
|
FillRect(mem, &cl, (HBRUSH) GetStockObject(BLACK_BRUSH));
|
|
SetBkMode(mem, TRANSPARENT);
|
|
int th = cl.bottom / 22; if (th < 16) th = 16; if (th > 28) th = 28;
|
|
HFONT big = CreateFontA(-(th*2),0,0,0,FW_BOLD,0,0,0,ANSI_CHARSET,OUT_DEFAULT_PRECIS,
|
|
CLIP_DEFAULT_PRECIS,ANTIALIASED_QUALITY,DEFAULT_PITCH|FF_MODERN,"Consolas");
|
|
HFONT reg = CreateFontA(-th,0,0,0,FW_NORMAL,0,0,0,ANSI_CHARSET,OUT_DEFAULT_PRECIS,
|
|
CLIP_DEFAULT_PRECIS,ANTIALIASED_QUALITY,DEFAULT_PITCH|FF_MODERN,"Consolas");
|
|
const COLORREF gB = RGB(64,255,64), gD = RGB(24,140,24);
|
|
SelectObject(mem, big); SetTextColor(mem, gB);
|
|
RECT t = cl; t.top = cl.bottom/12; t.bottom = t.top + th*3;
|
|
DrawTextA(mem, "M I S S I O N C O M P L E T E", -1, &t, DT_CENTER|DT_TOP|DT_SINGLELINE);
|
|
SelectObject(mem, reg);
|
|
int y = cl.bottom/4; int cxN = cl.right/6, cxK = cl.right/2, cxD = (cl.right*2)/3;
|
|
SetTextColor(mem, gB);
|
|
RECT h; h.top=y; h.bottom=y+th*2;
|
|
h.left=cxN; h.right=cxK; DrawTextA(mem,"PILOT",-1,&h,DT_LEFT|DT_VCENTER|DT_SINGLELINE);
|
|
h.left=cxK; h.right=cxD; DrawTextA(mem,"KILLS",-1,&h,DT_LEFT|DT_VCENTER|DT_SINGLELINE);
|
|
h.left=cxD; h.right=cl.right; DrawTextA(mem,"DEATHS",-1,&h,DT_LEFT|DT_VCENTER|DT_SINGLELINE);
|
|
y += th*2;
|
|
int n = BTLocalConsole_ResultCount();
|
|
if (n <= 0) n = 1; // always show the local pilot line
|
|
for (int i = 0; i < n; ++i) {
|
|
int k = 0, d = 0; BTLocalConsole_GetResult(i, &k, &d);
|
|
char name[24];
|
|
const char *rn = BTLocalConsole_GetResultName(i); // set when the lobby collated the sheet
|
|
if (rn && rn[0]) { strncpy(name, rn, sizeof(name)-1); name[sizeof(name)-1]=0; }
|
|
else if (i == 0) { strncpy(name, gLastPilotName, sizeof(name)-1); name[sizeof(name)-1]=0; }
|
|
else sprintf(name, "Pilot %d", i + 1);
|
|
char kb[8], db[8]; sprintf(kb, "%d", k); sprintf(db, "%d", d);
|
|
SetTextColor(mem, i == 0 ? gB : gD);
|
|
RECT r; r.top=y; r.bottom=y+th*2;
|
|
r.left=cxN; r.right=cxK; DrawTextA(mem,name,-1,&r,DT_LEFT|DT_VCENTER|DT_SINGLELINE);
|
|
r.left=cxK; r.right=cxD; DrawTextA(mem,kb,-1,&r,DT_LEFT|DT_VCENTER|DT_SINGLELINE);
|
|
r.left=cxD; r.right=cl.right; DrawTextA(mem,db,-1,&r,DT_LEFT|DT_VCENTER|DT_SINGLELINE);
|
|
y += th*2;
|
|
}
|
|
SetTextColor(mem, gD);
|
|
RECT f = cl; f.top = cl.bottom - th*3; f.bottom = cl.bottom - th;
|
|
DrawTextA(mem, "press any key", -1, &f, DT_CENTER|DT_TOP|DT_SINGLELINE);
|
|
BitBlt(hdc, 0,0, cl.right, cl.bottom, mem, 0,0, SRCCOPY);
|
|
SelectObject(mem, old); DeleteObject(surf); DeleteDC(mem);
|
|
DeleteObject(big); DeleteObject(reg);
|
|
EndPaint(hwnd, &ps); return 0; }
|
|
case WM_KEYDOWN: case WM_LBUTTONDOWN: gResultsDone = 1; return 0;
|
|
case WM_CLOSE: gResultsDone = 1; return 0;
|
|
}
|
|
return DefWindowProcA(hwnd, msg, wp, lp);
|
|
}
|
|
} // namespace
|
|
|
|
// Show the scoreboard until dismissed (or ~12s timeout). Returns always.
|
|
void BTFrontEnd_ShowResults()
|
|
{
|
|
HINSTANCE instance = GetModuleHandleA(NULL);
|
|
static int reg = 0;
|
|
if (!reg) { reg = 1;
|
|
WNDCLASSA wc; memset(&wc,0,sizeof(wc));
|
|
wc.lpfnWndProc = ResultsWndProc; wc.hInstance = instance;
|
|
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
|
|
wc.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
|
|
wc.lpszClassName = "BTResults";
|
|
RegisterClassA(&wc);
|
|
}
|
|
int W = 800, H = 560;
|
|
int sx = (GetSystemMetrics(SM_CXSCREEN) - W) / 2;
|
|
int sy = (GetSystemMetrics(SM_CYSCREEN) - H) / 2;
|
|
gResultsDone = 0;
|
|
gResultsWin = CreateWindowExA(0, "BTResults", "BattleTech",
|
|
WS_OVERLAPPEDWINDOW | WS_VISIBLE, sx<0?0:sx, sy<0?0:sy, W, H, NULL, NULL, instance, NULL);
|
|
if (gResultsWin == NULL) return;
|
|
SetForegroundWindow(gResultsWin);
|
|
DWORD deadline = GetTickCount() + 12000;
|
|
MSG msg;
|
|
while (!gResultsDone && GetTickCount() < deadline) {
|
|
while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) {
|
|
if (msg.message == WM_KEYDOWN || msg.message == WM_QUIT) gResultsDone = 1;
|
|
TranslateMessage(&msg); DispatchMessageA(&msg);
|
|
}
|
|
Sleep(15);
|
|
}
|
|
DestroyWindow(gResultsWin); gResultsWin = NULL;
|
|
}
|