# Damage-hub call site of the gyro fan-out FUN_004b2980 (Mech::TakeDamageMessageHandler @0x4a0230, call @0x4a02fb)

## semantics
HUB (function 0x4a0230 = Mech::TakeDamageMessageHandler(this=ebx=[ebp+8], TakeDamageMessage *msg=esi=[ebp+0xC]); message layout: +0x1C inflictingEntity(EntityID,8B), +0x24 damageZone(int), +0x28 invalidDamageZone(Logical), +0x2C damageData(Damage,0x30B), +0x58 = damageData.burstCount -- matches engine/MUNGA/ENTITY3.h:119-148 + DAMAGE.h:24-58 exactly):

1. 0x4a0242: if ((this->flags@+0x28 & 0xC) == 4)  // replicant check (non-blocking)
       DebugPrint("Replicant Mech recieving takedamagemessage!", "d:\\tesla_bt\\bt\\MECH.CPP", 0x3DA=986);   // FUN_0040385c
2. 0x4a0264: Gyroscope *gyro = this->gyroSubsystem;   // [ebx+0x528]
   if (gyro != 0)   // <-- THE ONLY GATE
   {
3.   0x4a0272-0x4a02f0: sub esp,0x30; copy the 12 dwords msg+0x2C..msg+0x58 (the whole Damage struct BY VALUE: damageType, damageAmount, damageForce.xyz, surfaceNormal.xyz, impactPoint.xyz, burstCount) into the outgoing frame;
4.   0x4a02fa-0x4a0300: push gyro; call FUN_004b2980; add esp,0x34.   // Gyroscope::ApplyDamage(Damage) -- BEFORE any zone resolution / inflictor bookkeeping / collision branch
   }
5. 0x4a0308: flag = FUN_0049fb54(this) ? 1 : 0;   // unrelated predicate local
6. 0x4a0327: FUN_00420ef4(&this->lastInflictingID@+0x43C, &msg->inflictingEntity@+0x1C);   // EntityID assign (our mech.cpp:602)
7. 0x4a033c-0x4a034d: attacker = EntityRegistry([0x4efc94]+0x2C) vfunc[+0xC] lookup by &msg->inflictingEntity; read attacker->player@+0x190 and this->player@+0x190.
8. 0x4a0361: if (msg->damageData.damageType == 0 /*Collision*/) { FUN_0049ffcc(this, &msg->damageData); goto 0x4a07b5; }   // collision detours; gyro was ALREADY called (fan-out no-ops type 0 internally)
9. 0x4a037a: if (msg->invalidDamageZone@+0x28) { msg->damageZone@+0x24 = FUN_0049ed0c(this->damageLookupTable@+0x444, &msg->damageData.impactPoint@+0x4C); msg->invalidDamageZone = 0; }   // == our port's ResolveHit path
10. 0x4a039c-0x4a03f0: threatDir = attackerPos(attacker+0x100) - myPos(this+0x100) (FUN_00408644, seeded from ZeroVector@0x4e0f74); FUN_004b7810(FUN_0049fe70(this,&threatDir));   // HUD threat feed (== our BTPushHudThreat block)
... (rest of hub: per-zone consolidation loops, FUN_004a0164 shield-band test using [this+0x190]->+0x25C, FUN_0049ccc4 etc. -- beyond this deliverable).

FAN-OUT GATES/USE (0x4b2980, cross-verified so the call args are named correctly):
- return if damageAmount==0.0f (const@0x4b2d80) OR damageType==0 (Collision);
- dir = damageForce; if |dir-Zero|<1e-4 componentwise: dir=(+/-RandomUnit(),0,+/-RandomUnit()); Normalize(dir);
- placeRot@0x2A8=(0, *externalPitchPtr@0x258, 0); placeQuat@0x298=QuatFromRotation; placePos@0x28C=Zero; workMatrix@0x25C=MatrixFromPlacement(FUN_0040ab44); dir = dir * workMatrix (FUN_00408744); Normalize(dir);
- switch(damageType): case 1/3/4: s = damageAmount / damageMultiplier[t]@(0x32C+4t); case 2: s = burstCount * damageAmount / damageMultiplier[2]; then trans/pitchRoll/yaw/vibration = s * damageResponse[t].{+0,+4,+8,+C}@(0x340+0x10*t);
- clamp each of the four to <= 1.3f (const@0x4b2d88 = 0x3FA66666);
- ApplyDamageImpulse(gyro, dir.xyz, trans) @0x4b2d00; ApplyDamageTorque(gyro, dir.xyz, pitchRoll) @0x4b2d23; ApplyDamageImpulse(gyro, *(Vector3D*)(gyro+0x390), vibration) @0x4b2d4b; ApplyVerticalImpulse(gyro, dir.xyz, yaw) @0x4b2d6e.
NOTE/CORRECTION for the recon: gyro+0x390..0x398 is read as a Vector3D (vibration axis, ctor-init (0,1,0) = up) -- our gyro.hpp:323-325 currently splits it into three scalars animationOffset/animationScale/animationPhase; the fan-out proves it doubles as the vibration direction vector [T1].

