Front end: post-mission results screen (kills/deaths scoreboard)

Completes the marshal chain: menu -> mission -> timed stop -> results ->
relaunch.  BT had no mission-end score flow; this builds one from the
data the Comm MFD pilotList already tracks.

- btl4console: at the timed stop (game thread, state still live) snapshot
  each roster pilot's kills/deaths via the BTResolveRosterPilot /
  BTPilotKills / BTPilotDeaths bridges; expose BTLocalConsole_ResultCount
  / GetResult.
- btl4fe: BTFrontEnd_ShowResults -- a GDI green-on-black 'MISSION COMPLETE'
  scoreboard (PILOT / KILLS / DEATHS, slot 0 named from the menu), any
  key or ~12s to dismiss; store the last pilot name.
- btl4main: show the scoreboard after a marshaled mission, before the
  relaunch.

Verified: MISSION COMPLETE screen renders with the pilot row and the
snapshotted score (0/0 on a no-combat test run; the data path is the
same one the live Comm MFD reads).

Phase 5 (front end) COMPLETE: menu + egg builder + marshal + timed stop
+ results + single-binary (relaunch) loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-16 22:48:09 -05:00
co-authored by Claude Fable 5
parent 9d660fa50b
commit e74957758b
5 changed files with 161 additions and 3 deletions
+5
View File
@@ -530,6 +530,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
//=======================================================================//
extern int BTFrontEnd_Run();
extern int BTFrontEnd_LastMissionSeconds();
extern void BTFrontEnd_ShowResults();
extern void BTLocalConsole_Install(int mission_seconds);
extern int BTLocalConsole_MissionCompleted();
@@ -607,6 +608,10 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
//-------------------------------------------------------------------//
if (front_end_mode && BTLocalConsole_MissionCompleted() && IsWindow(hWnd))
{
// Scoreboard first (kills/deaths snapshotted at the stop), then relaunch.
ShowWindow(hWnd, SW_HIDE);
BTFrontEnd_ShowResults();
wchar_t exePath[MAX_PATH];
GetModuleFileNameW(NULL, exePath, MAX_PATH);
STARTUPINFOW si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si);
+46 -2
View File
@@ -13,6 +13,13 @@
#include "appmgr.hpp" // gPerFrameHook
#include "btl4console.hpp"
// The pilot roster + score bridges (btl4gau3.cpp / mechmppr.cpp / btplayer.cpp):
// the pilotList reads them for the Comm MFD, and the marshal snapshots them at
// the stop so the results screen has the final scores.
extern void *BTResolveRosterPilot(int slot);
extern int BTPilotKills(void *pilot);
extern int BTPilotDeaths(void *pilot);
namespace
{
enum Phase { PhaseIdle, PhaseWaiting, PhaseRunning, PhaseStopped };
@@ -22,6 +29,27 @@ namespace
unsigned long gStartTick = 0;
Application *gWatched = 0;
// Final scores snapshotted at the stop (roster order; slot 0 = local).
enum { maxResults = 8 };
struct Result { int kills, deaths; };
Result gResults[maxResults];
int gResultCount = 0;
void SnapshotResults()
{
gResultCount = 0;
for (int slot = 0; slot < maxResults; ++slot)
{
void *pilot = BTResolveRosterPilot(slot);
if (pilot == 0)
break; // past the last occupied roster slot
gResults[gResultCount].kills = BTPilotKills(pilot);
gResults[gResultCount].deaths = BTPilotDeaths(pilot);
++gResultCount;
}
DEBUG_STREAM << "[marshal] snapshot " << gResultCount << " pilot score(s)\n" << std::flush;
}
//
// The game-thread tick (called every frame while an application is live).
//
@@ -59,9 +87,11 @@ namespace
else if (gMissionSeconds > 0 &&
(GetTickCount() - gStartTick) >= (unsigned long)(gMissionSeconds * 1000))
{
// The console clock expired: stop the race exactly as the
// arcade console did (StopMissionMessage on our own pod).
// The console clock expired: snapshot the final scores (the
// game state is still live here), then stop the race exactly
// as the arcade console did (StopMissionMessage on our pod).
DEBUG_STREAM << "[marshal] time expired -- stopping mission\n" << std::flush;
SnapshotResults();
Application::StopMissionMessage message(0);
application->Dispatch(&message);
gPhase = PhaseStopped;
@@ -92,6 +122,20 @@ int BTLocalConsole_MissionCompleted()
return (gPhase == PhaseStopped) ? 1 : 0;
}
int BTLocalConsole_ResultCount()
{
return gResultCount;
}
int BTLocalConsole_GetResult(int index, int *kills, int *deaths)
{
if (index < 0 || index >= gResultCount)
return 0;
if (kills) *kills = gResults[index].kills;
if (deaths) *deaths = gResults[index].deaths;
return 1;
}
void BTLocalConsole_Uninstall()
{
gPerFrameHook = 0;
+4
View File
@@ -21,6 +21,10 @@ void BTLocalConsole_Install(int mission_seconds);
// other way while armed) -- the WinMain loop cycles back to the menu.
int BTLocalConsole_MissionCompleted();
// Final scores snapshotted at the stop (roster order; slot 0 = local pilot).
int BTLocalConsole_ResultCount();
int BTLocalConsole_GetResult(int index, int *kills, int *deaths);
// Drop the marshal + clear the per-frame hook.
void BTLocalConsole_Uninstall();
+103 -1
View File
@@ -21,7 +21,8 @@
#include "l4app.hpp" // L4Application::SetEggNotationFileName
// Mission length (seconds) chosen on the last LAUNCH; WinMain arms the marshal.
static int gLastMissionSeconds = 300;
static int gLastMissionSeconds = 300;
static char gLastPilotName[24] = "Pilot";
//---------------------------------------------------------------------------//
// Catalogs -- the BattleTech content the console's RPConfig.xml named.
@@ -633,6 +634,7 @@ int BTFrontEnd_Run()
strcpy(mission.weather, kMenuWeather[selWx].key);
mission.lengthSeconds = kMenuLengths[selLen].seconds;
strcpy(mission.pilots[0].name, pilotName);
strncpy(gLastPilotName, pilotName, sizeof(gLastPilotName) - 1);
strcpy(mission.pilots[0].vehicle, kMenuMechs[selMech].key);
strcpy(mission.pilots[0].color, kMenuColors[selColor].key);
@@ -648,3 +650,103 @@ int BTFrontEnd_LastMissionSeconds()
{
return gLastMissionSeconds;
}
//===========================================================================//
// THE RESULTS SCREEN -- shown after a marshaled mission, before the relaunch.
// A GDI green-on-black scoreboard reading the marshal's snapshotted scores
// (kills/deaths per roster slot; slot 0 = the local pilot, named from the
// menu). Dismissed by any key/click or after a timeout.
//===========================================================================//
namespace { HWND gResultsWin = NULL; int gResultsDone = 0; }
extern int BTLocalConsole_ResultCount();
extern int BTLocalConsole_GetResult(int index, int *kills, int *deaths);
namespace {
LRESULT CALLBACK ResultsWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_PAINT: {
PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps);
RECT cl; GetClientRect(hwnd, &cl);
HDC mem = CreateCompatibleDC(hdc);
HBITMAP surf = CreateCompatibleBitmap(hdc, cl.right, cl.bottom);
HBITMAP old = (HBITMAP) SelectObject(mem, surf);
FillRect(mem, &cl, (HBRUSH) GetStockObject(BLACK_BRUSH));
SetBkMode(mem, TRANSPARENT);
int th = cl.bottom / 22; if (th < 16) th = 16; if (th > 28) th = 28;
HFONT big = CreateFontA(-(th*2),0,0,0,FW_BOLD,0,0,0,ANSI_CHARSET,OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS,ANTIALIASED_QUALITY,DEFAULT_PITCH|FF_MODERN,"Consolas");
HFONT reg = CreateFontA(-th,0,0,0,FW_NORMAL,0,0,0,ANSI_CHARSET,OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS,ANTIALIASED_QUALITY,DEFAULT_PITCH|FF_MODERN,"Consolas");
const COLORREF gB = RGB(64,255,64), gD = RGB(24,140,24);
SelectObject(mem, big); SetTextColor(mem, gB);
RECT t = cl; t.top = cl.bottom/12; t.bottom = t.top + th*3;
DrawTextA(mem, "M I S S I O N C O M P L E T E", -1, &t, DT_CENTER|DT_TOP|DT_SINGLELINE);
SelectObject(mem, reg);
int y = cl.bottom/4; int cxN = cl.right/6, cxK = cl.right/2, cxD = (cl.right*2)/3;
SetTextColor(mem, gB);
RECT h; h.top=y; h.bottom=y+th*2;
h.left=cxN; h.right=cxK; DrawTextA(mem,"PILOT",-1,&h,DT_LEFT|DT_VCENTER|DT_SINGLELINE);
h.left=cxK; h.right=cxD; DrawTextA(mem,"KILLS",-1,&h,DT_LEFT|DT_VCENTER|DT_SINGLELINE);
h.left=cxD; h.right=cl.right; DrawTextA(mem,"DEATHS",-1,&h,DT_LEFT|DT_VCENTER|DT_SINGLELINE);
y += th*2;
int n = BTLocalConsole_ResultCount();
if (n <= 0) n = 1; // always show the local pilot line
for (int i = 0; i < n; ++i) {
int k = 0, d = 0; BTLocalConsole_GetResult(i, &k, &d);
char name[24]; if (i == 0) strncpy(name, gLastPilotName, sizeof(name)-1), name[sizeof(name)-1]=0;
else sprintf(name, "Pilot %d", i + 1);
char kb[8], db[8]; sprintf(kb, "%d", k); sprintf(db, "%d", d);
SetTextColor(mem, i == 0 ? gB : gD);
RECT r; r.top=y; r.bottom=y+th*2;
r.left=cxN; r.right=cxK; DrawTextA(mem,name,-1,&r,DT_LEFT|DT_VCENTER|DT_SINGLELINE);
r.left=cxK; r.right=cxD; DrawTextA(mem,kb,-1,&r,DT_LEFT|DT_VCENTER|DT_SINGLELINE);
r.left=cxD; r.right=cl.right; DrawTextA(mem,db,-1,&r,DT_LEFT|DT_VCENTER|DT_SINGLELINE);
y += th*2;
}
SetTextColor(mem, gD);
RECT f = cl; f.top = cl.bottom - th*3; f.bottom = cl.bottom - th;
DrawTextA(mem, "press any key", -1, &f, DT_CENTER|DT_TOP|DT_SINGLELINE);
BitBlt(hdc, 0,0, cl.right, cl.bottom, mem, 0,0, SRCCOPY);
SelectObject(mem, old); DeleteObject(surf); DeleteDC(mem);
DeleteObject(big); DeleteObject(reg);
EndPaint(hwnd, &ps); return 0; }
case WM_KEYDOWN: case WM_LBUTTONDOWN: gResultsDone = 1; return 0;
case WM_CLOSE: gResultsDone = 1; return 0;
}
return DefWindowProcA(hwnd, msg, wp, lp);
}
} // namespace
// Show the scoreboard until dismissed (or ~12s timeout). Returns always.
void BTFrontEnd_ShowResults()
{
HINSTANCE instance = GetModuleHandleA(NULL);
static int reg = 0;
if (!reg) { reg = 1;
WNDCLASSA wc; memset(&wc,0,sizeof(wc));
wc.lpfnWndProc = ResultsWndProc; wc.hInstance = instance;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
wc.lpszClassName = "BTResults";
RegisterClassA(&wc);
}
int W = 800, H = 560;
int sx = (GetSystemMetrics(SM_CXSCREEN) - W) / 2;
int sy = (GetSystemMetrics(SM_CYSCREEN) - H) / 2;
gResultsDone = 0;
gResultsWin = CreateWindowExA(0, "BTResults", "BattleTech",
WS_OVERLAPPEDWINDOW | WS_VISIBLE, sx<0?0:sx, sy<0?0:sy, W, H, NULL, NULL, instance, NULL);
if (gResultsWin == NULL) return;
SetForegroundWindow(gResultsWin);
DWORD deadline = GetTickCount() + 12000;
MSG msg;
while (!gResultsDone && GetTickCount() < deadline) {
while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_KEYDOWN || msg.message == WM_QUIT) gResultsDone = 1;
TranslateMessage(&msg); DispatchMessageA(&msg);
}
Sleep(15);
}
DestroyWindow(gResultsWin); gResultsWin = NULL;
}
+3
View File
@@ -73,4 +73,7 @@ int BTFrontEnd_Run();
// LAUNCH -- WinMain arms the LocalConsole marshal with it.
int BTFrontEnd_LastMissionSeconds();
// Show the post-mission scoreboard (marshal snapshot) until dismissed.
void BTFrontEnd_ShowResults();
#endif // BTL4FE_HPP