The col-1 stack (MAP 8 + SCENARIO 2 + TIME 4 + WEATHER 3 + LENGTH 4 with headers/gaps) needs 630px; at 620 the last LENGTH rows collided with the 'click to select' footer band (client.bottom-30) -- user-reported from a screenshot. 672 fits the stack + footer. The LAUNCH/steam buttons and frame rect derive from MenuClientH and follow automatically. Also: 'No Return (one life)' clipped at the column edge -> '(1 life)'. Verified by screen capture of the live menu: LENGTH fully above the footer, scenario label unclipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1256 lines
37 KiB
C++
1256 lines
37 KiB
C++
//###########################################################################
|
|
// btl4fe -- the miniconsole front end (BT_GLASS only; compiled only when
|
|
// the gate is on -- see CMakeLists.txt). Design: btl4fe.hpp.
|
|
//###########################################################################
|
|
|
|
#include "btl4fe.hpp"
|
|
#ifdef BT_STEAM
|
|
#include "btl4lobby.hpp"
|
|
#endif
|
|
|
|
#include <windows.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
|
|
//###########################################################################
|
|
// Catalogs -- the verified content values (context/content-archives.md map
|
|
// sweep; vehicle/color values from the shipped eggs). The egg writer
|
|
// accepts anything; these are the menu's cycle sets.
|
|
//###########################################################################
|
|
|
|
|
|
//###########################################################################
|
|
// The 4 static rank-place ordinal bitmaps -- byte-identical to the console
|
|
// eggs (content/MP.EGG [ordinals] block: "1st" "2nd" "3rd" "4th" as 128x32
|
|
// plasma pages).
|
|
//###########################################################################
|
|
|
|
static const char *kOrdinalBlock =
|
|
"[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";
|
|
|
|
//###########################################################################
|
|
// GDI-rendered pilot-name plasma bitmaps.
|
|
//
|
|
// The console rendered each pilot's name into 128x32 (large) and 64x16
|
|
// (small) 1bpp plasma pages. We render with GDI into a mono bitmap and
|
|
// hex-encode: `bitmap=` lines carry 16 BYTES each (32 hex chars) -- one
|
|
// line per row for the large page (16-byte rows), TWO 8-byte rows per
|
|
// line for the small page -- byte-identical framing to MP.EGG.
|
|
//###########################################################################
|
|
|
|
static int
|
|
RenderNameBits(const char *name, int width, int height,
|
|
int font_height, unsigned char *bits /* width/8*height bytes */)
|
|
{
|
|
int stride = width / 8; // mono GDI stride for 64/128 px
|
|
HDC screen = GetDC(NULL);
|
|
HDC dc = CreateCompatibleDC(screen);
|
|
ReleaseDC(NULL, screen);
|
|
if (dc == NULL)
|
|
{
|
|
return -1;
|
|
}
|
|
HBITMAP bitmap = CreateBitmap(width, height, 1, 1, NULL);
|
|
HBITMAP old_bitmap = (HBITMAP)SelectObject(dc, bitmap);
|
|
HFONT font = CreateFontW(-font_height, 0, 0, 0, FW_BOLD, 0, 0, 0,
|
|
DEFAULT_CHARSET, 0, 0, NONANTIALIASED_QUALITY, 0, L"Arial");
|
|
HFONT old_font = (HFONT)SelectObject(dc, font);
|
|
|
|
RECT r = { 0, 0, width, height };
|
|
SetBkColor(dc, RGB(0, 0, 0));
|
|
SetTextColor(dc, RGB(255, 255, 255));
|
|
HBRUSH black = (HBRUSH)GetStockObject(BLACK_BRUSH);
|
|
FillRect(dc, &r, black);
|
|
|
|
WCHAR wide[64];
|
|
int n = 0;
|
|
for (const char *s = name; *s && n < 63; ++s)
|
|
{
|
|
wide[n++] = (WCHAR)*s;
|
|
}
|
|
wide[n] = 0;
|
|
DrawTextW(dc, wide, -1, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
|
|
|
GdiFlush();
|
|
int got = GetBitmapBits(bitmap, stride * height, bits);
|
|
|
|
SelectObject(dc, old_font);
|
|
DeleteObject(font);
|
|
SelectObject(dc, old_bitmap);
|
|
DeleteObject(bitmap);
|
|
DeleteDC(dc);
|
|
return (got == stride * height) ? 0 : -1;
|
|
}
|
|
|
|
static void
|
|
WriteBitmapPage(FILE *f, const char *page_name, const char *pilot_name,
|
|
int width, int height, int font_height)
|
|
{
|
|
unsigned char bits[16 * 32]; // worst case 128x32
|
|
memset(bits, 0, sizeof(bits));
|
|
RenderNameBits(pilot_name, width, height, font_height, bits);
|
|
|
|
fprintf(f, "[%s]\n", page_name);
|
|
int total = (width / 8) * height;
|
|
for (int off = 0; off < total; off += 16)
|
|
{
|
|
fprintf(f, "bitmap=");
|
|
for (int b = 0; b < 16; ++b)
|
|
{
|
|
fprintf(f, "%02X", bits[off + b]);
|
|
}
|
|
fprintf(f, "\n");
|
|
}
|
|
fprintf(f, "x=%d\ny=%d\nwidth=%d\n", width, height, width / 16);
|
|
}
|
|
|
|
//###########################################################################
|
|
// The egg writer
|
|
//###########################################################################
|
|
|
|
int
|
|
BTFeMission_WriteEgg(const BTFeMission *mission, const char *path)
|
|
{
|
|
FILE *f = fopen(path, "wt");
|
|
if (f == NULL)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
fprintf(f,
|
|
"[mission]\n"
|
|
"adventure=BattleTech\n"
|
|
"map=%s\n"
|
|
"scenario=freeforall\n"
|
|
"time=%s\n"
|
|
"weather=%s\n"
|
|
"temperature=%d\n"
|
|
"length=%d\n",
|
|
mission->map, mission->time, mission->weather,
|
|
mission->temperature, mission->lengthSeconds);
|
|
|
|
fputs(kOrdinalBlock, f);
|
|
|
|
fprintf(f, "[pilots]\n");
|
|
for (int p = 0; p < mission->pilotCount; ++p)
|
|
{
|
|
fprintf(f, "pilot=%s\n", mission->pilots[p].address);
|
|
}
|
|
for (int p = 0; p < mission->pilotCount; ++p)
|
|
{
|
|
const BTFePilot &pilot = mission->pilots[p];
|
|
fprintf(f,
|
|
"[%s]\n"
|
|
"hostType=0\n"
|
|
"advancedDamage=%d\n"
|
|
"loadzones=1\n"
|
|
"name=%s\n"
|
|
"bitmapindex=%d\n"
|
|
"experience=%s\n"
|
|
"badge=%s\n"
|
|
"patch=%s\n"
|
|
"role=Role::%s\n"
|
|
"dropzone=%s\n"
|
|
"vehicle=%s\n"
|
|
"vehicleValue=1000\n"
|
|
"color=%s\n",
|
|
pilot.address, pilot.advancedDamage ? 1 : 0, pilot.name, p + 1,
|
|
pilot.experience[0] ? pilot.experience : "veteran",
|
|
pilot.badge[0] ? pilot.badge : "VGL",
|
|
pilot.patch[0] ? pilot.patch : "Red",
|
|
mission->roleKey[0] ? mission->roleKey : "Default",
|
|
pilot.dropzone[0] ? pilot.dropzone : "one",
|
|
pilot.vehicle,
|
|
pilot.color[0] ? pilot.color : "Grey");
|
|
}
|
|
|
|
//
|
|
// Name plasma pages. The shipped eggs carry ONE large/small pair (the
|
|
// console personalized per pod); list every pilot's pair -- the engine
|
|
// resolves by name.
|
|
//
|
|
fprintf(f, "[largebitmap]\n");
|
|
for (int p = 0; p < mission->pilotCount; ++p)
|
|
{
|
|
fprintf(f, "bitmap=BitMap::Large::%s\n", mission->pilots[p].name);
|
|
}
|
|
for (int p = 0; p < mission->pilotCount; ++p)
|
|
{
|
|
char page[64];
|
|
sprintf(page, "BitMap::Large::%s", mission->pilots[p].name);
|
|
WriteBitmapPage(f, page, mission->pilots[p].name, 128, 32, 24);
|
|
}
|
|
fprintf(f, "[smallbitmap]\n");
|
|
for (int p = 0; p < mission->pilotCount; ++p)
|
|
{
|
|
fprintf(f, "bitmap=BitMap::Small::%s\n", mission->pilots[p].name);
|
|
}
|
|
for (int p = 0; p < mission->pilotCount; ++p)
|
|
{
|
|
char page[64];
|
|
sprintf(page, "BitMap::Small::%s", mission->pilots[p].name);
|
|
WriteBitmapPage(f, page, mission->pilots[p].name, 64, 16, 12);
|
|
}
|
|
|
|
fprintf(f,
|
|
"[Role::Default]\n"
|
|
"model=dfltrole\n"
|
|
"[Role::NoReturn]\n"
|
|
"model=noretun\n");
|
|
|
|
fclose(f);
|
|
return 0;
|
|
}
|
|
|
|
//###########################################################################
|
|
// The menu -- the RP412 front-end pattern (RP_L4/RPL4FE.cpp, per Cyd):
|
|
// full catalogs as always-visible clickable LISTS in three columns with
|
|
// friendly display names -- bright green = selected, dim green = the rest --
|
|
// a real EDIT control for the pilot name (and one for LAN peers), framed
|
|
// LAUNCH / HOST STEAM / JOIN STEAM buttons, double-buffered paint, and the
|
|
// player's loadout persisted across runs (fe_last.ini beside the eggs; our
|
|
// per-mission process relaunch would otherwise forget it).
|
|
// ENTER = launch, ESC = quit.
|
|
//###########################################################################
|
|
|
|
struct CatalogEntry
|
|
{
|
|
const char *key;
|
|
const char *name;
|
|
};
|
|
|
|
// ALL option catalogs are the AUTHENTIC BattleTech mission options recovered
|
|
// from the Mac 4.10 operator console's BT:: adventure tree
|
|
// (C:\VWE\TeslaSuite\410console\4_10-console-extracted\Console.ini); the `key`
|
|
// is the egg field value (the console's `tag=`), the `name` is the display
|
|
// label. Full catalog + provenance: docs/GLASS_COCKPIT.md 2026-07-19.
|
|
|
|
static const CatalogEntry kMaps[] =
|
|
{
|
|
{ "cavern", "Cavern" },
|
|
{ "grass", "Savannah" },
|
|
{ "rav", "Ravines" },
|
|
{ "polar3", "Polar Missile Base" },
|
|
{ "polar4", "Helicopter Base" },
|
|
{ "arena1", "Abandoned Arena" },
|
|
{ "arena2", "Destroyed Arena" },
|
|
{ "dbase", "Badlands" },
|
|
};
|
|
|
|
// SCENARIO / role: the tag is always freeforall; the choice is the RESPAWN
|
|
// role -- Default (respawn) vs NoReturn (one life). Stored as key=role model.
|
|
struct RoleEntry { const char *roleKey; const char *model; const char *name; };
|
|
static const RoleEntry kScenarios[] =
|
|
{
|
|
{ "Default", "dfltrole", "Free For All" },
|
|
{ "NoReturn", "noretun", "No Return (1 life)" }, // model = the game's 8.3 resource
|
|
};
|
|
|
|
// The 18 mechs (console friendly name -> egg tag), matching the 2026-07-18
|
|
// ModelList certification.
|
|
static const CatalogEntry kVehicles[] =
|
|
{
|
|
{ "blkhawk", "Black Hawk" },
|
|
{ "loki", "Loki" },
|
|
{ "bhk1", "Nova" },
|
|
{ "madcat", "Mad Cat" },
|
|
{ "thor", "Thor" },
|
|
{ "owens", "Owens" },
|
|
{ "own1", "Kotori" },
|
|
{ "thr1", "Summoner" },
|
|
{ "lok1", "Hellbringer" },
|
|
{ "mad1", "Timber Wolf" },
|
|
{ "avatar", "Avatar" },
|
|
{ "ava1", "Satyr" },
|
|
{ "sunder", "Sunder" },
|
|
{ "snd1", "Sunder V1" },
|
|
{ "vulture", "Vulture" },
|
|
{ "vul1", "Mad Dog" },
|
|
{ "lok2", "Mischief" },
|
|
{ "mad2", "Zanin Neko" },
|
|
};
|
|
|
|
// experience = the pod's per-pilot simulation-fidelity tier (see
|
|
// context/experience-levels.md); 4 game levels.
|
|
static const CatalogEntry kExperience[] =
|
|
{
|
|
{ "novice", "Novice" },
|
|
{ "standard", "Standard" },
|
|
{ "veteran", "Veteran" },
|
|
{ "expert", "Expert" },
|
|
};
|
|
|
|
// color/camo (console display -> egg tag: Red=Crimson, Gray=Grey)
|
|
static const CatalogEntry kColors[] =
|
|
{
|
|
{ "White", "White" },
|
|
{ "Black", "Black" },
|
|
{ "Brown", "Brown" },
|
|
{ "Crimson","Crimson" },
|
|
{ "Green", "Green" },
|
|
{ "Grey", "Grey" },
|
|
{ "Tan", "Tan" },
|
|
};
|
|
|
|
// badge / team emblem (the egg `badge=` field)
|
|
static const CatalogEntry kBadges[] =
|
|
{
|
|
{ "VGL", "VGL" },
|
|
{ "Davion", "Davion" },
|
|
{ "Kurita", "Kurita" },
|
|
{ "Liao", "Liao" },
|
|
{ "Marik", "Marik" },
|
|
{ "Steiner", "Steiner" },
|
|
{ "None", "None" },
|
|
};
|
|
|
|
// patch / badge colour (the egg `patch=` field)
|
|
static const CatalogEntry kPatches[] =
|
|
{
|
|
{ "Red", "Red" },
|
|
{ "Yellow", "Yellow" },
|
|
{ "Blue", "Blue" },
|
|
{ "Green", "Green" },
|
|
{ "Violet", "Violet" },
|
|
{ "Grey", "Grey" },
|
|
{ "White", "White" },
|
|
{ "Black", "Black" },
|
|
};
|
|
|
|
// drop zone (the egg `dropzone=` field)
|
|
static const CatalogEntry kDropZones[] =
|
|
{
|
|
{ "one", "One" },
|
|
{ "two", "Two" },
|
|
{ "three", "Three" },
|
|
{ "four", "Four" },
|
|
{ "five", "Five" },
|
|
};
|
|
|
|
// advanced damage model (the egg `advancedDamage=` field)
|
|
static const CatalogEntry kAdvDamage[] =
|
|
{
|
|
{ "1", "On" },
|
|
{ "0", "Off" },
|
|
};
|
|
|
|
static const CatalogEntry kTimes[] =
|
|
{
|
|
{ "morning","Morning" },
|
|
{ "day", "Day" },
|
|
{ "evening","Evening" },
|
|
{ "night", "Night" },
|
|
};
|
|
|
|
static const CatalogEntry kWeathers[] =
|
|
{
|
|
{ "clear", "Clear" }, { "fog", "Light Fog" }, { "soup", "Heavy Fog" },
|
|
};
|
|
|
|
struct LengthEntry
|
|
{
|
|
int seconds;
|
|
const char *name;
|
|
};
|
|
static const LengthEntry kLengths[] =
|
|
{
|
|
{ 60, "1:00" }, { 300, "5:00" }, { 600, "10:00" }, { 1200, "20:00" },
|
|
};
|
|
|
|
static const CatalogEntry kModes[] =
|
|
{
|
|
{ "solo", "SOLO MISSION" },
|
|
{ "raw", "RAW SOLO (endless)" },
|
|
{ "hostlan", "HOST LAN" },
|
|
{ "joinlan", "JOIN LAN" },
|
|
};
|
|
|
|
static const int kPorts[] = { 1501, 1601, 1701, 1801 };
|
|
|
|
#define COUNT(a) ((int)(sizeof(a)/sizeof((a)[0])))
|
|
|
|
enum MenuGroup
|
|
{
|
|
GroupMap = 0,
|
|
GroupScenario,
|
|
GroupTime,
|
|
GroupWeather,
|
|
GroupLength,
|
|
GroupVehicle,
|
|
GroupExperience,
|
|
GroupColor,
|
|
GroupBadge,
|
|
GroupPatch,
|
|
GroupDropZone,
|
|
GroupAdvDamage,
|
|
GroupMode,
|
|
GroupPort,
|
|
GroupLastSelectable = GroupPort, // the last catalog group (persist + headers)
|
|
GroupLaunch,
|
|
GroupSteamHost,
|
|
GroupSteamJoin,
|
|
GroupCount
|
|
};
|
|
|
|
struct MenuItem
|
|
{
|
|
int group;
|
|
int index;
|
|
RECT rect;
|
|
};
|
|
|
|
struct MenuState
|
|
{
|
|
int selection[GroupCount];
|
|
MenuItem items[128];
|
|
int itemCount;
|
|
HWND window;
|
|
HWND nameEdit;
|
|
HWND peersEdit;
|
|
HFONT textFont;
|
|
HFONT titleFont;
|
|
HBRUSH editBrush;
|
|
int result; // 0 pending, 1 launch, -1 quit
|
|
int steamAction; // 0 none, 1 host lobby, 2 join lobby
|
|
};
|
|
|
|
static MenuState menu;
|
|
|
|
static const COLORREF kGreenBright = RGB(64, 255, 64);
|
|
static const COLORREF kGreenDim = RGB(24, 140, 24);
|
|
|
|
//
|
|
// BT_FE_SOLO=1 (play_solo.bat): a SOLO-ONLY menu. The mode cycle keeps just
|
|
// the two solo entries (kModes lists them FIRST: solo, raw), and the network
|
|
// rows/buttons (NET PORT, HOST/JOIN STEAM) are not laid out -- LAN play goes
|
|
// through the join bats + operator, Steam through play_steam.bat. A stale
|
|
// fe_last.ini mode of hostlan/joinlan is rejected by the GroupSize clamp in
|
|
// LoadPersisted, so the selection can never land on a hidden mode.
|
|
//
|
|
static int
|
|
FeSoloOnly()
|
|
{
|
|
static int s = -1;
|
|
if (s < 0)
|
|
{
|
|
const char *e = getenv("BT_FE_SOLO");
|
|
s = (e != NULL && *e != '0') ? 1 : 0;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
static int
|
|
GroupSize(int group)
|
|
{
|
|
switch (group)
|
|
{
|
|
case GroupMap: return COUNT(kMaps);
|
|
case GroupScenario: return COUNT(kScenarios);
|
|
case GroupTime: return COUNT(kTimes);
|
|
case GroupWeather: return COUNT(kWeathers);
|
|
case GroupLength: return COUNT(kLengths);
|
|
case GroupVehicle: return COUNT(kVehicles);
|
|
case GroupExperience: return COUNT(kExperience);
|
|
case GroupColor: return COUNT(kColors);
|
|
case GroupBadge: return COUNT(kBadges);
|
|
case GroupPatch: return COUNT(kPatches);
|
|
case GroupDropZone: return COUNT(kDropZones);
|
|
case GroupAdvDamage: return COUNT(kAdvDamage);
|
|
case GroupMode: return FeSoloOnly() ? 2 : COUNT(kModes); // solo, raw
|
|
case GroupPort: return COUNT(kPorts);
|
|
default: return 1;
|
|
}
|
|
}
|
|
|
|
static void
|
|
GroupRowText(int group, int index, char *out /*64*/)
|
|
{
|
|
switch (group)
|
|
{
|
|
case GroupMap: strcpy(out, kMaps[index].name); break;
|
|
case GroupScenario: strcpy(out, kScenarios[index].name); break;
|
|
case GroupTime: strcpy(out, kTimes[index].name); break;
|
|
case GroupWeather: strcpy(out, kWeathers[index].name); break;
|
|
case GroupLength: strcpy(out, kLengths[index].name); break;
|
|
case GroupVehicle: strcpy(out, kVehicles[index].name); break;
|
|
case GroupExperience: strcpy(out, kExperience[index].name); break;
|
|
case GroupColor: strcpy(out, kColors[index].name); break;
|
|
case GroupBadge: strcpy(out, kBadges[index].name); break;
|
|
case GroupPatch: strcpy(out, kPatches[index].name); break;
|
|
case GroupDropZone: strcpy(out, kDropZones[index].name); break;
|
|
case GroupAdvDamage: strcpy(out, kAdvDamage[index].name); break;
|
|
case GroupMode: strcpy(out, kModes[index].name); break;
|
|
case GroupPort: sprintf(out, "%d", kPorts[index]); break;
|
|
default: out[0] = 0; break;
|
|
}
|
|
}
|
|
|
|
static const char *
|
|
GroupHeader(int group)
|
|
{
|
|
switch (group)
|
|
{
|
|
case GroupMap: return "MAP";
|
|
case GroupScenario: return "SCENARIO";
|
|
case GroupTime: return "TIME";
|
|
case GroupWeather: return "WEATHER";
|
|
case GroupLength: return "LENGTH";
|
|
case GroupVehicle: return "MECH";
|
|
case GroupExperience: return "EXPERIENCE";
|
|
case GroupColor: return "COLOR";
|
|
case GroupBadge: return "BADGE";
|
|
case GroupPatch: return "PATCH";
|
|
case GroupDropZone: return "DROP ZONE";
|
|
case GroupAdvDamage: return "ADV. DAMAGE";
|
|
case GroupMode: return "MODE";
|
|
case GroupPort: return "NET PORT";
|
|
default: return "";
|
|
}
|
|
}
|
|
|
|
//
|
|
// Layout: five catalog columns + the button column.
|
|
// col1 MISSION: MAP, SCENARIO, TIME, WEATHER, LENGTH
|
|
// col2 MECH (18) col3 EXPERIENCE, COLOR, DROP ZONE, ADV. DAMAGE
|
|
// col4 BADGE, PATCH col5 MODE, NET PORT + name/peers edits + buttons
|
|
//
|
|
enum
|
|
{
|
|
MenuClientW = 1180,
|
|
MenuClientH = 672, // col1 stack (MAP 8 + SCENARIO 2 + TIME 4 + WEATHER 3 +
|
|
// LENGTH 4 = 630px) + the 30px footer band; 620 overlapped
|
|
// LENGTH's last rows with the footer (user-reported)
|
|
MenuRowH = 20,
|
|
MenuColW = 180,
|
|
MenuCol1X = 24,
|
|
MenuCol2X = 224,
|
|
MenuCol3X = 424,
|
|
MenuCol4X = 624,
|
|
MenuCol5X = 824,
|
|
MenuTopY = 60,
|
|
MenuNameLabelY = 316, // col5: PILOT NAME label + edit
|
|
MenuNameEditY = 336,
|
|
MenuPeersLabelY = 368, // col5: LAN PEERS label + edit
|
|
MenuPeersEditY = 388
|
|
};
|
|
|
|
static void
|
|
AddGroup(int group, int x, int *y)
|
|
{
|
|
*y += MenuRowH; // the header row
|
|
int count = GroupSize(group);
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
MenuItem *item = &menu.items[menu.itemCount++];
|
|
item->group = group;
|
|
item->index = i;
|
|
item->rect.left = x;
|
|
item->rect.top = *y;
|
|
item->rect.right = x + MenuColW;
|
|
item->rect.bottom = *y + MenuRowH;
|
|
*y += MenuRowH;
|
|
}
|
|
*y += MenuRowH / 2; // gap below the group
|
|
}
|
|
|
|
static void
|
|
AddButton(int group, int x, int y, int width, int height)
|
|
{
|
|
MenuItem *item = &menu.items[menu.itemCount++];
|
|
item->group = group;
|
|
item->index = 0;
|
|
item->rect.left = x;
|
|
item->rect.top = y;
|
|
item->rect.right = x + width;
|
|
item->rect.bottom = y + height;
|
|
}
|
|
|
|
static void
|
|
LayoutMenu()
|
|
{
|
|
menu.itemCount = 0;
|
|
int y;
|
|
// col1: the mission
|
|
y = MenuTopY; AddGroup(GroupMap, MenuCol1X, &y);
|
|
AddGroup(GroupScenario, MenuCol1X, &y);
|
|
AddGroup(GroupTime, MenuCol1X, &y);
|
|
AddGroup(GroupWeather, MenuCol1X, &y);
|
|
AddGroup(GroupLength, MenuCol1X, &y);
|
|
// col2: the mech (tall)
|
|
y = MenuTopY; AddGroup(GroupVehicle, MenuCol2X, &y);
|
|
// col3: the pilot loadout
|
|
y = MenuTopY; AddGroup(GroupExperience, MenuCol3X, &y);
|
|
AddGroup(GroupColor, MenuCol3X, &y);
|
|
AddGroup(GroupDropZone, MenuCol3X, &y);
|
|
AddGroup(GroupAdvDamage, MenuCol3X, &y);
|
|
// col4: emblem + patch
|
|
y = MenuTopY; AddGroup(GroupBadge, MenuCol4X, &y);
|
|
AddGroup(GroupPatch, MenuCol4X, &y);
|
|
// col5: the session + controls (solo-only: no net port, no steam buttons)
|
|
y = MenuTopY; AddGroup(GroupMode, MenuCol5X, &y);
|
|
if (!FeSoloOnly())
|
|
{
|
|
AddGroup(GroupPort, MenuCol5X, &y);
|
|
}
|
|
AddButton(GroupLaunch, MenuCol5X, MenuClientH - 190, 220, 52);
|
|
#ifdef BT_STEAM
|
|
if (!FeSoloOnly())
|
|
{
|
|
AddButton(GroupSteamHost, MenuCol5X, MenuClientH - 120, 220, 34);
|
|
AddButton(GroupSteamJoin, MenuCol5X, MenuClientH - 76, 220, 34);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
//
|
|
// fe_last.ini -- the persisted loadout (our process-relaunch model would
|
|
// otherwise forget it between missions; RP412 persists in-process).
|
|
//
|
|
static void
|
|
LoadPersisted(char *pilot_name, int name_capacity,
|
|
char *peers, int peers_capacity)
|
|
{
|
|
FILE *f = fopen("fe_last.ini", "rt");
|
|
if (f == NULL)
|
|
{
|
|
return;
|
|
}
|
|
char line[160];
|
|
while (fgets(line, sizeof(line), f))
|
|
{
|
|
char *newline = strchr(line, '\n');
|
|
if (newline) *newline = 0;
|
|
char *equals = strchr(line, '=');
|
|
if (equals == NULL) continue;
|
|
*equals = 0;
|
|
const char *value = equals + 1;
|
|
for (int g = 0; g < GroupCount; ++g)
|
|
{
|
|
if (_stricmp(line, GroupHeader(g)) == 0)
|
|
{
|
|
int v = atoi(value);
|
|
if (v >= 0 && v < GroupSize(g)) menu.selection[g] = v;
|
|
}
|
|
}
|
|
if (_stricmp(line, "NAME") == 0 && value[0])
|
|
{
|
|
strncpy(pilot_name, value, name_capacity - 1);
|
|
pilot_name[name_capacity - 1] = 0;
|
|
}
|
|
if (_stricmp(line, "PEERS") == 0)
|
|
{
|
|
strncpy(peers, value, peers_capacity - 1);
|
|
peers[peers_capacity - 1] = 0;
|
|
}
|
|
}
|
|
fclose(f);
|
|
}
|
|
|
|
static void
|
|
SavePersisted(const char *pilot_name, const char *peers)
|
|
{
|
|
FILE *f = fopen("fe_last.ini", "wt");
|
|
if (f == NULL)
|
|
{
|
|
return;
|
|
}
|
|
for (int g = 0; g <= GroupPort; ++g)
|
|
{
|
|
fprintf(f, "%s=%d\n", GroupHeader(g), menu.selection[g]);
|
|
}
|
|
fprintf(f, "NAME=%s\nPEERS=%s\n", pilot_name, peers);
|
|
fclose(f);
|
|
}
|
|
|
|
static void
|
|
DrawMenuText(HDC dc, const char *text, RECT *rect, COLORREF color,
|
|
UINT format)
|
|
{
|
|
WCHAR wide[128];
|
|
int n = 0;
|
|
for (const char *s = text; *s && n < 127; ++s) wide[n++] = (WCHAR)*s;
|
|
wide[n] = 0;
|
|
SetTextColor(dc, color);
|
|
DrawTextW(dc, wide, -1, rect, format);
|
|
}
|
|
|
|
static void
|
|
PaintMenu(HWND window)
|
|
{
|
|
PAINTSTRUCT paint;
|
|
HDC screen = BeginPaint(window, &paint);
|
|
RECT client;
|
|
GetClientRect(window, &client);
|
|
|
|
// double-buffered (the RP412 pattern)
|
|
HDC dc = CreateCompatibleDC(screen);
|
|
HBITMAP backing = CreateCompatibleBitmap(screen, client.right, client.bottom);
|
|
HBITMAP old_backing = (HBITMAP)SelectObject(dc, backing);
|
|
|
|
HBRUSH background = CreateSolidBrush(RGB(0, 0, 0));
|
|
FillRect(dc, &client, background);
|
|
DeleteObject(background);
|
|
SetBkMode(dc, TRANSPARENT);
|
|
|
|
HFONT old_font = (HFONT)SelectObject(dc, menu.titleFont);
|
|
RECT title_rect = { MenuCol1X, 14, client.right - 24, 54 };
|
|
DrawMenuText(dc, "BATTLETECH -- MISSION CONSOLE", &title_rect,
|
|
kGreenBright, DT_LEFT | DT_TOP | DT_SINGLELINE);
|
|
|
|
SelectObject(dc, menu.textFont);
|
|
|
|
//
|
|
// Group headers (above each group's first row).
|
|
//
|
|
for (int i = 0; i < menu.itemCount; ++i)
|
|
{
|
|
const MenuItem &item = menu.items[i];
|
|
if (item.index != 0 || item.group > GroupPort)
|
|
{
|
|
continue;
|
|
}
|
|
RECT header = item.rect;
|
|
header.top -= MenuRowH;
|
|
header.bottom = item.rect.top;
|
|
DrawMenuText(dc, GroupHeader(item.group), &header, kGreenDim,
|
|
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
|
}
|
|
|
|
//
|
|
// Catalog rows: bright = selected, dim = not.
|
|
//
|
|
for (int i = 0; i < menu.itemCount; ++i)
|
|
{
|
|
const MenuItem &item = menu.items[i];
|
|
if (item.group > GroupPort)
|
|
{
|
|
continue;
|
|
}
|
|
char text[64];
|
|
GroupRowText(item.group, item.index, text);
|
|
int selected = (menu.selection[item.group] == item.index);
|
|
RECT row = item.rect;
|
|
row.left += 14;
|
|
if (selected)
|
|
{
|
|
RECT marker = item.rect;
|
|
marker.right = marker.left + 14;
|
|
DrawMenuText(dc, ">", &marker, kGreenBright,
|
|
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
|
}
|
|
DrawMenuText(dc, text, &row,
|
|
selected ? kGreenBright : kGreenDim,
|
|
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
|
}
|
|
|
|
//
|
|
// Edit labels (the controls themselves are child windows in col5).
|
|
//
|
|
RECT label = { MenuCol5X, MenuNameLabelY, MenuCol5X + 240, MenuNameLabelY + MenuRowH };
|
|
DrawMenuText(dc, "PILOT NAME", &label, kGreenDim,
|
|
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
|
label.top = MenuPeersLabelY;
|
|
label.bottom = label.top + MenuRowH;
|
|
DrawMenuText(dc, "LAN PEERS (host: ip:port,...)", &label, kGreenDim,
|
|
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
|
|
|
//
|
|
// Buttons (framed, RP412 style).
|
|
//
|
|
HBRUSH frame = CreateSolidBrush(kGreenBright);
|
|
for (int i = 0; i < menu.itemCount; ++i)
|
|
{
|
|
const MenuItem &item = menu.items[i];
|
|
const char *text =
|
|
(item.group == GroupLaunch) ? "L A U N C H" :
|
|
(item.group == GroupSteamHost) ? "HOST STEAM LOBBY" :
|
|
(item.group == GroupSteamJoin) ? "JOIN STEAM LOBBY" : NULL;
|
|
if (text == NULL)
|
|
{
|
|
continue;
|
|
}
|
|
RECT box = item.rect;
|
|
FrameRect(dc, &box, frame);
|
|
DrawMenuText(dc, text, &box, kGreenBright,
|
|
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
|
}
|
|
DeleteObject(frame);
|
|
|
|
RECT help = { MenuCol1X, client.bottom - 30, client.right - 24, client.bottom - 6 };
|
|
DrawMenuText(dc, "click to select ENTER = launch ESC = quit",
|
|
&help, kGreenDim, DT_LEFT | DT_TOP | DT_SINGLELINE);
|
|
|
|
SelectObject(dc, old_font);
|
|
BitBlt(screen, 0, 0, client.right, client.bottom, dc, 0, 0, SRCCOPY);
|
|
SelectObject(dc, old_backing);
|
|
DeleteObject(backing);
|
|
DeleteDC(dc);
|
|
EndPaint(window, &paint);
|
|
}
|
|
|
|
static LRESULT CALLBACK
|
|
MenuWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam)
|
|
{
|
|
switch (message)
|
|
{
|
|
case WM_PAINT:
|
|
PaintMenu(window);
|
|
return 0;
|
|
|
|
case WM_CTLCOLOREDIT:
|
|
//
|
|
// The edits render green-on-black like everything else.
|
|
//
|
|
SetTextColor((HDC)wparam, kGreenBright);
|
|
SetBkColor((HDC)wparam, RGB(6, 20, 6));
|
|
return (LRESULT)menu.editBrush;
|
|
|
|
case WM_LBUTTONDOWN:
|
|
{
|
|
int x = (int)(short)LOWORD(lparam);
|
|
int y = (int)(short)HIWORD(lparam);
|
|
for (int i = 0; i < menu.itemCount; ++i)
|
|
{
|
|
const MenuItem &item = menu.items[i];
|
|
if (x < item.rect.left || x >= item.rect.right ||
|
|
y < item.rect.top || y >= item.rect.bottom)
|
|
{
|
|
continue;
|
|
}
|
|
if (item.group == GroupLaunch)
|
|
{
|
|
menu.result = 1;
|
|
}
|
|
else if (item.group == GroupSteamHost)
|
|
{
|
|
menu.steamAction = 1;
|
|
menu.result = 1;
|
|
}
|
|
else if (item.group == GroupSteamJoin)
|
|
{
|
|
menu.steamAction = 2;
|
|
menu.result = 1;
|
|
}
|
|
else
|
|
{
|
|
menu.selection[item.group] = item.index;
|
|
InvalidateRect(window, NULL, FALSE);
|
|
}
|
|
break;
|
|
}
|
|
SetFocus(window); // clicks outside the edits take focus back
|
|
}
|
|
return 0;
|
|
|
|
case WM_KEYDOWN:
|
|
if (wparam == VK_RETURN)
|
|
{
|
|
menu.result = 1;
|
|
}
|
|
else if (wparam == VK_ESCAPE)
|
|
{
|
|
menu.result = -1;
|
|
}
|
|
return 0;
|
|
|
|
case WM_CLOSE:
|
|
menu.result = -1;
|
|
return 0;
|
|
}
|
|
return DefWindowProcW(window, message, wparam, lparam);
|
|
}
|
|
|
|
int
|
|
BTFrontEnd_Run(BTFeLaunchSpec *spec)
|
|
{
|
|
memset(spec, 0, sizeof(*spec));
|
|
memset(&menu, 0, sizeof(menu));
|
|
menu.selection[GroupLength] = 2; // 10:00, the console default
|
|
menu.selection[GroupExperience] = 2; // veteran
|
|
menu.selection[GroupColor] = 5; // Grey (the console default)
|
|
menu.selection[GroupBadge] = 0; // VGL
|
|
menu.selection[GroupDropZone] = 0; // one
|
|
|
|
char pilot_name[32] = "Dev";
|
|
char peers[128] = "";
|
|
LoadPersisted(pilot_name, sizeof(pilot_name), peers, sizeof(peers));
|
|
LayoutMenu();
|
|
|
|
WNDCLASSW window_class;
|
|
memset(&window_class, 0, sizeof(window_class));
|
|
window_class.lpfnWndProc = MenuWndProc;
|
|
window_class.hInstance = GetModuleHandleW(NULL);
|
|
window_class.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW);
|
|
window_class.lpszClassName = L"BTFrontEndWnd";
|
|
RegisterClassW(&window_class);
|
|
|
|
RECT frame_rect = { 0, 0, MenuClientW, MenuClientH };
|
|
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
|
|
AdjustWindowRect(&frame_rect, style, FALSE);
|
|
menu.window = CreateWindowW(
|
|
L"BTFrontEndWnd", L"BattleTech - Mission Console",
|
|
style, 100, 80,
|
|
frame_rect.right - frame_rect.left, frame_rect.bottom - frame_rect.top,
|
|
NULL, NULL, GetModuleHandleW(NULL), NULL);
|
|
if (menu.window == NULL)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
menu.textFont = CreateFontW(-16, 0, 0, 0, FW_NORMAL, 0, 0, 0,
|
|
DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, FIXED_PITCH, L"Consolas");
|
|
menu.titleFont = CreateFontW(-24, 0, 0, 0, FW_BOLD, 0, 0, 0,
|
|
DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, FIXED_PITCH, L"Consolas");
|
|
menu.editBrush = CreateSolidBrush(RGB(6, 20, 6));
|
|
|
|
//
|
|
// The two edit controls (name; LAN peers for host mode).
|
|
//
|
|
menu.nameEdit = CreateWindowW(L"EDIT", L"",
|
|
WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
|
|
MenuCol5X, MenuNameEditY, 240, MenuRowH + 4,
|
|
menu.window, NULL, GetModuleHandleW(NULL), NULL);
|
|
menu.peersEdit = CreateWindowW(L"EDIT", L"",
|
|
WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
|
|
MenuCol5X, MenuPeersEditY, 240, MenuRowH + 4,
|
|
menu.window, NULL, GetModuleHandleW(NULL), NULL);
|
|
SendMessageW(menu.nameEdit, WM_SETFONT, (WPARAM)menu.textFont, TRUE);
|
|
SendMessageW(menu.peersEdit, WM_SETFONT, (WPARAM)menu.textFont, TRUE);
|
|
SetWindowTextA(menu.nameEdit, pilot_name);
|
|
SetWindowTextA(menu.peersEdit, peers);
|
|
|
|
ShowWindow(menu.window, SW_SHOW);
|
|
SetForegroundWindow(menu.window);
|
|
|
|
MSG message;
|
|
while (menu.result == 0 && GetMessageW(&message, NULL, 0, 0))
|
|
{
|
|
//
|
|
// ENTER/ESC work with an edit focused too.
|
|
//
|
|
if (message.message == WM_KEYDOWN &&
|
|
(message.wParam == VK_RETURN || message.wParam == VK_ESCAPE))
|
|
{
|
|
menu.result = (message.wParam == VK_RETURN) ? 1 : -1;
|
|
continue;
|
|
}
|
|
TranslateMessage(&message);
|
|
DispatchMessageW(&message);
|
|
}
|
|
|
|
GetWindowTextA(menu.nameEdit, pilot_name, sizeof(pilot_name));
|
|
GetWindowTextA(menu.peersEdit, peers, sizeof(peers));
|
|
|
|
DestroyWindow(menu.window);
|
|
DeleteObject(menu.textFont);
|
|
DeleteObject(menu.titleFont);
|
|
DeleteObject(menu.editBrush);
|
|
|
|
if (menu.result != 1)
|
|
{
|
|
return 1; // quit
|
|
}
|
|
SavePersisted(pilot_name[0] ? pilot_name : "Dev", peers);
|
|
|
|
//
|
|
// Resolve the launch spec + write the egg.
|
|
//
|
|
int console_port = kPorts[menu.selection[GroupPort]];
|
|
const RoleEntry &scenario = kScenarios[menu.selection[GroupScenario]];
|
|
BTFeMission mission;
|
|
memset(&mission, 0, sizeof(mission));
|
|
strcpy(mission.map, kMaps[menu.selection[GroupMap]].key);
|
|
strcpy(mission.time, kTimes[menu.selection[GroupTime]].key);
|
|
strcpy(mission.weather, kWeathers[menu.selection[GroupWeather]].key);
|
|
strcpy(mission.roleKey, scenario.roleKey);
|
|
strcpy(mission.roleModel, scenario.model);
|
|
mission.temperature = 27;
|
|
mission.lengthSeconds = kLengths[menu.selection[GroupLength]].seconds;
|
|
mission.pilotCount = 1;
|
|
BTFePilot &self = mission.pilots[0];
|
|
strcpy(self.name, pilot_name[0] ? pilot_name : "Dev");
|
|
strcpy(self.vehicle, kVehicles[menu.selection[GroupVehicle]].key);
|
|
strcpy(self.color, kColors[menu.selection[GroupColor]].key);
|
|
strcpy(self.experience, kExperience[menu.selection[GroupExperience]].key);
|
|
strcpy(self.badge, kBadges[menu.selection[GroupBadge]].key);
|
|
strcpy(self.patch, kPatches[menu.selection[GroupPatch]].key);
|
|
strcpy(self.dropzone, kDropZones[menu.selection[GroupDropZone]].key);
|
|
self.advancedDamage = (kAdvDamage[menu.selection[GroupAdvDamage]].key[0] == '1');
|
|
|
|
spec->consolePort = console_port;
|
|
spec->missionSeconds = mission.lengthSeconds;
|
|
strcpy(spec->eggPath, "frontend.egg");
|
|
|
|
int mode = menu.selection[GroupMode];
|
|
#ifdef BT_STEAM
|
|
if (menu.steamAction == 1)
|
|
{
|
|
BTLobbyRoster roster;
|
|
if (BTLobby_HostAndRoom(self.name, self.vehicle, self.color,
|
|
&roster, spec->steamMyToken, sizeof(spec->steamMyToken),
|
|
spec->steamMap, sizeof(spec->steamMap)) != 0 ||
|
|
roster.memberCount == 0)
|
|
{
|
|
return 1; // cancelled / Steam unavailable
|
|
}
|
|
spec->mode = BTFeLaunchHostSteam;
|
|
mission.pilotCount = 0;
|
|
sprintf(spec->podList, "127.0.0.1:%d", console_port);
|
|
for (int m = 0; m < roster.memberCount && m < 8; ++m)
|
|
{
|
|
const BTLobbyMember &member = roster.members[m];
|
|
BTFePilot &pilot = mission.pilots[mission.pilotCount++];
|
|
strncpy(pilot.name, member.name, sizeof(pilot.name) - 1);
|
|
strncpy(pilot.vehicle,
|
|
member.vehicle[0] ? member.vehicle : kVehicles[0].key,
|
|
sizeof(pilot.vehicle) - 1);
|
|
strncpy(pilot.color,
|
|
member.color[0] ? member.color : kColors[0].key,
|
|
sizeof(pilot.color) - 1);
|
|
sprintf(pilot.address, "%s:%d",
|
|
member.fakeAddress, member.gamePort);
|
|
if (!member.isSelf)
|
|
{
|
|
char entry[64];
|
|
sprintf(entry, ",%s:%d",
|
|
member.fakeAddress, member.consolePort);
|
|
strcat(spec->podList, entry);
|
|
}
|
|
}
|
|
if (BTFeMission_WriteEgg(&mission, spec->eggPath) != 0)
|
|
{
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
if (menu.steamAction == 2)
|
|
{
|
|
if (BTLobby_JoinAndWait(self.name, self.vehicle, self.color,
|
|
spec->steamMyToken, sizeof(spec->steamMyToken),
|
|
spec->steamMap, sizeof(spec->steamMap)) != 0)
|
|
{
|
|
return 1;
|
|
}
|
|
spec->mode = BTFeLaunchJoinSteam;
|
|
spec->eggPath[0] = 0;
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
switch (mode)
|
|
{
|
|
case 0: // SOLO MISSION: networked 1-pod, marshal feeds self
|
|
spec->mode = BTFeLaunchSolo;
|
|
sprintf(self.address, "127.0.0.1:%d", console_port + 1);
|
|
sprintf(spec->podList, "127.0.0.1:%d", console_port);
|
|
break;
|
|
|
|
case 1: // RAW SOLO: the plain -egg endless path
|
|
spec->mode = BTFeLaunchRawSolo;
|
|
strcpy(self.address, "127.0.0.1");
|
|
break;
|
|
|
|
case 2: // HOST LAN: marshal feeds self + the peers list
|
|
{
|
|
spec->mode = BTFeLaunchHostLan;
|
|
sprintf(self.address, "127.0.0.1:%d", console_port + 1);
|
|
sprintf(spec->podList, "127.0.0.1:%d", console_port);
|
|
char peers_copy[128];
|
|
strcpy(peers_copy, peers);
|
|
char *cursor = strtok(peers_copy, ", ");
|
|
while (cursor != NULL && mission.pilotCount < 8)
|
|
{
|
|
char host[48];
|
|
int port = 0;
|
|
const char *colon = strrchr(cursor, ':');
|
|
if (colon != NULL && (port = atoi(colon + 1)) > 0 &&
|
|
(int)(colon - cursor) < (int)sizeof(host))
|
|
{
|
|
memcpy(host, cursor, colon - cursor);
|
|
host[colon - cursor] = 0;
|
|
BTFePilot &peer = mission.pilots[mission.pilotCount];
|
|
sprintf(peer.name, "Pilot%d", mission.pilotCount + 1);
|
|
strcpy(peer.vehicle, kVehicles[
|
|
(menu.selection[GroupVehicle] + mission.pilotCount) % COUNT(kVehicles)].key);
|
|
strcpy(peer.color, kColors[
|
|
(menu.selection[GroupColor] + mission.pilotCount) % COUNT(kColors)].key);
|
|
// peers inherit the host's loadout defaults (the operator
|
|
// sets per-pilot detail on the real console)
|
|
strcpy(peer.experience, self.experience);
|
|
strcpy(peer.badge, self.badge);
|
|
strcpy(peer.patch, self.patch);
|
|
strcpy(peer.dropzone, self.dropzone);
|
|
peer.advancedDamage = self.advancedDamage;
|
|
sprintf(peer.address, "%s:%d", host, port + 1);
|
|
++mission.pilotCount;
|
|
char entry[64];
|
|
sprintf(entry, ",%s:%d", host, port);
|
|
strcat(spec->podList, entry);
|
|
}
|
|
cursor = strtok(NULL, ", ");
|
|
}
|
|
}
|
|
break;
|
|
|
|
default: // JOIN LAN: plain -net pod; the host's marshal feeds us
|
|
spec->mode = BTFeLaunchJoinLan;
|
|
spec->eggPath[0] = 0;
|
|
return 0;
|
|
}
|
|
|
|
if (BTFeMission_WriteEgg(&mission, spec->eggPath) != 0)
|
|
{
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|