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
@@ -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,
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
// (Authentic path = the gyro-driven eye-joint DCS chain -- deferred.)
|
||||
float gBTEyeBobY = 0.0f;
|
||||
float gBTEyeSwayX = 0.0f; // lateral weight-shift (the walk "swagger"), same source
|
||||
// task #58: the player torso's live twist (rad), published by mech4.cpp's HUD
|
||||
// tick. NOT consumed by the eye anymore -- the cockpit eye inherits the twist
|
||||
// through the jointtorso HingeRenderable in the draw traversal (see the note in
|
||||
// DPLEyeRenderable::Execute). Kept published for diagnostics/correlation.
|
||||
float gBTEyeTwist = 0.0f;
|
||||
#pragma hdrstop
|
||||
|
||||
#include "l4vidrnd.h"
|
||||
@@ -1340,6 +1345,21 @@ void
|
||||
//
|
||||
if(oldHinge != *myHinge)
|
||||
{
|
||||
// task #58 probe (BT_HINGE_LOG): which hinge renderables actually see
|
||||
// their joint move (standing + BT_FORCE_TORSO => only the torso hinge
|
||||
// and any doors change). kids = child renderables that would inherit
|
||||
// the rotation through the stack.
|
||||
{
|
||||
static const int s_hl = getenv("BT_HINGE_LOG") ? 1 : 0;
|
||||
static int s_hln = 0;
|
||||
if (s_hl && (s_hln++ % 60) == 0)
|
||||
DEBUG_STREAM << "[hinge] this=" << (void*)this
|
||||
<< " hinge=" << (void*)myHinge
|
||||
<< " rot=" << (float)(Radian)(myHinge->rotationAmount)
|
||||
<< " kids=" << (int)(End() - Enumerate())
|
||||
<< " par=" << (void*)GetParentProbe()
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
oldHinge = *myHinge;
|
||||
hingeOffsetMatrix.BuildIdentity();
|
||||
//#if SINGLE_AXIS_HINGE
|
||||
@@ -5604,6 +5624,24 @@ void
|
||||
// that assumed +Z=forward/+Y=up (which mis-framed the cockpit); the look/up axes now
|
||||
// fall out of the eye's own basis. pos/at/up above are kept only for the aim-boresight.
|
||||
D3DXMATRIX eyeW = mat4.ToD3DMatrix();
|
||||
// task #58 NOTE (torso twist vs the camera): the COCKPIT eye needs
|
||||
// NO explicit twist compose -- it executes inside the mech's draw
|
||||
// traversal under jointtorso's HingeRenderable (Torso::PushTwist ->
|
||||
// Joint::SetRotation -> hinge rotationAmount; HingeRenderable
|
||||
// multiplies the live hinge into the matrix stack), so `prev` above
|
||||
// already carries the twist and the view yaws authentically with
|
||||
// the canopy. VERIFIED live: with the real jointtorso swept, the
|
||||
// active cockpit [eyefwd] swings ~60 deg and the torso hinge sits
|
||||
// in the eye's parent chain (kids=2). An explicit
|
||||
// W' = W*E^-1*R_y(twist)*E compose was briefly added here and
|
||||
// DOUBLE-ROTATED the cockpit view (2x view vs 1x canopy: "the HUD
|
||||
// passes through the cockpit frame") and swung the CHASE camera
|
||||
// (parented on the root, same class) with the twist ("torso fixed,
|
||||
// legs twisting"). Both "eye is frozen" measurements that motivated
|
||||
// it were instrumentation artifacts: (1) the chase eye was the
|
||||
// active camera in the headless default view; (2) BT_FORCE_TORSO
|
||||
// used to redirect the sweep into the SHADOW hinge (see the hook's
|
||||
// trap note in torso.cpp).
|
||||
D3DXMATRIX view;
|
||||
D3DXMatrixInverse(&view, NULL, &eyeW);
|
||||
|
||||
@@ -5618,6 +5656,30 @@ void
|
||||
D3DXMatrixInverse(&view, NULL, &eyeW);
|
||||
}
|
||||
}
|
||||
// TWIST-FOLLOW PROBE (task #58, BT_EYE_LOG): does the ACTIVE eye's
|
||||
// forward yaw with the torso twist? Correlate with [torso] twist.
|
||||
// Also print the STACK TOP the eye composed with (prev) -- its
|
||||
// translation tells whether the eye executed inside the full
|
||||
// parent-chain traversal (t ~= eye rest position) or with a bare
|
||||
// stack (t == entity origin / zero).
|
||||
{
|
||||
static const int s_eyeLog = getenv("BT_EYE_LOG") ? 1 : 0;
|
||||
static int s_eyeLogN = 0;
|
||||
if (s_eyeLog && myRenderer->mCamera == this && (s_eyeLogN++ % 120) == 0)
|
||||
{
|
||||
DEBUG_STREAM << "[eyefwd] this=" << (void*)this
|
||||
<< " fwd=(" << eyeFwdW.x << "," << eyeFwdW.y
|
||||
<< "," << eyeFwdW.z << ") stackT=(" << prev(3,0) << ","
|
||||
<< prev(3,1) << "," << prev(3,2) << ") chain:";
|
||||
HierarchicalDrawComponent *p = GetParentProbe();
|
||||
for (int pc = 0; p != NULL && pc < 8; ++pc)
|
||||
{
|
||||
DEBUG_STREAM << " " << (void*)p;
|
||||
p = p->GetParentProbe();
|
||||
}
|
||||
DEBUG_STREAM << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
if (dbgEyeExec < 8)
|
||||
{
|
||||
DEBUG_STREAM << "[EYECHK] eyeW=(" << eyeW._41 << "," << eyeW._42 << "," << eyeW._43
|
||||
@@ -5650,9 +5712,10 @@ void
|
||||
// on a slope (body pitched ~8 deg to conform to the ground) aim
|
||||
// its pick ray INTO the ground, short of a distant target
|
||||
// (mechPicks=0). Level the boresight: drop the view direction's
|
||||
// pitch and rebuild an upright basis (world +Y up). The reticle
|
||||
// X still carries the torso twist (BTTwistToReticleX); the reticle
|
||||
// Y carries any aim elevation. Falls back to the raw basis only
|
||||
// pitch and rebuild an upright basis (world +Y up). The torso
|
||||
// twist rides IN eyeFwdW now (task #58 eye compose above) --
|
||||
// yaw survives the leveling, so the centered crosshair's pick
|
||||
// ray follows the twist. Falls back to the raw basis only
|
||||
// if the view is (degenerately) near-vertical.
|
||||
// Derive the boresight from the AUTHORITATIVE view matrix
|
||||
// (eyeFwdW = the world look direction from the inverse view),
|
||||
|
||||
@@ -69,6 +69,9 @@ public:
|
||||
|
||||
virtual bool IsStatic() { return false; }
|
||||
|
||||
// task #58 diagnostics: expose the parent link so probes can walk the tree.
|
||||
HierarchicalDrawComponent *GetParentProbe() const { return m_parent; }
|
||||
|
||||
d3d_OBJECT *GetDrawObj() { return this->graphicalObject; }
|
||||
// Swap this component's drawable in place. Execute() re-reads graphicalObject
|
||||
// every frame, so changing it makes the segment draw a different mesh next
|
||||
|
||||
Reference in New Issue
Block a user