Files
BT412/game/reconstructed/btdirect.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

405 lines
16 KiB
C++

//===========================================================================//
// File: btdirect.cpp //
// Project: BattleTech Brick: Camera / Spectator //
// Contents: BattleTech camera director -- decides which player the spectator //
// camera ships follow (BT analog of RP's RPCameraDirector). //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// 01/15/95 JM Initial coding. (date from the surviving BTDIRECT.HPP) //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (BTL4OPT.EXE). The class interface
// is ground truth (surviving BTDIRECT.HPP). Bodies follow the Ghidra
// pseudo-C for the bt camera cluster plus a direct objdump disassembly of the
// two performance methods (they fell in a gap Ghidra never decompiled,
// @004c11b3..@004c1408). Each method cites its originating @ADDR.
//
// CLASS MAP
// BTCameraDirector : public CameraDirector vtable @005134c4
// ctor @004c1408 (chains CameraDirector @0042eec8)
// Make @004c1454 (operator new 0x238 + ctor)
// ~BTCameraDirector @004c1484 (slot0; chains base dtor @0042efa4)
// TestInstance @004c14b0 (IsDerivedFrom ClassDerivations @00513414)
// CreateBTCameraShip @004c13d4 (initial Performance; chains base
// CameraDirector::CreateCameraShip @0042ed68)
// BeABTDirector @004c1230 (steady Performance; BT override of the
// base CameraDirector::BeADirector @0042ec3a)
//
// Only slot0 of the vtable is a BT override; slots 1..16 are inherited
// CameraDirector/Director/Simulation/Receiver implementations (41xxxx/42xxxx).
//
// HEX FLOAT / STATE CONSTANTS
// 0xbf800000 = -1.0f (base ctor seed for switchTimer @0x218)
// 0x41200000 = 10.0f (base BeADirector rotation interval / base member 0x230)
// 0x41700000 = 15.0f (base member 0x234)
// 0x40a00000 = 5.0f (BT eye offset / view height in the track message)
// 0x41f00000 = 30.0f (BT rotation interval -- BTCameraDirector overrides 10->30)
// _DAT_004c13d0 = 0.0f (switchTimer comparison)
// application state (app+0x88): 5 = MissionRunning, 6 = EndingMission,
// 7 = (mission over / scoreboard)
//
// INHERITED CameraDirector BASE LAYOUT (DIRECTOR.HPP; base vtable @004e6b4c):
// this+0x1c activePerformance (Simulation::Performance ptr-to-member, 12B)
// this+0x28 simulationFlags ((flags & 0xc)==4 => replicant)
// this+0x1fc cameraShip receiver used during ship creation (base idx 0x7f)
// this+0x20c followedVehicle (embedded target Connection, base idx 0x83)
// this+0x218 switchTimer (Scalar, base idx 0x86)
// this+0x21c cameraShip* (Receiver*, base idx 0x87; Dispatch target)
//
// ENGINE HELPERS referenced (names inferred; same pool as btplayer.cpp):
// FUN_0042eec8 CameraDirector::CameraDirector(make, shared) [base ctor]
// FUN_0042efa4 CameraDirector::~CameraDirector [base dtor]
// FUN_0042e328 Player/Entity base ctor (chained by 0042eec8)
// FUN_0042e100 CameraDirector::DirectorSimulation(ts) [base, vtable slot14]
// FUN_0042eb38 CameraDirector::UpdateViewpoint(ts) [base viewpoint cut FSM]
// FUN_0042ed68 CameraDirector::CreateCameraShip(ts) [base ship spawn]
// FUN_0042ec3a CameraDirector::BeADirector(ts) [base steady perf]
// FUN_0042efe4 CameraDirector::GetPlayerByIndex(i) -- walks the "Players"
// EntityGroup (@004e6b3c) returning the player whose
// displayIndex(+0x1cc)==i.
// FUN_0049fb54 Mech-validity test on a candidate vehicle (alive / right class)
// FUN_00417ab4 Connection::GetTarget(&conn) -- resolve followedVehicle
// FUN_00417a5c Connection::Reset(&conn)
// FUN_00420ea4 EntityID::EntityID(const char*) (empty string @00522524)
// FUN_0041a1a4 IsDerivedFrom(classDerivations)
// FUN_00402298 operator new (raw alloc)
// DAT_004efc94 the application singleton (app+0x88 state, app+0x24 EntityMgr)
//
// d:\tesla_bt\bt\btdirect.cpp is the presumed original path (not asserted by
// any string in this TU -- BTCameraDirector raises no Verify()s).
//
#include <bt.hpp>
#pragma hdrstop
#if !defined(BTDIRECT_HPP)
# include <btdirect.hpp>
#endif
#if !defined(BTPLAYER_HPP)
# include <btplayer.hpp>
#endif
#if !defined(DIRECTOR_HPP)
# include <director.hpp>
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#endif
#if !defined(NTTMGR_HPP)
# include <nttmgr.hpp>
#endif
#if !defined(CHAIN_HPP)
# include <chain.hpp>
#endif
#define CAMERA_SWITCH_INTERVAL 30.0f // 0x41f00000 this+0x218 reset value
#define CAMERA_EYE_HEIGHT 5.0f // 0x40a00000 fixed eye offset in track msg
//
// FUN_0049fb54 -- BT mech-validity test on a candidate vehicle (alive / right
// class). This is a BT-side helper (lives outside this TU in the shipped
// binary); declared here so BeABTDirector can call it. Typed against the
// engine's Entity* (goalEntity / GetPlayerVehicle both deal in Entity*),
// which is what the followed "mech" is stored as.
//
static Logical
IsLiveMech(Entity *vehicle);
//#############################################################################
//########################### BTCameraDirector ############################
//#############################################################################
//#############################################################################
// Shared Data Support
//
// ClassDerivations @00513414, name string "BTCameraDirector" @005134b1.
// DefaultData @00513444. BTCameraDirector registers no message handlers of
// its own (the vtable's HandleMessage slot is inherited), so the shared-data
// record reuses the base CameraDirector handler set. (Best-effort: the static
// initializer lives in the CRT global-ctor array and was not decompiled; the
// argument list is reconstructed by analogy to BTPlayer::DefaultData.)
//
// (Engine modernization: the shipped code referenced the static member
// CameraDirector::ClassDerivations / ::AttributeIndex directly; the WinTesla
// engine replaced those with GetClassDerivations()/GetAttributeIndex()
// accessors -- mapped through here.)
Derivation
BTCameraDirector::ClassDerivations(
CameraDirector::GetClassDerivations(),
"BTCameraDirector" // @005134b1
);
BTCameraDirector::SharedData
BTCameraDirector::DefaultData(
&BTCameraDirector::ClassDerivations,
CameraDirector::MessageHandlers, // no BT-specific handlers
CameraDirector::GetAttributeIndex(),
CameraDirector::StateCount,
(Entity::MakeHandler)BTCameraDirector::Make
);
//#############################################################################
// Simulation Support
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateBTCameraShip
//
// @004c13d4 The director's *initial* Performance (installed by the ctor on a
// master/standalone copy). It defers to the engine CameraDirector to spawn
// the spectator camera ship and post its setup messages, then hands the
// per-frame work off to the steady-state BeABTDirector performance.
//
// Recovered by direct disassembly (Ghidra left @004c11b3..@004c1408 raw):
// call 0042ed68(this,ts) ; CameraDirector::CreateCameraShip
// this[0x1c..0x24] = {004c1230,0,0} ; activePerformance = &BeABTDirector
//
void
BTCameraDirector::CreateBTCameraShip(Scalar time_slice)
{
//
// Let the base director create the camera ship and wire up its viewport.
//
CameraDirector::CreateCameraShip(time_slice); // FUN_0042ed68
//
// One-shot: from now on run the steady director loop.
//
SetPerformance(&BTCameraDirector::BeABTDirector); // this+0x1c = {004c1230,0,0}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// BeABTDirector
//
// @004c1230 The steady-state Performance. Once per frame it runs the base
// director simulation, then decides which player's vehicle the camera ships
// should track. This is the BT override of CameraDirector::BeADirector
// (@0042ec3a): the base watches a single player (GetPlayerByIndex(0)) and
// rotates on a 10s timer; BattleTech scans the first four player slots and
// rotates on a 30s timer. When it changes the followed vehicle it restarts
// the timer and sends a "track this transform" message (type 0x15, 0x34 bytes)
// to the camera ship.
//
// Recovered by direct disassembly (gap, see above).
//
void
BTCameraDirector::BeABTDirector(Scalar time_slice)
{
//
// Inherited per-frame director update + the base viewpoint blend/cut state
// machine (which advances this+0x224/0x228/0x22c against the 10/15s base
// members and may drop the cached owner-id at this+0x220).
//
// (Engine modernization: the shipped binary called base helpers
// DirectorSimulation(@0042e100)+UpdateViewpoint(@0042eb38); the WinTesla
// base BeADirector implements the same per-frame work as PlayerSimulation
// + UpdateHUD -- mapped through here.)
Player::PlayerSimulation(time_slice); // FUN_0042e100 (sim update)
UpdateHUD(time_slice); // FUN_0042eb38 (viewpoint/HUD)
Logical missionEnding = False; // ebp-0x4
//
// While the rotation timer is still running and the mission is live, keep
// the cameras on the current vehicle -- unless it has been destroyed, in
// which case linger on the wreck while the timer bleeds down to zero (the
// top-of-frame test then forces a re-pick on a later frame).
//
// (switchTimer @0x218 == the engine base member timeLeftOnPlayer;
// app state 5 "MissionRunning" == Application::RunningMission.)
//
if (timeLeftOnPlayer > 0.0f // this+0x218
&& application->GetApplicationState() == Application::RunningMission) // app+0x88 == 5
{
Entity *current = GetGoalEntity(); // FUN_00417ab4(this+0x20c)
if (!IsLiveMech(current)) // FUN_0049fb54 == 0
{
timeLeftOnPlayer -= time_slice; // this+0x218 -= ts
return;
}
// current vehicle still alive: fall through and re-evaluate the pick.
}
else if (application->GetApplicationState() == Application::EndingMission) // app+0x88 == 6
{
missionEnding = True;
}
//
// Pick the lowest-indexed player slot (0..3) that currently owns a live
// vehicle.
//
BTPlayer *player = 0; // esi
Entity *vehicle = 0; // edi (the picked mech, stored as Entity*)
for (int i = 0; i < 4; ++i) // ebp-0x8
{
player = (BTPlayer *)GetPlayerByIndex(i); // FUN_0042efe4(i)
if (player)
{
vehicle = player->GetPlayerVehicle(); // player+0x1fc
if (IsLiveMech(vehicle)) // FUN_0049fb54 != 0
{
break;
}
}
}
if (player == 0 || vehicle == 0)
{
return;
}
//
// If the cameras are already locked on this vehicle, do nothing -- unless
// the mission is ending, in which case refresh the lock.
//
Entity *current = GetGoalEntity(); // FUN_00417ab4(this+0x20c)
if (vehicle == current && !missionEnding)
{
return;
}
//
// Switch the followed vehicle and restart the rotation timer.
// (followedVehicle.Reset()+Connect() == the engine SetGoalEntity().)
//
SetGoalEntity(vehicle); // FUN_00417a5c + Connect(this+0x20c)
timeLeftOnPlayer = CAMERA_SWITCH_INTERVAL; // this+0x218 = 30.0f
//
// Tell the camera ship which target to follow. The shipped binary used a
// BT-specific transform-carrying message (type 0x15, 0x34 bytes) with a
// fixed eye offset (0, 5.0, 0); the WinTesla engine resolves the followed
// entity's transform inside the camera ship, so we send the engine's
// CameraShip::DirectionMessage with the followed entity's ID and the BT
// eye offset preserved as the focus offset.
//
Entity *target = GetGoalEntity(); // FUN_00417ab4(this+0x20c)
CameraShip::DirectionMessage track(
CameraShip::DirectionMessageID,
sizeof(CameraShip::DirectionMessage),
target->GetEntityID(), // target+0x184 transform owner
CAMERA_SWITCH_INTERVAL, // time on camera
Point3D(0.0f, CAMERA_EYE_HEIGHT, 0.0f) // (0, 5.0, 0)
);
cameraShip->Dispatch(&track); // (*this[0x21c]->vtbl[3])(this[0x21c], &track)
}
//#############################################################################
// Construction and Destruction
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// BTCameraDirector (constructor)
//
// @004c1408 Chains the engine CameraDirector ctor, stamps our vtable, and --
// on a master/standalone copy (not a network replicant) -- installs
// CreateBTCameraShip as the initial Performance. Replicant copies leave the
// base performance in place (they only mirror the chosen viewpoint).
//
BTCameraDirector::BTCameraDirector(
MakeMessage *creation_message,
SharedData &shared_data
):
CameraDirector(creation_message, shared_data) // FUN_0042eec8
{
// (decompiler artifact removed: "*(void**)this = &BTCameraDirectorVTable"
// -- the vtable pointer is stamped by the C++ compiler automatically.)
if ((simulationFlags & 0xc) != 4) // this+0x28 -- not a replicant
{
SetPerformance(&BTCameraDirector::CreateBTCameraShip); // this+0x1c = {004c13d4,0,0}
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Make
//
// @004c1454 Allocate a 0x238-byte BTCameraDirector and construct it with the
// default shared data (@00513444).
//
// (The bt entity factory @004c15f0 also builds one of these directly, without
// going through Make: when an incoming creation request names the entity
// "camera" (@00513549) it news a 0x238 BTCameraDirector via the ctor @004c1408;
// otherwise it news a 0x294 BTPlayer. See btplayer.cpp.)
//
BTCameraDirector*
BTCameraDirector::Make(CameraDirector::MakeMessage *creation_message)
{
return new BTCameraDirector(creation_message);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~BTCameraDirector
//
// @004c1484 (scalar-deleting destructor thunk, vtable slot0). Restores our
// vtable and chains ~CameraDirector (which tears down the followedVehicle
// connection at this+0x20c). BTCameraDirector owns no extra resources.
//
BTCameraDirector::~BTCameraDirector()
{
// (decompiler artifact removed: "*(void**)this = &BTCameraDirectorVTable"
// -- the compiler restores the vtable pointer during destruction.)
// base chain: CameraDirector::~CameraDirector (FUN_0042efa4)
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// TestInstance
//
// @004c14b0
//
Logical
BTCameraDirector::TestInstance() const
{
return IsDerivedFrom(ClassDerivations); // FUN_0041a1a4(**this[3], 0x513414)
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// GetPlayerByIndex
//
// @0042efe4 Walk the "Players" EntityGroup and return the player occupying
// slot `index` (the shipped binary matched displayIndex @0x1cc; the WinTesla
// engine exposes the slot as Player::playerBitmapIndex). Mirrors the engine
// CameraDirector::FindPlayerByRank iteration idiom.
//
Player*
BTCameraDirector::GetPlayerByIndex(int index)
{
Check(application);
EntityGroup *player_group =
application->GetEntityManager()->FindGroup("Players");
if (player_group)
{
Player *candidate;
ChainIteratorOf<Node*> iterator(player_group->groupMembers);
while ((candidate = (Player *)iterator.ReadAndNext()) != NULL)
{
Check(candidate);
if (candidate->playerBitmapIndex == index)
{
return candidate;
}
}
}
return NULL;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// IsLiveMech (FUN_0049fb54)
//
// Best-effort reconstruction: the candidate is a "live mech" if it exists and
// still carries the engine ValidFlag (i.e. has not been torn down / destroyed).
// The shipped helper additionally screened on the mech class; that screen is
// elided here because the BT Mech class is reconstructed in a separate TU.
//
static Logical
IsLiveMech(Entity *vehicle)
{
return (vehicle != NULL) && vehicle->IsValid();
}