Torso: the TWIST goes LIVE -- electrical watchdog chain, centered crosshair, coherent controls (task #57/#58)

The MadCat torso twists, the view turns with it, and targeting follows.
Three reconstruction fronts closed:

THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up):
- PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered
  Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog
  MIRRORS the watched subsystem's electrical level (+0x278), brownout
  downgrade when gen output <= minVoltage% x rated.  @004b1804 relabeled
  ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong.
- The factory watcher-CONNECT pass reconstructed (vtable slot +0x38,
  @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes):
  watchedLink.Add(roster[watchedSubsystem]) on the master node.  Was the
  SubProxy::Start() no-op -- every watchdog sat at 0 forever.
- MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f =
  permanent brownout) and PowerWatcher's Derivation chains its REAL base
  HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the
  whole Torso/Searchlight/ThermalSight family).
- KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not
  "HeatSink"); the btl4gaug heat-widget gate now tests it via the
  BTIsHeatWatcher bridge.

THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes):
- The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in
  every twist-capable .SKL; the camera + canopy ride the same hinge
  subtree through HingeRenderable's live matrix-stack compose -- ALREADY
  WORKING in the port.  The crosshair stays screen-centered (center IS
  the boresight); the twist reads on the tape carets/compass/radar.
- The real bug was the port's gBTAimX = tan(twist) slew (the falsified
  "body-mounted view" model): the camera already carried the twist, so
  the crosshair counter-slid to hull-forward and the fire ray with it.
  Deleted; the pick ray inherits the twist from the yawing eye basis.
- Two instrumentation traps documented (chase-eye-as-default-camera,
  BT_FORCE_TORSO clobbering real joints -> the hook now only fills
  unresolved ones); an over-correcting explicit eye compose was added on
  those false readings and retired the same day.

CONTROLS + REPLICATION:
- Q/E spring-center on release (the axis is a twist-RATE demand; the old
  hold-deflection model drifted forever); X also zeroes the axis and
  pulses the authentic torso Recenter (@004b6918).  M cycles control
  mode via the real CycleControlMode body.
- Torso update-record DIRECTION fixed: engine truth is Write=serialize /
  Read=apply; @004b6a78 is the READ (was mislabeled Write) and the
  missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C,
  twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD
  -140-degree ghost twist.
- Marching-ghost desync: 4 Standing-case guards zero stale reverse
  cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate).
