//===========================================================================// // File: dmgtable.cpp // // 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. Behaviour follows the Ghidra // pseudo-C for the cluster @0x49de14..@0x49f5fb. No function in the cluster // was linker-tagged (all file=?), so the module name "dmgtable.cpp" is // inferred from CLASSMAP and the .tbl section keywords; the cluster was // recovered via the DamageZone (mechdmg.cpp) adjacency, the vtables // @0x50bd84/0x50bd90/0x50bd9c, and the assertion / format strings // @0x50bc00..0x50bd00. Each non-trivial method cites the originating @ADDR. // // Float constants converted to decimal: // _DAT_0049e520 = 0x3f800000 = 1.0f (percentage-sum target) // _DAT_0049e72c = 0x40c90fdb = 6.2831853f = 2*PI (angular wrap) // _DAT_0049e810 = 0x40c90fdb = 2*PI (slice-angle span) // _DAT_0049e730 = 0.0f (angle floor) // _DAT_0049f369 = 0x38d1b717 ~= 1.0e-4f (sort epsilon) // // Helper-function name mapping (engine internals referenced by the decomp): // FUN_004178cc Plug ctor FUN_0041790c Plug::Clear // FUN_00417858 Plug::Detach // FUN_00402298 operator new FUN_004022d0 operator delete // FUN_004023f4 MemoryBlock/RefCount ctor FUN_004024d8 RefCount release // FUN_00402460 TextString ctor // FUN_00408050 RandomUnit() -> Scalar in [0,1) // FUN_004dcd10 (float)floor() setup FUN_004dcd94 lrint()/truncate // FUN_004dcd00 fabsf() // FUN_004dd3ec itoa(value, buf, radix) // FUN_004274f8 Get_Segment_Index(skeletonModel, zoneName) // FUN_00404088 NotationFile::Read(name,key,char**) // FUN_00404118 NotationFile::Read(name,key,float*) // FUN_00404190 NotationFile::Read(name,key,int*) // FUN_00404720 NotationFile::GetPage(name,key) // FUN_00403f84 NotationFile::PageExists(name) // FUN_004d7f84 strcmp() / format match FUN_004d4b58 stricmp() // FUN_004dbb24 DebugStream::operator<<(char*) FUN_004d9c38 endl // The TableOf<>/PlugOf<> template slots (ctor/dtor/iterator) live at // @0x49f204..@0x49f5b8 (vtables 0x50bd30/0x50bd50/0x50bd64) and are // engine template instantiations, not damage-table logic. // #include #pragma hdrstop #if !defined(DMGTABLE_HPP) # include #endif #if !defined(MECH_HPP) # include #endif // // Tuning constants observed as read-only globals in the decomp. // static const Scalar PercentSumTarget = 1.0f; // _DAT_0049e520 static const Scalar TwoPi = 6.2831853f; // _DAT_0049e72c / _DAT_0049e810 static const Scalar SortEpsilon = 1.0e-4f; // _DAT_0049f369 // Per-entry byte layout inside the sorted TableOf entry pool: // entry+0x0c int zone segment index (payload returned by the roll) // entry+0x14 Scalar cumulative percentage (ascending sort key) //########################################################################### //########################################################################### // DamageZonePercentTable (leaf) //########################################################################### //########################################################################### // // @0x49deb0 -- stream constructor. Lays down the vtable @0x50bd9c, stores the // owner (param_1[3]), allocates the entry-table memory block (param_1[4]), // constructs the empty TableOf at +0x14 (FUN_0049f204), then rebuilds the // entries from the object stream (ReadEntries @0x49e5e4). // DamageZonePercentTable::DamageZonePercentTable( Mech *owner, GenericObjectStream *stream ): Plug() // FUN_004178cc(this, 2) { this->owner = owner; // param_1[3] refCount = NewRefCount(0x10); // param_1[4] entries.Init(); // FUN_0049f204(this+5, 0, 0) ReadEntries(stream); // FUN_0049e5e4 } // // @0x49df80 -- detaches the entry table, releases the memory block, clears the // Plug base. // DamageZonePercentTable::~DamageZonePercentTable() { // *this = &PTR_FUN_0050bd9c; Detach(entries); release(refCount); Plug::Clear } Logical DamageZonePercentTable::TestInstance() const // @0x49e000 { return True; } // // @0x49de14 -- the weighted random roll (the "selector"). Iterates the entry // table in ascending cumulative order, draws a single uniform sample in [0,1) // and returns the zone index of the first entry whose cumulative percentage // exceeds the draw. -1 when the table is empty. // int DamageZonePercentTable::SelectZone() const { int selected = -1; Scalar roll = RandomUnit(); // FUN_00408050 ReconTableIter it(entries); // FUN_0040139b(.,this+0x14) it.First(); // slot+4 for (void *entry = it.Current(); entry != 0 && selected == -1; entry = it.Current()) { Scalar threshold = *(Scalar *)((char *)entry + 0x14); // cumulative % if (roll < threshold) { selected = *(int *)((char *)entry + 0xc); // zone segment index } it.Next(); // slot+0xc // it.Current() re-read at slot+0x30 (end test) } return selected; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Object-stream I/O // // @0x49e524 -- write count, then each entry as {cumulativePercent, zoneIndex}. // void DamageZonePercentTable::WriteEntries(GenericObjectStream *stream) const { stream->Write(&entries.count, 4); ReconTableIter it(entries); for (void *entry = it.First(); entry != 0; entry = it.Next()) { stream->Write((char *)entry + 0x14, 4); // cumulative percentage stream->Write((char *)entry + 0x0c, 4); // zone index } } // // @0x49e5e4 -- read count, then that many {cumulativePercent, zoneIndex} pairs, // allocating a TableEntry (FUN_00424716) per pair and inserting into the table. // void DamageZonePercentTable::ReadEntries(GenericObjectStream *stream) { int n; stream->Read(&n, 4); for (int i = 0; i < n; ++i) { int zoneIndex; Scalar cumulative; stream->Read(&cumulative, 4); stream->Read(&zoneIndex, 4); entries.Insert(NewTableEntry(zoneIndex), &cumulative); // FUN_00424716 + slot+8 } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // BuildFromNotation -- DamageZonePercentTable (@0x49e00c) // // Parses a "DamageZone" page and validates that the percentages total 1.0. // int DamageZonePercentTable::BuildFromNotation( NotationFile *model_file, const char *model_name, const char *page_name, NotationFile *table_file, Mech *owner, GenericObjectStream *stream ) { if (!model_file->PageExists(page_name)) // FUN_00403f84 { DebugStream << page_name << " does not exist in " << model_file->GetFileName(); return False; } DamageZonePercentTable table; NotationLine *line = ReconGetPage(model_file, page_name, "DamageZone"); // FUN_00404720 + 0050bc00 // "video"/"skeleton" model -> resolve to a skeleton so zone names map to // segment indices. const char *skeletonName = 0; model_file->GetEntry("video", "skeleton", &skeletonName); // FUN_00404088 (0050bc0b/0050bc11) Skeleton *skeleton = LoadSkeleton(ReconDir(table_file), skeletonName); if (skeleton->SegmentCount() == 0) { DebugStream << skeletonName << " is empty or missing!"; // 0050bc1a return False; } Scalar runningTotal = 0.0f; for (; line != 0; line = line->Next()) { // every line must be " " ("%s %f") if (strcmp(line->Format(), "%s %f") != 0) // FUN_004d7f84 / 0050bc30 { DebugStream << model_name << " " << model_file->GetFileName() << ": " << page_name << " error in DamageZone Table Format!"; // 0050bc3b return False; // -> cleanup @LAB_0049e1a9 } char zoneName[64]; Scalar percentage; line->Parse(zoneName, &percentage); int segmentIndex = ReconSegIndex(skeleton, zoneName); // FUN_004274f8 if (segmentIndex == -1) { DebugStream << model_file->GetFileName() << " " << page_name << " " << zoneName << " Not Found!"; // 0050bc63 return False; } runningTotal += percentage; table.entries.Insert(NewTableEntry(segmentIndex), &runningTotal); // FUN_00424716 + slot+8 } if (runningTotal != PercentSumTarget) // _DAT_0049e520 == local_18 { DebugStream << model_file->GetFileName() << " " << page_name << ": percentages do not sum to 100%(i.e., 1.0)!"; // 0050bc72 return False; } table.WriteEntries(stream); // FUN_0049e524(param_6, &table) return True; } //########################################################################### //########################################################################### // PieSlice //########################################################################### //########################################################################### // // @0x49e740 -- stream constructor (vtable 0x50bd90). Reads owner@+0xc and the // slice count@+0x2c from the stream, then builds 'sliceCount' leaf tables, each // stamped with the cumulative slice angle i*(2*PI/sliceCount). Caches the // torso orientation source pointer = owner+0x438 (used by SelectSlice). // PieSlice::PieSlice( Mech *owner, GenericObjectStream *stream ): Plug() // FUN_004178cc(this, 2) { slices.Init(); // FUN_0049f2b2(this+5, 0, 1) stream->Read(&this->owner, 4); // param_1+3 (param_3 slot+0x1c) stream->Read(&sliceCount, 4); // param_1+0xb Scalar sliceSpan = TwoPi / (Scalar)sliceCount; // _DAT_0049e810 / count for (int i = 1; i <= sliceCount; ++i) { DamageZonePercentTable *slice = new DamageZonePercentTable(owner, stream); // FUN_0049deb0 (size 0x2c) Scalar cumulativeAngle = (Scalar)i * sliceSpan; slices.Insert(slice, &cumulativeAngle); // slot+8 } orientationSource = owner->TorsoOrientationSource(); // param_1[4] = *(owner+0x438) } PieSlice::~PieSlice() { // detaches 'slices', clears Plug base } // // @0x49e678 -- choose the slice for incoming angle 'theta'. If the slice wheel // rotates with the torso, add the live torso orientation // (orientationSource+0x1d8) and wrap into [0, 2*PI). Then index the slice // table by floor(theta * sliceCount / (2*PI)) and return its leaf table. // DamageZonePercentTable * PieSlice::SelectSlice(Scalar theta) const { if (rotateWithTorso) // param_1+0xc != 0 { theta += *(Scalar *)((char *)orientationSource + 0x1d8); // torso heading if (TwoPi <= theta) theta -= TwoPi; if (theta < 0.0f) theta += TwoPi; // _DAT_0049e730 } int index = (int)floorf(theta * (Scalar)sliceCount * (1.0f / TwoPi)) + 1; // FUN_004dcd10/94 return (DamageZonePercentTable *)slices.Lookup(&index); // slot+0xc on this+0x14 } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // BuildFromNotation -- PieSlice (@0x49e88c) // int PieSlice::BuildFromNotation( NotationFile *model_file, const char *model_name, const char *page_name, NotationFile *table_file, Mech *owner, GenericObjectStream *stream ) { int rotateWithTorso; if (!model_file->GetEntry(page_name, "RotateWithTorso", &rotateWithTorso)) // FUN_00404190 / 0050bc9f { DebugStream << page_name << " missing RotateWithTorso!"; // 0050bcaf return False; } stream->Write(&rotateWithTorso, 4); // param_6 slot+0x20 NotationLine *line = ReconGetPage(model_file, page_name, "PieSlice"); // FUN_00404720 / 0050bcca int sliceCount = line->Count(); // FUN_00403b60 stream->Write(&sliceCount, 4); for (; line != 0; line = line->Next()) { const char *sliceName = line->Value(); if (sliceName[0] == '\0') // puVar6[2] == 0 { DebugStream << model_name << " error in damage table format " << page_name; // 0050bcd3 return -1; } // each named sub-page is a leaf percentage table DamageZonePercentTable::BuildFromNotation( model_file, model_name, sliceName, table_file, owner, stream); // FUN_0049e00c } return True; } //########################################################################### //########################################################################### // DamageLookupTable (top -- layered) //########################################################################### //########################################################################### // // @0x49ea48 -- stream constructor (vtable 0x50bd84). Reads the layer count and // builds that many PieSlice layers from the stream. // DamageLookupTable::DamageLookupTable( Mech *owner, GenericObjectStream *stream ): Plug() // FUN_004178cc(this, 2) { this->owner = owner; // param_1[3] layers.Init(); // FUN_0049f3d5(this+4, 0, 1) stream->Read(&layerCount, 4); // param_1[10] for (int i = 0; i < layerCount; ++i) { PieSlice *layer = new PieSlice(owner, stream); // FUN_0049e740 (size 0x30) layers.Insert(layer, &i); // slot+8 on this+4 } } DamageLookupTable::~DamageLookupTable() // @0x49eadc { // detaches 'layers', clears Plug base } Logical DamageLookupTable::TestInstance() const // @0x49eb48 / @0x49e880 { return True; } // // @0x49eb54 -- resolve an impact point to a damage zone. Transforms 'impact' // into the mech's local frame (owner+0xd0), then derives a normalised // penetration-depth fraction (clamped against the armour reference held at // owner+0x2ec[+0xc]). That fraction selects a layer; the chosen PieSlice is // then handed an incident angle -- either computed from the in-plane hit // direction (atan2-style, FUN_004dc8ec) or, when the direction is degenerate, // a uniform random angle in [0, 2*PI) -- and PieSlice::SelectSlice resolves it. // void DamageLookupTable::ResolveHit(const Point3D &impact) const { Point3D local = owner->WorldToLocal(impact); // FUN_00408bf8 / owner+0xd0 Scalar armourRef = *(Scalar *)(*(int *)((char *)owner + 0x2ec) + 0xc); Scalar depth = local.y; // local_2c if (0.0f <= depth) // _DAT_0049ed00 { if (armourRef < depth) depth = armourRef; // clamp to deepest layer } int layerIndex = (int)floorf((Scalar)layerCount * (depth / armourRef)); // FUN_004dcd10/94 if (layerIndex < 0) layerIndex = 0; if (layerCount - 1 < layerIndex) layerIndex = layerCount - 1; PieSlice *layer = (PieSlice *)layers.Lookup(&layerIndex); // slot+0xc // incident angle from the in-plane (x,z) hit direction, else random Scalar theta; if (fabsf(local.z) > SortEpsilon && fabsf(local.x) > SortEpsilon) // _DAT_0049ed04 { theta = atan2f(local.z, local.x); // FUN_004dc8ec if (theta < 0.0f) theta += TwoPi; } else { theta = RandomUnit() * TwoPi; // FUN_00408050 * _DAT_0049ed08 } layer->SelectSlice(theta); // FUN_0049e678 // (caller then rolls the returned leaf table via SelectZone @0x49de14) } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // BuildFromNotation -- DamageLookupTable (@0x49ed28) // // Reads consecutive "Layer1", "Layer2", ... pages (number appended via itoa, // FUN_004dd3ec) and parses each as a PieSlice page. When no layers are found // the page is malformed. // int DamageLookupTable::BuildFromNotation( NotationFile *model_file, const char *model_name, NotationFile *table_file, Mech *owner, GenericObjectStream *stream ) { char numbered[20]; int layer = 1; // "Layer" + itoa(layer) -> "Layer1" TextString key = TextString("Layer") + itoa(layer, numbered, 10); // 0050bcf2 / FUN_004dd3ec while (model_file->PageExists(key)) // FUN_00403f84 { ++layer; key = TextString("Layer") + itoa(layer, numbered, 10); // each "LayerN" page is a PieSlice page PieSlice::BuildFromNotation( model_file, model_name, key, table_file, owner, stream); // FUN_0049e88c (@line 8809) } if (layer == 1) // no layers parsed { DebugStream << model_file->GetFileName() << " has bad format!"; // 0050bcf8 return False; } return True; } //########################################################################### // Tool support // // @0x49f301 -- table-entry comparator: orders by cumulative threshold (+0x14) // within SortEpsilon. Returns -1 / 0 / +1. // int CompareTableEntryThreshold(const void *a, const void *b) { Scalar diff = *(Scalar *)((char *)a + 0x14) - *(Scalar *)((char *)b + 0x14); if (fabsf(diff) < SortEpsilon) // _DAT_0049f369 return 0; return (*(Scalar *)((char *)a + 0x14) <= *(Scalar *)((char *)b + 0x14)) ? -1 : 1; }