Files
BT411/scratchpad/fanout_1.md
T
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

8.6 KiB

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