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
+129
View File
@@ -49,6 +49,9 @@
#if !defined(MECHWEAP_HPP)
# include <mechweap.hpp> // MechWeapon::GetClassDerivations (reticle pip)
#endif
#if !defined(MECHDMG_HPP)
# include <mechdmg.hpp> // Mech__DamageZone::segmentIndex (the @004d097c dispatcher)
#endif
#if !defined(NOTATION_HPP)
# include <notation.hpp>
#endif
@@ -368,6 +371,22 @@ HierarchicalDrawComponent*
LinearMatrix offset_matrix;
offset_matrix = segment->GetBaseOffset(); // [0x74]
// task #58 diagnostics (BT_SEG_DUMP): the draw-tree topology -- which
// segment carries which joint, and who parents whom.
if (getenv("BT_SEG_DUMP"))
{
int ji = segment->GetJointIndex();
DEBUG_STREAM << "[seg] " << segment->GetIndex()
<< " '" << (const char *)segment->GetName()
<< "' parent=" << (segment->GetParent()
? segment->GetParentIndex() : -1)
<< " joint=" << ji
<< " jtype=" << (ji == -1 ? -1
: (int)joint_subsystem->GetJoint(ji)->GetJointType())
<< " site=" << (int)(segment->IsSiteSegment() != 0)
<< "\n" << std::flush;
}
//
// Parent renderable: root if the segment has no parent, else the
// renderable already built for its parent segment.
@@ -866,6 +885,116 @@ void BTRebuildMechModel(Entity *entity)
//
//#############################################################################
// StartEntityEffectImplementation @004d097c (coverage-audit reconstruction)
//#############################################################################
//
// The per-zone EFFECT DISPATCHER -- the target of the whole authentic chain:
// MechDeathHandler's class-5 -> RendererManager::StartEntityEffect ->
// Renderer::StartEntityEffectMessageHandler [T0: resolves the GameModel +
// applies the ExplosionResourceTable graphic-state remap] -> this virtual.
// The binary body (@004d097c, part_014.c:5839):
// 1. resolve the zone -> its transform (+0x74, row 3 = the world position)
// and its VIDEO INDEX (+0xc4->+0xc0) = the segment slot;
// 2. look the entity up in the renderer's per-entity tree (renderer+0x3a4,
// == our mMechRenderTrees) -> the segment's DCS = the effect SOCKET;
// 3. SearchList(resource, VideoModelResourceType) -> walk the video-object
// records; atoi(name) < 1000 WARNS (the authentic gate: only INDIE/psfx
// ids attach per-zone), >= 1000 starts the effect ON the socket, tagged
// with the owning player (+0x190 -> +0x1e0) for the stop-all sweep.
// Port mapping: the socket attachment = BTStartPfxAttached (the emitter rides
// the segment via BTResolveSegmentWorld each frame); the tag = the entity
// (BTStopEntityPfx kills by entity on respawn). Layout access is by NAMED
// members only (zone->segmentIndex; the binary micro-offsets differ on our
// compiled classes -- the databinding trap).
//
void
BTL4VideoRenderer::StartEntityEffectImplementation(
Entity *entity,
DamageZone *damage_zone,
ResourceDescription::ResourceID resource_ID
)
{
if (entity == NULL || damage_zone == NULL)
{
DEBUG_STREAM << "StartEntityEffectImplementation: no entity/zone" << std::endl;
return;
}
// 1. the zone's segment slot (binary +0xc4->+0xc0; our named member)
int seg_index = ((Mech__DamageZone *)damage_zone)->EffectSegmentIndex();
// 2. the segment's world position + frame (binary: zone transform +0x74)
extern int BTResolveSegmentWorld(void *entity, int seg_index, float *pos3, float *rows9);
float pos[3], rows[9];
if (!BTResolveSegmentWorld(entity, seg_index, pos, rows))
{
DEBUG_STREAM << "StartEntityEffectImplementation: entity has no segment table" << std::endl;
return;
}
// 3. the effect resource's VIDEO MODEL (type 10) -- the effect-number list
Check(application);
ResourceDescription *res = application->GetResourceFile()->SearchList(
resource_ID, ResourceDescription::VideoModelResourceType);
if (res == NULL)
{
DEBUG_STREAM << "StartEntityEffectImplementation: " << (long)resource_ID
<< " has no video resource" << std::endl;
return;
}
res->Lock();
// VideoModel payload [T0 L4VIDEO.h L4VideoObject, RES byte-verified]:
// int32 count + count x 32-byte records {char name[15]; pad; int type;
// int modes; float blinkPeriod; float pctOn} -- atoi(name) = the effect id.
const unsigned char *pay = (const unsigned char *)res->resourceAddress;
int count = *(const int *)pay;
if (count < 0 || count > 16)
count = 0; // malformed -- refuse
const unsigned char *rec = pay + 4;
for (int i = 0; i < count; ++i, rec += 32)
{
char name[16];
memcpy(name, rec, 15);
name[15] = 0;
int fx = atoi(name);
if (fx < 1000)
{
// authentic gate (@004d097c: "< 1000" warns) -- per-zone effects
// are INDIE/psfx-band only
DEBUG_STREAM << "StartEntityEffectImplementation: non-INDIE effect "
<< fx << " in zone effect list" << std::endl;
continue;
}
extern void BTStartPfxAttached(int, void *, int, float, float, float, const float *);
BTStartPfxAttached(fx - 1000, (void *)entity, seg_index,
pos[0], pos[1], pos[2], rows);
if (getenv("BT_DEATH_LOG"))
DEBUG_STREAM << "[zonefx] entity " << entity->GetEntityID()
<< " seg " << seg_index << " psfx " << (fx - 1000)
<< " at(" << pos[0] << "," << pos[1] << "," << pos[2] << ")" << std::endl;
}
res->Unlock();
}
//
// StopAllEntityEffectsImplementation @004d0c14 -- the respawn cleanup: the
// binary kills every effect tagged (playerIdx<<16 .. |0xffff); the port kills
// every emitter tagged to the entity.
//
void
BTL4VideoRenderer::StopAllEntityEffectsImplementation(Entity *entity)
{
if (entity == NULL)
{
DEBUG_STREAM << "StopAllEntityEffectsImplementation: no entity" << std::endl;
return;
}
extern void BTStopEntityPfx(void *owner);
BTStopEntityPfx((void *)entity);
}
//#############################################################################
// SwapToWreck (ExplosionScripts effect 104, reconstructed)
//#############################################################################