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>
263 lines
10 KiB
C++
263 lines
10 KiB
C++
//===========================================================================//
|
|
// File: dmgtable.hpp //
|
|
// Project: BattleTech Brick: Entity Manager //
|
|
// Contents: Damage lookup table -- layered / pie-slice hit-location selector //
|
|
//---------------------------------------------------------------------------//
|
|
// Date Who Modification //
|
|
// -------- --- ---------------------------------------------------------- //
|
|
// --/--/95 ?? Initial coding. //
|
|
//---------------------------------------------------------------------------//
|
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
|
|
// PROPRIETARY AND CONFIDENTIAL //
|
|
//===========================================================================//
|
|
//
|
|
// RECONSTRUCTED from the shipped binary. No header survived for this module
|
|
// (dmgtable.hpp/.cpp). The class cluster lives at @0x49de14..@0x49f5fb, was
|
|
// found ADJACENT to (but separate from) the DamageZone code in mechdmg.cpp,
|
|
// and is streamed from the ".tbl" notation file by the mech's
|
|
// CreateDamageZoneStream (@0x4a474c, mech.cpp) -- caller sites @0x49ed28 is
|
|
// invoked from the per-zone parser @line~11580 and the streamed form @0x49ea48
|
|
// from the object-stream reader @line~10423.
|
|
//
|
|
// Identifying strings (data @0x50bc00..0x50bd00):
|
|
// "DamageZone" "video" "skeleton" "%s %f" "RotateWithTorso" "PieSlice"
|
|
// "Layer" "TableEntryOf" "DamageZone Table Format!" "Not Found!"
|
|
// "percentages do not sum to 100%(i.e., 1.0)!" "missing RotateWithTorso!"
|
|
// "error in damage table format" " has bad format!"
|
|
// vtables: 0x50bd84 (DamageLookupTable), 0x50bd90 (PieSlice),
|
|
// 0x50bd9c (DamageZonePercentTable / leaf).
|
|
//
|
|
// The table is a three-level hit-location resolver:
|
|
//
|
|
// DamageLookupTable -- stack of armour LAYERS (penetration depth)
|
|
// -> PieSlice -- an angular sector wheel (optionally torso-relative)
|
|
// -> DamageZonePercentTable
|
|
// -- a weighted list of {zone segment index, percentage}
|
|
// whose percentages must sum to 1.0; a uniform random
|
|
// roll then selects the final damage-zone index.
|
|
//
|
|
// Class names are reconstructed from the .tbl section keywords; member names
|
|
// are inferred from usage and flagged best-effort. See dmgtable.cpp for the
|
|
// per-method @ADDR evidence.
|
|
//
|
|
|
|
#if !defined(DMGTABLE_HPP)
|
|
# define DMGTABLE_HPP
|
|
|
|
#if !defined(PLUG_HPP)
|
|
# include <plug.hpp>
|
|
#endif
|
|
#if !defined(TABLE_HPP)
|
|
# include <table.hpp>
|
|
#endif
|
|
|
|
#include "mechrecon.hpp" // reconstruction shim
|
|
|
|
//##################### Forward Class Declarations #######################
|
|
class Mech;
|
|
class NotationFile;
|
|
class GenericObjectStream;
|
|
class Point3D;
|
|
|
|
//###########################################################################
|
|
//##################### DamageZonePercentTable ##########################
|
|
//###########################################################################
|
|
//
|
|
// Leaf node. Holds a sorted, cumulative-weighted list of damage-zone
|
|
// segment indices and the uniform random "roll" that turns a [0,1) draw into
|
|
// one of those zones. Built from a ".tbl" "DamageZone" page whose lines are
|
|
// "<zoneName> <percentage>" ("%s %f") and whose percentages must total 1.0.
|
|
// (vtable @0x50bd9c, ctor @0x49deb0, dtor @0x49df80, object size 0x2c.)
|
|
//
|
|
class DamageZonePercentTable:
|
|
public Plug
|
|
{
|
|
public:
|
|
//
|
|
// Stream constructor -- rebuilds the entry list from an object stream.
|
|
// @0x49deb0 (FUN_004178cc base Plug ctor, then ReadEntries @0x49e5e4)
|
|
//
|
|
DamageZonePercentTable() {} // reconstruction: stack-temp default ctor
|
|
DamageZonePercentTable(
|
|
Mech *owner,
|
|
GenericObjectStream *stream
|
|
);
|
|
~DamageZonePercentTable(); // @0x49df80
|
|
|
|
Logical
|
|
TestInstance() const; // @0x49e000 (returns True)
|
|
|
|
//
|
|
// @0x49de14 -- uniform random weighted roll. Walks the entries in
|
|
// ascending cumulative-percentage order, draws a [0,1) sample
|
|
// (FUN_00408050) and returns the segment index (entry+0xc) of the first
|
|
// entry whose cumulative threshold (entry+0x14) exceeds the draw.
|
|
// Returns -1 if the list is empty.
|
|
//
|
|
int
|
|
SelectZone() const;
|
|
|
|
//
|
|
// @0x49e00c -- parse a "DamageZone" page from the model .tbl: read the
|
|
// "video"/"skeleton" model, then each "<zone> <pct>" line; resolve the
|
|
// zone name to a skeleton segment index (Get_Segment_Index @0x4274f8),
|
|
// accumulate the percentages and verify the total == 1.0.
|
|
// Returns False (and logs) on a bad line, an unknown zone, or a total
|
|
// that does not sum to 1.0.
|
|
//
|
|
static int
|
|
BuildFromNotation(
|
|
NotationFile *model_file,
|
|
const char *model_name,
|
|
const char *page_name,
|
|
NotationFile *table_file,
|
|
Mech *owner,
|
|
GenericObjectStream *stream
|
|
);
|
|
|
|
//
|
|
// @0x49e524 WriteEntries (serialise count + {threshold,zone} pairs),
|
|
// @0x49e5e4 ReadEntries (deserialise into the entry table).
|
|
//
|
|
void WriteEntries(GenericObjectStream *stream) const; // @0x49e524
|
|
void ReadEntries (GenericObjectStream *stream); // @0x49e5e4
|
|
|
|
protected:
|
|
Mech *owner; // @0x0c param_1[3]
|
|
void *refCount; // @0x10 param_1[4] (MemoryBlock/RefCount)
|
|
ReconTable<int> entries; // @0x14 param_1+5 sorted by cumulative %
|
|
// each entry: zoneIndex@+0xc, cumulativePercent@+0x14
|
|
};
|
|
|
|
//###########################################################################
|
|
//############################# PieSlice ################################
|
|
//###########################################################################
|
|
//
|
|
// An angular sector "wheel". N equal-angle slices (each 2*PI/N radians) wrap
|
|
// a circle; the slice that contains the incoming hit direction is chosen and
|
|
// its leaf DamageZonePercentTable is rolled. When "RotateWithTorso" is set
|
|
// the incoming angle is measured relative to the live torso orientation
|
|
// (orientationSource+0x1d8), so the slice layout turns with the mech's torso.
|
|
// (vtable @0x50bd90, ctor @0x49e740, object size 0x30.)
|
|
//
|
|
class PieSlice:
|
|
public Plug
|
|
{
|
|
public:
|
|
//
|
|
// @0x49e740 -- stream constructor. Reads owner@+0xc and sliceCount@+0x2c,
|
|
// builds 'sliceCount' DamageZonePercentTable children (@0x49deb0) each
|
|
// keyed to a cumulative angle i*(2*PI/sliceCount), and caches the torso
|
|
// orientation source = owner+0x438.
|
|
//
|
|
PieSlice(
|
|
Mech *owner,
|
|
GenericObjectStream *stream
|
|
);
|
|
~PieSlice();
|
|
|
|
//
|
|
// @0x49e678 -- pick a slice for incoming angle 'theta' (radians). When
|
|
// rotateWithTorso is set, theta is shifted by the torso orientation
|
|
// (orientationSource+0x1d8) and wrapped into [0, 2*PI). The slice index
|
|
// = floor(theta * sliceCount / (2*PI)) feeds the child table lookup.
|
|
//
|
|
DamageZonePercentTable *
|
|
SelectSlice(Scalar theta) const;
|
|
|
|
//
|
|
// @0x49e88c -- parse a "PieSlice" page: read the mandatory bool
|
|
// "RotateWithTorso" (logged "missing RotateWithTorso!" if absent), then
|
|
// each named sub-page as a DamageZonePercentTable (@0x49e00c).
|
|
// Logs "error in damage table format" on a malformed entry.
|
|
//
|
|
static int
|
|
BuildFromNotation(
|
|
NotationFile *model_file,
|
|
const char *model_name,
|
|
const char *page_name,
|
|
NotationFile *table_file,
|
|
Mech *owner,
|
|
GenericObjectStream *stream
|
|
);
|
|
|
|
protected:
|
|
Mech *owner; // @0x0c param_1[3]
|
|
void *orientationSource; // @0x10 param_1[4] = owner+0x438 (torso)
|
|
ReconTable<DamageZonePercentTable*>
|
|
slices; // @0x14 param_1+5
|
|
int rotateWithTorso; // @0x28 (read first in BuildFromNotation)
|
|
int sliceCount; // @0x2c param_1[0xb]
|
|
};
|
|
|
|
//###########################################################################
|
|
//######################### DamageLookupTable ###########################
|
|
//###########################################################################
|
|
//
|
|
// Top-level table streamed from the ".tbl" key for a mech. A stack of armour
|
|
// LAYERS: the deeper a shot penetrates, the higher the layer index selected.
|
|
// Each layer is a PieSlice wheel. (vtable @0x50bd84, ctor @0x49ea48,
|
|
// dtor @0x49eadc, object size 0x30.)
|
|
//
|
|
class DamageLookupTable:
|
|
public Plug
|
|
{
|
|
public:
|
|
//
|
|
// @0x49ea48 -- stream constructor. Reads layerCount@+0x28 and builds
|
|
// that many PieSlice layers (@0x49e740).
|
|
//
|
|
DamageLookupTable(
|
|
Mech *owner,
|
|
GenericObjectStream *stream
|
|
);
|
|
~DamageLookupTable(); // @0x49eadc
|
|
|
|
Logical
|
|
TestInstance() const; // @0x49eb48 / @0x49e880 (returns True)
|
|
|
|
//
|
|
// @0x49eb54 -- resolve a hit to a damage zone. Transforms the impact
|
|
// point into the mech frame (owner+0xd0), derives a penetration-depth
|
|
// fraction (clamped against the armour reference at owner+0x2ec[+0xc]),
|
|
// maps it to a layer index, fetches that layer's PieSlice and forwards
|
|
// an incident angle (computed from the hit direction, or a random draw
|
|
// when the direction is degenerate) to PieSlice::SelectSlice (@0x49e678).
|
|
//
|
|
void
|
|
ResolveHit(const Point3D &impact) const;
|
|
|
|
//
|
|
// @0x49ed28 -- parse the layered table. Reads consecutive "Layer1",
|
|
// "Layer2", ... pages (number suffix via FUN_004dd3ec), parsing each as
|
|
// a PieSlice page (@0x49e88c). Logs "<name> has bad format!" when no
|
|
// layers are found.
|
|
//
|
|
static int
|
|
BuildFromNotation(
|
|
NotationFile *model_file,
|
|
const char *model_name,
|
|
NotationFile *table_file,
|
|
Mech *owner,
|
|
GenericObjectStream *stream
|
|
);
|
|
|
|
protected:
|
|
Mech *owner; // @0x0c param_1[3]
|
|
ReconTable<PieSlice*>
|
|
layers; // @0x10 param_1+4
|
|
int layerCount; // @0x28 param_1[10]
|
|
};
|
|
|
|
//###########################################################################
|
|
// Tool support -- entry comparator
|
|
//
|
|
// @0x49f301 -- orders table entries by their cumulative threshold (+0x14)
|
|
// within an epsilon (_DAT_0049f369 ~= 1.0e-4). Used to keep the entry tables
|
|
// sorted so the SelectZone / SelectSlice scans are monotone.
|
|
//
|
|
int
|
|
CompareTableEntryThreshold(const void *a, const void *b);
|
|
|
|
#endif
|