Files
BT411/game/reconstructed/mechtech.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

521 lines
18 KiB
C++

//===========================================================================//
// File: mechtech.cpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: MechTech -- the mech's subsystem-monitor / diagnostics subsystem //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary. Behaviour follows the Ghidra
// pseudo-C for the @004ad124-@004ad6c9 cluster; the public interface and the
// member / type names are GROUND TRUTH from the surviving MECHTECH.HPP.
// Each non-trivial method cites its originating @ADDR.
//
// What MechTech does:
// At construction it walks the owning Mech's segment roster and, for every
// damageable MechSubsystem (those flagged with segment-bit 0x8), allocates a
// SubsystemMonitor and threads it onto the `subsystemMonitors` chain. Each
// frame the registered Performance (TechnicalAssistance @004ad33c) iterates
// that chain, samples each watched subsystem's status flags, and -- whenever
// a status bit changes -- posts an "alert" / "clear" message into the
// cockpit status / MFD message sink. Set alerts carry the `alarmModel`
// resource; the per-cell wooHoo timer latches an alert for a duration so the
// readout doesn't flicker.
//
// Binary identity (mech.cpp's switch mislabelled these -- corrected here):
// MechTech == ClassID 0xBDC, ctor @004ad228, size 0x104,
// vtable @0050e3a0.
// (mech.cpp's "MechTech" @004b7f94 / 0xBD6 is actually the HUD
// torso-horizon gimbal display and is NOT this class.)
//
// Helper-function name mapping (engine internals referenced by the decomp):
// FUN_0041c52c Subsystem base constructor
// FUN_0041c684 Subsystem::CreateStreamedSubsystem
// FUN_0041c648 Subsystem destructor
// FUN_0041a1a4 IsDerivedFrom(classDerivations)
// FUN_004022d0 operator delete (global heap)
// FUN_00415e90 Plug constructor FUN_00415ed8 Plug destructor
// FUN_004de998 array-construct N elements (count, stride, ..., elem-init)
// FUN_004ad619 trivial element-init thunk (returns its arg)
// FUN_00402f74 MemoryBlock::New(&block) FUN_00402f98 MemoryBlock::Delete(&block,p)
// FUN_00417be0 / 00417c0c / 00417d00 / 00417d28 / 00417d54
// ChainOf<> / chain-iterator internals (ctor/dtor/iter)
// FUN_004ad621 ChainOf<SubsystemMonitor*> ctor (vtable @0050e398)
// FUN_004ad640 ChainOf<> dtor
// FUN_004ad68b chain iterator ctor (vtable @0050e348)
// FUN_004ad6c9 chain iterator dtor
// FUN_00402f74(0x50e248) ... SubsystemMonitor::operator new
// FUN_00404088 NotationFile::Read(section,key,&string)
// FUN_00406ff8 ResourceFile::ResolveModel(name,...) -> ResourceID*
// FUN_004dbb24 DebugStream << (error reporting)
// FUN_004366b8 build "status cleared" cockpit message
// FUN_00436688 build "status set" cockpit message (carries alarmModel)
// FUN_004364e4 dispatch cockpit/MFD message to the status sink
// DAT_004efc94 the running Application/World; (+0x38) == status sink
//
#include <bt.hpp>
#pragma hdrstop
#if !defined(MECHTECH_HPP)
# include <mechtech.hpp>
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#endif
#if !defined(TESTBT_HPP)
# include <testbt.hpp>
#endif
//###########################################################################
//###########################################################################
// MechTech::SubsystemMonitor
//###########################################################################
//###########################################################################
//#############################################################################
// Memory Allocation Support
//
// Static MemoryBlock @0050e248 (zero-filled in .data, primed at runtime).
//
MemoryBlock
MechTech::SubsystemMonitor::AllocatedMemory(
sizeof(MechTech__SubsystemMonitor), // record size (0x80)
8, // initial record count
8, // growth delta
"MechTech::SubsystemMonitor"
);
//#############################################################################
// Construction / Destruction
//
// @004ad124 -- construct a monitor bound to one watched MechSubsystem.
//
// FUN_00415e90(this,1) -> Plug base ctor
// *this = &PTR_FUN_0050e3d0 -> SubsystemMonitor vtable
// FUN_004de998(this+0xC, 0x10, 7, 1, ...) -> placement-build statusArray[7]
// this[0x7C] = subsystem -> monitoredSubsystem
// loop x7: zero {currentStatus,wooHooed,startTime,wooHooEnd}
//
MechTech__SubsystemMonitor::MechTech__SubsystemMonitor(MechSubsystem *subsystem)
:
Plug()
{
monitoredSubsystem = subsystem;
for (int i = 0; i < MechSubsystem::TechStatusTypeCount; ++i) // 7
{
statusArray[i].currentStatus = 0;
statusArray[i].wooHooed = 0;
statusArray[i].startTime = 0L; // Time::operator=(long)
statusArray[i].wooHooEnd = 0L; // Time::operator=(long)
}
}
//
// @004ad180 -- destructor + deleting-destructor thunk. Re-seats the vtable,
// runs the Plug base teardown (FUN_00415ed8) and, when invoked as the
// deleting destructor (flag bit 0), returns the storage to the MemoryBlock.
//
MechTech__SubsystemMonitor::~MechTech__SubsystemMonitor()
{
// Plug base + AllocatedMemory.Delete(this) handled by the generated
// deleting-destructor thunk (FUN_00402f98(&AllocatedMemory, this)).
}
//###########################################################################
//###########################################################################
// MechTech
//###########################################################################
//###########################################################################
//#############################################################################
// Shared Data Support
//
// DefaultData @0050e270 ClassDerivations @0050e280
//
Derivation
MechTech::ClassDerivations(
Subsystem::GetClassDerivations(), // MUNGA Subsystem exposes a Derivation* getter
"MechTech"
);
Receiver::MessageHandlerSet
MechTech::MessageHandlers;
MechTech::AttributeIndexSet
MechTech::AttributeIndex;
MechTech::SharedData
MechTech::DefaultData(
&MechTech::ClassDerivations,
MechTech::MessageHandlers,
MechTech::AttributeIndex,
MechTech::StateCount
);
//#############################################################################
// Construction / Destruction
//
// @004ad228 (mech.cpp allocates 0x104 bytes and calls this for ClassID 0xBDC).
//
// FUN_0041c52c(...) -> Subsystem base ctor
// *this = &PTR_FUN_0050e3a0 -> MechTech vtable
// FUN_004ad621(this+0xE4, 0) -> subsystemMonitors chain ctor
//
// A live (non-copy / non-destroyed) instance -- (entity flags & 0xC) == 0 --
// installs the per-frame Performance and builds its monitor roster; a copy
// just flags itself dormant (this[10] |= 2).
//
MechTech::MechTech(
Mech *entity,
int subsystem_id,
SubsystemResource *model,
SharedData &shared_data
):
Subsystem(entity, subsystem_id, model, shared_data),
subsystemMonitors(0) // FUN_004ad621(this+0xE4, 0) -- Chain(Node* == 0)
{
Check(entity);
Check_Pointer(model);
segmentFlags = 0;
// (entity+0x28 & 0xC) == 0 -- owner is a live primary, not a copy/template.
// CROSS-FAMILY: the original reads the owning Mech's per-segment flags
// (Mech::GetSegmentFlags); mapped here to the entity's simulationFlags.
if ((entity->simulationFlags & SegmentCopyMask) == 0)
{
SetPerformance(&MechTech::TechnicalAssistance); // this[7..9] = {&TechnicalAssistance,0,0} @0050e2b0
alarmModel = model->alarmModel; // res +0x30 -> this+0x100
wooHooMinimumDuration = model->wooHooMinimumDuration; // res +0x34 -> this+0xF4
wooHooDurationRange = model->wooHooDurationRange; // res +0x38 -> this+0xF8
wooHooChance = model->wooHooChance; // res +0x3C -> this+0xFC
//
// Walk the owner's subsystem roster and attach a monitor to every
// damageable MechSubsystem. (Binary walked a Mech-specific segment
// table at owner+0x124/+0x128 whose entries are these subsystem
// objects; the engine exposes the same roster via Entity::GetSubsystem.)
//
for (int seg = entity->GetSubsystemCount() - 1; seg >= 0; --seg)
{
Subsystem *sub = entity->GetSubsystem(seg);
if (
sub != 0
&& sub->IsDerivedFrom(MechSubsystem::ClassDerivations) // FUN_0041a1a4(**sub[3], 0x50de2c)
&& sub->damageZone != 0 // damageable segment (segment bit 0x8)
)
{
SubsystemMonitor *monitor =
new SubsystemMonitor((MechSubsystem *)sub); // MemoryBlock @0050e248
subsystemMonitors.Add(monitor); // (*this[0x39].vt[1])(...)
}
}
}
else
{
segmentFlags |= DormantInstanceFlag; // this[10] |= 2 -- dormant (copy / template instance)
}
Check_Fpu();
}
//
// @004ad1b8 -- destructor + deleting-destructor thunk.
//
// *this = &PTR_FUN_0050e3a0
// FUN_004ad68b/FUN_004ad6c9 ... walk + tear down the monitor chain,
// FUN_004ad640(this+0xE4, 2) -> subsystemMonitors dtor,
// FUN_0041c648(this, 0) -> ~Subsystem.
//
MechTech::~MechTech()
{
Check(this);
//
// Release every SubsystemMonitor still on the chain, then the chain
// itself; ~Subsystem finishes the base teardown.
//
ChainIteratorOf<SubsystemMonitor*> iterator(&subsystemMonitors);
for (
iterator.First();
SubsystemMonitor *monitor = iterator.GetCurrent();
/* Remove() re-seats currentLink */
)
{
iterator.Remove();
delete monitor;
}
Check_Fpu();
}
//###########################################################################
// TestInstance -- MechTech
//
// @004ad470 -> FUN_0041a1a4(**this[3], 0x50e280)
//
Logical
MechTech::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
//#############################################################################
// Per-frame Performance
//
//
// @004ad33c -- TechnicalAssistance: poll every monitored subsystem's status
// flags and emit cockpit/MFD status messages on change.
//
// Decomp shape:
// sink = *(DAT_004efc94 + 0x38); // the status / MFD message sink
// for (monitor in subsystemMonitors) // iterator FUN_004ad68b
// {
// sub = monitor->monitoredSubsystem; // monitor[0x7C]
// flags = sub->GetStatusFlags(); // vtable slot @+0x30
// for (type = 0; type < 7; ++type) // TechStatusTypeCount
// {
// bit = flags & 1;
// cell = &monitor->statusArray[type];
// if (cell->wooHooed) // an alert is latched
// {
// if (cell->wooHooEnd < GetTime()) cell->wooHooed = 0; // expired
// else bit = cell->currentStatus; // hold
// }
// if (bit != cell->currentStatus) // status changed -> report
// {
// Mech *owner = GetEntity();
// if (bit == 0) // cleared
// FUN_004364e4(sink, owner, BuildClear(owner, sub, type));
// else // set (carries alarmModel)
// FUN_004364e4(sink, owner, BuildSet(owner, sub, type, alarmModel));
// }
// cell->currentStatus = bit;
// flags >>= 1;
// }
// }
//
// NOTE (best-effort): the actual latch -- setting cell->wooHooed /
// cell->wooHooEnd from wooHooChance / wooHooMinimumDuration /
// wooHooDurationRange -- lives inside the message-builder helpers
// (FUN_00436688 et al.), which are part of the HUD/message brick and were
// not captured in this window. TechnicalAssistance only *clears* expired
// latches and detects edges.
//
void
MechTech::TechnicalAssistance(Scalar /*time_slice*/)
{
Check(this);
Mech *owner = GetEntity();
void *sink = StatusMessageSink(); // *(DAT_004efc94 + 0x38)
ChainIteratorOf<SubsystemMonitor*> it(&subsystemMonitors); // FUN_004ad68b
SubsystemMonitor *monitor;
for (
it.First();
(monitor = it.GetCurrent()) != 0;
it.Next()
)
{
MechSubsystem *sub = monitor->monitoredSubsystem;
LWord flags = sub->GetStatusFlags(); // vt[+0x30]
for (int type = 0; type < MechSubsystem::TechStatusTypeCount; ++type)
{
int bit = (int)(flags & 1);
StatusInfo &cell = monitor->statusArray[type];
if (cell.wooHooed != 0)
{
if (cell.wooHooEnd.ticks < GetTime().ticks) // this+0x10 == current time (integer tick compare)
{
cell.wooHooed = 0; // latch expired
}
else
{
bit = cell.currentStatus; // hold the latched state
}
}
if (bit != cell.currentStatus)
{
if (bit == 0)
{
ReportStatusCleared(sink, owner, sub, type); // FUN_004366b8 + FUN_004364e4
}
else
{
ReportStatusSet(sink, owner, sub, type, alarmModel);// FUN_00436688 + FUN_004364e4
}
}
cell.currentStatus = bit;
flags >>= 1;
}
}
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateStreamedSubsystem -- MechTech
//
// @004ad48c
//
// FUN_0041c684(...) -> Subsystem::CreateStreamedSubsystem
// resource->subsystemModelSize = 0x40 (res[0x24] = 0x40)
// resource->classID = 0xBDC (res[0x20] = 0x0BDC)
// resource->alarmModel = -1 (res+0x30)
// resource->wooHoo{Min,Range,Chance} = 0 (res+0x34,+0x38,+0x3C)
//
// "AlarmModel" is optional: if absent (and alarmModel still -1) the parse
// succeeds with no alarm. If present, the named model is resolved against
// the resource file; failure reports "<name> couldn't locate <name>.mod".
//
Logical
MechTech::CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
ResourceFile *resource_file
)
{
if (
!Subsystem::CreateStreamedSubsystem( // FUN_0041c684
model_file, model_name, subsystem_name,
subsystem_resource, subsystem_file, directories
)
)
{
return False;
}
subsystem_resource->subsystemModelSize = sizeof(*subsystem_resource); // 0x40
subsystem_resource->classID = RegisteredClass::MechTechClassID; // 0xBDC
subsystem_resource->alarmModel = -1;
subsystem_resource->wooHooMinimumDuration = 0.0f;
subsystem_resource->wooHooDurationRange = 0.0f;
subsystem_resource->wooHooChance = 0.0f;
//
// "AlarmModel" -- optional model name resolved to a resource id.
//
const char *alarmModelName = 0;
if (
subsystem_resource->alarmModel == -1
&& !subsystem_file->GetEntry(subsystem_name, "AlarmModel", &alarmModelName) // FUN_00404088(param_5,param_3,"AlarmModel",&local_8)
)
{
return True; // no alarm model specified -- fine
}
//
// Resolve the named model to a resource id. The binary calls
// FUN_00406ff8(resource_file, name, 1, -1) ("ResolveModel"); the engine
// ResourceFile exposes FindResourceDescription instead.
// TODO(verify): confirm the resource type used for the alarm .mod.
//
ResourceDescription *description =
resource_file->FindResourceDescription(
alarmModelName,
ResourceDescription::GameModelResourceType
);
if (description == 0)
{
// @004ad48c: "<subsystem_name> couldn't locate <name>.mod"
// (original logged via DebugStream / FUN_004dbb24 -- HUD/diag brick).
return False;
}
subsystem_resource->alarmModel = description->resourceID;
Check_Fpu();
return True;
}
//#############################################################################
// Status reporting support
//
// CROSS-FAMILY stubs. The shipped code dispatches cockpit/MFD status messages
// through the running Application/World status sink (DAT_004efc94 + 0x38) using
// the HUD message builders FUN_004366b8 ("status cleared") / FUN_00436688
// ("status set", carries alarmModel) and dispatcher FUN_004364e4. Those live
// in the cockpit-message brick (btl4gaug / app), not reconstructed here, so the
// bodies are stubs that satisfy the call sites in TechnicalAssistance.
//
//
// @0050e248-era global: the status / MFD message sink == *(DAT_004efc94 + 0x38).
//
void*
MechTech::StatusMessageSink()
{
return 0; // TODO(cross-family): *(theApplication + 0x38) -- HUD status sink
}
//
// Engine current time (binary read this+0x10; the WinTesla clock is Now()).
//
Time
MechTech::GetTime() const
{
return Now();
}
//
// FUN_004366b8 (build "status cleared" message) + FUN_004364e4 (dispatch).
//
void
MechTech::ReportStatusCleared(
void * /*sink*/,
Mech * /*owner*/,
MechSubsystem * /*sub*/,
int /*type*/
)
{
// TODO(cross-family): build + dispatch the cockpit "status cleared" message.
}
//
// FUN_00436688 (build "status set" message, carries alarmModel) + FUN_004364e4.
//
void
MechTech::ReportStatusSet(
void * /*sink*/,
Mech * /*owner*/,
MechSubsystem * /*sub*/,
int /*type*/,
ResourceDescription::ResourceID /*alarm_model*/
)
{
// TODO(cross-family): build + dispatch the cockpit "status set" message.
}
//===========================================================================//
// WAVE 2 factory bridge -- MechTech (factory case 0xBDC, "HeatableSubsystem"
// label; the case currently builds the heat base + discards the pointer).
//===========================================================================//
Subsystem *CreateMechTechSubsystem(Mech *owner, int id, void *seg)
{
Check(sizeof(MechTech) <= 0x140);
return (Subsystem *) new (Memory::Allocate(0x140))
MechTech(owner, id, (MechTech::SubsystemResource *)seg, MechTech::DefaultData);
}