rack-eject speed; the authored MissileThruster was never applied The binary Missile hosts a MissileThruster subsystem: authored acceleration for BurnTime seconds after the MuzzleVelocity eject. Authored values decoded from BTL4.RES (type-15 missile model records, reached from the AmmoBin's ammoModelFile via the type-1 MODELLIST indirection): SRM 2.5s @ 600 u/s^2, LRM 10s @ 300 (climb 50), Streak 3s @ 300 (turn 360deg/s), NARC 10s @ 300; splash 30 all (port constant was already right); authored drag ~0.001 = negligible. The port pool flew every round at CONSTANT |MuzzleVelocity| -- SRMs 100 u/s, LRMs 30 u/s (10x slower than authored) -- the entirety of the night-7 "missiles are very slow" report, and the driver of the "explosion before the missiles arrive" perception (the salvo-lead detonates while the slow spread rounds straggle in). Fix: BTMissileThrustOf (mech4.cpp) lazily parses + caches the RES thruster table (ModelList ids aliased to their type-15 member's burn/accel); both launch paths (master FireWeapon + the replicant salvo mirror -- peers see the same speed) resolve through the launcher's bin and pass burn/accel into the pool; the advance integrates speed += accel*dt while burn remains, heading preserved. Ballistic rounds (autocannon/gauss) pass 0/0 -- unchanged. Impact log now prints v=/burnLeft= evidence. Verified live (solo, BT_SPAWN_ENEMY + BT_AF_MISSILE): thrust table 8 entries cached; Black Hawk Streak resolve burn=3 accel=300; impacts at v=277 u/s (was a constant 100). Fun authenticity note: the Black Hawk's "SRM6" fires strk (STREAK) ammo per its authored bin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
308 lines
14 KiB
C++
308 lines
14 KiB
C++
//============================================================================//
|
|
// File: ammobin.hpp //
|
|
// Project: BattleTech //
|
|
// Contents: Ammunition bin -- holds rounds, feeds a ProjectileWeapon, and //
|
|
// cooks off when overheated. //
|
|
//----------------------------------------------------------------------------//
|
|
// Date Who Modification //
|
|
// -------- --- ------------------------------------------------------------//
|
|
// 04/13/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
|
|
// @004bd2c0-@004bdb94 + CSS @004bd6f0 in part_013.c). Only the test fragment
|
|
// AMMOBIN.TCP survives (AmmoBin::TestClass -> True; AmmoBin::ResetToInitialState
|
|
// -> empty). Member/method names are inferred from the streamed resource keys
|
|
// ("AmmoCount"/"FeedRate"/"AmmoClassID"/"ProjectileClassID"/"MissileClassID"/
|
|
// "AmmoModelFile"/"ExplosionModelFile") and from usage; see ammobin.cpp for the
|
|
// per-method @ADDR evidence. Names past the HeatWatcher base are best-effort.
|
|
//
|
|
// Inheritance chain established from the decomp:
|
|
// Subsystem -> HeatableSubsystem -> HeatWatcher (ctor @4aeb40, vtable
|
|
// @0050ed10) -> AmmoBin (ctor @4bd5c4, vtable @0051286c, classID 0x0BCB).
|
|
// AmmoBin IS a HeatWatcher: it watches a temperature against the resource's
|
|
// degradation/failure thresholds, and when the watch alarm (this+0x140) reaches
|
|
// FAILURE it arms a randomised cook-off timer that dumps all stored rounds'
|
|
// heat into the mech and empties the bin. A ProjectileWeapon links to it as a
|
|
// SharedData connection (launcher this[0x10F]@0x43C, connection ctor @4bcbb0,
|
|
// vtable @00512424) and pulls a round via FeedAmmo() each time it fires.
|
|
//
|
|
// Object byte offsets (this is an int* in the decomp, word index in parens).
|
|
// Everything below +0x180 is the HeatWatcher / HeatableSubsystem / Subsystem
|
|
// base (watch heatAlarm level @0x140 = word 0x50; base status alarm @0x2C).
|
|
//
|
|
|
|
#if !defined (AMMOBIN_HPP)
|
|
# define AMMOBIN_HPP
|
|
|
|
# if !defined(HEATFAMILY_RESLICE_HPP)
|
|
# include <heatfamily_reslice.hpp> // HeatWatcher base (heat family)
|
|
# endif
|
|
|
|
//######################### Forward Class Declarations ########################
|
|
class Mech;
|
|
|
|
//###########################################################################
|
|
//################# AmmoBin Model Resource ##############################
|
|
//###########################################################################
|
|
//
|
|
// Streamed by AmmoBin::CreateStreamedSubsystem @004bd6f0 (record size 0x104,
|
|
// classID 0x0BCB). The ammo-specific block sits at +0xF0 past the heat-watcher
|
|
// resource header. "AmmoClassID" accepts the symbolic forms "ProjectileClassID"
|
|
// (stamps 0x0BD1) and "MissileClassID" (stamps 0x0BBA), naming the entity class
|
|
// the bin spawns when a round is fired.
|
|
//
|
|
struct AmmoBin__SubsystemResource:
|
|
public HeatWatcher::SubsystemResource
|
|
{
|
|
int ammoClassID; // +0xF0 "AmmoClassID"/"ProjectileClassID"/"MissileClassID"
|
|
int ammoModelFile; // +0xF4 "AmmoModelFile" (resolved model index)
|
|
int explosionModelFile; // +0xF8 "ExplosionModelFile" (resolved model index)
|
|
int ammoCount; // +0xFC "AmmoCount"
|
|
Scalar feedRate; // +0x100 "FeedRate" (seconds per round feed)
|
|
}; // ends 0x104
|
|
static_assert(offsetof(AmmoBin__SubsystemResource, ammoClassID) == 0xF0, "AmmoBin ammoClassID must be at 0xF0");
|
|
static_assert(offsetof(AmmoBin__SubsystemResource, feedRate) == 0x100, "AmmoBin feedRate must be at 0x100");
|
|
static_assert(sizeof(AmmoBin__SubsystemResource) == 0x104, "AmmoBin record must be 0x104");
|
|
|
|
//###########################################################################
|
|
//######################### CLASS -- AmmoBin #########################
|
|
//###########################################################################
|
|
//
|
|
// (vtable @0051286c, ctor @004bd5c4, dtor @004bd6b0, CSS @004bd6f0.)
|
|
//
|
|
class AmmoBin:
|
|
public HeatWatcher
|
|
{
|
|
friend struct AmmoBinLayoutCheck; // compile-time offset locks (ammobin.cpp)
|
|
friend int *BTAmmoBinCountPtr(void *bin); // panel ammo counter bridge (Streak fix)
|
|
friend int BTAmmoBinFeeding(void *bin); // panel reload-state bridge
|
|
friend int BTAmmoRoundModelResource(void *bin); // splash-radius resolve (round GameModel +0x50)
|
|
friend int BTAmmoBinCookOffArmed(void *bin); // #47: the eng-panel FIRE icon (bin+0x18C)
|
|
friend int BTAmmoBinCookOffTime(void *bin); // #47: the cook-off countdown numeric (bin+0x190)
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Shared Data Support
|
|
//
|
|
public:
|
|
static Derivation ClassDerivations; // IsDerivedFrom tag 0x512488
|
|
static Receiver::MessageHandlerSet MessageHandlers;
|
|
static AttributeIndexSet AttributeIndex;
|
|
static SharedData DefaultData; // resolved as &DAT_005125bc
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Ammo feed state machine (this->ammoAlarm @0x194, level @0x1A8 = word 0x6A)
|
|
//
|
|
// The 6-level AlarmIndicator carries the feed cycle. Levels confirmed from
|
|
// usage; 3/4 decoded 2026-07-23 from the EjectAmmo handler @004bb9b8 (press
|
|
// raises 3 while the eject countdown runs; a tap-release blips 4 then 1
|
|
// before the FeedAmmo pull cycles the round out).
|
|
//
|
|
public:
|
|
enum AmmoState {
|
|
Feeding = 0, // a round is in transit (feedTimer counting down)
|
|
Loaded = 1, // a round is chambered and ready to fire
|
|
Empty = 2, // no rounds remain
|
|
Ejecting = 3, // EJECT held -- countdown live (@004bb9b8 press)
|
|
Ejected = 4, // tap-release blip before the round cycles out
|
|
Dumped = 5 // transient pulse raised by DumpAmmo() before Empty
|
|
};
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Model Support
|
|
//
|
|
public:
|
|
enum {
|
|
CookingOff = HeatWatcher::StateCount,
|
|
StateCount
|
|
};
|
|
|
|
// Attribute Support -- audio binds an AudioStateWatcher to AmmoState; it
|
|
// resolves to ammoAlarm (@0x194, a 0x54 StateIndicator-compatible alarm) so
|
|
// the feed/reload/empty audio fires on the AmmoState transition. The static
|
|
// AttributeIndex (ammobin.cpp) is now chained to HeatWatcher -- previously it
|
|
// was a bare, unchained index, so even SimulationState resolved to NULL.
|
|
enum {
|
|
AmmoStateAttributeID = HeatWatcher::NextAttributeID,
|
|
FireCountdownStartedAttributeID, // F18: the ammo COOK-OFF warning (binary
|
|
// AmmoBin table @0x512600 -> +0x18C); the 4
|
|
// authored start/stop match watchers gate the
|
|
// cook-off countdown klaxon on it
|
|
NextAttributeID
|
|
};
|
|
static const IndexEntry AttributePointers[];
|
|
|
|
typedef void
|
|
(AmmoBin::*Performance)(Scalar time_slice);
|
|
|
|
// @004bd394 -- the registered Performance. Drives the watch alarm, ticks
|
|
// the feed timer (Feeding -> Loaded), arms/fires the cook-off, and trips
|
|
// the Empty alarm when exhausted.
|
|
void
|
|
AmmoBinSimulation(Scalar time_slice);
|
|
|
|
void
|
|
SetPerformance(Performance performance)
|
|
{
|
|
Check(this);
|
|
activePerformance = (Simulation::Performance)performance;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Ammo feed interface (called by the linked ProjectileWeapon)
|
|
//
|
|
public:
|
|
// @004bd4f4 -- request one round. When Loaded: chamber the next round,
|
|
// reset the feed timer to feedRate, decrement ammoCount, and return the
|
|
// remaining count (Empty alarm raised at 0). Returns 0 if not ready /
|
|
// already feeding / empty / destroyed.
|
|
int
|
|
FeedAmmo();
|
|
|
|
// @004bd588 -- forcibly empty the bin (eject / strip): zero ammoCount and
|
|
// pulse the alarm Dumped(5) -> Empty(2).
|
|
void
|
|
DumpAmmo();
|
|
|
|
// The feed alarm's current level (this+0x1A8) -- the linked weapon's
|
|
// ProjectileWeaponSimulation @004bbd04 reads it raw off the resolved bin:
|
|
// gate 2 @4bbd80 (level 2/3 or bin destroyed -> weapon alarm 7), the
|
|
// Loading recoil-clamp Loaded test @4bbe25 (level 1 -> weapon Loaded), and
|
|
// the post-fire Empty test @4bbf11 (level 2 -> weapon alarm 7). Named
|
|
// accessor per the databinding rule (never a raw offset on our layout).
|
|
int
|
|
GetAmmoState() const { return ammoAlarm.GetLevel(); }
|
|
|
|
// The EjectAmmo handler @004bb9b8 drives the feed alarm from the linked
|
|
// weapon (press -> Ejecting(3); tap-release -> Ejected(4) then Loaded(1)
|
|
// ahead of the FeedAmmo pull). Named setter per the databinding rule.
|
|
void
|
|
SetAmmoState(int level) { ammoAlarm.SetLevel(level); }
|
|
|
|
// Rounds remaining (this+0x180) -- read-only, for the AMMO digits gauge and
|
|
// the [ammo] fire trace (named accessor per the databinding rule).
|
|
int
|
|
GetAmmoCount() const { return ammoCount; }
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Subsystem virtual overrides (vtable @0051286c)
|
|
//
|
|
public:
|
|
Logical
|
|
HandleMessage(int message); // slot 8, @004bdb94
|
|
void
|
|
ReadUpdateRecord(UpdateRecord *update); // slot 9, @004bd2c0
|
|
void
|
|
ResetToInitialState(); // slot 10 (AMMOBIN.TCP: empty)
|
|
// issue #22 (PORT, fresh-mech respawn emulation): refill to the ctor
|
|
// state. AMMOBIN.TCP's RTIS is authentically EMPTY (kept so); the
|
|
// binary's respawn refilled ammo by building a NEW mech -- our
|
|
// entity-reuse respawn refills here instead.
|
|
void
|
|
DeathReset(int reset_command);
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Local helpers
|
|
//
|
|
protected:
|
|
// @004bd300 -- cook-off: inject (ammoCount * heatPerRound) heat into the
|
|
// mech, then zero ammoCount and raise the Empty alarm.
|
|
void
|
|
CookOff();
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// TestClass Support (AMMOBIN.TCP)
|
|
//
|
|
public:
|
|
static Logical
|
|
TestClass(Mech &); // AMMOBIN.TCP: returns True
|
|
Logical
|
|
TestInstance() const; // @004bd1e0
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Construction and Destruction
|
|
//
|
|
public:
|
|
typedef AmmoBin__SubsystemResource SubsystemResource;
|
|
|
|
AmmoBin( // @004bd5c4
|
|
Mech *owner,
|
|
int subsystem_ID,
|
|
SubsystemResource *subsystem_resource
|
|
);
|
|
~AmmoBin(); // @004bd6b0
|
|
|
|
static int
|
|
CreateStreamedSubsystem( // @004bd6f0 (record size 0x104)
|
|
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 appended past the HeatWatcher base (offsets best-effort).
|
|
//
|
|
protected:
|
|
int ammoCount; // @0x180 (word 0x60) rounds remaining
|
|
Scalar feedTimer; // @0x184 (word 0x61) seconds left in current feed
|
|
int ammoClassID; // @0x188 (word 0x62) entity class spawned per round
|
|
int cookOffArmed; // @0x18C (word 0x63) cook-off timer running
|
|
int cookOffTime; // @0x190 (word 0x64) game clock at which it detonates
|
|
WatcherGaugeAlarm ammoAlarm; // @0x194 (0x54) 6-level feed alarm (level @0x1A8)
|
|
// (WAVE 4) `int statusState` DELETED -- it was a this+0x40 SHADOW of the
|
|
// inherited MechSubsystem::simulationState; the reads now use that base
|
|
// member. The 0x54 ammoAlarm (was an 8-byte HeatAlarm) lands ammoModelFile
|
|
// at 0x1E8 -- exact binary layout, locked by AmmoBinLayoutCheck.
|
|
int ammoModelFile; // @0x1E8 (word 0x7A) round model index
|
|
public:
|
|
// #84: the round's model RESOURCE ID (the type-15 record carrying the
|
|
// MissileThruster tail the binary Missile ctor copies) -- read by the
|
|
// launcher's fire path via the BTAmmoBinModelFile bridge.
|
|
int AmmoModelFileID() const { return ammoModelFile; }
|
|
protected:
|
|
int explosionModelFile; // @0x1EC (word 0x7B) cook-off explosion model index
|
|
|
|
// @0x1F0..0x21C (words 0x7C..0x87): the cook-off DAMAGE record -- a real
|
|
// engine `Damage` (0x30 bytes: type@+0, amount@+4, force@+8, normal@+0x14,
|
|
// impact@+0x20, burstCount@+0x2C). Gitea #46 CORRECTION: the old
|
|
// reconstruction modeled this span as an opaque "heat-injection
|
|
// descriptor" (`cookOffHeatSource/heatPerRound/cookOffHeatRest`) -- wrong
|
|
// on three counts, all [T1]:
|
|
// * FUN_0041db7c is `Damage::Damage()` (raw disasm == the T0
|
|
// DAMAGE.cpp:34 ctor: amount 0, force identity, normal (0,1,0),
|
|
// burst 1);
|
|
// * "@0x1F4 = heatPerRound" is the record's `damageAmount` slot -- the
|
|
// LINKED WEAPON's per-round damage, stamped by the ProjectileWeapon
|
|
// ctor @004bc3fc (bin+0x1F0..0x21C <- weapon->damageData @0x3A8,
|
|
// via owner->roster[0x128][resource+0x1C0]; the projweap.cpp comment
|
|
// calling this "the bin's HUD display block" was wrong, and the
|
|
// stamping had never been wired -- so the record stayed all-zero);
|
|
// * CookOff() does not "inject heat": it builds a local copy with
|
|
// amount = ammoCount x damageAmount (fild [0x180]; fmul [0x1F4])
|
|
// and hands it to @004ac274 = MechSubsystem::DistributeCriticalHit,
|
|
// the per-DamageZone TakeDamage fan-out (see mechsub.cpp).
|
|
Damage cookOffDamage; // @0x1F0 (words 0x7C..0x87)
|
|
|
|
int initialAmmoCount; // @0x220 (word 0x88) ammoCount captured at ctor
|
|
Scalar feedRate; // @0x224 (word 0x89) resource FeedRate
|
|
int reserved; // @0x228 (word 0x8A) init 0 (unused)
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Cross-family stamp (ProjectileWeapon ctor @004bc3fc -> this bin).
|
|
//
|
|
public:
|
|
// The binary writes the roster slot UNGUARDED (no AmmoBin type test) --
|
|
// every authored weapon's AmmoBin index names a real bin, so the port
|
|
// keeps the same contract behind this typed accessor.
|
|
void
|
|
StampCookOffDamage(const Damage &weapon_damage)
|
|
{ cookOffDamage = weapon_damage; }
|
|
};
|
|
#endif
|