The recovered system: fire channels = LBE4ControlsManager buttonGroups (0x40/0x45/0x46/0x47); default groups = the per-mech type-6 controls-map resource in BTL4.RES, installed by the T0 CreateStreamedMappings the port already called -- it needed only the TriggerState attribute (id 0x13 PINNED to the binary value; fireImpulse@0x31C is the binary's TriggerState) and an input feed. Keyboard/harness now push press/release edges into the button groups; the gBT*Trigger bypasses, per-type keyboard split and 1,0 pulse hack are retired -- weapons sharing a button fire TOGETHER (madcat Trigger = 4 weapons). Myomers @4b9550/@4b95b8 misattribution corrected (they are MechWeapon ConfigureMappables/ChooseButton). Verified 2-node: kill through the authentic chain (12 hits vs ~36 pre-groups). Config-mode session (regrouping UI) = the remaining stage, KB-scoped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
9.3 KiB
Gyro damage fan-out FUN_004b2980 (0x4b2980-0x4b2d8b, unexported gap) — disassembled byte-exact via scratchpad/dis_4b2980.py from reference/decomp/section_dump.txt, plus its hub call site 0x4a02fb inside the take-damage hub FUN_004a0230
semantics
SIGNATURE (cdecl, caller cleans 0x34 = 13 dwords; args are the engine Damage struct — engine/MUNGA/DAMAGE.h, T0 — flattened by value after this):
void GyroApplyDamageFanOut(Gyroscope g /ebp+8/, Enumeration damageType /+0xC*/, Scalar damageAmount /+0x10/, Vector3D damageForce /+0x14..0x1C/, Normal surfaceNormal /+0x20..0x28 UNUSED/, Point3D impactPoint /+0x2C..0x34 UNUSED/, int burstCount /+0x38/)
BODY (every step from the disassembly):
- @4b298c: if (damageAmount == 0.0f) return; // const 0.0f @0x4b2d80
- @4b299e: if (damageType == 0 /Damage::CollisionDamageType/) return; // collision NEVER bounces via this path (jump-table case 0 that zeroes all four responses is dead code behind this early return)
- @4b29a9: Scalar trans=0 /ebp-0x10/, pitchRoll=0 /-0xC/, yaw=0 /-8/, vibration=0 /-4/;
- Vector3D dir; /* ebp-0x1C: x@-0x1C y@-0x18 z@-0x14 / @4b29bd: if (VecEqualWithinEps(&damageForce, &kZeroVec3 /global 0x4e0f74 = (0,0,0)/, 1e-4f /imm 0x38d1b717/)) // FUN_004084fc: |dx|<=eps && |dy|<=eps && |dz|<=eps -> 1 { // RANDOM horizontal direction; RandomUnit = FUN_00408050 = (float)laggedXorRand() * 1/32768 (0x38000000 @0x408078) -> [0,1); TWO rolls per component (sign roll, then value roll) @4b29d7: dir.x = (RandomUnit() >= 0.5f /@0x4b2d84*/) ? RandomUnit() : -RandomUnit(); // fchs on the < branch @4b2a0d: dir.z = (RandomUnit() >= 0.5f) ? RandomUnit() : -RandomUnit(); @4b2a43: dir.y = 0.0f; } else @4b2a4a: dir = damageForce; // FUN_00408440 vec3 copy
- @4b2a62: Normalize(dir, dir); // FUN_004087f4 (dst,src) = src/|src|
- @4b2a67: g->placeRot.y /0x2AC/ = *(g->externalPitchPtr /0x258/); // = torso currentTwist g->placeRot.x /0x2A8/ = 0; g->placeRot.z /0x2B0/ = 0;
- @4b2a90: QuatFromRotation(&g->placeQuat /0x298/, &g->placeRot); // FUN_00409a00
- @4b2aa8: g->placePos /0x28C/ = kZeroVec3; // vec3 copy from 0x4e0f74
- @4b2ac0: Mat34FromPlacement(&g->workMatrix /0x25C/, &g->placePos); // FUN_0040ab44; placement = {pos@+0, quat@+0xC} (0x298-0x28C=0xC checks)
- @4b2ac7: Vector3D tmp /ebp-0x28/ = dir; Vec3TimesMat34Rows(&dir, &tmp, &g->workMatrix); // FUN_00408744 — rotate the world-space hit direction by yaw=torsoTwist into the torso/body frame
- @4b2aed: Normalize(dir, dir);
- @4b2afd: switch (damageType) // jump table @0x4b2b10, cases 0..4; damageType>4 falls through with all four responses = 0 case 1 /Ballistic, @4b2b3d/: each computed independently as fld amount; fdiv mult; fmul resp: trans = damageAmount / g->damageMultiplier[1] /0x330/ * g->damageResponse[1].trans /0x350/; pitchRoll = damageAmount / g->damageMultiplier[1] * g->damageResponse[1].pitchRoll /0x354/; yaw = damageAmount / g->damageMultiplier[1] * g->damageResponse[1].yaw /0x358/; vibration = damageAmount / g->damageMultiplier[1] * g->damageResponse[1].vibration /0x35C/; case 2 /Explosive, @4b2b8a/: ONLY case that reads burstCount (fild ebp+0x38 FIRST, then fmul amount): trans = (Scalar)burstCount * damageAmount / g->damageMultiplier[2] /0x334/ * g->damageResponse[2].trans /0x360/; (…same pattern for .pitchRoll 0x364 / .yaw 0x368 / .vibration 0x36C) case 3 /Laser, @4b2be3/: damageAmount / g->damageMultiplier[3] /0x338/ * g->damageResponse[3].{trans 0x370, pitchRoll 0x374, yaw 0x378, vibration 0x37C}; case 4 /Energy, @4b2c2d/: damageAmount / g->damageMultiplier[4] /0x33C/ * g->damageResponse[4].{trans 0x380, pitchRoll 0x384, yaw 0x388, vibration 0x38C};
- @4b2c75: upper-clamp each of the four independently: if (v > 1.3f /0x3fa66666 @0x4b2d88/) v = 1.3f; // no lower clamp
- Four calls, each cdecl (this, x, y, z, magnitude), add esp 0x14:
@4b2d00: g->ApplyDamageImpulse(dir.x, dir.y, dir.z, trans); // translational eye kick along the body-frame hit direction
@4b2d23: g->ApplyDamageTorque (dir.x, dir.y, dir.z, pitchRoll);
@4b2d4b: g->ApplyDamageImpulse(vibDir.x, vibDir.y, vibDir.z, vibration); // SECOND impulse call: direction = the vec3 at gyro+0x390 — WHY TWO: call 1 is the directional knock (resp.trans), call 2 is the vibration shake along the FIXED member vector at +0x390, ctor-initialized (0,1,0) = straight up. HEADER FIX NEEDED: gyro.hpp:323-325 currently names +0x390/+0x394/+0x398 as scalars animationOffset(0)/animationScale(1)/animationPhase(0) — the binary reads them as one Vector3D; the 0/1/0 init IS the unit +Y vibrationDirection. Rename to
Vector3D vibrationDirection; // @0x390 init (0,1,0). @4b2d6e: g->ApplyVerticalImpulse(dir.x, dir.y, dir.z, yaw); // NOTE: the response row's third field (offset +8, namedyawin our header) is what feeds ApplyVerticalImpulse (pitch-only bodyForce)
CALL SITE @0x4a02fb (inside hub FUN_004a0230, args: Mech* this@ebp+8 in ebx, TakeDamageMessage* @ebp+0xC in esi — this hub IS the take-damage entry; the gyro kick is its FIRST action):
- @4a0242: if ((mech->flags@+0x28 & 0xC) == 4) Warn("Replicant Mech recieving takedamagemessage!", "d:\tesla_bt\bt\MECH.CPP", 986 /0x3DA/) via FUN_0040385c — warning only, does NOT return;
- @4a0264: if (mech->gyroSubsystem /mech+0x528/ != 0) FUN_004b2980(gyro, <12 dwords copied verbatim from &msg->damageData at msg+0x2C>); add esp,0x34;
- then the rest of the hub (@4a0303: FUN_0049fb54(mech) bool check, …) — the gyro kick precedes ALL zone routing and is independent of damageZone validity.
call_args
- [esp+0x00] Gyroscope* this -> the mech's gyro subsystem -> ecx = [ebx+0x528] (Mech::gyroSubsystem, mech.hpp:422), pushed last, gated non-null @4a0264
- [esp+0x04] Enumeration damageType -> Damage::damageType (0=Collision 1=Ballistic 2=Explosive 3=Laser 4=Energy, DAMAGE.h) -> [eax] where eax = esi+0x2C = &message->damageData (message in esi)
- [esp+0x08] Scalar damageAmount -> Damage::damageAmount -> [eax+4] = message+0x30
- [esp+0x0C..0x14] Vector3D damageForce (by value, x/y/z) -> Damage::damageForce = the hit direction; if ~zero the callee rolls a random horizontal direction -> [eax+8/0xC/0x10] = message+0x34/0x38/0x3C via ebp-4 pointer temp
- [esp+0x18..0x20] Normal surfaceNormal (by value) -> Damage::surfaceNormal -> [eax+0x14/0x18/0x1C] = message+0x40/0x44/0x48 via ebp-8 temp -- passed but NEVER READ by FUN_004b2980
- [esp+0x24..0x2C] Point3D impactPoint (by value) -> Damage::impactPoint -> [eax+0x20/0x24/0x28] = message+0x4C/0x50/0x54 via ebp-0xC temp -- passed but NEVER READ
- [esp+0x30] int burstCount -> Damage::burstCount (missile volley count; multiplies damage ONLY in the Explosive case) -> [eax+0x2C] = message+0x58
- cleanup: add esp,0x34 = 13 dwords, confirming cdecl with the whole Damage struct flattened by value after this
port_integration
WHERE: game/reconstructed/mech.cpp:583 Mech::TakeDamageMessageHandler(TakeDamageMessage *message) — the binary's hub FUN_004a0230 kicks the gyro FIRST, before any zone resolution, so add the call near the top of our handler (after the Check/diag block, before the cylinder zone-resolve at mech.cpp:618 and before Entity::TakeDamageMessageHandler(message) at mech.cpp:630). The gate is exactly gyroSubsystem != 0 (already a named member, bound at mech.cpp:1036; external pitch already bound at mech.cpp:1155 via GyroBindExternalPitch so step 6's externalPitchPtr read is live). EXACT CALL TO ADD (bridge-fn pattern, same as GyroBindExternalPitch/GyroFrameJointWrite, because mech.cpp must not touch Gyroscope's layout — databinding trap): in mech.cpp: if (gyroSubsystem != 0) GyroApplyDamage(gyroSubsystem, message->damageData); and in gyro.cpp (complete-type TU) add the fan-out as a Gyroscope method void Gyroscope::ApplyDamage(Enumeration damageType, Scalar damageAmount, Vector3D damageForce, int burstCount) implementing the exact_semantics above (surfaceNormal/impactPoint are unread — safe to drop from the port signature), plus the bridge void GyroApplyDamage(Subsystem *s, const Damage &d) { static_cast<Gyroscope*>(s)->ApplyDamage(d.damageType, d.damageAmount, d.damageForce, d.burstCount); }. RandomUnit()/[0,1) already exists (mechrecon.hpp:98, RANDOM.h contract == FUN_00408050). REQUIRED HEADER FIX: gyro.hpp:323-325 — replace the three scalars animationOffset/animationScale/animationPhase @0x390/394/398 with Vector3D vibrationDirection; // @0x390 ctor init (0,1,0) (gyro.cpp:309-311 init collapses to one vec3 assignment); the fan-out's second ApplyDamageImpulse reads it as a vec3. IMPORTANT SCOPE NOTE: CollisionDamageType (0) early-returns in FUN_004b2980, so the collision/ground crunch bounce does NOT come from this fan-out — the dormant 'gyro crunch feed (0.4normal + 0.2up)' at mech4.cpp:3310 corresponds to the DIRECT Apply calls in the 0x4aa254/0x4aa288/0x4aa342/0x4aa81e/0x4aa86c region and 0x4bc194 (separate reconstruction; do not route collisions through ApplyDamage). Also note: on a replicant ((flags&0xC)==4) the binary only WARNS ('Replicant Mech recieving takedamagemessage!' MECH.CPP:986) and still proceeds — no replicant gate needed around the gyro kick.
confidence: high