//===========================================================================// // 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 #pragma hdrstop #if !defined(HUD_HPP) # include #endif #if !defined(APP_HPP) # include #endif #if !defined(TESTBT_HPP) # include #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); }