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>
This commit is contained in:
Cyd
2026-07-18 18:54:25 -05:00
co-authored by Claude Fable 5
parent ec3985e9da
commit 8cf2cc1f07
4 changed files with 637 additions and 329 deletions
+35 -51
View File
@@ -1,11 +1,11 @@
[mission]
adventure=BattleTech
map=cavern
map=grass
scenario=freeforall
time=night
time=day
weather=clear
temperature=27
length=600
length=60
[ordinals]
bitmap=Ordinal::BitMap::1
bitmap=Ordinal::BitMap::2
@@ -157,64 +157,49 @@ y=32
width=8
[pilots]
pilot=127.0.0.1:1502
pilot=127.0.0.1:1602
[127.0.0.1:1502]
hostType=0
advancedDamage=1
loadzones=1
name=Aeolus
name=Dev
bitmapindex=1
experience=expert
badge=VGL
patch=Yellow
role=Role::Default
dropzone=one
vehicle=bhk1
vehicle=avatar
vehicleValue=1000
color=White
[127.0.0.1:1602]
hostType=0
advancedDamage=1
loadzones=1
name=Boreas
bitmapindex=2
experience=expert
badge=VGL
patch=Yellow
role=Role::Default
dropzone=one
vehicle=ava1
vehicleValue=1000
color=Red
[largebitmap]
bitmap=BitMap::Large::Aeolus
[BitMap::Large::Aeolus]
bitmap=BitMap::Large::Dev
[BitMap::Large::Dev]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=000003FE0000000001E0000000000000
bitmap=000007FF0000000001E0000000000000
bitmap=00000F8F8000000001E0000000000000
bitmap=00000F078000000001E0000000000000
bitmap=00000F0781FF00FF81E0F0781FC00000
bitmap=00000F0783FF81FFC1E0F0783FE00000
bitmap=00000F0787C7C3E3E1E0F0783C700000
bitmap=00000F078783C3C1E1E0F07878300000
bitmap=00000F078783C3C1E1E0F07878000000
bitmap=00000FFF8783C3C1E1E0F0787C000000
bitmap=00000FFF8783C3C1E1E0F0787F000000
bitmap=00000F0787FFC3C1E1E0F0783FC00000
bitmap=00000F0787FFC3C1E1E0F0781FE00000
bitmap=00000F07878003C1E1E0F07807F00000
bitmap=00000F07878003C1E1E0F07801F00000
bitmap=00000F07878003C1E1E0F07800F00000
bitmap=00000F078780C3C1E1E0F07860F00000
bitmap=00000F0787C1C3E3E1E078F871E00000
bitmap=00000F0783FF81FFC1E07FF83FE00000
bitmap=00000F0781FF00FF81E03F781FC00000
bitmap=00000000000000000000000000000000
bitmap=00000000000FFC000000000000000000
bitmap=00000000000FFE000000000000000000
bitmap=00000000000FFF000000000000000000
bitmap=00000000000E07800000000000000000
bitmap=00000000000E0381F070700000000000
bitmap=00000000000E01C3F870700000000000
bitmap=00000000000E01C7FC70700000000000
bitmap=00000000000E01CF1C38E00000000000
bitmap=00000000000E01CE0E38E00000000000
bitmap=00000000000E01CFFE38E00000000000
bitmap=00000000000E01CFFE1DC00000000000
bitmap=00000000000E01CE001DC00000000000
bitmap=00000000000E038E000D800000000000
bitmap=00000000000E07870E0F800000000000
bitmap=00000000000FFF07FC0F800000000000
bitmap=00000000000FFE03FC07000000000000
bitmap=00000000000FFC01F007000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
@@ -225,15 +210,15 @@ x=128
y=32
width=8
[smallbitmap]
bitmap=BitMap::Small::Aeolus
[BitMap::Small::Aeolus]
bitmap=BitMap::Small::Dev
[BitMap::Small::Dev]
bitmap=00000000000000000000000000000000
bitmap=0000000000000000000007C000000000
bitmap=000006600000000000000633D8C00000
bitmap=0000063678C00000000006366D800000
bitmap=00000637ED800000000006360D800000
bitmap=0000066667000000000007C3C7000000
bitmap=00000000000000000000000000000000
bitmap=0000000000000000003E00000C000000
bitmap=006300000C00000000631F0F8CC63C00
bitmap=00633198CCC66200007F3198CCC66000
bitmap=00633F98CCC6780000633018CCC61E00
bitmap=00633018CCC6060000633098CCC64600
bitmap=00631F0F8C7E3C000000000000000000
bitmap=00000000000000000000000000000000
x=64
y=16
@@ -242,4 +227,3 @@ width=4
model=dfltrole
[Role::NoReturn]
model=noretun
+10
View File
@@ -0,0 +1,10 @@
MAP=0
TIME=0
WEATHER=0
MECH=0
LENGTH=0
MODE=0
COLOR=0
NET PORT=0
NAME=Dev
PEERS=
+546 -271
View File
@@ -19,23 +19,6 @@
// 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
@@ -363,184 +346,435 @@ int
}
//###########################################################################
// 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.
// 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.
//###########################################################################
enum MenuField
struct CatalogEntry
{
FieldMap = 0,
FieldTime,
FieldWeather,
FieldLength,
FieldVehicle,
FieldColor,
FieldName,
FieldMode,
FieldPort,
FieldPeers,
FieldLaunch,
FieldCount
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 mapIndex, timeIndex, weatherIndex, lengthIndex;
int vehicleIndex, colorIndex, modeIndex, portIndex;
char pilotName[32];
char peers[128];
int selected;
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 void
CycleField(int field, int direction)
static const COLORREF kGreenBright = RGB(64, 255, 64);
static const COLORREF kGreenDim = RGB(24, 140, 24);
static int
GroupSize(int group)
{
switch (field)
switch (group)
{
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;
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
FieldLine(int field, char *out /*128*/)
GroupRowText(int group, int index, char *out /*64*/)
{
switch (field)
switch (group)
{
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;
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 dc = BeginPaint(window, &paint);
HDC screen = BeginPaint(window, &paint);
RECT client;
GetClientRect(window, &client);
HBRUSH background = CreateSolidBrush(RGB(4, 12, 4));
// 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 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);
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);
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);
SelectObject(dc, menu.textFont);
for (int field = 0; field < FieldCount; ++field)
//
// Group headers (above each group's first row).
//
for (int i = 0; i < menu.itemCount; ++i)
{
char line[160];
FieldLine(field, line);
WCHAR wide[160];
int n = 0;
for (const char *s = line; *s && n < 159; ++s)
const MenuItem &item = menu.items[i];
if (item.index != 0 || item.group > GroupPort)
{
wide[n++] = (WCHAR)*s;
continue;
}
wide[n] = 0;
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);
}
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 };
//
// 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 = { 24, row.top, 40, row.bottom };
DrawTextW(dc, L">", -1, &marker, DT_LEFT | DT_TOP | DT_SINGLELINE);
RECT marker = item.rect;
marker.right = marker.left + 14;
DrawMenuText(dc, ">", &marker, kGreenBright,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
}
DrawTextW(dc, wide, -1, &row, DT_LEFT | DT_TOP | DT_SINGLELINE);
DrawMenuText(dc, text, &row,
selected ? kGreenBright : kGreenDim,
DT_LEFT | DT_VCENTER | 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);
//
// 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);
DeleteObject(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 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)
{
@@ -550,59 +784,59 @@ static LRESULT CALLBACK
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_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);
int field = (y - 64) / 28;
if (field >= 0 && field < FieldCount)
for (int i = 0; i < menu.itemCount; ++i)
{
menu.selected = field;
if (field == FieldLaunch)
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
{
CycleField(field, +1);
menu.selection[item.group] = item.index;
InvalidateRect(window, NULL, FALSE);
}
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;
@@ -618,8 +852,12 @@ int
{
memset(spec, 0, sizeof(*spec));
memset(&menu, 0, sizeof(menu));
strcpy(menu.pilotName, "Dev");
menu.lengthIndex = 1; // 600 s, the console default
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));
@@ -629,53 +867,152 @@ int
window_class.lpszClassName = L"BTFrontEndWnd";
RegisterClassW(&window_class);
HWND window = CreateWindowW(
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",
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
120, 120, 700, 480,
style, 100, 80,
frame_rect.right - frame_rect.left, frame_rect.bottom - frame_rect.top,
NULL, NULL, GetModuleHandleW(NULL), NULL);
if (window == NULL)
if (menu.window == NULL)
{
return -1;
}
ShowWindow(window, SW_SHOW);
SetForegroundWindow(window);
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);
}
DestroyWindow(window);
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.portIndex];
int console_port = kPorts[menu.selection[GroupPort]];
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]);
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.lengthIndex];
mission.lengthSeconds = kLengths[menu.selection[GroupLength]].seconds;
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]);
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");
switch (menu.modeIndex)
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;
@@ -693,12 +1030,8 @@ int
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);
strcpy(peers_copy, peers);
char *cursor = strtok(peers_copy, ", ");
while (cursor != NULL && mission.pilotCount < 8)
{
@@ -713,9 +1046,9 @@ int
BTFePilot &peer = mission.pilots[mission.pilotCount];
sprintf(peer.name, "Pilot%d", mission.pilotCount + 1);
strcpy(peer.vehicle, kVehicles[
(menu.vehicleIndex + mission.pilotCount) % COUNT(kVehicles)]);
(menu.selection[GroupVehicle] + mission.pilotCount) % COUNT(kVehicles)].key);
strcpy(peer.color, kColors[
(menu.colorIndex + mission.pilotCount) % COUNT(kColors)]);
(menu.selection[GroupColor] + mission.pilotCount) % COUNT(kColors)].key);
sprintf(peer.address, "%s:%d", host, port + 1);
++mission.pilotCount;
char entry[64];
@@ -727,68 +1060,10 @@ int
}
break;
case 3: // JOIN LAN: plain -net pod; the host's marshal feeds us
default: // 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)
+46 -7
View File
@@ -129,6 +129,8 @@ static int
static int roomIsHost = 0;
static int roomResult = 0; // 0 pending, 1 go, -1 cancel
static RECT roomLaunchRect = { 140, 250, 420, 296 };
static RECT roomLeaveRect = { 140, 306, 300, 336 };
static void
PaintRoom(HWND window)
@@ -147,14 +149,17 @@ static void
HFONT old_font = (HFONT)SelectObject(dc, font);
SetTextColor(dc, RGB(90, 255, 90));
//
// The RP412 room look (RP_L4/RPL4LOBBY.cpp): pilot roster with
// loadouts, a framed LAUNCH button for the owner, LEAVE for all.
//
WCHAR line[160];
int y = 16;
wsprintfW(line, L"STEAM LOBBY -- %s",
roomIsHost ? L"HOSTING (ENTER = launch, ESC = cancel)"
: L"WAITING FOR THE HOST (ESC = leave)");
RECT row = { 24, y, client.right - 24, y + 24 };
roomIsHost ? L"YOU ARE HOSTING" : L"WAITING FOR THE HOST");
RECT row = { 24, y, client.right - 24, y + 26 };
DrawTextW(dc, line, -1, &row, DT_LEFT | DT_TOP | DT_SINGLELINE);
y += 40;
y += 44;
ISteamMatchmaking *matchmaking = SteamMatchmaking();
int count = matchmaking->GetNumLobbyMembers(currentLobby);
@@ -167,18 +172,35 @@ static void
int n = 0;
for (const char *s = member.name; *s && n < 60; ++s) name[n++] = (WCHAR)*s;
name[n] = 0;
wsprintfW(line, L" %d. %s%s", i + 1, name,
member.isSelf ? L" (you)" : L"");
wsprintfW(line, L" %d. %-16s %hs, %hs%s", i + 1, name,
member.vehicle[0] ? member.vehicle : "mech",
member.color[0] ? member.color : "-",
member.isSelf ? L" (you)" : L"");
}
else
{
wsprintfW(line, L" %d. (joining...)", i + 1);
wsprintfW(line, L" %d. (joining...)", i + 1);
}
RECT member_row = { 24, y, client.right - 24, y + 24 };
DrawTextW(dc, line, -1, &member_row, DT_LEFT | DT_TOP | DT_SINGLELINE);
y += 26;
}
//
// Framed buttons (hit rects shared with the WndProc).
//
HBRUSH frame = CreateSolidBrush(RGB(90, 255, 90));
if (roomIsHost)
{
FrameRect(dc, &roomLaunchRect, frame);
DrawTextW(dc, L"L A U N C H M I S S I O N", -1, &roomLaunchRect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
FrameRect(dc, &roomLeaveRect, frame);
DrawTextW(dc, L"LEAVE LOBBY", -1, &roomLeaveRect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
DeleteObject(frame);
SelectObject(dc, old_font);
DeleteObject(font);
EndPaint(window, &paint);
@@ -217,6 +239,23 @@ static LRESULT CALLBACK
roomResult = -1;
}
return 0;
case WM_LBUTTONDOWN:
{
int x = (int)(short)LOWORD(lparam);
int y = (int)(short)HIWORD(lparam);
if (roomIsHost &&
x >= roomLaunchRect.left && x < roomLaunchRect.right &&
y >= roomLaunchRect.top && y < roomLaunchRect.bottom)
{
roomResult = 1;
}
else if (x >= roomLeaveRect.left && x < roomLeaveRect.right &&
y >= roomLeaveRect.top && y < roomLeaveRect.bottom)
{
roomResult = -1;
}
}
return 0;
case WM_CLOSE:
roomResult = -1;
return 0;