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
+152
-16
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user