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
+152 -16
View File
@@ -646,6 +646,9 @@ static int gBTLaserKey = 0; // raw key states (set by the keyboar
static int gBTPPCKey = 0;
static int gBTMissileKey = 0;
static int gBTPinkyKey = 0; // key '4' = the pod's 4th fire button (Pinky 0x45)
int gBTModeCycle = 0; // 'M' edge: cycle the control mode (mapper consumes)
float gBTTwistAxis = 0.0f; // Q/E torso-twist deflection (assisted-mode stick X)
int gBTTorsoRecenter = 0; // 'X' edge: pulse the authentic torso recenter (mapper consumes)
static int gBTConfigKey = 0; // task #6: HOLD 'G' = the weapon-configure button
static int gBTGenSelKey = 0; // task #12: F5..F8 = SelectGeneratorA..D, F9 = mode toggle
// (0 idle; else the message id 4..8)
@@ -785,6 +788,69 @@ static BTProjectile gProjectiles[64];
extern void BTPushBeam(float,float,float, float,float,float, unsigned, float, float);
//###########################################################################
// Zone-effect bridge (mechdmg has no `application` access): route the damage-
// band effect through the AUTHENTIC RendererManager::StartEntityEffect chain
// (the @004d097c dispatcher; the AudioRenderer hears the same broadcast).
// Returns 0 when the manager isn't up so the caller can fall back.
//###########################################################################
int
BTStartZoneEffect(Mech *mech, void *zone, int resource)
{
if (mech == 0 || zone == 0 || resource <= 0
|| application == 0 || application->GetRendererManager() == 0)
return 0;
application->GetRendererManager()->StartEntityEffect(
(Entity *)mech, (DamageZone *)zone,
(ResourceDescription::ResourceID)resource);
return 1;
}
//###########################################################################
// Segment world-transform bridge (the @004d097c per-zone effect dispatcher +
// the attached-emitter follow in L4VIDEO's PFX layer). Resolves the entity's
// segment (by index) to its world position + 3x3 basis rows; falls back to
// the mech origin at torso height when the segment doesn't resolve. Returns
// 0 for a non-mech / unregistered entity (the follow then keeps its last
// frame -- and the emitter dies with its authored window anyway).
//###########################################################################
int
BTResolveSegmentWorld(void *entity, int seg_index, float *pos3, float *rows9)
{
extern int BTIsRegisteredMech(Entity *e);
if (entity == 0 || !BTIsRegisteredMech((Entity *)entity))
return 0;
Mech *m = (Mech *)entity;
Point3D p = m->localOrigin.linearPosition;
p.y += kMuzzleHeight; // fallback: torso height
if (seg_index >= 0)
{
EntitySegment::SegmentTableIterator it(m->segmentTable);
EntitySegment *seg;
while ((seg = it.ReadAndNext()) != NULL)
{
if (seg->GetIndex() == seg_index)
{
AffineMatrix mw;
mw.Multiply(seg->GetSegmentToEntity(), m->localToWorld);
p = mw; // Point3D = matrix translation
break;
}
}
}
pos3[0] = (float)p.x; pos3[1] = (float)p.y; pos3[2] = (float)p.z;
UnitVector ax, ay, az;
m->localToWorld.GetFromAxis(X_Axis, &ax);
m->localToWorld.GetFromAxis(Y_Axis, &ay);
m->localToWorld.GetFromAxis(Z_Axis, &az);
rows9[0] = (float)ax.x; rows9[1] = (float)ax.y; rows9[2] = (float)ax.z;
rows9[3] = (float)ay.x; rows9[4] = (float)ay.y; rows9[5] = (float)ay.z;
rows9[6] = (float)az.x; rows9[7] = (float)az.y; rows9[8] = (float)az.z;
return 1;
}
//###########################################################################
// PER-ROUND DETONATION (the binary's Missile::MoveAndCollide @004bef78: every
// round spawns ITS OWN ExplosionModelFile at its impact point, resource
@@ -1299,6 +1365,11 @@ void
frameEntryWorldVelocity = Vector3D(0.0f, 0.0f, 0.0f);
ramLastVictim = 0;
ramContactLinger = 0.0f;
// StopAllEntityEffects (@004d0c14): a respawned mech must not trail its
// corpse's attached zone effects -- the authentic per-entity effect
// cleanup, broadcast to every renderer.
if (application != 0 && application->GetRendererManager() != 0)
application->GetRendererManager()->StopAllEntityEffects((Entity *)this);
// --- our RELOCATED gait/motion accumulators -> identity (the 1995 offsets
// the decomp zeroes map to these named members in our layout) ---
@@ -1823,6 +1894,56 @@ void
// task #13: 'C' cycles the coolant valve (BT_VALVE_SLOT
// picks the condenser roster slot; default = Condenser1).
gBTValveKey = focused && (pAsync('C') & dn) ? 1 : 0;
// TORSO CONTROLS (2026-07-13): 'M' cycles the control mode
// (Basic -> Standard -> Veteran -- the pod console button,
// CycleControlModeMessageHandler); Q/E deflect the torso
// twist axis (the STICK in Standard/Veteran, where A/D
// become the pedals). Ramped like the turn stick.
{
static int sPrevM = 0;
const int mNow = focused && (pAsync('M') & dn) ? 1 : 0;
if (mNow && !sPrevM) gBTModeCycle = 1; // edge -> one cycle
sPrevM = mNow;
const int tw = (focused && (pAsync('E') & dn) ? 1 : 0)
- (focused && (pAsync('Q') & dn) ? 1 : 0);
static float sTwist = 0.0f;
if (tw != 0)
{
sTwist += tw * kStickRate * dt;
if (sTwist > 1.0f) sTwist = 1.0f;
if (sTwist < -1.0f) sTwist = -1.0f;
}
else if (sTwist != 0.0f)
{
// SPRING-CENTER on release (user fix 2026-07-13: the
// old hold-deflection model left a residual axis ->
// the torso drifted forever with no way to stop).
// The pod stick is spring-centered: the axis is a
// twist-RATE demand, so release = rate 0 = the torso
// HOLDS where you aimed it (position is kept by
// Torso::currentTwist, not by the axis). Same model
// as the A/D turn stick (kStickCenterRate).
const float step = kStickCenterRate * dt;
if (sTwist > step) sTwist -= step;
else if (sTwist < -step) sTwist += step;
else sTwist = 0.0f;
}
// X = all-stop for the torso too: zero the axis NOW and
// pulse the AUTHENTIC recenter (torso centerCommand ->
// Recenter @004b6918 slews currentTwist back to 0; any
// new Q/E deflection cancels it, sim-side). Separate
// edge detector from the drive all-stop below -- both
// fire on the same press.
static int sPrevXT = 0;
const int xtNow = focused && (pAsync('X') & dn) ? 1 : 0;
if (xtNow && !sPrevXT)
{
sTwist = 0.0f;
gBTTorsoRecenter = 1; // mapper consumes -> CommandRecenter()
}
sPrevXT = xtNow;
gBTTwistAxis = sTwist;
}
// gBTDrive.fire = "any weapon trigger down" (feeds the bring-up
// damage dispatcher + the beam-visual keepalive)
gBTDrive.fire = (gBTLaserKey || gBTPPCKey || gBTMissileKey || gBTPinkyKey) ? 1 : 0;
@@ -1842,18 +1963,24 @@ void
}
sPrevV = vNow;
// RETICLE = TORSO BORESIGHT (task #39 correction): the pod had
// NO free-aim cursor -- the binary's HudSimulation computes
// reticlePosition from the mech's POSE quaternions
// (part_013.c:5652+, a rate-limited quat->euler of the torso
// aim), i.e. the crosshair marks where the TORSO GUNS point
// relative to the view, and you aim by steering the mech /
// twisting the torso (the pod's right stick). The earlier
// mouse-cursor slew was a mis-sourced stand-in and is
// REMOVED. Our cockpit view is body-mounted, so the
// crosshair deflection = the torso twist projected to screen
// -- identically ZERO on the fixed-torso BLH (dead-centre
// boresight). BT_AIM="x y" remains as the headless harness.
// RETICLE = SCREEN CENTER (task #58 correction, supersedes the
// task #39 "body-mounted view" model): the CAMERA yaws with
// the torso (the cockpit sits on jointtorso -- see the
// gBTEyeTwist publisher), so screen center always IS the gun
// boresight and the crosshair stays centered while the WORLD
// rotates past it -- the twist reads on the bottom tape
// carets / compass / radar wedge, not the crosshair. [T1:
// jointtorso->jointeye->siteeyepoint chain + FUN_004c22c4
// inverse-chain view; crosshair-twist forensics 2026-07-13.]
// The old gBTAimX = tan(twist) slew was a port invention on
// the falsified body-mounted premise -- with the yawing eye
// it double-counts (crosshair pinned to HULL-forward, fire
// ray resolving body-forward: "twisting leaves the
// crosshairs behind"). Reticle mobility exists in the
// binary (Reticle::reticlePosition, RETICLE.h:42) but its
// twist-era use is the FIXED-TORSO free-aim channel
// (mech+0x36c, writer un-exported) -- deferred.
// BT_AIM="x y" remains as the headless harness.
{
static int sAimEnv = -1;
static float sAimEnvX = 0.0f, sAimEnvY = 0.0f;
@@ -1870,10 +1997,10 @@ void
}
else
{
// boresight = torso twist vs the body-mounted view,
// projected through the live per-axis projection.
extern float BTTwistToReticleX(float twist_rad);
gBTAimX = BTTwistToReticleX(gBTHudTwist); // BLH: 0
// Boresight = screen center: the eye carries the
// twist (task #58), the pick ray inherits it from
// the published eye basis.
gBTAimX = 0.0f;
gBTAimY = 0.0f;
}
}
@@ -3299,6 +3426,15 @@ void
gBTHudTwistLimit = (float)GetHorizontalFiringReach();// HorizontalTorsoLimit (attrs 5/6)
gBTHudGroupMask = (int)targetReticle.reticleElementMask & 0xF;
gBTHudPrimary = ((int)targetReticle.reticleElementMask & 0x20) != 0;
// task #58: publish the live twist for renderer-side DIAGNOSTICS
// (correlating [eyefwd] against the twist). The cockpit eye does
// NOT consume this -- it inherits the twist authentically through
// jointtorso's HingeRenderable in the draw traversal (see the note
// in DPLEyeRenderable::Execute).
{
extern float gBTEyeTwist;
gBTEyeTwist = gBTHudTwist;
}
}
// task #6 ORDER FIX: the scripted block must run BEFORE the fire-push