Files
BT411/game/glass/btl4fe.cpp
T
CydandClaude Fable 5 8cf2cc1f07 FE: the RP412 menu + lobby-room pattern; flat single-exe dist (per Cyd)
Menu (btl4fe, modeled on RP412 RP_L4/RPL4FE.cpp): full catalogs as
always-visible clickable LISTS in three columns with friendly names --
8 maps, the canonical 8 certified mechs, colors, times, weather, lengths,
modes -- bright/dim green with a selection chevron, double-buffered paint,
real EDIT controls for pilot name + LAN peers (green-on-black via
WM_CTLCOLOREDIT), framed LAUNCH / HOST STEAM / JOIN STEAM buttons, and the
loadout persisted across the per-mission relaunch (fe_last.ini, gitignored
implicitly by *.ini? no -- plain file beside the eggs).  ENTER/ESC kept.

Lobby room (RP412 RPL4LOBBY look): pilot roster with loadouts (mech, color),
framed L A U N C H  M I S S I O N (owner) + LEAVE LOBBY buttons, clickable.

Dist: FLAT layout (the RP412 convention) -- ONE btl4.exe entry point at the
folder root beside the content tree (the shape TeslaConsole manages), DLLs +
launchers at root, content flattened from git-tracked files.  Verified: the
staged flat tree boots the Mission Console zero-arg from its own root; the
click-driven menu launches a full marshaled mission (row click -> length=60
in the egg, LAUNCH click -> ladder -> 41+ ticks, fe_last.ini written).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:54:25 -05:00

1075 lines
31 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=1\n"
"loadzones=1\n"
"name=%s\n"
"bitmapindex=%d\n"
"experience=expert\n"
"badge=VGL\n"
"patch=Yellow\n"
"role=Role::Default\n"
"dropzone=one\n"
"vehicle=%s\n"
"vehicleValue=1000\n"
"color=%s\n",
pilot.address, pilot.name, p + 1,
pilot.vehicle, pilot.color);
}
//
// 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;
};
static const CatalogEntry kMaps[] =
{
{ "grass", "Grasslands" },
{ "cavern", "The Cavern" },
{ "rav", "The Ravine" },
{ "polar3", "Polar Station 3" },
{ "polar4", "Polar Station 4" },
{ "arena1", "Arena One" },
{ "arena2", "Arena Two" },
{ "dbase", "Desert Base" },
};
// The canonical 8 (ALL 18 ModelList names certified 2026-07-18; the menu
// offers the full mechs -- variants remain reachable via hand-built eggs).
static const CatalogEntry kVehicles[] =
{
{ "avatar", "Avatar" },
{ "blkhawk", "Black Hawk" },
{ "loki", "Loki" },
{ "madcat", "Mad Cat" },
{ "owens", "Owens" },
{ "sunder", "Sunder" },
{ "thor", "Thor" },
{ "vulture", "Vulture" },
};
static const CatalogEntry kColors[] =
{
{ "White", "White" }, { "Black", "Black" }, { "Crimson", "Crimson" },
};
static const CatalogEntry kTimes[] =
{
{ "day", "Day" }, { "night", "Night" },
{ "morning", "Morning" }, { "dusk", "Dusk" },
};
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,
GroupTime,
GroupWeather,
GroupVehicle,
GroupLength,
GroupMode,
GroupColor,
GroupPort,
GroupLaunch,
GroupSteamHost,
GroupSteamJoin,
GroupCount
};
struct MenuItem
{
int group;
int index;
RECT rect;
};
struct MenuState
{
int selection[GroupCount];
MenuItem items[96];
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);
static int
GroupSize(int group)
{
switch (group)
{
case GroupMap: return COUNT(kMaps);
case GroupTime: return COUNT(kTimes);
case GroupWeather: return COUNT(kWeathers);
case GroupVehicle: return COUNT(kVehicles);
case GroupLength: return COUNT(kLengths);
case GroupMode: return COUNT(kModes);
case GroupColor: return COUNT(kColors);
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 GroupTime: strcpy(out, kTimes[index].name); break;
case GroupWeather: strcpy(out, kWeathers[index].name); break;
case GroupVehicle: strcpy(out, kVehicles[index].name); break;
case GroupLength: strcpy(out, kLengths[index].name); break;
case GroupMode: strcpy(out, kModes[index].name); break;
case GroupColor: strcpy(out, kColors[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 GroupTime: return "TIME";
case GroupWeather: return "WEATHER";
case GroupVehicle: return "MECH";
case GroupLength: return "LENGTH";
case GroupMode: return "MODE";
case GroupColor: return "COLOR";
case GroupPort: return "NET PORT";
default: return "";
}
}
//
// Layout: three catalog columns + the button column.
// col1: MAP, TIME, WEATHER col2: MECH, LENGTH
// col3: MODE, COLOR, NET PORT col4: name/peers edits + buttons
//
enum
{
MenuClientW = 960,
MenuClientH = 560,
MenuRowH = 22,
MenuColW = 190,
MenuCol1X = 28,
MenuCol2X = 254,
MenuCol3X = 480,
MenuCol4X = 706,
MenuTopY = 64
};
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;
y = MenuTopY; AddGroup(GroupMap, MenuCol1X, &y);
AddGroup(GroupTime, MenuCol1X, &y);
AddGroup(GroupWeather, MenuCol1X, &y);
y = MenuTopY; AddGroup(GroupVehicle, MenuCol2X, &y);
AddGroup(GroupLength, MenuCol2X, &y);
y = MenuTopY; AddGroup(GroupMode, MenuCol3X, &y);
AddGroup(GroupColor, MenuCol3X, &y);
AddGroup(GroupPort, MenuCol3X, &y);
AddButton(GroupLaunch, MenuCol4X, MenuClientH - 190, 220, 52);
#ifdef BT_STEAM
AddButton(GroupSteamHost, MenuCol4X, MenuClientH - 120, 220, 34);
AddButton(GroupSteamJoin, MenuCol4X, 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).
//
RECT label = { MenuCol4X, MenuTopY, MenuCol4X + 220, MenuTopY + MenuRowH };
DrawMenuText(dc, "PILOT NAME", &label, kGreenDim,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
label.top = MenuTopY + 3 * MenuRowH;
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
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,
MenuCol4X, MenuTopY + MenuRowH, 220, MenuRowH + 4,
menu.window, NULL, GetModuleHandleW(NULL), NULL);
menu.peersEdit = CreateWindowW(L"EDIT", L"",
WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
MenuCol4X, MenuTopY + 4 * MenuRowH, 220, 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]];
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);
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);
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);
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;
}