From 14ff3519ac80e5571cc1a0ce19784fa024b54259 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Thu, 30 Jul 2026 17:04:24 -0500 Subject: [PATCH] #83: the COLLISION DIVERT -- type-0 damage prices as internal rattle, never armor The crash self-damage reconstruction (9c83a46) exposed its missing second half: the binary's TakeDamage hub DIVERTS damageType 0 (@0x4a0368) into FUN_0049ffcc -- an export-gap distributor recovered by raw disasm -- so collision damage NEVER reaches the zone/armor loop. Without the divert the mover's raw kinetic-energy figure (60-ton mech: ~1000+ per wall tap, 90k+ on a hard slam) fell into the WEAPON loop: 'tapped a wall and died instantly'. The distributor, byte-anchored: gate on the owning player's advancedDamage copy (+0x268 -- the manual's 'splash/collision damage' technician setting); scale = (2000/moverMass) / (100 km/h in u/s)^2 / (1 - elasticity^2) (tbyte 1/3.6 @0x4a0148; mass @+0x20c; elasticity @+0x244); scaled < 0.5 is FREE; else Round(2x) sub-hits of 0.5 land on random INTERNAL subsystems (HeatSink family / Gyroscope / Torso, drawn by collisionCriticalHitWeight @0x10C) via ApplyDamageAndMeasure (@0x4ac07c). Bridges: family derivations (heat/gyro/torso.cpp), BTPlayerAdvancedDamageOn (btplayer.cpp). Verified: bhk1 wall test -- 94k slam = 4.2 rattle pts in 8 sub-hits, taps free (0.049 < 0.5), armor untouched. A/B: arena wall-grind now 0 deaths, 27 rattle events, the limper STAYS GIMPED and peers' replicants re-enter the gimp cycle between bounded staggers; grass circling unchanged (198 replicant gimp entries). [crashdmg]/[colldmg] probes document the pricing. Co-Authored-By: Claude Fable 5 --- context/combat-damage.md | 17 +++++++ context/locomotion.md | 23 ++++++--- game/reconstructed/btplayer.cpp | 8 +++ game/reconstructed/btplayer.hpp | 1 + game/reconstructed/gyro.cpp | 12 +++++ game/reconstructed/heat.cpp | 12 +++++ game/reconstructed/mech.cpp | 88 +++++++++++++++++++++++++++++++++ game/reconstructed/mech.hpp | 5 ++ game/reconstructed/mech4.cpp | 9 ++++ game/reconstructed/mechsub.hpp | 5 ++ game/reconstructed/torso.cpp | 12 +++++ 11 files changed, 184 insertions(+), 8 deletions(-) diff --git a/context/combat-damage.md b/context/combat-damage.md index bb32e3f..abc43f1 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -872,6 +872,23 @@ its decomp writers (4668/10512) read as a per-frame state toggle, not a clean co port's `showDamageInflicted` label is itself a guess; port treats authored `SplashRadius>0` as the enable. See [[open-questions]]. +## Collision damage — the type-0 DIVERT + rattle distributor (#83, 2026-07-30) [T1 disasm/T2] +`Mech::TakeDamageMessageHandler` diverts `damageType==0` (@0x4a0368) into +`DistributeCollisionDamage` (FUN_0049ffcc, export gap, raw disasm `scratchpad/night6/ +gap_49ffcc.txt`): collision damage **never reaches the zone/armor loop**. Gate: the owning +player's advancedDamage copy (+0x268, `BTPlayerAdvancedDamageOn` bridge) — the manual's +"splash/collision damage" technician setting. Pricing: `raw × (2000/moverMass)/(100·(1/3.6))²/ +(1−elasticity²)` (constants @0x4a0148=1/3.6 tbyte, 2000f, 0.5f threshold; mass@+0x20c, +elasticity@+0x244 by Mover-layout walk); scaled <0.5 = FREE. Above: `n=Round(2×amount)` +sub-hits of amount/n, each landed on ONE roster subsystem drawn by cumulative +`collisionCriticalHitWeight` (@0x10C, MechSubsystem) vs a [0,1) roll, eligibility = +IsDerivedFrom HeatSink-family/Gyroscope/Torso (GUIDs 0x50e590/0x50fdc0/0x510b08; all +MechSubsystem-based, so +0x10C is valid), applied via `ApplyDamageAndMeasure` (@0x4ac07c — +the subsystem TakeDamage virtual into its PRIVATE crit zone). Un-won rolls land nowhere +(weights un-normalized — faithful). Mech-vs-mech ram damage arriving as type 0 prices the +same way. Measured on bhk1 (mass 60000, e=0.2): scale 4.5e-5; a 94k spawn-slam = 4.2 rattle +points; taps free. `[colldmg]` (BT_DMG_LOG) + `[crashdmg]` (always-on, rare) are the probes. + ## Key Relationships - Weapons/roster: [[subsystems]]. Aim source: [[locomotion]] (drive/facing). Effects: [[rendering]]. - P5 forensics: `docs/HARD_PROBLEMS.md`. Data: [[decomp-reference]] §4-5. diff --git a/context/locomotion.md b/context/locomotion.md index e70103e..6facc7e 100644 --- a/context/locomotion.md +++ b/context/locomotion.md @@ -223,14 +223,21 @@ churn — gliding + trn leg-lifts = "skating". 4-config A/B (`scratchpad/night6/ `[animind]` inst=1 verdict): grass straight/circle clean (198 gimp entries; **17 car crunches during it — crushable icons are the `0.00123f` sentinel path, gyro kick only, NO broadcast**); arena1 single strike recovers (196); arena1 wall-grind = 0 gimp entries. -**What actually bounds the loop in the binary — and what the port was missing — is the crash -SELF-DAMAGE**: the blocking branch (export gap @4aa89f-4aaab4, raw disasm -`scratchpad/night6/gap_4a9770.txt`) fills `throttleState`=surface material, -`fallDirection`=worldToLocal(dmg.damageForce) (FUN_0040879c = Mᵀv, un-normalized), -`fallScalar`=−(fallDirection·localVelocity.linearMotion), then **Dispatches -`TakeDamageMessage{0x64, zone=−1, the engine-computed collision Damage verbatim}` at its own -mech** — wall crashes hurt, grinding degrades the mech, the storm self-limits. That dispatch was -the port's long-standing "DEFERRED" note; now reconstructed (mech4.cpp blocking branch). +**What the port was missing is the crash SELF-DAMAGE *and its own pricing path*** (two halves; +the first alone one-shot mechs on wall taps — corrected same day): the blocking branch (export +gap @4aa89f-4aaab4, `scratchpad/night6/gap_4a9770.txt`) fills `fallDirection`=worldToLocal( +dmg.damageForce) (FUN_0040879c = Mᵀv, un-normalized), `fallScalar`=−(fallDirection·localVelocity +.linearMotion), then **Dispatches `TakeDamageMessage{0x64, zone=−1, the collision Damage +verbatim}` at its own mech** (was the long-standing "DEFERRED" note; mech4.cpp). CRUCIALLY the +TakeDamage hub then **DIVERTS damageType 0 (@0x4a0368 → FUN_0049ffcc, gap disasm +`gap_49ffcc.txt`) — collision damage NEVER touches zone armor.** It is the manual's +"splash/collision damage" technician setting: gated on the player's advancedDamage copy +(+0x268), priced `raw × (2000/mass)/(100/3.6)²/(1−elasticity²)` (a 60-ton mech: ~4.5e-5 — a +wall tap prices ~0.05, and <0.5 is FREE), then split into 0.5-point RATTLE CRITS applied to +random internal subsystems (HeatSink family/Gyroscope/Torso, drawn by +`collisionCriticalHitWeight`@0x10C). See [[combat-damage]]. Verified: wall-grinding no longer +kills or heals the limper (0 deaths, 27 rattle events, mech stays gimped) and the peer's +replicant re-enters the gimp cycle between bounded staggers; grass circling unchanged (198). **Falsified en route (do not revive):** (a) "the stagger's action-request flags feed a drive suppressor" — image-wide + full-gap sweep shows `word[this+0x18]` is read ONLY by the net record emitter; (b) "the bmp clip's ~6.5 u/s root motion presses back into the wall" — measured `adv=0 diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index 3c6f123..59cbd3c 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -1829,6 +1829,14 @@ Logical // these read the COMPILED named members the scoring handlers write, replacing the // earlier raw-offset reads (pilot+0x27c / +0x200) that don't match our layout. // +// #83 (collision rattle): the technician "splash/collision damage" setting -- +// the binary's collision distributor gates on the owning player's +// advancedDamage copy 2 (@0x268, FUN_0049ffcc @0x49ffde). TU-safe bridge. +int BTPlayerAdvancedDamageOn(void *player_v) +{ + return (player_v != 0) ? ((BTPlayer *)player_v)->advancedDamageOn2 : 0; +} + int BTPilotKills(void *pilot) { int k = pilot ? ((BTPlayer *)pilot)->GetKillCount() : 0; diff --git a/game/reconstructed/btplayer.hpp b/game/reconstructed/btplayer.hpp index 7090e80..9fa81ee 100644 --- a/game/reconstructed/btplayer.hpp +++ b/game/reconstructed/btplayer.hpp @@ -436,6 +436,7 @@ class DropZone__ReplyMessage; friend int BTPlayerRoleLocksAdvanced(void *); // the FUN_004ac9c8 bridge (task #12): experienceLevel == 0 friend int BTPlayerExperienceSimLive(void *); // +0x25c reader (issue #2): jams / lights / powersub short path friend int BTPlayerExperienceHeatModelOn(void *); // +0x260 reader (issue #2): FUN_004ad7d4 heat-model gate + friend int BTPlayerAdvancedDamageOn(void *); // +0x268 reader (#83): FUN_0049ffcc collision-rattle gate // TARGET DESIGNATION (Gitea #48/#57): the mapper used to write the target // RAW at `pilot + 0x284` -- the BINARY's objectiveMech offset, which on our // compiled object is `deathPending` (the respawn latch). These bridges diff --git a/game/reconstructed/gyro.cpp b/game/reconstructed/gyro.cpp index 6aaab3b..5b3d81f 100644 --- a/game/reconstructed/gyro.cpp +++ b/game/reconstructed/gyro.cpp @@ -1092,3 +1092,15 @@ void { ResetToInitialState(); // @004b2678 (takes no arg) } + + +// +// #83 bridge: the collision-damage distributor (mech.cpp, FUN_0049ffcc) tests +// roster members against this family's derivation chain (binary GUID 0x50fdc0). +// Lives here because Gyroscope is a complete type only in this TU. +// +Derivation * + BTGyroscopeFamily() +{ + return &Gyroscope::ClassDerivations; +} diff --git a/game/reconstructed/heat.cpp b/game/reconstructed/heat.cpp index 6b1b8e1..3d37372 100644 --- a/game/reconstructed/heat.cpp +++ b/game/reconstructed/heat.cpp @@ -1282,3 +1282,15 @@ int BTCondenserNumber(::Subsystem *sub) { return (sub != 0) ? ((Condenser *)sub)->condenserNumber : -1; // +0x1D4 } + + +// +// #83 bridge: the collision-damage distributor (mech.cpp, FUN_0049ffcc) tests +// roster members against this family's derivation chain (binary GUID 0x50e590). +// Lives here because HeatSink is a complete type only in this TU. +// +Derivation * + BTHeatSinkFamily() +{ + return HeatSink::GetClassDerivations(); +} diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 8c2e987..ad69197 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -807,6 +807,77 @@ static int // id-0x16 damage/kill reports (see the tail comment) [T3/T4 -- decoded in // context/combat-damage.md]. // +// +// @0049ffcc -- the COLLISION-DAMAGE DISTRIBUTOR (#83; raw disasm +// scratchpad/night6/gap_49ffcc.txt -- an export-gap fn reached only from the +// TakeDamage hub's damageType==0 divert @0x4a0368). Collision damage is the +// manual's "splash/collision damage" TECHNICIAN SETTING: gated on the owning +// player's advancedDamage copy (+0x268, @0x49ffde), priced against a +// 100 km/h reference impact, and applied as 0.5-point rattle crits to random +// INTERNAL subsystems -- armor is never touched. +// scale = (2000 / moverMass) / (100 * 1/3.6)^2 / (1 - elasticity^2) +// (tbyte @0x4a0148 = 0.2777778 = 1/3.6 km/h->u/s; 2000f @0x4a0154; +// mass @+0x20c; elasticity @+0x244 -- Mover layout walk) +// amount = raw * scale; amount < 0.5f (@0x4a015c) -> FREE (taps cost nothing) +// n = Round(amount * 2) sub-hits (@0x4a0051) of amount/n each +// each landing on ONE roster subsystem drawn by cumulative +// collisionCriticalHitWeight (@0x10C) vs a [0,1) roll, restricted to the +// HeatSink family / Gyroscope / Torso (derivation GUIDs 0x50e590 / 0x50fdc0 / +// 0x510b08 -- all MechSubsystem-based, which is why +0x10C is valid on every +// eligible entry). An un-won roll (total weight < roll) lands nowhere -- +// faithful: the binary does not normalize the weights. +// +void + Mech::DistributeCollisionDamage(Damage *dmg) +{ + extern int BTPlayerAdvancedDamageOn(void *player_v); // btplayer.cpp (+0x268) + void *player = GetPlayerLink(); // mech+0x190 (ENTITY.h:430) + if (player == 0 || !BTPlayerAdvancedDamageOn(player)) // @0x49ffde the technician gate + return; + + const Scalar c = 100.0f * 0.27777779f; // 100 km/h in u/s + Scalar denom = 1.0f - elasticityCoefficient * elasticityCoefficient; // @0x244 + Scalar scale = ((2000.0f / moverMass) / (c * c)) / denom; // @0x49ffff-0x4a0027 + dmg->damageAmount *= scale; // @0x4a0030 + if (dmg->damageAmount < 0.5f) // @0x4a003c: a tap is FREE + return; + + int n = Round(dmg->damageAmount * 2.0f); // @0x4a0051 (FUN_004dcd94) + if (n < 1) + n = 1; + dmg->damageAmount /= (Scalar)n; // @0x4a0065 + + if (getenv("BT_DMG_LOG")) + DEBUG_STREAM << "[colldmg] rattle " << (dmg->damageAmount * n) + << " pts in " << n << " sub-hits (scale=" << scale << ")" + << std::endl << std::flush; + + extern Derivation *BTHeatSinkFamily(void); // heat.cpp (0x50e590) + extern Derivation *BTGyroscopeFamily(void); // gyro.cpp (0x50fdc0) + extern Derivation *BTTorsoFamily(void); // torso.cpp (0x510b08) + for (int i = 0; i < n; ++i) // @0x4a006b + { + Scalar threshold = RandomUnit(); // @0x4a007d (FUN_00408050) + Scalar acc = 0.0f; + for (int b = 0; b < GetSubsystemCount(); ++b) // @0x4a0124 (count @+0x124) + { + Subsystem *s = GetSubsystem(b); // roster @+0x128 + if (s == 0) + continue; + if (!s->IsDerivedFrom(*BTHeatSinkFamily()) // @0x4a0092 + && !s->IsDerivedFrom(*BTGyroscopeFamily()) // @0x4a00b2 + && !s->IsDerivedFrom(*BTTorsoFamily())) // @0x4a00d2 + continue; + acc += ((MechSubsystem *)s)->CollisionCritWeight(); // @0x4a00f2 (+0x10C) + if (acc < threshold) // @0x4a010a + continue; + ((MechSubsystem *)s)->ApplyDamageAndMeasure(*dmg); // @0x4a0117 (FUN_004ac07c) + break; // @0x4a0121 -> next sub-hit + } + } + Check_Fpu(); +} + void Mech::TakeDamageMessageHandler(TakeDamageMessage *message) { @@ -860,6 +931,23 @@ void (float)(message->damageData.impactPoint.z - localOrigin.linearPosition.z)); } + // + // @0x4a0368 -- the COLLISION DIVERT (#83, closing the #82 chain). Damage + // type 0 == the mover's collision damage (what the crash response policy + // forwards). It NEVER reaches the zone/armor loop below: it is priced + + // distributed as INTERNAL RATTLE by DistributeCollisionDamage (@0049ffcc) + // and the handler is done. This was the missing half of the crash + // self-damage reconstruction -- without the divert, the raw kinetic-energy + // amounts (a 60-ton mech prices ~1000+ per wall tap) fell through into the + // WEAPON loop and a single bump killed the mech (run-14 field report). + // + if (message->damageData.damageType == 0) + { + DistributeCollisionDamage(&message->damageData); + Check_Fpu(); + return; + } + DamageLookupTable *table = (DamageLookupTable *)damageLookupTable; // named member (Wword absorbs!) if (message->invalidDamageZone && table != 0) { diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index 213a469..d9e3217 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -1197,6 +1197,11 @@ protected: // Mech override of Entity::TakeDamageMessageHandler: resolve an unaimed // (invalidDamageZone) hit's zone via the cylinder table, then base-route. void TakeDamageMessageHandler(TakeDamageMessage *message); + // @0049ffcc (#83): the COLLISION-damage distributor the handler's + // damageType==0 divert (@0x4a0368) routes to -- prices the mover's + // kinetic figure vs a 100 km/h reference and lands 0.5-point rattle + // crits on weighted random internal subsystems. Never touches armor. + void DistributeCollisionDamage(Damage *dmg); // Mech override of Entity::PlayerLinkMessageHandler (@0049f624): the // base resolves mech->player; BT adds the REVERSE link // player->playerVehicle = this on EVERY node (replicants included -- diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index aa675e6..1b46826 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -6871,6 +6871,15 @@ void fallScalar = -(fallDirection.x * impactVel.x + fallDirection.y * impactVel.y + fallDirection.z * impactVel.z); + // ALWAYS-log the crash pricing (rare event; the arbiter for the + // wall-damage economy -- run 14 field report: "tapped a wall and + // died instantly", so the forwarded amount is suspect vs binary). + DEBUG_STREAM << "[crashdmg] amount=" << dmg.damageAmount + << " mass=" << moverMass + << " |v|=" << (float)Sqrt(impactVel.x * impactVel.x + + impactVel.y * impactVel.y + impactVel.z * impactVel.z) + << " force=(" << dmg.damageForce.x << "," << dmg.damageForce.y + << "," << dmg.damageForce.z << ")\n" << std::flush; Entity::TakeDamageMessage td( Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage), diff --git a/game/reconstructed/mechsub.hpp b/game/reconstructed/mechsub.hpp index 1a3d157..e8d0364 100644 --- a/game/reconstructed/mechsub.hpp +++ b/game/reconstructed/mechsub.hpp @@ -291,6 +291,11 @@ class Damage; // (binary @0x4a0473: tally += critted->+0x108). Scalar CriticalScoreBonus() const { return criticalReference; } + // #83 (collision rattle): the authored lottery weight the collision- + // damage distributor (FUN_0049ffcc @0x4a00f2) accumulates per eligible + // subsystem against a [0,1) roll. + Scalar CollisionCritWeight() const { return collisionCriticalHitWeight; } + // task #2 (crit propagation): the zone-side crit paths // (Mech__DamageZone::CriticalHit @0049ccc4 / SendSubsystemDamage // @0049c9a8) reach these subsystem facets. diff --git a/game/reconstructed/torso.cpp b/game/reconstructed/torso.cpp index a33c3a4..f95de2b4 100644 --- a/game/reconstructed/torso.cpp +++ b/game/reconstructed/torso.cpp @@ -1101,3 +1101,15 @@ void { ResetToInitialState(); // @004b5bf8 (takes no arg) } + + +// +// #83 bridge: the collision-damage distributor (mech.cpp, FUN_0049ffcc) tests +// roster members against this family's derivation chain (binary GUID 0x510b08). +// Lives here because Torso is a complete type only in this TU. +// +Derivation * + BTTorsoFamily() +{ + return &Torso::ClassDerivations; +}