- Kill credit rerouted to the OBSERVED killer (lastInflictingID ->
  killer's player link) -- kills count, target K/D populates.

KB: subsystems.md (watcher chain), multiplayer.md (record direction),
combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view
re-correction), decomp-reference.md (new addresses + tag fix),
open-questions.md (dead capability-roster loops 2-4, snapshot CD read).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-13 13:27:49 -05:00
co-authored by Claude Fable 5
parent 065c114590
commit 02cdfd6576
29 changed files with 1112 additions and 124 deletions
+67
View File
@@ -669,6 +669,12 @@ struct BTPfxEmitter
float emitAccum; // fractional particles owed (rate * dt integration)
int issued;
int active;
// ATTACHED effects (the @004d097c per-zone dispatcher): the emitter rides
// the owner entity's SEGMENT -- zone smoke follows the walking mech, and
// StopAllEntityEffects (@004d0c14: the respawn cleanup) kills every
// emitter tagged to the entity. ownerTag==0 = a free world effect.
void *ownerTag;
int followSeg;
};
struct BTPfxParticle
{
@@ -798,6 +804,8 @@ void BTStartPfxFrame(int effect_number, float x, float y, float z,
e.emitAccum = 1.0f; // first particle immediately
e.issued = 0;
e.active = 1;
e.ownerTag = 0;
e.followSeg = -1;
gBTPfxEmitters.push_back(e);
}
@@ -806,6 +814,52 @@ void BTStartPfx(int effect_number, float x, float y, float z)
BTStartPfxFrame(effect_number, x, y, z, 0, 0, 0);
}
// ATTACHED effect start (the @004d097c per-zone dispatcher): like
// BTStartPfxFrame, but the emitter rides the owner entity's segment -- the
// per-frame sim re-resolves the segment's world transform through the
// game-side bridge below, so a damage-band smoke plume TRAILS the walking
// mech instead of hanging at the hit point. rows9 = the initial 3x3 basis
// (rows = local axes in world), pos = the segment's current world position.
extern int BTResolveSegmentWorld(void *entity, int seg_index,
float *pos3, float *rows9); // mech4.cpp (game side)
void BTStartPfxAttached(int effect_number, void *owner, int seg_index,
float x, float y, float z, const float *rows9)
{
if (effect_number < 0 || effect_number >= BT_PFX_SLOTS)
return;
const BTPfxDef &d = gBTPfxDefs[effect_number];
if (!d.valid)
return;
if (gBTPfxEmitters.size() > 256)
return;
BTPfxEmitter e;
e.def = &d;
e.pos = D3DXVECTOR3(x, y, z);
e.ax = rows9 ? D3DXVECTOR3(rows9[0], rows9[1], rows9[2]) : D3DXVECTOR3(1, 0, 0);
e.ay = rows9 ? D3DXVECTOR3(rows9[3], rows9[4], rows9[5]) : D3DXVECTOR3(0, 1, 0);
e.az = rows9 ? D3DXVECTOR3(rows9[6], rows9[7], rows9[8]) : D3DXVECTOR3(0, 0, 1);
e.emitAccum = 1.0f;
e.issued = 0;
e.active = 1;
e.ownerTag = owner;
e.followSeg = seg_index;
gBTPfxEmitters.push_back(e);
}
// StopAllEntityEffects (@004d0c14 analog): the firmware killed every effect
// instance tagged (playerIdx<<16 .. |0xffff); the port kills every emitter
// tagged to the entity (the respawn cleanup -- a respawned mech must not
// trail its corpse's zone smoke). Emitted particles fade out naturally.
void BTStopEntityPfx(void *owner)
{
if (owner == 0)
return;
for (size_t i = 0; i < gBTPfxEmitters.size(); ++i)
if (gBTPfxEmitters[i].ownerTag == owner)
gBTPfxEmitters[i].active = 0;
}
// Spawn a few particles of a slot's effect DIRECTLY at a moving point (no
// emitter instance) -- the per-frame projectile SMOKE TRAIL (psfx 0 = dsrm,
// "the lrm smoke trail": its velocities stream +Z = BEHIND the round, so the
@@ -832,6 +886,7 @@ void BTPfxTrailPuff(int effect_number, float x, float y, float z,
D3DXVECTOR3 ay; D3DXVec3Cross(&ay, &az, &ax);
e.ax = ax; e.ay = ay; e.az = az;
e.emitAccum = 0.0f; e.issued = 0; e.active = 0;
e.ownerTag = 0; e.followSeg = -1;
for (int i = 0; i < count; ++i)
BTPfxSpawn(e);
}
@@ -1064,6 +1119,18 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt)
{
BTPfxEmitter &e = gBTPfxEmitters[ei];
if (!e.active) continue;
// attached emitters ride their owner's segment (@004d097c dispatcher)
if (e.ownerTag != 0)
{
float p3[3], r9[9];
if (BTResolveSegmentWorld(e.ownerTag, e.followSeg, p3, r9))
{
e.pos = D3DXVECTOR3(p3[0], p3[1], p3[2]);
e.ax = D3DXVECTOR3(r9[0], r9[1], r9[2]);
e.ay = D3DXVECTOR3(r9[3], r9[4], r9[5]);
e.az = D3DXVECTOR3(r9[6], r9[7], r9[8]);
}
}
const BTPfxDef &d = *e.def;
// CONTINUOUS emission at `rate` particles/second until maximum_issue is
// exhausted. Data-verified semantics: in EVERY shipped .PFX,