Files
BT411/game/reconstructed/hud.hpp
T
Joe DiPrimaandClaude Opus 5 4ccc2a7eec control mode: Basic re-centre used the STICKY held-button cell -- and the elevation-limit swap was never ported
Sauron: "toggled through advanced controls from standard to advanced and back
to standard -- lost torso control."

The cycle is 0 Basic -> 1 Standard -> 2 Veteran -> WRAPS TO BASIC, so getting
from "advanced" back to Standard PASSES THROUGH BASIC, whose arm re-centres the
torso.  @004afbe0 is a complete spec and the port got three things wrong:

    iVar1 = mech+0x438 (TORSO)        iVar2 = mech+0x5b4 (HUD)
    if (mode == 0) {                                 // BASIC
        *(iVar1 + 0x1f0) = 0;                        // analogTwistAxis
        *(iVar1 + 0x274) = 1;                        // recenterActive
        *(iVar1 + 0x220) = *(iVar1 + 0x228);         // vertLimitTop
        *(iVar1 + 0x224) = *(iVar1 + 0x22c);         // vertLimitBottom
        *(iVar2 + 0x2a0) = 1;                        // HUD flickerActive
    } else if (mode - 1U < 2) {                      // STANDARD/VETERAN
        *(iVar1 + 0x220) = *(iVar1 + 0x230);
        *(iVar1 + 0x224) = *(iVar1 + 0x234);
    }

1. WRONG CELL.  Basic called CommandRecenter() -> centerCommand (@0x208), the
   HELD-BUTTON cell: TorsoSimulation re-arms recenterActive from it EVERY frame
   it is non-zero, and only the input path clears it -- a mode switch has no
   button release to follow.  Digital twist commands are processed BEFORE the
   centerCommand block, so while it is set they are overridden as fast as they
   are applied: the torso stops responding.  The binary sets recenterActive
   (@0x274) directly -- a ONE-SHOT that self-clears on settle
   (`recenterActive = Recenter(dt)`) and is cancelled by any twist input.
2. THE ELEVATION-LIMIT SWAP WAS MISSING ENTIRELY.  Two authored pairs exist --
   BASIC @0x228/@0x22C (full top, HALF bottom) vs STANDARD/VETERAN @0x230/@0x234
   (the full pair) -- and all four members were ctor-written and read by NOTHING.
   Basic never restricted downward travel; the assisted modes never restored it.
3. Basic also raises the HUD's flickerActive (@0x2A0) so the horizon re-settles
   with the torso it just re-centred.  Not ported.  (New BTSetHudFlickerActive
   bridge in hud.cpp -- mechmppr sees Subsystem*, not HUD.)
Also removed an invented SetAnalogElevationAxis(0); the binary zeroes only 0x1F0.

MEASURED A/B (scratchpad/night13/modecycle.sh, LEGACY=1 for the old path;
BT_LEGACY_MODE_RECENTER=1 is the revert switch):

    ctrCmd=1 samples   legacy 26   fixed 0
    vLim pairs         fixed run shows BOTH -- (-0.698..0.349) = -40..20 deg
                       assisted, and (-0.349..0.349) = -20..20 deg Basic.
                       Before this commit only the ctor pair ever appeared.

WHAT IS *NOT* PROVEN.  I did not reproduce Sauron's PERMANENT loss.  In this
bench the legacy latch is periodic, not sticky:

    ..........LLLL......LLLL......LLLL......LLLL......LLLL......LL

