The two deferred TakeDamage dispatches in Mech::ProcessCollision now fire,
unblocked by STEP 6 (the cylinder hit-location override that resolves zone==-1):
- Mover branch (:15324-15358): on a collision with another Mech, dispatch the
collision damage to it.
- CulturalIcon branch (:15369-15401): crunch dispatch to a building/tree/prop,
before the walk-through sentinel overwrites the amount.
Both go through a new file-scope helper BTDispatchCollisionDamage, which builds
an Entity::TakeDamageMessage{zone==-1} (the engine ctor -- same idiom as the
weapon-impact path) with the world centre of the overlap slice as the impact
point and this mech as the inflictor, then Dispatch()es it to the victim. The
receiver turns the world impact point into a damage zone: a Mech via its cylinder
table (STEP 6), an icon via its base handler (crushable props have no zones -> a
harmless no-op). Terrain (walls/hills) matches neither branch, so it still
BLOCKS without damage (faithful to the binary).
Faithful to the binary's raw DamageMessage dispatch (:15324-15401) but via the
engine's named TakeDamageMessage API (no databinding-trap field-by-field build);
the binary's inflictor global DAT_0050b9ac is a sentinel EntityID (only ever
read, never set -> a collision has no "shooter"), so this mech is the inflictor.
Verify: builds clean; reachability GUARANTEED (Mover::ProcessCollisionList calls
the VIRTUAL ProcessCollision -> Mech::ProcessCollision, on the active
AuthenticGroundAndCollide path); stable across runs; both mechs build their
cylinder tables. The live dispatch was not captured headlessly (the solo
auto-walker never rammed a tree/mech), but the path is proven reachable and
composed of runtime-verified pieces (the weapon TakeDamage path + STEP 6).
Also validated STEP 6's height ref: collisionTemplate->maxY ~= 7.1 (a real mech
height), confirming CylinderReferenceHeight reads a height (not the heat value
the mech+0x2ec dual-labeling hinted at).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
The cylinder table resource = type 29 DamageLookupTableStream, 18 in BTL4.RES (one per mech) ->
STEP 6 is FEASIBLE, not content-blocked. Name copied from a sibling resource (local_130+0xc).
RE phase complete; build (containers + handler + load + wire) is the next phase.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
De-risk + format-reversal pass complete. The CylinderDamageZoneTable is a passive nested-list
height x angle grid: TABLE (FUN_0049ea48) -> ROWS-by-height (FUN_0049e740, cell key i*2pi/count,
_DAT_0049e810=6.2831855) -> CELLS-by-angle (FUN_0049deb0) -> zone. All 3 vtables minimal (dtor +
2 Node slots, NO lookup method) -> the lookup is entirely in the unexported
Mech::TakeDamageMessageHandler (handler-set entry, not virtual) = the disassembly target.
Full ctor/dtor/vtable address map + the 4-step build plan recorded in combat-damage.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per the 'correct errors' convention: the cited FUN_004a0230/FUN_0049ed0c don't exist. The real
CylinderDamageZoneTable = a list of 0x30-byte entries (FUN_0049e740) from resource 0x1d; the recon
builds the table (FUN_0049ea48, mislabeled StandingAnimation @Mech[0x111]) with an EMPTY name -> 0
entries -> no-op. The lookup + Mech::TakeDamageMessageHandler override are NOT in the exported decomp
(need disassembly). Recorded in combat-damage + open-questions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Propagate the verified finding (0xBD3 = damage/explosion hub @0x434, NOT the valve/message
gate; the valve/Myomers gates read the owning BTPlayer @mech+0x190) into the topic files that
still carried the old claim: decomp-reference (ClassID row + both offset rows), gauges-hud,
subsystems, open-questions (Myomers coupling), + frontmatter. Graph validates clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The valve/Myomers 'gates' (FUN_004ac9c8 / FUN_004ad7d4) read the owning BTPlayer's
+0x274 / +0x260 (mech+0x190 = the player, bound by FUN_0049f624). So there's no new
subsystem to build -- the wiring is 2 gate accessors -> GetPlayerLink() named members.
Open: the true semantics of player+0x260/0x274 (scoring names showKills/roleClassIndex
may not match the gate use) + the FUN_004ad7d4 HeatModelActive-vs-OwnerAdvancedDamage
label conflict + these are likely MODE flags authentically-off in the basic mission.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Risk-4 investigation (before wiring WAVE 8) found my earlier 'keystone unblocks 4 things'
claim was wrong. Verified from the binary:
- 0xBD3 SubsystemMessageManager = a damage/explosion consolidation hub (ConsolidateAndSendDamage,
weaponExplosions), cached to Mech[0x10d]=0x434, mislabeled 'controlsMapper' in our recon (the
live drive squats there; the real mapper is roster slot 0 via SetMappingSubsystem).
- The valve MoveValve guard (FUN_004ac9c8 ->owner+0x190+0x274) AND the Myomers gate
(FUN_004ad7d4 ->owner+0x190+0x260) read a DIFFERENT object at mech+0x190 (!= 0x434). So 0xBD3
does NOT gate the valve/Myomers/MessageBoard; mech+0x190 (unidentified, no decompiled writer yet)
is the real keystone for those gauge-adjacent routes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>