NEW gated (BT_STEAM) game/glass/btl4lobby: an ISteamMatchmaking room replacing the arcade Site-Management screen -- host creates (lobby data btl4=1), members join by filter, pilots publish name/mech/color, the host ENTER mints the roster and signals GO. THE FAKEIP LESSON (live-verified, prior-art assumption DISPROVEN): a fresh process gets a DIFFERENT FakeIP, so menu-time fake addresses go stale by mission time -- the first cycle timed out connecting to its own stale address. Redesign: roster addresses are opaque ipv4-shaped TOKENS (169.254.77.N with ports 1501/1502) minted by the host at GO, mapped to Steam IDENTITIES; connections ride ConnectP2P(identity, channel) with console=0/game=1 virtual ports; the map reaches mission processes via env (BT_FE_MYFAKE + BT_FE_STEAMMAP); the GetMyAddress seam feeds the pod its own token for roster self-match; CheckSocket reports peers by token. L4STEAMNET reworked (no FakeIP allocation at all); the marshal gained the Steam-wire branch. Verified single-machine (ON/ON, live Steam): menu -> HOST STEAM LOBBY -> room -> GO -> token map minted -> egg roster carries the token -> mission process up with 1 roster token incl. self -> pod self-matches -> stock console ladder -> mission RUNS (46 ticks). Remaining: the live 2-account cross-machine session (docs/STEAM_TEST.md). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
800 lines
24 KiB
C++
800 lines
24 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.
|
|
//###########################################################################
|
|
|
|
static const char *kMaps[] =
|
|
{ "grass", "cavern", "rav", "polar3", "polar4", "arena1", "arena2", "dbase" };
|
|
static const char *kTimes[] = { "day", "night", "morning", "dusk" };
|
|
static const char *kWeathers[] = { "clear", "fog", "rain", "snow" };
|
|
static const int kLengths[] = { 300, 600, 1200, 60 };
|
|
static const char *kVehicles[] = { "bhk1", "madcat", "ava1" };
|
|
static const char *kColors[] = { "White", "Black", "Crimson" };
|
|
static const char *kModes[] =
|
|
{ "SOLO MISSION (networked)", "RAW SOLO (-egg, endless)",
|
|
"HOST LAN", "JOIN LAN",
|
|
#ifdef BT_STEAM
|
|
"HOST STEAM LOBBY", "JOIN STEAM LOBBY",
|
|
#endif
|
|
};
|
|
static const int kPorts[] = { 1501, 1601, 1701, 1801 };
|
|
|
|
#define COUNT(a) ((int)(sizeof(a)/sizeof((a)[0])))
|
|
|
|
//###########################################################################
|
|
// 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 -- a green-on-black GDI catalog, own message loop (runs before
|
|
// any engine init). Up/Down select, Left/Right (or click) cycle, type
|
|
// into the text fields, ENTER launches, ESC quits.
|
|
//###########################################################################
|
|
|
|
enum MenuField
|
|
{
|
|
FieldMap = 0,
|
|
FieldTime,
|
|
FieldWeather,
|
|
FieldLength,
|
|
FieldVehicle,
|
|
FieldColor,
|
|
FieldName,
|
|
FieldMode,
|
|
FieldPort,
|
|
FieldPeers,
|
|
FieldLaunch,
|
|
FieldCount
|
|
};
|
|
|
|
struct MenuState
|
|
{
|
|
int mapIndex, timeIndex, weatherIndex, lengthIndex;
|
|
int vehicleIndex, colorIndex, modeIndex, portIndex;
|
|
char pilotName[32];
|
|
char peers[128];
|
|
int selected;
|
|
int result; // 0 pending, 1 launch, -1 quit
|
|
};
|
|
|
|
static MenuState menu;
|
|
|
|
static void
|
|
CycleField(int field, int direction)
|
|
{
|
|
switch (field)
|
|
{
|
|
case FieldMap:
|
|
menu.mapIndex = (menu.mapIndex + direction + COUNT(kMaps)) % COUNT(kMaps);
|
|
break;
|
|
case FieldTime:
|
|
menu.timeIndex = (menu.timeIndex + direction + COUNT(kTimes)) % COUNT(kTimes);
|
|
break;
|
|
case FieldWeather:
|
|
menu.weatherIndex = (menu.weatherIndex + direction + COUNT(kWeathers)) % COUNT(kWeathers);
|
|
break;
|
|
case FieldLength:
|
|
menu.lengthIndex = (menu.lengthIndex + direction + COUNT(kLengths)) % COUNT(kLengths);
|
|
break;
|
|
case FieldVehicle:
|
|
menu.vehicleIndex = (menu.vehicleIndex + direction + COUNT(kVehicles)) % COUNT(kVehicles);
|
|
break;
|
|
case FieldColor:
|
|
menu.colorIndex = (menu.colorIndex + direction + COUNT(kColors)) % COUNT(kColors);
|
|
break;
|
|
case FieldMode:
|
|
menu.modeIndex = (menu.modeIndex + direction + COUNT(kModes)) % COUNT(kModes);
|
|
break;
|
|
case FieldPort:
|
|
menu.portIndex = (menu.portIndex + direction + COUNT(kPorts)) % COUNT(kPorts);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
FieldLine(int field, char *out /*128*/)
|
|
{
|
|
switch (field)
|
|
{
|
|
case FieldMap: sprintf(out, "MAP < %s >", kMaps[menu.mapIndex]); break;
|
|
case FieldTime: sprintf(out, "TIME < %s >", kTimes[menu.timeIndex]); break;
|
|
case FieldWeather: sprintf(out, "WEATHER < %s >", kWeathers[menu.weatherIndex]); break;
|
|
case FieldLength: sprintf(out, "LENGTH < %d s >", kLengths[menu.lengthIndex]); break;
|
|
case FieldVehicle: sprintf(out, "MECH < %s >", kVehicles[menu.vehicleIndex]); break;
|
|
case FieldColor: sprintf(out, "COLOR < %s >", kColors[menu.colorIndex]); break;
|
|
case FieldName: sprintf(out, "PILOT [ %s_ ]", menu.pilotName); break;
|
|
case FieldMode: sprintf(out, "MODE < %s >", kModes[menu.modeIndex]); break;
|
|
case FieldPort: sprintf(out, "NET PORT < %d >", kPorts[menu.portIndex]); break;
|
|
case FieldPeers: sprintf(out, "PEERS [ %s_ ] (host: ip:port,...)", menu.peers); break;
|
|
case FieldLaunch: sprintf(out, " >>> LAUNCH <<<"); break;
|
|
default: out[0] = 0; break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
PaintMenu(HWND window)
|
|
{
|
|
PAINTSTRUCT paint;
|
|
HDC dc = BeginPaint(window, &paint);
|
|
|
|
RECT client;
|
|
GetClientRect(window, &client);
|
|
HBRUSH background = CreateSolidBrush(RGB(4, 12, 4));
|
|
FillRect(dc, &client, background);
|
|
DeleteObject(background);
|
|
SetBkMode(dc, TRANSPARENT);
|
|
|
|
HFONT font = CreateFontW(-16, 0, 0, 0, FW_NORMAL, 0, 0, 0,
|
|
DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, FIXED_PITCH, L"Consolas");
|
|
HFONT old_font = (HFONT)SelectObject(dc, font);
|
|
|
|
SetTextColor(dc, RGB(90, 255, 90));
|
|
RECT title_rect = { 24, 16, client.right - 24, 44 };
|
|
DrawTextW(dc, L"BATTLETECH -- MISSION CONSOLE", -1, &title_rect,
|
|
DT_LEFT | DT_TOP | DT_SINGLELINE);
|
|
|
|
for (int field = 0; field < FieldCount; ++field)
|
|
{
|
|
char line[160];
|
|
FieldLine(field, line);
|
|
WCHAR wide[160];
|
|
int n = 0;
|
|
for (const char *s = line; *s && n < 159; ++s)
|
|
{
|
|
wide[n++] = (WCHAR)*s;
|
|
}
|
|
wide[n] = 0;
|
|
|
|
int selected = (field == menu.selected);
|
|
SetTextColor(dc, selected ? RGB(200, 255, 120) : RGB(70, 200, 70));
|
|
RECT row = { 40, 64 + field * 28, client.right - 24, 92 + field * 28 };
|
|
if (selected)
|
|
{
|
|
RECT marker = { 24, row.top, 40, row.bottom };
|
|
DrawTextW(dc, L">", -1, &marker, DT_LEFT | DT_TOP | DT_SINGLELINE);
|
|
}
|
|
DrawTextW(dc, wide, -1, &row, DT_LEFT | DT_TOP | DT_SINGLELINE);
|
|
}
|
|
|
|
SetTextColor(dc, RGB(40, 140, 40));
|
|
RECT help_rect = { 24, client.bottom - 52, client.right - 24, client.bottom - 8 };
|
|
DrawTextW(dc,
|
|
L"UP/DOWN select LEFT/RIGHT or click = change type into [ ] fields\n"
|
|
L"ENTER = launch ESC = quit",
|
|
-1, &help_rect, DT_LEFT | DT_TOP);
|
|
|
|
SelectObject(dc, old_font);
|
|
DeleteObject(font);
|
|
EndPaint(window, &paint);
|
|
}
|
|
|
|
static void
|
|
TypeIntoField(int field, WPARAM ch)
|
|
{
|
|
char *buffer = (field == FieldName) ? menu.pilotName
|
|
: (field == FieldPeers) ? menu.peers : NULL;
|
|
int capacity = (field == FieldName) ? (int)sizeof(menu.pilotName)
|
|
: (int)sizeof(menu.peers);
|
|
if (buffer == NULL)
|
|
{
|
|
return;
|
|
}
|
|
int length = (int)strlen(buffer);
|
|
if (ch == VK_BACK)
|
|
{
|
|
if (length > 0)
|
|
{
|
|
buffer[length - 1] = 0;
|
|
}
|
|
return;
|
|
}
|
|
if (ch >= 32 && ch < 127 && length < capacity - 1)
|
|
{
|
|
//
|
|
// Pilot names become NotationFile page names -- keep them clean.
|
|
//
|
|
if (field == FieldName &&
|
|
!((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') ||
|
|
(ch >= '0' && ch <= '9')))
|
|
{
|
|
return;
|
|
}
|
|
buffer[length] = (char)ch;
|
|
buffer[length + 1] = 0;
|
|
}
|
|
}
|
|
|
|
static LRESULT CALLBACK
|
|
MenuWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam)
|
|
{
|
|
switch (message)
|
|
{
|
|
case WM_PAINT:
|
|
PaintMenu(window);
|
|
return 0;
|
|
|
|
case WM_KEYDOWN:
|
|
switch (wparam)
|
|
{
|
|
case VK_UP:
|
|
menu.selected = (menu.selected + FieldCount - 1) % FieldCount;
|
|
break;
|
|
case VK_DOWN:
|
|
menu.selected = (menu.selected + 1) % FieldCount;
|
|
break;
|
|
case VK_LEFT:
|
|
CycleField(menu.selected, -1);
|
|
break;
|
|
case VK_RIGHT:
|
|
CycleField(menu.selected, +1);
|
|
break;
|
|
case VK_RETURN:
|
|
menu.result = 1;
|
|
break;
|
|
case VK_ESCAPE:
|
|
menu.result = -1;
|
|
break;
|
|
case VK_BACK:
|
|
TypeIntoField(menu.selected, VK_BACK);
|
|
break;
|
|
}
|
|
InvalidateRect(window, NULL, FALSE);
|
|
return 0;
|
|
|
|
case WM_CHAR:
|
|
if (wparam >= 32)
|
|
{
|
|
TypeIntoField(menu.selected, wparam);
|
|
InvalidateRect(window, NULL, FALSE);
|
|
}
|
|
return 0;
|
|
|
|
case WM_LBUTTONDOWN:
|
|
{
|
|
int y = (int)(short)HIWORD(lparam);
|
|
int field = (y - 64) / 28;
|
|
if (field >= 0 && field < FieldCount)
|
|
{
|
|
menu.selected = field;
|
|
if (field == FieldLaunch)
|
|
{
|
|
menu.result = 1;
|
|
}
|
|
else
|
|
{
|
|
CycleField(field, +1);
|
|
}
|
|
InvalidateRect(window, NULL, FALSE);
|
|
}
|
|
}
|
|
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));
|
|
strcpy(menu.pilotName, "Dev");
|
|
menu.lengthIndex = 1; // 600 s, the console default
|
|
|
|
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);
|
|
|
|
HWND window = CreateWindowW(
|
|
L"BTFrontEndWnd", L"BattleTech - Mission Console",
|
|
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
|
|
120, 120, 700, 480,
|
|
NULL, NULL, GetModuleHandleW(NULL), NULL);
|
|
if (window == NULL)
|
|
{
|
|
return -1;
|
|
}
|
|
ShowWindow(window, SW_SHOW);
|
|
SetForegroundWindow(window);
|
|
|
|
MSG message;
|
|
while (menu.result == 0 && GetMessageW(&message, NULL, 0, 0))
|
|
{
|
|
TranslateMessage(&message);
|
|
DispatchMessageW(&message);
|
|
}
|
|
DestroyWindow(window);
|
|
|
|
if (menu.result != 1)
|
|
{
|
|
return 1; // quit
|
|
}
|
|
|
|
//
|
|
// Resolve the launch spec + write the egg.
|
|
//
|
|
int console_port = kPorts[menu.portIndex];
|
|
BTFeMission mission;
|
|
memset(&mission, 0, sizeof(mission));
|
|
strcpy(mission.map, kMaps[menu.mapIndex]);
|
|
strcpy(mission.time, kTimes[menu.timeIndex]);
|
|
strcpy(mission.weather, kWeathers[menu.weatherIndex]);
|
|
mission.temperature = 27;
|
|
mission.lengthSeconds = kLengths[menu.lengthIndex];
|
|
mission.pilotCount = 1;
|
|
BTFePilot &self = mission.pilots[0];
|
|
strcpy(self.name, menu.pilotName[0] ? menu.pilotName : "Dev");
|
|
strcpy(self.vehicle, kVehicles[menu.vehicleIndex]);
|
|
strcpy(self.color, kColors[menu.colorIndex]);
|
|
|
|
spec->consolePort = console_port;
|
|
spec->missionSeconds = mission.lengthSeconds;
|
|
strcpy(spec->eggPath, "frontend.egg");
|
|
|
|
switch (menu.modeIndex)
|
|
{
|
|
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);
|
|
//
|
|
// Each peer "ip:consolePort" joins the roster with its GAME
|
|
// port (console+1) and the marshal list with its console port.
|
|
//
|
|
char peers_copy[128];
|
|
strcpy(peers_copy, menu.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.vehicleIndex + mission.pilotCount) % COUNT(kVehicles)]);
|
|
strcpy(peer.color, kColors[
|
|
(menu.colorIndex + mission.pilotCount) % COUNT(kColors)]);
|
|
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;
|
|
|
|
case 3: // JOIN LAN: plain -net pod; the host's marshal feeds us
|
|
spec->mode = BTFeLaunchJoinLan;
|
|
spec->eggPath[0] = 0; // no egg written; host provides
|
|
return 0;
|
|
|
|
#ifdef BT_STEAM
|
|
case 4: // HOST STEAM LOBBY: the room fills the roster (self first)
|
|
{
|
|
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;
|
|
//
|
|
// Roster -> egg pilots (FAKE game addresses; self is pilot 1)
|
|
// + the marshal pod list (self over TCP loopback, members at
|
|
// their FAKE console addresses over the Steam wire).
|
|
//
|
|
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],
|
|
sizeof(pilot.vehicle) - 1);
|
|
strncpy(pilot.color,
|
|
member.color[0] ? member.color : kColors[0],
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 5: // JOIN STEAM LOBBY: wait for GO, then a Steam-wire pod
|
|
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
|
|
|
|
default:
|
|
return 1;
|
|
}
|
|
|
|
if (BTFeMission_WriteEgg(&mission, spec->eggPath) != 0)
|
|
{
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|