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:
co-authored by
Claude Opus 4.8
parent
33fee712e9
commit
d07ac7dd49
+91
-14
@@ -53,8 +53,8 @@ offsets). [T2]
|
||||
**Base handler IGNORES zone==−1** (`Entity::TakeDamageMessageHandler`, ENTITY.cpp:878 — returns on
|
||||
`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
|
||||
model**, the deferred **STEP 6**) is NOT reconstructed — meanwhile send a VALID zone (mech4 aims a vital
|
||||
zone). **Investigated 2026-07 (durable):** the earlier-cited addresses `FUN_004a0230`/`FUN_0049ed0c` do
|
||||
model**, **STEP 6**) is now **RECONSTRUCTED + runtime-verified [T2]** — see "STEP 6 COMPLETE" below.
|
||||
(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
|
||||
(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
|
||||
@@ -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
|
||||
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`,
|
||||
vtable 0x50bd90, `this[0xb]`=cell count, cell key `i·(2π/count)` — `_DAT_0049e810`=**6.2831855=2π**;
|
||||
`this[4]`=mech+0x438) → CELLS by ANGLE (0x2c, ctor `FUN_0049deb0` / dtor `0x49df80`, vtable 0x50bd9c) →
|
||||
zone ref(s). All three vtables are MINIMAL (dtor + 2 Node slots, no method) → **the lookup is entirely in
|
||||
the unexported `Mech::TakeDamageMessageHandler`** (a handler-SET entry, not a vtable slot — ENTITY.h has
|
||||
no `virtual`; find it via the Mech message-handler registration, then disassemble). So STEP 6 remaining =
|
||||
(1) fix the resource-name load (build the table WITH data, not the empty-name stub); (2) reconstruct the 3
|
||||
container classes from the captured ctors; (3) disassemble + reconstruct `Mech::TakeDamageMessageHandler`
|
||||
(compute the impactPoint's local height+angle → index the grid → dispatch to `damageZones[zone]->TakeDamage`);
|
||||
(4) register the handler + verify unaimed hits distribute across zones. A genuine multi-pass reconstruction
|
||||
(the container classes are tedious; the handler is the disassembly). **Content confirmed (2026-07): the
|
||||
resource is type 29 = `DamageLookupTableStream`, 18 present in `BTL4.RES` (one per mech) — so STEP 6 is
|
||||
FEASIBLE, not content-blocked.** The recon's empty-name stub (mech.cpp:1219) must be fixed to the real
|
||||
vtable 0x50bd90, `this[3]`=RotateWithTorso flag, `this[4]`=torso (mech+0x438), `this[5]`=cell list,
|
||||
`this[0xb]`=cell count, cell key `i·(2π/count)` — `_DAT_0049e810`=**6.2831855=2π**) → CELLS by ANGLE
|
||||
(0x2c, ctor `FUN_0049deb0` / dtor `0x49df80`, vtable 0x50bd9c, `this[4]`=a refcounted zone-dict filled
|
||||
by `FUN_0049e5e4`, count-prefixed {int→int} entries) → zone ref(s).
|
||||
**⚠ CORRECTION (2026-07, verified from decomp): the lookup is NOT "entirely in the unexported handler"
|
||||
— it is EXPORTED pseudocode [T1].** Two functions:
|
||||
- `FUN_0049eb54` (@0049eb54, TABLE-level, `void(table, float* worldImpactPoint)` — Ghidra mistypes the
|
||||
return; it tail-returns the found cell): (1) transform world impact → **mech-local** via
|
||||
`FUN_00408bf8`+`FUN_00408440` reading the mech's transform at `mech+0xd0`; (2) **HEIGHT→row**:
|
||||
`mechHeight = *(*(mech+0x2ec)+0xc)`; clamp `local.y` to `[0,mechHeight]`;
|
||||
`rowIndex = clamp(floor(rowCount·y/mechHeight), 0, rowCount-1)`; `row = rowList.Find(&rowIndex)`
|
||||
(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
|
||||
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
|
||||
|
||||
@@ -71,13 +71,26 @@ authentic path scoped.
|
||||
- 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);
|
||||
airborne callbacks (`FUN_004a6344`/`FUN_004a7970`).
|
||||
- Wall-block-vs-climb tuning; collision DAMAGE application (computed, not applied — blocked on the
|
||||
cylinder hit-location model / STEP 6, below, to resolve the impact point → zone; else aim a fixed zone).
|
||||
- **Cylinder hit-location (STEP 6)** — the Mech override that maps an unaimed hit's impactPoint → a damage
|
||||
zone. Investigated 2026-07 (see [[combat-damage]]): the table (list of 0x30-byte entries, `FUN_0049e740`,
|
||||
resource type 0x1d) IS built (recon `StandingAnimation` @Mech[0x111], but with an EMPTY name → 0 entries);
|
||||
the lookup + `Mech::TakeDamageMessageHandler` override are NOT in the exported decomp (need disassembly).
|
||||
Meatier than a captured-fn reconstruction — the cited `FUN_004a0230`/`FUN_0049ed0c` don't exist.
|
||||
- Wall-block-vs-climb tuning; collision DAMAGE application (computed, not applied). **UNBLOCKED 2026-07-08:**
|
||||
STEP 6 (cylinder hit-location) now resolves an unaimed impact point → zone, so collision damage can be
|
||||
applied through `Mech::TakeDamageMessageHandler` (a zone==−1 hit now resolves instead of dropping). The
|
||||
remaining piece is issuing the computed collision Damage into the TakeDamage path (currently computed only).
|
||||
- **✅ Cylinder hit-location (STEP 6) — DONE 2026-07-08 [T2].** Built + runtime-verified: the `dmgtable.cpp`
|
||||
classes now have real storage + a working `ResolveHit→zone`, the mech ctor loads the type-0x1d table by the
|
||||
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
|
||||
skin. The wreck-stays behavior we ship is faithful; do NOT issue DestroyEntityMessage on death.
|
||||
|
||||
|
||||
+125
-344
@@ -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,293 +48,150 @@
|
||||
//
|
||||
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
|
||||
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())
|
||||
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
|
||||
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
|
||||
@@ -358,125 +199,65 @@ int
|
||||
//
|
||||
DamageLookupTable::DamageLookupTable(
|
||||
Mech *owner,
|
||||
GenericObjectStream *stream
|
||||
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
|
||||
{
|
||||
// detaches 'layers', clears Plug base
|
||||
}
|
||||
|
||||
Logical
|
||||
DamageLookupTable::TestInstance() const // @0x49eb48 / @0x49e880
|
||||
for (unsigned i = 0; i < layers.size(); ++i)
|
||||
{
|
||||
return True;
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
+88
-165
@@ -6,257 +6,180 @@
|
||||
// Date Who Modification //
|
||||
// -------- --- ---------------------------------------------------------- //
|
||||
// --/--/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 //
|
||||
// PROPRIETARY AND CONFIDENTIAL //
|
||||
//===========================================================================//
|
||||
//
|
||||
// RECONSTRUCTED from the shipped binary. No header survived for this module
|
||||
// (dmgtable.hpp/.cpp). The class cluster lives at @0x49de14..@0x49f5fb, was
|
||||
// found ADJACENT to (but separate from) the DamageZone code in mechdmg.cpp,
|
||||
// and is streamed from the ".tbl" notation file by the mech's
|
||||
// CreateDamageZoneStream (@0x4a474c, mech.cpp) -- caller sites @0x49ed28 is
|
||||
// invoked from the per-zone parser @line~11580 and the streamed form @0x49ea48
|
||||
// from the object-stream reader @line~10423.
|
||||
// RECONSTRUCTED from the shipped binary. Behaviour follows the Ghidra
|
||||
// pseudo-C for the cluster @0x49de14..@0x49f5fb. The three-level resolver is:
|
||||
//
|
||||
// Identifying strings (data @0x50bc00..0x50bd00):
|
||||
// "DamageZone" "video" "skeleton" "%s %f" "RotateWithTorso" "PieSlice"
|
||||
// "Layer" "TableEntryOf" "DamageZone Table Format!" "Not Found!"
|
||||
// "percentages do not sum to 100%(i.e., 1.0)!" "missing RotateWithTorso!"
|
||||
// "error in damage table format" " has bad format!"
|
||||
// vtables: 0x50bd84 (DamageLookupTable), 0x50bd90 (PieSlice),
|
||||
// 0x50bd9c (DamageZonePercentTable / leaf).
|
||||
//
|
||||
// The table is a three-level hit-location resolver:
|
||||
//
|
||||
// DamageLookupTable -- stack of armour LAYERS (penetration depth)
|
||||
// DamageLookupTable -- stack of armour LAYERS by penetration depth (height)
|
||||
// -> PieSlice -- an angular sector wheel (optionally torso-relative)
|
||||
// -> DamageZonePercentTable
|
||||
// -- a weighted list of {zone segment index, percentage}
|
||||
// whose percentages must sum to 1.0; a uniform random
|
||||
// roll then selects the final damage-zone index.
|
||||
// -- a weighted list of {cumulative %, zone segment index}
|
||||
// whose percentages rise to 1.0; a uniform random roll
|
||||
// then selects the final damage-zone index.
|
||||
//
|
||||
// Class names are reconstructed from the .tbl section keywords; member names
|
||||
// are inferred from usage and flagged best-effort. See dmgtable.cpp for the
|
||||
// per-method @ADDR evidence.
|
||||
// Stream format (type-29 DamageLookupTableStream), byte-verified against the
|
||||
// shipped BTL4.RES (18 tables, EXACT consumption):
|
||||
// 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)
|
||||
# define DMGTABLE_HPP
|
||||
|
||||
#include <vector>
|
||||
|
||||
#if !defined(PLUG_HPP)
|
||||
# include <plug.hpp>
|
||||
#endif
|
||||
#if !defined(TABLE_HPP)
|
||||
# include <table.hpp>
|
||||
#endif
|
||||
|
||||
#include "mechrecon.hpp" // reconstruction shim
|
||||
#include "mechrecon.hpp" // reconstruction shim (Scalar, DebugStream, ...)
|
||||
|
||||
//##################### Forward Class Declarations #######################
|
||||
class Mech;
|
||||
class NotationFile;
|
||||
class GenericObjectStream;
|
||||
class MemoryStream;
|
||||
class Point3D;
|
||||
|
||||
//###########################################################################
|
||||
//##################### DamageZonePercentTable ##########################
|
||||
//###########################################################################
|
||||
//
|
||||
// Leaf node. Holds a sorted, cumulative-weighted list of damage-zone
|
||||
// segment indices and the uniform random "roll" that turns a [0,1) draw into
|
||||
// one of those zones. Built from a ".tbl" "DamageZone" page whose lines are
|
||||
// "<zoneName> <percentage>" ("%s %f") and whose percentages must total 1.0.
|
||||
// (vtable @0x50bd9c, ctor @0x49deb0, dtor @0x49df80, object size 0x2c.)
|
||||
// Leaf node. Holds a sorted, cumulative-weighted list of damage-zone segment
|
||||
// indices; a uniform random roll turns a [0,1) draw into one of those zones.
|
||||
// (vtable @0x50bd9c, ctor @0x49deb0, dtor @0x49df80.)
|
||||
//
|
||||
class DamageZonePercentTable:
|
||||
public Plug
|
||||
{
|
||||
public:
|
||||
//
|
||||
// Stream constructor -- rebuilds the entry list from an object stream.
|
||||
// @0x49deb0 (FUN_004178cc base Plug ctor, then ReadEntries @0x49e5e4)
|
||||
//
|
||||
DamageZonePercentTable() {} // reconstruction: stack-temp default ctor
|
||||
DamageZonePercentTable(
|
||||
struct Entry // binary: zoneIndex@+0xc, cumulative@+0x14
|
||||
{
|
||||
Scalar cumulative; // ascending sort key (rises to 1.0)
|
||||
int zoneIndex; // damageZones[] / skeleton segment index
|
||||
};
|
||||
|
||||
DamageZonePercentTable() {} // stack-temp / offline builder
|
||||
DamageZonePercentTable( // @0x49deb0 (ReadEntries @0x49e5e4)
|
||||
Mech *owner,
|
||||
GenericObjectStream *stream
|
||||
MemoryStream *stream
|
||||
);
|
||||
~DamageZonePercentTable(); // @0x49df80
|
||||
|
||||
Logical
|
||||
TestInstance() const; // @0x49e000 (returns True)
|
||||
Logical TestInstance() const { return True; } // @0x49e000
|
||||
|
||||
//
|
||||
// @0x49de14 -- uniform random weighted roll. Walks the entries in
|
||||
// ascending cumulative-percentage order, draws a [0,1) sample
|
||||
// (FUN_00408050) and returns the segment index (entry+0xc) of the first
|
||||
// entry whose cumulative threshold (entry+0x14) exceeds the draw.
|
||||
// Returns -1 if the list is empty.
|
||||
// @0x49de14 -- uniform weighted roll: draw a [0,1) sample (RandomUnit,
|
||||
// FUN_00408050) and return the zone index of the first entry whose
|
||||
// cumulative threshold exceeds the draw; -1 if the list is empty.
|
||||
//
|
||||
int
|
||||
SelectZone() const;
|
||||
int SelectZone() const;
|
||||
|
||||
//
|
||||
// @0x49e00c -- parse a "DamageZone" page from the model .tbl: read the
|
||||
// "video"/"skeleton" model, then each "<zone> <pct>" line; resolve the
|
||||
// zone name to a skeleton segment index (Get_Segment_Index @0x4274f8),
|
||||
// accumulate the percentages and verify the total == 1.0.
|
||||
// Returns False (and logs) on a bad line, an unknown zone, or a total
|
||||
// that does not sum to 1.0.
|
||||
// @0x49e5e4 -- read the leading cell NAME string (i32 len + len+1 bytes
|
||||
// incl. NUL, per FUN_00402948), then the entry count and that many
|
||||
// {cumulative, zoneIndex} pairs.
|
||||
//
|
||||
static int
|
||||
BuildFromNotation(
|
||||
NotationFile *model_file,
|
||||
const char *model_name,
|
||||
const char *page_name,
|
||||
NotationFile *table_file,
|
||||
Mech *owner,
|
||||
GenericObjectStream *stream
|
||||
);
|
||||
void ReadEntries(MemoryStream *stream);
|
||||
|
||||
//
|
||||
// @0x49e524 WriteEntries (serialise count + {threshold,zone} pairs),
|
||||
// @0x49e5e4 ReadEntries (deserialise into the entry table).
|
||||
//
|
||||
void WriteEntries(GenericObjectStream *stream) const; // @0x49e524
|
||||
void ReadEntries (GenericObjectStream *stream); // @0x49e5e4
|
||||
// (offline @0x49e00c BuildFromNotation / @0x49e524 WriteEntries -- the
|
||||
// content-build authoring path -- is not part of the runtime port; the
|
||||
// shipped .RES is pre-built. Omitted here.)
|
||||
|
||||
protected:
|
||||
Mech *owner; // @0x0c param_1[3]
|
||||
void *refCount; // @0x10 param_1[4] (MemoryBlock/RefCount)
|
||||
ReconTable<int> entries; // @0x14 param_1+5 sorted by cumulative %
|
||||
// each entry: zoneIndex@+0xc, cumulativePercent@+0x14
|
||||
Mech *owner; // @0x0c
|
||||
std::vector<Entry> entries; // @0x14 (was ReconTable) -- sorted ascending
|
||||
};
|
||||
|
||||
//###########################################################################
|
||||
//############################# PieSlice ################################
|
||||
//###########################################################################
|
||||
//
|
||||
// An angular sector "wheel". N equal-angle slices (each 2*PI/N radians) wrap
|
||||
// a circle; the slice that contains the incoming hit direction is chosen and
|
||||
// its leaf DamageZonePercentTable is rolled. When "RotateWithTorso" is set
|
||||
// the incoming angle is measured relative to the live torso orientation
|
||||
// (orientationSource+0x1d8), so the slice layout turns with the mech's torso.
|
||||
// (vtable @0x50bd90, ctor @0x49e740, object size 0x30.)
|
||||
// An angular sector "wheel". N equal-angle slices (each 2*PI/N rad); the slice
|
||||
// containing the incoming hit direction is chosen and its leaf table rolled.
|
||||
// When rotateWithTorso is set the incoming angle is measured relative to the
|
||||
// live torso twist (owner->TorsoHeading, == torso+0x1d8).
|
||||
// (vtable @0x50bd90, ctor @0x49e740.)
|
||||
//
|
||||
class PieSlice:
|
||||
public Plug
|
||||
{
|
||||
public:
|
||||
//
|
||||
// @0x49e740 -- stream constructor. Reads owner@+0xc and sliceCount@+0x2c,
|
||||
// builds 'sliceCount' DamageZonePercentTable children (@0x49deb0) each
|
||||
// keyed to a cumulative angle i*(2*PI/sliceCount), and caches the torso
|
||||
// orientation source = owner+0x438.
|
||||
//
|
||||
PieSlice(
|
||||
PieSlice() : owner(0), rotateWithTorso(0), sliceCount(0) {}
|
||||
PieSlice( // @0x49e740
|
||||
Mech *owner,
|
||||
GenericObjectStream *stream
|
||||
MemoryStream *stream
|
||||
);
|
||||
~PieSlice();
|
||||
|
||||
//
|
||||
// @0x49e678 -- pick a slice for incoming angle 'theta' (radians). When
|
||||
// rotateWithTorso is set, theta is shifted by the torso orientation
|
||||
// (orientationSource+0x1d8) and wrapped into [0, 2*PI). The slice index
|
||||
// = floor(theta * sliceCount / (2*PI)) feeds the child table lookup.
|
||||
// @0x49e678 -- pick the slice for incoming angle 'theta' (radians). When
|
||||
// rotateWithTorso, theta is shifted by the live torso twist and wrapped
|
||||
// into [0, 2*PI); slice index = floor(theta * sliceCount / 2*PI).
|
||||
//
|
||||
DamageZonePercentTable *
|
||||
SelectSlice(Scalar theta) const;
|
||||
|
||||
//
|
||||
// @0x49e88c -- parse a "PieSlice" page: read the mandatory bool
|
||||
// "RotateWithTorso" (logged "missing RotateWithTorso!" if absent), then
|
||||
// each named sub-page as a DamageZonePercentTable (@0x49e00c).
|
||||
// Logs "error in damage table format" on a malformed entry.
|
||||
//
|
||||
static int
|
||||
BuildFromNotation(
|
||||
NotationFile *model_file,
|
||||
const char *model_name,
|
||||
const char *page_name,
|
||||
NotationFile *table_file,
|
||||
Mech *owner,
|
||||
GenericObjectStream *stream
|
||||
);
|
||||
DamageZonePercentTable *SelectSlice(Scalar theta) const;
|
||||
|
||||
protected:
|
||||
Mech *owner; // @0x0c param_1[3]
|
||||
void *orientationSource; // @0x10 param_1[4] = owner+0x438 (torso)
|
||||
ReconTable<DamageZonePercentTable*>
|
||||
slices; // @0x14 param_1+5
|
||||
int rotateWithTorso; // @0x28 (read first in BuildFromNotation)
|
||||
int sliceCount; // @0x2c param_1[0xb]
|
||||
Mech *owner; // @0x0c
|
||||
std::vector<DamageZonePercentTable*>
|
||||
slices; // @0x14 (was ReconTable)
|
||||
int rotateWithTorso; // @0x0c-flag in binary (this[3])
|
||||
int sliceCount; // @0x2c (this[0xb])
|
||||
};
|
||||
|
||||
//###########################################################################
|
||||
//######################### DamageLookupTable ###########################
|
||||
//###########################################################################
|
||||
//
|
||||
// Top-level table streamed from the ".tbl" key for a mech. A stack of armour
|
||||
// LAYERS: the deeper a shot penetrates, the higher the layer index selected.
|
||||
// Each layer is a PieSlice wheel. (vtable @0x50bd84, ctor @0x49ea48,
|
||||
// dtor @0x49eadc, object size 0x30.)
|
||||
// Top-level table streamed from the mech's ".tbl". A stack of armour LAYERS:
|
||||
// the higher the local-frame impact, the higher the layer index. Each layer
|
||||
// is a PieSlice wheel. (vtable @0x50bd84, ctor @0x49ea48, dtor @0x49eadc.)
|
||||
//
|
||||
class DamageLookupTable:
|
||||
public Plug
|
||||
{
|
||||
public:
|
||||
//
|
||||
// @0x49ea48 -- stream constructor. Reads layerCount@+0x28 and builds
|
||||
// that many PieSlice layers (@0x49e740).
|
||||
//
|
||||
DamageLookupTable(
|
||||
DamageLookupTable( // @0x49ea48
|
||||
Mech *owner,
|
||||
GenericObjectStream *stream
|
||||
MemoryStream *stream
|
||||
);
|
||||
~DamageLookupTable(); // @0x49eadc
|
||||
|
||||
Logical
|
||||
TestInstance() const; // @0x49eb48 / @0x49e880 (returns True)
|
||||
Logical TestInstance() const { return True; } // @0x49eb48
|
||||
|
||||
//
|
||||
// @0x49eb54 -- resolve a hit to a damage zone. Transforms the impact
|
||||
// point into the mech frame (owner+0xd0), derives a penetration-depth
|
||||
// fraction (clamped against the armour reference at owner+0x2ec[+0xc]),
|
||||
// maps it to a layer index, fetches that layer's PieSlice and forwards
|
||||
// an incident angle (computed from the hit direction, or a random draw
|
||||
// when the direction is degenerate) to PieSlice::SelectSlice (@0x49e678).
|
||||
// @0x49eb54 (+ @0x49e678 slice + @0x49de14 roll, chained by glue @0x49ed0c)
|
||||
// -- resolve a world impact point to a damage-zone index. Returns -1 when
|
||||
// the table is empty or the roll falls through (treated as a miss).
|
||||
//
|
||||
void
|
||||
ResolveHit(const Point3D &impact) const;
|
||||
int ResolveHit(const Point3D &impact) const;
|
||||
|
||||
//
|
||||
// @0x49ed28 -- parse the layered table. Reads consecutive "Layer1",
|
||||
// "Layer2", ... pages (number suffix via FUN_004dd3ec), parsing each as
|
||||
// a PieSlice page (@0x49e88c). Logs "<name> has bad format!" when no
|
||||
// layers are found.
|
||||
//
|
||||
static int
|
||||
BuildFromNotation(
|
||||
NotationFile *model_file,
|
||||
const char *model_name,
|
||||
NotationFile *table_file,
|
||||
Mech *owner,
|
||||
GenericObjectStream *stream
|
||||
);
|
||||
int LayerCount() const { return layerCount; } // bring-up verify
|
||||
|
||||
protected:
|
||||
Mech *owner; // @0x0c param_1[3]
|
||||
ReconTable<PieSlice*>
|
||||
layers; // @0x10 param_1+4
|
||||
int layerCount; // @0x28 param_1[10]
|
||||
Mech *owner; // @0x0c
|
||||
std::vector<PieSlice*>
|
||||
layers; // @0x10 (was ReconTable)
|
||||
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
|
||||
|
||||
+144
-19
@@ -72,6 +72,13 @@
|
||||
// AUTHENTIC GROUND MODEL ctor half (task #15): complete BoxedSolid type for the
|
||||
// collisionTemplate/collisionVolume extent reads + the template bottom lift.
|
||||
#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)
|
||||
# include <app.hpp>
|
||||
#endif
|
||||
@@ -394,14 +401,107 @@ Derivation
|
||||
"Mech"
|
||||
);
|
||||
|
||||
// Chain Mech's handler set to the parent's (Entity's, which Mover/JointedMover use
|
||||
// directly -- they add none). An EMPTY set with no inheritance made Receiver::Receive
|
||||
// find NO handler for TakeDamageMessageID (and every other inherited message), so the
|
||||
// engine base Entity::TakeDamageMessageHandler -- which routes damageZones[zone]->
|
||||
// TakeDamage -- never ran and damage silently dropped. (Mech's OWN TakeDamage override
|
||||
// + cylinder lookup is the later STEP-6 reconstruction; this restores the base routing.)
|
||||
// Mech's OWN handler table, chained to the parent's (Entity's, which Mover/
|
||||
// JointedMover use directly -- they add none). It OVERRIDES Entity's TakeDamage
|
||||
// with the cylinder hit-location resolver (STEP 6): Build() overlays the derived
|
||||
// entry onto the inherited table by message ID (RECEIVER.cpp), so every other
|
||||
// inherited message still routes to its base handler. (Previously this was a
|
||||
// bare copy of Entity's set -- base TakeDamage dropped unaimed/-1 zone hits.)
|
||||
const Receiver::HandlerEntry
|
||||
Mech::MessageHandlerEntries[] =
|
||||
{
|
||||
MESSAGE_ENTRY(Mech, TakeDamage),
|
||||
};
|
||||
|
||||
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
|
||||
@@ -1208,17 +1308,42 @@ Mech::Mech(
|
||||
}
|
||||
|
||||
//
|
||||
// Cylinder damage-zone table (resource type 0x1d, by name) -- the per-impact zone
|
||||
// resolver consumed by Mech::TakeDamageMessageHandler for unaimed/-1 hits. STILL A
|
||||
// STUB here (StandingAnimation placeholder); the real CylinderDamageZoneTable is
|
||||
// STEP 6a of the damage reconstruction. Left as-was so the rest builds; the empty
|
||||
// name makes the lookup a no-op until then.
|
||||
// Cylinder hit-location table (resource type 0x1d = DamageLookupTableStream) --
|
||||
// the per-impact zone resolver consumed by Mech::TakeDamageMessageHandler for
|
||||
// unaimed/-1 hits (STEP 6). Faithful to the ctor @part_012.c:10411-10425: the
|
||||
// table is found by the mech's DamageZoneStream (type-0x14) NAME -- the offline
|
||||
// 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];
|
||||
shadowName[0] = '\0';
|
||||
int animSeg = ResourceFindByName(shadowName, 0x1d); // FUN_00406ff8 (TODO STEP 6a: cylinder table)
|
||||
MemStreamX animMem(animSeg);
|
||||
Wword(0x111) = (int)new (Memory::Allocate(0x2c)) StandingAnimation(this, &animMem); // FUN_0049ea48
|
||||
Wword(0x111) = 0;
|
||||
ResourceDescription *dzForName =
|
||||
MechFindResource(creation_message->resourceID,
|
||||
ResourceDescription::DamageZoneStreamResourceType); // type 0x14 (for its name)
|
||||
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.
|
||||
@@ -1276,9 +1401,9 @@ Mech::~Mech()
|
||||
<< "\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
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -503,6 +503,9 @@ protected:
|
||||
//
|
||||
public:
|
||||
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 };
|
||||
|
||||
//
|
||||
@@ -735,6 +738,17 @@ protected:
|
||||
// --- damage-table support (dmgtable) --------------------------------
|
||||
Point3D WorldToLocal(const Point3D &world); // FUN_00408bf8 (owner+0xd0)
|
||||
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) -----------------------
|
||||
// Typed access to the inherited Entity::damageZones[] (engine stores DamageZone*;
|
||||
|
||||
@@ -889,3 +889,14 @@ Subsystem *CreateTorsoSubsystem(Mech *owner, int id, void *seg)
|
||||
return (Subsystem *) new (Memory::Allocate(0x280))
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -137,6 +137,9 @@ class Joint; // engine skeleton node (JOINT.h); the twist target
|
||||
PoweredSubsystem::ElectricalState ElectricalStateLevel() const{ return (PoweredSubsystem::ElectricalState)watchdogAlarm.GetLevel(); }
|
||||
LWord GetCreationTime() 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:
|
||||
// Resolve a named skeleton node to its live Joint* via the owning Mech
|
||||
// (the ctor @004b6b0c inlines Mech::ResolveJoint == FUN_00424b60). Defined
|
||||
|
||||
Reference in New Issue
Block a user