STEP 6 COMPLETE: cylinder hit-location LIVE — unaimed hits resolve to zones

The Mech per-impact hit-location resolver (the cylinder damage table) is now
functional, wired, and runtime-verified [T2].  Unaimed (zone==-1) hits — the
collision-damage path — now resolve an impact point to a damage zone via the
authentic height x angle grid + weighted dice roll, instead of dropping.

dmgtable.cpp/.hpp was a non-functional skeleton on no-op ReconTable/stream
shims; backed it with real std::vector storage and fixed 5 latent runtime bugs:
  - ReadEntries now consumes the leading cell name-string ([i32 len][len+1])
  - PieSlice ctor reads rotateWithTorso into the correct member
  - SelectSlice direct-indexes (was int lookup on a float-keyed table)
  - ResolveHit returns the zone (chains SelectSlice -> SelectZone)
  - real MemoryStream::ReadBytes (was a variadic no-op)

mech.cpp ctor: replaced the empty-name StandingAnimation stub with the real
load — FindResourceDescription(dzRes->resourceName, type 0x1d) -> stream ->
new DamageLookupTable, cached at mech[0x111]; ~Mech deletes it.

Mech::TakeDamageMessageHandler override registered (MESSAGE_ENTRY overlays
Entity's by ID): on invalidDamageZone, resolve via the table then base-route;
aimed reticle hits pass through unchanged.

Three named accessors (no databinding-trap raw reads): WorldToLocal
(localToWorld.MultiplyByInverse), CylinderReferenceHeight (standingTemplateMaxY
== collisionTemplate->maxY == binary mech+0x2ec[+0xc]), TorsoHeading via a
BTGetTorsoTwist bridge in torso.cpp (Torso::CurrentTwist == torso+0x1d8;
torso.hpp cannot be included into mech.cpp — subsystem-stub collision).

Stream format + geometry + roll + handler were all byte-verified against the
shipped BTL4.RES type-29 resources (18 tables, exact consumption) and the
disassembly (FUN_0049eb54/e678/de14, glue 0x49ed0c, handler @0x4a037a).

Runtime: boots clean, "[cyl] table 'bhk1' layers=7" (exact byte-verified layer
count, found by name), mech spawns + walks, no asserts/AV/0xCDCDCDCD.  Env gate
BT_CYL_LOG=1.  Unblocks collision-damage application.

KB updated (combat-damage.md STEP 6 COMPLETE, open-questions.md marked done).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-08 08:32:17 -05:00
co-authored by Claude Opus 4.8
parent 33fee712e9
commit d07ac7dd49
8 changed files with 504 additions and 557 deletions
+132 -351
View File
@@ -6,52 +6,36 @@
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// --/--/95 ?? Initial coding. //
// 07/08/26 RE Completed for RUNTIME (was a non-functional skeleton on //
// no-op ReconTable/stream shims). See dmgtable.hpp header. //
//---------------------------------------------------------------------------//
// 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.
// pseudo-C for the cluster @0x49de14..@0x49f5fb, cross-checked BYTE-FOR-BYTE
// against the shipped BTL4.RES type-29 resources (18 tables, exact stream
// consumption -- see context/combat-damage.md). Each method cites its @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)
// Float constants (read-only globals in the decomp):
// _DAT_0049e520 = 1.0f (percentage-sum target, offline validation)
// _DAT_0049e72c / _DAT_0049e810 / _DAT_0049ed08 = 6.2831853f = 2*PI
// _DAT_0049ed04 = ~1e-4f (degenerate-direction epsilon)
// _DAT_0049ed00 / _DAT_0049e730 = 0.0f
//
// 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.
// Engine-helper name mapping:
// FUN_00408050 RandomUnit() -> Scalar in [0,1) (engine RandomGenerator)
// FUN_00402948 read shared string [i32 len][len+1 bytes: chars + NUL]
// FUN_004dc8ec atan2f() FUN_004dcd00 fabsf() FUN_004dcd10/94 floorf
// FUN_00408bf8 world->local point xform (owner+0xd0) == Mech::WorldToLocal
//
#include <bt.hpp>
#pragma hdrstop
#include <math.h>
#if !defined(DMGTABLE_HPP)
# include <dmgtable.hpp>
#endif
@@ -64,419 +48,216 @@
//
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<int> entry pool:
// entry+0x0c int zone segment index (payload returned by the roll)
// entry+0x14 Scalar cumulative percentage (ascending sort key)
static const Scalar DirEpsilon = 1.0e-4f; // _DAT_0049ed04
//###########################################################################
//###########################################################################
// 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).
// @0x49deb0 -- stream constructor. Stores the owner and rebuilds the entry
// list from the object stream (ReadEntries @0x49e5e4).
//
DamageZonePercentTable::DamageZonePercentTable(
Mech *owner,
GenericObjectStream *stream
Mech *owner,
MemoryStream *stream
):
Plug() // FUN_004178cc(this, 2)
Plug(),
owner(owner)
{
this->owner = owner; // param_1[3]
refCount = NewRefCount(0x10); // param_1[4]
entries.Init(); // FUN_0049f204(this+5, 0, 0)
ReadEntries(stream); // FUN_0049e5e4
ReadEntries(stream);
}
DamageZonePercentTable::~DamageZonePercentTable() // @0x49df80
{
// entries is a value vector -- frees itself.
}
//
// @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.
// @0x49de14 -- the weighted random roll. Walks the entries in ascending
// cumulative order, draws ONE uniform sample in [0,1) and returns the zone
// index of the first entry whose cumulative threshold exceeds the draw.
// -1 when the table is empty (treated as a miss by the caller).
//
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 roll = RandomUnit(); // FUN_00408050
for (unsigned i = 0; i < entries.size(); ++i)
{
Scalar threshold = *(Scalar *)((char *)entry + 0x14); // cumulative %
if (roll < threshold)
if (roll < entries[i].cumulative) // first threshold past the draw
{
selected = *(int *)((char *)entry + 0xc); // zone segment index
return entries[i].zoneIndex;
}
it.Next(); // slot+0xc
// it.Current() re-read at slot+0x30 (end test)
}
return selected;
return -1;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Object-stream I/O
//
// @0x49e524 -- write count, then each entry as {cumulativePercent, zoneIndex}.
// @0x49e5e4 -- read the leading cell NAME string (FUN_00402948: [i32 len] then
// len+1 bytes = chars + trailing NUL), then the entry count and that many
// {cumulativePercent, zoneIndex} pairs. The name is descriptive only (the
// binary caches it but the roll never uses it), so it is consumed and dropped.
//
void
DamageZonePercentTable::WriteEntries(GenericObjectStream *stream) const
DamageZonePercentTable::ReadEntries(MemoryStream *stream)
{
stream->Write(&entries.count, 4);
ReconTableIter it(entries);
for (void *entry = it.First(); entry != 0; entry = it.Next())
int nameLen = 0;
stream->ReadBytes(&nameLen, 4); // FUN_00402948: string length
if (nameLen > 0)
{
stream->Write((char *)entry + 0x14, 4); // cumulative percentage
stream->Write((char *)entry + 0x0c, 4); // zone index
char scratch[128];
int total = nameLen + 1; // chars + NUL (FUN_00402948 reads len+1)
if (total > (int)sizeof(scratch)) total = (int)sizeof(scratch);
stream->ReadBytes(scratch, total); // consume name + NUL
}
int count = 0;
stream->ReadBytes(&count, 4);
entries.reserve(count > 0 ? count : 0);
for (int i = 0; i < count; ++i)
{
Entry e;
stream->ReadBytes(&e.cumulative, 4); // f32 cumulative threshold
stream->ReadBytes(&e.zoneIndex, 4); // i32 zone segment index
entries.push_back(e);
}
}
//
// @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 "<zoneName> <percentage>" ("%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).
// @0x49e740 -- stream constructor (vtable 0x50bd90). Reads the RotateWithTorso
// flag (binary this[3]) and the slice count (this[0xb]) from the stream, then
// builds 'sliceCount' leaf tables. (The torso orientation is read live via
// owner->TorsoHeading() in SelectSlice -- no cached raw pointer.)
//
PieSlice::PieSlice(
Mech *owner,
GenericObjectStream *stream
Mech *owner,
MemoryStream *stream
):
Plug() // FUN_004178cc(this, 2)
Plug(),
owner(owner),
rotateWithTorso(0),
sliceCount(0)
{
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
stream->ReadBytes(&rotateWithTorso, 4); // binary reads into this[3]
stream->ReadBytes(&sliceCount, 4); // this[0xb]
Scalar sliceSpan = TwoPi / (Scalar)sliceCount; // _DAT_0049e810 / count
for (int i = 1; i <= sliceCount; ++i)
slices.reserve(sliceCount > 0 ? sliceCount : 0);
for (int i = 0; i < sliceCount; ++i) // binary loops i=1..count building leaves
{
DamageZonePercentTable *slice =
new DamageZonePercentTable(owner, stream); // FUN_0049deb0 (size 0x2c)
Scalar cumulativeAngle = (Scalar)i * sliceSpan;
slices.Insert(slice, &cumulativeAngle); // slot+8
slices.push_back(new DamageZonePercentTable(owner, stream));
}
orientationSource = owner->TorsoOrientationSource(); // param_1[4] = *(owner+0x438)
}
PieSlice::~PieSlice()
{
// detaches 'slices', clears Plug base
for (unsigned i = 0; i < slices.size(); ++i)
{
delete slices[i];
}
}
//
// @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.
// @0x49e678 -- choose the slice for incoming angle 'theta'. When the wheel
// rotates with the torso, add the live torso twist (torso+0x1d8, via
// owner->TorsoHeading) and wrap into [0, 2*PI). Slice index =
// floor(theta * sliceCount / 2*PI) -- direct 0-based index (equivalent to the
// binary's float-key Find((index+1)*2*PI/sliceCount)).
//
DamageZonePercentTable *
PieSlice::SelectSlice(Scalar theta) const
{
if (rotateWithTorso) // param_1+0xc != 0
if (sliceCount <= 0) return 0;
if (rotateWithTorso) // binary this[3] != 0
{
theta += *(Scalar *)((char *)orientationSource + 0x1d8); // torso heading
if (TwoPi <= theta) theta -= TwoPi;
if (theta < 0.0f) theta += TwoPi; // _DAT_0049e730
theta += owner->TorsoHeading(); // torso+0x1d8
if (theta >= TwoPi) theta -= TwoPi;
if (theta < 0.0f) theta += TwoPi;
}
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;
int index = (int)floorf(theta * (Scalar)sliceCount * (1.0f / TwoPi));
if (index < 0) index = 0;
if (index > sliceCount - 1) index = sliceCount - 1;
return slices[index];
}
//###########################################################################
//###########################################################################
// 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
Mech *owner,
MemoryStream *stream
):
Plug() // FUN_004178cc(this, 2)
Plug(),
owner(owner),
layerCount(0)
{
this->owner = owner; // param_1[3]
layers.Init(); // FUN_0049f3d5(this+4, 0, 1)
stream->Read(&layerCount, 4); // param_1[10]
stream->ReadBytes(&layerCount, 4); // this[10]
layers.reserve(layerCount > 0 ? layerCount : 0);
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
layers.push_back(new PieSlice(owner, stream));
}
}
DamageLookupTable::~DamageLookupTable() // @0x49eadc
DamageLookupTable::~DamageLookupTable() // @0x49eadc
{
// detaches 'layers', clears Plug base
}
Logical
DamageLookupTable::TestInstance() const // @0x49eb48 / @0x49e880
{
return True;
for (unsigned i = 0; i < layers.size(); ++i)
{
delete layers[i];
}
}
//
// @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.
// @0x49eb54 (+ slice @0x49e678 + roll @0x49de14, chained by glue @0x49ed0c) --
// resolve a world impact point to a damage-zone index.
// local = owner->WorldToLocal(impact) (owner+0xd0 xform)
// layer = clamp(floor(layerCount * local.y / heightRef), 0, layerCount-1)
// theta = atan2(local.z, local.x) (degenerate direction -> random*2*PI)
// zone = layer.SelectSlice(theta).SelectZone()
// Returns -1 when the table is empty or the roll falls through (a miss).
//
void
int
DamageLookupTable::ResolveHit(const Point3D &impact) const
{
Point3D local = owner->WorldToLocal(impact); // FUN_00408bf8 / owner+0xd0
if (layerCount <= 0) return -1;
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
}
Point3D local = owner->WorldToLocal(impact); // FUN_00408bf8
Scalar heightRef = owner->CylinderReferenceHeight(); // owner+0x2ec[+0xc]
if (heightRef <= 0.0f) return -1;
int layerIndex = (int)floorf((Scalar)layerCount * (depth / armourRef)); // FUN_004dcd10/94
// --- height -> layer (penetration depth) ---
Scalar depth = local.y; // binary local_2c
int layerIndex = (int)floorf((Scalar)layerCount * (depth / heightRef));
if (layerIndex < 0) layerIndex = 0;
if (layerCount - 1 < layerIndex) layerIndex = layerCount - 1;
if (layerIndex > layerCount - 1) layerIndex = layerCount - 1;
PieSlice *layer = layers[layerIndex];
PieSlice *layer = (PieSlice *)layers.Lookup(&layerIndex); // slot+0xc
// incident angle from the in-plane (x,z) hit direction, else random
// --- in-plane hit direction -> incident angle (else random scatter) ---
Scalar theta;
if (fabsf(local.z) > SortEpsilon && fabsf(local.x) > SortEpsilon) // _DAT_0049ed04
if (fabsf(local.z) > DirEpsilon && fabsf(local.x) > DirEpsilon)
{
theta = atan2f(local.z, local.x); // FUN_004dc8ec
theta = atan2f(local.z, local.x); // FUN_004dc8ec
if (theta < 0.0f) theta += TwoPi;
}
else
{
theta = RandomUnit() * TwoPi; // FUN_00408050 * _DAT_0049ed08
theta = RandomUnit() * TwoPi; // degenerate -> uniform angle
}
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;
DamageZonePercentTable *leaf = layer->SelectSlice(theta); // FUN_0049e678
return leaf ? leaf->SelectZone() : -1; // FUN_0049de14
}