Files
BT412/game/reconstructed/mechweap.hpp
T
arcattackandClaude Opus 4.8 9d82be46a1 P7: subsystem-tree alarm unification -- the whole PoweredSubsystem weapon/power subtree is byte-exact
The reconstruction modeled the binary's 0x54 AlarmIndicator (FUN_0041b9ec) with undersized
stand-ins (AlarmIndicator==ReconAlarm==4B; HeatAlarm==8B) across the entire subsystem tree, so
every field above an alarm sat at the wrong compiled offset.  Retype every such stand-in to
GaugeAlarm54(0x54) and de-phantom each class against its ctor, so the whole PoweredSubsystem
subtree becomes byte-exact.  An 8-agent read-only decomp-mapping workflow decoded every ctor
first; then hands-on implementation.  static_assert-locked chain (verified vs the raw ctors):

  HeatSink 0x1D0
   -> PoweredSubsystem 0x31C   retype electricalStateAlarm@0x264 + modeAlarm@0x2B8   @004b0f74
   -> MechWeapon      0x3F0   retype weaponAlarm@0x350; delete 5 phantom tail fields  @004b99a8
   -> Emitter         0x478   delete outputVoltage/beamLengthRatio/firingArmed aliases
                              + beamHit*/beamColor/beamHitData/energyRampTime phantoms;
                              retype beamOrientation EulerAngles->Quaternion(16B)       @004bb120
   -> PPC             0x478   (no own fields)                                           @004bb888
  PoweredSubsystem -> Sensor  0x328   (no alarm; 3 own fields)                          @004b1d18
  PoweredSubsystem -> Myomers 0x358   (no alarm)                                        @004b8fec

New systemic bug-class instances fixed (added to the CLAUDE.md checklist):
  * alias field    - Emitter outputVoltage==inherited rechargeLevel@0x320; beamLengthRatio==
                     beamScale.z@0x434; firingArmed==inherited useConfiguredPip@0x3E0
  * phantom field  - MechWeapon segmentReference/pipSegment/hasTarget/targetPoint/muzzlePoint
                     (past 0x3F0); Emitter beamHitPoint/beamImpact/beamImpactScalar/beamColor/
                     beamHitData/energyRampTime (binary writes inherited damageData/voltageScale
                     or the value is a method local)

