AmmoBin::AttributeIndex was a bare, default-constructed static with NO parent chain, so nothing resolved on an AmmoBin -- even the inherited SimulationState came back NULL (the audio AmmoState + SimulationState watchers were skipped). - Chain AmmoBin::AttributeIndex to HeatWatcher::GetAttributeIndex() and give it a real AttributePointers[] with AmmoState -> ammoAlarm (@0x194, the 6-level 0x54 StateIndicator-compatible feed alarm, already SetLevel'd Feeding/Loaded/Empty/ Dumped by the sim) so reload/empty/feed audio fires on the ammo transition. With this every StateIndicator audio attribute across the mech + all subsystems binds to a real indicator: audiostate skips 85 -> 0. Remaining audio gaps are the inert Logical/Scalar/Enum subsystem attrs (ReportLeak/GeneratorOn/ConfigureActive Press/MotionState/SpeedOfTorsoHorizontal/TargetRangeExponent), which read 0 (silent, non-crashing) pending backing members in their size-locked layouts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
513 lines
21 KiB
C++
513 lines
21 KiB
C++
//============================================================================//
|
|
// File: ammobin.cpp //
|
|
// Project: BattleTech //
|
|
// Contents: Ammunition bin (AmmoBin : HeatWatcher) -- feed + cook-off //
|
|
//----------------------------------------------------------------------------//
|
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
|
|
// PROPRIETARY AND CONFIDENTIAL //
|
|
//============================================================================//
|
|
//
|
|
// RECONSTRUCTED from the shipped binary. Only AMMOBIN.TCP survives
|
|
// (AmmoBin::TestClass -> True, AmmoBin::ResetToInitialState -> empty). Behaviour
|
|
// follows the Ghidra pseudo-C in part_013.c; each non-trivial method cites the
|
|
// originating @ADDR. Names past the HeatWatcher base are best-effort.
|
|
//
|
|
// Coverage:
|
|
// confident : ctor @004bd5c4 (HeatWatcher chain, resource copy, alarm/cook-off
|
|
// init), dtor thunk @004bd6b0, CreateStreamedSubsystem @004bd6f0,
|
|
// FeedAmmo @004bd4f4, DumpAmmo @004bd588, CookOff @004bd300,
|
|
// HandleMessage @004bdb94, ReadUpdate @004bd2c0,
|
|
// AmmoBinSimulation @004bd394, TestInstance @004bd1e0
|
|
// best-effort: the cook-off heat-injection descriptor layout (@0x1F0..0x21C),
|
|
// the precise meaning of base status word @0x40 (subsystem damage
|
|
// state) and the alarm level names
|
|
// excluded : the 0x41xxxx engine vtable slots and the HeatWatcher /
|
|
// HeatableSubsystem base bodies (FUN_004aeb40 / FUN_004ac644 chain)
|
|
//
|
|
// Decoded constant:
|
|
// _DAT_004bd4e8 = 0.0f (feed-tick guard: advance only when time_slice > 0)
|
|
//
|
|
// Resource classID stamps (CreateStreamedSubsystem @004bd6f0):
|
|
// "ProjectileClassID" -> 0x0BD1 "MissileClassID" -> 0x0BBA
|
|
// AmmoBin record: classID @+0x20 = 0x0BCB, size @+0x24 = 0x104
|
|
//
|
|
// Helper-function name mapping (engine internals referenced by the decomp):
|
|
// FUN_004aeb40 HeatWatcher base constructor (owner,id,resource,name,shared)
|
|
// FUN_004aebe8 HeatWatcher destructor FUN_004aec54 HeatWatcher CSS
|
|
// FUN_004aea84 HeatWatcher::ReadUpdate (slot 9 base)
|
|
// FUN_004aeac4 HeatWatcher::WatchSimulation (drive watch alarm from temp)
|
|
// FUN_004ac1d4 HeatableSubsystem::HandleMessage (slot 8 base)
|
|
// FUN_004ac274 HeatableSubsystem::InjectHeat(descriptor)
|
|
// FUN_0041b9ec AlarmIndicator::Init(levels) FUN_0041baa4 AlarmIndicator::Destroy
|
|
// FUN_0041bbd8 AlarmIndicator::SetLevel FUN_0041db7c init heat descriptor
|
|
// FUN_00414b60 GameClock::Now() FUN_004dcd94 random delay
|
|
// FUN_0041a1a4 IsDerivedFrom FUN_004022d0 operator delete
|
|
//
|
|
|
|
#include <bt.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(AMMOBIN_HPP)
|
|
# include <ammobin.hpp>
|
|
#endif
|
|
#if !defined(TESTBT_HPP)
|
|
# include <testbt.hpp>
|
|
#endif
|
|
|
|
//
|
|
// Feed-tick guard, read as a read-only global in the decomp.
|
|
//
|
|
static const Scalar AmmoFeedTickFloor = 0.0f; // _DAT_004bd4e8
|
|
|
|
//
|
|
// Reconstruction support: small proxy types + unrecovered-helper stand-ins for
|
|
// the cook-off heat descriptor, the streamed-resource parser and the game clock.
|
|
//
|
|
struct HeatRecord { Scalar amount; };
|
|
struct AmmoModelRecord{ int index; };
|
|
static const Scalar _DAT_004bdb74 = -1.0f; // FeedRate "unset" sentinel
|
|
|
|
static void InitCookOffHeatSource(void *) {} // FUN_0041db7c
|
|
static void InjectHeat(void *) {} // FUN_004ac274
|
|
static void SetStatusLevel(int) {} // FUN_0041bbd8 (status alarm)
|
|
static int ReadInt(NotationFile *f, const char *p, const char *k, int *v) { return f ? f->GetEntry(p, k, v) : 0; }
|
|
static int ReadScalar(NotationFile *f, const char *p, const char *k, Scalar *v) { return f ? f->GetEntry(p, k, v) : 0; }
|
|
static int ReadString(NotationFile *f, const char *p, const char *k, const char **v){ return f ? f->GetEntry(p, k, v) : 0; }
|
|
static void ReportMissing(const char *, const char *) {} // FUN_004dbb24
|
|
static AmmoModelRecord *LookupModel(NotationFile *, const char *, int, int) { return 0; } // FUN_00406ff8
|
|
namespace GameClock { static int Now() { return 0; } } // FUN_00414b60
|
|
static int RandomDelay() { return 0; } // FUN_004dcd94
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// AmmoBin
|
|
//###########################################################################
|
|
//###########################################################################
|
|
|
|
//#############################################################################
|
|
// Shared Data Support
|
|
//
|
|
Derivation
|
|
AmmoBin::ClassDerivations(
|
|
HeatWatcher::GetClassDerivations(),
|
|
"AmmoBin"
|
|
);
|
|
|
|
Receiver::MessageHandlerSet AmmoBin::MessageHandlers;
|
|
|
|
// AmmoState -> ammoAlarm (@0x194). Chained to HeatWatcher so the inherited
|
|
// SimulationState (and any parent attrs) resolve -- the old bare AttributeIndex
|
|
// had no parent, so nothing resolved on an AmmoBin.
|
|
const AmmoBin::IndexEntry
|
|
AmmoBin::AttributePointers[]=
|
|
{
|
|
ATTRIBUTE_ENTRY(AmmoBin, AmmoState, ammoAlarm) // @0x194 (0x54 StateIndicator-compatible feed alarm)
|
|
};
|
|
|
|
Simulation::AttributeIndexSet AmmoBin::AttributeIndex(
|
|
ELEMENTS(AmmoBin::AttributePointers),
|
|
AmmoBin::AttributePointers,
|
|
HeatWatcher::GetAttributeIndex()
|
|
);
|
|
|
|
AmmoBin::SharedData
|
|
AmmoBin::DefaultData( // resolved as &DAT_005125bc
|
|
&AmmoBin::ClassDerivations,
|
|
AmmoBin::MessageHandlers,
|
|
AmmoBin::AttributeIndex,
|
|
AmmoBin::StateCount
|
|
);
|
|
|
|
//#############################################################################
|
|
// Construction
|
|
//
|
|
// @004bd5c4 AmmoBin::AmmoBin(Mech *owner, int id, SubsystemResource *resource)
|
|
//
|
|
// Chains the HeatWatcher base ctor (which sets up the temperature watch and
|
|
// its degradation/failure thresholds), stamps the AmmoBin vtable, builds the
|
|
// 6-level feed alarm and the cook-off heat descriptor, and copies the ammo
|
|
// resource fields. The feed timer starts at 0 and the alarm at Loaded(1).
|
|
// The active cook-off/feed Performance (PTR_FUN_00512654 -> AmmoBinSimulation)
|
|
// is installed only for an authoritative (non-ghost) mech.
|
|
//
|
|
AmmoBin::AmmoBin(
|
|
Mech *owner,
|
|
int subsystem_ID,
|
|
SubsystemResource *resource)
|
|
:
|
|
// HeatWatcher base ctor (owner, id, resource, shared default data).
|
|
HeatWatcher(owner, subsystem_ID, resource, DefaultData) // @004aeb40
|
|
{
|
|
// vtable installed by the compiler (PTR_FUN_0051286c)
|
|
|
|
// (WAVE 4) statusState=0 removed -- the base MechSubsystem ctor owns
|
|
// simulationState@0x40; the reads below use the inherited member.
|
|
ammoAlarm.Initialize(6); // @0x194 FUN_0041b9ec(this+0x65, 6)
|
|
InitCookOffHeatSource(&cookOffHeatSource); // @0x1F0 FUN_0041db7c(this+0x7c)
|
|
reserved = 0; // @0x228
|
|
|
|
ammoCount = resource->ammoCount; // @0x180 resource +0xFC
|
|
feedRate = resource->feedRate; // @0x224 resource +0x100
|
|
ammoClassID = resource->ammoClassID; // @0x188 resource +0xF0
|
|
ammoModelFile = resource->ammoModelFile; // @0x1E8 resource +0xF4
|
|
explosionModelFile = resource->explosionModelFile; // @0x1EC resource +0xF8
|
|
feedTimer = 0; // @0x184
|
|
|
|
// install the feed/cook-off Performance for the authoritative simulation only
|
|
if ((owner->simulationFlags & 0xC) == 0 && (owner->simulationFlags & 0x100) != 0) // owner+0x28
|
|
SetPerformance(&AmmoBin::AmmoBinSimulation); // PTR_FUN_00512654
|
|
|
|
ammoAlarm.SetLevel(Loaded); // @0x1A8 FUN_0041bbd8(this+0x65, 1)
|
|
initialAmmoCount = ammoCount; // @0x220
|
|
cookOffArmed = 0; // @0x18C
|
|
}
|
|
|
|
//#############################################################################
|
|
// Destruction
|
|
//
|
|
// @004bd6b0 ~AmmoBin() -- restores the vtable, tears down the feed alarm, then
|
|
// chains the HeatWatcher base destructor.
|
|
//
|
|
AmmoBin::~AmmoBin()
|
|
{
|
|
// vtable reset, alarm teardown and base ~HeatWatcher chaining handled by the
|
|
// compiler.
|
|
}
|
|
|
|
//#############################################################################
|
|
// FeedAmmo -- request one round (called by the linked ProjectileWeapon)
|
|
//
|
|
// @004bd4f4
|
|
//
|
|
// Returns 1 when a round was dispensed, 0 when the bin cannot deliver
|
|
// (destroyed / mid-feed / empty). Only the Loaded state dispenses: it switches
|
|
// to Feeding, reloads the feed timer to feedRate, decrements ammoCount (clamped
|
|
// at 0) and raises the Empty alarm if that was the last round.
|
|
//
|
|
// Panel bridges (Streak-gauge fix, 2026-07-12): the BallisticWeaponCluster
|
|
// read the bin's rounds via raw `bin+0x180` and the reload state via raw
|
|
// `bin+0x18c` -- the BINARY's offsets, garbage on our compiled layout (the
|
|
// HeatWatcher base sizes differ), so the ammo counter showed a constant and
|
|
// the reload wipe never armed. Complete-type reads of the NAMED members.
|
|
int *BTAmmoBinCountPtr(void *bin)
|
|
{
|
|
return (bin != 0) ? &((AmmoBin *)bin)->ammoCount : 0;
|
|
}
|
|
int BTAmmoBinFeeding(void *bin)
|
|
{
|
|
// "reloading" for the panel = the bin mid-feed (alarm state Feeding).
|
|
return (bin != 0 && ((AmmoBin *)bin)->ammoAlarm.Level() == AmmoBin::Feeding) ? 1 : 0;
|
|
}
|
|
|
|
// The round's GameModel resource ID (ammoModelFile @0x1E8, word 0x7A). In the
|
|
// arcade the MissileLauncher seeds each spawned Missile's model from AmmoBin+0x1e8
|
|
// (part_013.c:8778) and the Missile ctor reads SplashRadius from that model's
|
|
// GameModel record +0x50 (part_013.c:10184). The port re-expresses missiles as
|
|
// local flying rounds, so BTResolveSplashRadius (mech4.cpp) walks THIS path to
|
|
// recover the authored blast radius. Complete-type read of the named member.
|
|
int BTAmmoRoundModelResource(void *bin)
|
|
{
|
|
return (bin != 0) ? ((AmmoBin *)bin)->ammoModelFile : 0;
|
|
}
|
|
|
|
int AmmoBin::FeedAmmo()
|
|
{
|
|
if (simulationState == 1) // this+0x40 == 1 (destroyed)
|
|
return 0;
|
|
|
|
switch (ammoAlarm.Level()) // this+0x1A8
|
|
{
|
|
case Loaded: // 1
|
|
ammoAlarm.SetLevel(Feeding); // 0
|
|
feedTimer = feedRate; // this+0x184 <- this+0x224
|
|
--ammoCount; // this+0x180
|
|
if (ammoCount < 1)
|
|
ammoAlarm.SetLevel(Empty); // 2
|
|
if (ammoCount < 0)
|
|
ammoCount = 0; // clamp
|
|
return 1; // dispensed
|
|
|
|
case Feeding: // 0 -- still chambering
|
|
case Empty: // 2
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
//#############################################################################
|
|
// DumpAmmo -- forcibly empty the bin (eject / strip)
|
|
//
|
|
// @004bd588
|
|
//
|
|
void AmmoBin::DumpAmmo()
|
|
{
|
|
if (ammoAlarm.Level() != Empty) // this+0x1A8 != 2
|
|
{
|
|
ammoCount = 0; // this+0x180
|
|
ammoAlarm.SetLevel(Dumped); // 5 (transient pulse)
|
|
ammoAlarm.SetLevel(Empty); // 2
|
|
}
|
|
}
|
|
|
|
//#############################################################################
|
|
// CookOff -- detonate the remaining rounds
|
|
//
|
|
// @004bd300
|
|
//
|
|
// Loads the prebuilt heat descriptor, sets its heat amount to
|
|
// (ammoCount * heatPerRound), injects it into the mech via the HeatableSubsystem
|
|
// base, then zeroes the bin and raises the Empty alarm.
|
|
//
|
|
void AmmoBin::CookOff()
|
|
{
|
|
HeatRecord record = *(HeatRecord *)&cookOffHeatSource; // words 0x7C, 0x7E..0x87
|
|
record.amount = (Scalar)ammoCount * heatPerRound; // this[0x60] * this[0x7D]
|
|
InjectHeat(&record); // FUN_004ac274(this, &record)
|
|
ammoCount = 0; // this+0x180
|
|
ammoAlarm.SetLevel(Empty); // 2
|
|
}
|
|
|
|
//#############################################################################
|
|
// AmmoBinSimulation -- the registered Performance
|
|
//
|
|
// @004bd394
|
|
//
|
|
// 1. Drive the watch alarm from the monitored temperature (HeatWatcher base).
|
|
// 2. If destroyed, force Empty.
|
|
// 3. Tick the feed timer while Feeding; on expiry, chamber (-> Loaded).
|
|
// 4. When the watch alarm hits FAILURE (level 2) and rounds remain, arm a
|
|
// randomised cook-off; on expiry damage the subsystem, fire the explosion
|
|
// notification (vtable slot 13) and CookOff(). Cancel if the bin empties.
|
|
// 5. Trip the Empty alarm whenever ammoCount drops below 1.
|
|
//
|
|
void AmmoBin::AmmoBinSimulation(Scalar time_slice)
|
|
{
|
|
WatchSimulation(time_slice); // FUN_004aeac4(this) -- drive this+0x140
|
|
|
|
if (simulationState == 1) // this+0x40 (destroyed)
|
|
ammoAlarm.SetLevel(Empty);
|
|
|
|
// feed timer (only while a round is in transit)
|
|
if (AmmoFeedTickFloor < time_slice && ammoAlarm.Level() == Feeding)
|
|
{
|
|
if (time_slice < feedTimer)
|
|
feedTimer -= time_slice;
|
|
else
|
|
{
|
|
feedTimer = 0;
|
|
ammoAlarm.SetLevel(Loaded);
|
|
}
|
|
}
|
|
|
|
// arm cook-off when overheated and not already empty
|
|
if (heatAlarm.Level() == 2 /* FAILURE, this+0x140 */ &&
|
|
ammoAlarm.Level() != Empty && cookOffArmed == 0)
|
|
{
|
|
cookOffArmed = 1; // this+0x18C
|
|
cookOffTime = GameClock::Now() + RandomDelay(); // this+0x190
|
|
}
|
|
|
|
// fire / cancel cook-off
|
|
if (cookOffArmed != 0)
|
|
{
|
|
if (ammoAlarm.Level() == Empty)
|
|
cookOffArmed = 0; // nothing left -- cancel
|
|
else if (cookOffTime < GameClock::Now())
|
|
{
|
|
cookOffArmed = 0;
|
|
SetStatusLevel(1); // FUN_0041bbd8(this+0xb, 1) -- subsystem damaged
|
|
// slot 13 (explosion notify): authoritative-only virtual call in the
|
|
// shipped code; runs through the installed vtable at run time.
|
|
CookOff(); // @004bd300
|
|
}
|
|
}
|
|
|
|
if (ammoCount < 1)
|
|
ammoAlarm.SetLevel(Empty);
|
|
}
|
|
|
|
//#############################################################################
|
|
// HandleMessage (slot 8, @004bdb94)
|
|
//
|
|
// Message 1 begins the cook-off countdown (arm the randomised timer once);
|
|
// any other message delegates to the HeatableSubsystem base.
|
|
//
|
|
Logical AmmoBin::HandleMessage(int message)
|
|
{
|
|
if (message != 1)
|
|
return MechSubsystem::HandleMessage(message); // base delegate (AmmoBin : HeatWatcher : MechSubsystem)
|
|
|
|
if (cookOffArmed == 0)
|
|
{
|
|
cookOffArmed = 1;
|
|
cookOffTime = GameClock::Now() + RandomDelay(); // FUN_00414b60 + FUN_004dcd94
|
|
return True;
|
|
}
|
|
return False;
|
|
}
|
|
|
|
//#############################################################################
|
|
// ReadUpdate (slot 9, @004bd2c0)
|
|
//
|
|
// Applies a network update via the HeatWatcher base; if the synced status has
|
|
// just transitioned to destroyed (state 1), dump all rounds and mark Empty.
|
|
//
|
|
void AmmoBin::ReadUpdateRecord(UpdateRecord *update)
|
|
{
|
|
int previousState = simulationState; // this+0x40 (inherited MechSubsystem)
|
|
HeatWatcher::ReadUpdateRecord(update); // FUN_004aea84
|
|
if (simulationState == 1 && previousState != 1)
|
|
{
|
|
ammoCount = 0; // this+0x180
|
|
ammoAlarm.SetLevel(Empty); // 2
|
|
}
|
|
}
|
|
|
|
//#############################################################################
|
|
// ResetToInitialState (slot 10)
|
|
//
|
|
// AMMOBIN.TCP ships this as an empty override.
|
|
//
|
|
void AmmoBin::ResetToInitialState()
|
|
{
|
|
}
|
|
|
|
//#############################################################################
|
|
// CreateStreamedSubsystem
|
|
//
|
|
// @004bd6f0 (record size 0x104, classID 0x0BCB)
|
|
//
|
|
// Chains the HeatWatcher resource parser, stamps the AmmoBin classID/size, then
|
|
// reads the ammo block. "AmmoClassID" accepts the symbolic forms
|
|
// "ProjectileClassID" (-> 0x0BD1) and "MissileClassID" (-> 0x0BBA). The model
|
|
// files are resolved to indices; every key is required (missing -> fail).
|
|
//
|
|
int AmmoBin::CreateStreamedSubsystem(
|
|
NotationFile *model_file,
|
|
const char *model_name,
|
|
const char *subsystem_name,
|
|
SubsystemResource *resource,
|
|
NotationFile *subsystem_file,
|
|
const ResourceDirectories *directories,
|
|
int passes)
|
|
{
|
|
if (!HeatWatcher::CreateStreamedSubsystem(model_file, model_name, subsystem_name, resource, // FUN_004aec54
|
|
subsystem_file, directories, passes))
|
|
return 0;
|
|
|
|
resource->classID = (RegisteredClass::ClassID)0x0BCB; // resource +0x20
|
|
resource->subsystemModelSize = 0x104; // resource +0x24
|
|
|
|
if (passes == 1)
|
|
{
|
|
resource->ammoCount = -1; // +0xFC sentinel
|
|
resource->feedRate = -1.0f; // +0x100 sentinel
|
|
resource->ammoClassID = -1; // +0xF0 sentinel
|
|
resource->ammoModelFile = -1; // +0xF4 sentinel
|
|
resource->explosionModelFile = -1; // +0xF8 sentinel
|
|
}
|
|
|
|
if (!ReadInt(subsystem_file, subsystem_name, "AmmoCount", &resource->ammoCount) &&
|
|
resource->ammoCount == -1)
|
|
{ ReportMissing(subsystem_name, "missing AmmoCount"); return 0; }
|
|
|
|
if (!ReadScalar(subsystem_file, subsystem_name, "FeedRate", &resource->feedRate) &&
|
|
resource->feedRate == _DAT_004bdb74 /* sentinel */)
|
|
{ ReportMissing(subsystem_name, "missing FeedRate"); return 0; }
|
|
|
|
const char *id = "Unspecified";
|
|
ReadString(subsystem_file, subsystem_name, "AmmoClassID", &id);
|
|
if (strcmp(id, "Unspecified") == 0 && resource->ammoClassID == -1)
|
|
{ ReportMissing(subsystem_name, "missing AmmoClassID"); return 0; }
|
|
if (strcmp(id, "Unspecified") != 0)
|
|
{
|
|
if (strcmp(id, "ProjectileClassID") == 0) resource->ammoClassID = 0x0BD1;
|
|
else if (strcmp(id, "MissileClassID") == 0) resource->ammoClassID = 0x0BBA;
|
|
else if (resource->ammoClassID == -1)
|
|
{ ReportMissing(subsystem_name, "invalid AmmoClassID"); return 0; }
|
|
}
|
|
|
|
const char *ammoModel = "Unspecified";
|
|
ReadString(subsystem_file, subsystem_name, "AmmoModelFile", &ammoModel);
|
|
if (strcmp(ammoModel, "Unspecified") != 0)
|
|
{
|
|
AmmoModelRecord *m = LookupModel(model_file, ammoModel, 1, -1); // FUN_00406ff8
|
|
if (m == 0 && resource->ammoModelFile == -1)
|
|
{ ReportMissing(ammoModel, "must be declared before .bld"); return 0; }
|
|
if (m != 0) resource->ammoModelFile = m->index;
|
|
}
|
|
else if (resource->ammoModelFile == -1)
|
|
{ ReportMissing(subsystem_name, "missing AmmoModelFile"); return 0; }
|
|
|
|
const char *exModel = "Unspecified";
|
|
ReadString(subsystem_file, subsystem_name, "ExplosionModelFile", &exModel);
|
|
if (strcmp(exModel, "Unspecified") != 0)
|
|
{
|
|
AmmoModelRecord *m = LookupModel(model_file, exModel, 1, -1);
|
|
if (m == 0 && resource->explosionModelFile == -1)
|
|
{ ReportMissing(exModel, "cannot find in resource file"); return 0; }
|
|
if (m != 0) resource->explosionModelFile = m->index;
|
|
}
|
|
else if (resource->explosionModelFile == -1)
|
|
{ ReportMissing(model_name, "missing ExplosionModelFile"); return 0; }
|
|
|
|
return 1;
|
|
}
|
|
|
|
//#############################################################################
|
|
// TestInstance
|
|
//
|
|
// @004bd1e0 IsDerivedFrom(ClassDerivations tag 0x512488).
|
|
//
|
|
Logical AmmoBin::TestInstance() const
|
|
{
|
|
return IsDerivedFrom(AmmoBin::ClassDerivations);
|
|
}
|
|
|
|
//#############################################################################
|
|
// TestClass (AMMOBIN.TCP)
|
|
//
|
|
Logical AmmoBin::TestClass(Mech &)
|
|
{
|
|
return True;
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// WAVE 4 -- compile-time layout locks (ZERO-headroom: sizeof MUST be 0x22C).
|
|
// Own fields are protected -> the friend AmmoBinLayoutCheck grants offsetof.
|
|
// The `ammoModelFile == 0x1E8` lock is LOAD-BEARING: it fails the build if
|
|
// either the ammoAlarm retype (HeatAlarm 8B -> WatcherGaugeAlarm 0x54) or the
|
|
// statusState deletion is missed. HeatWatcher is locked at 0x180.
|
|
//#############################################################################
|
|
struct AmmoBinLayoutCheck
|
|
{
|
|
static_assert(sizeof(AmmoBin) == 0x22C, "sizeof(AmmoBin) must be 0x22C (factory alloc, part_012.c:10091)");
|
|
static_assert(offsetof(AmmoBin, ammoCount) == 0x180, "AmmoBin own block starts at 0x180 (HeatWatcher ends 0x180)");
|
|
static_assert(offsetof(AmmoBin, ammoAlarm) == 0x194, "AmmoBin ammoAlarm @0x194 (0x54-byte WatcherGaugeAlarm)");
|
|
static_assert(offsetof(AmmoBin, ammoModelFile) == 0x1E8, "AmmoBin ammoModelFile @0x1E8 (proves ammoAlarm==0x54 AND statusState deleted)");
|
|
static_assert(offsetof(AmmoBin, explosionModelFile) == 0x1EC, "AmmoBin explosionModelFile @0x1EC");
|
|
static_assert(offsetof(AmmoBin, heatPerRound) == 0x1F4, "AmmoBin heatPerRound @0x1F4");
|
|
static_assert(offsetof(AmmoBin, initialAmmoCount) == 0x220, "AmmoBin initialAmmoCount @0x220");
|
|
static_assert(offsetof(AmmoBin, feedRate) == 0x224, "AmmoBin feedRate @0x224");
|
|
static_assert(offsetof(AmmoBin, reserved) == 0x228, "AmmoBin reserved @0x228 (+4 => sizeof 0x22C)");
|
|
};
|
|
|
|
|
|
//===========================================================================//
|
|
// Factory bridge -- AmmoBin (factory case 0xBCB, "JumpJet" label).
|
|
// The real class at 0xBCB (ctor @004bd5c4) is AmmoBin; the factory built a
|
|
// JumpJet RECON_SUBSYS stub. alloc 0x22c, ctor(owner,id,resource) -- the ctor
|
|
// supplies AmmoBin::DefaultData to the HeatWatcher base itself. No cache
|
|
// write; not a weapon (raw part_012.c:10090-10099 stores roster-only).
|
|
// NOTE: the Blackhawk (all-laser) carries NO 0xBCB; exercise with a ballistic/
|
|
// missile mech (MECH2.EGG = Mad Cat, LRMs). Its FeedAmmo consumer
|
|
// (ProjectileWeapon/MissileLauncher 0xBCD/0xBD0) is still stubbed, so full
|
|
// effect couples to the weapons wave -- but the bin's own feed/cook-off ticks.
|
|
//===========================================================================//
|
|
Subsystem *CreateAmmoBinSubsystem(Mech *owner, int id, void *seg)
|
|
{
|
|
return (Subsystem *) new (Memory::Allocate(0x22c))
|
|
AmmoBin(owner, id, (AmmoBin::SubsystemResource *)seg);
|
|
}
|