From e74957758b29db846018c3806fa83f48b95c2fe4 Mon Sep 17 00:00:00 2001 From: Cyd Date: Thu, 16 Jul 2026 22:48:09 -0500 Subject: [PATCH] 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 --- game/btl4main.cpp | 5 ++ game/reconstructed/btl4console.cpp | 48 ++++++++++++- game/reconstructed/btl4console.hpp | 4 ++ game/reconstructed/btl4fe.cpp | 104 ++++++++++++++++++++++++++++- game/reconstructed/btl4fe.hpp | 3 + 5 files changed, 161 insertions(+), 3 deletions(-) diff --git a/game/btl4main.cpp b/game/btl4main.cpp index b10d831..7b3b2f4 100644 --- a/game/btl4main.cpp +++ b/game/btl4main.cpp @@ -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); diff --git a/game/reconstructed/btl4console.cpp b/game/reconstructed/btl4console.cpp index bfdd282..722c459 100644 --- a/game/reconstructed/btl4console.cpp +++ b/game/reconstructed/btl4console.cpp @@ -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; diff --git a/game/reconstructed/btl4console.hpp b/game/reconstructed/btl4console.hpp index e45c24d..f1c0a91 100644 --- a/game/reconstructed/btl4console.hpp +++ b/game/reconstructed/btl4console.hpp @@ -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(); diff --git a/game/reconstructed/btl4fe.cpp b/game/reconstructed/btl4fe.cpp index 1d8b20c..7003d69 100644 --- a/game/reconstructed/btl4fe.cpp +++ b/game/reconstructed/btl4fe.cpp @@ -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; +} diff --git a/game/reconstructed/btl4fe.hpp b/game/reconstructed/btl4fe.hpp index d6af28e..ab54289 100644 --- a/game/reconstructed/btl4fe.hpp +++ b/game/reconstructed/btl4fe.hpp @@ -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