Files
arcattackandClaude Fable 5 8ed6184d65 Combat: AUTHENTIC weapon groups -- streamed per-mech button bindings + the real fire chain (task #5)
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>
2026-07-11 13:06:29 -05:00

89 lines
9.1 KiB
Markdown

# Direct gyro crunch kicks: mech master performance FUN_004a9b5c (@4aa254/@4aa288 = alternate-gait engage jolt; @4aa342 = engaged-gait 0.4s rumble; @4aa81e/@4aa86c = crushable-icon crunch in the ground/collide response) + ProjectileWeapon::FireWeapon @4bc104 (@4bc194 = firing recoil kick). Verified NO gyro calls in the blocking-hit branch (@4aa88a-4aab5f) and NO kick in MissileLauncher::FireWeapon @4bcc60.
## semantics
CONTAINER 1: FUN_004a9b5c = Mech master per-frame performance, cdecl(Mech *this[ebp+8] -> ebx, Scalar dt[ebp+0xc]). edi = *(mech+0x128) = controls-mapper subsystem (port: controlSource / controlsMapper, subsystemArray[0]). gyro = mech+0x528 (port: gyroSubsystem).
=== BLOCK 1 (@4aa158-4aa2be): alternate-gait toggle + ENGAGE JOLT (sites @4aa254 + @4aa288) ===
if (airborneCycleRate@0x5bc != -1.1f) { // fcomp const @0x4ab174 = 0xbf8ccccd = -1.1f; == -1.1 -> skip whole toggle ("mech has no alternate cycle rate" sentinel)
if (legCycleSpeed@0x348 > 0.0f // fcomp @0x4ab178 = 0.0f
&& controlsMapper->speedDemand@+0x128 < 0.0f) { // [edi+0x128] fcomp 0.0, jae -> disengage
if (airborneSelect@0x3f4 == 0) { // RISING EDGE only
airborneSelect = 1;
RequestActionFlags(0x100); // FUN_004a4c54(this, 0x100)
forwardCycleRate@0x344 = airborneCycleRate@0x5bc;
if (gyro@0x528) {
Scalar r = RandomUnit(); // FUN_00408050, [0,1)
if (RandomUnit() > 0.5f) r = -r; // const @0x4ab170 = 0.5f (random sign)
// dir = (0, 0, r) -- Z-axis, random magnitude+sign
Gyroscope::ApplyDamageTorque (gyro, 0,0,r, 0.4f); // call @4aa254 -> 4b2de4, mag 0x3ecccccd
Gyroscope::ApplyDamageImpulse(gyro, 0,0,r, 0.4f); // call @4aa288 -> 4b2d8c, SAME vec
}
}
} else { // @4aa292 falling edge
if (airborneSelect@0x3f4) { airborneSelect = 0; RequestActionFlags(0x100);
forwardCycleRate@0x344 = groundCycleRate@0x5b8; }
}
}
SEMANTIC FLAG [T3]: the engage gate is (cycling && speedDemand < 0); by the port's own convention (negative demand = reverse throttle, mech4.cpp:2009) 0x3f4 reads as a REVERSE-gait select, so mech.hpp's names airborneSelect@0x3f4 / airborneCycleRate@0x5bc are suspect misnomers. Mechanics above are exact [T1] regardless of the name.
=== BLOCK 2 (@4aa2be-4aa365): ENGAGED-GAIT RUMBLE (site @4aa342) ===
if (airborneSelect@0x3f4 && legCycleSpeed@0x348 > walkStrideLength@0x534 && gyro@0x528) {
if (*(Scalar*)&this->0x5c4 > 0.0f) *(Scalar*)&this->0x5c4 -= dt; // countdown, [ebp+0xc]
else { Gyroscope::ApplyDamageImpulse(gyro, 0,1,0, 0.2f); // call @4aa342, dir=(0,1,0)=up, mag 0x3e4ccccd
*(Scalar*)&this->0x5c4 = 0.4f; } // period reset 0x3ecccccd @4aa347
}
FLAG: mech.hpp declares 0x5c4 as `int clipLoadGuard` -- the binary uses it here as a FLOAT rumble-period timer (mis-typed; needs a Scalar member).
=== BLOCK 3 (@4aa76c-4aa885): CRUSHABLE-ICON CRUNCH (sites @4aa81e + @4aa86c) -- the mech4.cpp:3310 dormant comment ===
Runs in the ground/collide response policy, ONLY on the sentinel branch:
if (dmg.damageAmount == 0.00123f) { // fcomp const @0x4ab180 = 0x3aa137f4 (crushable-CulturalIcon walk-through sentinel)
worldLinearVelocity = savedWorldVel; localOrigin.linearPosition = savedPos; // restore POST-SNAP saves
MoveCollisionVolume(); // vtbl+0x34
dmg.damageAmount = 0.0f;
if (gyro@0x528) {
Vector3D n = Normalize(dmg.damageForce); // FUN_004087f4 on Damage+8; damageForce = DELTA worldLinearVelocity across the bounce, filled by engine Mover::ProcessCollisionList (MOVER.cpp:1299 `damage->damageForce.Subtract(worldLinearVelocity, initial_velocity)`) -- i.e. the bounce-impulse direction ("the 0.4*normal")
Gyroscope::ApplyDamageTorque (gyro, n.x,n.y,n.z, 0.4f); // call @4aa81e, mag 0x3ecccccd
Gyroscope::ApplyDamageImpulse(gyro, 0,1,0, 0.2f); // call @4aa86c, up, mag 0x3e4ccccd
}
AlarmIndicator::SetLevel(mech+0x450 /*heatAlarm*/, 0); // FUN_0041bbd8 @4aa87d -- existing tail, not gyro
goto end; // jmp 4aab5f
}
The BLOCKING branch (damageAmount > 0.0, @4aa88a-4aab5f) has NO gyro calls (verified: only 41bbd8/408440/420ea4/4a7fc4/4a4c54 crash-anim calls). Caveat: FUN_004087f4 divides by |v| UNGUARDED -- if Delta-v ~= 0 the original NaN'd too; a Small_Enough guard is a flaggable deviation.
CONTAINER 2: ProjectileWeapon::FireWeapon @4bc104 (vtable slot 18 / +0x48 in vtable 0x51242c; `this` = weapon [ebp+8] -> ebx; owner mech = [ebx+0xd0]).
=== BLOCK 4 (@4bc136-4bc19c): FIRING RECOIL KICK (site @4bc194) ===
if (HeatModelActive()) // FUN_004ad7d4(this); gates ONLY the heat add
currentHeat@0x1c8 += heatPerShot@0x3d8;
Mech *m = owner@0xd0;
if (damageData.damageAmount@0x3ac > 3.0f // const @0x4bc3f4 = 3.0f (Damage embedded @0x3A8: type@0x3A8, amount@0x3AC)
&& m->gyroSubsystem@0x528) {
Gyroscope::ApplyDamageImpulse(gyro, 0.0f, 0.6f, -1.5f, // dir consts 0x3f19999a / 0xbfc00000
damageData.damageAmount * 0.0625f); // mag = amount/16, const @0x4bc3f8
}
then tracerCounter@0x408++, tracer modulo tracerInterval@0x438, matrix/muzzle spawn path... The kick precedes ammo consumption in the binary (no early-out before it). MissileLauncher::FireWeapon @4bcc60 does NOT chain to @4bc104 and has NO gyro kick (grep: no 0x528/FUN_004b2d8c refs; it uses +0x3ac only as per-missile damage).
## call_args
- placeholder
## port_integration
All target methods exist byte-exact in game/reconstructed/gyro.cpp (ApplyDamageImpulse @691, ApplyDamageTorque @706; signatures (Scalar x, Scalar y, Scalar z, Scalar magnitude) -- match the binary 5-dword cdecl frames). Access pattern: mech.hpp:422 `Subsystem *gyroSubsystem; // @0x528`; either include gyro.hpp and static_cast<Gyroscope*>, or follow the existing complete-type bridge pattern (extern GyroFrameJointWrite, declared mech4.cpp:3166, defined gyro.cpp:487) with e.g. GyroApplyDamageImpulse/Torque(Subsystem*, Scalar,Scalar,Scalar,Scalar) bridges in gyro.cpp -- the bridge avoids the databinding trap and matches house style.
(1) CRUSHABLE-ICON CRUNCH (@4aa81e/@4aa86c) -> game/reconstructed/mech4.cpp:3304-3315, inside Mech::AuthenticGroundAndCollide's `if (dmg.damageAmount == 0.00123f)` branch, REPLACING the dormant comment at mech4.cpp:3310. Exact insertion (after `dmg.damageAmount = 0.0f;`, before the GroundLog):
if (gyroSubsystem) {
Vector3D n; n.Normalize(dmg.damageForce); // FUN_004087f4; damageForce already filled by engine Mover::ProcessCollisionList (engine/MUNGA/MOVER.cpp:1299)
GyroApplyDamageTorque (gyroSubsystem, n.x, n.y, n.z, 0.4f); // @4aa81e
GyroApplyDamageImpulse(gyroSubsystem, 0.0f, 1.0f, 0.0f, 0.2f); // @4aa86c
}
dmg.damageForce needs no new plumbing -- ProcessCollisionList at mech4.cpp:3302 is the engine method that writes it. Do NOT add a kick to the blocking-hit branch (mech4.cpp:3316+) -- binary-verified none exists.
(2) ALTERNATE-GAIT ENGAGE JOLT + RUMBLE (@4aa254/@4aa288/@4aa342) -> mech4.cpp local-drive path, immediately BEFORE the channel advance at mech4.cpp:2056-2058 (`adv = AdvanceBodyAnimation(dt, 1); legAdv = AdvanceLegAnimation(dt);`) -- the binary runs these two blocks (@4aa158-4aa365) directly before its IsDisabled + AdvanceLegAnimation sequence (@4aa365-4aa399). All gate members exist in mech.hpp (airborneSelect, forwardCycleRate@0x344, legCycleSpeed@0x348, walkStrideLength@0x534, groundCycleRate@0x5b8, airborneCycleRate@0x5bc, RequestActionFlags @004a4c54); speedDemand is already read as controlsMapper->speedDemand at mech4.cpp:2008. Two fixes needed: (a) the port currently has NO writer of airborneSelect -- this block IS the authentic writer (engage: legCycleSpeed>0 && speedDemand<0 && airborneCycleRate != -1.1f, +RequestActionFlags(0x100) +forwardCycleRate switch on BOTH edges); (b) mech+0x5c4 is declared `int clipLoadGuard` in mech.hpp:662 -- the binary uses it as a Scalar rumble timer here (add/re-type a Scalar member, e.g. gyroRumbleTimer). RandomUnit exists (dmgtable.cpp:86 RandomUnit / projweap.cpp:486 UniformRandom = FUN_00408050).
(3) FIRING RECOIL KICK (@4bc194) -> game/reconstructed/projweap.cpp:603 ProjectileWeapon::FireWeapon, at the TOP (binary order: heat add, then kick, then tracer/spawn -- i.e. insert after Check(this), before ConsumeRound()):
if (damageData.damageAmount > 3.0f && owner) { // damageData @0x3A8 (mechweap.hpp:326), amount @0x3AC
Subsystem *g = ((Mech *)owner)->gyroSubsystem; // owner @0xd0 -> mech+0x528
if (g) GyroApplyDamageImpulse(g, 0.0f, 0.6f, -1.5f, damageData.damageAmount * 0.0625f);
}
(Placing it before ConsumeRound matches the binary exactly; the binary has no early-out before the kick.) Do NOT add the kick to MissileLauncher::FireWeapon (mislanch.cpp @4bcc60) -- binary-verified it has none. The binary's heat add ([+0x3d8] -> [+0x1c8] gated by FUN_004ad7d4/HeatModelActive) precedes the kick; if the port's FireWeapon lacks it, that is a separate (pre-existing) gap, not part of this hook.
confidence: high