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

801 lines
18 KiB
C++

#include "munga.h"
#pragma hdrstop
#include "segment.h"
#include "exptbl.h"
#include "fileutil.h"
#include "jmover.h"
#include "schain.h"
#include "notation.h"
#include "namelist.h"
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
MaterialList::MaterialList() :
materialList(NULL)
{
listIterator = new CStringSocketIterator(materialList);
Register_Object(listIterator);
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
MaterialList::~MaterialList()
{
DeletePlugs();
Unregister_Object(listIterator);
delete listIterator;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Damage::Damage()
{
damageAmount = 0.0f;
damageForce = Vector3D::Identity;
surfaceNormal.x = 0.0f;
surfaceNormal.y = 1.0f;
surfaceNormal.z = 0.0f;
impactPoint = Point3D::Identity;
burstCount = 1.0f;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
EntitySegment*
DamageZone::GetCurrentEffectSite(Enumeration effect_site_type)
{
Check(this);
EntitySegment *return_segment;
switch(effect_site_type)
{
Return_Default_Effect:
case DefaultEffectSite:
default:
{
SChainIteratorOf<EntitySegment*> iterator(defaultEffectSiteChain);
Check(iterator.GetCurrent());
return iterator.GetCurrent();
}
case InternalVideoEffectSite:
{
SChainIteratorOf<EntitySegment*> iterator(internalVideoEffectSiteChain);
return_segment = iterator.GetCurrent();
if (return_segment)
{
Check(return_segment);
return return_segment;
}
else
{
goto Return_Default_Effect;
}
}
case ExternalVideoEffectSite:
{
SChainIteratorOf<EntitySegment*> iterator(externalVideoEffectSiteChain);
return_segment = iterator.GetCurrent();
if (return_segment)
{
Check(return_segment);
return return_segment;
}
else
{
goto Return_Default_Effect;
}
}
case InternalAudioEffectSite:
{
SChainIteratorOf<EntitySegment*> iterator(internalAudioEffectSiteChain);
return_segment = iterator.GetCurrent();
if (return_segment)
{
Check(return_segment);
return return_segment;
}
else
{
goto Return_Default_Effect;
}
}
case ExternalAudioEffectSite:
{
SChainIteratorOf<EntitySegment*> iterator(externalAudioEffectSiteChain);
return_segment = iterator.GetCurrent();
if (return_segment)
{
Check(return_segment);
return return_segment;
}
else
{
goto Return_Default_Effect;
}
}
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
DamageZone::Reset(Logical )
{
SetDamageZoneState(DefaultState);
damageZoneGraphicState.SetState(ExistsGraphicState);
damageLevel = 0.0f;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
DamageZone::WriteUpdateRecord(UpdateRecord *update_record)
{
Check(this);
Check_Pointer(update_record);
update_record->damageLevel = damageLevel;
update_record->damageZoneState = damageZoneState.GetState();
update_record->damageZoneGraphicState = damageZoneGraphicState.GetState();
update_record->damageZoneIndex = damageZoneIndex;
update_record->changedFlags = changedFlags;
update_record->recordLength = sizeof(*update_record);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Clear Flags here for MasterInstance DamageZones!
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
ResetChangedFlags();
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
DamageZone::ReadUpdateRecord(UpdateRecord *update_record)
{
Check(this);
Check_Pointer(update_record);
Verify(damageZoneIndex == update_record->damageZoneIndex);
damageLevel = update_record->damageLevel;
damageZoneState.SetState(update_record->damageZoneState);
damageZoneGraphicState.SetState(update_record->damageZoneGraphicState);
changedFlags = update_record->changedFlags;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
DamageZone::DamageZone(Simulation *owner, int damage_zone_index) :
Node(DamageZoneClassID),
materialTable(NULL, False),
damageZoneState(DamageStateCount),
damageZoneGraphicState(GraphicStateCount),
defaultEffectSiteChain(NULL),
internalVideoEffectSiteChain(NULL),
externalVideoEffectSiteChain(NULL),
internalAudioEffectSiteChain(NULL),
externalAudioEffectSiteChain(NULL)
{
Check(owner);
damageZoneIndex = damage_zone_index;
owningSimulation = owner;
damageLevel = 0.0f;
explosionTable = NULL;
for(int ii=0;ii<Damage::DamageTypeCount;ii++)
{
damageScale[ii] = 0.0f;
}
damageZoneName = "TrivialDamageZone_Name";
changedFlags = 0;
Reset();
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
DamageZone::DamageZone(
Simulation *owning_simulation,
int damage_zone_index,
MemoryStream *stream
) :
Node(DamageZoneClassID),
materialTable(NULL, False),
damageZoneState(DamageStateCount),
damageZoneGraphicState(GraphicStateCount),
defaultEffectSiteChain(NULL),
internalVideoEffectSiteChain(NULL),
externalVideoEffectSiteChain(NULL),
internalAudioEffectSiteChain(NULL),
externalAudioEffectSiteChain(NULL)
{
Check(owning_simulation);
Check(stream);
owningSimulation = owning_simulation;
damageZoneIndex = damage_zone_index;
damageLevel = 0.0f;
explosionTable = NULL;
Reset();
*stream >> damageZoneName;
if (owningSimulation->IsDerivedFrom(*JointedMover::GetClassDerivations()))
{
JointedMover *jointed_mover = Cast_Object(
JointedMover*,
owningSimulation
);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~
// Read in all Effect Sites
//~~~~~~~~~~~~~~~~~~~~~~~~~
//
int effect_site_count;
int effect_site_index;
EntitySegment *effect_site_segment;
//
//~~~~~~~~~~~~~~~~~~~~~~~
// Read in External Video
//~~~~~~~~~~~~~~~~~~~~~~~
//
for (int jj=0;jj<EffectSiteCount;++jj)
{
*stream >> effect_site_count;
for(int ii=0;ii<effect_site_count;++ii)
{
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Get the Site Segment From the Index
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
*stream >> effect_site_index;
effect_site_segment = jointed_mover->GetSegment(effect_site_index);
Check(effect_site_segment);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Add the Segment to the Appropriate Chain
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
switch (jj)
{
case DefaultEffectSite:
defaultEffectSiteChain.Add(effect_site_segment);
break;
case ExternalVideoEffectSite :
externalVideoEffectSiteChain.Add(effect_site_segment);
break;
case InternalVideoEffectSite :
internalVideoEffectSiteChain.Add(effect_site_segment);
break;
case ExternalAudioEffectSite :
externalAudioEffectSiteChain.Add(effect_site_segment);
break;
case InternalAudioEffectSite :
internalAudioEffectSiteChain.Add(effect_site_segment);
break;
}
}
}
}
else
{
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Read in these for consistancy
// and for later improvements for
// not Jointed Mover Objects!
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
int filler_int;
for (int jj=0;jj<EffectSiteCount;++jj)
{
*stream >> filler_int;
}
}
*stream >> defaultArmorPoints;
int ii;
for (ii=0; ii<Damage::DamageTypeCount; ii++)
{
*stream >> damageScale[ii];
}
int skeleton_count;
EntitySegment::SkeletonType skl_type;
*stream >> skeleton_count;
for(ii=0;ii<skeleton_count;++ii)
{
*stream >> skl_type;
int material_count;
*stream >> material_count;
if(!material_count)
{
continue;
}
MaterialList *material_list = new MaterialList;
Register_Object(material_list);
for(int jj=0;jj<material_count;++jj)
{
CString new_material;
*stream >> new_material;
MaterialList::CStringPlug *new_plug =
new MaterialList::CStringPlug(new_material);
Register_Object(new_plug);
material_list->Add(new_plug);
}
material_list->First();
materialTable.AddValue(material_list, skl_type);
}
changedFlags = 0;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
DamageZone::~DamageZone()
{
MaterialTableIterator iterator(materialTable);
iterator.DeletePlugs();
if (explosionTable)
{
Unregister_Object(explosionTable);
delete explosionTable;
}
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
ExplosionTable*
DamageZone::CreateExplosionTable(MemoryStream *explosion_stream)
{
Check(this);
explosionTable = new ExplosionTable(explosion_stream);
Register_Object(explosionTable);
Check_Fpu();
return explosionTable;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
DamageZone::TakeDamage(Damage &damage)
{
Check(this);
Check_Pointer(&damage);
damageLevel += damage.damageAmount * damageScale[damage.damageType];
Clamp(damageLevel, 0.0f, 1.0f);
SetDamageLevelChangedFlag();
if (damageLevel >= 1.0f)
{
SetDamageZoneState(BurningState);
SetGraphicState(DestroyedGraphicState);
SetGraphicStateChangedFlag();
}
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Logical
DamageZone::TestInstance() const
{
return True;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Logical
DamageZone::CreateStreamedDamageZone(
NotationFile *model_file,
const char *,
NotationFile *skl_file,
CString damage_zone_name,
MemoryStream *damage_zone_stream,
NotationFile *dmg_file,
const ResourceDirectories *directories
)
{
Check_Pointer(damage_zone_name);
Check_Pointer(&damage_zone_stream);
Check_Pointer(directories);
Check(dmg_file);
*damage_zone_stream << damage_zone_name;
//
// Find dzone name in .dmg file
//
if(!dmg_file->PageExists(damage_zone_name))
{
std::cerr << damage_zone_name <<" listed in .skl, does not exist in .dmg file"<<std::endl;
Check_Fpu();
return -1;
}
if (skl_file)
{
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Read and Write All The Effect Sites
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
WriteEffectSegmentIndicies(
damage_zone_stream,
damage_zone_name,
dmg_file,
skl_file,
"DefaultEffectSiteName"
);
WriteEffectSegmentIndicies(
damage_zone_stream,
damage_zone_name,
dmg_file,
skl_file,
"ExternalVideoEffectSiteName"
);
WriteEffectSegmentIndicies(
damage_zone_stream,
damage_zone_name,
dmg_file,
skl_file,
"InternalVideoEffectSiteName"
);
WriteEffectSegmentIndicies(
damage_zone_stream,
damage_zone_name,
dmg_file,
skl_file,
"ExternalAudioEffectSiteName"
);
WriteEffectSegmentIndicies(
damage_zone_stream,
damage_zone_name,
dmg_file,
skl_file,
"InternalAudioEffectSiteName"
);
}
else
{
int zero_int(0);
for(int ii=0;ii<EffectSiteCount;++ii)
{
*damage_zone_stream << zero_int;
}
}
Scalar default_points;
if(!dmg_file->GetEntry(
damage_zone_name,
"WeaponDamagePoints",
&default_points
)
)
{
std::cerr<<damage_zone_name<<" Must have a WeaponDamagePoints"<<std::endl;
return False;
}
if (!default_points)
{
std::cerr<<"DefaultDamagePoints must have a non-zero value !"<<std::endl;
Check_Fpu();
return False;
}
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Write default armor values
//~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
*damage_zone_stream << default_points;
//
// Read the damage Points values
//
float point_data;
Scalar modified_armor;
if(!dmg_file->GetEntry(
damage_zone_name,
"CollisionDamagePoints",
&point_data
)
)
{
modified_armor = 1.0f/default_points;
}
else
{
if (point_data == 0.0f)
{
modified_armor = 1.0f/default_points;
}
else
{
modified_armor = 1.0f/point_data;
}
Check_Fpu();
}
*damage_zone_stream << modified_armor;
if(!dmg_file->GetEntry(
damage_zone_name,
"BallisticDamagePoints",
&point_data
)
)
{
modified_armor = 1.0f/default_points;
}
else
{
modified_armor = 1.0f/point_data;
}
*damage_zone_stream << modified_armor;
if(!dmg_file->GetEntry(
damage_zone_name,
"ExplosiveDamagePoints",
&point_data
)
)
{
modified_armor = 1.0f/default_points;
}
else
{
modified_armor = 1.0f/point_data;
}
*damage_zone_stream << modified_armor;
if(!dmg_file->GetEntry(
damage_zone_name,
"LaserDamagePoints",
&point_data
)
)
{
modified_armor = 1.0f/default_points;
}
else
{
modified_armor = 1.0f/point_data;
}
*damage_zone_stream << modified_armor;
if(!dmg_file->GetEntry(
damage_zone_name,
"EnergyDamagePoints",
&point_data
)
)
{
modified_armor = 1.0f/default_points;
}
else
{
modified_armor = 1.0f/point_data;
}
*damage_zone_stream << modified_armor;
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~
// Get dzm filename
//~~~~~~~~~~~~~~~~~~~~~~~~~~
//
NameList *dzm_namelist = model_file->MakeEntryList("video","dzm");
if(!dzm_namelist)
{
int zero_scalar(0.0f);
*damage_zone_stream << zero_scalar;
return True;
}
Register_Object(dzm_namelist);
int dzm_filecount;
dzm_filecount = dzm_namelist->EntryCount();
*damage_zone_stream << dzm_filecount;
if(dzm_filecount)
{
NameList::Entry *dzm_fileentry = dzm_namelist->GetFirstEntry();
while(dzm_fileentry)
{
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// write out the skeleton type of the material
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//
CString dzm_entry_name;
dzm_entry_name = dzm_fileentry->GetName();
EntitySegment::SkeletonType skeleton_type;
if (! dzm_entry_name.Compare("dzm") )
{
skeleton_type = EntitySegment::SkeletonType_N;
}
else if (! dzm_entry_name.Compare("dzms") )
{
skeleton_type = EntitySegment::SkeletonType_S;
}
else if (! dzm_entry_name.Compare("dzmt") )
{
skeleton_type = EntitySegment::SkeletonType_T;
}
else if (! dzm_entry_name.Compare("dzmo") )
{
skeleton_type = EntitySegment::SkeletonType_O;
}
else if (! dzm_entry_name.Compare("dzma") )
{
skeleton_type = EntitySegment::SkeletonType_A;
}
else if (! dzm_entry_name.Compare("dzmb") )
{
skeleton_type = EntitySegment::SkeletonType_B;
}
else if (! dzm_entry_name.Compare("dzmc") )
{
skeleton_type = EntitySegment::SkeletonType_C;
}
else if (! dzm_entry_name.Compare("dzmd") )
{
skeleton_type = EntitySegment::SkeletonType_D;
}
*damage_zone_stream << skeleton_type;
//
// Get the name of the dzm File
//
char *filename = MakePathedFilename(
directories->videoDirectory,
dzm_fileentry->GetChar()
);
Register_Pointer(filename);
//
// Create a Notation file from the dzm ASCII File
//
NotationFile *dzm_file = new NotationFile(filename);
Register_Object(dzm_file);
NameList *material_list;
material_list = dzm_file->MakeEntryList(damage_zone_name, "material");
int entry_count(0);
if(material_list)
{
Register_Object(material_list);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Write out how many materials
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
entry_count = material_list->EntryCount();
*damage_zone_stream << entry_count;
if(entry_count)
{
NameList::Entry *dzm_entry;
dzm_entry = material_list->GetFirstEntry();
Check(dzm_entry);
CString material_name;
while(dzm_entry)
{
material_name = dzm_entry->GetChar();
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Write out the material name
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
*damage_zone_stream << material_name;
dzm_entry = dzm_entry->GetNextEntry();
}
}
Unregister_Object(material_list);
delete material_list;
}
else
{
*damage_zone_stream << entry_count;
}
dzm_fileentry = dzm_fileentry->GetNextEntry();
Unregister_Pointer(filename);
delete filename;
Unregister_Object(dzm_file);
delete dzm_file;
}
}
Unregister_Object(dzm_namelist);
delete dzm_namelist;
Check_Fpu();
return True;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
int
DamageZone::WriteEffectSegmentIndicies(
MemoryStream *damage_zone_stream,
const char *damage_zone_name,
NotationFile *dmg_file,
NotationFile *skl_file,
const char *effect_type
)
{
Check(dmg_file);
Check(skl_file);
Check_Pointer(damage_zone_name);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Make an Entry list for this type of effect Site
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
int effect_count(0);
NameList::Entry *effect_entry;
NameList *effect_namelist = dmg_file->MakeEntryList(damage_zone_name, effect_type);
if (!effect_namelist)
{
//
//~~~~~~~~~~~
// No Entries
//~~~~~~~~~~~
//
*damage_zone_stream << effect_count;
Check_Fpu();
return 1;
}
else
{
Register_Object(effect_namelist);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~
// Write Out number of sites
//~~~~~~~~~~~~~~~~~~~~~~~~~~
//
effect_count = effect_namelist->EntryCount();
*damage_zone_stream << effect_count;
effect_entry = effect_namelist->GetFirstEntry();
CString site_page_name;
int effect_site_index;
while(effect_entry)
{
site_page_name = effect_entry->GetChar();
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Get SegmentIndex for this effect site Name
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
effect_site_index =
JointedMover::GetSegmentIndex(site_page_name, skl_file);
if (effect_site_index == -1)
{
std::cerr<<site_page_name<<" not found in skl file !"<<std::endl;
Unregister_Object(effect_namelist);
delete effect_namelist;
return -1;
}
//
//~~~~~~~~~~~~~~~~~~~~
// Write Out the Index
//~~~~~~~~~~~~~~~~~~~~
//
*damage_zone_stream << effect_site_index;
effect_entry = effect_entry->GetNextEntry();
}
Unregister_Object(effect_namelist);
delete effect_namelist;
}
Check_Fpu();
return 1;
}