Files
BT412/game/reconstructed/btl4console.cpp
T
CydandClaude Opus 4.8 101562978d Terminology: BattleTech missions, not races
"Race" is Red Planet's term (it's a racing game) and rode in with the
RPL4LOBBY port. In BattleTech the gameplay unit is a MISSION. Sweep it out
of the front-end / lobby / marshal code + the roadmap and steamification
digest: rename the public API BTLobby_Push/PullRaceResults ->
Push/PullMissionResults, the internal PrimeHostedRace -> PrimeHostedMission,
the deferred marshal InstallNetworkRace -> InstallNetworkMission, and the
user-facing strings ("STEAM MISSION LOBBY", "L A U N C H  M I S S I O N",
"HOST/JOIN STEAM MISSION"). Comments follow. No behavior change; both gates
(default + BT412_STEAM) still build + link clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:29:38 -05:00

174 lines
5.2 KiB
C++

//===========================================================================//
// btl4console.cpp -- BT412 in-process LocalConsole marshal (solo path).
//
// See btl4console.hpp. Models RP412's RPL4CONSOLE ConsoleTick, reduced to the
// single-player case: no remote pods, no wire -- just own the mission clock and
// dispatch StopMissionMessage at expiry. The tick runs on the game thread
// (gPerFrameHook), so the engine call (Dispatch) is safe.
//===========================================================================//
#include <bt.hpp> // Application, application, DEBUG_STREAM
#pragma hdrstop
#include <string.h> // strncpy (result names from the wire)
#include "appmsg.hpp" // full Application::StopMissionMessage definition
#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 };
Phase gPhase = PhaseIdle;
int gMissionSeconds = 0;
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; char name[24]; };
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);
gResults[gResultCount].name[0] = 0; // filled by the lobby (owner) if networked
++gResultCount;
}
DEBUG_STREAM << "[marshal] snapshot " << gResultCount << " pilot score(s)\n" << std::flush;
}
//
// The game-thread tick (called every frame while an application is live).
//
void ConsoleTick()
{
if (application == 0)
return;
int state = application->GetApplicationState();
switch (gPhase)
{
case PhaseWaiting:
// The mission has staged and started running -> start the clock.
if (state == Application::RunningMission)
{
gPhase = PhaseRunning;
gWatched = application;
gStartTick = GetTickCount();
DEBUG_STREAM << "[marshal] mission running -- length "
<< gMissionSeconds << "s\n" << std::flush;
}
break;
case PhaseRunning:
if (application != gWatched)
return; // only marshal our own mission
if (state != Application::RunningMission)
{
// ended some other way (pilot abort, teardown)
gPhase = PhaseStopped;
DEBUG_STREAM << "[marshal] mission ended (state change)\n" << std::flush;
}
else if (gMissionSeconds > 0 &&
(GetTickCount() - gStartTick) >= (unsigned long)(gMissionSeconds * 1000))
{
// The console clock expired: snapshot the final scores (the
// game state is still live here), then stop the mission 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;
}
break;
default:
break;
}
}
}
void BTLocalConsole_Install(int mission_seconds)
{
gMissionSeconds = mission_seconds;
gPhase = PhaseWaiting;
gStartTick = 0;
gWatched = 0;
// Marshaled: the in-process console owns the clock, so a console-loss
// (there is none locally) would end the mission -- harmless for solo.
gConsoleLossEndsMission = True;
gPerFrameHook = &ConsoleTick;
DEBUG_STREAM << "[marshal] installed (length " << mission_seconds << "s)\n" << std::flush;
}
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;
}
const char *BTLocalConsole_GetResultName(int index)
{
if (index < 0 || index >= gResultCount)
return "";
return gResults[index].name;
}
// The lobby rebuilds the sheet from wire data (owner collates every pod's
// scores) before showing the shared results, so it needs to clear + refill.
void BTLocalConsole_ClearResults()
{
gResultCount = 0;
}
void BTLocalConsole_InjectResult(int slot, int kills, int deaths, const char *name)
{
if (slot < 0 || slot >= maxResults)
return;
gResults[slot].kills = kills;
gResults[slot].deaths = deaths;
gResults[slot].name[0] = 0;
if (name) { strncpy(gResults[slot].name, name, sizeof(gResults[slot].name) - 1);
gResults[slot].name[sizeof(gResults[slot].name) - 1] = 0; }
if (slot >= gResultCount)
gResultCount = slot + 1;
}
void BTLocalConsole_Uninstall()
{
gPerFrameHook = 0;
gPhase = PhaseIdle;
}