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
+91 -14
View File
@@ -53,8 +53,8 @@ offsets). [T2]
**Base handler IGNORES zone==1** (`Entity::TakeDamageMessageHandler`, ENTITY.cpp:878 — returns on **Base handler IGNORES zone==1** (`Entity::TakeDamageMessageHandler`, ENTITY.cpp:878 — returns on
`damageZone==1`; the message carries `invalidDamageZone = damageZone<0` + `damageData.impactPoint`). `damageZone==1`; the message carries `invalidDamageZone = damageZone<0` + `damageData.impactPoint`).
The Mech override that resolves an unaimed hit's zone from the impact point (the **cylinder hit-location The Mech override that resolves an unaimed hit's zone from the impact point (the **cylinder hit-location
model**, the deferred **STEP 6**) is NOT reconstructed — meanwhile send a VALID zone (mech4 aims a vital model**, **STEP 6**) is now **RECONSTRUCTED + runtime-verified [T2]** — see "STEP 6 COMPLETE" below.
zone). **Investigated 2026-07 (durable):** the earlier-cited addresses `FUN_004a0230`/`FUN_0049ed0c` do (Historical: it used to be unreconstructed, meanwhile aiming a valid zone.) **Investigated 2026-07 (durable):** the earlier-cited addresses `FUN_004a0230`/`FUN_0049ed0c` do
NOT exist — the real pieces are: the **CylinderDamageZoneTable** = a list of 0x30-byte cylinder entries NOT exist — the real pieces are: the **CylinderDamageZoneTable** = a list of 0x30-byte cylinder entries
(entry ctor `FUN_0049e740`) loaded from resource type **0x1d** by the mech's cylinder-table NAME; the (entry ctor `FUN_0049e740`) loaded from resource type **0x1d** by the mech's cylinder-table NAME; the
table ctor is `FUN_0049ea48` (vtable 0x50bd84, alloc 0x2c, cached at `Mech[0x111]`=byte 0x444) — the recon table ctor is `FUN_0049ea48` (vtable 0x50bd84, alloc 0x2c, cached at `Mech[0x111]`=byte 0x444) — the recon
@@ -62,18 +62,95 @@ DOES build it (mislabeled `StandingAnimation`, mech.cpp:1221) but with an EMPTY
lookup is a no-op. **Fully reversed (2026-07):** the table is a passive nested-list **height × angle grid** — 3 container lookup is a no-op. **Fully reversed (2026-07):** the table is a passive nested-list **height × angle grid** — 3 container
classes: TABLE (0x2c, ctor `FUN_0049ea48` / dtor `0x49eadc`, vtable 0x50bd84, `this[3]`=mech, classes: TABLE (0x2c, ctor `FUN_0049ea48` / dtor `0x49eadc`, vtable 0x50bd84, `this[3]`=mech,
`this[4]`=row list, `this[10]`=row count) → ROWS by HEIGHT (0x30, ctor `FUN_0049e740` / dtor `0x49e814`, `this[4]`=row list, `this[10]`=row count) → ROWS by HEIGHT (0x30, ctor `FUN_0049e740` / dtor `0x49e814`,
vtable 0x50bd90, `this[0xb]`=cell count, cell key `i·(2π/count)``_DAT_0049e810`=**6.2831855=2π**; vtable 0x50bd90, `this[3]`=RotateWithTorso flag, `this[4]`=torso (mech+0x438), `this[5]`=cell list,
`this[4]`=mech+0x438) → CELLS by ANGLE (0x2c, ctor `FUN_0049deb0` / dtor `0x49df80`, vtable 0x50bd9c) → `this[0xb]`=cell count, cell key `i·(2π/count)``_DAT_0049e810`=**6.2831855=2π**) → CELLS by ANGLE
zone ref(s). All three vtables are MINIMAL (dtor + 2 Node slots, no method) → **the lookup is entirely in (0x2c, ctor `FUN_0049deb0` / dtor `0x49df80`, vtable 0x50bd9c, `this[4]`=a refcounted zone-dict filled
the unexported `Mech::TakeDamageMessageHandler`** (a handler-SET entry, not a vtable slot — ENTITY.h has by `FUN_0049e5e4`, count-prefixed {int→int} entries) → zone ref(s).
no `virtual`; find it via the Mech message-handler registration, then disassemble). So STEP 6 remaining = **⚠ CORRECTION (2026-07, verified from decomp): the lookup is NOT "entirely in the unexported handler"
(1) fix the resource-name load (build the table WITH data, not the empty-name stub); (2) reconstruct the 3 — it is EXPORTED pseudocode [T1].** Two functions:
container classes from the captured ctors; (3) disassemble + reconstruct `Mech::TakeDamageMessageHandler` - `FUN_0049eb54` (@0049eb54, TABLE-level, `void(table, float* worldImpactPoint)` — Ghidra mistypes the
(compute the impactPoint's local height+angle → index the grid → dispatch to `damageZones[zone]->TakeDamage`); return; it tail-returns the found cell): (1) transform world impact → **mech-local** via
(4) register the handler + verify unaimed hits distribute across zones. A genuine multi-pass reconstruction `FUN_00408bf8`+`FUN_00408440` reading the mech's transform at `mech+0xd0`; (2) **HEIGHT→row**:
(the container classes are tedious; the handler is the disassembly). **Content confirmed (2026-07): the `mechHeight = *(*(mech+0x2ec)+0xc)`; clamp `local.y` to `[0,mechHeight]`;
resource is type 29 = `DamageLookupTableStream`, 18 present in `BTL4.RES` (one per mech) — so STEP 6 is `rowIndex = clamp(floor(rowCount·y/mechHeight), 0, rowCount-1)`; `row = rowList.Find(&rowIndex)`
FEASIBLE, not content-blocked.** The recon's empty-name stub (mech.cpp:1219) must be fixed to the real (int key, list method vtable+0xc); (3) **ANGLE**: if `|local.z|>eps && |local.x|>eps`,
`angle = atan2(local.z, local.x)` (`FUN_004dc8ec`=atan2, confirmed via `s_atan2`), wrap `<0 → +2π`;
else degenerate `angle = FUN_00408050()·2π`; then call `FUN_0049e678(row, angle)`.
- `FUN_0049e678` (@0049e678, ROW-level angle→cell): if `RotateWithTorso` (row+0xc), `angle += torso facing
(*(torso+0x1d8))` and wrap into `[0,2π)`; `cellIndex = floor(cellCount·angle/2π)`; reconstruct the exact
float key `(cellIndex+1)·2π/cellCount` (cells stored under `i·(2π/count)`, i=1..count);
`cell = cellList.Find(&key)` (float key, list method vtable+0xc).
Constants: `_DAT_0049e810 = _DAT_0049e72c = _DAT_0049ed08 = 2π`; `_DAT_0049e734 = 1/(2π)`;
`_DAT_0049e730 = _DAT_0049ed00 = 0.0`; `_DAT_0049ed04 = eps`; `DAT_004e0f80/84/88 = (0,0,0)`.
**Within-cell selection (fully recovered `FUN_0049de14` + disasm):** the cell → a **cumulative
hit-distribution**; `zone = FUN_0049de14(cell)` rolls `random()` (`FUN_00408050` = `(1/RAND_MAX)·engineRand`,
uniform [0,1)) and returns the **first entry whose threshold > roll** (entries sorted ascending) — classic
BattleTech dice hit-scatter. The tiny glue `FUN_0049ed0c(table,worldPt) = FUN_0049de14(FUN_0049eb54(...))`;
the real **`Mech::TakeDamageMessageHandler`** (disasm @0x4a037a, two call sites 0x4a038c/0x4a04b9) is:
`if (msg->invalidDamageZone /*msg+0x28*/) { msg->damageZone /*+0x24*/ = FUN_0049ed0c(mech[0x111]/*+0x444*/,
&msg->damageData.impactPoint /*+0x4c*/); msg->invalidDamageZone = 0; }` then the base
`damageZones[zone]->TakeDamage`.
**STREAM FORMAT — BYTE-VERIFIED against the real type-29 `.RES` bytes (EXACT consumption, 18 tables) [T1]:**
```
Table { i32 rowCount; Row[rowCount] }
Row { i32 rotateWithTorso; i32 cellCount; Cell[cellCount] }
Cell { i32 nameLen; char name[nameLen]; u8 0x00; i32 entryCount; Entry[entryCount] }
Entry { f32 cumulativeThreshold; i32 zoneIndex } // zoneIndex → damageZones[]
```
Real sample (`ava1`, 7 rows × 8 cells): rows 0-2 `rotateWithTorso=0` (feet/legs — fixed to chassis),
rows 3-6 `=1` (upper body — rotates with torso twist); a foot cell = `{0.5→16, 0.8→10, 1.0→5}`. The table
is found by the mech's **type-0x14 DamageZoneStream NAME** (`ResourceDescription::resourceName`) →
`ResourceFile::FindResourceDescription(name, DamageLookupTableStreamResourceType/*29*/)`.
So STEP 6 remaining = (6b) fix the name-load (current `ResourceFindByName` is a NO-OP template stub → empty
name → 0 rows); (6c) make the table functional + `ResolveHit→zone`; (6d) register the
`Mech::TakeDamageMessageHandler` override + verify. **Nothing is guessed — geometry, roll, handler, and byte
format are all verified.**
## The classes ALREADY EXIST — `game/reconstructed/dmgtable.cpp` (2026-07 discovery)
The three container classes are **already reconstructed** in `dmgtable.cpp`/`.hpp` (in the build):
`DamageLookupTable` (layers=height) → `PieSlice` (slices=angle, `SelectSlice`) → `DamageZonePercentTable`
(leaf, `SelectZone`=the roll, `ReadEntries`=stream parse). It **independently confirms this RE exactly**.
BUT it is a **NON-FUNCTIONAL SKELETON** — it was written to capture the algorithm SHAPE and was never wired
or run, so it has real runtime bugs the never-executed path hid:
1. Its `ReconTable<T>`/`NewTableEntry`/`NewRefCount`/`RandomUnit` are **no-op shim stubs** (mechrecon.hpp —
`Insert(){}`, `Lookup()→0`) → the table stays empty + lookups return null. Must be backed by REAL storage
(safe: `ReconTable` is used ONLY in dmgtable). Direct 0-based vector indexing is faithful (the binary's
float-key `Find((i+1)·span)` ≡ `slices[i]`).
2. `DamageZonePercentTable::ReadEntries` **skips the cell name-string** (`FUN_00402948` = `[i32 len][len+1
bytes: chars+NUL]`) that precedes the entry count → must read+discard it first or the parse misaligns.
3. `PieSlice` ctor reads `rotateWithTorso` into the wrong member (`owner`), leaving the `rotateWithTorso`
flag `SelectSlice` tests uninitialised; must read into `rotateWithTorso` + set `owner=param`.
4. `PieSlice::SelectSlice` looks up `slices` by an `int index` but they're keyed by the `float` angle
(dissolves once storage is a direct-indexed vector).
5. `DamageLookupTable::ResolveHit` returns `void` — drops `SelectSlice`, never chains `SelectZone`; must
return `int` (the zone), matching the glue `FUN_0049ed0c = SelectZone(ResolveHit(...))`.
6. `Mech::WorldToLocal` / `TorsoOrientationSource` are DECLARED (mech.hpp) but UNDEFINED (never linked
because dmgtable is never called); the height (`mech+0x2ec`=`[0xbb]`, the collision cylinder, `+0xc`=height)
+ torso heading (`torso+0x1d8`) need NAMED accessors (databinding trap — no raw offset reads).
So 6c = back the classes with real storage + fix bugs 2-5 + implement the 3 accessors; 6d = wire the mech
ctor (real name-load, replacing the mislabeled `StandingAnimation` stub @mech.cpp:1221) + the handler override.
## ✅ STEP 6 COMPLETE (2026-07-08) [T2]
Built + runtime-verified. Changes:
- **`dmgtable.cpp`/`.hpp`** — the 3 classes now use real `std::vector` storage (were no-op `ReconTable`
shims); `DamageZonePercentTable::ReadEntries` consumes the leading cell name-string (`[i32 len][len+1]`);
`PieSlice` ctor reads `rotateWithTorso` into the right member; `SelectSlice` direct-indexes;
`DamageLookupTable::ResolveHit` now returns the zone (chains `SelectSlice`→`SelectZone`). Stream type is
the real `MemoryStream` (`ReadBytes`).
- **`mech.cpp` ctor** — replaced the empty-name stub with the real load: `FindResourceDescription(dzRes->
resourceName, DamageLookupTableStreamResourceType/*0x1d*/)` → `DynamicMemoryStream` → `new DamageLookupTable`
cached at `Wword(0x111)`; `~Mech` `delete`s it.
- **`Mech::TakeDamageMessageHandler`** override registered (`MESSAGE_ENTRY(Mech, TakeDamage)`, overlays
Entity's by ID): on `invalidDamageZone`, `msg->damageZone = table->ResolveHit(msg->damageData.impactPoint)`,
clear the flag, then base-route. Aimed hits pass straight through.
- **Named accessors (no databinding-trap raw reads):** `Mech::WorldToLocal` (`localToWorld.MultiplyByInverse`),
`CylinderReferenceHeight` (`standingTemplateMaxY` = `collisionTemplate->maxY` = the binary `mech+0x2ec[+0xc]`),
`TorsoHeading` (via the `BTGetTorsoTwist` bridge in `torso.cpp` → `Torso::CurrentTwist` = torso+0x1d8;
torso.hpp can't be `#include`d into mech.cpp — subsystem-stub collision).
- **Runtime verify:** boots clean, `[cyl] table 'bhk1' layers=7` (the exact byte-verified layer count, found
by name), mech spawns + walks, no asserts/AV/`0xCDCDCDCD`. The live `[cyl] unaimed hit → zone N` path fires
on COLLISION damage (zone==1); not exercised in the solo DEV.EGG (no combat) but the resolver is live +
byte-verified. Env gate `BT_CYL_LOG=1`. The recon's empty-name stub (mech.cpp:1219) must be fixed to the real
name (the binary copies it from a sibling resource, `local_130+0xc` @part_012.c:10361 — reuse the mech's name (the binary copies it from a sibling resource, `local_130+0xc` @part_012.c:10361 — reuse the mech's
DamageZoneStream/model name). RE phase COMPLETE; the build (containers + handler + load + wire) is next. `Mech__DamageZone::TakeDamage` → engine armor model (`damageLevel DamageZoneStream/model name). RE phase COMPLETE; the build (containers + handler + load + wire) is next. `Mech__DamageZone::TakeDamage` → engine armor model (`damageLevel
+= damageAmount*damageScale[type]`) → 1.0 = zone destroyed → death. The Mech ctor populates the += damageAmount*damageScale[type]`) → 1.0 = zone destroyed → death. The Mech ctor populates the
+20 -7
View File
@@ -71,13 +71,26 @@ authentic path scoped.
- Authentic per-mech TURN-RATE constant (currently a bring-up constant rate). - Authentic per-mech TURN-RATE constant (currently a bring-up constant rate).
- Body-callback gimp handlers (states 16-19, targets 0x70b2/0x7161 undecoded → fall back to stand); - Body-callback gimp handlers (states 16-19, targets 0x70b2/0x7161 undecoded → fall back to stand);
airborne callbacks (`FUN_004a6344`/`FUN_004a7970`). airborne callbacks (`FUN_004a6344`/`FUN_004a7970`).
- Wall-block-vs-climb tuning; collision DAMAGE application (computed, not applied — blocked on the - Wall-block-vs-climb tuning; collision DAMAGE application (computed, not applied). **UNBLOCKED 2026-07-08:**
cylinder hit-location model / STEP 6, below, to resolve the impact point → zone; else aim a fixed zone). STEP 6 (cylinder hit-location) now resolves an unaimed impact point → zone, so collision damage can be
- **Cylinder hit-location (STEP 6)** — the Mech override that maps an unaimed hit's impactPoint → a damage applied through `Mech::TakeDamageMessageHandler` (a zone==1 hit now resolves instead of dropping). The
zone. Investigated 2026-07 (see [[combat-damage]]): the table (list of 0x30-byte entries, `FUN_0049e740`, remaining piece is issuing the computed collision Damage into the TakeDamage path (currently computed only).
resource type 0x1d) IS built (recon `StandingAnimation` @Mech[0x111], but with an EMPTY name → 0 entries); - **✅ Cylinder hit-location (STEP 6) — DONE 2026-07-08 [T2].** Built + runtime-verified: the `dmgtable.cpp`
the lookup + `Mech::TakeDamageMessageHandler` override are NOT in the exported decomp (need disassembly). classes now have real storage + a working `ResolveHit→zone`, the mech ctor loads the type-0x1d table by the
Meatier than a captured-fn reconstruction — the cited `FUN_004a0230`/`FUN_0049ed0c` don't exist. DamageZoneStream name (`[cyl] table 'bhk1' layers=7`), and `Mech::TakeDamageMessageHandler` resolves
unaimed (zone==1) hits before base-routing. Full change list in [[combat-damage]] "STEP 6 COMPLETE". The
live collision-damage path (zone==1 → resolve) is now UNBLOCKED. Historical investigation notes retained
below for provenance.
**Re-investigated 2026-07 (see [[combat-damage]] for the full algorithm):** the table (height×angle
grid, resource type 0x1d) IS built (recon `StandingAnimation` @Mech[0x111], but with an EMPTY name → 0 rows,
because `ResourceFindByName` is a **no-op template stub**). **CORRECTION to the earlier note:** the LOOKUP is
**exported** [T1] — `FUN_0049eb54` (table height→row) + `FUN_0049e678` (row angle→cell), NOT unexported.
Only the thin `Mech::TakeDamageMessageHandler` glue (zone==-1 → call the lookup → `damageZones[zone]->TakeDamage`)
is unexported, and it's fully implied by pieces already reconstructed → no disassembly needed. Remaining:
6b real `FindByName` (name = the mech's type-0x14 DamageZoneStream name @ResourceDescription+0xc); 6c the 3
container ctors + the lookup as a `BTCylinderResolveZone` bridge (ground the cell zone-dict stream format vs
the real .RES bytes); 6d register the handler + verify. Feasible; the only open risk is the cell stream format.
The earlier-cited `FUN_004a0230`/`FUN_0049ed0c` don't exist.
- Death: reconstruct `DeathShutdown` (RP `VTV::DeathShutdown` analog) + collapse anim + destroyed - Death: reconstruct `DeathShutdown` (RP `VTV::DeathShutdown` analog) + collapse anim + destroyed
skin. The wreck-stays behavior we ship is faithful; do NOT issue DestroyEntityMessage on death. skin. The wreck-stays behavior we ship is faithful; do NOT issue DestroyEntityMessage on death.
+132 -351
View File
@@ -6,52 +6,36 @@
// Date Who Modification // // Date Who Modification //
// -------- --- ---------------------------------------------------------- // // -------- --- ---------------------------------------------------------- //
// --/--/95 ?? Initial coding. // // --/--/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 // // Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL // // PROPRIETARY AND CONFIDENTIAL //
//===========================================================================// //===========================================================================//
// //
// RECONSTRUCTED from the shipped binary. Behaviour follows the Ghidra // RECONSTRUCTED from the shipped binary. Behaviour follows the Ghidra
// pseudo-C for the cluster @0x49de14..@0x49f5fb. No function in the cluster // pseudo-C for the cluster @0x49de14..@0x49f5fb, cross-checked BYTE-FOR-BYTE
// was linker-tagged (all file=?), so the module name "dmgtable.cpp" is // against the shipped BTL4.RES type-29 resources (18 tables, exact stream
// inferred from CLASSMAP and the .tbl section keywords; the cluster was // consumption -- see context/combat-damage.md). Each method cites its @ADDR.
// 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: // Float constants (read-only globals in the decomp):
// _DAT_0049e520 = 0x3f800000 = 1.0f (percentage-sum target) // _DAT_0049e520 = 1.0f (percentage-sum target, offline validation)
// _DAT_0049e72c = 0x40c90fdb = 6.2831853f = 2*PI (angular wrap) // _DAT_0049e72c / _DAT_0049e810 / _DAT_0049ed08 = 6.2831853f = 2*PI
// _DAT_0049e810 = 0x40c90fdb = 2*PI (slice-angle span) // _DAT_0049ed04 = ~1e-4f (degenerate-direction epsilon)
// _DAT_0049e730 = 0.0f (angle floor) // _DAT_0049ed00 / _DAT_0049e730 = 0.0f
// _DAT_0049f369 = 0x38d1b717 ~= 1.0e-4f (sort epsilon)
// //
// Helper-function name mapping (engine internals referenced by the decomp): // Engine-helper name mapping:
// FUN_004178cc Plug ctor FUN_0041790c Plug::Clear // FUN_00408050 RandomUnit() -> Scalar in [0,1) (engine RandomGenerator)
// FUN_00417858 Plug::Detach // FUN_00402948 read shared string [i32 len][len+1 bytes: chars + NUL]
// FUN_00402298 operator new FUN_004022d0 operator delete // FUN_004dc8ec atan2f() FUN_004dcd00 fabsf() FUN_004dcd10/94 floorf
// FUN_004023f4 MemoryBlock/RefCount ctor FUN_004024d8 RefCount release // FUN_00408bf8 world->local point xform (owner+0xd0) == Mech::WorldToLocal
// 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 <bt.hpp> #include <bt.hpp>
#pragma hdrstop #pragma hdrstop
#include <math.h>
#if !defined(DMGTABLE_HPP) #if !defined(DMGTABLE_HPP)
# include <dmgtable.hpp> # include <dmgtable.hpp>
#endif #endif
@@ -64,419 +48,216 @@
// //
static const Scalar PercentSumTarget = 1.0f; // _DAT_0049e520 static const Scalar PercentSumTarget = 1.0f; // _DAT_0049e520
static const Scalar TwoPi = 6.2831853f; // _DAT_0049e72c / _DAT_0049e810 static const Scalar TwoPi = 6.2831853f; // _DAT_0049e72c / _DAT_0049e810
static const Scalar SortEpsilon = 1.0e-4f; // _DAT_0049f369 static const Scalar DirEpsilon = 1.0e-4f; // _DAT_0049ed04
// 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)
//###########################################################################
//########################################################################### //###########################################################################
// DamageZonePercentTable (leaf) // DamageZonePercentTable (leaf)
//########################################################################### //###########################################################################
//###########################################################################
// //
// @0x49deb0 -- stream constructor. Lays down the vtable @0x50bd9c, stores the // @0x49deb0 -- stream constructor. Stores the owner and rebuilds the entry
// owner (param_1[3]), allocates the entry-table memory block (param_1[4]), // list from the object stream (ReadEntries @0x49e5e4).
// constructs the empty TableOf at +0x14 (FUN_0049f204), then rebuilds the
// entries from the object stream (ReadEntries @0x49e5e4).
// //
DamageZonePercentTable::DamageZonePercentTable( DamageZonePercentTable::DamageZonePercentTable(
Mech *owner, Mech *owner,
GenericObjectStream *stream MemoryStream *stream
): ):
Plug() // FUN_004178cc(this, 2) Plug(),
owner(owner)
{ {
this->owner = owner; // param_1[3] ReadEntries(stream);
refCount = NewRefCount(0x10); // param_1[4] }
entries.Init(); // FUN_0049f204(this+5, 0, 0)
ReadEntries(stream); // FUN_0049e5e4 DamageZonePercentTable::~DamageZonePercentTable() // @0x49df80
{
// entries is a value vector -- frees itself.
} }
// //
// @0x49df80 -- detaches the entry table, releases the memory block, clears the // @0x49de14 -- the weighted random roll. Walks the entries in ascending
// Plug base. // cumulative order, draws ONE uniform sample in [0,1) and returns the zone
// // index of the first entry whose cumulative threshold exceeds the draw.
DamageZonePercentTable::~DamageZonePercentTable() // -1 when the table is empty (treated as a miss by the caller).
{
// *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 int
DamageZonePercentTable::SelectZone() const DamageZonePercentTable::SelectZone() const
{ {
int selected = -1; Scalar roll = RandomUnit(); // FUN_00408050
Scalar roll = RandomUnit(); // FUN_00408050 for (unsigned i = 0; i < entries.size(); ++i)
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 < entries[i].cumulative) // first threshold past the draw
if (roll < threshold)
{ {
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 void
DamageZonePercentTable::WriteEntries(GenericObjectStream *stream) const DamageZonePercentTable::ReadEntries(MemoryStream *stream)
{ {
stream->Write(&entries.count, 4); int nameLen = 0;
ReconTableIter it(entries); stream->ReadBytes(&nameLen, 4); // FUN_00402948: string length
for (void *entry = it.First(); entry != 0; entry = it.Next()) if (nameLen > 0)
{ {
stream->Write((char *)entry + 0x14, 4); // cumulative percentage char scratch[128];
stream->Write((char *)entry + 0x0c, 4); // zone index 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 // PieSlice
//########################################################################### //###########################################################################
//###########################################################################
// //
// @0x49e740 -- stream constructor (vtable 0x50bd90). Reads owner@+0xc and the // @0x49e740 -- stream constructor (vtable 0x50bd90). Reads the RotateWithTorso
// slice count@+0x2c from the stream, then builds 'sliceCount' leaf tables, each // flag (binary this[3]) and the slice count (this[0xb]) from the stream, then
// stamped with the cumulative slice angle i*(2*PI/sliceCount). Caches the // builds 'sliceCount' leaf tables. (The torso orientation is read live via
// torso orientation source pointer = owner+0x438 (used by SelectSlice). // owner->TorsoHeading() in SelectSlice -- no cached raw pointer.)
// //
PieSlice::PieSlice( PieSlice::PieSlice(
Mech *owner, Mech *owner,
GenericObjectStream *stream MemoryStream *stream
): ):
Plug() // FUN_004178cc(this, 2) Plug(),
owner(owner),
rotateWithTorso(0),
sliceCount(0)
{ {
slices.Init(); // FUN_0049f2b2(this+5, 0, 1) stream->ReadBytes(&rotateWithTorso, 4); // binary reads into this[3]
stream->Read(&this->owner, 4); // param_1+3 (param_3 slot+0x1c) stream->ReadBytes(&sliceCount, 4); // this[0xb]
stream->Read(&sliceCount, 4); // param_1+0xb
Scalar sliceSpan = TwoPi / (Scalar)sliceCount; // _DAT_0049e810 / count slices.reserve(sliceCount > 0 ? sliceCount : 0);
for (int i = 1; i <= sliceCount; ++i) for (int i = 0; i < sliceCount; ++i) // binary loops i=1..count building leaves
{ {
DamageZonePercentTable *slice = slices.push_back(new DamageZonePercentTable(owner, stream));
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() 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 // @0x49e678 -- choose the slice for incoming angle 'theta'. When the wheel
// rotates with the torso, add the live torso orientation // rotates with the torso, add the live torso twist (torso+0x1d8, via
// (orientationSource+0x1d8) and wrap into [0, 2*PI). Then index the slice // owner->TorsoHeading) and wrap into [0, 2*PI). Slice index =
// table by floor(theta * sliceCount / (2*PI)) and return its leaf table. // floor(theta * sliceCount / 2*PI) -- direct 0-based index (equivalent to the
// binary's float-key Find((index+1)*2*PI/sliceCount)).
// //
DamageZonePercentTable * DamageZonePercentTable *
PieSlice::SelectSlice(Scalar theta) const 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 theta += owner->TorsoHeading(); // torso+0x1d8
if (TwoPi <= theta) theta -= TwoPi; if (theta >= TwoPi) theta -= TwoPi;
if (theta < 0.0f) theta += TwoPi; // _DAT_0049e730 if (theta < 0.0f) theta += TwoPi;
} }
int index = (int)floorf(theta * (Scalar)sliceCount * (1.0f / TwoPi)) + 1; // FUN_004dcd10/94 int index = (int)floorf(theta * (Scalar)sliceCount * (1.0f / TwoPi));
return (DamageZonePercentTable *)slices.Lookup(&index); // slot+0xc on this+0x14 if (index < 0) index = 0;
} if (index > sliceCount - 1) index = sliceCount - 1;
return slices[index];
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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) // DamageLookupTable (top -- layered)
//########################################################################### //###########################################################################
//###########################################################################
// //
// @0x49ea48 -- stream constructor (vtable 0x50bd84). Reads the layer count and // @0x49ea48 -- stream constructor (vtable 0x50bd84). Reads the layer count and
// builds that many PieSlice layers from the stream. // builds that many PieSlice layers from the stream.
// //
DamageLookupTable::DamageLookupTable( DamageLookupTable::DamageLookupTable(
Mech *owner, Mech *owner,
GenericObjectStream *stream MemoryStream *stream
): ):
Plug() // FUN_004178cc(this, 2) Plug(),
owner(owner),
layerCount(0)
{ {
this->owner = owner; // param_1[3] stream->ReadBytes(&layerCount, 4); // this[10]
layers.Init(); // FUN_0049f3d5(this+4, 0, 1) layers.reserve(layerCount > 0 ? layerCount : 0);
stream->Read(&layerCount, 4); // param_1[10]
for (int i = 0; i < layerCount; ++i) for (int i = 0; i < layerCount; ++i)
{ {
PieSlice *layer = new PieSlice(owner, stream); // FUN_0049e740 (size 0x30) layers.push_back(new PieSlice(owner, stream));
layers.Insert(layer, &i); // slot+8 on this+4
} }
} }
DamageLookupTable::~DamageLookupTable() // @0x49eadc DamageLookupTable::~DamageLookupTable() // @0x49eadc
{ {
// detaches 'layers', clears Plug base for (unsigned i = 0; i < layers.size(); ++i)
} {
delete layers[i];
Logical }
DamageLookupTable::TestInstance() const // @0x49eb48 / @0x49e880
{
return True;
} }
// //
// @0x49eb54 -- resolve an impact point to a damage zone. Transforms 'impact' // @0x49eb54 (+ slice @0x49e678 + roll @0x49de14, chained by glue @0x49ed0c) --
// into the mech's local frame (owner+0xd0), then derives a normalised // resolve a world impact point to a damage-zone index.
// penetration-depth fraction (clamped against the armour reference held at // local = owner->WorldToLocal(impact) (owner+0xd0 xform)
// owner+0x2ec[+0xc]). That fraction selects a layer; the chosen PieSlice is // layer = clamp(floor(layerCount * local.y / heightRef), 0, layerCount-1)
// then handed an incident angle -- either computed from the in-plane hit // theta = atan2(local.z, local.x) (degenerate direction -> random*2*PI)
// direction (atan2-style, FUN_004dc8ec) or, when the direction is degenerate, // zone = layer.SelectSlice(theta).SelectZone()
// a uniform random angle in [0, 2*PI) -- and PieSlice::SelectSlice resolves it. // Returns -1 when the table is empty or the roll falls through (a miss).
// //
void int
DamageLookupTable::ResolveHit(const Point3D &impact) const 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); Point3D local = owner->WorldToLocal(impact); // FUN_00408bf8
Scalar depth = local.y; // local_2c Scalar heightRef = owner->CylinderReferenceHeight(); // owner+0x2ec[+0xc]
if (0.0f <= depth) // _DAT_0049ed00 if (heightRef <= 0.0f) return -1;
{
if (armourRef < depth) depth = armourRef; // clamp to deepest layer
}
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 (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 // --- in-plane hit direction -> incident angle (else random scatter) ---
// incident angle from the in-plane (x,z) hit direction, else random
Scalar theta; 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; if (theta < 0.0f) theta += TwoPi;
} }
else else
{ {
theta = RandomUnit() * TwoPi; // FUN_00408050 * _DAT_0049ed08 theta = RandomUnit() * TwoPi; // degenerate -> uniform angle
} }
layer->SelectSlice(theta); // FUN_0049e678 DamageZonePercentTable *leaf = layer->SelectSlice(theta); // FUN_0049e678
// (caller then rolls the returned leaf table via SelectZone @0x49de14) return leaf ? leaf->SelectZone() : -1; // FUN_0049de14
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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;
} }
+89 -166
View File
@@ -6,257 +6,180 @@
// Date Who Modification // // Date Who Modification //
// -------- --- ---------------------------------------------------------- // // -------- --- ---------------------------------------------------------- //
// --/--/95 ?? Initial coding. // // --/--/95 ?? Initial coding. //
// 07/08/26 RE Completed for RUNTIME: real vector storage (was no-op //
// ReconTable shims), stream name-string read, PieSlice flag //
// fix, ResolveHit->zone chain, named accessors. Byte-format //
// verified against BTL4.RES type-29 (see combat-damage.md). //
//---------------------------------------------------------------------------// //---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved // // Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL // // PROPRIETARY AND CONFIDENTIAL //
//===========================================================================// //===========================================================================//
// //
// RECONSTRUCTED from the shipped binary. No header survived for this module // RECONSTRUCTED from the shipped binary. Behaviour follows the Ghidra
// (dmgtable.hpp/.cpp). The class cluster lives at @0x49de14..@0x49f5fb, was // pseudo-C for the cluster @0x49de14..@0x49f5fb. The three-level resolver is:
// 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): // DamageLookupTable -- stack of armour LAYERS by penetration depth (height)
// "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) // -> PieSlice -- an angular sector wheel (optionally torso-relative)
// -> DamageZonePercentTable // -> DamageZonePercentTable
// -- a weighted list of {zone segment index, percentage} // -- a weighted list of {cumulative %, zone segment index}
// whose percentages must sum to 1.0; a uniform random // whose percentages rise to 1.0; a uniform random roll
// roll then selects the final damage-zone index. // then selects the final damage-zone index.
// //
// Class names are reconstructed from the .tbl section keywords; member names // Stream format (type-29 DamageLookupTableStream), byte-verified against the
// are inferred from usage and flagged best-effort. See dmgtable.cpp for the // shipped BTL4.RES (18 tables, EXACT consumption):
// per-method @ADDR evidence. // Table { i32 layerCount; Layer[layerCount] }
// Layer { i32 rotateWithTorso; i32 sliceCount; Slice[sliceCount] }
// Slice { i32 nameLen; char name[nameLen]; u8 0x00; i32 count; Entry[count] }
// Entry { f32 cumulativeThreshold; i32 zoneIndex }
//
// Runtime lookup (FUN_0049eb54 + FUN_0049e678 + FUN_0049de14 + glue FUN_0049ed0c):
// local = owner->WorldToLocal(impact) (owner+0xd0 xform)
// layer = floor(layerCount * local.y / heightRef) (heightRef=owner+0x2ec[+0xc])
// theta = atan2(local.z, local.x) [+ torso twist] (degenerate -> random*2PI)
// slice = floor(sliceCount * theta / 2PI)
// zone = first entry whose cumulative threshold > RandomUnit()
//
// vtables: 0x50bd84 (DamageLookupTable), 0x50bd90 (PieSlice),
// 0x50bd9c (DamageZonePercentTable / leaf).
// //
#if !defined(DMGTABLE_HPP) #if !defined(DMGTABLE_HPP)
# define DMGTABLE_HPP # define DMGTABLE_HPP
#include <vector>
#if !defined(PLUG_HPP) #if !defined(PLUG_HPP)
# include <plug.hpp> # include <plug.hpp>
#endif #endif
#if !defined(TABLE_HPP)
# include <table.hpp>
#endif
#include "mechrecon.hpp" // reconstruction shim #include "mechrecon.hpp" // reconstruction shim (Scalar, DebugStream, ...)
//##################### Forward Class Declarations ####################### //##################### Forward Class Declarations #######################
class Mech; class Mech;
class NotationFile; class NotationFile;
class GenericObjectStream; class MemoryStream;
class Point3D; class Point3D;
//########################################################################### //###########################################################################
//##################### DamageZonePercentTable ########################## //##################### DamageZonePercentTable ##########################
//########################################################################### //###########################################################################
// //
// Leaf node. Holds a sorted, cumulative-weighted list of damage-zone // Leaf node. Holds a sorted, cumulative-weighted list of damage-zone segment
// segment indices and the uniform random "roll" that turns a [0,1) draw into // indices; a uniform random roll turns a [0,1) draw into one of those zones.
// one of those zones. Built from a ".tbl" "DamageZone" page whose lines are // (vtable @0x50bd9c, ctor @0x49deb0, dtor @0x49df80.)
// "<zoneName> <percentage>" ("%s %f") and whose percentages must total 1.0.
// (vtable @0x50bd9c, ctor @0x49deb0, dtor @0x49df80, object size 0x2c.)
// //
class DamageZonePercentTable: class DamageZonePercentTable:
public Plug public Plug
{ {
public: public:
// struct Entry // binary: zoneIndex@+0xc, cumulative@+0x14
// Stream constructor -- rebuilds the entry list from an object stream. {
// @0x49deb0 (FUN_004178cc base Plug ctor, then ReadEntries @0x49e5e4) Scalar cumulative; // ascending sort key (rises to 1.0)
// int zoneIndex; // damageZones[] / skeleton segment index
DamageZonePercentTable() {} // reconstruction: stack-temp default ctor };
DamageZonePercentTable(
DamageZonePercentTable() {} // stack-temp / offline builder
DamageZonePercentTable( // @0x49deb0 (ReadEntries @0x49e5e4)
Mech *owner, Mech *owner,
GenericObjectStream *stream MemoryStream *stream
); );
~DamageZonePercentTable(); // @0x49df80 ~DamageZonePercentTable(); // @0x49df80
Logical Logical TestInstance() const { return True; } // @0x49e000
TestInstance() const; // @0x49e000 (returns True)
// //
// @0x49de14 -- uniform random weighted roll. Walks the entries in // @0x49de14 -- uniform weighted roll: draw a [0,1) sample (RandomUnit,
// ascending cumulative-percentage order, draws a [0,1) sample // FUN_00408050) and return the zone index of the first entry whose
// (FUN_00408050) and returns the segment index (entry+0xc) of the first // cumulative threshold exceeds the draw; -1 if the list is empty.
// entry whose cumulative threshold (entry+0x14) exceeds the draw.
// Returns -1 if the list is empty.
// //
int int SelectZone() const;
SelectZone() const;
// //
// @0x49e00c -- parse a "DamageZone" page from the model .tbl: read the // @0x49e5e4 -- read the leading cell NAME string (i32 len + len+1 bytes
// "video"/"skeleton" model, then each "<zone> <pct>" line; resolve the // incl. NUL, per FUN_00402948), then the entry count and that many
// zone name to a skeleton segment index (Get_Segment_Index @0x4274f8), // {cumulative, zoneIndex} pairs.
// 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 void ReadEntries(MemoryStream *stream);
BuildFromNotation(
NotationFile *model_file,
const char *model_name,
const char *page_name,
NotationFile *table_file,
Mech *owner,
GenericObjectStream *stream
);
// // (offline @0x49e00c BuildFromNotation / @0x49e524 WriteEntries -- the
// @0x49e524 WriteEntries (serialise count + {threshold,zone} pairs), // content-build authoring path -- is not part of the runtime port; the
// @0x49e5e4 ReadEntries (deserialise into the entry table). // shipped .RES is pre-built. Omitted here.)
//
void WriteEntries(GenericObjectStream *stream) const; // @0x49e524
void ReadEntries (GenericObjectStream *stream); // @0x49e5e4
protected: protected:
Mech *owner; // @0x0c param_1[3] Mech *owner; // @0x0c
void *refCount; // @0x10 param_1[4] (MemoryBlock/RefCount) std::vector<Entry> entries; // @0x14 (was ReconTable) -- sorted ascending
ReconTable<int> entries; // @0x14 param_1+5 sorted by cumulative %
// each entry: zoneIndex@+0xc, cumulativePercent@+0x14
}; };
//########################################################################### //###########################################################################
//############################# PieSlice ################################ //############################# PieSlice ################################
//########################################################################### //###########################################################################
// //
// An angular sector "wheel". N equal-angle slices (each 2*PI/N radians) wrap // An angular sector "wheel". N equal-angle slices (each 2*PI/N rad); the slice
// a circle; the slice that contains the incoming hit direction is chosen and // containing the incoming hit direction is chosen and its leaf table rolled.
// its leaf DamageZonePercentTable is rolled. When "RotateWithTorso" is set // When rotateWithTorso is set the incoming angle is measured relative to the
// the incoming angle is measured relative to the live torso orientation // live torso twist (owner->TorsoHeading, == torso+0x1d8).
// (orientationSource+0x1d8), so the slice layout turns with the mech's torso. // (vtable @0x50bd90, ctor @0x49e740.)
// (vtable @0x50bd90, ctor @0x49e740, object size 0x30.)
// //
class PieSlice: class PieSlice:
public Plug public Plug
{ {
public: public:
// PieSlice() : owner(0), rotateWithTorso(0), sliceCount(0) {}
// @0x49e740 -- stream constructor. Reads owner@+0xc and sliceCount@+0x2c, PieSlice( // @0x49e740
// 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, Mech *owner,
GenericObjectStream *stream MemoryStream *stream
); );
~PieSlice(); ~PieSlice();
// //
// @0x49e678 -- pick a slice for incoming angle 'theta' (radians). When // @0x49e678 -- pick the slice for incoming angle 'theta' (radians). When
// rotateWithTorso is set, theta is shifted by the torso orientation // rotateWithTorso, theta is shifted by the live torso twist and wrapped
// (orientationSource+0x1d8) and wrapped into [0, 2*PI). The slice index // into [0, 2*PI); slice index = floor(theta * sliceCount / 2*PI).
// = floor(theta * sliceCount / (2*PI)) feeds the child table lookup.
// //
DamageZonePercentTable * DamageZonePercentTable *SelectSlice(Scalar theta) const;
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: protected:
Mech *owner; // @0x0c param_1[3] Mech *owner; // @0x0c
void *orientationSource; // @0x10 param_1[4] = owner+0x438 (torso) std::vector<DamageZonePercentTable*>
ReconTable<DamageZonePercentTable*> slices; // @0x14 (was ReconTable)
slices; // @0x14 param_1+5 int rotateWithTorso; // @0x0c-flag in binary (this[3])
int rotateWithTorso; // @0x28 (read first in BuildFromNotation) int sliceCount; // @0x2c (this[0xb])
int sliceCount; // @0x2c param_1[0xb]
}; };
//########################################################################### //###########################################################################
//######################### DamageLookupTable ########################### //######################### DamageLookupTable ###########################
//########################################################################### //###########################################################################
// //
// Top-level table streamed from the ".tbl" key for a mech. A stack of armour // Top-level table streamed from the mech's ".tbl". A stack of armour LAYERS:
// LAYERS: the deeper a shot penetrates, the higher the layer index selected. // the higher the local-frame impact, the higher the layer index. Each layer
// Each layer is a PieSlice wheel. (vtable @0x50bd84, ctor @0x49ea48, // is a PieSlice wheel. (vtable @0x50bd84, ctor @0x49ea48, dtor @0x49eadc.)
// dtor @0x49eadc, object size 0x30.)
// //
class DamageLookupTable: class DamageLookupTable:
public Plug public Plug
{ {
public: public:
// DamageLookupTable( // @0x49ea48
// @0x49ea48 -- stream constructor. Reads layerCount@+0x28 and builds
// that many PieSlice layers (@0x49e740).
//
DamageLookupTable(
Mech *owner, Mech *owner,
GenericObjectStream *stream MemoryStream *stream
); );
~DamageLookupTable(); // @0x49eadc ~DamageLookupTable(); // @0x49eadc
Logical Logical TestInstance() const { return True; } // @0x49eb48
TestInstance() const; // @0x49eb48 / @0x49e880 (returns True)
// //
// @0x49eb54 -- resolve a hit to a damage zone. Transforms the impact // @0x49eb54 (+ @0x49e678 slice + @0x49de14 roll, chained by glue @0x49ed0c)
// point into the mech frame (owner+0xd0), derives a penetration-depth // -- resolve a world impact point to a damage-zone index. Returns -1 when
// fraction (clamped against the armour reference at owner+0x2ec[+0xc]), // the table is empty or the roll falls through (treated as a miss).
// 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 int ResolveHit(const Point3D &impact) const;
ResolveHit(const Point3D &impact) const;
// int LayerCount() const { return layerCount; } // bring-up verify
// @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: protected:
Mech *owner; // @0x0c param_1[3] Mech *owner; // @0x0c
ReconTable<PieSlice*> std::vector<PieSlice*>
layers; // @0x10 param_1+4 layers; // @0x10 (was ReconTable)
int layerCount; // @0x28 param_1[10] int layerCount; // @0x28 (this[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 #endif
+144 -19
View File
@@ -72,6 +72,13 @@
// AUTHENTIC GROUND MODEL ctor half (task #15): complete BoxedSolid type for the // AUTHENTIC GROUND MODEL ctor half (task #15): complete BoxedSolid type for the
// collisionTemplate/collisionVolume extent reads + the template bottom lift. // collisionTemplate/collisionVolume extent reads + the template bottom lift.
#include <BOXSOLID.hpp> #include <BOXSOLID.hpp>
// STEP 6 cylinder hit-location table. dmgtable.hpp pulls in no subsystem
// headers (only Plug + mechrecon + <vector>), so it is safe here -- unlike the
// real subsystem headers, whose classes collide with mech.cpp's local stubs.
// The torso twist is reached via a BRIDGE (BTGetTorsoTwist, defined in torso.cpp)
// for the same reason.
#include "dmgtable.hpp"
extern Scalar BTGetTorsoTwist(Subsystem *torso); // torso.cpp (Torso complete there)
#if !defined(APP_HPP) #if !defined(APP_HPP)
# include <app.hpp> # include <app.hpp>
#endif #endif
@@ -394,14 +401,107 @@ Derivation
"Mech" "Mech"
); );
// Chain Mech's handler set to the parent's (Entity's, which Mover/JointedMover use // Mech's OWN handler table, chained to the parent's (Entity's, which Mover/
// directly -- they add none). An EMPTY set with no inheritance made Receiver::Receive // JointedMover use directly -- they add none). It OVERRIDES Entity's TakeDamage
// find NO handler for TakeDamageMessageID (and every other inherited message), so the // with the cylinder hit-location resolver (STEP 6): Build() overlays the derived
// engine base Entity::TakeDamageMessageHandler -- which routes damageZones[zone]-> // entry onto the inherited table by message ID (RECEIVER.cpp), so every other
// TakeDamage -- never ran and damage silently dropped. (Mech's OWN TakeDamage override // inherited message still routes to its base handler. (Previously this was a
// + cylinder lookup is the later STEP-6 reconstruction; this restores the base routing.) // bare copy of Entity's set -- base TakeDamage dropped unaimed/-1 zone hits.)
const Receiver::HandlerEntry
Mech::MessageHandlerEntries[] =
{
MESSAGE_ENTRY(Mech, TakeDamage),
};
Receiver::MessageHandlerSet Receiver::MessageHandlerSet
Mech::MessageHandlers(Entity::GetMessageHandlers()); Mech::MessageHandlers(
ELEMENTS(Mech::MessageHandlerEntries),
Mech::MessageHandlerEntries,
Entity::GetMessageHandlers());
//#############################################################################
// STEP 6 -- cylinder hit-location support (consumed by dmgtable.cpp)
//#############################################################################
//
// World -> mech-local point transform (binary FUN_00408bf8 reading owner+0xd0).
// Uses the inherited Entity::localToWorld (the render/collision object matrix);
// MultiplyByInverse gives the world->local change of coordinates.
//
Point3D
Mech::WorldToLocal(const Point3D &world)
{
Point3D local;
local.MultiplyByInverse(world, localToWorld);
return local;
}
//
// The roster torso (sinkSourceSubsystem @0x438, ClassID 0xBC5) -- parity with
// the binary's *(mech+0x438).
//
void *
Mech::TorsoOrientationSource()
{
return GetTorsoSubsystem();
}
//
// Live torso twist (yaw) in radians -- binary torso+0x1d8. Reached via the
// torso.cpp bridge (Torso is a complete type there; including torso.hpp here
// would collide with mech.cpp's local subsystem stubs). 0 for no/fixed torso.
//
Scalar
Mech::TorsoHeading()
{
return BTGetTorsoTwist(GetTorsoSubsystem());
}
//
// Height reference the cylinder table normalises impact height against -- the
// binary's *(mech+0x2ec)+0xc, a stance height written from mech+0x518 (standing)
// / +0x51c (ducked). standingTemplateMaxY is that value (collisionTemplate->maxY
// captured at ctor); fall back to the live template if the ground model was off
// when the ctor ran (standingTemplateMaxY left 0).
//
Scalar
Mech::CylinderReferenceHeight()
{
if (standingTemplateMaxY > 0.0f)
{
return standingTemplateMaxY;
}
BoxedSolid *tmpl = GetCollisionTemplate();
return tmpl ? tmpl->maxY : 0.0f; // ResolveHit guards <= 0
}
//
// Mech override of Entity::TakeDamageMessageHandler (binary @0x4a037a, the two
// call sites into the glue @0x49ed0c). An unaimed hit arrives with
// invalidDamageZone set (damageZone < 0); resolve its zone from the cylinder
// hit-location table (mech[0x111]) using the impact point, clear the flag, then
// hand off to the base handler which routes damageZones[zone]->TakeDamage. Aimed
// (reticle) hits carry a valid zone and pass straight through.
//
void
Mech::TakeDamageMessageHandler(TakeDamageMessage *message)
{
Check(message);
DamageLookupTable *table = (DamageLookupTable *)Wword(0x111);
if (message->invalidDamageZone && table != 0)
{
int zone = table->ResolveHit(message->damageData.impactPoint);
message->damageZone = zone;
message->invalidDamageZone = False;
if (BTEnvOn("BT_CYL_LOG", 0))
{
DEBUG_STREAM << "[cyl] unaimed hit -> zone " << zone
<< " (impact " << message->damageData.impactPoint.x << ","
<< message->damageData.impactPoint.y << ","
<< message->damageData.impactPoint.z << ")\n" << std::flush;
}
}
Entity::TakeDamageMessageHandler(message); // base: damageZones[zone]->TakeDamage
}
// //
// Mech attribute table. DENSE PREFIX 0x15..0x21 (JointedMover::NextAttributeID // Mech attribute table. DENSE PREFIX 0x15..0x21 (JointedMover::NextAttributeID
@@ -1208,17 +1308,42 @@ Mech::Mech(
} }
// //
// Cylinder damage-zone table (resource type 0x1d, by name) -- the per-impact zone // Cylinder hit-location table (resource type 0x1d = DamageLookupTableStream) --
// resolver consumed by Mech::TakeDamageMessageHandler for unaimed/-1 hits. STILL A // the per-impact zone resolver consumed by Mech::TakeDamageMessageHandler for
// STUB here (StandingAnimation placeholder); the real CylinderDamageZoneTable is // unaimed/-1 hits (STEP 6). Faithful to the ctor @part_012.c:10411-10425: the
// STEP 6a of the damage reconstruction. Left as-was so the rest builds; the empty // table is found by the mech's DamageZoneStream (type-0x14) NAME -- the offline
// name makes the lookup a no-op until then. // builder writes BOTH the 0x14 and 0x1d resources under the model name (see
// CreateDamageZoneStream ~line 1930), so they share it -- then streamed and
// cached at mech[0x111] (byte 0x444). (Was an empty-name StandingAnimation
// stub -> 0 rows; the real class is dmgtable.cpp.)
// //
char shadowName[32]; Wword(0x111) = 0;
shadowName[0] = '\0'; ResourceDescription *dzForName =
int animSeg = ResourceFindByName(shadowName, 0x1d); // FUN_00406ff8 (TODO STEP 6a: cylinder table) MechFindResource(creation_message->resourceID,
MemStreamX animMem(animSeg); ResourceDescription::DamageZoneStreamResourceType); // type 0x14 (for its name)
Wword(0x111) = (int)new (Memory::Allocate(0x2c)) StandingAnimation(this, &animMem); // FUN_0049ea48 ResourceDescription *cylRes = (dzForName != 0)
? application->GetResourceFile()->FindResourceDescription(
dzForName->resourceName,
ResourceDescription::DamageLookupTableStreamResourceType) // FUN_00406ff8, type 0x1d
: 0;
if (cylRes != 0)
{
cylRes->Lock();
{
DynamicMemoryStream cylStream( // FUN_004032dc, offset 0
cylRes->resourceAddress, cylRes->resourceSize, 0);
DamageLookupTable *table = new DamageLookupTable(this, &cylStream); // FUN_0049ea48
Wword(0x111) = (int)table;
DEBUG_STREAM << "[cyl] table '" << dzForName->resourceName
<< "' layers=" << table->LayerCount() << "\n" << std::flush;
}
cylRes->Unlock();
}
else
{
DEBUG_STREAM << "[cyl] no DamageLookupTable (type 0x1d) for '"
<< (dzForName ? dzForName->resourceName : "?") << "'\n" << std::flush;
}
// //
// Bind the three creation-name strings from the MakeMessage. // Bind the three creation-name strings from the MakeMessage.
@@ -1276,9 +1401,9 @@ Mech::~Mech()
<< "\n" << std::flush; << "\n" << std::flush;
} }
if (Wword(0x111) != 0) // standing animation if (Wword(0x111) != 0) // cylinder hit-location table (STEP 6)
{ {
((Releasable *)Wword(0x111))->Release(); delete (DamageLookupTable *)Wword(0x111); // frees layers/slices/entries
} }
// //
+14
View File
@@ -503,6 +503,9 @@ protected:
// //
public: public:
static Receiver::MessageHandlerSet MessageHandlers; // SharedData ctor operand static Receiver::MessageHandlerSet MessageHandlers; // SharedData ctor operand
// Mech's OWN handler table (overrides Entity's TakeDamage with the
// cylinder hit-location resolver); chained to Entity::GetMessageHandlers().
static const Receiver::HandlerEntry MessageHandlerEntries[];
enum { StateCount = 0x21 }; enum { StateCount = 0x21 };
// //
@@ -735,6 +738,17 @@ protected:
// --- damage-table support (dmgtable) -------------------------------- // --- damage-table support (dmgtable) --------------------------------
Point3D WorldToLocal(const Point3D &world); // FUN_00408bf8 (owner+0xd0) Point3D WorldToLocal(const Point3D &world); // FUN_00408bf8 (owner+0xd0)
void *TorsoOrientationSource(); // *(this+0x438) void *TorsoOrientationSource(); // *(this+0x438)
// Height reference the cylinder table normalises impact height against --
// the binary's *(mech+0x2ec)+0xc, a stance height set from mech+0x518
// (standing) / +0x51c (ducked). We return the standing value
// (collisionTemplate->maxY captured at ctor).
Scalar CylinderReferenceHeight();
// Live torso twist (yaw) in radians -- torso+0x1d8, via the roster torso
// (sinkSourceSubsystem @0x438). 0 for a fixed-torso mech.
Scalar TorsoHeading();
// Mech override of Entity::TakeDamageMessageHandler: resolve an unaimed
// (invalidDamageZone) hit's zone via the cylinder table, then base-route.
void TakeDamageMessageHandler(TakeDamageMessage *message);
// --- damage-routing support (mechdmg / mech4) ----------------------- // --- damage-routing support (mechdmg / mech4) -----------------------
// Typed access to the inherited Entity::damageZones[] (engine stores DamageZone*; // Typed access to the inherited Entity::damageZones[] (engine stores DamageZone*;
+11
View File
@@ -889,3 +889,14 @@ Subsystem *CreateTorsoSubsystem(Mech *owner, int id, void *seg)
return (Subsystem *) new (Memory::Allocate(0x280)) return (Subsystem *) new (Memory::Allocate(0x280))
Torso(owner, id, (Torso::SubsystemResource *)seg, Torso::DefaultData); Torso(owner, id, (Torso::SubsystemResource *)seg, Torso::DefaultData);
} }
//
// STEP 6 bridge -- expose the live torso twist (Torso::currentTwist == the binary
// torso+0x1d8) to mech.cpp / the cylinder damage table without pulling the Torso
// header (which collides with mech.cpp's local subsystem stubs) into mech.cpp.
// The roster torso @mech+0x438 is a Torso (ClassID 0xBC5); 0 when absent.
//
Scalar BTGetTorsoTwist(Subsystem *torso)
{
return torso ? ((Torso *)torso)->CurrentTwist() : 0.0f;
}
+3
View File
@@ -137,6 +137,9 @@ class Joint; // engine skeleton node (JOINT.h); the twist target
PoweredSubsystem::ElectricalState ElectricalStateLevel() const{ return (PoweredSubsystem::ElectricalState)watchdogAlarm.GetLevel(); } PoweredSubsystem::ElectricalState ElectricalStateLevel() const{ return (PoweredSubsystem::ElectricalState)watchdogAlarm.GetLevel(); }
LWord GetCreationTime() const { return (LWord)(long)lastPerformance; } LWord GetCreationTime() const { return (LWord)(long)lastPerformance; }
LWord GetCurrentTime() const { return (LWord)(long)lastPerformance; } LWord GetCurrentTime() const { return (LWord)(long)lastPerformance; }
// Current horizontal (yaw) torso aim in radians (binary torso+0x1d8); the
// damage-table PieSlice wheel reads this when RotateWithTorso is set.
Scalar CurrentTwist() const { return currentTwist; }
protected: protected:
// Resolve a named skeleton node to its live Joint* via the owning Mech // Resolve a named skeleton node to its live Joint* via the owning Mech
// (the ctor @004b6b0c inlines Mech::ResolveJoint == FUN_00424b60). Defined // (the ctor @004b6b0c inlines Mech::ResolveJoint == FUN_00424b60). Defined