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>
521 lines
18 KiB
C++
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);
|
|
}
|