From 946323b69630aca1a2ce8e2f171de8608ecc42ff Mon Sep 17 00:00:00 2001 From: arcattack Date: Sat, 11 Jul 2026 07:36:25 -0500 Subject: [PATCH] Gyro: damage fan-out FUN_004b2980 -- the cockpit hit-BOUNCE is live (task #56) Re-disassembled the unexported gap 0x4b2980-0x4b2d8b byte-exact (capstone over section_dump.txt) and reconstructed Gyroscope::ApplyDamageResponse: - zero-damage + Collision(type 0) no-op; hit direction = Damage::damageForce or a RANDOM horizontal fallback when ~zero (per-component sign roll then value roll -- binary-legal until senders fill damageForce) - direction rotated by the yaw-only torso-twist frame (placeRot=(0,twist,0) -> euler->matrix, the FUN_00408744 raw row-dot reproduced verbatim) - per-type scaling amount / damageMultiplier[type] * damageResponse[type] .{trans,pitchRoll,yaw,vibration}; Explosive alone multiplies burstCount; each clamped at 1.3f - four kicks: impulse(trans, dir), torque(pitchRoll, dir), impulse(vibration, vibrationDirection@0x390) and verticalImpulse(yaw, dir) Layout corrections (byte-verified): gyro+0x390 is ONE Vector3D vibration axis init (0,1,0) -- was mis-split as scalars animationOffset/Scale/Phase; mech +0x5c4 is a FLOAT rumble-period countdown (gyroRumbleTimer) -- was mis-typed int clipLoadGuard. Call sites wired (binary-gated): - take-damage hub: mech.cpp TakeDamageMessageHandler, FIRST action (@0x4a0264 in hub FUN_004a0230) -- an invalid-zone hit still bounces - crushable-icon crunch: mech4.cpp, torque 0.4 along the bounce delta-v + up impulse 0.2 (@4aa81e/@4aa86c) - firing recoil: projweap.cpp FireWeapon, damage>3 -> impulse (0,0.6,-1.5) x damage/16 (@4bc136-4bc19c) via the new Mech::GetGyroSubsystem accessor DEFERRED (recorded in KB with the byte recipe): the alternate-gait engage jolt + 0.4s rumble (@4aa158-4aa365) -- its gates are [T3]-flagged and it mutates the #49/#50-stabilized gait machine. Verified live: [gyro-dmg] fires per hit with correct type scaling; eyePosition shows the damped bounce (ramp to ~0.015u, oscillating Y, decay to zero); kill chain un-regressed; no NaN. Also recorded in KB: the OFFSET-COCKPIT verification -- Thor's authored jointeye tranx=+1.13 matches its canopy X-center exactly AND our in-game eye reproduces it to the hundredth (the placement model is correct per-mech); and the playable roster is 8 models (strider/raptor/firstrtr/blkjack have assets but no model resource -- BT_FORCE_MODEL with those names = NULL-resource AV, cdb-verified, not a code bug). Co-Authored-By: Claude Fable 5 --- context/cockpit-view.md | 50 ++++++++-- context/decomp-reference.md | 10 ++ context/open-questions.md | 23 +++-- game/reconstructed/gyro.cpp | 157 +++++++++++++++++++++++++++++++- game/reconstructed/gyro.hpp | 17 +++- game/reconstructed/mech.cpp | 11 +++ game/reconstructed/mech.hpp | 7 +- game/reconstructed/mech3.cpp | 4 +- game/reconstructed/mech4.cpp | 15 ++- game/reconstructed/projweap.cpp | 14 +++ 10 files changed, 283 insertions(+), 25 deletions(-) diff --git a/context/cockpit-view.md b/context/cockpit-view.md index 3d9c707..c3e0d45 100644 --- a/context/cockpit-view.md +++ b/context/cockpit-view.md @@ -92,13 +92,49 @@ inverse(eyeWorld)`. No LookAt anywhere. relationship is off by ~0.6u for that group by some OTHER mechanism (candidates: a joint in the siteeyepoint parent chain we don't pose, per-mech skeleton rest differences, or the canopy authored for a view state we don't replicate). OPEN. -- **Hit-bounce: plumbing live, trigger missing.** The integrators + ApplyDamageImpulse/Torque/ - Vertical hooks are faithful and ready, but the binary's damage→gyro fan-out **FUN_004b2980** - (0x4b2980-0x4b2d8b, called from the damage hub @0x4a02fb; also direct crunch kicks - @0x4aa254/0x4aa288/…) is an unexported gap not yet reconstructed — nothing kicks the gyro yet. - Also pending: the mech+0x3F0 overspeed sway model (bytes 0x4aade8-0x4aaf14; we feed swayBias=0), - and the per-frame `EyepointRotation.pitch = torso pitch` writer (FUN_004b66b4 @0x4aafab) + the - mapper glance-look states (FUN_004afd10) — separate fidelity items. +- **Hit-bounce: LIVE and verified [T2] (2026-07-11).** The damage→gyro fan-out **FUN_004b2980** + was re-disassembled byte-exact from the raw image (capstone; scratchpad/dis_4b2980.py) and + reconstructed as `Gyroscope::ApplyDamageResponse` (gyro.cpp): zero-damage + Collision(0) no-op; + hit direction = `Damage::damageForce` or a RANDOM horizontal fallback when ~zero (our senders + don't fill damageForce yet — the fallback is binary-legal; directional fidelity = fill it later); + rotated by the yaw-only torso-twist frame; per-type scaling `amount / damageMultiplier[type] * + damageResponse[type].{trans,pitchRoll,yaw,vibration}` (Explosive alone multiplies burstCount); + clamp 1.3; four kicks — impulse(trans, dir), torque(pitchRoll, dir), impulse(vibration, + **vibrationDirection@0x390 = (0,1,0)** — the "animationOffset/Scale/Phase scalars" were this + unit vector all along), verticalImpulse(yaw, dir). Call sites wired: the take-damage hub + (mech.cpp TakeDamageMessageHandler, FIRST action, binary @0x4a0264), the crushable-icon crunch + (mech4.cpp, torque 0.4 along Δv + up-impulse 0.2, @4aa81e/@4aa86c), the firing recoil + (projweap.cpp FireWeapon: damage>3 → impulse (0,0.6,−1.5) × damage/16, @4bc136-4bc19c). + Verified live: `[gyro-dmg]` fires per hit with correct scaling, eyePosition shows the damped + oscillation (ramp to ~0.015u, Y oscillating, decay to 0), no NaN, kill chain un-regressed. +- **Deferred gyro tail:** (a) the alternate-gait engage JOLT + 0.4s engaged-gait RUMBLE + (@4aa158-4aa365 — full byte recipe in the wf_6880e605 synthesis; **flagged [T3] on the gate + naming** and it mutates the gait state machine tasks #49/#50 stabilized — do NOT wire without + re-verifying the gate semantics); (b) the mech+0x3F0 overspeed sway model (swayBias fed 0); + (c) the per-frame `EyepointRotation.pitch = torso pitch` writer (FUN_004b66b4 @0x4aafab) + the + mapper glance-look states (FUN_004afd10); (d) senders filling `Damage::damageForce` (beam: + impact−muzzle; projectile: velocity) for directional bounce fidelity; (e) FireWeapon's heat add + (pre-existing separate gap). `gyroRumbleTimer@0x5c4` was re-typed from the mis-named + `int clipLoadGuard` (binary uses it as a float rumble countdown). + +## Offset cockpits — the placement-model verification [T1/T2] + +Some mechs have laterally OFFSET cockpits, and the data + our port agree exactly (2026-07-11): +`[jointeye]` rest translation per SKL vs the `_COP` canopy X-center — **thor: tranx=+1.13, canopy +center +1.13 (exact)**; loki −0.25/−0.15; owens +0.19/+0.16; the other five ≈0/≈0. Our in-game +measured eye (`[EYECHK]`) reproduces the Thor offset to the hundredth (lateral 1.13 from the mech +origin). The eye chain (baseOffset + parent-segment DCS composition) is therefore CORRECT per-mech; +the still-blocked cockpit views are a canopy-RENDER question, not a camera one. + +## The playable roster is 8, not 12 [T1] + +`BTL4.RES` contains exactly 8 mech model resources (avatar, bhk1/blkhawk, loki, madcat, owens, +sunder, thor, vulture — each with ``, variants `1/2`, and a `dead`). Strider / +Raptor / Firestarter / Blackjack have `_COP` meshes, SKLs and gauge configs but NO model resource +(cut/other-version leftovers); forcing them via `BT_FORCE_MODEL` feeds NULL to +`ResourceDescription::Lock` → AV in `CreatePlayerVehicle` (cdb-verified). Valid `BT_FORCE_MODEL` +names: `avatar bhk1 loki madcat owens sunder thor vulture` (+ variants ava1/lok1/lok2/mad1/mad2/ +own1/snd1/thr1/vul1/blkhawk). ## Diagnostics / env gates diff --git a/context/decomp-reference.md b/context/decomp-reference.md index 9fc69d3..f4fd1b8 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -178,6 +178,16 @@ BT_PUNCH, …) are catalogued in [[rendering]]. Warp visuals: [[translocation-wa per-frame Execute (spin about local Z + submit, NO texcoord/ramp work) = **`FUN_00453dc4`** (`reference/decomp/all/part_007.c`), using axis-rotation helper `FUN_0040998c` (axis idx 2 = Z). +**Gyro damage fan-out** [T1, re-disassembled from raw bytes] (full story: [[cockpit-view]]): +**`FUN_004b2980`** (0x4b2980-0x4b2d8b, unexported gap) = `Gyroscope::ApplyDamageResponse` — args = +`(Gyroscope*, Damage BY VALUE 12 dwords)`, caller cleans 0x34; constants: dir-eps 1e-4, sign 0.5f +(@0x4b2d84), clamp 1.3f (@0x4b2d88); `vibrationDirection` = gyro+0x390 (0,1,0). Call sites: the +take-damage hub @0x4a02fb (hub = FUN_004a0230, gyro kick FIRST); crushable crunch @4aa81e (torque +0.4)/@4aa86c (up 0.2); gait jolt/rumble @4aa254/@4aa288/@4aa342 (DEFERRED [T3 gates]); firing +recoil @4bc136-4bc19c (damage>3 → (0,0.6,−1.5) × damage/16; `gyroRumbleTimer` = mech+0x5c4 float, +ex-"clipLoadGuard"). Byte tooling: `scratchpad/dis_4b2980.py` / `dis_range.py` over +`reference/decomp/section_dump.txt`. + **Cockpit-eye / punch addresses** [T1] (full story: [[cockpit-view]]): eye ctor **`FUN_004579a8`** (part_007.c:9274 = DPLEyeRenderable; offset = segment `GetBaseOffset` segment+0x74, parent = parent-segment DCS; caller part_014.c:5525-66 gates on `"siteeyepoint"`); diff --git a/context/open-questions.md b/context/open-questions.md index 23fc52b..90dbe6e 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -75,8 +75,10 @@ authentic path scoped. world through the openings, verified vs gameplay footage; shows by DEFAULT now (`BT_HIDE_COCKPIT=1` hides). NOTE the old claim here ("canopy windows are punch texels") was WRONG — the shell has no texture; the openings are geometry. STILL OPEN: the non-Madcat mechs' - views are blocked because the resting eye sits ~0.6u inside the canopy wall — the missing - gyro eye-joint steady offset (task #56, [[cockpit-view]] §gyro). (b) the **3D marker + views render dark/blocked — NOT an eye-placement bug (disproven: the gyro equilibrium is zero, + and the per-mech eye placement is verified EXACT against the SKL jointeye offsets, incl. the + Thor's authored +1.13 lateral offset cockpit — [[cockpit-view]] §offset-cockpits). It is a + canopy-RENDER question (how those shells read from inside). (b) the **3D marker chain** ([0x31c]/[0x320] dpl objects + the PNAME1-8.bgf player-name meshes over the locked target — matters for MP). (c) the compass rotation SIGN is derived-not-verified [T3] (turn left → the stem should swing right; flip the sin sign in Draw if footage disagrees). @@ -92,12 +94,17 @@ authentic path scoped. Performance). Layout locked (sizeof==0x3D0); runtime clean (joints type 5, finite state, no NaN). Empirical: spring targets are SYMMETRIC → eye equilibrium (0,0,0) → the gyro is a ±0.1-0.15u BOUNCE mechanism, NOT a steady offset (the earlier hypothesis is disproven). - REMAINING (task #56 tail): (a) the damage→gyro fan-out **FUN_004b2980** (0x4b2980-0x4b2d8b, - unexported gap; called @0x4a02fb + direct crunch kicks) — without it no hit-bounce fires; - (b) the mech+0x3F0 overspeed sway model (swayBias fed 0); (c) the per-frame - `EyepointRotation.pitch = torso pitch` writer (FUN_004b66b4) + mapper glance-look states - (FUN_004afd10 — yaw/pitch from mech+0x564..0x570). Also: `deathAnimationLatched`/`legResetLatch` - were never ctor-initialised (0xCDCDCDCD gate bug, fixed in mech.cpp ctor). See [[cockpit-view]]. + **FAN-OUT LANDED (2026-07-11):** FUN_004b2980 re-disassembled from raw bytes and reconstructed + (`Gyroscope::ApplyDamageResponse`) + wired at the take-damage hub, the crushable crunch and the + firing recoil — the hit-BOUNCE is live and verified (damped eye oscillation, no NaN). + REMAINING (task #56 tail): (a) the alternate-gait engage jolt + engaged-gait rumble + (@4aa158-4aa365 — [T3] gate naming, mutates the #49/#50-stabilized gait machine; byte recipe in + the wf_6880e605 synthesis); (b) the mech+0x3F0 overspeed sway model (swayBias fed 0); (c) the + per-frame `EyepointRotation.pitch = torso pitch` writer (FUN_004b66b4) + mapper glance-look + states (FUN_004afd10 — yaw/pitch from mech+0x564..0x570); (d) senders filling + `Damage::damageForce` for directional bounce (random-fallback is binary-legal meanwhile). + Also: `deathAnimationLatched`/`legResetLatch` were never ctor-initialised (0xCDCDCDCD gate bug, + fixed in mech.cpp ctor). See [[cockpit-view]]. - **MechControlsMapper look/eyepoint commit** — reconstructed but its offsets collide with declared members; arbitrate before enabling (no port consumer yet). The fire-trigger-through-mapper is still the `gBTWeaponTrigger` bring-up. diff --git a/game/reconstructed/gyro.cpp b/game/reconstructed/gyro.cpp index 7851f65..a6fec11 100644 --- a/game/reconstructed/gyro.cpp +++ b/game/reconstructed/gyro.cpp @@ -177,6 +177,7 @@ struct GyroLayoutCheck static_assert(offsetof(Gyroscope, bodyWork) == 0x320, "bodyWork @0x320"); static_assert(offsetof(Gyroscope, damageMultiplier) == 0x32C, "damageMultiplier @0x32C"); static_assert(offsetof(Gyroscope, damageResponse) == 0x340, "damageResponse @0x340"); + static_assert(offsetof(Gyroscope, vibrationDirection) == 0x390, "vibrationDirection @0x390"); static_assert(offsetof(Gyroscope, swayBias) == 0x3A8, "swayBias @0x3A8"); static_assert(offsetof(Gyroscope, swayAngle) == 0x3BC, "swayAngle @0x3BC"); static_assert(offsetof(Gyroscope, eyeJointNode) == 0x3C8, "eyeJointNode @0x3C8"); @@ -306,9 +307,7 @@ Gyroscope::Gyroscope( externalPitchPtr = &spare0; // @0x258 self-pointer (:2933); // bt_mech tail re-points to &torso pitch } - animationOffset = 0.0f; // @0x390 - animationScale = 1.0f; // @0x394 - animationPhase = 0.0f; // @0x398 + vibrationDirection = Vector3D(0.0f, 1.0f, 0.0f); // @0x390 the up vibration axis (:2921-2923) swayAngle = 0.0f; // @0x3BC swayVelocity = 0.0f; // @0x3C0 swayActive = 0; // @0x3C4 @@ -632,6 +631,18 @@ void DEBUG_STREAM << "[gyro] *** NaN DETECTED in integrator state ***\n" << std::flush; } ++s_gc; + // bounce visibility probe: log the first N frames where the eye is actually + // displaced (the periodic sampler above misses short oscillations). + static int s_bounceSeen = 0; + if (s_glog && s_bounceSeen < 40 + && (eyePosition.x > 1e-3f || eyePosition.x < -1e-3f + || eyePosition.y > 1e-3f || eyePosition.y < -1e-3f + || eyePosition.z > 1e-3f || eyePosition.z < -1e-3f)) + { + ++s_bounceSeen; + DEBUG_STREAM << "[gyro-bounce] eyePos=(" << (float)eyePosition.x << "," + << (float)eyePosition.y << "," << (float)eyePosition.z << ")\n" << std::flush; + } if (!NodeRotationEquals(mechJointNode, eyePosition, QuantiseEps)) // FUN_004084fc vs value+0 { SetNodeRotation(mechJointNode, eyePosition); // FUN_0041d11c = SetTRANSLATION @@ -728,6 +739,124 @@ void bodyForce.y = bodyForce.x; // *(this+0x30C) = *(this+0x308) } +// +// @004b2980 [T1 -- re-disassembled byte-exact, task #56] -- the damage->gyro +// fan-out (the cockpit hit-BOUNCE). Constants from the dump: eps 1e-4 +// (@0x38d1b717), sign threshold 0.5f (@0x4b2d84), clamp 1.3f (@0x4b2d88). +// Collision (type 0) and zero damage no-op. Direction: the Damage record's +// damageForce, or a RANDOM horizontal direction when ~zero (per component a +// sign roll then a value roll); rotated by the yaw-only torso-twist frame +// (placeRot=(0, *externalPitchPtr, 0) -> matrix @workMatrix, the engine +// euler->matrix conversion == the binary's euler->quat->matrix FUN_00409a00 + +// FUN_0040ab44) then re-normalized. Per-type scaling: amount / multiplier * +// response{trans,pitchRoll,yaw,vibration}; the Explosive case alone multiplies +// by burstCount. Four kicks: directional impulse (trans), torque (pitchRoll), +// a vibration shake along the FIXED member axis vibrationDirection@0x390 (up), +// and the vertical impulse fed by the row's third field ("yaw"). +// +void + Gyroscope::ApplyDamageResponse(const Damage &damage) +{ + if (damage.damageAmount == 0.0f) // @4b298c (0.0f @0x4b2d80) + return; + if (damage.damageType == Damage::CollisionDamageType) // @4b299e -- collisions never bounce here + return; + + Scalar trans = 0.0f; // ebp-0x10 + Scalar pitchRoll = 0.0f; // ebp-0x0C + Scalar yaw = 0.0f; // ebp-0x08 + Scalar vibration = 0.0f; // ebp-0x04 + + Vector3D dir; // ebp-0x1C + if (Close_Enough(damage.damageForce, Vector3D(0.0f, 0.0f, 0.0f), 1e-4f)) // @4b29bd FUN_004084fc + { + // random horizontal: sign roll (>= 0.5 keeps +) then value roll, y=0 + dir.x = (RandomUnit() >= 0.5f) ? RandomUnit() : -RandomUnit(); // @4b29d7 + dir.z = (RandomUnit() >= 0.5f) ? RandomUnit() : -RandomUnit(); // @4b2a0d + dir.y = 0.0f; // @4b2a43 + } + else + { + dir = damage.damageForce; // @4b2a4a FUN_00408440 + } + dir.Normalize(dir); // @4b2a62 FUN_004087f4 (unguarded in the binary too) + + // @4b2a67-4b2af5: yaw-only body frame from the torso twist; rotate the + // WORLD hit direction into it (FUN_00408744 raw row-dot on entries[0..2]/ + // [4..6]/[8..10] -- reproduced verbatim below) and re-normalize. The + // placement state @0x28C/0x298/0x2A8 is written for member fidelity. + placeRot = Vector3D(0.0f, *externalPitchPtr, 0.0f); // gyro+0x2A8..0x2B0 + placePos = Vector3D(0.0f, 0.0f, 0.0f); // @4b2aa8 + { + AffineMatrix m; + m = EulerAngles(Radian(placeRot.x), Radian(placeRot.y), Radian(placeRot.z)); + for (int i = 0; i < 12; ++i) workMatrix[i] = m.entries[i]; // @4b2ac2 FUN_0040ab44 + Vector3D tmp = dir; // ebp-0x28 @4b2ac7 + dir.x = tmp.x*workMatrix[0] + tmp.y*workMatrix[1] + tmp.z*workMatrix[2]; // FUN_00408744 + dir.y = tmp.x*workMatrix[4] + tmp.y*workMatrix[5] + tmp.z*workMatrix[6]; + dir.z = tmp.x*workMatrix[8] + tmp.y*workMatrix[9] + tmp.z*workMatrix[10]; + } + dir.Normalize(dir); // @4b2aed + + // @4b2afd: per-type scaling (jump table @0x4b2b10; type > 4 leaves all + // four at zero). Each term computed independently, matching the binary's + // per-term fld/fdiv/fmul. + switch (damage.damageType) + { + case Damage::BallisticDamageType: // @4b2b3d mult@0x330 resp@0x350 + trans = damage.damageAmount / damageMultiplier[1] * damageResponse[1].trans; + pitchRoll = damage.damageAmount / damageMultiplier[1] * damageResponse[1].pitchRoll; + yaw = damage.damageAmount / damageMultiplier[1] * damageResponse[1].yaw; + vibration = damage.damageAmount / damageMultiplier[1] * damageResponse[1].vibration; + break; + case Damage::ExplosiveDamageType: // @4b2b8a the ONLY case reading burstCount + trans = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].trans; + pitchRoll = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].pitchRoll; + yaw = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].yaw; + vibration = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].vibration; + break; + case Damage::LaserDamageType: // @4b2be3 mult@0x338 resp@0x370 + trans = damage.damageAmount / damageMultiplier[3] * damageResponse[3].trans; + pitchRoll = damage.damageAmount / damageMultiplier[3] * damageResponse[3].pitchRoll; + yaw = damage.damageAmount / damageMultiplier[3] * damageResponse[3].yaw; + vibration = damage.damageAmount / damageMultiplier[3] * damageResponse[3].vibration; + break; + case Damage::EnergyDamageType: // @4b2c2d mult@0x33C resp@0x380 + trans = damage.damageAmount / damageMultiplier[4] * damageResponse[4].trans; + pitchRoll = damage.damageAmount / damageMultiplier[4] * damageResponse[4].pitchRoll; + yaw = damage.damageAmount / damageMultiplier[4] * damageResponse[4].yaw; + vibration = damage.damageAmount / damageMultiplier[4] * damageResponse[4].vibration; + break; + default: + break; + } + + // @4b2c75-4b2ce2: upper-clamp each at 1.3f; NO lower clamp. + if (trans > 1.3f) trans = 1.3f; + if (pitchRoll > 1.3f) pitchRoll = 1.3f; + if (yaw > 1.3f) yaw = 1.3f; + if (vibration > 1.3f) vibration = 1.3f; + + if (getenv("BT_GYRO_LOG")) + DEBUG_STREAM << "[gyro-dmg] type=" << (int)damage.damageType + << " amt=" << (float)damage.damageAmount << " burst=" << (int)damage.burstCount + << " dir=(" << (float)dir.x << "," << (float)dir.y << "," << (float)dir.z + << ") t/p/y/v=" << (float)trans << "/" << (float)pitchRoll << "/" + << (float)yaw << "/" << (float)vibration << "\n" << std::flush; + + ApplyDamageImpulse (dir.x, dir.y, dir.z, trans); // @4b2d00 the directional knock + ApplyDamageTorque (dir.x, dir.y, dir.z, pitchRoll); // @4b2d23 + ApplyDamageImpulse (vibrationDirection.x, vibrationDirection.y, + vibrationDirection.z, vibration); // @4b2d4b the up-shake + ApplyVerticalImpulse(dir.x, dir.y, dir.z, yaw); // @4b2d6e + + if (getenv("BT_GYRO_LOG")) + DEBUG_STREAM << "[gyro-dmg] post-kick exag=" << (float)exageration + << " eyeForce=(" << (float)eyeForce.x << "," << (float)eyeForce.y << "," << (float)eyeForce.z + << ") bodyForce=(" << (float)bodyForce.x << "," << (float)bodyForce.y << "," + << (float)bodyForce.z << ")\n" << std::flush; +} + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // CreateStreamedSubsystem -- Gyroscope @@ -905,3 +1034,25 @@ void GyroBindExternalPitch(Subsystem *gyro, Scalar *pitch) if (gyro != 0) static_cast(gyro)->BindExternalPitch(pitch); } + +//===========================================================================// +// Damage->gyro bridges (task #56 fan-out; binary call sites: the take-damage +// hub @0x4a02fb, the performance crunch/jolt/rumble kicks @0x4aa254/0x4aa288/ +// 0x4aa342/0x4aa81e/0x4aa86c, and the firing-recoil kick @0x4bc194). +// Complete-type-TU pattern: callers hold only Subsystem*. +//===========================================================================// +void GyroApplyDamage(Subsystem *gyro, const Damage &damage) +{ + if (gyro != 0) + static_cast(gyro)->ApplyDamageResponse(damage); +} +void GyroApplyDamageImpulse(Subsystem *gyro, Scalar x, Scalar y, Scalar z, Scalar magnitude) +{ + if (gyro != 0) + static_cast(gyro)->ApplyDamageImpulse(x, y, z, magnitude); +} +void GyroApplyDamageTorque(Subsystem *gyro, Scalar x, Scalar y, Scalar z, Scalar magnitude) +{ + if (gyro != 0) + static_cast(gyro)->ApplyDamageTorque(x, y, z, magnitude); +} diff --git a/game/reconstructed/gyro.hpp b/game/reconstructed/gyro.hpp index f850319..4f90f72 100644 --- a/game/reconstructed/gyro.hpp +++ b/game/reconstructed/gyro.hpp @@ -52,6 +52,7 @@ //##################### Forward Class Declarations ####################### class Mech; class Joint; // engine skeleton node (JOINT.h); eye/mech joint targets +class Damage; // engine damage record (DAMAGE.h); ApplyDamageResponse input //########################################################################### //################# Gyroscope Model Resource ########################## @@ -212,6 +213,13 @@ class Joint; // engine skeleton node (JOINT.h); eye/mech joint targets Scalar x, Scalar y, Scalar z, Scalar magnitude); void ApplyVerticalImpulse( // @004b2e50 Scalar pitch, Scalar /*y*/, Scalar /*z*/, Scalar magnitude); + // @004b2980 -- the damage->gyro fan-out (task #56): normalize the hit + // direction (random horizontal if ~zero), rotate into the torso-twist + // frame, scale by the per-damage-type multiplier/response curves, clamp + // at 1.3, fire the four Apply* kicks. The binary passes the whole + // Damage BY VALUE (12 dwords, caller add esp,0x34) but never mutates it + // and never reads surfaceNormal/impactPoint -- const-ref is byte-equal. + void ApplyDamageResponse(const Damage &damage); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Per-frame joint-write dispatch (called from the MECH master performance @@ -320,9 +328,12 @@ class Joint; // engine skeleton node (JOINT.h); eye/mech joint targets Gyroscope__DamageResponse damageResponse[5]; // @0x340 res +0x1CC..0x21B (5 x {trans,pitchRoll,yaw,vibration}) - Scalar animationOffset; // @0x390 (this[0xE4]) idle-noise phase, init 0 - Scalar animationScale; // @0x394 (this[0xE5]) init 1.0f - Scalar animationPhase; // @0x398 (this[0xE6]) init 0 + // @0x390 (this[0xE4..0xE6]) -- the VIBRATION AXIS: ctor init (0,1,0) = + // straight up. Read as ONE Vector3D by ApplyDamageResponse @4b2d2b (the + // second ApplyDamageImpulse = the vibration shake). [T1 re-disassembled] + // (Was mis-split as scalars animationOffset/animationScale/animationPhase + // -- the 0/1/0 "init pattern" was this unit vector all along.) + Vector3D vibrationDirection; // @0x390 init (0.0f, 1.0f, 0.0f) Scalar maxAnimationNoise; // @0x39C resource +0xFC Scalar minAnimationNoise; // @0x3A0 resource +0x100 diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 7788c59..3a759bd 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -216,6 +216,7 @@ extern Subsystem *CreateGaussRifleSubsystem(Mech *, int, void *); // 0xBCE (WAVE extern Subsystem *CreateGyroSubsystem(Mech *, int, void *); // 0xBC4 (task #56 -- ctor/integrators byte-exact, ENABLED) extern void GyroFrameJointWrite(Subsystem *, Scalar, int, int); // task #56 -- mech-performance joint-write dispatch (mech4.cpp) extern void GyroBindExternalPitch(Subsystem *, Scalar *); // task #56 -- post-stream gyro+0x258 = &torso twist +extern void GyroApplyDamage(Subsystem *, const Damage &); // task #56 -- the @0x4a0264 hit-bounce fan-out extern Scalar *BTGetTorsoTwistAddr(Subsystem *); // torso.cpp bridge (complete-type TU) // Post-stream entity glue + cosmetic objects. @@ -594,6 +595,16 @@ void << " table=" << (void*)damageLookupTable << " zones=" << damageZoneCount << "\n" << std::flush; + // Binary @0x4a0264-0x4a0300 (hub FUN_004a0230): the cockpit hit-BOUNCE. + // Feed the gyro the raw Damage record FIRST -- before inflictor + // bookkeeping, threat feed and zone resolution (an invalid-zone hit still + // bounces). Sole gate: non-null gyro (mech+0x528). The fan-out no-ops + // CollisionDamageType(0) and damageAmount==0 itself (@4b298c/@4b299e); on + // a replicant the binary only WARNS ("Replicant Mech recieving + // takedamagemessage!", MECH.CPP:986) and proceeds -- no replicant gate. + if (gyroSubsystem != 0) + GyroApplyDamage(gyroSubsystem, message->damageData); + // Maintain the last-attacker bookkeeping (mech[0x43c]): read by the // DamageZone LOD router (same-attacker redirect reuse, mechdmg.cpp:374) // and by the damage-band effect orientation. Was declared but never diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index 67342bc..08efb9e 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -423,6 +423,9 @@ struct ShotDescriptor public: // Cache accessors for the controls mapper (@004afd10 reads mech+0x438/+0x5b4). Subsystem *GetTorsoSubsystem() { return sinkSourceSubsystem; } + // Gyro accessor (task #56): the firing-recoil kick (projweap.cpp, + // binary @4bc194) reads mech+0x528. + Subsystem *GetGyroSubsystem() { return gyroSubsystem; } Subsystem *GetHudSubsystem() { return hudSubsystem; } // Reachable horizontal firing half-arc (radians) the mech's torso can bring // its guns to bear -- the wider torso twist limit, or 0 for a fixed torso. @@ -659,7 +662,9 @@ protected: // speedDemand = topSpeed@0x34c * throttle * THIS). No writer found in the // decomp windows (likely model/status-driven); ctor inits 1.0 (neutral). Scalar forwardThrottleScale; // @0x5c0 - int clipLoadGuard; // @0x5c4 + Scalar gyroRumbleTimer; // @0x5c4 binary: FLOAT rumble-period countdown + // (engaged-gait rumble @4aa2eb-4aa35f, task #56); + // reset at clip load. (Was mis-typed int clipLoadGuard.) Scalar globalTimeScale; // @0x5a8 Scalar idleStrideScale; // @0x5ac Scalar gimpCycleRate; // @0x5b0 diff --git a/game/reconstructed/mech3.cpp b/game/reconstructed/mech3.cpp index 7572497..5955911 100644 --- a/game/reconstructed/mech3.cpp +++ b/game/reconstructed/mech3.cpp @@ -221,7 +221,7 @@ template inline void *FUN_00406db4(A&&...) { return 0; } // ResourceF // @0x588 hasCrashSet (int)? set 1 iff the third optional clip set exists // @0x5b8 groundCycleRate forward-cycle slew rate, on-ground (-> 0x344) // @0x5bc airborneCycleRate forward-cycle slew rate, airborne (-> 0x344) -// @0x5c4 clipLoadGuard (int) reset to 0 at the top of a clip load +// @0x5c4 gyroRumbleTimer (float) reset to 0 at the top of a clip load // @0x5cc animationClips[] (ptr[]) gait-state -> clip handle (mech2); indexed // by MeasureClipStride // @0x5d4 crashClipB (ptr) optional-set clip handle @@ -327,7 +327,7 @@ void { const char *prefix = model->animationPrefix; // param_2 + 0x40 - clipLoadGuard = 0; // this+0x5c4 + gyroRumbleTimer = 0.0f; // this+0x5c4 (float; bit-identical to the old int 0) // --- stand -> walk : standSpeed is that clip's final-keyframe stride ---- namedClip[ 0] = *ResolveAnimationClip(prefix, "swr"); // 0x5e0 diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index ffe5d8e..292d3eb 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -3307,7 +3307,20 @@ void localOrigin.linearPosition = savedPos; MoveCollisionVolume(); dmg.damageAmount = 0.0f; - // gyro crunch feed (0.4*normal + 0.2*up): DORMANT until the Gyroscope un-stub + // Binary @4aa7ce-4aa871 (task #56): the gyro CRUNCH. n = Normalize( + // dmg.damageForce) (the delta-v across the bounce, filled by the engine + // Mover::ProcessCollisionList -- no new plumbing), then a torque along + // n @ 0.4 (@4aa81e, 0x3ecccccd) + an upward impulse @ 0.2 (@4aa86c, + // 0x3e4ccccd). Normalize is unguarded in the binary too. + if (gyroSubsystem != 0) + { + extern void GyroApplyDamageTorque (Subsystem *, Scalar, Scalar, Scalar, Scalar); + extern void GyroApplyDamageImpulse(Subsystem *, Scalar, Scalar, Scalar, Scalar); + Vector3D n; + n.Normalize(dmg.damageForce); + GyroApplyDamageTorque (gyroSubsystem, n.x, n.y, n.z, 0.4f); // @4aa81e + GyroApplyDamageImpulse(gyroSubsystem, 0.0f, 1.0f, 0.0f, 0.2f); // @4aa86c + } if (GroundLog()) DEBUG_STREAM << "[ground] CRUNCH (crushable icon) at (" << savedPos.x << ", " << savedPos.z << ")\n" << std::flush; diff --git a/game/reconstructed/projweap.cpp b/game/reconstructed/projweap.cpp index 8354dc3..49e53e7 100644 --- a/game/reconstructed/projweap.cpp +++ b/game/reconstructed/projweap.cpp @@ -604,6 +604,20 @@ void { Check(this); + // Binary @4bc136-4bc19c (task #56): the firing RECOIL kick, BEFORE the ammo + // pull (no early-out precedes it in the binary). Gate: per-shot damage > + // 3.0f (@0x4bc3f4) && the owner mech has a gyro. Direction (0, 0.6, -1.5) + // (0x3f19999a/0xbfc00000), magnitude = damageAmount / 16 (@0x4bc3f8). + // (The binary's preceding heat add is a separate pre-existing gap.) + if (damageData.damageAmount > 3.0f && owner != 0) + { + extern void GyroApplyDamageImpulse(Subsystem *, Scalar, Scalar, Scalar, Scalar); + Subsystem *g = ((Mech *)owner)->GetGyroSubsystem(); // mech+0x528 + if (g != 0) + GyroApplyDamageImpulse(g, 0.0f, 0.6f, -1.5f, + damageData.damageAmount * 0.0625f); + } + // Pull a round; a dry / not-ready bin latches NoAmmo and aborts the shot. if (!ConsumeRound()) // FUN_004bd4f4 (AmmoBin::FeedAmmo) {