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

988 lines
22 KiB
C++

#include "munga.h"
#pragma hdrstop
#include "tool.h"
#include "fileutil.h"
#include "eyecandy.h"
#include "dropzone.h"
#include "terrain.h"
#include "cultural.h"
#include "doorfram.h"
#include "camship.h"
#include "boxsolid.h"
#include "team.h"
#include "namelist.h"
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Logical
ApplicationTool::CreateMakeMessage(
const char* class_name,
Entity::MakeMessage *creation_message,
NotationFile *model_file,
const ResourceDirectories *directories
)
{
if (!stricmp(class_name,"TerrainClassID"))
{
Terrain::CreateMakeMessage(
(Terrain::MakeMessage*)creation_message,
model_file,
directories
);
return True;
}
else if (!stricmp(class_name,"UnscalableTerrainClassID"))
{
UnscalableTerrain::CreateMakeMessage(
(UnscalableTerrain::MakeMessage*)creation_message,
model_file,
directories
);
return True;
}
else if (!stricmp(class_name,"CulturalIconClassID"))
{
CulturalIcon::CreateMakeMessage(
(CulturalIcon::MakeMessage*)creation_message,
model_file,
directories
);
return True;
}
else if (!stricmp(class_name,"LandmarkClassID"))
{
Landmark::CreateMakeMessage(
(Landmark::MakeMessage*)creation_message,
model_file,
directories
);
return True;
}
else if (!stricmp(class_name,"DoorFrameClassID"))
{
DoorFrame::CreateMakeMessage(
(DoorFrame::MakeMessage*)creation_message,
model_file,
directories
);
return True;
}
else if (!stricmp(class_name,"EyeCandyClassID"))
{
EyeCandy::CreateMakeMessage(
(EyeCandy::MakeMessage*)creation_message,
model_file,
directories
);
return True;
}
else if (!stricmp(class_name, "TeamClassID"))
{
Team::CreateMakeMessage(
(Team::MakeMessage*)creation_message,
model_file,
directories
);
return True;
}
//
//-----------------------------------------------
// Not a class MUNGA knows about, so return False
//-----------------------------------------------
//
return False;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Logical
ApplicationTool::WriteMaps(
char *map_name,
NotationFile *notation_file,
NotationFile *ops_file,
StreamableResourceFile *file,
const ResourceDirectories *resource_directories,
int resource_id
)
{
const char
*entry_data, *type, *ops_name;
char *p;
char
short_map_name[15],
ops_file_page[100],
ops_file_data[100],
temp[100];
Str_Copy(short_map_name, map_name, 15);
p = short_map_name;
p += strlen(short_map_name) - 4;
*p = '\0';
//----------------------
// Write ops_file stuff
//----------------------
ops_file->GetEntry("resource", "res name", &ops_name);
strcpy(ops_file_data, ops_name);
strcat(ops_file_data, "::Standard::Location::");
strcat(ops_file_data, short_map_name );
strcpy(ops_file_page, ops_name);
strcat(ops_file_page, "::Standard::LocationList");
ops_file->AppendEntry(ops_file_page, "location", ops_file_data);
ops_file->SetEntry(ops_file_data, "tag", short_map_name );
ops_file->AppendEntry(ops_file_data, NULL, (char *)NULL);
//
//-----------------------------------------------
// Get exsitance box stream from notation file
// named after map then save it to the resource
// Not required yet!
//-----------------------------------------------
//
strcpy(temp, short_map_name);
strcat(temp, ".xst");
char *filename = MakePathedFilename(
resource_directories->collisionDirectory,
temp);
Register_Pointer(filename);
NotationFile *exist_notation_file = new NotationFile(filename);
Register_Object(exist_notation_file);
Unregister_Pointer(filename);
delete filename;
labOnly |= exist_notation_file->IsMarkedLabOnly();
if (exist_notation_file->PageCount())
{
int size;
ExtentBox *existance_box = MakeExistanceBoxStream(exist_notation_file, &size);
Register_Pointer(existance_box);
file->AddResource(
short_map_name,
ResourceDescription::ExistanceBoxStreamResourceType,
1,
ResourceDescription::Preload,
existance_box,
size * sizeof(ExtentBox)
);
Unregister_Pointer(existance_box);
delete[] existance_box;
}
Unregister_Object(exist_notation_file);
delete exist_notation_file;
//---------------------
// Make camera stream
//---------------------
CameraInstanceManager::CreateCameraInstancesStream(
file,
map_name,
resource_directories
);
//
//--------------------------------------------------------
// Get the name and types of all the pages in the map file
//--------------------------------------------------------
//
NameList *namelist = notation_file->MakePageList();
Register_Object(namelist);
NameList::Entry *entry = namelist->GetFirstEntry();
Logical model_failed = False;
char model_name[100];
while (entry)
{
entry_data = entry->GetName();
if (!stricmp(entry_data, "LAB_ONLY"))
{
labOnly = True;
entry = entry->GetNextEntry();
continue;
}
else if(!notation_file->GetEntry(entry_data, "type", &type))
{
std::cout << "No Type in " << entry_data << std::endl;
Unregister_Object(namelist);
delete namelist;
return False;
}
//
//----------------------------
// Read in a model description
//----------------------------
//
char scenario_name[100];
if (!stricmp(type, "model"))
{
strcpy(model_name, entry_data);
if(
!notation_file->GetEntry(
model_name,
"modelfile",
&entry_data
)
)
{
std::cout << "No modelfile in " << model_name << std::endl;
Unregister_Object(namelist);
delete namelist;
return False;
}
ResourceDescription *res = file->FindResourceDescription(
model_name,
ResourceDescription::ModelListResourceType
);
if (!res)
{
char *filename =
MakePathedFilename(
resource_directories->modelDirectory,
entry_data
);
Register_Pointer(filename);
NotationFile *new_notation_file = new NotationFile(filename);
Register_Object(new_notation_file);
labOnly |= new_notation_file->IsMarkedLabOnly();
if (new_notation_file->PageCount())
{
if (
!WriteModels(
new_notation_file,
ops_file,
file,
model_name,
resource_directories
)
)
{
std::cout << "Model Failed : " << filename << std::endl;
model_failed = True;
}
}
else
{
std::cout << "Model File : " << filename << " does not exist!\n";
model_failed = True;
}
Unregister_Pointer(filename);
delete filename;
Unregister_Object(new_notation_file);
delete new_notation_file;
}
}
else if (!stricmp(type, "dropzone"))
{
#if 0
strcpy(model_name, entry_data);
ResourceDescription *res =
file->FindResourceDescription(
model_name,
ResourceDescription::ModelListResourceType
);
if (!res)
{
//
// make drop zone name be blah.name
//
char dropzone_name[32];
Str_Copy(dropzone_name, short_map_name, sizeof(dropzone_name));
Str_Cat(dropzone_name, ".", sizeof(dropzone_name));
Str_Cat(dropzone_name, model_name,sizeof(dropzone_name));
strcpy(ops_file_data, ops_name);
strcat(ops_file_data, "::");
strcat(ops_file_data, short_map_name);
strcat(ops_file_data, "::DropZone::" );
strcat(ops_file_data, dropzone_name);
strcpy(ops_file_page, ops_name);
strcat(ops_file_page, "::");
strcat(ops_file_page, short_map_name );
strcat(ops_file_page, "::DropZoneList");
ops_file->AppendEntry(ops_file_page, "DropZone", ops_file_data);
ops_file->SetEntry(ops_file_data, "tag", dropzone_name );
ops_file->SetEntry(ops_file_data, "classID", "DpZn");
ops_file->AppendEntry(ops_file_data, NULL, (char *)NULL);
ResourceDescription::ResourceID res_number =
DropZone::CreateModelResource(
file,
model_name,
notation_file,
dropzone_name,
resource_directories
);
if (res_number != ResourceDescription::NullResourceID)
{
file->AddResourceList(
dropzone_name,
ResourceDescription::ModelListResourceType,
1,
ResourceDescription::Preload,
&res_number,
1
);
}
else
{
model_failed = True;
}
}
#endif
}
else if (!stricmp(type, "include"))
{
strcpy(scenario_name, entry_data);
NameList *namelist2 =
notation_file->MakeEntryList(entry_data, "file");
Register_Object(namelist2);
NameList::Entry *entry2 = namelist2->GetFirstEntry();
while(entry2 != NULL)
{
const char *entry2_data = entry2->GetChar();
char *filename =
MakePathedFilename(
resource_directories->mapDirectory,
entry2_data
);
Register_Pointer(filename);
NotationFile *new_notation_file = new NotationFile(filename);
Register_Object(new_notation_file);
labOnly |= new_notation_file->IsMarkedLabOnly();
//
// Write out drop zones....
// Write out maps...
//
char short_name[100];
strcpy( short_name, StripDirectory(entry2_data) );
if (new_notation_file->PageCount())
{
if(
!WriteMaps(
short_name,
new_notation_file,
ops_file,
file,
resource_directories
)
)
{
std::cout << "Include Failed : " << filename << std::endl;
model_failed = True;
}
}
else
{
std::cout << "Include File : " << filename << " does not exist\n";
model_failed = True;
}
Unregister_Pointer(filename);
delete filename;
Unregister_Object(new_notation_file);
delete new_notation_file;
entry2 = entry2->GetNextEntry();
}
Unregister_Object(namelist2);
delete namelist2;
}
else if (!stricmp(type, "map"))
{
if (model_failed == False)
{
//
// Check to see that map is not already in resource file
//
ResourceDescription *test = file->FindResourceDescription(
short_map_name, ResourceDescription::MakeMessageStreamResourceType);
BoxedSolidList *box_solid_list = new BoxedSolidList;
Register_Object(box_solid_list);
if ( test == NULL)
{
NameList *namelist2 =
notation_file->MakeEntryList(entry_data, "instance");
Register_Object(namelist2);
NameList::Entry *entry2 = namelist2->GetFirstEntry();
//
//----------------
// Count instances
//----------------
//
int instance_count;
for (instance_count=0; entry2; ++instance_count)
{
entry2 = entry2->GetNextEntry();
}
entry2 = namelist2->GetFirstEntry();
//
//-------------------------------------------------------
// make a buffer to hold all these messages, and have the
// beginning store the number of messages
//-------------------------------------------------------
//
int streamSize = instance_count * 4 * sizeof(Entity::MakeMessage);
Entity::MakeMessage *stream = (Entity::MakeMessage*)new char[streamSize];
memset(stream, 0, streamSize);
MemoryStream m(stream, streamSize);
*(int*)m.GetPointer() = instance_count;
m.AdvancePointer(sizeof(int));
EulerAngles angular_position;
Degree value;
while (entry2)
{
char find_me[100];
strcpy(find_me, entry2->GetChar());
char *p = find_me;
char *token = strtok(p,White_Space);
ResourceDescription *res = NULL;
if (!stricmp( entry2->GetName(), "instancedropzone"))
{
char dropzone_name[32];
#if 0
Str_Copy(dropzone_name, short_map_name, sizeof(dropzone_name));
Str_Cat(dropzone_name, ".", sizeof(dropzone_name));
Str_Cat(dropzone_name,token,sizeof(dropzone_name));
#else
Str_Copy(dropzone_name,token,sizeof(dropzone_name));
#endif
DropZone::CreateMakeMessage(
(DropZone::MakeMessage*)m.GetPointer(),
token,
dropzone_name,
notation_file
);
}
else
{
notation_file->GetEntry(token,"type",&type);
if(!stricmp(type, "include"))
{
res =
file->FindResourceDescription(
token,
ResourceDescription::MakeMessageStreamResourceType
);
}
else
{
res =
file->FindResourceDescription(
token,
ResourceDescription::ModelListResourceType
);
}
if (!res)
{
std::cout << "Instance Not Found : " << token << std::endl;
Unregister_Object(namelist2);
Unregister_Pointer(stream);
Unregister_Object(namelist);
delete namelist2;
delete[] stream;
delete namelist;
return False;
}
}
//
//--------------
// Next instance
//--------------
//
//
// If it's not a drop zone
//
if (!stricmp( entry2->GetName(), "instancedropzone"))
{
// do nothing this is already done.
}
else
{
notation_file->GetEntry(token,"type",&type);
if(!stricmp(type, "include"))
{
char name[100];
Str_Copy(name, token, 100);
ResourceDescription *map_res = file->FindResourceDescription(
name, ResourceDescription::MakeMessageStreamResourceType);
if (!map_res)
{
std::cout << "CreateMakeMapMessage Failed To Find Map : " << name << std::endl;
Unregister_Object(namelist2);
Unregister_Pointer(stream);
Unregister_Object(namelist);
delete namelist2;
delete[] stream;
delete namelist;
return False;
}
if (
!Entity::CreateMakeMapMessage(
(Entity::MakeMapMessage*)m.GetPointer(),
map_res->resourceID,
notation_file,
resource_directories
)
)
{
std::cout << "CreateMakeMapMessage Failed : " << type << std::endl;
Unregister_Object(namelist2);
Unregister_Pointer(stream);
Unregister_Object(namelist);
delete namelist2;
delete[] stream;
delete namelist;
return False;
}
}
else
{
if (!notation_file->GetEntry(token,"modelfile",&type))
{
std::cout << token << " has no modelfile specification\n";
Unregister_Object(namelist2);
Unregister_Pointer(stream);
Unregister_Object(namelist);
delete namelist2;
delete[] stream;
delete namelist;
return False;
}
char *filename3 =
MakePathedFilename(
resource_directories->modelDirectory,
type
);
Register_Pointer(filename3);
NotationFile *new_notation_file =
new NotationFile(filename3);
Register_Object(new_notation_file);
labOnly |= new_notation_file->IsMarkedLabOnly();
Unregister_Pointer(filename3);
delete filename3;
if (!new_notation_file->PageCount())
{
std::cout << filename3 << " does not exist!";
Unregister_Object(namelist2);
Unregister_Pointer(stream);
Unregister_Object(namelist);
delete namelist2;
delete[] stream;
delete namelist;
return False;
}
if(!new_notation_file->GetEntry("gamedata","class",&type))
{
std::cout << "gamedata not found\n";
Unregister_Object(namelist2);
Unregister_Pointer(stream);
Unregister_Object(namelist);
delete namelist2;
delete[] stream;
delete namelist;
return False;
}
if (
!CreateMakeMessage(
type,
(Entity::MakeMessage*)m.GetPointer(),
new_notation_file,
resource_directories
)
)
{
std::cout << "Invalid gamedata type : " << type << std::endl;
Unregister_Object(namelist2);
Unregister_Pointer(stream);
Unregister_Object(namelist);
delete namelist2;
delete[] stream;
delete namelist;
return False;
}
Unregister_Object(new_notation_file);
delete new_notation_file;
}
}
Entity::MakeMessage *message =
(Entity::MakeMessage*)m.GetPointer();
if (
!(message->messageFlags
& Entity::MakeMessage::MapStreamMarkerFlag)
)
{
message->resourceID =
(res) ?
res->resourceID
: ResourceDescription::NullResourceID;
message->ownerID = MapHostID;
message->interestZoneID = 0;
}
m.AdvancePointer(message->messageLength);
entry2 = entry2->GetNextEntry();
}
if (resource_id == -1)
{
file->AddResource(
short_map_name,
ResourceDescription::MakeMessageStreamResourceType,
1,
ResourceDescription::Preload,
stream,
(char*)m.GetPointer() - (char*)stream
);
}
else
{
//
// Check for resource_id , if there Fail.
//
ResourceDescription *res_des =
file->FindResourceDescription(resource_id);
if (!res_des)
{
file->AddResource(
short_map_name,
ResourceDescription::MakeMessageStreamResourceType,
1,
ResourceDescription::Preload,
stream,
(char*)m.GetPointer() - (char*)stream,
resource_id
);
}
else
{
DEBUG_STREAM << short_map_name << " : " << resource_id << std::endl << std::flush;
Fail("Duplicate Resource Number");
}
}
Unregister_Pointer(stream);
delete[] stream;
Unregister_Object(namelist2);
delete namelist2;
}
Unregister_Object(box_solid_list);
delete box_solid_list;
}
else
{
Unregister_Object(namelist);
delete namelist;
return False;
}
}
else
{
std::cout << "Error: unknown type " << type << "!\n";
Unregister_Object(namelist);
delete namelist;
return False;
}
entry = entry->GetNextEntry();
}
strcpy(ops_file_page, ops_name);
strcat(ops_file_page, "::");
strcat(ops_file_page, short_map_name );
strcat(ops_file_page, "::DropZoneList");
ops_file->AppendEntry(ops_file_page, NULL, (char *)NULL);
Unregister_Object(namelist);
delete namelist;
return True;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// This does NOT write the maps to the ops file, Write maps does that.
// This just finishes the ops file up
//
void
ApplicationTool::WriteOpsFile(
NotationFile *ops_file,
StreamableResourceFile *file,
const ResourceDirectories *resource_directories
)
{
//
//--------------------------------------
// open vehicle table and write out info
//--------------------------------------
//
const char *ops_name;
ops_file->GetEntry("resource", "res name", &ops_name);
char *filename3 =
MakePathedFilename(resource_directories->modelDirectory, "vehicle.tbl");
Register_Pointer(filename3);
NotationFile *new_notation_file = new NotationFile(filename3);
Register_Object(new_notation_file);
//
// Kill if file isn't there.
//
if (!new_notation_file->PageCount())
{
std::cout << "vehicle.tbl does not exist in the resource directory!\n" << std::flush;
Unregister_Pointer(filename3);
delete filename3;
PostQuitMessage(AbortExitCodeID);
}
Unregister_Pointer(filename3);
delete filename3;
NameList *list = new_notation_file->MakeEntryList("color");
Register_Object(list);
NameList::Entry *entry = list->GetFirstEntry();
char ops_file_data[100];
char ops_file_page[100];
while(entry)
{
if(entry->GetChar() != NULL)
{
strcpy(ops_file_data, ops_name);
strcat(ops_file_data, "::Standard::Color::");
strcat(ops_file_data, entry->GetName() );
strcpy(ops_file_page, ops_name);
strcat(ops_file_page, "::Standard::ColorList");
ops_file->AppendEntry(ops_file_page, "color", ops_file_data);
ops_file->SetEntry(ops_file_data, "tag", entry->GetName() );
ops_file->AppendEntry(ops_file_data, NULL, (char *)NULL);
}
entry = entry->GetNextEntry();
}
ops_file->AppendEntry(ops_file_page, NULL, (char *)NULL);
Unregister_Pointer(list);
delete list;
list = new_notation_file->MakeEntryList("badge");
Register_Object(list);
entry = list->GetFirstEntry();
while(entry)
{
if(entry->GetChar() != NULL)
{
strcpy(ops_file_data, ops_name);
strcat(ops_file_data, "::Standard::Badge::");
strcat(ops_file_data, entry->GetName() );
strcpy(ops_file_page, ops_name);
strcat(ops_file_page, "::Standard::BadgeList");
ops_file->AppendEntry(ops_file_page, "badge", ops_file_data);
ops_file->SetEntry(ops_file_data, "tag", entry->GetName() );
ops_file->AppendEntry(ops_file_data, NULL, (char *)NULL);
}
entry = entry->GetNextEntry();
}
ops_file->AppendEntry(ops_file_page, NULL, (char *)NULL);
Unregister_Pointer(list);
delete list;
//
// Put spaces at the end of vehicles and maps
// that were defined elsewere.
//
char temp[30];
strcpy(temp, ops_name);
strcat(temp, "::Standard::LocationList");
ops_file->AppendEntry(temp, NULL, (char *)NULL);
strcpy(temp, ops_name);
strcat(temp, "::Standard::VehicleList");
ops_file->AppendEntry(temp, NULL, (char *)NULL);
//
// Add notation file to resource.
//
long size;
size = new_notation_file->SizeOfText();
Verify( size >= 0 );
#define EDO_DEBUG False
#if EDO_DEBUG
Dump( size );
#endif
char *notation_string = new char[size];
Register_Pointer(notation_string);
#if DEBUG_LEVEL>0
long actual_size;
actual_size =
#endif
new_notation_file->WriteText(notation_string, size);
#if EDO_DEBUG
{
ofstream edo_debug;
edo_debug.open("DEBUGRES.EDO",std::ios::out);
if (!edo_debug) { Fail("can't open DEBUGRES.EDO"); }
const char
*whip = notation_string,
*stop = whip + size;
std::cout << "writing DEBUGRES.EDO" << std::endl;
while (whip < stop)
{
edo_debug << "|" << whip << "|" << std::endl;
whip = strchr(whip, '\0') + 1;
}
edo_debug << "<EOF>" << std::endl;
edo_debug.close();
}
#endif
Verify( actual_size == size );
file->AddResource(
"vehicletable",
ResourceDescription::VehicleTableResourceType,
1,
ResourceDescription::Preload,
notation_string,
sizeof(char) * size
);
Unregister_Pointer(notation_string);
Unregister_Object(new_notation_file);
delete[] notation_string;
delete new_notation_file;
}