//============================================================================// // 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 constants (raw disasm, scratchpad/disammo.py -- Gitea #46): // _DAT_004bd4e8 = 0.0f (feed-tick guard: advance only when time_slice > 0) // _DAT_004bd4ec = 10.0f _DAT_004bd4f0 = 0.5f (the cook-off FUSE, below) // // 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 MechSubsystem::DistributeCriticalHit -- the ammo-explosion // per-DamageZone TakeDamage fan-out (mechsub.cpp). The old // "HeatableSubsystem::InjectHeat" label was WRONG (#46). // FUN_0041b9ec AlarmIndicator::Init(levels) FUN_0041baa4 AlarmIndicator::Destroy // FUN_0041bbd8 AlarmIndicator::SetLevel // FUN_0041db7c Damage::Damage() -- the T0 DAMAGE.cpp ctor, byte-matched. // The old "init heat descriptor" label was WRONG (#46). // FUN_00414b60 Now() -- returns &Time (pause-aware tick counter); the // caller dereferences .ticks (same mapping as btl4pb.cpp). // FUN_004dcd94 __ftol (float->int). The old "random delay" label was a // GHIDRA ARTIFACT: the export dropped the caller's x87 // expression and left a bare `iVar = FUN_004dcd94();`. The // raw bytes @004bd450 are: // fld dword [0x4bd4ec] ; 10.0f // fmul dword [0x52140c] ; x SystemClock ticksPerSecond // fadd dword [0x4bd4f0] ; + 0.5f (rounding) // call __ftol // i.e. the cook-off fuse is a FIXED 10.0 SECONDS, not random. // (DAT_0052140c is the ticks-per-second global -- set to 28.0 // or 18.206 at startup, part_000.c:921/926, and used as the // ticks->seconds divisor everywhere else.) [T1] // FUN_0041a1a4 IsDerivedFrom FUN_004022d0 operator delete // #include #pragma hdrstop #if !defined(AMMOBIN_HPP) # include #endif #if !defined(TESTBT_HPP) # include #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 for the streamed-resource parser. // struct AmmoModelRecord{ int index; }; static const Scalar _DAT_004bdb74 = -1.0f; // FeedRate "unset" sentinel 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 // // Gitea #46 -- THE COOK-OFF CLOCK. Three stand-ins used to sit here and each // was a kill-switch stacked on the same path: // * `GameClock::Now() { return 0; }` -> `cookOffTime < Now()` was `0 < 0`, // so an ARMED bay fire never detonated (the field report exactly: // "two ammo bay fires and no death"); // * `RandomDelay() { return 0; }` -> a misdecode; see the header block -- // the real fuse is a FIXED 10.0 seconds in clock ticks; // * `InjectHeat(void*) {}` -> even a firing cook-off applied // NOTHING; the real @004ac274 is the per-DamageZone TakeDamage fan-out. // static int BTGameClockNowTicks() // FUN_00414b60, dereferenced { return ((volatile Time &)Now()).ticks; } static int BTCookOffFuseTicks() // fld 10.0; fmul tps; fadd 0.5; __ftol { return (int)(10.0f * (float)SystemClock::GetTicksPerSecond() + 0.5f); } //########################################################################### //########################################################################### // 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) ATTRIBUTE_ENTRY(AmmoBin, FireCountdownStarted, cookOffArmed) // F18 @0x18C -- ammo cook-off warning gate }; 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) // @0x1F0: FUN_0041db7c(this+0x7c) is Damage::Damage() -- our cookOffDamage // member ctor already ran it (amount 0, normal (0,1,0), burst 1). The // REAL type/amount arrive later, stamped by the linked ProjectileWeapon's // ctor @004bc3fc (StampCookOffDamage <- weapon->damageData). [#46] 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; } // Gitea #47: the eng-panel FIRE icon + cook-off countdown bridges. The // binary's BallisticWeaponCluster::Execute (@004c9a38) reads // this[0x3b] = *(bin + 0x18C) <- cookOffArmed (the btefire TwoState) // this[0x3e] = (*Now() - *(bin+0x190)) / ticksPerSecond // <- seconds relative to DETONATION // The old gauge reconstruction misread bin+0x18C as "the reload state" and // bridged it to BTAmmoBinFeeding -- so the FIRE icon blinked on every reload // feed and never lit on a bay fire (RajelAran: "it doesn't"). int BTAmmoBinCookOffArmed(void *bin) { return (bin != 0) ? ((AmmoBin *)bin)->cookOffArmed : 0; } int BTAmmoBinCookOffTime(void *bin) { return (bin != 0) ? ((AmmoBin *)bin)->cookOffTime : 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 { // ALWAYS log the bay dump (rare + forensic -- eject-hold / strip) DEBUG_STREAM << "[ammo] " << GetName() << " bay DUMPED overboard (" << ammoCount << " rounds jettisoned)\n" << std::flush; ammoCount = 0; // this+0x180 ammoAlarm.SetLevel(Dumped); // 5 (transient pulse) ammoAlarm.SetLevel(Empty); // 2 } } //############################################################################# // CookOff -- detonate the remaining rounds // // @004bd300 (raw disasm, scratchpad/disammo.py -- Gitea #46) // // Copies the weapon-stamped cook-off Damage record to a local, scales its // amount to (ammoCount x per-round damageAmount): // fild dword [ebx+0x180] ; ammoCount (int) // fmul dword [ebp-0x2c] ; x damageAmount (FLOAT -- fmul, not fild; // ; the old "(float)param_1[0x7d]" int // ; reading was a Ghidra rendering) // then hands it to @004ac274 = MechSubsystem::DistributeCriticalHit -- the // status-alarm pulse + own-zone gut + per-DamageZone TakeDamage fan-out // (mechsub.cpp) -- and finally zeroes the bin and raises the Empty alarm. // void AmmoBin::CookOff() { Damage record = cookOffDamage; // words 0x7C..0x87 -> locals record.damageAmount = (Scalar)ammoCount * cookOffDamage.damageAmount; // fild [0x180] * fmul [0x1F4] // Rare + forensic -- always log (the DumpAmmo precedent). DEBUG_STREAM << "[ammo] " << GetName() << " BAY FIRE DETONATION: " << ammoCount << " rounds x " << cookOffDamage.damageAmount << " = " << record.damageAmount << " (type " << (int)record.damageType << ")\n" << std::flush; DistributeCriticalHit(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: the 10-second fuse if (heatAlarm.Level() == 2 /* FAILURE, this+0x140 */ && ammoAlarm.Level() != Empty && cookOffArmed == 0) { cookOffArmed = 1; // this+0x18C cookOffTime = BTGameClockNowTicks() + BTCookOffFuseTicks(); // this+0x190 // Rare + forensic -- always log (mirrors the detonation line). DEBUG_STREAM << "[ammo] " << GetName() << " BAY FIRE: cook-off armed, " << ammoCount << " rounds, detonation in 10s (purge to cancel)\n" << std::flush; } // fire / cancel cook-off if (cookOffArmed != 0) { if (ammoAlarm.Level() == Empty) { cookOffArmed = 0; // nothing left -- cancel DEBUG_STREAM << "[ammo] " << GetName() << " bay fire EXTINGUISHED (bin empty)\n" << std::flush; } else if (cookOffTime < BTGameClockNowTicks()) { cookOffArmed = 0; statusAlarm.SetLevel(1); // FUN_0041bbd8(this+0xb=+0x2C, 1) -- Destroyed // @004bd4b1: `if (this[0x41]) (*vtbl+0x34)(this)` -- this+0x104 is // printSimulationState and slot 13 is the OnAlarmChanged state // PRINT (" = __Destroyed/__Exploding", @004ac8c0). The old // "authoritative-only explosion notify" comment was wrong [#46]; // the explosion VISUAL rides the TakeDamageMessage's // inflictingSubsystemID (the message-manager explosion bundling). if (printSimulationState) OnAlarmChanged(); 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 = BTGameClockNowTicks() + BTCookOffFuseTicks(); // Now() + the 10s fuse DEBUG_STREAM << "[ammo] " << GetName() << " BAY FIRE (message): cook-off armed, " << ammoCount << " rounds, detonation in 10s\n" << std::flush; 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 } } //############################################################################# // DeathReset -- issue #22 (PORT, fresh-mech respawn emulation) // // The binary respawn built a NEW mech: a fresh AmmoBin ctor = full bin, // Loaded feed alarm, idle feed timer, cook-off disarmed. Our entity-reuse // respawn restores exactly those ctor values. (ResetToInitialState below // stays authentically EMPTY per AMMOBIN.TCP.) // void AmmoBin::DeathReset(int reset_command) { // // THE ARG GATE IS AUTHENTIC AND LOAD-BEARING (@004bd26c, #55 step 2): // if (arg) { feedTimer = 0; [0x228] = 0; ammoCount = initialAmmoCount; // cookOffArmed = 0; } // ammoAlarm.SetLevel(Loaded); // UNCONDITIONAL // The refill runs on a RESPAWN (arg 1) and must NOT run at DEATH (arg 0) -- // a wreck keeps its counts. This body used to ignore the arg, which was // harmless only while the death sweep was a no-op; the moment that sweep // was restored (SUBSYSTM.h DeathShutdown -> DeathReset with arg 0) an // unguarded refill would have re-armed every corpse. // if (reset_command != 0) { if (getenv("BT_DEATH_LOG")) DEBUG_STREAM << "[respawn] ammo bin " << subsystemID << " refilled " << ammoCount << " -> " << initialAmmoCount << "\n" << std::flush; ammoCount = initialAmmoCount; // @0x180 <- @0x220 (ctor value) feedTimer = 0; // @0x184 cookOffArmed = 0; // @0x18C } ammoAlarm.SetLevel(Loaded); // @0x1A8 -- unconditional per the binary } //############################################################################# // 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"); // #46: the cook-off Damage record occupies exactly words 0x7C..0x87. static_assert(offsetof(AmmoBin, cookOffDamage) == 0x1F0, "AmmoBin cookOffDamage @0x1F0 (words 0x7C..0x87)"); static_assert(sizeof(Damage) == 0x30, "Damage must be 0x30 (12 words -- the binary descriptor span)"); static_assert(offsetof(Damage, damageAmount) == 0x4, "Damage::damageAmount @+4 -- @004bd300 fmul reads bin+0x1F4"); static_assert(offsetof(Damage, burstCount) == 0x2C, "Damage::burstCount @+0x2C -- @004bc3fc writes bin+0x21C"); 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); }