Files
BT412/engine/MUNGA/EXPLODE.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

774 lines
20 KiB
C++

#include "munga.h"
#pragma hdrstop
#include "explode.h"
#include "mover.h"
#include "boxsolid.h"
#include "collasst.h"
#include "damage.h"
#include "random.h"
#include "app.h"
#include "objstrm.h"
#include "hostmgr.h"
#include "notation.h"
#include "namelist.h"
//#############################################################################
//############################ Explosion ################################
//#############################################################################
//#############################################################################
// Shared Data Support
//
Derivation* Explosion::GetClassDerivations()
{
static Derivation classDerivations(Entity::GetClassDerivations(), "Explosion");
return &classDerivations;
}
Explosion::SharedData
Explosion::DefaultData(
Explosion::GetClassDerivations(),
Explosion::GetMessageHandlers(),
Explosion::GetAttributeIndex(),
Explosion::StateCount,
(Entity::MakeHandler)Explosion::Make
);
void
Explosion::ExplosionSimulation(Scalar)
{
Check(application);
if ((Now() - creationTime) > explosionDuration)
{
CondemnToDeathRow();
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void Explosion::SplashDamage
( EntityID inflicting,
Damage &damage,
Scalar collisionRadius,
Vector3D offset,
Entity * excluded) // Excluded should really be a set of entities!
{
Check(this);
Check(entityHit);
if (excluded)
{
Check(excluded);
}
Check_Pointer(&damage);
Verify(0 < collisionRadius);
//
// The list of volumes we intersect
//
BoxedSolidCollisionList collision_list;
collision_list.Reset();
//
// Create the collision assistant to check movers against
//
CollisionAssistant *collision_assistant;
collision_assistant = new CollisionAssistant(entityHit->GetInterestZoneID());
Register_Object(collision_assistant);
Check(collision_assistant);
BoxedSolidResource box;
Check_Pointer(&box);
box.solidType = BoxedSolid::YAxisCylinderType;
box.materialType = BoxedSolid::ConcreteMaterial;
const Vector3D min = Vector3D(-collisionRadius,-collisionRadius,-collisionRadius);
const Vector3D max = Vector3D(collisionRadius,collisionRadius,collisionRadius);
ExtentBox ext_box = ExtentBox(min,max);
box.solidExtents = ext_box;
box.sliceExtents = ext_box;
BoxedSolidResource local_box;
local_box.Instance(box,localOrigin);
BoxedSolid *splash_volume = BoxedSolid::MakeBoxedSolid(&local_box, this, NULL);
Register_Object(splash_volume);
//
//---------------------------------
// Test against the tangible movers
//---------------------------------
//
Check(collision_assistant);
CollisionAssistant::MovingEntityIterator iterator(collision_assistant);
Entity *entity;
Mover *mover;
Check(splash_volume);
while ((entity = iterator.ReadAndNext()) != NULL)
{
//
//------------------------------------------------------------------
// If we are checking against ourselves, skip it
//------------------------------------------------------------------
//
Check(entity);
if (entity == this)
{
continue;
}
//
//--------------------------------------------------
// If we have a mover class object, check against it
//--------------------------------------------------
//
if (entity->IsDerivedFrom(*Mover::GetClassDerivations()))
{
mover = Cast_Object(Mover *,entity);
Check(mover);
mover->CheckVolumeAgainstBoxedSolidChain(&collision_list, splash_volume);
}
}
//
//-------------------------------------------------------------------------
// Test against the static world
//-------------------------------------------------------------------------
//
InterestManager *interest_mgr = application->GetInterestManager();
Check(interest_mgr);
InterestZone *zone = interest_mgr->GetInterestZone(interestZoneID);
Check(zone);
BoxedSolidTree* tree = zone->GetCollisionRoot();
Check(tree);
tree->FindBoundingBoxesContaining(
splash_volume,
collision_list
);
Check_Fpu();
Unregister_Object(collision_assistant);
delete collision_assistant;
Unregister_Object(splash_volume);
delete splash_volume;
//
//-------------------------------------------------------------------------
// Make a sorted list of all the entities from the collision list,
// sorted by their distance from the explosion's origin.
//-------------------------------------------------------------------------
//
VChainOf<Entity*,Vector3D> radius_chain(NULL,False);
VChainIteratorOf<Entity*,Vector3D> radius_chain_iterator(radius_chain);
Vector3D radius;
Entity *target_entity;
//
for(int i = 0;i < collision_list.GetCollisionCount(); i++)
{
BoxedSolidCollision bsc = collision_list[i];
Check(&bsc);
target_entity = (Entity*) ((bsc.GetTreeVolume())->GetOwningSimulation());
Check(target_entity);
Verify(target_entity->IsDerivedFrom(Entity::GetClassDerivations()));
if ((target_entity != entityHit) && (target_entity != this) && (target_entity != excluded))
{
radius = Vector3D::Identity;
radius.Subtract(target_entity->localOrigin.linearPosition,localOrigin.linearPosition);
radius_chain.AddValue(target_entity, radius);
}
}
// While remaining_bursts > 0, advance through list, pick a number of bursts
// to inflict, and call SendDamage to the current entity.
Damage some_damage = damage;
int possible_targets = 0;
int damageable_targets = 0;
radius_chain_iterator.First();
while ((target_entity = (Entity*) radius_chain_iterator.GetCurrent()) != NULL)
{
Check(target_entity);
radius = radius_chain_iterator.GetValue();
radius_chain_iterator.Next();
possible_targets++;
if (target_entity->damageZones)
{
damageable_targets++;
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Set the damage data
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
some_damage.burstCount =damage.burstCount / (pow(radius.Length(), 1.2f));
Min_Clamp(some_damage.burstCount,1);
#if DEBUG_LEVEL > 0
DEBUG_STREAM << "At radius " << radius.Length() << ", applied burstCount is " << some_damage.burstCount << std::endl << std::flush;
DEBUG_STREAM << "Damage per burst is " << some_damage.damageAmount << std::endl << std::flush;
#endif
Vector3D force = Vector3D::Identity;
force.Multiply(radius,localToWorld);
some_damage.damageForce = force;
Point3D origin_point = Point3D::Identity;
origin_point.Add(localOrigin.linearPosition,offset);
some_damage.impactPoint = origin_point;
//some_damage.impactPoint.Add(origin_point,radius);
//
//------------------------------------------
// Send a damage message to the current target entity
//------------------------------------------
//
if (target_entity->GetEntityID() == inflicting)
{
DEBUG_STREAM << "Splash damage hurting inflicting entity." << std::endl << std::flush;
}
if (excluded && target_entity->GetEntityID() == excluded->GetEntityID() )
{
DEBUG_STREAM << "Splash damage hurting excluded entity!" << std::endl << std::flush;
}
Entity::TakeDamageMessage take_damage(
Entity::TakeDamageMessageID,
sizeof(Entity::TakeDamageMessage),
inflicting,
-1, // We can't know what damage zone we hit
some_damage
);
target_entity->Dispatch(&take_damage);
}
}
#if DEBUG_LEVEL >0
DEBUG_STREAM << "There were " << possible_targets << "entities within range, only " << damageable_targets << " had damage zones" << std::endl << std::flush;
#endif
Check_Fpu();
}
//#############################################################################
// Construction and Destruction
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Explosion::Explosion(
Explosion::MakeMessage *creation_message,
Explosion::SharedData &virtual_data
):
Entity(creation_message, virtual_data)
{
Check_Pointer(this);
Check_Pointer(creation_message);
//
//~~~~~~~~~~~~~~~~~~~~~~~
// zero out any rotation
//~~~~~~~~~~~~~~~~~~~~~~~
//
localOrigin.angularPosition = Quaternion::Identity;
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Get the explosion model resource
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
ResourceDescription *resource_description;
ModelResource *model_resource;
Check(application);
Check(application->GetResourceFile());
resource_description =
application->GetResourceFile()->SearchList(
resourceID,
ResourceDescription::GameModelResourceType
);
Check(resource_description);
resource_description->Lock();
model_resource = (ModelResource*)resource_description->resourceAddress;
Check_Pointer(model_resource);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Get the creating entity and entity hit by explosion
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Check(application->GetHostManager());
entityHit =
application->GetHostManager()->GetEntityPointer(
creation_message->entityHitID
);
Check(entityHit);
if (creation_message->creatingEntityID != EntityID::Null)
{
creatingEntity =
application->GetHostManager()->GetEntityPointer(
creation_message->creatingEntityID
);
Check(creatingEntity);
}
else
{
creatingEntity = NULL;
}
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Calculate the localHitPosition in terms
// local coordinate system of the entityHit
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
localHitPosition.MultiplyByInverse(
localOrigin.linearPosition,
entityHit->localToWorld
);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Get the explosion duration
//~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
explosionDuration = model_resource->explosionDuration;
#if 0
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// If an explosion resource table is being used then
// use it to set the resource
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
if (model_resource->resourceTableObjectID != NullObjectID)
{
//
// If the explosion hit us, set the material to our craft,
// If the explosion hit an opponent, set the material to other craft,
// If the explosion hit something else then get the material...
//
Enumeration material_type;
Check(application);
Entity *viewpoint_entity = application->GetViewpointEntity();
Check(viewpoint_entity);
Verify(creation_message->entityHitID != EntityID::Null);
// ECH 8/25/95 - Is this the best way to get our vehicle?
if (
creation_message->entityHitID ==
viewpoint_entity->GetEntityID()
)
{
material_type = BoxedSolid::OurCraftMaterial;
}
else
{
// ECH 8/25/95 - Is this the best way to identify other vehicles?
if (
entityHit != NULL &&
entityHit->GetPlayerLink() != NULL
)
{
material_type = BoxedSolid::OtherCraftMaterial;
}
else
{
// ECH 8/25/95 - How to get material? Just use this for now.
material_type = BoxedSolid::ConcreteMaterial;
}
}
//
// Get resource table pointer,
// Then set the appropriate resource ID
//
Plug *plug;
ExplosionResourceTable *resource_table;
ResourceDescription::ResourceID new_resource_ID;
plug = PlugStreamManager::FindPlug(model_resource->resourceTableObjectID);
resource_table = Cast_Object(ExplosionResourceTable*, plug);
Check(resource_table);
new_resource_ID = resource_table->FindResourceID(material_type);
if (new_resource_ID != ResourceDescription::NullResourceID)
{
resourceID = new_resource_ID;
}
}
#else
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// If an explosion resource table is being used then
// use it to set the resource
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
if (model_resource->resourceTableObjectID != NullObjectID)
{
//
// Get resource table pointer,
// Then set the appropriate resource ID
//
Plug *plug;
ExplosionResourceTable *resource_table;
ResourceDescription::ResourceID new_resource_ID;
plug = PlugStreamManager::FindPlug(model_resource->resourceTableObjectID);
resource_table = Cast_Object(ExplosionResourceTable*, plug);
Check(resource_table);
new_resource_ID = resource_table->FindResourceID(entityHit);
if (new_resource_ID != ResourceDescription::NullResourceID)
{
resourceID = new_resource_ID;
}
}
#endif
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Set performance and entity validity
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
resource_description->Unlock();
SetPerformance(&Explosion::ExplosionSimulation);
SetValidFlag();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Explosion*
Explosion::Make(Explosion::MakeMessage *creation_message)
{
Check(application);
HostManager *host_mgr = application->GetHostManager();
Check(host_mgr);
Entity *entity = host_mgr->GetEntityPointer(creation_message->entityHitID);
if (entity)
{
return new Explosion(creation_message);
}
Tell(
"Warning: Explosion ignored - entity " << creation_message->entityHitID
<< " couldn't be found!\n"
);
return NULL;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Explosion::~Explosion()
{
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Logical
Explosion::TestInstance() const
{
return IsDerivedFrom(*GetClassDerivations());
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateModelResource
//
ResourceDescription::ResourceID
Explosion::CreateModelResource(
ResourceFile *resource_file,
const char *model_name,
NotationFile *model_file,
const ResourceDirectories*
)
{
Check(resource_file);
Check_Pointer(model_name);
Check(model_file);
ModelResource local_model;
if(
!model_file->GetEntry(
"gamedata",
"ExplosionDuration",
&local_model.explosionDuration
)
)
{
std::cerr<<model_file<<" missing ExplosionDuration"<<std::endl;
return -1;
}
//
//--------------------------------------------------------------------------
// If there exists the name of an explosion resource table then write
// its object ID
//--------------------------------------------------------------------------
//
const char *resource_table_name;
if(
model_file->GetEntry(
"gamedata",
"ExplosionResourceTable",
&resource_table_name
)
)
{
//
// Get the object ID and set it in the model resource
//
local_model.resourceTableObjectID =
PlugStreamManager::FindPlugName(resource_table_name);
if (local_model.resourceTableObjectID == NullObjectID)
{
std::cout << "resource_table_name == " << resource_table_name << "\n";
Fail("Explosion::CreateModelResource - ExplosionResourceTable not found");
}
Verify(local_model.resourceTableObjectID != NullObjectID);
}
else
{
local_model.resourceTableObjectID = NullObjectID;
}
ResourceDescription *resource_description =
resource_file->AddResource(
model_name,
ResourceDescription::GameModelResourceType,
1,
ResourceDescription::Preload,
&local_model,
sizeof(ModelResource)
);
Check(resource_description);
return resource_description->resourceID;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Enumeration
Explosion::GetInterestPriority(Entity *linked_entity)
{
Check(this);
Check(linked_entity);
return
(
(linked_entity == creatingEntity) ?
HighInterestPriority :
LowInterestPriority
);
}
//#############################################################################
//###################### ExplosionResourceTable #########################
//#############################################################################
//
//#############################################################################
//#############################################################################
//
ExplosionResourceTable::ExplosionResourceTable(PlugStream *stream):
Plug(stream),
resourceIDSocket(NULL, True)
{
Check(stream);
Check(&resourceIDSocket);
CollectionSize table_size;
*stream >> table_size;
for (CollectionSize i = 0; i < table_size; i++)
{
Enumeration material_type;
ResourceDescription::ResourceID resource_ID;
ResourceIDPlug *resource_ID_Plug;
*stream >> material_type >> resource_ID;
resource_ID_Plug = new ResourceIDPlug(resource_ID);
Register_Object(resource_ID_Plug);
resourceIDSocket.AddValue(resource_ID_Plug, material_type);
}
}
//
//#############################################################################
//#############################################################################
//
ExplosionResourceTable::~ExplosionResourceTable()
{
TableIteratorOf<ResourceIDPlug*, Enumeration>
iterator(&resourceIDSocket);
Check(&iterator);
iterator.DeletePlugs();
}
//
//#############################################################################
//#############################################################################
//
void
ExplosionResourceTable::BuildFromPage(
ObjectStream *stream,
NameList *name_list,
ClassID class_ID,
ObjectID object_ID
)
{
Check(stream);
Check(name_list);
Plug::BuildFromPage(stream, name_list, class_ID, object_ID);
//
//--------------------------------------------------------------------------
// Write number of entries
//--------------------------------------------------------------------------
//
CollectionSize table_size = 0;
NameList::Entry *entry;
Check(name_list);
entry = name_list->GetFirstEntry();
while (entry != NULL)
{
Check(entry);
if (entry->IsName("table_entry"))
table_size++;
entry = entry->GetNextEntry();
}
*stream << table_size;
//
//--------------------------------------------------------------------------
// Write entries
//--------------------------------------------------------------------------
//
#if DEBUG_LEVEL>0
CollectionSize i = 0;
#endif
Check(name_list);
entry = name_list->GetFirstEntry();
while (entry != NULL)
{
Check(entry);
if (entry->IsName("table_entry"))
{
CString table_entry, token;
Enumeration material_type;
ResourceDescription *resource_description;
ResourceDescription::ResourceID resource_ID;
Check_Pointer(entry->GetChar());
table_entry = entry->GetChar();
//
// Material
//
Check_Pointer(table_entry.GetNthToken(0));
material_type = atol(table_entry.GetNthToken(0));
//
// Resource
//
Check_Pointer(table_entry.GetNthToken(1));
Check(stream->GetResourceFile());
resource_description =
stream->GetResourceFile()->FindResourceDescription(
table_entry.GetNthToken(1),
ResourceDescription::ModelListResourceType
);
Check(resource_description);
resource_ID = resource_description->resourceID;
*stream << material_type << resource_ID;
#if DEBUG_LEVEL>0
i++;
#endif
}
entry = entry->GetNextEntry();
}
#if DEBUG_LEVEL>0
Verify(i == table_size);
#endif
}
//
//#############################################################################
//#############################################################################
//
Logical
ExplosionResourceTable::TestInstance() const
{
Plug::TestInstance();
Check(&resourceIDSocket);
return True;
}
//
//#############################################################################
//#############################################################################
//
ResourceDescription::ResourceID
ExplosionResourceTable::FindResourceID(Enumeration material_type)
{
Check(this);
ResourceIDPlug *resource_ID_Plug;
if ((resource_ID_Plug = resourceIDSocket.Find(material_type)) != NULL)
{
Check(resource_ID_Plug);
return resource_ID_Plug->GetItem();
}
return ResourceDescription::NullResourceID;
}
//
//#############################################################################
//#############################################################################
//
ResourceDescription::ResourceID
ExplosionResourceTable::FindResourceID(Entity *entity_hit)
{
Check(this);
Check(entity_hit);
//
// If the explosion hit us, set the material to our craft,
// If the explosion hit an opponent, set the material to other craft,
// If the explosion hit something else then get the material...
//
Enumeration material_type;
Entity *viewpoint_entity;
Check(application);
viewpoint_entity = application->GetViewpointEntity();
Check(viewpoint_entity);
// ECH 8/25/95 - Is this the best way to get our vehicle?
if (entity_hit->GetEntityID() == viewpoint_entity->GetEntityID())
{
material_type = BoxedSolid::OurCraftMaterial;
}
// ECH 8/25/95 - Is this the best way to identify other vehicles?
else if (entity_hit->GetPlayerLink() != NULL)
{
material_type = BoxedSolid::OtherCraftMaterial;
}
else
{
// ECH 8/25/95 - How to get material? Just use this for now.
material_type = BoxedSolid::ConcreteMaterial;
}
//
// Return the appropriate resource ID
//
return FindResourceID(material_type);
}