THE CLASS OF BUG. A default-constructed Receiver::MessageHandlerSet is a total blackhole: entryCount 0 and NO parent chain, so Find() returns NullHandler for every id and Receiver::Receive dropped the message with no log, no counter, nothing. docs/INPUT_PATH_AUDIT.md ranked this systemic cause #1; running its own lint finds SEVEN such sets (Sensor, Searchlight, ThermalSight, Gyroscope, MechTech, SubsystemMessageManager, Torso). It is also why generators cannot be switched off (#53) -- same shape. FIX 1 -- Sensor / Avionics (the big one). The streamed mappings aim 108 records across the 18 mechs at it: the MFD2 ENG1 power bank, 6 clickable buttons plus keys F5-F9 (route Avionics power to generators A-D, gen mode, coolant cut). All silently swallowed. Sensor needed NO handlers of its own -- ids 4-8 already live on PoweredSubsystem, which chains HeatSink for id 3, covering the whole 3-8 range those buttons send. It only ever needed to CHAIN, which is exactly why the byte-identical bank on ENG2 works (Myomers chains, myomers.cpp:88-100; Sensor did not). Added Sensor::GetMessageHandlers() with the same function-local-static idiom and pointed DefaultData at it instead of the empty set. VERIFIED LIVE (scratchpad/avionics.py -- pages MFD2 and presses the bank): [gensel] Avionics -> GeneratorA (tapped) ... B, C, D [gensel] Avionics mode -> 2 i.e. Avionics now behaves exactly like Myomers. 24 handler hits, 0 unhandled. FIX 2 -- the standing guard. Receiver::Receive now logs once per (class, message id) pair when no handler is found: '[msg] UNHANDLED: <class> has no handler for message id N -- silently dropped (is its MessageHandlerSet chained?)'. Once-per-pair on purpose, since some ids broadcast every frame. This one line would have printed Avionics, Searchlight, ThermalSight, crouch and all four generator buttons on the first boot instead of costing us months of player reports. Uses SharedData::derivedClasses -> Derivation::className. DEFERRED, with reasons (not guessed at): Searchlight's ToggleLamp body exists but is unreachable -- it needs a correctly-typed forwarder (Receiver::Handler is void(const Message*); ToggleLamp is Logical(Message&), so a cast would be UB that happens to work on x86) AND an id check, because its id 3 collides with HeatSink's ToggleCooling and I have not traced whether PowerWatcher's chain reaches HeatSink. #53 needs more than chaining: ToggleGeneratorOnOff @004b1ed0 has no body at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
236 lines
8.8 KiB
C++
236 lines
8.8 KiB
C++
//===========================================================================//
|
|
// File: sensor.hpp //
|
|
// Project: BattleTech //
|
|
// Contents: Sensor subsystem //
|
|
//---------------------------------------------------------------------------//
|
|
// Date Who Modification //
|
|
// -------- --- ---------------------------------------------------------- //
|
|
// 8/10/95 GDU Initial coding. //
|
|
//---------------------------------------------------------------------------//
|
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
|
|
// PROPRIETARY AND CONFIDENTIAL //
|
|
//===========================================================================//
|
|
//
|
|
// This declaration is the SURVIVING SENSOR.HPP
|
|
// (410SRC/.../CODE/BT/BT/SENSOR.HPP) reproduced verbatim, with reconstruction
|
|
// annotations added in comments only. The class body below is GROUND TRUTH;
|
|
// the @ADDR / @offset notes were recovered from the shipped binary and tie
|
|
// each declaration to the evidence used in sensor.cpp.
|
|
//
|
|
// Recovered binary facts (see sensor.cpp for per-method @ADDR evidence):
|
|
// - Sensor IS-A PoweredSubsystem (powersub.hpp), object size 0x328.
|
|
// - vtable @0050fb0c ctor @004b1d18 dtor @004b1d90
|
|
// - DefaultData @0050fa1c ClassDerivations @0050fa2c
|
|
// - AttributePointers @0050fa50 (three IndexEntry records, see below)
|
|
// - RegisteredClass::SensorClassID == 0x0BC3 (resource +0x20),
|
|
// streamed model size 0x190 (resource +0x24 -- same record as
|
|
// PoweredSubsystem; the Sensor resource adds no fields).
|
|
//
|
|
// IMPORTANT CONFLICT NOTE: the prior powersub.cpp/CLASSMAP reconstruction
|
|
// labelled the class at vtable @0050fb0c / ctor @004b1d18 "Myomers" with
|
|
// inferred members outputVoltage/powered/voltageAvailable. That was a
|
|
// misidentification. The shipped attribute string pool (@0050fae0:
|
|
// "Sensor", "RadarPercent", "SelfTest", "BadVoltage") and the surviving
|
|
// SENSOR.HPP prove the class is Sensor. classID 0xBC3 is Sensor, not
|
|
// Myomers. See CLASSMAP.md "Resolved conflict".
|
|
//
|
|
#if !defined(SENSOR_HPP)
|
|
# define SENSOR_HPP
|
|
|
|
#if !defined(POWERSUB_HPP)
|
|
#include "powersub.hpp"
|
|
#endif
|
|
|
|
|
|
//##########################################################################
|
|
//################# SensorSubsystemResource #######################
|
|
//##########################################################################
|
|
//
|
|
// Empty extension of the PoweredSubsystem resource. Confirmed by
|
|
// CreateStreamedSubsystem @004b1dcc, which only chains to
|
|
// PoweredSubsystem::CreateStreamedSubsystem (@004b13ac) and stamps the
|
|
// classID/model-size -- it parses NO sensor-specific fields, and the streamed
|
|
// model size is 0x190 (identical to the PoweredSubsystem record).
|
|
//
|
|
struct Sensor__SubsystemResource:
|
|
public PoweredSubsystem::SubsystemResource
|
|
{
|
|
|
|
};
|
|
|
|
//##########################################################################
|
|
//########################## Sensor ###############################
|
|
//##########################################################################
|
|
|
|
class Sensor:
|
|
public PoweredSubsystem
|
|
{
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Death and Damage Support
|
|
//
|
|
// NOTE: the Sensor vtable @0050fb0c carries the inherited (PoweredSubsystem
|
|
// / HeatSink) addresses in the damage/death slots -- no Sensor-specific
|
|
// bodies were captured in the decomp. The definitions in sensor.cpp are
|
|
// BEST-EFFORT (chain to base) and flagged accordingly.
|
|
//
|
|
public:
|
|
|
|
void
|
|
TakeDamage(Damage &damage);
|
|
|
|
void
|
|
DeathReset(Logical full_recovery);
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Shared Data Support
|
|
//
|
|
// DefaultData @0050fa1c, ClassDerivations @0050fa2c.
|
|
//
|
|
public:
|
|
static Derivation ClassDerivations;
|
|
static SharedData DefaultData;
|
|
static Receiver::MessageHandlerSet MessageHandlers;
|
|
// The set the DefaultData actually hands out. Chains
|
|
// PoweredSubsystem's ids 4-8 -- the bare `MessageHandlers` above is
|
|
// default-constructed and swallows every message (see sensor.cpp).
|
|
static Receiver::MessageHandlerSet& GetMessageHandlers();
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// BT segment / base-state compatibility shims.
|
|
//
|
|
// CROSS-FAMILY: these accessors and segment-flag constants logically belong
|
|
// on the shared bases (Mech::GetSegmentFlags, HeatSink::HeatStateLevel /
|
|
// HeatModelOff / heatEnergy, PoweredSubsystem::ElectricalStateLevel) plus a
|
|
// real multi-level AlarmIndicator. Until the heat/powersub/mech families
|
|
// expose them, they are backed locally here so this module compiles; the
|
|
// backing fields are written by the (cross-family) base simulation once the
|
|
// real accessors land. See report "CROSS-FAMILY NEEDS".
|
|
//
|
|
#if !defined(BT_SEGMENT_FLAG_CONSTS)
|
|
# define BT_SEGMENT_FLAG_CONSTS
|
|
enum {
|
|
SegmentCopyMask = 0x0C, // (flags & 0xC): 0 == primary, 4 == damaged copy
|
|
SegmentLiveFlag = 0x01,
|
|
MasterHeatSinkFlag = 0x0100
|
|
};
|
|
#endif
|
|
public:
|
|
// De-shimmed (WAVE 4 un-stub): the 5 CROSS-FAMILY backing fields
|
|
// (segmentFlags/hasActivePerformance/heatStateLevel/heatModelOff/
|
|
// electricalState) are DELETED -- they over-sized the object past its
|
|
// zero-headroom 0x328 factory alloc AND duplicated engine-base state.
|
|
// The accessors now read the REAL inherited base members:
|
|
// HeatStateLevel <- HeatSink::heatAlarm level (this+0x170)
|
|
// ElectricalState <- PoweredSubsystem::electricalStateAlarm level (this+0x278)
|
|
// HeatModelOff <- MechSubsystem::simulationState == Destroyed (this+0x40)
|
|
// HeatMasterEnergy <- inherited HeatSink::heatEnergy (this+0x158)
|
|
// SetHasPerformanceFlag -> engine Simulation::simulationFlags |= 8 (this+0x28)
|
|
// GetSegmentFlags() removed (unused; the ctor gate reads owner->simulationFlags).
|
|
HeatSink::HeatState
|
|
HeatStateLevel() const { return (HeatSink::HeatState)heatAlarm.GetLevel(); }
|
|
Logical
|
|
HeatModelOff() const { return simulationState == 1; /* MechSubsystem::DestroyedState */ }
|
|
Scalar
|
|
HeatMasterEnergy() const { return heatEnergy; } // inherited HeatSink sink
|
|
PoweredSubsystem::ElectricalState
|
|
ElectricalStateLevel() const { return (PoweredSubsystem::ElectricalState)electricalStateAlarm.GetLevel(); }
|
|
void
|
|
SetHasPerformanceFlag() { simulationFlags |= 8; } // engine Simulation flag bit 3
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Attribute Support
|
|
//
|
|
// Recovered IndexEntry table @0050fa50 (one {nameptr, attribID, offset+1}
|
|
// record each). The offset field is encoded as (byte_offset | 1):
|
|
// RadarPercent -> id 0x12, 0x31d => radarPercent @0x31C
|
|
// SelfTest -> id 0x13, 0x321 => selfTest @0x320
|
|
// BadVoltage -> id 0x14, 0x325 => badVoltage @0x324
|
|
// (PoweredSubsystem::NextAttributeID == 0x12, so the enum below resolves to
|
|
// RadarPercent=0x12, SelfTest=0x13, BadVoltage=0x14, NextAttributeID=0x15.)
|
|
//
|
|
public:
|
|
enum {
|
|
RadarPercentAttributeID = PoweredSubsystem::NextAttributeID,
|
|
SelfTestAttributeID,
|
|
BadVoltageAttributeID,
|
|
NextAttributeID
|
|
};
|
|
|
|
private:
|
|
static const IndexEntry AttributePointers[];
|
|
|
|
protected:
|
|
static AttributeIndexSet AttributeIndex;
|
|
|
|
|
|
|
|
public:
|
|
Scalar
|
|
radarPercent; // @0x31C ctor init 1.0f
|
|
Logical
|
|
selfTest, // @0x320 ctor init 0
|
|
badVoltage; // @0x324 ctor init 0
|
|
|
|
|
|
|
|
//##########################################################################
|
|
// Simulation Support
|
|
//
|
|
public:
|
|
|
|
typedef void
|
|
(Sensor::*Performance)(Scalar time_slice);
|
|
|
|
void
|
|
SetPerformance(Performance performance)
|
|
{
|
|
Check(this);
|
|
activePerformance = (Simulation::Performance)performance;
|
|
}
|
|
|
|
void
|
|
SensorSimulation(Scalar time_slice); // @004b1c4c (Performance, PTR @0050fa94)
|
|
|
|
//##########################################################################
|
|
// Test Class Support
|
|
//
|
|
public:
|
|
static Logical
|
|
TestClass(Mech&);
|
|
void
|
|
ResetToInitialState(); // slot 10, @004b1c18
|
|
|
|
//##########################################################################
|
|
// Construction and Destruction
|
|
//
|
|
public:
|
|
typedef Sensor__SubsystemResource SubsystemResource;
|
|
|
|
Sensor(
|
|
Mech *owner,
|
|
int subsystem_ID,
|
|
SubsystemResource *subsystem_resource
|
|
); // @004b1d18
|
|
~Sensor(); // @004b1d90
|
|
|
|
Logical
|
|
TestInstance() const; // @004b1e18
|
|
|
|
static int
|
|
CreateStreamedSubsystem( // @004b1dcc
|
|
NotationFile *model_file,
|
|
const char *model_name,
|
|
const char *subsystem_name,
|
|
SubsystemResource *subsystem_resource,
|
|
NotationFile *subsystem_file,
|
|
const ResourceDirectories *directories,
|
|
int passes = 1
|
|
);
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
#endif
|