Files
BT411/game/glass/btl4fe.cpp
T
Joe DiPrimaandClaude Opus 5 3109cfc49a #116: steam JOINERS were silently locked to VETERAN -- restore their experience selector
Oracle isolated it: "standard mode still having heat and leaks only occurred in
steam not in solo.  Sauron and Lynx did not have the issue but Conn Man, Rajel
and myself did."  The night-9 logs close the case:

    affected  (Oracle/ConnMan/Rajel):  [exp] experience=2 heatModelOn=1
    clean     (Sauron's std sessions): [exp] experience=1 heatModelOn=0

CAUSE: the glass front end's JOIN layout trimmed the menu to "the mech list +
the JOIN button; everything else is the operator's call" -- and the experience
selector's hidden default is 2 (veteran).  So HOSTS got whatever they picked
(Sauron hosted the lobbies all night -> his standard landed), while every
JOINER launched as veteran regardless of anything they did -- which is why
Oracle's cycling-the-settings experiment changed nothing, and why solo (full
menu) never showed the problem.  heat >= veteran, hence "standard but heating".

DESIGN CONFIRMED BY THE USER before fixing: experience is PER-PLAYER by the
original design -- the sysop set each user's tier and mixed-experience matches
were legal.  Architecture already supports it end to end: each node's master
player reads its OWN egg's experience (btMission+0xE4 -> BTPlayer @004c0bc8),
so per-node choice IS per-player choice.

FIX: the join layout gains the GroupExperience selector (the selection->egg
write path is the pre-existing host path, field-proven).  Also: the front end
now prints "[fe] pilot experience=<x> (join|host/solo)" at launch, so every
future field log answers this class of report without asking anyone.

VERIFICATION BOUNDARY, stated plainly: builds both configs; the write path is
shared with the host flow which the field already exercises; the visible
selector + the [fe] line need one join-mode launch to eyeball, and the field
re-test is Oracle running a standard steam match as a JOINER and seeing no
heat.

Tooling note recorded in test-harness.md: bash-heredoc python collapses one
backslash level even single-quoted -- a "\n" arrives as a real newline and
replaces silently no-op.  Build backslashes from bytes([92]); verify replaces
by length delta, not by the script saying "fixed".  (This burned four edit
rounds tonight and several earlier C2001 hunts.)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 10:40:51 -05:00

1320 lines
39 KiB
C++