because the desktop key bridge writes centerCommand every frame and zeroes it
when no button is held, so it self-recovers.  The torso IS locked while the cell
is set, which is the symptom -- but whether it stays locked depends on the input
path OWNING that cell.  On the glass/pad route (Sauron's) nothing may clear it,
which would make it permanent.  So: mechanism fixed and binary-grounded, exact
field persistence unverified.  Field-verify by cycling modes on a pad build.

Probe: the BT_TORSO_LOG gate line now carries ctrCmd / recen / vLim.
Bench hook: BT_MODECYCLE_EVERY=<n> cycles the mode from the mapper (the pod's
own route is console key 0x13d -- not a RIO button, so BT_BTNTEST cannot press
it, and mech4's BT_MODECYCLE_TEST counter did not advance in a solo run).
NOTE the bench needs BT_KEY_BRIDGE=1: with a PadRIO present the key-bridge
block that consumes the cycle is skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
2026-08-08 10:36:36 -05:00

293 lines
14 KiB
C++

//===========================================================================//
// File: hud.hpp //
// Project: BattleTech //
// Contents: Cockpit Heads-Up Display subsystem //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// --/--/95 ?? Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (Ghidra pseudo-C, recovered shard
// part_013.c) cross-referenced with the sibling subsystem sources
// (powersub.hpp/.cpp, heat.hpp/.cpp, sensor.hpp/.cpp). NO HUD.HPP survived;
// the class body below is inferred from usage, the embedded string pool and
// the streamed-resource / attribute tables. Each method cites its @ADDR.
//
// ---------------------------------------------------------------------------
// IDENTIFICATION (resolves a CLASSMAP "gap")
// ---------------------------------------------------------------------------
// * mech3.cpp factory map: HUD::CreateStreamedSubsystem @004b81d4,
// DefaultData &DAT_00511078.
// * vtable @0050511510 (slot0 dtor @004b816c), ctor @004b7f94.
// * CreateStreamedSubsystem @004b81d4 stamps classID 0x0BD6 and streamed
// model size 0x104 (resource +0x20 / +0x24), and parses the HUD-specific
// model fields "FlickerRate" / "HorizontalMovementPerSecond" /
// "HorizontalLimit" plus a "SegmentPageName" index.
// * String pool @005112d0..@00511390 ("HUD", "FlickerRate",
// "RotationOfTorsoHorizontal", "HorizontalTorsoLimitRight/Left",
// "SpeedOfTorsoHorizontal", "RangeToTarget", "Visible", "Lock",
// "HotBoxVector", "ThreatVector", "CompassHeading") names the gauge
// read-out attributes.
//
// CLASSMAP.md "Resolved conflict" already flagged @004b7f94 / vtable
// @00511510 as "the HUD torso-horizon gimbal display" mis-labelled
// "MechTech" by mech.cpp's factory switch (case 0xBD6). This file confirms
// that: classID 0xBD6, base PowerWatcher.
//
// ---------------------------------------------------------------------------
// BASE CLASS
// ---------------------------------------------------------------------------
// HUD IS-A PowerWatcher (powersub.hpp): the ctor @004b7f94 chains to the
// PowerWatcher ctor FUN_004b18a4 and CreateStreamedSubsystem chains to
// PowerWatcher::CreateStreamedSubsystem FUN_004b198c. PowerWatcher is a
// HeatWatcher/HeatableSubsystem-derived *monitor* subsystem (NOT a
// PoweredSubsystem); the HUD therefore inherits the heat/voltage watchdog
// state (heat-state level @0x140, watched-voltage alarm level @0x198) used
// by the per-frame update. The shared base object occupies bytes 0..0x1D8;
// HUD's own members begin at 0x1D8. Total object size 0x2A4 (matches
// mech.cpp factory allocation for the 0xBD6 case).
//
#if !defined(HUD_HPP)
# define HUD_HPP
#if !defined(POWERSUB_HPP)
#include "powersub.hpp"
#endif
//##########################################################################
//####################### HUD::SubsystemResource #####################
//##########################################################################
//
// Extends the PowerWatcher/HeatableSubsystem resource record. The HUD fields
// begin at +0xF4 (see CreateStreamedSubsystem @004b81d4).
//
struct HUD__SubsystemResource:
public PowerWatcher::SubsystemResource
{
Scalar flickerRate; // +0xF4 "FlickerRate" (defaults to -1.0 in the SegmentPage form)
int segmentPageIndex; // +0xF8 resolved "SegmentPageName" (copied from +0x28)
Scalar horizontalMovementPerSecond; // +0xFC "HorizontalMovementPerSecond"
Scalar horizontalLimit; // +0x100 "HorizontalLimit"
};
//##########################################################################
//############################## HUD #################################
//##########################################################################
class HUD:
public PowerWatcher
{
friend struct HUDLayoutCheck; // live Watcher-branch re-base witness (hud.cpp)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support
//
// DefaultData @00511078, ClassDerivations @00511088 ("HUD").
//
public:
static Derivation *GetClassDerivations(); // @00511088 (engine moved to accessor form)
static SharedData DefaultData; // @00511078
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support
//
// AttributePointers IndexEntry table @005110c0 (each record binds a name
// to an attribute id and an object offset, the offset encoded as
// byte_offset|1 -- the same scheme used by sensor.hpp).
//
// RE-DUMPED 2026-07-19 (Gitea #10 audit; section_dump.txt @5110b8, 16-byte
// records {id, namePtr, offset|1, 0}, terminator @511160). The table
// actually STARTS at @005110b8 with a FULL id-3 FlickerRate record (the old
// transcription had misread it as a "category label" and shifted every
// subsequent offset down one slot):
// id 3 "FlickerRate" -> @0x1D8
// id 4 "RotationOfTorsoHorizontal" -> @0x1DC
// id 5 "HorizontalTorsoLimitRight" -> @0x1E0
// id 6 "HorizontalTorsoLimitLeft" -> @0x1E4
// id 7 "SpeedOfTorsoHorizontal" -> @0x1E8
// id 8 "RangeToTarget" -> @0x1EC
// id 9 "Visible" -> @0x1F4 (init 1; @0x1F0 is unbound)
// id 10 "Lock" -> @0x1F8 (init 0)
// id 11 "HotBoxVector" -> @0x1FC (Point3D)
// id 12 "ThreatVector" -> @0x208 (Point3D)
// id 13 "CompassHeading" -> @0x214 (Scalar: yaw + torso twist)
// CROSS-CHECKED against the ctor @004b7f94 (inits @0x1F4=1 Visible-on /
// @0x1F8=0 Lock-off; @0x1F0 uninitialized) and HudSimulation @004b7830
// (per-frame @0x1DC <- linkedTorso+0x1D8 = Torso::currentTwist; @0x1EC
// range default 1200.0 + the 500 m/s display slide; the Lock rule writes
// @0x1F8 at :5622/:5633; @0x214 = EulerAngles(mech)[0] + @0x1DC at :5676;
// the hotbox Point3D copy @0x1FC at :5680). All consistent.
//
// CONFIRMED (task #37): the recovered reticle Execute @004cdcf0 binds and
// USES ids 4/5/6/8/0xA/0xB/0xC/0xD exactly per these names --
// RotationOfTorsoHorizontal drives the twist tape over the
// HorizontalTorsoLimit full-scales, RangeToTarget the range caret, Lock
// the spinning lock ring, HotBoxVector the target box (eye-space vec3),
// ThreatVector the compass threat trail, CompassHeading the rose
// rotation.
//
public:
enum {
RotationOfTorsoHorizontalAttributeID = PowerWatcher::NextAttributeID, // 4
HorizontalTorsoLimitRightAttributeID, // 5
HorizontalTorsoLimitLeftAttributeID, // 6
SpeedOfTorsoHorizontalAttributeID, // 7
RangeToTargetAttributeID, // 8
VisibleAttributeID, // 9
LockAttributeID, // 10
HotBoxVectorAttributeID, // 11
ThreatVectorAttributeID, // 12
CompassHeadingAttributeID, // 13
NextAttributeID
};
private:
static const IndexEntry AttributePointers[]; // @005110c0
protected:
static AttributeIndexSet AttributeIndex;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Local data. Offsets are byte offsets into the shipped object; names are
// best-effort from the attribute pool + ctor/HudSimulation usage.
//
// Member names re-based 2026-07-19 (Gitea #10) against the RE-DUMPED attr
// table @005110b8 -- the old names were shifted one slot (see the table
// comment above).
protected:
Scalar flickerRate; // @0x1D8 resource +0xF4 ("FlickerRate", attr id 3)
// Torso / target read-outs sampled at construction AND per-frame in
// HudSimulation from the linked Torso subsystem (owner Mech +0x438 ->
// this->linkedEntity @0x234):
Scalar rotationOfTorsoHorizontal; // @0x1DC <- linkedEntity +0x1D8 (Torso::currentTwist; attr 4)
Scalar torsoLimitRight; // @0x1E0 <- linkedEntity +0x1DC (attr 5)
Scalar torsoLimitLeft; // @0x1E4 <- linkedEntity +0x1E0 (attr 6)
Scalar torsoSpeed; // @0x1E8 <- linkedEntity +0x1E8 (attr 7 "SpeedOfTorsoHorizontal")
Scalar rangeToTarget; // @0x1EC computed target distance; 1200.0 default; 500 m/s display slide (attr 8)
int scratch1F0; // @0x1F0 unbound / not ctor-initialized in the binary
int visible; // @0x1F4 init 1 ("Visible", attr 9; heat/page gating)
int lockFlag; // @0x1F8 init 0 ("Lock", attr 10; the :5619-5634 lock rule)
Point3D hotBoxVector; // @0x1FC init (0,0,0) ("HotBoxVector", attr 11)
Point3D threatVector; // @0x208 init (0,0,0) ("ThreatVector", attr 12); see SetThreatVector
Scalar compassHeading; // @0x214 init 0 ("CompassHeading", attr 13; yaw euler[0] + torso twist @:5676)
Scalar blinkTimer; // @0x218 accumulates time, reset against flickerRate
Scalar flickerTimer; // @0x21C
int blinkState; // @0x224 init 1 (visible phase of the blink)
int transitionFlag; // @0x22C init 0
Scalar previousHorizontal; // @0x230
int linkedEntity; // @0x234 owner Mech +0x438 (active weapon/reticle source)
HeatAlarm statusAlarm; // @0x238 2-level page/status indicator (Initialize/SetLevel surface)
// Free-aim / reticle-slew demand: WRITTEN by MechControlsMapper::InterpretControls
// (@004afd10 cockpit+0x28c <- stick yaw when the torso's horizontal twist is
// disabled; zeroed in Basic mode). Was placeholder-named flickerPhase (init-only).
Scalar freeAimSlew; // @0x28C
public:
void SetFreeAimSlew(Scalar v) { freeAimSlew = v; } // mapper write (@004afd10 cockpit+0x28c)
protected:
Scalar scratch290; // @0x290 init 0
Scalar horizontalTorsoOffset; // @0x294 init 0; clamped to +/- horizontalLimit, written to owner +0x36C
Scalar horizontalMovementPerSecond; // @0x298 resource +0xFC
Scalar horizontalLimit; // @0x29C resource +0x100
int flickerActive; // @0x2A0 current flicker on/off (UpdateFlicker @004b7ed4)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Simulation Support
//
public:
// @0x2A0 -- raised by the CONTROL-MODE switch's BASIC arm
// (`*(mech+0x5b4 + 0x2a0) = 1`, @004afbe0) so the HUD horizon re-settles
// with the torso that Basic just re-centred. Reached from mechmppr via
// hud.cpp's BTSetHudFlickerActive bridge (that TU sees Subsystem*, not HUD).
void SetFlickerActive(int on) { Check(this); flickerActive = on; }
typedef void
(HUD::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
void
HudSimulation(Scalar time_slice); // @004b7830 (Performance, PTR @00511170)
// @004b7810 writes the Point3D @0x208 -- which the RE-DUMPED attr table
// (2026-07-19) proves is the THREAT vector, not the compass (the compass
// is the Scalar @0x214). Renamed from the old mislabel SetCompassHeading;
// no port caller exists (the port threat-trail feed goes through the
// reticle globals, btl4vid.cpp).
void
SetThreatVector(const Point3D &threat); // @004b7810 (writes threatVector @0x208)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Subsystem virtual overrides (vtable @00511510)
//
public:
Logical
HandleMessage(int message); // slot 9, @004b7780 (body not captured)
void
ResetToInitialState(Logical powered = True); // slot 10, @004b77bc
// #55 step 4: the respawn sweep (Mech::Reset -> DeathReset) never
// reached this class -- it fell through to the empty Subsystem base, so
// this subsystem kept its previous life's state. Chains our own
// authentic slot-10 body.
void DeathReset(int reset_command);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Test Class Support
//
public:
static Logical TestClass(Mech&);
Logical TestInstance() const; // @004b81b8
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
public:
typedef HUD__SubsystemResource SubsystemResource;
HUD(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
); // @004b7f94
~HUD(); // @004b816c
static int
CreateStreamedSubsystem( // @004b81d4
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes = 1
);
protected:
// Flicker helper @004b7ed4: drives flickerActive (@0x2A0) toward 0,
// returns non-zero while still flickering.
Logical UpdateFlicker(Scalar time_slice); // @004b7ed4
//
// Reconstruction accessors for the inherited watchdog state read by
// HudSimulation: heat-watcher flag (this+0x40), watched-voltage alarm
// level (@0x198) and heat-state level (@0x140). The PowerWatcher /
// HeatWatcher base in this header set does not expose these, so they are
// best-effort placeholders that let the per-frame page-gating logic
// compile; a human should bind them to the real inherited members.
// (See CROSS-FAMILY NEEDS.)
//
int HeatWatcherFlag() const { return 0; }
int WatchedVoltageLevel() const { return 0; }
int HeatStateLevel() const { return 0; }
};
#endif