Files
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

606 lines
14 KiB
C++

#include "munga.h"
#pragma hdrstop
#include "exptbl.h"
#include "namelist.h"
#include "registry.h"
#include "app.h"
#include "explode.h"
#include "damage.h"
#include "notation.h"
#include "fileutil.h"
#include "renderer.h"
//##############################################################################
//###################### Class ExplosionTableEntry ########################
//##############################################################################
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
ExplosionTableEntry::CreateExplosion(
const EntityID &entity_hit_ID,
const EntityID &creating_entity_ID,
const Point3D &explode_position
)
{
Check(this);
Origin explode_origin(Origin::Identity);
explode_origin = explode_position;
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Create the explosion MakeMessage
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Explosion::MakeMessage exp_message(
Explosion::MakeMessageID,
sizeof(Explosion::MakeMessage),
RegisteredClass::ExplosionClassID,
EntityID::Null,
explosionResourceID,
Explosion::DefaultFlags,
explode_origin,
entity_hit_ID,
creating_entity_ID
);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Calculate actual time based on time_delay
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Time when = Now();
when += timeDelay;
Check(application);
Registry *registry;
registry = application->GetRegistry();
Check_Pointer(registry);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Create a MakeEntityMessage
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Registry::MakeEntityMessage *registry_message;
registry_message = Registry::MakeEntityMessage::Make(&exp_message);
Register_Object(registry_message);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Post entity message to the registry
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
#if 0 // ECH 1/28/96
application->Post(
DefaultEventPriority,
registry,
registry_message,
when
);
#else
application->Post(
CreationEventPriority,
registry,
registry_message,
when
);
#endif
Unregister_Object(registry_message);
delete registry_message;
Check_Fpu();
}
//##############################################################################
// Construction/Destruction
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
ExplosionTableEntry::ExplosionTableEntry(MemoryStream *exp_stream)
{
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Read in the data from the stream
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
*exp_stream >> damageLevel >> explosionResourceID >> graphicState;
*exp_stream >> timeDelay;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
ExplosionTableEntry::~ExplosionTableEntry()
{
}
//##############################################################################
// Tool Support
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Logical
ExplosionTableEntry::CreateStreamedExplosion(
ResourceFile *resource_file,
CString exp_file_name,
NotationFile *exp_file,
CString exp_page_name,
MemoryStream *explosion_stream
)
{
Check(exp_file);
//
//~~~~~~~~~~~~~~~~~~~~
// Get the DamageLevel
//~~~~~~~~~~~~~~~~~~~~
//
Scalar damage_level;
if (!exp_file->GetEntry(
exp_page_name,
"DamageLevel",
&damage_level
)
)
{
damage_level = -1.0f;
}
*explosion_stream << damage_level;
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Get the Explosion Model File
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
const char *explosion_model_file;
if (!exp_file->GetEntry(
exp_page_name,
"ExplosionModelFile",
&explosion_model_file
)
)
{
DEBUG_STREAM <<exp_file_name<<" : "<<exp_page_name<<" missing ExplosionModelFile!"<<std::endl << std::flush;
return False;
}
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Hunt for Model file in ResourceFile
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
ResourceDescription *res;
res = resource_file->FindResourceDescription(
explosion_model_file,
ResourceDescription::ModelListResourceType
);
if (!res)
{
std::cout << exp_file_name << " cannot find " << explosion_model_file
<< " in resource file!\n";
return False;
}
//
//~~~~~~~~~~~~~~~~~~~~~~~~~
// Write out the resourceID
//~~~~~~~~~~~~~~~~~~~~~~~~~
//
*explosion_stream << res->resourceID;
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~
// Read in the Graphic State
//~~~~~~~~~~~~~~~~~~~~~~~~~~
//
const char *graphic_state_char;
Enumeration graphic_state;
if (exp_file->GetEntry(
exp_page_name,
"GraphicState",
&graphic_state_char
)
)
{
if (!strcmp(graphic_state_char, "Destroyed"))
{
graphic_state = DamageZone::DestroyedGraphicState;
}
else if (!strcmp(graphic_state_char, "Gone"))
{
graphic_state = DamageZone::GoneGraphicState;
}
}
else
{
graphic_state = DamageZone::ExistsGraphicState;
}
*explosion_stream << graphic_state;
//
//~~~~~~~~~~~~~~~~~~~~
// Get the TimeDelay
//~~~~~~~~~~~~~~~~~~~~
//
Scalar time_delay;
if (!exp_file->GetEntry(
exp_page_name,
"TimeDelay",
&time_delay
)
)
{
time_delay = 0.0f;
}
*explosion_stream << time_delay;
return True;
}
//##############################################################################
//######################## Class ExplosionTable ###########################
//##############################################################################
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Logical
ExplosionTable::CrossedDamageLevelThreshold(
Scalar old_value,
Scalar new_value
)
{
ExplosionTableIterator iterator(this);
ExplosionTableEntry *current_exp;
while ((current_exp = iterator.ReadAndNext()) != NULL)
{
if (
(old_value < current_exp->GetDamageLevel()) &&
(new_value >= current_exp->GetDamageLevel())
)
{
return True;
}
}
return False;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
ExplosionTableEntry*
ExplosionTable::SelectDamageLevelEntry(Scalar damage_level)
{
ExplosionTableIterator iterator(this);
ExplosionTableEntry *current_exp;
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Choose ExplosionTableEntry by damage Level
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
while ((current_exp = iterator.ReadAndNext()) != NULL)
{
if (damage_level < current_exp->GetDamageLevel())
{
Check_Fpu();
return current_exp;
}
}
Check_Fpu();
return NULL;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
ExplosionTableEntry*
ExplosionTable::SelectGraphicStateEntry(Enumeration graphic_state)
{
ExplosionTableIterator iterator(this);
ExplosionTableEntry *current_entry;
while((current_entry = iterator.ReadAndNext() ) != NULL)
{
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Display Explosion if an Entry Exists
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
if (current_entry->GetGraphicState() == graphic_state)
{
Check_Fpu();
return current_entry;
}
}
Check_Fpu();
return NULL;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
ExplosionTable::CreateExplosion(
ExplosionTableEntry *explosion_entry,
const EntityID &entity_hit_ID,
const EntityID &creating_entity_ID,
Point3D explode_position
)
{
Check(this);
Check(explosion_entry);
explosion_entry->CreateExplosion(
entity_hit_ID,
creating_entity_ID,
explode_position
);
}
//##############################################################################
// Construction/Destruction
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
ExplosionTable::ExplosionTable(MemoryStream *exp_stream):
TableOf<ExplosionTableEntry*, int>(NULL, True)
{
int entry_count;
*exp_stream >> entry_count;
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Create all of the ExplosionTableEntries
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
ExplosionTableEntry *exp_entry;
for(int ii=0;ii<entry_count;++ii)
{
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Create a new ExplosionTableEntry
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
exp_entry = new ExplosionTableEntry(exp_stream);
Register_Object(exp_entry);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Add the entry the the ExplosionTable
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
AddValue(exp_entry, ii);
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
ExplosionTable::~ExplosionTable()
{
ExplosionTableIterator iterator(this);
iterator.DeletePlugs();
}
//##############################################################################
// Tool Support
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Logical
ExplosionTable::CreateStreamedExplosionTable(
ResourceFile *resource_file,
CString model_name,
NotationFile *exp_file,
CString exp_filename,
MemoryStream *explosion_stream
)
{
Check(resource_file);
Check(exp_file);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Make a namelist of all Explosion Entries
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
NameList *exp_namelist = exp_file->MakePageList();
Register_Object(exp_namelist);
if (!exp_namelist->EntryCount())
{
DEBUG_STREAM << exp_file << " is empty or missing \n"<<std::endl << std::flush;
Unregister_Object(exp_namelist);
delete exp_namelist;
return False;
}
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Write the Number of Explosion Entries for this Entity
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
int entry_count = exp_namelist->EntryCount();
*explosion_stream << entry_count;
NameList::Entry *exp_entry = exp_namelist->GetFirstEntry();
while(exp_entry)
{
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// While more pages Add ExplosionEntries to the stream
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
ExplosionTableEntry::CreateStreamedExplosion(
resource_file,
exp_filename,
exp_file,
exp_entry->GetName(),
explosion_stream
);
exp_entry = exp_entry->GetNextEntry();
}
Unregister_Object(exp_namelist);
delete exp_namelist;
return True;
}
//##########################################################################
//###################### EntityEffectWatcher #########################
//##########################################################################
//##############################################################################
// Construction/Destruction
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
EntityEffectWatcher::EntityEffectWatcher(
Entity *entity_watched
)
{
entityWatched = entity_watched;
entityWatched->AddEffectWatcher(this);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Init array to hold previous damageLevel values
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Verify(entityWatched->damageZoneCount > 0);
oldDamageLevel = new Scalar[entityWatched->damageZoneCount];
Register_Pointer(oldDamageLevel);
for(int ii=0;ii<entityWatched->damageZoneCount;++ii)
{
oldDamageLevel[ii] = 0.0f;
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
EntityEffectWatcher::~EntityEffectWatcher()
{
Unregister_Pointer(oldDamageLevel);
delete[] oldDamageLevel;
}
//##############################################################################
// Execute
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
EntityEffectWatcher::Execute()
{
Check(this);
Check(entityWatched);
Check_Pointer(entityWatched->damageZones);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Check the flags to see if any damageZones
// have been modified since the last Execute was called
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
DamageZone *current_zone;
ExplosionTable *exp_table;
ExplosionTableEntry *exp_entry(NULL);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Traverse damageZones looking for Changes
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
for (int ii=0;ii<entityWatched->damageZoneCount; ++ii)
{
current_zone = entityWatched->damageZones[ii];
Check(current_zone);
ResourceDescription::ResourceID exp_res;
exp_table = current_zone->GetExplosionTable();
Check(exp_table);
//
//~~~~~~~~~~~~~~~~~~~
// Check Damage Level
//~~~~~~~~~~~~~~~~~~~
//
if (current_zone->DamageLevelChanged())
{
exp_entry = exp_table->SelectDamageLevelEntry(current_zone->damageLevel);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// If delta was large enough signal an update
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
if (
exp_table->CrossedDamageLevelThreshold(
oldDamageLevel[ii],
current_zone->damageLevel
) &&
(entityWatched->GetInstance() == Entity::MasterInstance)
)
{
entityWatched->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
}
oldDamageLevel[ii] = current_zone->damageLevel;
}
//
//~~~~~~~~~~~~~~~~~~~~
// Check Graphic State
//~~~~~~~~~~~~~~~~~~~~
//
if (current_zone->GraphicStateChanged())
{
exp_entry = exp_table->SelectGraphicStateEntry(
current_zone->GetGraphicState()
);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Always update for a graphic state change
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
if (entityWatched->GetInstance() == Entity::MasterInstance)
{
entityWatched->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
}
}
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Notify RendererManager of Any Change
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
if (exp_entry)
{
Check(exp_entry);
exp_res = exp_entry->GetExplosionResourceID();
Check(application);
Check(application->GetRendererManager());
application->GetRendererManager()->StartEntityEffect(
entityWatched,
current_zone,
exp_res
);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Reset the DamageZone::changedFlags if
// I am a master and no update occurred OR
// if I am a replicant
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
if (
(
(entityWatched->GetInstance() == Entity::MasterInstance) &&
!entityWatched->DamageZoneUpdateModelFlagSet()
) ||
(
(entityWatched->GetInstance() == Entity::ReplicantInstance)
)
)
{
current_zone->ResetChangedFlags();
}
}
exp_entry = NULL;
}
Check_Fpu();
}
//##############################################################################
// Test Support
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Logical
EntityEffectWatcher::TestInstance() const
{
Check(entityWatched);
return True;
}