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:
co-authored by
Claude Fable 5
parent
065c114590
commit
02cdfd6576
+101
-14
@@ -94,6 +94,9 @@ namespace {
|
||||
inline Scalar
|
||||
RecordField(Simulation::UpdateRecord *record, int byte_offset)
|
||||
{ return *(const Scalar*)((const char*)record + byte_offset); }
|
||||
inline void
|
||||
WriteRecordField(Simulation::UpdateRecord *record, int byte_offset, Scalar value)
|
||||
{ *(Scalar*)((char*)record + byte_offset) = value; }
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
@@ -243,19 +246,29 @@ Torso::Torso(
|
||||
// rate. TorsoSimulation drives the sweep (below). Remove after verification.
|
||||
if (isDamagedCopy == 0 && getenv("BT_FORCE_TORSO"))
|
||||
{
|
||||
// ⚠ HARNESS TRAP (task #58 post-mortem): this hook used to override the
|
||||
// joint nodes UNCONDITIONALLY with the BLH demo names -- on a mech whose
|
||||
// resource already resolved REAL torso joints (the MadCat's jointtorso)
|
||||
// that silently redirected the whole sweep into the SHADOW hinge, and a
|
||||
// probe run "proved" the cockpit camera ignored the twist (it doesn't;
|
||||
// the joint chain delivers it). Now: only fill joints that did NOT
|
||||
// resolve from the resource; a twist-capable mech sweeps its real ones.
|
||||
horizontalEnabled = True; // @0x250
|
||||
const char *mj = getenv("BT_FORCE_TORSO_JOINT");
|
||||
if (mj == 0 || *mj == '\0') mj = "jointshakey2";
|
||||
horizontalJointNode = ResolveJoint(mj); // torso body (ball)
|
||||
horizontalShadowJointNode = ResolveJoint("jointtshadow"); // shadow twist (hingey)
|
||||
horizontalLimitLeft = 0.7f; // @0x1E0 ~40 deg
|
||||
horizontalLimitRight = -0.7f; // @0x1DC
|
||||
if (horizontalJointNode == 0)
|
||||
{
|
||||
const char *mj = getenv("BT_FORCE_TORSO_JOINT");
|
||||
if (mj == 0 || *mj == '\0') mj = "jointshakey2";
|
||||
horizontalJointNode = ResolveJoint(mj); // torso body (ball)
|
||||
horizontalShadowJointNode = ResolveJoint("jointtshadow"); // shadow twist (hingey)
|
||||
horizontalLimitLeft = 0.7f; // @0x1E0 ~40 deg
|
||||
horizontalLimitRight = -0.7f; // @0x1DC
|
||||
}
|
||||
baseTwistRate = 1.0f; // @0x23C rad/s slew
|
||||
if (getenv("BT_TORSO_LOG"))
|
||||
{
|
||||
DEBUG_STREAM << "[torso] FORCE-ENABLE '" << mj << "' -> " << (void*)horizontalJointNode;
|
||||
DEBUG_STREAM << "[torso] FORCE-ENABLE node=" << (void*)horizontalJointNode;
|
||||
if (horizontalJointNode) DEBUG_STREAM << " type=" << (int)horizontalJointNode->GetJointType();
|
||||
DEBUG_STREAM << " ; shadow 'jointtshadow' -> " << (void*)horizontalShadowJointNode;
|
||||
DEBUG_STREAM << " ; shadow node=" << (void*)horizontalShadowJointNode;
|
||||
if (horizontalShadowJointNode) DEBUG_STREAM << " type=" << (int)horizontalShadowJointNode->GetJointType();
|
||||
DEBUG_STREAM << "\n" << std::flush;
|
||||
}
|
||||
@@ -387,14 +400,17 @@ Logical
|
||||
}
|
||||
|
||||
//
|
||||
// @004b6a78 (slot 6) [CONFIDENT] -- network/replay update record. Samples the
|
||||
// @004b6a78 (slot 6) [CONFIDENT] -- the REPLICANT-side apply (engine semantics
|
||||
// [T0]: ReadUpdateRecord = write the object FROM the record). Samples the
|
||||
// clock (FUN_00414b60) into lastUpdateTime, biasing it forward by one interval
|
||||
// when the elapsed window is below MinSlewMs, chains to Subsystem::WriteUpdateRecord
|
||||
// (FUN_0041bd34), then writes twistAtUpdate / twistVelocity / twistRate from the
|
||||
// record fields (record +0x10 / +0x14 / +0x18).
|
||||
// when the elapsed window is below MinSlewMs, chains to the base apply
|
||||
// (FUN_0041bd34 = ReadUpdateRecord, 2 args), then applies twistAtUpdate /
|
||||
// twistVelocity / twistRate from the record extras (+0x10 / +0x14 / +0x18).
|
||||
// (Task #57: previously mislabeled as WriteUpdateRecord -- the direction flip
|
||||
// made the MASTER consume its own blank record.)
|
||||
//
|
||||
void
|
||||
Torso::WriteUpdateRecord(UpdateRecord *message, int update_model)
|
||||
Torso::ReadUpdateRecord(UpdateRecord *message)
|
||||
{
|
||||
lastUpdateTime = GetCurrentTime(); // @0x254
|
||||
if ((Scalar)(lastUpdateTime - GetCreationTime()) / MsPerSecond < MinSlewMs)
|
||||
@@ -402,13 +418,41 @@ void
|
||||
lastUpdateTime += (lastUpdateTime - GetCreationTime()); // stretch tiny windows
|
||||
}
|
||||
|
||||
Subsystem::WriteUpdateRecord(message, update_model); // FUN_0041bd34
|
||||
Subsystem::ReadUpdateRecord(message); // FUN_0041bd34
|
||||
|
||||
twistAtUpdate = RecordField(message, 0x10); // @0x21C
|
||||
twistVelocity = RecordField(message, 0x14); // @0x1E8
|
||||
twistRate = RecordField(message, 0x18); // @0x238
|
||||
}
|
||||
|
||||
//
|
||||
// @004b6a1c (slot 7) [CONFIDENT, raw-disasm recovery -- Ghidra missed the
|
||||
// function start] -- the MASTER-side serialize (engine semantics [T0]:
|
||||
// WriteUpdateRecord = fill the record FROM the object). Chains the base
|
||||
// producer (FUN_0041c500(this, record, model), 3 args), extends the record to
|
||||
// 0x1C bytes and appends currentTwist / twistVelocity / twistRate at
|
||||
// +0x10/+0x14/+0x18 (exactly what the replicant's @004b6a78 applies), then
|
||||
// snapshots twistAtUpdate = currentTwist:
|
||||
//
|
||||
// 004b6a2e call 0041c500 ; base WriteUpdateRecord
|
||||
// 004b6a38 mov [record], 0x1C ; recordLength = 0x1C
|
||||
// 004b6a44+ record+0x10 = this+0x1D8 ; currentTwist
|
||||
// 004b6a51 record+0x14 = this+0x1E8 ; twistVelocity
|
||||
// 004b6a5a record+0x18 = this+0x238 ; twistRate
|
||||
// 004b6a63+ this+0x21C = this+0x1D8 ; twistAtUpdate = currentTwist
|
||||
//
|
||||
void
|
||||
Torso::WriteUpdateRecord(UpdateRecord *message, int update_model)
|
||||
{
|
||||
Subsystem::WriteUpdateRecord(message, update_model); // FUN_0041c500
|
||||
|
||||
message->recordLength = 0x1C; // base header 0x10 + 3 Scalars
|
||||
WriteRecordField(message, 0x10, currentTwist); // @0x1D8
|
||||
WriteRecordField(message, 0x14, twistVelocity); // @0x1E8
|
||||
WriteRecordField(message, 0x18, twistRate); // @0x238
|
||||
twistAtUpdate = currentTwist; // @0x21C snapshot at send
|
||||
}
|
||||
|
||||
|
||||
//#############################################################################
|
||||
// Per-frame simulation
|
||||
@@ -464,6 +508,34 @@ void
|
||||
analogTwistAxis = ((++s_sweep / 90) & 1) ? -1.0f : 1.0f; // +/- every ~90 frames
|
||||
}
|
||||
|
||||
// TORSO GATE PROBE (BT_TORSO_LOG): why is the twist rate zero?
|
||||
if (getenv("BT_TORSO_LOG"))
|
||||
{
|
||||
static int s_tl = 0;
|
||||
if ((s_tl++ % 120) == 0)
|
||||
{
|
||||
PoweredSubsystem *w = (PoweredSubsystem *)watchedLink.Resolve();
|
||||
DEBUG_STREAM << "[torso] hmOff=" << (int)HeatModelOff()
|
||||
<< " elec=" << (int)ElectricalStateLevel()
|
||||
<< " heatState=" << (int)HeatStateLevel()
|
||||
<< " rate=" << effectiveTwistRate
|
||||
<< " base=" << baseTwistRate
|
||||
<< " hEn=" << (int)horizontalEnabled
|
||||
<< " limits=(" << horizontalLimitRight << ".." << horizontalLimitLeft << ")"
|
||||
<< " axis=" << analogTwistAxis
|
||||
<< " twist=" << currentTwist
|
||||
<< " wIdx=" << watchedSubsystem
|
||||
<< " w=" << (void*)w
|
||||
<< " wElec=" << (w ? w->electricalStateAlarm.GetLevel() : -1)
|
||||
<< " wSrc=" << (void*)(w ? w->ResolveVoltageSource() : 0);
|
||||
Generator *g = (w ? (Generator *)w->ResolveVoltageSource() : 0);
|
||||
if (g)
|
||||
DEBUG_STREAM << " gOut=" << g->MeasuredVoltage()
|
||||
<< " gRated=" << g->RatedVoltageOf()
|
||||
<< " minV=" << minVoltage;
|
||||
DEBUG_STREAM << std::endl;
|
||||
}
|
||||
}
|
||||
Scalar twistStep = effectiveTwistRate * time_slice;
|
||||
Scalar elevStep = effectiveElevationRate * time_slice;
|
||||
|
||||
@@ -616,6 +688,21 @@ void
|
||||
// external joint pass as the master path (see TorsoSimulation for the @004b67ec
|
||||
// note). Harmless in single-player bring-up (no copies); correct for MP.
|
||||
UpdateJoints(); // FUN_004b67ec
|
||||
|
||||
// COPY-SIDE PROBE (BT_TORSO_LOG): where does the replicant's twist come from?
|
||||
if (getenv("BT_TORSO_LOG"))
|
||||
{
|
||||
static int s_cl = 0;
|
||||
if ((s_cl++ % 120) == 0)
|
||||
DEBUG_STREAM << "[torso-copy] cur=" << currentTwist
|
||||
<< " target=" << targetTwist
|
||||
<< " atUpd=" << twistAtUpdate
|
||||
<< " rate=" << twistRate
|
||||
<< " vel=" << twistVelocity
|
||||
<< " lastUpd=" << lastUpdateTime
|
||||
<< " now=" << GetCurrentTime()
|
||||
<< " copy=" << (int)isDamagedCopy << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user