Files
BT412/game/reconstructed/btl4pb.cpp
T
arcattackandClaude Opus 4.8 7b7d465e5e Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.

Layout:
  engine/   MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
            work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
            models) + image codec; the minimal rp/ headers the audio HAL needs
  game/     reconstructed BT logic + surviving-original BT source + fwd shims
            + WinMain launcher
  content/  full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
  docs/     format specs + reconstruction ledgers
  reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
  tools/    MP console emulator + map/resource scanners

One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:03:40 -05:00

602 lines
22 KiB
C++

//===========================================================================//
// File: btl4pb.cpp //
// Project: BattleTech Brick: BattleTech LBE Application //
// Contents: BTL4PlaybackApplication -- the LBE mission-review / playback //
// application, and BTSpoolerTask, the background task that pumps //
// recorded NetworkPackets out of a SpoolFile back through the //
// network manager so a finished battle can be re-watched. //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary BTL4OPT.EXE. The class declarations
// are VERBATIM from the surviving header BTL4PB.HPP; the method bodies are
// recovered from the Ghidra pseudo-C for the 0x4d3b74-0x4d3f44 cluster
// (all/part_014.c) and cross-checked against the direct Red Planet analog
// RP_L4\RPL4PB.cpp (RPSpoolerTask / RPL4PlaybackApplication), which is a line-
// for-line structural twin.
//
// The object file maps onto these recovered functions:
//
// BTSpoolerTask --------------------------------- vtable @0051f204
// @004d3b74 BTSpoolerTask::BTSpoolerTask (ctor)
// @004d3bb8 BTSpoolerTask::~BTSpoolerTask (scalar-deleting dtor)
// @004d3b90 BTSpoolerTask::DispatchPacket
// @004d3ba8 BTSpoolerTask::GetTimeBias (16-byte tail-call thunk)
//
// BTL4PlaybackApplication ----------------------- vtable @0051f1a4
// @004d3be4 BTL4PlaybackApplication::BTL4PlaybackApplication (ctor)
// @004d3ea0 BTL4PlaybackApplication::~BTL4PlaybackApplication (dtor)
// @004d40d8 BTL4PlaybackApplication::TestInstance (out of OPT vtable)
// @004d3c14 BTL4PlaybackApplication::MakeNetworkManager
// @004d3c34 BTL4PlaybackApplication::Initialize
// @004d3c44 BTL4PlaybackApplication::LoadBackgroundTasks
// @004d3d40 BTL4PlaybackApplication::ExecuteForeground
// @004d3ecc BTL4PlaybackApplication::DispatchPacket
// @004d3ef8 BTL4PlaybackApplication::RunMissionMessageHandler
// @004d3f44 BTL4PlaybackApplication::StopMissionMessageHandler
// (ExecuteBackgroundTask is inherited from L4Application -- the OPT build
// did not emit a BT override; see note at that method.)
//
// Engine-helper name mapping (FUN_ / DAT_ -> intended symbol):
// FUN_0047c5d8 SpoolerTask::SpoolerTask (base ctor)
// FUN_0047c734 SpoolerTask::~SpoolerTask (base dtor)
// FUN_004d34c4 BTL4Application::BTL4Application (base ctor, btl4app.cpp)
// FUN_004d350c BTL4Application::~BTL4Application (base dtor)
// FUN_004d3a2c BTL4Application::RunMissionMessageHandler
// FUN_004d3a94 BTL4Application::StopMissionMessageHandler
// FUN_0047b468 L4Application::InitializeTillConsole
// FUN_0047bfc4 L4PlaybackNetworkManager::L4PlaybackNetworkManager (0x28)
// FUN_00402298 operator new (Make_Pointer)
// FUN_004022d0 operator delete
// FUN_0044de1c TaskManager::AddTask(task) (backgroundTasks@+0x64)
// FUN_0044dd08 ApplicationTask::ApplicationTask(priority)
// FUN_0041d7f0 NetworkManager::GetNetworkClientPointer(clientID)
// FUN_00414b60 Now() -> Time*
// FUN_0044f850 SpoolFile::NextPacket
// FUN_0044e13c Application::GetApplicationManager
// FUN_0044fac0 MissionReviewApplicationManager::ReleaseSpoolFile
// FUN_00403ad0 Group::FindGroup(root, name)
// FUN_0042efe4 CameraDirector::FindPlayerByRank(rank)
// FUN_00421414/00421452 ChainIteratorOf<Node*> ctor/dtor
// FUN_0049fb74 Mech::Reset(origin, MissionReviewReset)
// FUN_00419cb8 Entity::FlushEvents
// FUN_00436668/004364e4 rebuild/replace name bitmap entry
// FUN_00460840 DPLRenderer::SortAndReloadNameBitmaps
// FUN_0045a7a8 DPLRenderer::SetViewAngle
// FUN_0041a1a4 IsDerivedFrom(classDerivations)
// DAT_004efc94 the global Application*
// DAT_004efc98 Exit_Code
// DAT_004fd550 the global mission-review mode (== 2 -> interactive review)
//
// Observed Application sub-object byte offsets (this == Application*):
// +0x18 spoolFile (inherited L4Application member; SetSpoolFile)
// +0x20 networkManager
// +0x24 entityManager
// +0x34 updateManager
// +0x38 rendererManager
// +0x3c controlsManager
// +0x40 intercomManager
// +0x48 videoRenderer (DPLRenderer)
// +0x58 secondsRemainingInGame +0x5c gameStarted
// +0x64 backgroundTasks
// +0x70 executeFrames +0x88 applicationState
// +0xc8 currentMission +0xd4 timeBias (BTL4PlaybackApplication)
//
#include <bt.hpp>
#pragma hdrstop
#if !defined(BTL4PB_HPP)
# include <btl4pb.hpp>
#endif
#if !defined(L4VIDEO_HPP)
# include <l4video.hpp>
#endif
#if !defined(MISSION_HPP)
# include <mission.hpp>
#endif
#if !defined(L4NET_HPP)
# include <l4net.hpp>
#endif
#if !defined(UPDATE_HPP)
# include <update.hpp>
#endif
#if !defined(RENDERER_HPP)
# include <renderer.hpp>
#endif
#if !defined(ICOM_HPP)
# include <icom.hpp>
#endif
#if !defined(DROPZONE_HPP)
# include <dropzone.hpp>
#endif
#if !defined(DIRECTOR_HPP)
# include <director.hpp>
#endif
#if !defined(NTTMGR_HPP)
# include <nttmgr.hpp> // EntityManager / EntityGroup / FindGroup
#endif
#if !defined(MECH_HPP)
# include <mech.hpp>
#endif
//#############################################################################
//########################## BTSpoolerTask ##############################
//#############################################################################
//
//#############################################################################
// @004d3b74 -- chains to SpoolerTask() (FUN_0047c5d8) then stamps the
// BTSpoolerTask vtable (&0051f204). Adds no data of its own.
//#############################################################################
//
BTSpoolerTask::BTSpoolerTask()
{
}
//
//#############################################################################
// @004d3bb8 -- scalar-deleting destructor; reinstalls the vtable and chains
// to ~SpoolerTask (FUN_0047c734).
//#############################################################################
//
BTSpoolerTask::~BTSpoolerTask()
{
Check_Fpu();
}
//
//#############################################################################
// @004d3b90 -- the spooler's per-packet callback simply forwards to the
// running playback application (the global Application*), exactly as the Red
// Planet RPSpoolerTask::DispatchPacket does.
//#############################################################################
//
void
BTSpoolerTask::DispatchPacket(NetworkPacket *packet)
{
BTL4PlaybackApplication *playback_app =
Cast_Object(BTL4PlaybackApplication*, application);
playback_app->DispatchPacket(packet);
Check_Fpu();
}
//
//#############################################################################
// @004d3ba8 -- 16-byte tail-call thunk: returns the application's playback
// time bias so the spooler can re-time recorded packets.
//#############################################################################
//
long
BTSpoolerTask::GetTimeBias()
{
BTL4PlaybackApplication *playback_app =
Cast_Object(BTL4PlaybackApplication*, application);
return playback_app->GetTimeBias();
}
//#############################################################################
//####################### BTL4PlaybackApplication #######################
//#############################################################################
//#############################################################################
// Message Support
//
const Receiver::HandlerEntry
BTL4PlaybackApplication::MessageHandlerEntries[]=
{
MESSAGE_ENTRY(BTL4PlaybackApplication, RunMission), // handler @004d3ef8
MESSAGE_ENTRY(BTL4PlaybackApplication, StopMission) // handler @004d3f44
};
Receiver::MessageHandlerSet&
BTL4PlaybackApplication::GetMessageHandlers()
{
static Receiver::MessageHandlerSet messageHandlers(
ELEMENTS(BTL4PlaybackApplication::MessageHandlerEntries),
BTL4PlaybackApplication::MessageHandlerEntries,
BTL4Application::GetMessageHandlers() // inherited Application set
);
return messageHandlers;
}
//#############################################################################
// Shared Data Support
//
Derivation*
BTL4PlaybackApplication::GetClassDerivations() // @0051ee2c
{
static Derivation classDerivations(
BTL4Application::GetClassDerivations(),
"BTL4PlaybackApplication" // @0051f182
);
return &classDerivations;
}
BTL4PlaybackApplication::SharedData
BTL4PlaybackApplication::DefaultData( // @0051ee5c
BTL4PlaybackApplication::GetClassDerivations(),
BTL4PlaybackApplication::GetMessageHandlers()
);
//
//#############################################################################
// BTL4PlaybackApplication @004d3be4
//#############################################################################
//
// Base call FUN_004d34c4(this, resource, 0x3eb, DefaultData) in the DOS binary:
// BattleTech's BTL4Application ctor there took (ResourceFile*, ClassID,
// SharedData&) -- the DOS build carried no HINSTANCE/HWND. 0x3eb is the
// L4ApplicationClassID. For the WinTesla port the L4Application base ctor
// requires (HINSTANCE, HWND, ...), so those are threaded through here exactly
// as RPL4PlaybackApplication does. The spool file is stashed in the inherited
// spoolFile slot (this+0x18).
//
BTL4PlaybackApplication::BTL4PlaybackApplication(
HINSTANCE hInstance,
HWND hWnd,
ResourceFile *resource_file,
SpoolFile *spool_file
):
BTL4Application(hInstance, hWnd, resource_file, L4ApplicationClassID, DefaultData)
{
// vtable installed by the compiler == &PTR_FUN_0051f1a4
spoolFile = spool_file; // this[6] == this+0x18
Check_Fpu();
}
//
//#############################################################################
// ~BTL4PlaybackApplication @004d3ea0
//#############################################################################
//
BTL4PlaybackApplication::~BTL4PlaybackApplication()
{
// scalar-deleting dtor; reinstalls vtable and chains to ~BTL4Application
Check_Fpu();
}
//
//#############################################################################
// MakeNetworkManager @004d3c14
//#############################################################################
//
// new(0x28)/FUN_0047bfc4 -- identical to RP: the playback application drives a
// L4PlaybackNetworkManager rather than a live one.
//
NetworkManager*
BTL4PlaybackApplication::MakeNetworkManager()
{
Check_Fpu();
return new L4PlaybackNetworkManager;
}
//
//#############################################################################
// Initialize @004d3c34
//#############################################################################
//
void
BTL4PlaybackApplication::Initialize()
{
Check(this);
L4Application::InitializeTillConsole(); // FUN_0047b468
}
//
//#############################################################################
// LoadBackgroundTasks @004d3c44
//#############################################################################
//
// Adds the playback task pipeline. The first task is our own BTSpoolerTask
// (new/FUN_004d3b74); the remaining four are stock ApplicationTasks built with
// priority 0x48 and the engine task vtables observed in the decomp. (In the
// shipped binary the trailing tasks are only distinguished by their vtable
// pointers; the RP analog names them ProcessEventTask, GaugeRendererTask,
// CompleteCyclesTask, FryDeathRowTask, AudioRendererTask -- BT emits five of
// these stock tasks plus the spooler. Names below are best-effort against RP.)
//
void
BTL4PlaybackApplication::LoadBackgroundTasks()
{
Check(this);
ApplicationTask *application_task;
application_task = new BTSpoolerTask; // FUN_004d3b74
Register_Object(application_task);
backgroundTasks->AddTask(application_task); // FUN_0044de1c(this+0x64,..)
application_task = new ProcessEventTask; // vtable &PTR_FUN_004f037c
Register_Object(application_task);
backgroundTasks->AddTask(application_task);
application_task = new GaugeRendererTask; // vtable &PTR_FUN_004f031c
Register_Object(application_task);
backgroundTasks->AddTask(application_task);
application_task = new CompleteCyclesTask; // vtable &PTR_FUN_004f0304
Register_Object(application_task);
backgroundTasks->AddTask(application_task);
application_task = new AudioRendererTask; // vtable &PTR_FUN_004f0364
Register_Object(application_task);
backgroundTasks->AddTask(application_task);
Check_Fpu();
}
//
//#############################################################################
// ExecuteForeground @004d3d40
//#############################################################################
//
// The mission-review frame loop. Structurally the RP ExecuteForeground:
// when idle/not executing it merely services the video renderer; otherwise it
// runs the controls manager (only in interactive review, DAT_004fd550==2), the
// update manager, the interest/renderer managers, the intercom, and -- when
// actually running a mission (state==5) -- recomputes the remaining game time.
//
Logical
BTL4PlaybackApplication::ExecuteForeground(
Time start_of_frame,
Scalar frame_duration
)
{
SET_FOREGROUND_PROCESSING();
Check(this);
//
// executeFrames(+0x70)==0, or state(+0x88)==WaitingForEgg(1): idle.
//
if (!executeFrames || GetApplicationState() == WaitingForEgg)
{
videoRenderer->ExecuteIdle();
CLEAR_FOREGROUND_PROCESSING();
return executeFrames && !Exit_Code; // DAT_004efc98
}
Time frame_ticks;
frame_ticks = frame_duration; // Time::operator=(Scalar)
//
// Controls only matter in interactive ("scrub") review mode.
//
if (GetMissionReviewMode() == 2) // DAT_004fd550
{
Check(controlsManager);
controlsManager->Execute(); // vtbl+0x18
}
//
// Update manager -> interest manager -> renderer manager -> intercom.
//
SET_UPDATE_MANAGER();
Check(updateManager);
updateManager->Execute(start_of_frame); // FUN_00435794 / FUN_004363ec
CLEAR_UPDATE_MANAGER();
Check(rendererManager);
rendererManager->UpdateInterestOrigins(start_of_frame); // FUN_00434a08
Check(interestManager);
interestManager->Execute(); // FUN_0043649c
SET_RENDERER_MANAGER();
Check(rendererManager);
rendererManager->Execute(start_of_frame, frame_ticks, frame_ticks); // FUN_00436440
CLEAR_RENDERER_MANAGER();
Check(intercomManager);
intercomManager->Execute(); // (this+0x40)->vtbl+0x24
//
// Running a recorded mission: keep the on-screen game clock current.
//
if (GetApplicationState() == RunningMission) // state(+0x88)==5
{
// The decomp showed "(Now()-gameStarted)/TicksPerSecond": that
// division reflects the DOS Time-in-ticks representation. In the
// ported engine Time::operator-(Time) already yields Scalar seconds
// (time.h Time_To_Float), so the value is used directly here -- the
// same net result, matching the RP twin.
secondsRemainingInGame = // +0x58
currentMission->GetGameLength() // (this+0xc8)+0xe0
- (Now() - gameStarted); // +0x5c
}
CLEAR_FOREGROUND_PROCESSING();
Check_Fpu();
return executeFrames && !Exit_Code;
}
//
//#############################################################################
// ExecuteBackgroundTask
//#############################################################################
//
// NOTE: the OPT build did not emit a BT-specific override for this slot -- the
// playback application inherits L4Application::ExecuteBackgroundTask unchanged
// (the RP twin's body -- "if (ProcessOneEvent(HighEventPriority)) return; else
// backgroundTasks->Execute();" -- already lives in the L4 base here). Declared
// in the header for completeness; reconstructed best-effort to match RP.
//
void
BTL4PlaybackApplication::ExecuteBackgroundTask()
{
Check(this);
if (ProcessOneEvent(HighEventPriority))
{
return;
}
Check(backgroundTasks);
backgroundTasks->Execute();
}
//
//#############################################################################
// DispatchPacket @004d3ecc
//#############################################################################
//
// Route a recorded packet to its client. packet->clientID is the first word
// of the NetworkPacket; messageData lives at packet+0x10.
//
void
BTL4PlaybackApplication::DispatchPacket(NetworkPacket *packet)
{
Check(this);
NetworkManager *net_mgr = GetNetworkManager(); // this+0x20
Check(net_mgr);
NetworkClient *client =
net_mgr->GetNetworkClientPointer(packet->clientID); // FUN_0041d7f0
Check(client);
client->ReceiveNetworkPacket(packet, &packet->messageData); // vtbl+0x14
Check_Fpu();
}
//
//#############################################################################
// RunMissionMessageHandler @004d3ef8
//#############################################################################
//
// On the very first launch (state==WaitingForLaunch, 3) synchronise the
// playback clock: timeBias = Now - (timestamp of the spooled RunMission
// packet), then advance the spool one packet. Then chain to the base.
//
void
BTL4PlaybackApplication::RunMissionMessageHandler(
RunMissionMessage *message
)
{
Check(this);
if (GetApplicationState() == WaitingForLaunch) // state(+0x88)==3
{
SpoolFile *spool = GetSpoolFile(); // this+0x18
Check(spool);
NetworkPacket *packet = (NetworkPacket*)spool->GetPointer(); // vtbl slot0
timeBias = // this+0xd4
Now().ticks - packet->timeStamp.ticks; // FUN_00414b60, packet+0xc
spool->NextPacket(); // FUN_0044f850
}
BTL4Application::RunMissionMessageHandler(message); // FUN_004d3a2c
Check_Fpu();
}
//
//#############################################################################
// StopMissionMessageHandler @004d3f44
//#############################################################################
//
char winners_spot[]="win?"; // @0051ee63 (literal "win?\0")
void
BTL4PlaybackApplication::StopMissionMessageHandler(
StopMissionMessage *message
)
{
MissionReviewApplicationManager *app_mgr = GetApplicationManager(); // FUN_0044e13c
Check(app_mgr);
SpoolFile *spool = GetSpoolFile(); // this+0x18
if (spool)
{
app_mgr->ReleaseSpoolFile(spool); // FUN_0044fac0
spoolFile = NULL;
//
// Move every player into the dropzone matching its finishing rank
// (the "winners circle").
//
EntityManager *entity_manager = application->GetEntityManager(); // DAT_004efc94+0x24
EntityGroup *dropzones = entity_manager->FindGroup("DropZones"); // FUN_00403ad0
char *place = winners_spot + 3; // the trailing '?'
Player *p;
for (
int rank = 0;
(p = CameraDirector::FindPlayerByRank(rank)) != NULL; // FUN_0042efe4
++rank
)
{
*place = (char)('1' + rank); // "win1".."winN"
ChainIteratorOf<Node*> iterator(dropzones->groupMembers); // FUN_00421414
DropZone *dropzone;
while ((dropzone = (DropZone*)iterator.ReadAndNext()) != NULL)
{
if (!strcmp(dropzone->GetDropZoneName(), winners_spot)) // name@+0x1c8
{
break;
}
}
if (!dropzone)
{
continue;
}
//
// Park the player's Mech on that dropzone for the freeze-frame.
//
Mech *mech = (Mech*)p->GetPlayerVehicle(); // p+0x1fc
if (mech->GetClassID() == MechClassID) // classID(+4)==0xbb9
{
mech->Reset(dropzone->localOrigin, Mech::MissionReviewReset); // FUN_0049fb74, +0x100, 0
mech->SetPerformance(&Mech::DoNothing); // mech[7..9]=&PTR_0051ee69..
mech->FlushEvents(); // FUN_00419cb8
//
// Refresh this player's name bitmap entry.
//
// RECONSTRUCTION NOTE: the shipped BTL4OPT.EXE here built a
// per-entity NameBitmapEntry (FUN_00436668) and handed it to
// RendererManager::ReplaceNameBitmap (FUN_004364e4). That
// per-entry name-bitmap API belonged to the original Division
// IG image-generator renderer and was removed wholesale in the
// WinTesla port (L4D3D); RendererManager / NameBitmapEntry no
// longer expose it. The bulk reload below
// (SortAndReloadNameBitmaps) -- which the port retained and the
// RP twin RPL4PlaybackApplication::StopMissionMessageHandler
// relies on alone -- performs the equivalent refresh, so the
// per-entry rebuild is intentionally folded into it here.
}
}
//
// Re-sort the name bitmaps and tilt the camera up for the circle.
//
DPLRenderer *dpl_renderer = GetVideoRenderer(); // this+0x48
Check(dpl_renderer);
dpl_renderer->SortAndReloadNameBitmaps(); // FUN_00460840
dpl_renderer->SetViewAngle(Degree(45.0f)); // FUN_0045a7a8(.,4); cf. literal @0051ee66
}
//
// Let the application stop normally.
//
BTL4Application::StopMissionMessageHandler(message); // FUN_004d3a94
Check_Fpu();
}
//
//#############################################################################
// TestInstance @004d40d8
//#############################################################################
//
Logical
BTL4PlaybackApplication::TestInstance() const
{
return IsDerivedFrom(*GetClassDerivations()); // FUN_0041a1a4(.,0x51ee2c)
}