Files
BT411/engine/MUNGA/TOOL.cpp
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

863 lines
20 KiB
C++

#include "munga.h"
#pragma hdrstop
#include "tool.h"
#include "fileutil.h"
#include "audtools.h"
#include "registry.h"
#include "boxsolid.h"
#include "namelist.h"
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
int
GetId(char *string)
{
int id;
char *number = strstr( string, "/id:");
if (number != NULL)
{
number += 4;
id = atoi(number);
return id;
}
else
{
return -1;
}
}
Logical
PlatformTool::ConvertStringToModeMask(
const char */*string*/,
ModeMask */*returned_value_ptr*/
)
{
Fail("PlatformTool::ConvertStringToModeMask method not overridden!");
return False;
}
Logical
PlatformTool::ConvertStringToTechStatus(
const char */*string*/,
int */*returned_value_ptr*/
)
{
Fail("PlatformTool::ConvertStringToTechStatus method not overridden!");
return False;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
int
ApplicationTool::BuildResources(
int argc,
char **argv,
AudioCreateSymbols &create_symbols,
Byte minor_version
)
{
char temp_name[200];
const char *Res_Name;
char *resfilename;
char ops_filename[150];
char dirgauge[150];
char dircollision[150];
char dirmodel[150];
char dirmap[150];
char diraudio[150];
char dirvideo[150];
char diranim[150];
char short_name[100];
char long_name[200];
char id_name[100];
const char *tempdir;
ResourceDirectories
resource_directories;
NotationFile *ops_file = new NotationFile();
Register_Object(ops_file);
StreamableResourceFile file;
if (argc < 2)
{
DEBUG_STREAM << "Usage: buildXX resource.bld [res2.bld ...]\n" << std::flush;
return 1;
}
verboseMode = False;
labOnly = False;
//
// Write audio symbols, add static audio resources
//
create_symbols.Execute();
platformTool->CreateStaticAudioStreamResource(&file);
//
//------------------------------------
// Arguments are only build files,
// so step thru them
//------------------------------------
//
for(int arg_number = 1; arg_number < argc; ++arg_number)
{
strcpy(temp_name,argv[arg_number]);
NotationFile *notation_file;
WorkingDirectory = MakeWorkingDirectory(temp_name);
Register_Pointer(WorkingDirectory);
char *filename = MakePathedFilename(WorkingDirectory, temp_name);
Register_Pointer(filename);
notation_file = new NotationFile(filename);
Register_Object(notation_file);
labOnly |= notation_file->IsMarkedLabOnly();
if (!notation_file->PageCount())
{
DEBUG_STREAM << filename << " does not exist! " << std::endl << std::flush;
Unregister_Object(notation_file);
delete notation_file;
Stop_Registering();
return 0;
}
else if (!notation_file->PageExists("resource"))
{
DEBUG_STREAM << filename << " Not a valid resource list" << std::endl << std::flush;
Unregister_Object(notation_file);
delete notation_file;
Stop_Registering();
return 0;
}
Unregister_Pointer(filename);
delete filename;
//
//-----------------------------------------------
// If this is the first file check
// for required info, if not skip this
// section so required info isn't overwritten
//-----------------------------------------------
//
if(arg_number == 1)
{
//
// Check verboseMode
//
const char *verb_string;
if(notation_file->GetEntry("resource","verbose",&verb_string))
{
if(!stricmp(verb_string, "true"))
{
verboseMode = True;
}
}
//---------------------------------
// Set up directories
//--------------------------------
if(!notation_file->GetEntry("resource","dirmap",&tempdir))
{
DEBUG_STREAM << "No map directory specified!!!" << std::endl << std::flush;
PostQuitMessage(AbortExitCodeID);
}
strcpy(dirmap, tempdir);
if(!notation_file->GetEntry("resource","dirmodel", &tempdir))
{
DEBUG_STREAM << "No model directory specified!!!" << std::endl << std::flush;
PostQuitMessage(AbortExitCodeID);
}
strcpy(dirmodel, tempdir);
if(!notation_file->GetEntry("resource","dircollision", &tempdir))
{
DEBUG_STREAM << "No collision directory specified!!!" << std::endl << std::flush;
PostQuitMessage(AbortExitCodeID);
}
strcpy(dircollision, tempdir);
const char
*vp,
*version_string;
Byte version[2] = {0,0};
notation_file->GetEntry("resource","diraudio", &tempdir);
if (tempdir)
{
strcpy(diraudio, tempdir);
}
else
{
DEBUG_STREAM << "No directory specified for 'diraudio'!\n" << std::flush;
PostQuitMessage(AbortExitCodeID);
}
notation_file->GetEntry("resource","dirvideo", &tempdir);
if (tempdir)
{
strcpy(dirvideo, tempdir);
}
else
{
DEBUG_STREAM << "No directory specified for 'dirvideo'!\n" << std::flush;
PostQuitMessage(AbortExitCodeID);
}
notation_file->GetEntry("resource","diranimation", &tempdir);
if (tempdir)
{
strcpy(diranim, tempdir);
}
else
{
DEBUG_STREAM << "No directory specified for 'diranimation'!\n" << std::flush;
PostQuitMessage(AbortExitCodeID);
}
notation_file->GetEntry("resource","dirgauge", &tempdir);
if (tempdir)
{
strcpy(dirgauge, tempdir);
}
else
{
DEBUG_STREAM << "No directory specified for 'gauge'!\n" << std::flush;
PostQuitMessage(AbortExitCodeID);
}
resource_directories.collisionDirectory = dircollision;
resource_directories.modelDirectory = dirmodel;
resource_directories.mapDirectory = dirmap;
resource_directories.audioDirectory = diraudio;
resource_directories.videoDirectory = dirvideo;
resource_directories.animationDirectory = diranim;
resource_directories.gaugeDirectory = dirgauge;
//-------------------------
// Get version info
//-------------------------
if (!notation_file->GetEntry("release","version", &version_string))
{
DEBUG_STREAM << "No version number in build!!!!" << std::endl << std::flush;
PostQuitMessage(AbortExitCodeID);
}
version[0] = (Byte)atoi(version_string);
vp = strchr(version_string, '.');
if (vp)
{
vp++;
version[1] = (Byte)atoi(vp);
}
//-------------------------
// Get Resource Name
//-------------------------
if(!notation_file->GetEntry("resource","name",&Res_Name))
{
DEBUG_STREAM << "No resource file specified!!!" << std::endl << std::flush;
PostQuitMessage(AbortExitCodeID);
}
resfilename = MakePathedFilename(WorkingDirectory, Res_Name);
Register_Pointer(resfilename);
strcpy(ops_filename, Res_Name);
char *p = ops_filename;
p += strlen(ops_filename);
p -= 4;
*p = '\0';
//================================
// Set ops_file "res name"
// write maps and write models
// can use this for ops file
// format.
//================================
ops_file->SetEntry("resource", "res name", ops_filename);
ops_file->AppendEntry("resource", NULL, (char *)NULL);
strcat(ops_filename, ".ops");
file.versionArray[1] = version[0];
file.versionArray[2] = version[1];
file.versionArray[3] = minor_version;
}
//
//---------------------------
// Now step thru each entry
// in every build file
//---------------------------
//
NameList *entry_list =
notation_file->MakeEntryList("resource");
Register_Object(entry_list);
NameList::Entry *entry = entry_list->GetFirstEntry();
const char *entry_data;
int pre_allocated = 0;
while (entry != NULL)
{
entry_data = entry->GetName();
if (entry_data == NULL)
{
entry = entry->GetNextEntry();
continue;
}
entry_data = entry->GetChar();
if (entry_data == NULL)
{
entry = entry->GetNextEntry();
continue;
}
strcpy(id_name, entry_data);
int id = GetId(id_name);
if ( id != -1)
{
if(arg_number > 1)
{
Fail("Manual ResourceID's not allowed in second build files!");
}
if (id > pre_allocated)
{
pre_allocated = id;
}
}
entry = entry->GetNextEntry();
}
//
// pre_allocated number....
//
file.PreAllocateIDs(pre_allocated);
entry = entry_list->GetFirstEntry();
while (entry != NULL)
{
entry_data = entry->GetName();
if (entry_data == NULL)
{
entry = entry->GetNextEntry();
continue;
}
if (!stricmp(entry_data, "LAB_ONLY"))
{
labOnly = True;
entry = entry->GetNextEntry();
continue;
}
else if (!stricmp(entry_data, "map"))
{
const char *q = entry->GetChar();
strcpy(long_name, q);
int id = GetId(long_name);
if (id != -1)
{
//
// Strip id number off
//
char *p = strchr(long_name, '\t');
if (p)
{
*p = '\0';
}
else
{
char *p = strchr(long_name, ' ');
*p = '\0';
}
}
//
// open mapname
//
char *filename = MakePathedFilename(resource_directories.mapDirectory, long_name);
Register_Pointer(filename);
DEBUG_STREAM << "Opening Map " << filename << std::endl << std::flush;
NotationFile *new_notation_file = new NotationFile(filename);
Register_Object(new_notation_file);
labOnly |= new_notation_file->IsMarkedLabOnly();
StripDirectory(long_name);
strcpy(short_name, long_name);
if (new_notation_file->PageCount())
{
if (
!WriteMaps(
short_name,
new_notation_file,
ops_file,
&file,
&resource_directories,
id
)
)
{
DEBUG_STREAM << filename << " Did not work!!!! " << std::endl << std::flush;
}
}
else
{
DEBUG_STREAM << filename << " Does not exist! " << std::endl << std::flush;
}
Unregister_Pointer(filename);
delete filename;
Unregister_Object(new_notation_file);
delete new_notation_file;
}
else if (!stricmp(entry_data, "model"))
{
strcpy(long_name, entry->GetChar());
int id = GetId(long_name);
if (id != -1)
{
//
// Strip id number off
//
char *p = strchr(long_name, '\t');
if (p)
{
*p = '\0';
}
else
{
char *p = strchr(long_name, ' ');
*p = '\0';
}
}
//
// open modelname
//
char *filename = MakePathedFilename(resource_directories.modelDirectory, long_name);
Register_Pointer(filename);
DEBUG_STREAM << "Opening Model " << filename << std::endl << std::flush;
NotationFile *new_notation_file = new NotationFile(filename);
Register_Object(new_notation_file);
labOnly |= new_notation_file->IsMarkedLabOnly();
const char *p = StripExtension(StripDirectory(long_name));
strcpy(short_name, p);
if (new_notation_file->PageCount())
{
if (
!WriteModels(
new_notation_file,
ops_file,
&file,
short_name,
&resource_directories,
id
)
)
{
DEBUG_STREAM << "Error - Model did not work : " << filename << std::endl << std::flush;
}
}
else
{
DEBUG_STREAM << filename << " does not exist " << std::endl << std::flush;
}
Unregister_Pointer(filename);
delete filename;
Unregister_Object(new_notation_file);
delete new_notation_file;
}
else if (!stricmp(entry_data, "zone"))
{
char *q = entry->GetChar();
strcpy(long_name, q);
//
// open zone
//
char *filename = MakePathedFilename(resource_directories.mapDirectory, long_name);
Register_Pointer(filename);
DEBUG_STREAM << "Opening Zones " << filename << std::endl << std::flush;
NotationFile *new_notation_file = new NotationFile(filename);
Register_Object(new_notation_file);
labOnly |= new_notation_file->IsMarkedLabOnly();
if (new_notation_file->PageCount())
{
InterestLatticeManager::CreateZoneResources
(
&file,
new_notation_file
);
}
else
{
DEBUG_STREAM << filename << " Does not exist! " << std::endl << std::flush;
}
Unregister_Pointer(filename);
delete filename;
Unregister_Object(new_notation_file);
delete new_notation_file;
}
else if (!stricmp(entry_data, "static"))
{
//
// Add registry static objects
//
Check(entry);
const char *static_object_file_name = entry->GetChar();
Check_Pointer(static_object_file_name);
char *full_path_file_name =
MakePathedFilename(
resource_directories.modelDirectory,
static_object_file_name
);
Register_Pointer(full_path_file_name);
NotationFile
static_object_notation_file(full_path_file_name);
Registry::CreateStaticObjectStreamResource(
&static_object_notation_file,
&file
);
Unregister_Pointer(full_path_file_name);
delete full_path_file_name;
}
entry = entry->GetNextEntry();
}
Unregister_Object(entry_list);
delete entry_list;
//
//-------------------------------------------------------
// Make this resource Lab only if labOnly was specified
//-------------------------------------------------------
//
if (labOnly == True)
{
file.labOnly = 1;
}
Unregister_Pointer(WorkingDirectory);
Unregister_Object(notation_file);
delete WorkingDirectory;
delete notation_file;
}
WriteOpsFile(ops_file, &file, &resource_directories);
file.SaveAs(resfilename);
ops_file->WriteFile(ops_filename);
Unregister_Pointer(resfilename);
delete resfilename;
//------------------------
// open the vehicle.tbl
// and write to ops and
//------------------------
Unregister_Object(ops_file);
delete ops_file;
return 0;
}
//#############################################################################
void
ApplicationTool::ListResource(
ResourceDescription *resource
)
{
Check(resource);
switch (resource->resourceType)
{
case ResourceDescription::ModelListResourceType:
DEBUG_STREAM << resource->resourceName << ": Model List of "
<< *(int*)resource->resourceAddress << " elements";
break;
case ResourceDescription::MapListResourceType:
DEBUG_STREAM << resource->resourceName << ": Map List of "
<< *(int*)resource->resourceAddress << " elements";
break;
case ResourceDescription::AudioStreamListResourceType:
DEBUG_STREAM << resource->resourceName << ": Audio stream List of "
<< *(int*)resource->resourceAddress << " elements";
break;
case ResourceDescription::VideoListResourceType:
DEBUG_STREAM << resource->resourceName << ": Video List of "
<< *(int*)resource->resourceAddress << " elements";
break;
case ResourceDescription::SubsystemListResourceType:
DEBUG_STREAM << resource->resourceName << ": Subsystem List of "
<< *(int*)resource->resourceAddress << " elements";
break;
case ResourceDescription::ControlMappingsListResourceType:
DEBUG_STREAM << resource->resourceName << ": ControlsMappings List of "
<< *(int*)resource->resourceAddress << " elements";
break;
case ResourceDescription::DamageZoneListResourceType:
DEBUG_STREAM << resource->resourceName << ": DamageZone List of "
<< *(int*)resource->resourceAddress << " elements";
break;
case ResourceDescription::SkeletonListResourceType:
DEBUG_STREAM << resource->resourceName << ": Skeleton List of "
<< *(int*)resource->resourceAddress << " elements";
break;
case ResourceDescription::BoxedSolidStreamResourceType:
DEBUG_STREAM << resource->resourceName << ": Boxed Solid Stream of "
<< resource->resourceSize/sizeof(BoxedSolidResource)
<< " solids";
break;
case ResourceDescription::VideoModelResourceType:
DEBUG_STREAM << resource->resourceName << ": VideoModel" << std::flush;
break;
case ResourceDescription::StaticAudioStreamResourceType:
DEBUG_STREAM << resource->resourceName << ": StaticAudioStream" << std::flush;
break;
case ResourceDescription::InternalAudioStreamResourceType:
DEBUG_STREAM << resource->resourceName << ": InternalAudioStream" << std::flush;
break;
case ResourceDescription::ExternalAudioStreamResourceType:
DEBUG_STREAM << resource->resourceName << ": ExternalAudioStream" << std::flush;
break;
case ResourceDescription::MakeMessageStreamResourceType:
DEBUG_STREAM << resource->resourceName << ": Map stream of "
<< *(int*)resource->resourceAddress << " instances";
break;
case ResourceDescription::GameModelResourceType:
DEBUG_STREAM << resource->resourceName << ": GameModel" << std::flush;
break;
case ResourceDescription::AnimationResourceType:
DEBUG_STREAM << resource->resourceName << ": Animation" << std::flush;
break;
case ResourceDescription::SubsystemModelStreamResourceType:
DEBUG_STREAM << resource->resourceName << ": Stream of "
<< *(int*)resource->resourceAddress << " Subsystems";
break;
case ResourceDescription::GaugeImageStreamResourceType:
DEBUG_STREAM << resource->resourceName << ": GaugeImage" << std::flush;
break;
case ResourceDescription::ControlMappingStreamResourceType:
DEBUG_STREAM << resource->resourceName << ": Stream of "
<< *(int*)resource->resourceAddress << " ControlsMappings";
break;
case ResourceDescription::DamageZoneStreamResourceType:
DEBUG_STREAM << resource->resourceName << ": Stream of "
<< *(int*)resource->resourceAddress << " DamageZones";
break;
case ResourceDescription::SkeletonStreamResourceType:
DEBUG_STREAM << resource->resourceName << ": Stream of "
<< *(int*)resource->resourceAddress << " Segments";
break;
case ResourceDescription::EnvironmentZoneResourceType:
DEBUG_STREAM << resource->resourceName << ": EnvironmentZone" << std::flush;
break;
case ResourceDescription::InterestZoneResourceType:
DEBUG_STREAM << resource->resourceName << ": InterestZone" << std::flush;
break;
case ResourceDescription::VehicleTableResourceType:
DEBUG_STREAM << resource->resourceName << ": VehicleTable" << std::flush;
break;
case ResourceDescription::ExistanceBoxStreamResourceType:
DEBUG_STREAM << resource->resourceName << ": Stream of "
<< resource->resourceSize/sizeof(ExtentBox)
<< " Existance boxes";
break;
case ResourceDescription::CameraStreamResourceType:
DEBUG_STREAM << resource->resourceName << ": Stream of "
<< *(int*)resource->resourceAddress << " Cameras";
break;
case ResourceDescription::DamageLookupTableStreamResourceType:
DEBUG_STREAM << resource->resourceName << ": DamageZoneLookupTable" << std::flush;
break;
case ResourceDescription::ExplosionTableStreamResourceType:
DEBUG_STREAM << resource->resourceName << ": ExplosionTable" << std::flush;
break;
default:
DEBUG_STREAM << "Unknown type " << resource->resourceType << std::flush;
break;
}
}
//#############################################################################
int
ApplicationTool::ListResources(
int argc,
char **argv
)
{
if (argc < 2)
{
DEBUG_STREAM << "Usage: listXX resource.res [res2.res ...]\n" << std::flush;
return 1;
}
for (int i=1; i < argc; ++i)
{
char res_name[200];
strcpy(res_name, argv[i]);
StreamableResourceFile
file;
file.Open(res_name);
DEBUG_STREAM << res_name << ": v" << (int)file.versionArray[0] << '.'
<< (int)file.versionArray[1] << '.' << (int)file.versionArray[2] << '.'
<< (int)file.versionArray[3] << std::endl;
if (file.labOnly == 1)
{
DEBUG_STREAM << "LAB ONLY RESOURCE!" << std::endl << std::flush;
}
DEBUG_STREAM << std::endl << std::flush;
DEBUG_STREAM << "ID# Size Data\n" << std::flush;
DEBUG_STREAM << "---- ------ -----------------------------------------------------\n" << std::flush;
for (int j=0; j<=file.GetMaxResourceID(); ++j)
{
DEBUG_STREAM << std::setw(4) << j << " " << std::flush;
ResourceDescription *resource = file.FindResourceDescription(j);
if (resource)
{
DEBUG_STREAM << std::setw(6) << resource->resourceSize << " " << std::flush;
resource->Lock();
ListResource(resource);
resource->Unlock();
}
else
{
DEBUG_STREAM << " Not Used" << std::flush;
}
DEBUG_STREAM << std::endl << std::flush;
}
}
return 0;
}