## call_args
- [esp+0x00] arg1: Gyroscope* gyro -> mech->gyroSubsystem. Source: `mov ecx,[ebx+0x528]; push ecx` (ebx = mech `this` from [ebp+8]); mech+0x528 is our named member gyroSubsystem (game/reconstructed/mech.hpp:422). Pushed LAST (after the 0x30-byte block), so it is the first cdecl arg; caller cleanup `add esp,0x34` = 13 dwords total.
- [esp+0x04] arg2: int damageType (Enumeration 0..4 = Collision/Ballistic/Explosive/Laser/Energy, DAMAGE.h:27-34). Source: `mov edx,[eax]` where eax = lea(message+0x2C) = &message->damageData.damageType. Fan-out reads it at [ebp+0xC]: early `test/je exit` if 0 (Collision), then `cmp edx,4; ja default; jmp [edx*4+0x4b2b10]` jump table.
- [esp+0x08] arg3: Scalar damageAmount. Source: `mov ecx,[eax+4]` = message->damageData.damageAmount @msg+0x30. Fan-out [ebp+0x10]: `fld; fcomp 0.0(@0x4b2d80); je exit` -- zero damage no-ops; otherwise it is the numerator of every response term (amount / damageMultiplier[type] * damageResponse[type].field).
- [esp+0x0C..0x14] args4-6: Vector3D damageForce (x,y,z) -- the WORLD-SPACE hit force/direction from the attacker (NOT a zone normal). Source: dwords msg+0x34/0x38/0x3C via edx=lea(eax+8). Fan-out [ebp+0x14]: eps-compared (FUN_004084fc, eps 0x38d1b717=1e-4) against ZeroVector@0x4e0f74; if ~zero a RANDOM horizontal dir is rolled (x=+/-RandomUnit, y=0, z=+/-RandomUnit; sign = RandomUnit()>=0.5, RNG FUN_00408050 on stream 0x521f5c); then normalized (FUN_004087f4 = dst=src/|src|, byte-verified), rotated by the yaw-only twist matrix, re-normalized.
- [esp+0x18..0x20] args7-9: Normal surfaceNormal (x,y,z). Source: dwords msg+0x40/0x44/0x48 via ecx=lea(eax+0x14). Copied into the by-value block but NEVER READ by FUN_004b2980 (no [ebp+0x20..0x28] reference anywhere in 0x4b2980-0x4b2d7c) -- dead weight of the by-value Damage struct.
- [esp+0x24..0x2C] args10-12: Point3D impactPoint (x,y,z, global space). Source: dwords msg+0x4C/0x50/0x54 via edx=lea(eax+0x20). Also NEVER READ by the fan-out; the HUB reads it later for the cylinder zone lookup (binary 0x4a0381: FUN_0049ed0c(mech->damageLookupTable@0x444, &msg+0x4C) -- already our port's table->ResolveHit(message->damageData.impactPoint), mech.cpp:620).
- [esp+0x30] arg13: int burstCount ('number of times to apply the damage', DAMAGE.h:49-54). Source: `mov eax,[eax+0x2c]` = msg+0x58. Fan-out [ebp+0x38]: used ONLY in the ExplosiveDamageType(2) case -- `fild [ebp+0x38]; fmul amount; fdiv damageMultiplier[2]@0x334; fmul damageResponse[2].{trans@0x360,pitchRoll@0x364,yaw@0x368,vibration@0x36C}` -- i.e. a missile-volley multiplier. Ballistic/Laser/Energy ignore it.

## port_integration
Damage/collision path: game/reconstructed/mech.cpp:583 Mech::TakeDamageMessageHandler -- binary places the gyro feed FIRST (0x4a0264-0x4a0300), before the lastInflictingID write (our mech.cpp:602), before the threat feed (mech.cpp:609), and before zone resolution (mech.cpp:618-620); insert immediately after `Check(message);` (mech.cpp:585). Everything the call needs is already there: `gyroSubsystem` (mech.hpp:422, set at mech.cpp:1036) and `message->damageData` (the full engine Damage, ENTITY3.h:139-140). Because Gyroscope is a complete type only in gyro.cpp (databinding rule), use the existing bridge pattern (GyroBindExternalPitch @gyro.cpp:903 / extern decls @mech.cpp:217-218):

(1) gyro.cpp (next to the other bridges, ~line 903):
    // Binary 0x4a0264-0x4a02fb (Mech::TakeDamageMessageHandler @0x4a0230): damage->gyro
    // fan-out entry.  Damage is passed BY VALUE (12 dwords) in the binary; FUN_004b2980
    // never mutates it, so const-ref is byte-equivalent.
    void GyroApplyDamage(Subsystem *gyro, const Damage &damage)
    { ((Gyroscope *)gyro)->ApplyDamage(damage); }
    (ApplyDamage = the FUN_004b2980 body per exact_semantics; the other agent's deliverable.)

(2) mech.cpp:218-area:  extern void GyroApplyDamage(Subsystem *, const Damage &);

(3) mech.cpp, right after Check(message) at line 585:
    // Binary 0x4a0264-0x4a02fb: cockpit hit-bounce.  Feed the gyro the raw Damage
    // record FIRST; the sole gate is a non-null gyro (mech+0x528).  FUN_004b2980
    // itself no-ops CollisionDamageType(0) and damageAmount==0, so no extra gating.
    if (gyroSubsystem != 0)
        GyroApplyDamage(gyroSubsystem, message->damageData);

This covers ALL weapon damage (aimed and unaimed -- the kick precedes zone resolution, so invalid-zone hits still bounce). Collision-type TakeDamage messages routed through this handler correctly produce NO message-path kick (fan-out exits on type 0); the authentic collision crunches are the separate direct performance calls (0x4aa254/0x4aa288/0x4aa342/0x4aa81e/0x4aa86c/0x4bc194 -- the dormant 'gyro crunch feed' comment in mech4.cpp AuthenticGroundAndCollide; other agent's angle). Senders must fill Damage.damageForce with the world-space attack direction (beam/projectile flight dir); if a sender leaves it zero the fan-out authentically rolls a random horizontal direction, so existing zero-force senders still work.

confidence: high
