Files
BT411/game/reconstructed/hud.cpp
arcattackandClaude Opus 4.8 7b7d465e5e Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.

Layout:
  engine/   MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
            work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
            models) + image codec; the minimal rp/ headers the audio HAL needs
  game/     reconstructed BT logic + surviving-original BT source + fwd shims
            + WinMain launcher
  content/  full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
  docs/     format specs + reconstruction ledgers
  reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
  tools/    MP console emulator + map/resource scanners

One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:03:40 -05:00

452 lines
16 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 observed as read-only float globals adjacent to the
// HudSimulation body (.rdata, recovered from section_dump.txt).
//
static const Point3D HudZeroVector(0.0f, 0.0f, 0.0f); // DAT_004e0f74/78/7c
static const Scalar SegmentTempLimit = 0.0f; // _DAT_004b7ec4 (heat threshold for HUD page visibility)
static const Scalar TargetTempLimit = 0.0f; // _DAT_004b7ec8
static const Scalar RangeBias = 0.0f; // _DAT_004b7ecc
static const Scalar MaxTorsoSlew = 500.0f; // _DAT_004b7ed0
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);
flickerRate = 0.0f; // @0x1D8 (overwritten below from resource)
torsoLimitRight = 0.0f; // @0x1DC
torsoLimitLeft = 0.0f; // @0x1E0
torsoSpeed = 0.0f; // @0x1E4
rangeToTarget = 0.0f; // @0x1E8
targetRange = 0.0f; // @0x1EC
lockFlag = True; // @0x1F4 = 1
visible = False; // @0x1F8
threatVector = HudZeroVector; // @0x1FC
compassHeading = HudZeroVector; // @0x208
hotBoxScalar = 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/range readouts to 0; HudSimulation refreshes them per-frame once the link
// is live. TODO: wire a real Mech torso-orientation-source accessor.
linkedEntity = 0; // @0x234
torsoLimitRight = 0.0f;
torsoLimitLeft = 0.0f;
rangeToTarget = 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)
}
//###########################################################################
// SetCompassHeading -- @004b7810
//
// Copies a Point3D into compassHeading (@0x208).
//
void
HUD::SetCompassHeading(const Point3D &heading)
{
compassHeading = heading; // FUN_00408440(this+0x208, &heading)
}
//###########################################################################
// 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. A handler table
// @00511180 binds the message "ToggleLamp" to FUN_004b838c. Chains to the
// PowerWatcher handler otherwise.
//
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 line-of-sight: if a target entity (mech +0x388) exists and is
// not over the heat limits, computes targetRange (@0x1EC) as the distance
// between the HUD anchor (mech +0x37C) and the target (+0x100); sets
// visible (@0x1F8); otherwise blanks. A default range 1200.0 is used
// when there is no target.
// 6. Torso-horizon slew: horizontalTorsoOffset (@0x294) is moved toward the
// commanded torso heading at up to MaxTorsoSlew (500/sec), clamped to
// +/- horizontalLimit (@0x29C), then written to the graphic at
// mech +0x36C. The flicker helper (@004b7ed4) damps the settle.
//
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);
}