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>
405 lines
16 KiB
C++
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();
|
|
}
|