Three root causes, all fixed: 1. kDamageScale was 1.0 -- _DAT_004bafbc is an x87 float80 = 1e-7, cancelling the ctor's x1e7: damagePortion = authored DamageAmount x (charge/seekV)^2 (closed form at fire time; madcat AC=25/LRM=50/ERLL=6, bhk1 PPC=12/SRM=35). The observed "0.25" was the degenerate EC=1 fallback, never authored data. 2. CheckFireEdge NaN latch: TriggerState carries ControlsButton INTS; the release value (-65) is a negative NaN. The binary's x87 unordered compare read it as "released"; IEEE-correct float compares latched the edge detector shut after the first release -- the reason the emitter discharge chain NEVER fired in-game. Fixed with bit-pattern sign compares. 3. The weapon-side submission LIVE: Emitter::FireWeapon fills damageData (amount + damageForce=target-muzzle [the gyro directional-bounce feed] + impact) -> MechWeapon::SendDamageMessage (@004b9728 real body) -> messmgr consolidation with per-weapon records + explosion bundling. The mech4 bring-up damage block + flat kShotDamage retired to diag hooks. Bonus: LODReuseHysteresis 0.82 -> 0.33 (double misread). Zone model verified byte-exact (no change). Solo end-to-end: 5-record volleys (2 PPC + 3 ERML with authored amounts), per-weapon zone granularity, explosions, kill. Heat stays bring-up scale pending the heat-calibration audit [T3]. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
381 lines
17 KiB
C++
381 lines
17 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;
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Attribute Support (gauge data-binding wave)
|
|
//
|
|
// The energy/ballistic weapon engineering clusters read OutputVoltage (the
|
|
// charge/ready level -- animates as the weapon recharges after firing) and
|
|
// PercentDone (the recharge dial), both onto rechargeLevel (1.0 == ready).
|
|
// GetAttributeIndex() chains to PoweredSubsystem's so InputVoltage + the
|
|
// HeatSink temps also resolve; Emitter/PPC/ProjectileWeapon/MissileLauncher
|
|
// point their DefaultData at MechWeapon::GetAttributeIndex() to inherit it
|
|
// (they carried an EMPTY default-constructed index -> resolved NOTHING).
|
|
//
|
|
public:
|
|
enum {
|
|
// TriggerState is the BINARY's only MechWeapon attribute
|
|
// ({0x13, "TriggerState", +0x31d} @0x511890 -- fireImpulse@0x31C).
|
|
// The streamed per-mech controls maps (type-6 resources) bind the
|
|
// fire buttons DIRECTLY onto it by this NUMERIC id, so it is
|
|
// PINNED to the binary value: the binary's subsystem chain reaches
|
|
// 0x12 before MechWeapon (ConfigureActivePress@MechSubsystem=2,
|
|
// heat family 3..0xC, powered/aux-screen family 0xD..0x11 -- full
|
|
// dump in the task #5 attr-table scan), while our reconstruction
|
|
// publishes fewer base attrs (chain value 0xD). The id gap wastes
|
|
// a few dense-index slots (Build sizes by max id) -- harmless.
|
|
// OutputVoltage/PercentDone are port gauge additions
|
|
// (name-resolved) renumbered after it.
|
|
TriggerStateAttributeID = 0x13,
|
|
OutputVoltageAttributeID, // 0x14
|
|
PercentDoneAttributeID, // 0x15
|
|
NextAttributeID
|
|
};
|
|
private:
|
|
static const IndexEntry AttributePointers[];
|
|
public:
|
|
static AttributeIndexSet& GetAttributeIndex();
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// 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 MAP CORRECTED (task #51): the Read/TakeDamage attributions were
|
|
// SWAPPED. By symmetry with the Entity hierarchy (Mech slot 6 =
|
|
// ReadUpdateRecord @004a1232, slot 7 = WriteUpdateRecord @004a0c2c, both
|
|
// verified) and by body semantics (@004b964c reads record+0x10/+0x14 into
|
|
// the weapon alarm after chaining @0041bd34 = Simulation::ReadUpdateRecord;
|
|
// @004b0efc dispatches on *param==4 = Damage::damageType):
|
|
// slot 6 ReadUpdateRecord(...) @004b964c (base @0041bd34 = Simulation::ReadUpdateRecord)
|
|
// slot 7 WriteUpdateRecord(...) @004b9690 (base @0041c500 = Subsystem::WriteUpdateRecord)
|
|
// slot 9 TakeDamage(Damage&) @004b96d4 (chains @004b0efc = PoweredSubsystem::TakeDamage)
|
|
// slot 10 ResetToInitialState() (PoweredSubsystem base @004b0e6c)
|
|
// slot 13 PrintState() (PoweredSubsystem base @004b1224)
|
|
//
|
|
// NOTE the ENGINE-OVERRIDE signature rule: the virtuals are declared on
|
|
// Simulation with Simulation::UpdateRecord* (SIMULATE.h:127-131). Declaring
|
|
// them here with THIS class's shadowing `UpdateRecord` typedef silently
|
|
// fails to override (the engine base runs instead and none of the weapon
|
|
// fields ever serialize) -- base-typed params, cast inside.
|
|
//
|
|
public:
|
|
void
|
|
TakeDamage(Damage &damage); // slot 9, @004b96d4
|
|
void
|
|
ResetToInitialState(); // slot 10, @004b96ec
|
|
void
|
|
PrintState(); // slot 13, @004b9d00
|
|
|
|
void
|
|
WriteUpdateRecord( // slot 7, @004b9690
|
|
Simulation__UpdateRecord *message,
|
|
int update_model
|
|
);
|
|
void
|
|
ReadUpdateRecord(Simulation__UpdateRecord *message); // slot 6, @004b964c
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// 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);
|
|
|
|
public:
|
|
//
|
|
// Beam-render accessors (the visible-beam layer draws each weapon's beam
|
|
// from its own live state: real muzzle + the authored per-weapon colour).
|
|
//
|
|
void MuzzlePoint(Point3D &p) { GetMuzzlePoint(p); }
|
|
const RGBColor &PipColor() const { return pipColor; }
|
|
|
|
//
|
|
// Reticle/HUD accessors (the pip registration wiring @part_014.c:5390:
|
|
// per weapon the 1996 build reads TargetWithinRange / WeaponRange /
|
|
// PipPosition / PipColor / PipExtendedRange / SimulationState).
|
|
//
|
|
Scalar WeaponRange() const { return weaponRange; }
|
|
int PipPosition() const { return pipPosition; }
|
|
int PipExtendedRange() const { return pipExtendedRange; }
|
|
void *WithinRangePtr() { return &targetWithinRange; }
|
|
void *WeaponAlarmPtr() { return &weaponAlarm; }
|
|
void *SimulationStatePtr() { return &simulationState; } // attr 1 ("SimulationState" -- damage state; 1 = Destroyed)
|
|
Scalar *RechargeLevelPtr() { return &rechargeLevel; } // the pip ready source (1.0 == loaded/ready)
|
|
|
|
protected:
|
|
|
|
// @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
|
|
SendDamageMessage(Entity *target); // @004b9728 (ex "DrawWeaponPip" -- the
|
|
// weapon-fire damage submission into the
|
|
// owner's SubsystemMessageManager, task #8)
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// 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
|
|
public:
|
|
// task #7: the SubsystemMessageManager bundles the FIRING weapon's
|
|
// explosion per consolidated damage record (ENTITY3.h:143).
|
|
ResourceDescription::ResourceID
|
|
GetExplosionResourceID() const { return explosionResourceID; }
|
|
protected:
|
|
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
|