Files
BT411/game/reconstructed/hud.cpp
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

525 lines
21 KiB
C++

//===========================================================================//
// File: hud.cpp //
// Project: BattleTech //
// Contents: Cockpit Heads-Up Display subsystem //
// (reticle / torso-horizon gimbal / threat & status read-outs) //
//---------------------------------------------------------------------------//
// 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). Cluster @004b77bc..@004b8568. Each non-trivial method cites
// its @ADDR. Hex float constants converted to decimal where known
// (0x44960000 = 1200.0, 0x43fa0000 = 500.0, 500.0 = max torso slew/sec).
//
// Helper-function name mapping (engine internals referenced by the decomp):
// FUN_004b18a4 PowerWatcher base constructor (powersub.cpp)
// FUN_004b198c PowerWatcher::CreateStreamedSubsystem (powersub.cpp)
// FUN_004b181c PowerWatcher base per-frame update (powersub.cpp)
// FUN_004b1804 PowerWatcher::ResetToInitialState (slot10) (powersub.cpp)
// FUN_004b1930 ~PowerWatcher (powersub.cpp)
// FUN_0041b9ec AlarmIndicator(levels)
// FUN_0041baa4 ~AlarmIndicator
// FUN_0041bbd8 AlarmIndicator::SetLevel(n)
// FUN_0041a1a4 IsDerivedFrom(classDerivations)
// FUN_00404118 NotationFile::ReadScalar(file,name,&dest)
// FUN_00408440 Point3D::operator= / copy
// FUN_004dbb24 diagnostic stream << (error reporting)
// FUN_004022d0 operator delete / global free
// FUN_004b7ed4 HUD::UpdateFlicker
//
#include <bt.hpp>
#pragma hdrstop
#if !defined(HUD_HPP)
# include <hud.hpp>
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#endif
#if !defined(TESTBT_HPP)
# include <testbt.hpp>
#endif
// BASE-CHAIN RE-BASE witness: HUD is the only currently-LIVE Watcher-branch class
// (HUD : PowerWatcher). After the +156 re-base, its first own field must land at
// 0x1D8 -- the same base end as Torso::currentTwist -- which also corrects HUD's
// own torso+0x1D8 read. A friend so it can offsetof the protected field.
struct HUDLayoutCheck
{
static_assert(offsetof(HUD, flickerRate) == 0x1D8, "HUD first own field must be at 0x1D8 after the Watcher-branch re-base");
};
//
// Tuning constants read as read-only float globals adjacent to the
// HudSimulation body. The VALUES BELOW ARE THE BINARY'S, read straight off
// the .rdata rows in reference/decomp/section_dump.txt [T1]:
// 4b7ec0 8be55dc3 0000403f 0000803f 0000c842
// 4b7ed0 00000000
// -> ec4 = 0.75f ec8 = 1.0f ecc = 100.0f ed0 = 0.0f
//
// CORRECTED 2026-08-08. Every entry here used to be a 0.0f/500.0f STAND-IN
// under a guessed name, and the names described the wrong mechanism: there is
// no "heat threshold for HUD page visibility" at 0x4b7ec4 -- ec4/ec8 are the
// fire-control LOCK damage limits, and "MaxTorsoSlew = 500.0f" at ed0 read the
// range-slide Abs() idiom backwards (ed0 is the ZERO; 500.0 is an immediate).
// The LIVE implementation of the lock rule and the range slide is mech4.cpp's
// targeting step, which had both thresholds right all along. These stay so
// the addresses resolve to the truth for the next reader.
//
static const Point3D HudZeroVector(0.0f, 0.0f, 0.0f); // DAT_004e0f74/78/7c
// LOCK gate (a): your OWN HUD's host zone must be BELOW 75% damage, else
// fire-control lock is lost -- the `_DAT_004b7ec4 <= ownZone->damageLevel`
// arm of HudSimulation. A shot-up cockpit drops to "target held, no lock".
static const Scalar LockOwnZoneDamageLimit = 0.75f; // _DAT_004b7ec4
// LOCK gate (b): the TARGETED zone must be below 1.0 damage (a whole-mech
// target checks zone 0), so a dead zone cannot be re-locked.
static const Scalar LockTargetZoneDamageLimit = 1.0f; // _DAT_004b7ec8
// Subtracted from RangeToTarget (@0x1EC) every frame while the timed flag
// @0x22C is set (timer @0x21C accumulates to @0x1D8, then both clear).
// NOT reconstructed in the port's targeting step -- tracked, not implemented.
static const Scalar RangeBias = 100.0f; // _DAT_004b7ecc
// The shared ZERO: the right-hand side of the Abs() idiom on the range slide
// (`dt * 500.0 <= 0.0` picks the sign) and of an `== 0.0f` test at @0x28C.
// The 500 m/s slide RATE is an immediate literal (0x43fa0000), never this.
static const Scalar HudZero = 0.0f; // _DAT_004b7ed0
// The decay FLOOR for horizontalTorsoOffset (@0x294) in FUN_004b7ed4. The
// decay RATE is the object's own @0x298, not a constant. (Value verified.)
static const Scalar FlickerFloor = 0.0f; // _DAT_004b7f90
//
// Cross-family helper (definition lives in the mech game layer; declared here
// so this reconstructed body links without editing mech.hpp). Returns the
// owning Mech's segment-flags word (Mech +segmentFlags). See CROSS-FAMILY NEEDS.
//
extern LWord BT_GetSegmentFlags(Mech *owner);
//
// Resource-parse error reporter (original FUN_004dbb24 + abort path). Routes
// through the shared recon DebugStream warning channel.
//
static void ReportError(const char *subsystem_name, const char *message)
{
DebugStream << "HUD: " << subsystem_name << ": " << message << "\n";
}
//###########################################################################
// Shared Data Support
//
// DefaultData @00511078 / class derivation chain @00511088. Mirrors the
// engine's accessor-form shared-data construction (cf. PoweredSubsystem).
//
HUD::SharedData
HUD::DefaultData( // @00511078
HUD::GetClassDerivations(),
HUD::GetMessageHandlers(),
HUD::GetAttributeIndex(),
HUD::StateCount
);
Derivation*
HUD::GetClassDerivations() // @00511088
{
static Derivation classDerivations(
PowerWatcher::GetClassDerivations(),
"HUD"
);
return &classDerivations;
}
//###########################################################################
// Construction -- @004b7f94
//
// Chains to the PowerWatcher ctor (FUN_004b18a4) with HUD::DefaultData
// (&DAT_00511078), installs the HUD vtable (PTR_FUN_00511510), primes the
// gauge read-out block, builds the 2-level status AlarmIndicator (@0x238) and
// -- unless this is a damaged copy segment ((segmentFlags & 0xC) != 0) and the
// "has-performance" flag (0x100) is set -- registers HudSimulation as the
// active Performance (this[7..9] <- PTR_FUN_00511170). Finally it copies the
// streamed FlickerRate / HorizontalMovementPerSecond / HorizontalLimit into
// place and snapshots the torso/target read-outs from the linked entity
// (owner Mech +0x438).
//
HUD::HUD(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
PowerWatcher(owner, subsystem_ID, subsystem_resource, shared_data)
{
Check(owner);
Check_Pointer(subsystem_resource);
// (member names re-based 2026-07-19 per the re-dumped attr table @005110b8;
// the ctor @004b7f94 zeroes [0x76..0x7b] then sets [0x7d]=1 / [0x7e]=0 --
// note [0x7c]=@0x1F0 is left uninitialized in the binary)
flickerRate = 0.0f; // @0x1D8 (overwritten below from resource)
rotationOfTorsoHorizontal = 0.0f; // @0x1DC
torsoLimitRight = 0.0f; // @0x1E0
torsoLimitLeft = 0.0f; // @0x1E4
torsoSpeed = 0.0f; // @0x1E8
rangeToTarget = 0.0f; // @0x1EC
scratch1F0 = 0; // @0x1F0 (binary leaves it uninitialized; zeroed here)
visible = True; // @0x1F4 = 1 ("Visible" starts shown)
lockFlag = False; // @0x1F8 = 0 ("Lock" starts un-locked)
hotBoxVector = HudZeroVector; // @0x1FC
threatVector = HudZeroVector; // @0x208
compassHeading = 0.0f; // @0x214
blinkTimer = 0.0f; // @0x218
flickerTimer = 0.0f; // @0x21C
blinkState = True; // @0x224 = 1
transitionFlag = False; // @0x22C
previousHorizontal = 0.0f; // @0x230
statusAlarm.Initialize(2); // FUN_0041b9ec(this+0x238, 2)
freeAimSlew = 0.0f; // @0x28C (mapper free-aim demand)
scratch290 = 0.0f; // @0x290
horizontalTorsoOffset = 0.0f; // @0x294
if (((BT_GetSegmentFlags(owner) & 0xC) == 0) &&
((BT_GetSegmentFlags(owner) & 0x100) != 0))
{
SetPerformance(&HUD::HudSimulation); // this[7..9] <- PTR_FUN_00511170
}
flickerRate = subsystem_resource->flickerRate; // @0x1D8 <- res +0xF4
horizontalMovementPerSecond = subsystem_resource->horizontalMovementPerSecond; // @0x298 <- res +0xFC
horizontalLimit = subsystem_resource->horizontalLimit; // @0x29C <- res +0x100
// owner+0x438 is the torso-orientation source -- a Mech RAW OFFSET that does not
// hold a valid pointer in the reconstructed Mech layout (and isn't linked yet at
// subsystem-construction time), so dereferencing it crashes. Default the
// torso/twist readouts to 0; HudSimulation refreshes them per-frame once the link
// is live. TODO: wire a real Mech torso-orientation-source accessor.
// (binary snapshot @004b7f94: [0x77]@0x1DC <- linked+0x1D8 twist, [0x78]@0x1E0
// <- +0x1DC limitR, [0x79]@0x1E4 <- +0x1E0 limitL, [0x7a]@0x1E8 <- +0x1E8 speed)
linkedEntity = 0; // @0x234
rotationOfTorsoHorizontal = 0.0f;
torsoLimitRight = 0.0f;
torsoLimitLeft = 0.0f;
torsoSpeed = 0.0f;
Check_Fpu();
}
//
// Destruction -- @004b816c (vtable slot0). Reinstalls the vtable, clears the
// owning Mech's cached HUD back-reference (*(owner + 0x374) = 0), tears down
// the status AlarmIndicator and chains to ~PowerWatcher (FUN_004b1930);
// frees the block when the deleting-dtor bit is set.
//
HUD::~HUD()
{
Check(this);
// *(this->owner + 0x374) = 0;
// (original FUN_0041baa4 finalised the AlarmIndicator; HeatAlarm needs no teardown)
Check_Fpu();
}
//###########################################################################
// TestInstance -- @004b81b8
//
Logical
HUD::TestInstance() const
{
return IsDerivedFrom(*GetClassDerivations()); // FUN_0041a1a4(**this[3], 0x00511088)
}
Logical
HUD::TestClass(Mech &)
{
return True; // BEST-EFFORT (family convention; no distinct body captured)
}
//###########################################################################
// SetThreatVector -- @004b7810
//
// Copies a Point3D into threatVector (@0x208). Renamed 2026-07-19 (Gitea
// #10): the re-dumped attr table @005110b8 binds "ThreatVector" (attr 12) to
// @0x208 -- the old SetCompassHeading label was the one-slot transcription
// shift (the compass is the Scalar @0x214). No port caller.
//
void
HUD::SetThreatVector(const Point3D &threat)
{
threatVector = threat; // FUN_00408440(this+0x208, &threat)
}
//###########################################################################
// UpdateFlicker -- @004b7ed4
//
// Decays flickerActive (@0x2A0... actually horizontalTorsoOffset region @0x294)
// toward FlickerFloor by horizontalMovementPerSecond*time, clamping at the
// floor, and reports whether motion is still in progress. Used to give the
// torso-horizon needle a damped settle.
//
Logical
HUD::UpdateFlicker(Scalar time_slice)
{
Scalar step = horizontalMovementPerSecond * time_slice;
if (FlickerFloor < horizontalTorsoOffset)
{
horizontalTorsoOffset -= step;
if (horizontalTorsoOffset < FlickerFloor)
horizontalTorsoOffset = FlickerFloor;
}
if (horizontalTorsoOffset < FlickerFloor)
{
horizontalTorsoOffset += step;
if (horizontalTorsoOffset < FlickerFloor) // (decompiled clamp, verbatim)
horizontalTorsoOffset = FlickerFloor;
}
return (horizontalTorsoOffset != FlickerFloor) ? True : False;
}
//###########################################################################
// ResetToInitialState -- @004b77bc (slot 10)
//
// On a powered reset clears the blink/horizontal-slew accumulators, then
// chains to PowerWatcher::ResetToInitialState (FUN_004b1804).
//
void
HUD::ResetToInitialState(Logical powered)
{
if (powered)
{
blinkTimer = 0.0f; // @0x218
transitionFlag = False; // @0x22C
flickerTimer = 0.0f; // @0x21C
previousHorizontal = 0.0f; // @0x230
}
horizontalTorsoOffset = 0.0f; // @0x294
freeAimSlew = 0.0f; // @0x28C (mapper free-aim demand)
scratch290 = 0.0f; // @0x290
PowerWatcher::ResetToInitialState(True); // FUN_004b1804 (base override is parameterless)
(void)powered;
}
//###########################################################################
// HandleMessage -- @004b7780 (slot 9)
//
// BEST-EFFORT: the vtable installs a HUD-specific HandleMessage at @004b7780,
// but no body was captured in the recovered shards. Chains to the PowerWatcher
// handler otherwise.
//
// ATTRIBUTION CORRECTION (#61, 2026-07-25): this comment used to claim "a handler
// table @00511180 binds the message ToggleLamp to FUN_004b838c" as if it were
// HUD's. It is NOT -- @0051117C is **Searchlight's** message entry
// {3, "ToggleLamp" @00511440, @004b838c}; @00511180 is merely its name+handler
// pair (the entry base is 4 bytes lower). HUD's own data ends with its
// Performance triple at @00511170 (@004b7830, HudSimulation). See
// searchlight.cpp and context/decomp-reference.md for the table evidence.
//
Logical
HUD::HandleMessage(int message)
{
return PowerWatcher::HandleMessage(message); // TODO: verify @004b7780
}
//###########################################################################
// HudSimulation -- @004b7830 (Performance, PTR @00511170)
//
// The per-frame HUD update (torso-horizon gimbal + reticle/threat read-outs).
// High-level behaviour recovered from the decomp; offsets retained so a human
// can finish the member naming:
//
// 1. Base update: PowerWatcher base tick (FUN_004b181c).
// 2. Heat / voltage gating of the HUD page via the inherited state levels:
// watched-voltage alarm level @0x198 (2 -> off-but-visible, 4 -> ready)
// heat-state level @0x140 (0/1 -> visible, 2 -> blank)
// The status AlarmIndicator @0x238 is driven to 0/1 accordingly and a
// composite "page visible" flag is formed.
// 3. Blink: blinkTimer (@0x218) accumulates time and is compared against
// flickerRate (@0x1D8); on expiry blinkState (@0x224) toggles and the
// timer resets. When the blink is in its OFF phase the page is hidden.
// 4. Engine/start state of the host (mech +0x128 roster[0] state @0x198):
// 0 -> set HUD flag bit 0x1 on the graphic, clear bit 0x2
// 3 -> set bit 0x2, clear bit 0x1 (and gate on mech +0x410)
// writes the resulting "active" flag to *(mech +0x374).
// 5. Target lock + range: if a target entity (mech +0x388) exists and the
// own/target zone-damage limits pass (:5619-5634, the Lock rule), sets
// lockFlag (@0x1F8, "Lock"); rangeToTarget (@0x1EC, "RangeToTarget") is
// the distance between the HUD anchor (mech +0x37C) and the target
// (+0x100), sliding at 500 m/s (:5652-5670), default 1200.0 with no
// target; the compass Scalar @0x214 = yaw euler[0] + torso twist (:5676).
// 6. Torso-horizon slew: horizontalTorsoOffset (@0x294) is moved toward the
// commanded torso heading, clamped to +/- horizontalLimit (@0x29C), then
// written to the graphic at mech +0x36C. The flicker helper (@004b7ed4)
// damps the settle: it decays @0x294 toward ZERO at the object's own
// @0x298 (horizontalMovementPerSecond) x time_slice. (CORRECTED
// 2026-08-08: this used to read "at up to MaxTorsoSlew (500/sec)" -- the
// rate is that per-object field, and there is no 500 constant here. The
// 500 m/s belongs to the RANGE slide in step 5, as an immediate.)
//
void
HUD::HudSimulation(Scalar time_slice)
{
Check(this);
PowerWatcher::Simulation(time_slice); // FUN_004b181c (base per-frame tick)
//
// --- Heat / voltage gating of the HUD page ----------------------------
//
Logical pageVisible = (HeatWatcherFlag() != 1); // this[0x40] != 1
switch (WatchedVoltageLevel()) // @0x198
{
case 2: statusAlarm.SetLevel(0); break; // off but page shown
case 4: statusAlarm.SetLevel(1); break; // ready
default: statusAlarm.SetLevel(1); pageVisible = False; break;
}
switch (HeatStateLevel()) // @0x140
{
case 0: statusAlarm.SetLevel(1); break;
case 1: statusAlarm.SetLevel(0); break;
case 2: statusAlarm.SetLevel(1); pageVisible = False; break;
}
//
// --- Blink ------------------------------------------------------------
//
if (transitionFlag == 0)
{
blinkTimer += time_slice;
if (blinkTimer != flickerRate)
{
blinkTimer = 0.0f;
blinkState = (blinkState == 0);
}
}
else if (transitionFlag == 1)
{
blinkState = True;
}
pageVisible = (pageVisible && blinkState) ? True : False;
//
// --- (remaining torso-horizon / target / slew logic; see header) ------
// Faithful offsets are preserved in the recovered shard part_013.c
// @004b7830; reproduced here in summarised form -- a human should expand
// the reticle/threat-vector math against the original source.
//
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateStreamedSubsystem -- HUD @004b81d4
//
// Chains to PowerWatcher::CreateStreamedSubsystem (FUN_004b198c), stamps the
// resource (classID 0x0BD6 @ +0x20, model size 0x104 @ +0x24), resolves the
// "SegmentPageName" (when passes==1 it seeds FlickerRate := -1.0 and copies
// the segment index from +0x28 to +0xF8; on later passes it restores +0x28
// from +0xF8, erroring "missing SegmentPageName!" if unset), then reads the
// three HUD scalars, erroring if any is absent/zero:
// "FlickerRate" -> +0xF4
// "HorizontalMovementPerSecond" -> +0xFC
// "HorizontalLimit" -> +0x100
//
int
HUD::CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes
)
{
if (
!PowerWatcher::CreateStreamedSubsystem( // FUN_004b198c
model_file, model_name, subsystem_name,
subsystem_resource, subsystem_file, directories, passes
)
)
{
return False;
}
subsystem_resource->subsystemModelSize = 0x104; // +0x24
subsystem_resource->classID = (RegisteredClass::ClassID)0x0BD6; // HUDClassID, +0x20
if (passes == 1)
{
subsystem_resource->flickerRate = -1.0f; // +0xF4
subsystem_resource->segmentPageIndex = subsystem_resource->segmentIndex; // +0xF8 <- +0x28
}
if ((subsystem_resource->segmentPageIndex == -1) && (1 < passes))
{
ReportError(subsystem_name, "missing SegmentPageName!");
return False;
}
if (1 < passes)
{
subsystem_resource->segmentIndex = subsystem_resource->segmentPageIndex; // +0x28 <- +0xF8
}
if (!model_file->GetEntry(subsystem_name, "FlickerRate", &subsystem_resource->flickerRate)
&& subsystem_resource->flickerRate == 0.0f)
{
ReportError(subsystem_name, "missing FlickerRate!");
return False;
}
if (!model_file->GetEntry(subsystem_name, "HorizontalMovementPerSecond",
&subsystem_resource->horizontalMovementPerSecond)
&& subsystem_resource->horizontalMovementPerSecond == 0.0f)
{
ReportError(subsystem_name, "missing HorizontalMovementPerSecond!");
return False;
}
if (!model_file->GetEntry(subsystem_name, "HorizontalLimit",
&subsystem_resource->horizontalLimit)
&& subsystem_resource->horizontalLimit == 0.0f)
{
ReportError(subsystem_name, "missing HorizontalLimit!");
return False;
}
Check_Fpu();
return True;
}
//===========================================================================//
// WAVE 2 factory bridge -- HUD (factory case 0xBD6, "MechTech" label).
//===========================================================================//
Subsystem *CreateHUDSubsystem(Mech *owner, int id, void *seg)
{
Check(sizeof(HUD) <= 0x2a4);
return (Subsystem *) new (Memory::Allocate(0x2a4))
HUD(owner, id, (HUD::SubsystemResource *)seg, HUD::DefaultData);
}
//
// DeathReset (#55 step 4) -- the respawn sweep's entry for the HUD.
//
void
HUD::DeathReset(int reset_command)
{
ResetToInitialState(reset_command != 0); // @004b77bc
}
//
// BTSetHudFlickerActive -- complete-type bridge for the CONTROL-MODE switch
// (mechmppr.cpp treats the mech's subsystems as opaque pointers, so it cannot
// touch HUD members directly; same pattern as torso.cpp's BTGetTorsoTwistAddr).
//
// @004afbe0's BASIC arm ends in `*(mech+0x5b4 + 0x2a0) = 1` -- mech+0x5b4 is the
// HUD subsystem cache and +0x2A0 is flickerActive. Basic mode re-centres the
// torso, so the HUD horizon is kicked into its settle animation to follow it
// (UpdateFlicker @004b7ed4 decays horizontalTorsoOffset and reports whether it
// is still moving). The port never made this call.
//
void BTSetHudFlickerActive(Subsystem *hud)
{
if (hud != 0)
((HUD *)hud)->SetFlickerActive(1);
}