//###########################################################################
// btl4fe -- the miniconsole front end (BT_GLASS only; compiled only when
// the gate is on -- see CMakeLists.txt). Design: btl4fe.hpp.
//###########################################################################
#include "btl4fe.hpp"
#ifdef BT_STEAM
#include "btl4lobby.hpp"
#endif
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
//###########################################################################
// Catalogs -- the verified content values (context/content-archives.md map
// sweep; vehicle/color values from the shipped eggs). The egg writer
// accepts anything; these are the menu's cycle sets.
//###########################################################################
//###########################################################################
// The 4 static rank-place ordinal bitmaps -- byte-identical to the console
// eggs (content/MP.EGG [ordinals] block: "1st" "2nd" "3rd" "4th" as 128x32
// plasma pages).
//###########################################################################
static const char *kOrdinalBlock =
"[ordinals]\n"
"bitmap=Ordinal::BitMap::1\n"
"bitmap=Ordinal::BitMap::2\n"
"bitmap=Ordinal::BitMap::3\n"
"bitmap=Ordinal::BitMap::4\n"
"[Ordinal::BitMap::1]\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=000038000003C000001FF00000000F00\n"
"bitmap=0000F8000003C000003FF80000000F00\n"
"bitmap=0001F8000003C00000707C0000000F00\n"
"bitmap=0001F8000003C00000603C0000000F00\n"
"bitmap=00007801FC0FF00000003C3DF807FF00\n"
"bitmap=00007803FE0FF00000003C3FFC0FFF00\n"
"bitmap=00007803C703C00000003C3E3C1F0F00\n"
"bitmap=000078078303C0000000783C1E1E0F00\n"
"bitmap=000078078003C0000000783C1E1E0F00\n"
"bitmap=00007807C003C0000000F03C1E1E0F00\n"
"bitmap=00007807F003C0000001E03C1E1E0F00\n"
"bitmap=00007803FC03C0000003C03C1E1E0F00\n"
"bitmap=00007801FE03C0000007803C1E1E0F00\n"
"bitmap=000078007F03C000000F003C1E1E0F00\n"
"bitmap=000078001F03C000001E003C1E1E0F00\n"
"bitmap=000078000F03C000003C003C1E1E0F00\n"
"bitmap=000078060F03C0000078003C1E1E0F00\n"
"bitmap=000078071E03E0000078003C1E1F1F00\n"
"bitmap=00007803FE01F000007FFC3C1E0FFF00\n"
"bitmap=00007801FC00F000007FFC3C1E07EF00\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"x=128\n"
"y=32\n"
"width=8\n"
"[Ordinal::BitMap::2]\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=001FFF0000003C0000003C03C0780000\n"
"bitmap=001FFF0000003C0000007C03C0780000\n"
"bitmap=00000E0000003C000000FC03C0780000\n"
"bitmap=00003C0000003C000001BC03C0780000\n"
"bitmap=0000700F3E1FFC000003BC0FF07BF000\n"
"bitmap=0001E00F7E3FFC0000073C0FF07FF800\n"
"bitmap=0003800FFE7C3C0000063C03C07C7800\n"
"bitmap=0007F80FFE783C00000C3C03C0783C00\n"
"bitmap=0007FE0F80783C0000183C03C0783C00\n"
"bitmap=00001E0F00783C0000383C03C0783C00\n"
"bitmap=00000F0F00783C0000703C03C0783C00\n"
"bitmap=00000F0F00783C00007FFF03C0783C00\n"
"bitmap=00000F0F00783C00007FFF03C0783C00\n"
"bitmap=00000F0F00783C0000003C03C0783C00\n"
"bitmap=00000F0F00783C0000003C03C0783C00\n"
"bitmap=00000F0F00783C0000003C03C0783C00\n"
"bitmap=00180F0F00783C0000003C03C0783C00\n"
"bitmap=001C1F0F007C7C0000003C03E0783C00\n"
"bitmap=000FFE0F003FFC0000003C01F0783C00\n"
"bitmap=0007FC0F001FBC0000003C00F0783C00\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"x=128\n"
"y=32\n"
"width=8\n"
"[Ordinal::BitMap::3]\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=001FFF03C07800000000FC03C0780000\n"
"bitmap=001FFF03C07800000003FC03C0780000\n"
"bitmap=001E0003C07800000007C003C0780000\n"
"bitmap=001E0003C0780000000F0003C0780000\n"
"bitmap=001E000FF07BF000000F000FF07BF000\n"
"bitmap=001E000FF07FF800001E000FF07FF800\n"
"bitmap=001E0003C07C7800001E0003C07C7800\n"
"bitmap=001FFC03C0783C00001EFC03C0783C00\n"
"bitmap=001FFE03C0783C00001FFE03C0783C00\n"
"bitmap=00001F03C0783C00001F1F03C0783C00\n"
"bitmap=00000F03C0783C00001E0F03C0783C00\n"
"bitmap=00000F03C0783C00001E0F03C0783C00\n"
"bitmap=00000F03C0783C00001E0F03C0783C00\n"
"bitmap=00000F03C0783C00001E0F03C0783C00\n"
"bitmap=00000F03C0783C00001E0F03C0783C00\n"
"bitmap=00000F03C0783C00001E0F03C0783C00\n"
"bitmap=00180F03C0783C00001E0F03C0783C00\n"
"bitmap=001C1F03E0783C00001F1F03E0783C00\n"
"bitmap=000FFE01F0783C00000FFE01F0783C00\n"
"bitmap=0007FC00F0783C000007FC00F0783C00\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"x=128\n"
"y=32\n"
"width=8\n"
"[Ordinal::BitMap::4]\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=001FFF03C07800000007FC03C0780000\n"
"bitmap=001FFF03C0780000000FFE03C0780000\n"
"bitmap=00000F03C0780000001F1F03C0780000\n"
"bitmap=00000F03C0780000001E0F03C0780000\n"
"bitmap=00000F0FF07BF000001E0F0FF07BF000\n"
"bitmap=00001F0FF07FF800001E0F0FF07FF800\n"
"bitmap=00001E03C07C7800001E0F03C07C7800\n"
"bitmap=00003E03C0783C00001E0F03C0783C00\n"
"bitmap=00003C03C0783C00000F1E03C0783C00\n"
"bitmap=00003C03C0783C000007FC03C0783C00\n"
"bitmap=00007803C0783C000007FC03C0783C00\n"
"bitmap=00007803C0783C00000F1E03C0783C00\n"
"bitmap=00007803C0783C00001E0F03C0783C00\n"
"bitmap=0000F003C0783C00001E0F03C0783C00\n"
"bitmap=0000F003C0783C00001E0F03C0783C00\n"
"bitmap=0000F003C0783C00001E0F03C0783C00\n"
"bitmap=0000F003C0783C00001E0F03C0783C00\n"
"bitmap=0000F003E0783C00000F1E03E0783C00\n"
"bitmap=0000F001F0783C00000FFE01F0783C00\n"
"bitmap=0000F000F0783C000007FC00F0783C00\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"bitmap=00000000000000000000000000000000\n"
"x=128\n"
"y=32\n"
"width=8\n";
//###########################################################################
// GDI-rendered pilot-name plasma bitmaps.
//
// The console rendered each pilot's name into 128x32 (large) and 64x16
// (small) 1bpp plasma pages. We render with GDI into a mono bitmap and
// hex-encode: `bitmap=` lines carry 16 BYTES each (32 hex chars) -- one
// line per row for the large page (16-byte rows), TWO 8-byte rows per
// line for the small page -- byte-identical framing to MP.EGG.
//###########################################################################
static int
RenderNameBits(const char *name, int width, int height,
int font_height, unsigned char *bits /* width/8*height bytes */)
{
int stride = width / 8; // mono GDI stride for 64/128 px
HDC screen = GetDC(NULL);
HDC dc = CreateCompatibleDC(screen);
ReleaseDC(NULL, screen);
if (dc == NULL)
{
return -1;
}
HBITMAP bitmap = CreateBitmap(width, height, 1, 1, NULL);
HBITMAP old_bitmap = (HBITMAP)SelectObject(dc, bitmap);
HFONT font = CreateFontW(-font_height, 0, 0, 0, FW_BOLD, 0, 0, 0,
DEFAULT_CHARSET, 0, 0, NONANTIALIASED_QUALITY, 0, L"Arial");
HFONT old_font = (HFONT)SelectObject(dc, font);
RECT r = { 0, 0, width, height };
SetBkColor(dc, RGB(0, 0, 0));
SetTextColor(dc, RGB(255, 255, 255));
HBRUSH black = (HBRUSH)GetStockObject(BLACK_BRUSH);
FillRect(dc, &r, black);
WCHAR wide[64];
int n = 0;
for (const char *s = name; *s && n < 63; ++s)
{
wide[n++] = (WCHAR)*s;
}
wide[n] = 0;
DrawTextW(dc, wide, -1, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
GdiFlush();
int got = GetBitmapBits(bitmap, stride * height, bits);
SelectObject(dc, old_font);
DeleteObject(font);
SelectObject(dc, old_bitmap);
DeleteObject(bitmap);
DeleteDC(dc);
return (got == stride * height) ? 0 : -1;
}
static void
WriteBitmapPage(FILE *f, const char *page_name, const char *pilot_name,
int width, int height, int font_height)
{
unsigned char bits[16 * 32]; // worst case 128x32
memset(bits, 0, sizeof(bits));
RenderNameBits(pilot_name, width, height, font_height, bits);
fprintf(f, "[%s]\n", page_name);
int total = (width / 8) * height;
for (int off = 0; off < total; off += 16)
{
fprintf(f, "bitmap=");
for (int b = 0; b < 16; ++b)
{
fprintf(f, "%02X", bits[off + b]);
}
fprintf(f, "\n");
}
fprintf(f, "x=%d\ny=%d\nwidth=%d\n", width, height, width / 16);
}
//###########################################################################
// The egg writer
//###########################################################################
int
BTFeMission_WriteEgg(const BTFeMission *mission, const char *path)
{
FILE *f = fopen(path, "wt");
if (f == NULL)
{
return -1;
}
fprintf(f,
"[mission]\n"
"adventure=BattleTech\n"
"map=%s\n"
"scenario=freeforall\n"
"time=%s\n"
"weather=%s\n"
"temperature=%d\n"
"length=%d\n",
mission->map, mission->time, mission->weather,
mission->temperature, mission->lengthSeconds);
fputs(kOrdinalBlock, f);
fprintf(f, "[pilots]\n");
for (int p = 0; p < mission->pilotCount; ++p)
{
fprintf(f, "pilot=%s\n", mission->pilots[p].address);
}
for (int p = 0; p < mission->pilotCount; ++p)
{
const BTFePilot &pilot = mission->pilots[p];
fprintf(f,
"[%s]\n"
"hostType=0\n"
"advancedDamage=%d\n"
"loadzones=1\n"
"name=%s\n"
"bitmapindex=%d\n"
"experience=%s\n"
"badge=%s\n"
"patch=%s\n"
"role=Role::%s\n"
"dropzone=%s\n"
"vehicle=%s\n"
"vehicleValue=1000\n"
"color=%s\n",
pilot.address, pilot.advancedDamage ? 1 : 0, pilot.name, p + 1,
pilot.experience[0] ? pilot.experience : "veteran",
pilot.badge[0] ? pilot.badge : "VGL",
pilot.patch[0] ? pilot.patch : "Red",
mission->roleKey[0] ? mission->roleKey : "Default",
pilot.dropzone[0] ? pilot.dropzone : "one",
pilot.vehicle,
pilot.color[0] ? pilot.color : "Grey");
}
//
// Name plasma pages. The shipped eggs carry ONE large/small pair (the
// console personalized per pod); list every pilot's pair -- the engine
// resolves by name.
//
fprintf(f, "[largebitmap]\n");
for (int p = 0; p < mission->pilotCount; ++p)
{
fprintf(f, "bitmap=BitMap::Large::%s\n", mission->pilots[p].name);
}
for (int p = 0; p < mission->pilotCount; ++p)
{
char page[64];
sprintf(page, "BitMap::Large::%s", mission->pilots[p].name);
WriteBitmapPage(f, page, mission->pilots[p].name, 128, 32, 24);
}
fprintf(f, "[smallbitmap]\n");
for (int p = 0; p < mission->pilotCount; ++p)
{
fprintf(f, "bitmap=BitMap::Small::%s\n", mission->pilots[p].name);
}
for (int p = 0; p < mission->pilotCount; ++p)
{
char page[64];
sprintf(page, "BitMap::Small::%s", mission->pilots[p].name);
WriteBitmapPage(f, page, mission->pilots[p].name, 64, 16, 12);
}
fprintf(f,
"[Role::Default]\n"
"model=dfltrole\n"
"[Role::NoReturn]\n"
"model=noretun\n");
fclose(f);
return 0;
}
//###########################################################################
// The menu -- the RP412 front-end pattern (RP_L4/RPL4FE.cpp, per Cyd):
// full catalogs as always-visible clickable LISTS in three columns with
// friendly display names -- bright green = selected, dim green = the rest --
// a real EDIT control for the pilot name (and one for LAN peers), framed
// LAUNCH / HOST STEAM / JOIN STEAM buttons, double-buffered paint, and the
// player's loadout persisted across runs (fe_last.ini beside the eggs; our
// per-mission process relaunch would otherwise forget it).
// ENTER = launch, ESC = quit.
//###########################################################################
struct CatalogEntry
{
const char *key;
const char *name;
};
// ALL option catalogs are the AUTHENTIC BattleTech mission options recovered
// from the Mac 4.10 operator console's BT:: adventure tree
// (C:\VWE\TeslaSuite\410console\4_10-console-extracted\Console.ini); the `key`
// is the egg field value (the console's `tag=`), the `name` is the display
// label. Full catalog + provenance: docs/GLASS_COCKPIT.md 2026-07-19.
static const CatalogEntry kMaps[] =
{
{ "cavern", "Cavern" },
{ "grass", "Savannah" },
{ "rav", "Ravines" },
{ "polar3", "Polar Missile Base" },
{ "polar4", "Helicopter Base" },
{ "arena1", "Abandoned Arena" },
{ "arena2", "Destroyed Arena" },
{ "dbase", "Badlands" },
};
// SCENARIO / role: the tag is always freeforall; the choice is the RESPAWN
// role -- Default (respawn) vs NoReturn (one life). Stored as key=role model.
struct RoleEntry { const char *roleKey; const char *model; const char *name; };
static const RoleEntry kScenarios[] =
{
{ "Default", "dfltrole", "Free For All" },
{ "NoReturn", "noretun", "No Return (1 life)" }, // model = the game's 8.3 resource
};
// The 18 mechs (console friendly name -> egg tag), matching the 2026-07-18
// ModelList certification.
static const CatalogEntry kVehicles[] =
{
{ "blkhawk", "Black Hawk" },
{ "loki", "Loki" },
{ "bhk1", "Nova" },
{ "madcat", "Mad Cat" },
{ "thor", "Thor" },
{ "owens", "Owens" },
{ "own1", "Kotori" },
{ "thr1", "Summoner" },
{ "lok1", "Hellbringer" },
{ "mad1", "Timber Wolf" },
{ "avatar", "Avatar" },
{ "ava1", "Satyr" },
{ "sunder", "Sunder" },
{ "snd1", "Sunder V1" },
{ "vulture", "Vulture" },
{ "vul1", "Mad Dog" },
{ "lok2", "Mischief" },
{ "mad2", "Zanin Neko" },
};
// experience = the pod's per-pilot simulation-fidelity tier (see
// context/experience-levels.md); 4 game levels.
static const CatalogEntry kExperience[] =
{
{ "novice", "Novice" },
{ "standard", "Standard" },
{ "veteran", "Veteran" },
{ "expert", "Expert" },
};
// color/camo (console display -> egg tag: Red=Crimson, Gray=Grey)
static const CatalogEntry kColors[] =
{
{ "White", "White" },
{ "Black", "Black" },
{ "Brown", "Brown" },
{ "Crimson","Crimson" },
{ "Green", "Green" },
{ "Grey", "Grey" },
{ "Tan", "Tan" },
};
// badge / team emblem (the egg `badge=` field)
static const CatalogEntry kBadges[] =
{
{ "VGL", "VGL" },
{ "Davion", "Davion" },
{ "Kurita", "Kurita" },
{ "Liao", "Liao" },
{ "Marik", "Marik" },
{ "Steiner", "Steiner" },
{ "None", "None" },
};
// patch / badge colour (the egg `patch=` field)
static const CatalogEntry kPatches[] =
{
{ "Red", "Red" },
{ "Yellow", "Yellow" },
{ "Blue", "Blue" },
{ "Green", "Green" },
{ "Violet", "Violet" },
{ "Grey", "Grey" },
{ "White", "White" },
{ "Black", "Black" },
};
// drop zone (the egg `dropzone=` field)
static const CatalogEntry kDropZones[] =
{
{ "one", "One" },
{ "two", "Two" },
{ "three", "Three" },
{ "four", "Four" },
{ "five", "Five" },
};
// advanced damage model (the egg `advancedDamage=` field)
static const CatalogEntry kAdvDamage[] =
{
{ "1", "On" },
{ "0", "Off" },
};
static const CatalogEntry kTimes[] =
{
{ "morning","Morning" },
{ "day", "Day" },
{ "evening","Evening" },
{ "night", "Night" },
};
static const CatalogEntry kWeathers[] =
{
{ "clear", "Clear" }, { "fog", "Light Fog" }, { "soup", "Heavy Fog" },
};
struct LengthEntry
{
int seconds;
const char *name;
};
static const LengthEntry kLengths[] =
{
{ 60, "1:00" }, { 300, "5:00" }, { 600, "10:00" }, { 1200, "20:00" },
};
static const CatalogEntry kModes[] =
{
{ "solo", "SOLO MISSION" },
{ "raw", "RAW SOLO (endless)" },
{ "hostlan", "HOST LAN" },
{ "joinlan", "JOIN LAN" },
};
static const int kPorts[] = { 1501, 1601, 1701, 1801 };
#define COUNT(a) ((int)(sizeof(a)/sizeof((a)[0])))
enum MenuGroup
{
GroupMap = 0,
GroupScenario,
GroupTime,
GroupWeather,
GroupLength,
GroupVehicle,
GroupExperience,
GroupColor,
GroupBadge,
GroupPatch,
GroupDropZone,
GroupAdvDamage,
GroupMode,
GroupPort,
GroupLastSelectable = GroupPort, // the last catalog group (persist + headers)
GroupLaunch,
GroupSteamHost,
GroupSteamJoin,
GroupCount
};
struct MenuItem
{
int group;
int index;
RECT rect;
};
struct MenuState
{
int selection[GroupCount];
MenuItem items[128];
int itemCount;
HWND window;
HWND nameEdit;
HWND peersEdit;
HFONT textFont;
HFONT titleFont;
HBRUSH editBrush;
int result; // 0 pending, 1 launch, -1 quit
int steamAction; // 0 none, 1 host lobby, 2 join lobby
};
static MenuState menu;
static const COLORREF kGreenBright = RGB(64, 255, 64);
static const COLORREF kGreenDim = RGB(24, 140, 24);
//
// BT_FE_SOLO=1 (play_solo.bat): a SOLO-ONLY menu. The mode cycle keeps just
// the two solo entries (kModes lists them FIRST: solo, raw), and the network
// rows/buttons (NET PORT, HOST/JOIN STEAM) are not laid out -- LAN play goes
// through the join bats + operator, Steam through play_steam.bat. A stale
// fe_last.ini mode of hostlan/joinlan is rejected by the GroupSize clamp in
// LoadPersisted, so the selection can never land on a hidden mode.
//
static int
FeSoloOnly()
{
static int s = -1;
if (s < 0)
{
const char *e = getenv("BT_FE_SOLO");
s = (e != NULL && *e != '0') ? 1 : 0;
}
return s;
}
//
// BT_FE_JOIN=1 (join.bat / join_lan.bat): the JOIN-GAME menu -- the walk-up
// desk conversation of the 1995 centers, over the internet. Only CALLSIGN +
// MECH are the player's to pick (the operator owns the mission + the rest of
// the loadout); the choice rides the relay SEAT_REQUEST and the relay writes
// it into the session egg (btconsole.py).
//
static int
FeJoinOnly()
{
static int s = -1;
if (s < 0)
{
const char *e = getenv("BT_FE_JOIN");
s = (e != NULL && *e != '0') ? 1 : 0;
}
return s;
}
static int
GroupSize(int group)
{
switch (group)
{
case GroupMap: return COUNT(kMaps);
case GroupScenario: return COUNT(kScenarios);
case GroupTime: return COUNT(kTimes);
case GroupWeather: return COUNT(kWeathers);
case GroupLength: return COUNT(kLengths);
case GroupVehicle: return COUNT(kVehicles);
case GroupExperience: return COUNT(kExperience);
case GroupColor: return COUNT(kColors);
case GroupBadge: return COUNT(kBadges);
case GroupPatch: return COUNT(kPatches);
case GroupDropZone: return COUNT(kDropZones);
case GroupAdvDamage: return COUNT(kAdvDamage);
case GroupMode: return FeSoloOnly() ? 2 : COUNT(kModes); // solo, raw
case GroupPort: return COUNT(kPorts);
default: return 1;
}
}
static void
GroupRowText(int group, int index, char *out /*64*/)
{
switch (group)
{
case GroupMap: strcpy(out, kMaps[index].name); break;
case GroupScenario: strcpy(out, kScenarios[index].name); break;
case GroupTime: strcpy(out, kTimes[index].name); break;
case GroupWeather: strcpy(out, kWeathers[index].name); break;
case GroupLength: strcpy(out, kLengths[index].name); break;
case GroupVehicle: strcpy(out, kVehicles[index].name); break;
case GroupExperience: strcpy(out, kExperience[index].name); break;
case GroupColor: strcpy(out, kColors[index].name); break;
case GroupBadge: strcpy(out, kBadges[index].name); break;
case GroupPatch: strcpy(out, kPatches[index].name); break;
case GroupDropZone: strcpy(out, kDropZones[index].name); break;
case GroupAdvDamage: strcpy(out, kAdvDamage[index].name); break;
case GroupMode: strcpy(out, kModes[index].name); break;
case GroupPort: sprintf(out, "%d", kPorts[index]); break;
default: out[0] = 0; break;
}
}
static const char *
GroupHeader(int group)
{
switch (group)
{
case GroupMap: return "MAP";
case GroupScenario: return "SCENARIO";
case GroupTime: return "TIME";
case GroupWeather: return "WEATHER";
case GroupLength: return "LENGTH";
case GroupVehicle: return "MECH";
case GroupExperience: return "EXPERIENCE";
case GroupColor: return "COLOR";
case GroupBadge: return "BADGE";
case GroupPatch: return "PATCH";
case GroupDropZone: return "DROP ZONE";
case GroupAdvDamage: return "ADV. DAMAGE";
case GroupMode: return "MODE";
case GroupPort: return "NET PORT";
default: return "";
}
}
//
// Layout: five catalog columns + the button column.
// col1 MISSION: MAP, SCENARIO, TIME, WEATHER, LENGTH
// col2 MECH (18) col3 EXPERIENCE, COLOR, DROP ZONE, ADV. DAMAGE
// col4 BADGE, PATCH col5 MODE, NET PORT + name/peers edits + buttons
//
enum
{
MenuClientW = 1180,
MenuClientH = 672, // col1 stack (MAP 8 + SCENARIO 2 + TIME 4 + WEATHER 3 +
// LENGTH 4 = 630px) + the 30px footer band; 620 overlapped
// LENGTH's last rows with the footer (user-reported)
MenuRowH = 20,
MenuColW = 180,
MenuCol1X = 24,
MenuCol2X = 224,
MenuCol3X = 424,
MenuCol4X = 624,
MenuCol5X = 824,
MenuTopY = 60,
MenuNameLabelY = 316, // col5: PILOT NAME label + edit
MenuNameEditY = 336,
MenuPeersLabelY = 368, // col5: LAN PEERS label + edit
MenuPeersEditY = 388
};
static void
AddGroup(int group, int x, int *y)
{
*y += MenuRowH; // the header row
int count = GroupSize(group);
for (int i = 0; i < count; ++i)
{
MenuItem *item = &menu.items[menu.itemCount++];
item->group = group;
item->index = i;
item->rect.left = x;
item->rect.top = *y;
item->rect.right = x + MenuColW;
item->rect.bottom = *y + MenuRowH;
*y += MenuRowH;
}
*y += MenuRowH / 2; // gap below the group
}
static void
AddButton(int group, int x, int y, int width, int height)
{
MenuItem *item = &menu.items[menu.itemCount++];
item->group = group;
item->index = 0;
item->rect.left = x;
item->rect.top = y;
item->rect.right = x + width;
item->rect.bottom = y + height;
}
static void
LayoutMenu()
{
menu.itemCount = 0;
int y;
if (FeJoinOnly())
{
// JOIN trim: the mech list + the JOIN button; callsign is the edit
// control (repositioned in BTFrontEnd_Run). Everything else is the
// operator's call -- EXCEPT experience, which is PER-PLAYER by the
// original design (the sysop set each user's tier; mixed-experience
// matches were legal), and each node's master reads its OWN egg's
// value, so mixing already works end to end.
//
// #116: this group was missing here, so joiners silently launched with
// the hidden menu default (veteran) no matter what they believed they
// selected -- "standard mode still having heat and leaks only occurred
// in steam": hosts got their pick, all three affected players were
// JOINERS, and the one clean player (Sauron) was the one HOSTING.
y = MenuTopY; AddGroup(GroupVehicle, MenuCol2X, &y);
y = MenuTopY; AddGroup(GroupExperience, MenuCol3X, &y);
AddButton(GroupLaunch, MenuCol5X, MenuClientH - 190, 220, 52);
return;
}
// col1: the mission
y = MenuTopY; AddGroup(GroupMap, MenuCol1X, &y);
AddGroup(GroupScenario, MenuCol1X, &y);
AddGroup(GroupTime, MenuCol1X, &y);
AddGroup(GroupWeather, MenuCol1X, &y);
AddGroup(GroupLength, MenuCol1X, &y);
// col2: the mech (tall)
y = MenuTopY; AddGroup(GroupVehicle, MenuCol2X, &y);
// col3: the pilot loadout
y = MenuTopY; AddGroup(GroupExperience, MenuCol3X, &y);
AddGroup(GroupColor, MenuCol3X, &y);
AddGroup(GroupDropZone, MenuCol3X, &y);
AddGroup(GroupAdvDamage, MenuCol3X, &y);
// col4: emblem + patch
y = MenuTopY; AddGroup(GroupBadge, MenuCol4X, &y);
AddGroup(GroupPatch, MenuCol4X, &y);
// col5: the session + controls (solo-only: no net port, no steam buttons)
y = MenuTopY; AddGroup(GroupMode, MenuCol5X, &y);
if (!FeSoloOnly())
{
AddGroup(GroupPort, MenuCol5X, &y);
}
AddButton(GroupLaunch, MenuCol5X, MenuClientH - 190, 220, 52);
#ifdef BT_STEAM
if (!FeSoloOnly())
{
AddButton(GroupSteamHost, MenuCol5X, MenuClientH - 120, 220, 34);
AddButton(GroupSteamJoin, MenuCol5X, MenuClientH - 76, 220, 34);
}
#endif
}
//
// fe_last.ini -- the persisted loadout (our process-relaunch model would
// otherwise forget it between missions; RP412 persists in-process).
//
static void
LoadPersisted(char *pilot_name, int name_capacity,
char *peers, int peers_capacity)
{
FILE *f = fopen("fe_last.ini", "rt");
if (f == NULL)
{
return;
}
char line[160];
while (fgets(line, sizeof(line), f))
{
char *newline = strchr(line, '\n');
if (newline) *newline = 0;
char *equals = strchr(line, '=');
if (equals == NULL) continue;
*equals = 0;
const char *value = equals + 1;
for (int g = 0; g < GroupCount; ++g)
{
if (_stricmp(line, GroupHeader(g)) == 0)
{
int v = atoi(value);
if (v >= 0 && v < GroupSize(g)) menu.selection[g] = v;
}
}
if (_stricmp(line, "NAME") == 0 && value[0])
{
strncpy(pilot_name, value, name_capacity - 1);
pilot_name[name_capacity - 1] = 0;
}
if (_stricmp(line, "PEERS") == 0)
{
strncpy(peers, value, peers_capacity - 1);
peers[peers_capacity - 1] = 0;
}
}
fclose(f);
}
static void
SavePersisted(const char *pilot_name, const char *peers)
{
FILE *f = fopen("fe_last.ini", "wt");
if (f == NULL)
{
return;
}
for (int g = 0; g <= GroupPort; ++g)
{
fprintf(f, "%s=%d\n", GroupHeader(g), menu.selection[g]);
}
fprintf(f, "NAME=%s\nPEERS=%s\n", pilot_name, peers);
fclose(f);
}
static void
DrawMenuText(HDC dc, const char *text, RECT *rect, COLORREF color,
UINT format)
{
WCHAR wide[128];
int n = 0;
for (const char *s = text; *s && n < 127; ++s) wide[n++] = (WCHAR)*s;
wide[n] = 0;
SetTextColor(dc, color);
DrawTextW(dc, wide, -1, rect, format);
}
static void
PaintMenu(HWND window)
{
PAINTSTRUCT paint;
HDC screen = BeginPaint(window, &paint);
RECT client;
GetClientRect(window, &client);
// double-buffered (the RP412 pattern)
HDC dc = CreateCompatibleDC(screen);
HBITMAP backing = CreateCompatibleBitmap(screen, client.right, client.bottom);
HBITMAP old_backing = (HBITMAP)SelectObject(dc, backing);
HBRUSH background = CreateSolidBrush(RGB(0, 0, 0));
FillRect(dc, &client, background);
DeleteObject(background);
SetBkMode(dc, TRANSPARENT);
HFONT old_font = (HFONT)SelectObject(dc, menu.titleFont);
RECT title_rect = { MenuCol1X, 14, client.right - 24, 54 };
DrawMenuText(dc,
FeJoinOnly() ? "BATTLETECH -- JOIN GAME"
: "BATTLETECH -- MISSION CONSOLE",
&title_rect, kGreenBright, DT_LEFT | DT_TOP | DT_SINGLELINE);
SelectObject(dc, menu.textFont);
//
// Group headers (above each group's first row).
//
for (int i = 0; i < menu.itemCount; ++i)
{
const MenuItem &item = menu.items[i];
if (item.index != 0 || item.group > GroupPort)
{
continue;
}
RECT header = item.rect;
header.top -= MenuRowH;
header.bottom = item.rect.top;
DrawMenuText(dc, GroupHeader(item.group), &header, kGreenDim,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
}
//
// Catalog rows: bright = selected, dim = not.
//
for (int i = 0; i < menu.itemCount; ++i)
{
const MenuItem &item = menu.items[i];
if (item.group > GroupPort)
{
continue;
}
char text[64];
GroupRowText(item.group, item.index, text);
int selected = (menu.selection[item.group] == item.index);
RECT row = item.rect;
row.left += 14;
if (selected)
{
RECT marker = item.rect;
marker.right = marker.left + 14;
DrawMenuText(dc, ">", &marker, kGreenBright,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
}
DrawMenuText(dc, text, &row,
selected ? kGreenBright : kGreenDim,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
}
//
// Edit labels (the controls themselves are child windows in col5).
//
RECT label = { MenuCol5X, MenuNameLabelY, MenuCol5X + 240, MenuNameLabelY + MenuRowH };
DrawMenuText(dc, FeJoinOnly() ? "CALLSIGN" : "PILOT NAME", &label,
kGreenDim, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
if (!FeJoinOnly())
{
label.top = MenuPeersLabelY;
label.bottom = label.top + MenuRowH;
DrawMenuText(dc, "LAN PEERS (host: ip:port,...)", &label, kGreenDim,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
}
//
// Buttons (framed, RP412 style).
//
HBRUSH frame = CreateSolidBrush(kGreenBright);
for (int i = 0; i < menu.itemCount; ++i)
{
const MenuItem &item = menu.items[i];
const char *text =
(item.group == GroupLaunch) ? (FeJoinOnly() ? "J O I N" : "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,
FeJoinOnly() ? "click to select ENTER = join ESC = quit"
: "click to select ENTER = launch ESC = quit",
&help, kGreenDim, DT_LEFT | DT_TOP | DT_SINGLELINE);
SelectObject(dc, old_font);
BitBlt(screen, 0, 0, client.right, client.bottom, dc, 0, 0, SRCCOPY);
SelectObject(dc, old_backing);
DeleteObject(backing);
DeleteDC(dc);
EndPaint(window, &paint);
}
static LRESULT CALLBACK
MenuWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam)
{
switch (message)
{
case WM_PAINT:
PaintMenu(window);
return 0;
case WM_CTLCOLOREDIT:
//
// The edits render green-on-black like everything else.
//
SetTextColor((HDC)wparam, kGreenBright);
SetBkColor((HDC)wparam, RGB(6, 20, 6));
return (LRESULT)menu.editBrush;
case WM_LBUTTONDOWN:
{
int x = (int)(short)LOWORD(lparam);
int y = (int)(short)HIWORD(lparam);
for (int i = 0; i < menu.itemCount; ++i)
{
const MenuItem &item = menu.items[i];
if (x < item.rect.left || x >= item.rect.right ||
y < item.rect.top || y >= item.rect.bottom)
{
continue;
}
if (item.group == GroupLaunch)
{
menu.result = 1;
}
else if (item.group == GroupSteamHost)
{
menu.steamAction = 1;
menu.result = 1;
}
else if (item.group == GroupSteamJoin)
{
menu.steamAction = 2;
menu.result = 1;
}
else
{
menu.selection[item.group] = item.index;
InvalidateRect(window, NULL, FALSE);
}
break;
}
SetFocus(window); // clicks outside the edits take focus back
}
return 0;
case WM_KEYDOWN:
if (wparam == VK_RETURN)
{
menu.result = 1;
}
else if (wparam == VK_ESCAPE)
{
menu.result = -1;
}
return 0;
case WM_CLOSE:
menu.result = -1;
return 0;
}
return DefWindowProcW(window, message, wparam, lparam);
}
int
BTFrontEnd_Run(BTFeLaunchSpec *spec)
{
memset(spec, 0, sizeof(*spec));
memset(&menu, 0, sizeof(menu));
menu.selection[GroupLength] = 2; // 10:00, the console default
menu.selection[GroupExperience] = 2; // veteran
menu.selection[GroupColor] = 5; // Grey (the console default)
menu.selection[GroupBadge] = 0; // VGL
menu.selection[GroupDropZone] = 0; // one
char pilot_name[32] = "Dev";
char peers[128] = "";
LoadPersisted(pilot_name, sizeof(pilot_name), peers, sizeof(peers));
LayoutMenu();
WNDCLASSW window_class;
memset(&window_class, 0, sizeof(window_class));
window_class.lpfnWndProc = MenuWndProc;
window_class.hInstance = GetModuleHandleW(NULL);
window_class.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW);
window_class.lpszClassName = L"BTFrontEndWnd";
RegisterClassW(&window_class);
RECT frame_rect = { 0, 0, MenuClientW, MenuClientH };
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
AdjustWindowRect(&frame_rect, style, FALSE);
menu.window = CreateWindowW(
L"BTFrontEndWnd",
FeJoinOnly() ? L"BattleTech - Join Game" : L"BattleTech - Mission Console",
style, 100, 80,
frame_rect.right - frame_rect.left, frame_rect.bottom - frame_rect.top,
NULL, NULL, GetModuleHandleW(NULL), NULL);
if (menu.window == NULL)
{
return -1;
}
menu.textFont = CreateFontW(-16, 0, 0, 0, FW_NORMAL, 0, 0, 0,
DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, FIXED_PITCH, L"Consolas");
menu.titleFont = CreateFontW(-24, 0, 0, 0, FW_BOLD, 0, 0, 0,
DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, FIXED_PITCH, L"Consolas");
menu.editBrush = CreateSolidBrush(RGB(6, 20, 6));
//
// The two edit controls (name; LAN peers for host mode).
//
menu.nameEdit = CreateWindowW(L"EDIT", L"",
WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
MenuCol5X, MenuNameEditY, 240, MenuRowH + 4,
menu.window, NULL, GetModuleHandleW(NULL), NULL);
menu.peersEdit = CreateWindowW(L"EDIT", L"",
WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
MenuCol5X, MenuPeersEditY, 240, MenuRowH + 4,
menu.window, NULL, GetModuleHandleW(NULL), NULL);
SendMessageW(menu.nameEdit, WM_SETFONT, (WPARAM)menu.textFont, TRUE);
SendMessageW(menu.peersEdit, WM_SETFONT, (WPARAM)menu.textFont, TRUE);
SetWindowTextA(menu.nameEdit, pilot_name);
SetWindowTextA(menu.peersEdit, peers);
if (FeJoinOnly())
{
ShowWindow(menu.peersEdit, SW_HIDE); // operator's concern, not the joiner's
}
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.
//
if (FeJoinOnly())
{
// JOIN GAME: no egg, no port math -- the relay owns the session. The
// callsign + mech request rides the SEAT_REQUEST payload (L4NET reads
// BT_CALLSIGN / BT_MECH, set by WinMain from this spec).
spec->mode = BTFeLaunchJoinRelay;
strncpy(spec->joinCallsign, pilot_name[0] ? pilot_name : "Pilot",
sizeof(spec->joinCallsign) - 1);
strncpy(spec->joinVehicle, kVehicles[menu.selection[GroupVehicle]].key,
sizeof(spec->joinVehicle) - 1);
return 0;
}
int console_port = kPorts[menu.selection[GroupPort]];
const RoleEntry &scenario = kScenarios[menu.selection[GroupScenario]];
BTFeMission mission;
memset(&mission, 0, sizeof(mission));
strcpy(mission.map, kMaps[menu.selection[GroupMap]].key);
strcpy(mission.time, kTimes[menu.selection[GroupTime]].key);
strcpy(mission.weather, kWeathers[menu.selection[GroupWeather]].key);
strcpy(mission.roleKey, scenario.roleKey);
strcpy(mission.roleModel, scenario.model);
mission.temperature = 27;
mission.lengthSeconds = kLengths[menu.selection[GroupLength]].seconds;
mission.pilotCount = 1;
BTFePilot &self = mission.pilots[0];
strcpy(self.name, pilot_name[0] ? pilot_name : "Dev");
strcpy(self.vehicle, kVehicles[menu.selection[GroupVehicle]].key);
strcpy(self.color, kColors[menu.selection[GroupColor]].key);
strcpy(self.experience, kExperience[menu.selection[GroupExperience]].key);
printf("[fe] pilot experience=%s (%s)\n", self.experience,
FeJoinOnly() ? "join" : "host/solo");
strcpy(self.badge, kBadges[menu.selection[GroupBadge]].key);
strcpy(self.patch, kPatches[menu.selection[GroupPatch]].key);
strcpy(self.dropzone, kDropZones[menu.selection[GroupDropZone]].key);
self.advancedDamage = (kAdvDamage[menu.selection[GroupAdvDamage]].key[0] == '1');
spec->consolePort = console_port;
spec->missionSeconds = mission.lengthSeconds;
strcpy(spec->eggPath, "frontend.egg");
int mode = menu.selection[GroupMode];
#ifdef BT_STEAM
if (menu.steamAction == 1)
{
BTLobbyRoster roster;
if (BTLobby_HostAndRoom(self.name, self.vehicle, self.color,
&roster, spec->steamMyToken, sizeof(spec->steamMyToken),
spec->steamMap, sizeof(spec->steamMap)) != 0 ||
roster.memberCount == 0)
{
return 1; // cancelled / Steam unavailable
}
spec->mode = BTFeLaunchHostSteam;
mission.pilotCount = 0;
sprintf(spec->podList, "127.0.0.1:%d", console_port);
for (int m = 0; m < roster.memberCount && m < 8; ++m)
{
const BTLobbyMember &member = roster.members[m];
BTFePilot &pilot = mission.pilots[mission.pilotCount++];
strncpy(pilot.name, member.name, sizeof(pilot.name) - 1);
strncpy(pilot.vehicle,
member.vehicle[0] ? member.vehicle : kVehicles[0].key,
sizeof(pilot.vehicle) - 1);
strncpy(pilot.color,
member.color[0] ? member.color : kColors[0].key,
sizeof(pilot.color) - 1);
sprintf(pilot.address, "%s:%d",
member.fakeAddress, member.gamePort);
if (!member.isSelf)
{
char entry[64];
sprintf(entry, ",%s:%d",
member.fakeAddress, member.consolePort);
strcat(spec->podList, entry);
}
}
if (BTFeMission_WriteEgg(&mission, spec->eggPath) != 0)
{
return -1;
}
return 0;
}
if (menu.steamAction == 2)
{
if (BTLobby_JoinAndWait(self.name, self.vehicle, self.color,
spec->steamMyToken, sizeof(spec->steamMyToken),
spec->steamMap, sizeof(spec->steamMap)) != 0)
{
return 1;
}
spec->mode = BTFeLaunchJoinSteam;
spec->eggPath[0] = 0;
return 0;
}
#endif
switch (mode)
{
case 0: // SOLO MISSION: networked 1-pod, marshal feeds self
spec->mode = BTFeLaunchSolo;
sprintf(self.address, "127.0.0.1:%d", console_port + 1);
sprintf(spec->podList, "127.0.0.1:%d", console_port);
break;
case 1: // RAW SOLO: the plain -egg endless path
spec->mode = BTFeLaunchRawSolo;
strcpy(self.address, "127.0.0.1");
break;
case 2: // HOST LAN: marshal feeds self + the peers list
{
spec->mode = BTFeLaunchHostLan;
sprintf(self.address, "127.0.0.1:%d", console_port + 1);
sprintf(spec->podList, "127.0.0.1:%d", console_port);
char peers_copy[128];
strcpy(peers_copy, peers);
char *cursor = strtok(peers_copy, ", ");
while (cursor != NULL && mission.pilotCount < 8)
{
char host[48];
int port = 0;
const char *colon = strrchr(cursor, ':');
if (colon != NULL && (port = atoi(colon + 1)) > 0 &&
(int)(colon - cursor) < (int)sizeof(host))
{
memcpy(host, cursor, colon - cursor);
host[colon - cursor] = 0;
BTFePilot &peer = mission.pilots[mission.pilotCount];
sprintf(peer.name, "Pilot%d", mission.pilotCount + 1);
strcpy(peer.vehicle, kVehicles[
(menu.selection[GroupVehicle] + mission.pilotCount) % COUNT(kVehicles)].key);
strcpy(peer.color, kColors[
(menu.selection[GroupColor] + mission.pilotCount) % COUNT(kColors)].key);
// peers inherit the host's loadout defaults (the operator
// sets per-pilot detail on the real console)
strcpy(peer.experience, self.experience);
strcpy(peer.badge, self.badge);
strcpy(peer.patch, self.patch);
strcpy(peer.dropzone, self.dropzone);
peer.advancedDamage = self.advancedDamage;
sprintf(peer.address, "%s:%d", host, port + 1);
++mission.pilotCount;
char entry[64];
sprintf(entry, ",%s:%d", host, port);
strcat(spec->podList, entry);
}
cursor = strtok(NULL, ", ");
}
}
break;
default: // JOIN LAN: plain -net pod; the host's marshal feeds us
spec->mode = BTFeLaunchJoinLan;
spec->eggPath[0] = 0;
return 0;
}
if (BTFeMission_WriteEgg(&mission, spec->eggPath) != 0)
{
return -1;
}
return 0;
}