Non-layout fixes required in the same wave:
  * outputVoltage->rechargeLevel also in the compiled GAUSS.CPP:74/93 (external readers of the
    removed Emitter field -- must grep EVERY TU, not just the class's own .cpp)
  * MechWeapon::GetMuzzlePoint reimplemented faithfully (removed muzzlePoint collided with
    Emitter's own fields at 0x3F0) via a BTResolveWeaponMuzzle void* bridge in mech4.cpp,
    resolving the weapon's mount segment (inherited this+0xdc) through the owner segment table
  * DetachFromVoltageSource fixed to set electricalStateAlarm not modeAlarm (raw @004b0e30
    writes the 0x264 alarm)

The vehicleSubSystems aux-screen gauge raw reads (btl4gau2.cpp:868/952 at subsystem+0x2b8/+0x278)
and the Sensor RadarPercent path now read the CORRECT byte offsets (garbage under the short layout).

Verified: combat DESTROYED-in-8, 28 shots, 0 crashes, heat heatEnergy=1.34e7, every static_assert
lock passes, heapcheck-clean through construction (the phase the isolated PoweredSubsystem retype
had overflowed).  LESSON: a factory-bridge runtime Check(sizeof<=alloc) does NOT fail the build --
only a static_assert sizeof lock catches alloc overflow at compile time.

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

306 lines
13 KiB
C++

//===========================================================================//
// File: mechweap.hpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: MechWeapon -- abstract base for every mounted Mech weapon //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// --/--/95 JM Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (Ghidra pseudo-C, module cluster
// @004b95ec-@004b9d10, file=bt/mechweap.cpp; pure-virtual stub @004ba45c,
// file=bt/mechweap.hpp). Cross-referenced HEAVILY against the SURVIVING
// derived-class sources PPC.HPP/PPC.CPP and GAUSS.HPP/GAUSS.CPP (which derive
// from Emitter : MechWeapon) and MISLANCH.HPP (MissileLauncher : ProjectileWeapon
// : MechWeapon), plus the MUNGA SUBSYSTM.HPP base interface and the RP
// WEAPSYS.h analog. See mechweap.cpp for per-method @ADDR evidence.
//
// Inheritance chain established from the decomp:
// Simulation -> Subsystem -> PoweredSubsystem (powersub.cpp, ctor @004b0f74)
// -> MechWeapon (this file, vtable @00511d2c)
// -> Emitter / ProjectileWeapon -> PPC / GaussRifle / MissileLauncher
//
// The MechWeapon ctor (@004b99a8) installs vtable PTR_FUN_00511d2c and chains
// to the PoweredSubsystem ctor @004b0f74, confirming the base. PPC and Gauss
// FireWeapon() chain to Emitter::FireWeapon(); MechWeapon::FireWeapon() is the
// pure-virtual default that asserts "MechWeapon abstract class should..." (@004b95ec)
// -- the exact analog of RP GenericGun::Fire()'s "Should not be here!" stub.
//
// Field offsets in comments are byte offsets into the shipped object
// (e.g. "@0x34c" == this word index 0xd3). Names taken from PPC.CPP
// (targetWithinRange, damageData) and the resource-parser tag strings
// (WeaponRange, DamageAmount, HeatCostToFire, RechargeRate, DamageType,
// PipPosition/PipColor/PipExtendedRange, ExplosionModelFile); the remainder
// are inferred from usage and flagged best-effort where uncertain.
//
#if !defined(MECHWEAP_HPP)
# define MECHWEAP_HPP
#if !defined(POWERSUB_HPP)
# include <powersub.hpp>
#endif
#if !defined(DAMAGE_HPP)
# include <damage.hpp>
#endif
#if !defined(ALARM_HPP)
# include <alarm.hpp>
#endif
#include <color.hpp> // RGBColor
#include <linmtrx.hpp> // LinearMatrix
#include <vector3d.hpp> // Vector3D
#include <point3d.hpp> // Point3D
//##################### Forward Class Declarations #######################
class Mech;
class Entity;
class ResourceFile;
class NotationFile;
class CockpitHud;
//###########################################################################
//#################### MechWeapon Model Resource ########################
//###########################################################################
//
// Extends the PoweredSubsystem resource record. The MechWeapon-specific
// fields begin at +0x190 in the parsed record (see CreateStreamedSubsystem
// @004b9d10). Total record size is 0x1BC (444 bytes) -- the parser stamps
// subsystemModelSize = 0x1BC and classID = MechWeaponClassID (0xBCD).
//
struct MechWeapon__SubsystemResource:
public PoweredSubsystem::SubsystemResource
{
Scalar rechargeRate; // +0x190 "RechargeRate"
Scalar weaponRange; // +0x194 "WeaponRange"
ResourceDescription::ResourceID
explosionResourceID; // +0x198 "ExplosionModelFile"
Scalar damageAmount; // +0x19C "DamageAmount" (-> damageData)
Enumeration damageType; // +0x1A0 "DamageType" (Collision..Energy)
Scalar heatCostToFire; // +0x1A4 "HeatCostToFire"
int pipPosition; // +0x1A8 "PipPosition"
RGBColor pipColor; // +0x1AC "PipColor" (3 floats, init -1)
int pipExtendedRange; // +0x1B8 "PipExtendedRange"
}; // sizeof == 0x1BC
//###########################################################################
//######################### MechWeapon ############################
//###########################################################################
//
// Abstract base for any subsystem that can be aimed at a target and fired,
// dealing damage and (optionally) spawning an explosion model and a HUD pip.
// (vtable @00511d2c, ctor @004b99a8, dtor @004b9b9c.)
//
class MechWeapon:
public PoweredSubsystem
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support
//
public:
static Derivation ClassDerivations;
static Receiver::MessageHandlerSet MessageHandlers;
static AttributeIndexSet AttributeIndex;
static SharedData DefaultData;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Update record (replication) -- extends the base Simulation record with
// the weapon-specific alarm/idle state appended by WriteUpdateRecord.
//
public:
struct MechWeapon__UpdateRecord:
public PoweredSubsystem::UpdateRecord
{
Enumeration alarmState; // weaponAlarm state
int weaponIdle; // 1 == idle (alarm level 0)
};
typedef MechWeapon__UpdateRecord UpdateRecord;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Firing -- the central abstract operation
//
// Pure-virtual in spirit: the base body @004b95ec just asserts
// "MechWeapon abstract class should [not be fired]". Emitter (PPC, Gauss)
// and ProjectileWeapon (MissileLauncher) provide the real implementations;
// PPC::FireWeapon / GaussRifle::FireWeapon chain up to Emitter::FireWeapon.
//
protected:
virtual void
FireWeapon(); // @004b95ec (abstract stub)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Subsystem virtual overrides (slots on vtable @00511d2c)
//
// Slot indices verified by diffing against the Subsystem vtable tail of
// @0050e170 and the PoweredSubsystem vtable @0050f9d8:
// slot 6 TakeDamage(Damage&) (Subsystem base @0041bd34)
// slot 7 WriteUpdateRecord(...) (Subsystem base @0041c500)
// slot 9 ReadUpdateRecord(...) (PoweredSubsystem base @004b0efc)
// slot 10 ResetToInitialState() (PoweredSubsystem base @004b0e6c)
// slot 13 PrintState() (PoweredSubsystem base @004b1224)
//
public:
void
TakeDamage(Damage &damage); // slot 6, @004b964c
void
ResetToInitialState(); // slot 10, @004b96ec
void
PrintState(); // slot 13, @004b9d00
protected:
void
WriteUpdateRecord( // slot 7, @004b9690
UpdateRecord *message,
int update_model
);
void
ReadUpdateRecord(UpdateRecord *message); // slot 9, @004b96d4
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Test Class Support
//
public:
static Logical
TestClass(Mech&);
Logical
TestInstance() const;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Targeting / aiming helpers (used by FireWeapon and the per-frame update)
//
protected:
// E3: read the owning Mech's CURRENT target slot (mech+0x388, the Entity* that
// mech4.cpp's targeting step writes) directly -- the old `hasTarget` member was
// never set (the Mech per-frame refresh isn't reconstructed) so the fire gate was
// permanently false. GetOwningSimulation is non-const, hence the cast.
Logical
HasActiveTarget() const
{
char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech)
return (o != 0 && *(void **)(o + 0x388) != 0) ? True : False;
}
// @004b9bdc -- recompute rangeToTarget and the heat-degraded
// effectiveRange, then set targetWithinRange. No target => range -1.
// Returns the resulting targetWithinRange flag (consumed by Emitter /
// ProjectileWeapon simulation).
Logical
UpdateTargeting();
// @004b9608 -- rising-edge detector on fireImpulse (best-effort name);
// returns True the frame the weapon's discharge signal goes positive.
Logical
CheckFireEdge(); // TODO: confirm semantic (recharge/discharge edge)
// @004b9cbc -- copy the owning Mech's current target position.
void
GetTargetPosition(Point3D &position);
// @004b9cdc -- direction from a supplied point to the weapon mount.
void
GetVectorToWeapon(const Point3D &from, Vector3D &direction);
// @004b9948 -- resolve the weapon's muzzle/site world position from the
// owning Mech's segment table.
void
GetMuzzlePoint(Point3D &point);
// @004b9864 -- build an aim orientation (atan2 of a direction vector).
void
ComputeAimOrientation(LinearMatrix &orientation, const Vector3D &direction);
// @004b9728 -- register the targeting "pip" / fire marker with the
// owning Mech's cockpit HUD manager.
void
DrawWeaponPip(const LinearMatrix &transform);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
public:
typedef MechWeapon__SubsystemResource SubsystemResource;
MechWeapon(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
);
~MechWeapon();
static int
CreateStreamedSubsystem( // @004b9d10
ResourceFile *resource_file,
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes = 1
);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Local data for the weapon base class.
// Offsets are byte offsets into the shipped object (PoweredSubsystem
// occupies everything below +0x31c).
//
protected:
// --- recharge / firing state ---
Scalar fireImpulse; // @0x31C reset 0 (edge-detected; best-effort name)
Scalar rechargeLevel; // @0x320 reset 1.0 (1.0 == fully charged/ready)
Scalar rangeToTarget; // @0x324 distance to current target (-1 if none)
Scalar effectiveRange; // @0x328 (1 - heatLoad) * weaponRange (heat-degraded)
Scalar weaponRange; // @0x32C resource WeaponRange (configured max)
int pipState; // @0x330 reset 0 (TODO: name -- pip/active state)
Logical usesExternalModel; // @0x334 resource model-name compare flag (best-effort)
int pipPosition; // @0x338 resource PipPosition
RGBColor pipColor; // @0x33C resource PipColor (3 floats, init -1,-1,-1)
int pipExtendedRange; // @0x348 resource PipExtendedRange
Logical targetWithinRange; // @0x34C PPC.CPP: "if (targetWithinRange)"
// --- damage / display alarm ---
GaugeAlarm54 weaponAlarm; // @0x350 0x54 AlarmIndicator (ctor FUN_0041b9ec); level @0x364 -> ends 0x3A4
Scalar previousFireImpulse; // @0x3A4 reset 0 (fire-edge history, see @004b9608)
Damage damageData; // @0x3A8 damageType @0x3A8, damageAmount @0x3AC
// PPC.CPP: Damage energy_damage(damageData)
Scalar heatCostToFire; // @0x3D8 resource HeatCostToFire
Scalar rechargeRate; // @0x3DC resource RechargeRate
Logical useConfiguredPip; // @0x3E0 = (usesExternalModel == 0) (best-effort)
ResourceDescription::ResourceID
explosionResourceID; // @0x3E4 resource ExplosionModelFile
Scalar recoil; // @0x3E8 reset 0 (Emitter::firingArmed aliases this slot)
int segmentPageIndex; // @0x3EC resource segmentIndex (+0x28) -- LAST field, binary ends 0x3F0
// The binary MechWeapon ends at 0x3F0 (segmentPageIndex is the last ctor write,
// @004b99a8 param_1[0xfb]). The former tail fields segmentReference/pipSegment/
// hasTarget/targetPoint/muzzlePoint were PHANTOM (no binary storage) and, worse,
// collided with the Emitter subclass's own fields at 0x3F0+; removed:
// * segmentReference -> the muzzle segment is the inherited base slot this+0xdc
// (GetMuzzlePoint resolves it live via the mech4 BTResolveWeaponMuzzle bridge)
// * pipSegment -> DrawWeaponPip gates on useConfiguredPip
// * hasTarget -> dead (HasActiveTarget reads owner+0x388)
// * targetPoint -> GetTargetPosition reads owner+0x37c
// * muzzlePoint -> resolved live by GetMuzzlePoint
friend struct MechWeaponLayoutCheck;
};
struct MechWeaponLayoutCheck {
static_assert(offsetof(MechWeapon, weaponAlarm) == 0x350, "MechWeapon::weaponAlarm @0x350 (0x54 alarm)");
static_assert(offsetof(MechWeapon, previousFireImpulse) == 0x3A4, "MechWeapon::previousFireImpulse @0x3A4 (alarm ends here)");
static_assert(offsetof(MechWeapon, recoil) == 0x3E8, "MechWeapon::recoil @0x3E8");
static_assert(offsetof(MechWeapon, segmentPageIndex) == 0x3EC, "MechWeapon::segmentPageIndex @0x3EC (last field)");
static_assert(sizeof(MechWeapon) == 0x3F0, "sizeof(MechWeapon) 0x3F0");
};
#endif