Files
BT411/game/reconstructed/ammobin.hpp
T
arcattackandClaude Fable 5 48c9c8444f Fire VISUALS wave: the authored firesmoke sheet, vertex-alpha effect cards, the case-4 wreck dressing
The "fireballs like the demo vids" arc, decomp/content-grounded end to end,
plus the live-play UX batch verified over the same sessions:

FIRESMOKE SHEET (the PFX fireball fix): every firesmokeN_scr_tex in BTFX.VMF
maps the SAME 64x64 tileable noise image bintA (variants differ only in SCROLL
rate) and firesmoke1_mtl colours it through the "fiery" ramp (0.3,0.1,0.1)->
(0.9,0.7,0.3).  The particle layer now bakes ramp(lum(bintA)) as its sprite
colour (noise detail in alpha) and SCROLLS it at firesmoke1's authored rate
via a texture-transform; the port's radial soft-edge mask moved to a second
CLAMPed stage so the WRAPPED scroll rolls flame through the sprite without
scrolling the edge away.  Old grit x radial bake kept as the no-BINTA fallback.
Impact hits, damage-band smoke and death booms all ride this layer.

AUTHORED TEXTURE SCROLL in the model path: the BMF TEXTURE records carry
SPECIAL " SCROLL u0 v0 du dv" (tag 0x2037); the draw path always supported
per-op scrolling (SetTextureScrolling) but the BGF loader never parsed it, so
every scrolling material rendered frozen.  Wired TexRef -> MatInfo -> batch ->
L4TEXOP.doScroll: the flame cards (flamebig/fire5) now roll fire noise.

VERTEX-ALPHA EFFECT CARDS (the "twisted drill bit of fire" fix): FLAMEBIG's
verts carry authored float RGBA -- white-hot base (1.0,0.99,0.97) -> dark-red
tip fading to alpha -0.2 (the DPL clamp convention).  The loader kept a flat
batch colour and drew it OPAQUE = a solid orange spike.  Corpus sweep: exactly
14 shipped BGFs carry vertex alpha, ALL effect cards (flames, MUZFLASH,
EXDISK_A/B/C, TMST_A/B/C, beam models, DECLOUDS).  Such batches now keep the
authored per-vertex gradient and route to the alpha-blend pass, unlit,
colour = texture x gradient, alpha = the vertex fade; sky objects excluded
(drawAsSky + alphaTest passes NEITHER pass filter -- DECLOUDS stays in the
sky pass).  MUZFLASH/EXDISK render correctly for free when the muzzle-model
work lands.

WRECK DRESSING (the 1996 ExplosionScripts case-4 transcription): pieces spawn
HIDDEN and reveal 0.25s after the boom (the InstanceSwitch delay, behind the
dnboom flash); flamebig hangs over the pile, Y-BILLBOARDED at the camera
(SetOffsetYaw + a camera-pos getter -- the dpl_SetDCSReorientAxes analog);
the MakeDCSFall settle arms at the reveal with the two authored rates (hulk/
debris -0.025 t^2, fires -0.01 t^2 -- the flames ride above the sinking pile
and die with it at burial).  EMPTY-PLACEHOLDER hulk guard: THRDBR.BGF is a
153-byte zero-geometry stub that "loads fine" -- vertex-count check now routes
it to the gendbr fallback (a Thor wreck was invisible).  Hulk content census
recorded: AVADBR==MADDBR==VULDBR geometry (palette-only prefix diffs),
RAPDBR==SNDDBR==STIDBR byte-identical -- wreck variety is materials + the
dressing, not unique piles.

LIVE-PLAY BATCH: muzzle resolve uses the named segmentIndex (raw +0xdc read
was layout garbage); forward launch frame (authored MuzzleVelocity +Z vs the
mech's -Z facing); dock-bottom single window (gauge strip appended below the
world viewport, 1100x600 default, BT_DEV_GAUGES_WINDOW=1 restores the separate
window); portrait sec surface unrotated CW; ammo counters live via typed
bridges (BTAmmoBinCountPtr/BTAmmoBinFeeding/BTWeaponAmmoBin -- raw bin+0x180
and a hand-rolled link walk were garbage); fourth fire key ('4' = Pinky);
panel/arc probes de-aliased (%61 prime).

KB: rendering.md (vertex-alpha card family + scroll), combat-damage.md (hulk
census + THRDBR stub), gauges-hud.md (ammo bridges).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 20:04:29 -05:00

227 lines
9.8 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
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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 and 4 are unobserved.
//
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
Dumped = 5 // transient pulse raised by DumpAmmo() before Empty
};
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Model Support
//
public:
enum {
CookingOff = HeatWatcher::StateCount,
StateCount
};
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();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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
int explosionModelFile; // @0x1EC (word 0x7B) cook-off explosion model index
// @0x1F0 (word 0x7C) cook-off heat-injection descriptor, inited by
// FUN_0041db7c; CookOff() fills word 0x7D (@0x1F4) below with the total
// heat (ammoCount * heatPerRound) before injecting via HeatableSubsystem.
int cookOffHeatSource[1]; // @0x1F0 (word 0x7C)
Scalar heatPerRound; // @0x1F4 (word 0x7D)
int cookOffHeatRest[10]; // @0x1F8..0x21C (words 0x7E..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)
};
#endif