Miniconsole: the front end + LocalConsole marshal -- self-launched missions (step 3)
NEW gated dir game/glass/: btl4fe (green-on-black GDI catalog menu -- map/ time/weather/length/mech/color/pilot/mode/port/peers; egg writer emitting the FULL console egg shape, golden-tested vs MP.EGG: identical section sequence at identical line offsets, the 4 static ordinal pages verbatim, GDI-rendered 128x32/64x16 pilot-name plasma bitmaps in MP.EGG framing) and btl4console (the marshal: a worker-thread console CLIENT speaking the btconsole.py wire protocol verbatim -- 1040-byte chunked egg, 20s settle, RunMission x2, STAY CONNECTED, own the clock, StopMission at expiry; logs marshal.log). The engine sees a real connected console and runs the 100% stock ladder -- NO engine hooks (less invasive than planned: the L4APP.H setters proved unnecessary; launches go through real argv on relaunch). WinMain (one gated block): zero-mission-arg glass launch -> menu -> relaunch self with the mission argv (per-mission process relaunch; BT_FE_* env arms the marshal); post-RunMissions BT_FE_LOOP tail returns to the menu. Menu relaunches CLEAR the BT_FE_* handoff (found live: the inherited env re-armed a marshal instead of showing the menu). Verified end-to-end (synthetic menu drive): menu -> frontend.egg -> relaunch -> marshal feeds self over loopback -> stock console ladder -> mission RUNS (58+ ticks) -> 60s clock -> StopMission -> clean menu return, no env leak. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -347,6 +347,13 @@ target_include_directories(btl4 BEFORE PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/game/original/BT_L4"
|
||||
"${CMAKE_SOURCE_DIR}/game/fwd"
|
||||
"${DXSDK}/Include")
|
||||
if(BT_GLASS)
|
||||
target_sources(btl4 PRIVATE
|
||||
"game/glass/btl4fe.cpp"
|
||||
"game/glass/btl4console.cpp"
|
||||
)
|
||||
target_include_directories(btl4 BEFORE PRIVATE "${CMAKE_SOURCE_DIR}/game/glass")
|
||||
endif()
|
||||
target_compile_definitions(btl4 PRIVATE ${BT_DEFS})
|
||||
target_compile_options(btl4 PRIVATE ${BT_OPTS})
|
||||
target_link_libraries(btl4 PRIVATE
|
||||
|
||||
@@ -110,6 +110,11 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
||||
return DefWindowProc(hWnd, uMsg, wParam, lParam);
|
||||
}
|
||||
|
||||
#ifdef BT_GLASS
|
||||
#include "btl4fe.hpp"
|
||||
#include "btl4console.hpp"
|
||||
#endif
|
||||
|
||||
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
|
||||
{
|
||||
// Heap-corruption hunt (env BT_HEAPCHECK=1; default OFF -- ~100x slower): validate
|
||||
@@ -328,6 +333,67 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
: "DEV (single window + keyboard)")
|
||||
<< std::endl << std::flush;
|
||||
|
||||
#ifdef BT_GLASS
|
||||
//
|
||||
// MINICONSOLE FRONT END (glass step 3): a zero-mission-arg launch (no
|
||||
// -egg, no -net) runs the menu INSTEAD of the game, then RELAUNCHES
|
||||
// this exe with the chosen mission's real arguments -- the per-mission
|
||||
// process-relaunch pattern (reconstructed gBT* globals do not survive
|
||||
// in-process re-init). BT_FE_* env vars arm the mission process's
|
||||
// LocalConsole marshal; BT_FE_LOOP returns post-mission exits to the
|
||||
// menu. The console-wire ladder itself is 100% stock -- the engine
|
||||
// sees a real connected console (btl4console.cpp).
|
||||
//
|
||||
{
|
||||
int fe_has_egg = lpCmdLine && strstr(lpCmdLine, "-egg") != NULL;
|
||||
int fe_has_net = lpCmdLine && strstr(lpCmdLine, "-net") != NULL;
|
||||
if (!fe_has_egg && !fe_has_net && getenv("BT_FE_EGG") == NULL)
|
||||
{
|
||||
BTFeLaunchSpec fe_spec;
|
||||
if (BTFrontEnd_Run(&fe_spec) != 0 || fe_spec.mode == BTFeLaunchNone)
|
||||
{
|
||||
return 0; // quit from the menu
|
||||
}
|
||||
char fe_arguments[192];
|
||||
char fe_value[64];
|
||||
SetEnvironmentVariableA("BT_FE_LOOP", "1");
|
||||
SetEnvironmentVariableA("BT_PLATFORM", "glass");
|
||||
switch (fe_spec.mode)
|
||||
{
|
||||
case BTFeLaunchRawSolo:
|
||||
SetEnvironmentVariableA("BT_FE_EGG", NULL);
|
||||
sprintf(fe_arguments, "-egg %s -platform glass", fe_spec.eggPath);
|
||||
break;
|
||||
case BTFeLaunchJoinLan:
|
||||
SetEnvironmentVariableA("BT_FE_EGG", NULL);
|
||||
sprintf(fe_arguments, "-net %d -platform glass", fe_spec.consolePort);
|
||||
break;
|
||||
default: // Solo / HostLan: the marshal armed via env handoff
|
||||
SetEnvironmentVariableA("BT_FE_EGG", fe_spec.eggPath);
|
||||
SetEnvironmentVariableA("BT_FE_PODS", fe_spec.podList);
|
||||
sprintf(fe_value, "%d", fe_spec.missionSeconds);
|
||||
SetEnvironmentVariableA("BT_FE_SECS", fe_value);
|
||||
sprintf(fe_arguments, "-net %d -platform glass", fe_spec.consolePort);
|
||||
break;
|
||||
}
|
||||
BTFE_RelaunchSelfAndExit(fe_arguments); // never returns
|
||||
}
|
||||
//
|
||||
// A marshal-armed mission process: start the LocalConsole worker
|
||||
// (it retries until our own console listener is up).
|
||||
//
|
||||
const char *fe_egg = getenv("BT_FE_EGG");
|
||||
if (fe_egg != NULL && fe_egg[0])
|
||||
{
|
||||
const char *fe_pods = getenv("BT_FE_PODS");
|
||||
const char *fe_secs = getenv("BT_FE_SECS");
|
||||
BTLocalConsole_Start(fe_egg,
|
||||
fe_pods ? fe_pods : "",
|
||||
fe_secs ? atoi(fe_secs) : 600);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// DEBUG (bring-up, default OFF): force the player mech to walk full-ahead with
|
||||
// no keypress so locomotion + camera-follow can be verified in a headless run.
|
||||
// BT_FORCE_THROTTLE may carry a VALUE: +1 full run, ~0.3 walk, -1 reverse (default 1.0).
|
||||
@@ -508,6 +574,21 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
app_manager->RunMissions();
|
||||
std::cout << "[boot] RunMissions returned (mission loop exited)." << std::endl << std::flush;
|
||||
|
||||
#ifdef BT_GLASS
|
||||
//
|
||||
// Front-end loop (glass step 3): a menu-launched mission returns
|
||||
// to the menu on exit. (Marshal missions normally relaunch from
|
||||
// the marshal thread at clock expiry; this tail covers raw-solo
|
||||
// and early exits.)
|
||||
//
|
||||
if (getenv("BT_FE_LOOP") != NULL)
|
||||
{
|
||||
std::cout << "[fe] mission over -- relaunching the menu"
|
||||
<< std::endl << std::flush;
|
||||
BTFE_RelaunchSelfAndExit("");
|
||||
}
|
||||
#endif
|
||||
|
||||
btl4App = NULL;
|
||||
Unregister_Object(app_manager);
|
||||
delete app_manager;
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
//###########################################################################
|
||||
// btl4console -- the in-process LocalConsole marshal (BT_GLASS only;
|
||||
// compiled only when the gate is on -- see CMakeLists.txt).
|
||||
// Protocol + design: btl4console.hpp and tools/btconsole.py (the wire
|
||||
// ground truth; every constant below matches it).
|
||||
//###########################################################################
|
||||
|
||||
#include "btl4console.hpp"
|
||||
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
//
|
||||
// The marshal logs to its own file (marshal.log in the cwd = content\):
|
||||
// the engine's DEBUG log stream is single-threaded and this is a worker
|
||||
// thread. Ordinary printf-file logging, flushed per line.
|
||||
//
|
||||
static FILE *marshalLogFile = NULL;
|
||||
static void
|
||||
MarshalLog(const char *format, ...)
|
||||
{
|
||||
if (marshalLogFile == NULL)
|
||||
{
|
||||
marshalLogFile = fopen("marshal.log", "at");
|
||||
if (marshalLogFile == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
fprintf(marshalLogFile, "[marshal] ");
|
||||
vfprintf(marshalLogFile, format, arguments);
|
||||
fprintf(marshalLogFile, "\n");
|
||||
fflush(marshalLogFile);
|
||||
va_end(arguments);
|
||||
}
|
||||
|
||||
//
|
||||
// Wire constants -- tools/btconsole.py (T2-verified via BT_NET_PROBE=1).
|
||||
//
|
||||
enum
|
||||
{
|
||||
ChunkBytes = 1000,
|
||||
MessageLength = 1024, // sizeof(ReceiveEggFileMessage)
|
||||
EggMessageID = 3, // NetworkManager::ReceiveEggFileMessageID
|
||||
ReliableFlag = 1,
|
||||
ConsoleHostID = 1, // FirstLegalHostID
|
||||
ApplicationClientID = 4, // NetworkClient::ApplicationClientID
|
||||
RunMissionMessageID = 5, // Application::RunMissionMessageID
|
||||
StopMissionMessageID = 6, // Application::StopMissionMessageID (enum: Run+1, APP.h:383)
|
||||
LaunchSettleSeconds = 20, // egg -> first launch (WaitingForLaunch)
|
||||
LaunchStepSeconds = 4, // first -> second launch (Launching -> Running)
|
||||
MissionEndGraceSeconds = 8 // stop sent -> relaunch the menu
|
||||
};
|
||||
|
||||
struct MarshalState
|
||||
{
|
||||
char eggPath[128];
|
||||
char podList[512];
|
||||
int missionSeconds;
|
||||
};
|
||||
|
||||
static MarshalState marshalState;
|
||||
|
||||
//###########################################################################
|
||||
|
||||
void
|
||||
BTFE_RelaunchSelfAndExit(const char *arguments)
|
||||
{
|
||||
//
|
||||
// A MENU relaunch (empty arguments) must not leak the mission
|
||||
// process's marshal handoff into the child -- the child inherits our
|
||||
// environment, and a stale BT_FE_EGG would skip the menu and arm a
|
||||
// marshal with no pod listening (found live: the first full cycle
|
||||
// re-armed instead of returning to the menu).
|
||||
//
|
||||
if (arguments == NULL || arguments[0] == 0)
|
||||
{
|
||||
SetEnvironmentVariableA("BT_FE_EGG", NULL);
|
||||
SetEnvironmentVariableA("BT_FE_PODS", NULL);
|
||||
SetEnvironmentVariableA("BT_FE_SECS", NULL);
|
||||
SetEnvironmentVariableA("BT_FE_LOOP", NULL);
|
||||
}
|
||||
|
||||
WCHAR exe_path[MAX_PATH];
|
||||
GetModuleFileNameW(NULL, exe_path, MAX_PATH);
|
||||
|
||||
WCHAR command_line[MAX_PATH + 256];
|
||||
int n = 0;
|
||||
command_line[n++] = L'"';
|
||||
for (WCHAR *s = exe_path; *s; ++s) command_line[n++] = *s;
|
||||
command_line[n++] = L'"';
|
||||
if (arguments != NULL && arguments[0])
|
||||
{
|
||||
command_line[n++] = L' ';
|
||||
for (const char *s = arguments; *s && n < MAX_PATH + 250; ++s)
|
||||
{
|
||||
command_line[n++] = (WCHAR)*s;
|
||||
}
|
||||
}
|
||||
command_line[n] = 0;
|
||||
|
||||
STARTUPINFOW startup;
|
||||
PROCESS_INFORMATION process;
|
||||
memset(&startup, 0, sizeof(startup));
|
||||
startup.cb = sizeof(startup);
|
||||
memset(&process, 0, sizeof(process));
|
||||
if (CreateProcessW(exe_path, command_line, NULL, NULL, FALSE,
|
||||
0, NULL, NULL, &startup, &process))
|
||||
{
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
}
|
||||
ExitProcess(0);
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// Wire builders
|
||||
//###########################################################################
|
||||
|
||||
//
|
||||
// Egg text -> the wire image NotationFile::ReadText expects: NUL-separated
|
||||
// lines (NOTATION.cpp walks strchr(buffer,0)+1 per line).
|
||||
//
|
||||
static unsigned char *
|
||||
EggWireImage(const char *path, int *out_length)
|
||||
{
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (f == NULL)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
long raw_length = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
char *raw = (char *)malloc(raw_length + 1);
|
||||
fread(raw, 1, raw_length, f);
|
||||
fclose(f);
|
||||
raw[raw_length] = 0;
|
||||
|
||||
unsigned char *wire = (unsigned char *)malloc(raw_length + 2);
|
||||
int w = 0;
|
||||
for (long i = 0; i < raw_length; ++i)
|
||||
{
|
||||
char c = raw[i];
|
||||
if (c == '\r')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
wire[w++] = (c == '\n') ? 0 : (unsigned char)c;
|
||||
}
|
||||
if (w == 0 || wire[w - 1] != 0)
|
||||
{
|
||||
wire[w++] = 0; // terminate the final line
|
||||
}
|
||||
free(raw);
|
||||
*out_length = w;
|
||||
return wire;
|
||||
}
|
||||
|
||||
static void
|
||||
PutInt32(unsigned char *at, int value)
|
||||
{
|
||||
memcpy(at, &value, 4); // x86: little-endian, matching the wire
|
||||
}
|
||||
|
||||
//
|
||||
// One 1040-byte egg chunk: NetworkPacketHeader (clientID=0, gameID=0,
|
||||
// fromHost=1, timeStamp=0) + ReceiveEggFileMessage (len=1024, id=3,
|
||||
// flags=Reliable, seq, fileLen, thisLen, data[1000]).
|
||||
//
|
||||
static int
|
||||
SendEggChunks(SOCKET s, const unsigned char *wire, int wire_length)
|
||||
{
|
||||
unsigned char packet[16 + MessageLength];
|
||||
int sequence = 0;
|
||||
for (int off = 0; off < wire_length; off += ChunkBytes, ++sequence)
|
||||
{
|
||||
int this_length = wire_length - off;
|
||||
if (this_length > ChunkBytes)
|
||||
{
|
||||
this_length = ChunkBytes;
|
||||
}
|
||||
memset(packet, 0, sizeof(packet));
|
||||
PutInt32(packet + 0, 0); // clientID (NetworkManager)
|
||||
PutInt32(packet + 4, 0); // gameID
|
||||
PutInt32(packet + 8, ConsoleHostID); // fromHost
|
||||
PutInt32(packet + 12, 0); // timeStamp
|
||||
PutInt32(packet + 16, MessageLength);
|
||||
PutInt32(packet + 20, EggMessageID);
|
||||
PutInt32(packet + 24, ReliableFlag);
|
||||
PutInt32(packet + 28, sequence);
|
||||
PutInt32(packet + 32, wire_length);
|
||||
PutInt32(packet + 36, this_length);
|
||||
memcpy(packet + 40, wire + off, this_length);
|
||||
if (send(s, (const char *)packet, sizeof(packet), 0) != sizeof(packet))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return sequence;
|
||||
}
|
||||
|
||||
//
|
||||
// The 28-byte application message (RunMission / StopMission): header with
|
||||
// clientID=ApplicationClientID + a 12-byte message (len=12, id, flags).
|
||||
//
|
||||
static int
|
||||
SendApplicationMessage(SOCKET s, int message_id)
|
||||
{
|
||||
unsigned char packet[16 + 12];
|
||||
PutInt32(packet + 0, ApplicationClientID);
|
||||
PutInt32(packet + 4, 0);
|
||||
PutInt32(packet + 8, ConsoleHostID);
|
||||
PutInt32(packet + 12, 0);
|
||||
PutInt32(packet + 16, 12);
|
||||
PutInt32(packet + 20, message_id);
|
||||
PutInt32(packet + 24, ReliableFlag);
|
||||
return (send(s, (const char *)packet, sizeof(packet), 0) == sizeof(packet))
|
||||
? 0 : -1;
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// The marshal thread
|
||||
//###########################################################################
|
||||
|
||||
static SOCKET
|
||||
ConnectWithRetry(const char *host, int port, int attempts)
|
||||
{
|
||||
for (int attempt = 0; attempt < attempts; ++attempt)
|
||||
{
|
||||
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (s == INVALID_SOCKET)
|
||||
{
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
sockaddr_in address;
|
||||
memset(&address, 0, sizeof(address));
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons((unsigned short)port);
|
||||
address.sin_addr.s_addr = inet_addr(host);
|
||||
if (connect(s, (sockaddr *)&address, sizeof(address)) == 0)
|
||||
{
|
||||
int nodelay = 1;
|
||||
setsockopt(s, IPPROTO_TCP, TCP_NODELAY,
|
||||
(const char *)&nodelay, sizeof(nodelay));
|
||||
return s;
|
||||
}
|
||||
closesocket(s);
|
||||
Sleep(1000); // the pod may not be listening yet
|
||||
}
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
|
||||
static DWORD WINAPI
|
||||
MarshalThread(LPVOID)
|
||||
{
|
||||
WSADATA wsa;
|
||||
WSAStartup(MAKEWORD(2, 2), &wsa); // ref-counted; the engine does its own
|
||||
|
||||
int wire_length = 0;
|
||||
unsigned char *wire = EggWireImage(marshalState.eggPath, &wire_length);
|
||||
if (wire == NULL)
|
||||
{
|
||||
MarshalLog("cannot read egg %s -- marshal down", marshalState.eggPath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
//
|
||||
// Connect every pod (self first -- our own engine is still booting;
|
||||
// retry like the real console).
|
||||
//
|
||||
SOCKET pods[8];
|
||||
char pod_names[8][64];
|
||||
int pod_count = 0;
|
||||
{
|
||||
char list_copy[512];
|
||||
strcpy(list_copy, marshalState.podList);
|
||||
char *cursor = strtok(list_copy, ",");
|
||||
while (cursor != NULL && pod_count < 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;
|
||||
MarshalLog("connecting pod %s:%d", host, port);
|
||||
SOCKET s = ConnectWithRetry(host, port, 60);
|
||||
if (s == INVALID_SOCKET)
|
||||
{
|
||||
MarshalLog("pod %s:%d never answered -- aborting mission start",
|
||||
host, port);
|
||||
BTFE_RelaunchSelfAndExit("");
|
||||
}
|
||||
strcpy(pod_names[pod_count], cursor);
|
||||
pods[pod_count++] = s;
|
||||
}
|
||||
cursor = strtok(NULL, ",");
|
||||
}
|
||||
}
|
||||
if (pod_count == 0)
|
||||
{
|
||||
MarshalLog("empty pod list -- marshal down");
|
||||
return 1;
|
||||
}
|
||||
|
||||
//
|
||||
// Stream the egg to every pod, settle, launch twice, hold the clock.
|
||||
//
|
||||
for (int p = 0; p < pod_count; ++p)
|
||||
{
|
||||
int chunks = SendEggChunks(pods[p], wire, wire_length);
|
||||
MarshalLog("egg -> %s (%d bytes, %d chunks)", pod_names[p],
|
||||
wire_length, chunks);
|
||||
}
|
||||
free(wire);
|
||||
|
||||
Sleep(LaunchSettleSeconds * 1000);
|
||||
for (int p = 0; p < pod_count; ++p)
|
||||
{
|
||||
SendApplicationMessage(pods[p], RunMissionMessageID);
|
||||
}
|
||||
MarshalLog("RunMission #1 sent to %d pod(s)", pod_count);
|
||||
Sleep(LaunchStepSeconds * 1000);
|
||||
for (int p = 0; p < pod_count; ++p)
|
||||
{
|
||||
SendApplicationMessage(pods[p], RunMissionMessageID);
|
||||
}
|
||||
MarshalLog("RunMission #2 sent -- mission running; clock %ds",
|
||||
marshalState.missionSeconds);
|
||||
|
||||
//
|
||||
// Own the mission clock. Drain (and discard) pod->console traffic so
|
||||
// the sockets stay healthy -- the console must STAY CONNECTED (a
|
||||
// disconnect trips the pod's console-loss path).
|
||||
//
|
||||
DWORD mission_end = GetTickCount()
|
||||
+ (DWORD)marshalState.missionSeconds * 1000;
|
||||
char drain[4096];
|
||||
while (GetTickCount() < mission_end)
|
||||
{
|
||||
fd_set readable;
|
||||
FD_ZERO(&readable);
|
||||
for (int p = 0; p < pod_count; ++p)
|
||||
{
|
||||
FD_SET(pods[p], &readable);
|
||||
}
|
||||
timeval tv = { 1, 0 };
|
||||
int ready = select(0, &readable, NULL, NULL, &tv);
|
||||
if (ready > 0)
|
||||
{
|
||||
for (int p = 0; p < pod_count; ++p)
|
||||
{
|
||||
if (FD_ISSET(pods[p], &readable))
|
||||
{
|
||||
recv(pods[p], drain, sizeof(drain), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MarshalLog("mission clock expired -- StopMission to all pods");
|
||||
for (int p = 0; p < pod_count; ++p)
|
||||
{
|
||||
SendApplicationMessage(pods[p], StopMissionMessageID);
|
||||
}
|
||||
Sleep(MissionEndGraceSeconds * 1000);
|
||||
for (int p = 0; p < pod_count; ++p)
|
||||
{
|
||||
closesocket(pods[p]);
|
||||
}
|
||||
|
||||
MarshalLog("relaunching the menu");
|
||||
BTFE_RelaunchSelfAndExit("");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
BTLocalConsole_Start(
|
||||
const char *egg_path,
|
||||
const char *pod_list,
|
||||
int mission_seconds)
|
||||
{
|
||||
strncpy(marshalState.eggPath, egg_path, sizeof(marshalState.eggPath) - 1);
|
||||
strncpy(marshalState.podList, pod_list, sizeof(marshalState.podList) - 1);
|
||||
marshalState.missionSeconds =
|
||||
(mission_seconds > 0) ? mission_seconds : 600;
|
||||
|
||||
HANDLE thread = CreateThread(NULL, 0, MarshalThread, NULL, 0, NULL);
|
||||
if (thread == NULL)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
CloseHandle(thread);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
//###########################################################################
|
||||
//
|
||||
// btl4console -- the in-process LocalConsole marshal (BT_GLASS only).
|
||||
//
|
||||
// The arcade operator console is just a TCP peer speaking a documented
|
||||
// wire protocol (tools/btconsole.py, constants T2-verified via
|
||||
// BT_NET_PROBE): connect to each pod's -net console port, stream the
|
||||
// mission egg as 1040-byte chunked ReceiveEggFileMessage packets
|
||||
// (NUL-separated lines), wait the settle, send RunMissionMessage twice,
|
||||
// STAY CONNECTED, own the mission clock, and send StopMissionMessage at
|
||||
// expiry. This marshal plays that console from a worker thread over
|
||||
// loopback (and to remote pods when hosting) -- the engine sees a real
|
||||
// connected console and runs the stock ladder; NO engine hooks.
|
||||
//
|
||||
// The thread touches no engine state. At mission end (+grace) it
|
||||
// relaunches the front-end menu (per-mission process relaunch -- the
|
||||
// reconstructed gBT* globals do not survive in-process re-init) and
|
||||
// exits the process.
|
||||
//
|
||||
//###########################################################################
|
||||
|
||||
//
|
||||
// Start the marshal: egg file path (text form; converted to the NUL wire
|
||||
// image), comma-separated pod console addresses ("127.0.0.1:1501,..."),
|
||||
// and the mission length driving the stop clock. Returns 0 on thread
|
||||
// start.
|
||||
//
|
||||
int
|
||||
BTLocalConsole_Start(
|
||||
const char *egg_path,
|
||||
const char *pod_list,
|
||||
int mission_seconds);
|
||||
|
||||
//
|
||||
// Relaunch this exe with the given command line (empty = the menu) and
|
||||
// exit the current process. Shared by the marshal's mission-end path and
|
||||
// WinMain's post-mission tail.
|
||||
//
|
||||
void
|
||||
BTFE_RelaunchSelfAndExit(const char *arguments);
|
||||
@@ -0,0 +1,734 @@
|
||||
//###########################################################################
|
||||
// btl4fe -- the miniconsole front end (BT_GLASS only; compiled only when
|
||||
// the gate is on -- see CMakeLists.txt). Design: btl4fe.hpp.
|
||||
//###########################################################################
|
||||
|
||||
#include "btl4fe.hpp"
|
||||
|
||||
#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" };
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (BTFeMission_WriteEgg(&mission, spec->eggPath) != 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
//###########################################################################
|
||||
//
|
||||
// btl4fe -- the miniconsole front end (BT_GLASS only): a developer-grade
|
||||
// green-on-black GDI menu that composes a mission and self-launches it,
|
||||
// replacing the (absent) operator console for desktop testing.
|
||||
//
|
||||
// Runs BEFORE any engine init (the zero-arg path in WinMain): presents the
|
||||
// catalog menu, writes the console-shape mission egg (frontend.egg), and
|
||||
// returns a launch spec; WinMain then RELAUNCHES this exe with the real
|
||||
// arguments (the per-mission process-relaunch pattern -- reconstructed
|
||||
// gBT* globals do not survive in-process re-init).
|
||||
//
|
||||
// The egg writer emits the FULL console egg shape validated against
|
||||
// content/MP.EGG: [mission], [ordinals] + the 4 static rank bitmaps,
|
||||
// [pilots] roster, per-pilot pages, GDI-rendered [largebitmap]/
|
||||
// [smallbitmap] pilot-name plasma bitmaps, and the role model pages.
|
||||
//
|
||||
//###########################################################################
|
||||
|
||||
struct BTFePilot
|
||||
{
|
||||
char name[32];
|
||||
char vehicle[16];
|
||||
char color[16];
|
||||
char address[64]; // ip:gamePort for the roster
|
||||
};
|
||||
|
||||
struct BTFeMission
|
||||
{
|
||||
char map[16];
|
||||
char time[16];
|
||||
char weather[16];
|
||||
int temperature;
|
||||
int lengthSeconds;
|
||||
int pilotCount;
|
||||
BTFePilot pilots[8];
|
||||
};
|
||||
|
||||
//
|
||||
// Launch modes the menu can resolve to.
|
||||
//
|
||||
enum BTFeLaunchMode
|
||||
{
|
||||
BTFeLaunchNone = 0, // quit / window closed
|
||||
BTFeLaunchSolo, // networked 1-pod mission (console marshal + self)
|
||||
BTFeLaunchRawSolo, // plain -egg endless solo (renderer work)
|
||||
BTFeLaunchHostLan, // marshal feeds self + remote pods
|
||||
BTFeLaunchJoinLan // plain -net pod; a remote host's marshal feeds us
|
||||
};
|
||||
|
||||
struct BTFeLaunchSpec
|
||||
{
|
||||
BTFeLaunchMode mode;
|
||||
char eggPath[64]; // the written frontend.egg
|
||||
int consolePort; // this instance's -net port
|
||||
char podList[256]; // marshal targets (host modes)
|
||||
int missionSeconds;
|
||||
};
|
||||
|
||||
//
|
||||
// Write the console-shape egg for a mission. Returns 0 on success.
|
||||
//
|
||||
int
|
||||
BTFeMission_WriteEgg(const BTFeMission *mission, const char *path);
|
||||
|
||||
//
|
||||
// Run the menu (blocking, own message loop). Fills the spec; returns 0
|
||||
// when a launch was chosen, nonzero for quit.
|
||||
//
|
||||
int
|
||||
BTFrontEnd_Run(BTFeLaunchSpec *spec);
|
||||
Reference in New Issue
Block a user