Files
BT412/game/reconstructed/projweap.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

341 lines
16 KiB
C++

//===========================================================================//
// File: projweap.hpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: ProjectileWeapon -- MechWeapon that spawns physical projectiles //
// (tracers / missiles) and consumes ammunition from an AmmoBin. //
//---------------------------------------------------------------------------//
// 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
// @004bbae0-@004bcff0, file=bt/projweap.cpp). Cross-referenced against the
// SURVIVING sources PROJWEAP.TCP (TestClass/ResetToInitialState stubs),
// MISLANCH.HPP (MissileLauncher : ProjectileWeapon -- pins this interface and
// the resource layout), and the reconstructed base mechweap.hpp/.cpp.
//
// Inheritance chain established from the decomp:
// ... -> PoweredSubsystem -> MechWeapon (mechweap.cpp, ctor @004b99a8)
// -> ProjectileWeapon (this file, vtable @0051242c, ctor @004bc3fc)
// -> MissileLauncher (mislanch.cpp, vtable @00512570, ctor @004bcff0)
//
// EVIDENCE that this class is ProjectileWeapon (not Emitter):
// * The class-name string "ProjectileWeapon" lives at DAT_00512280, immediately
// adjacent to this class's vtable group (@00512424/@0051242c).
// * MechWeapon has exactly TWO direct children whose ctors chain to the
// MechWeapon ctor @004b99a8: the energy weapon Emitter (ctor @004bb120,
// vtable @00512078, parses SeekVoltage/DischargeTime) and THIS class
// (ctor @004bc3fc, vtable @0051242c, parses AmmoBin/TracerInterval).
// * MissileLauncher's ctor (@004bcff0) chains to THIS ctor (@004bc3fc) and adds
// exactly { int missileCount; Vector3D muzzleVelocity; } at resource +0x1D0 /
// +0x1D4 -- precisely the MissileLauncher__SubsystemResource of MISLANCH.HPP.
// By definition MissileLauncher's direct base IS ProjectileWeapon, so the
// class at @004bc3fc is ProjectileWeapon.
//
// NOTE on PROJWEAP.TCP: the surviving .TCP is an early test-scaffold (no copyright
// banner) in which TestClass() just returns True and ResetToInitialState() is
// empty. The shipped binary implements a real ResetToInitialState (@004bbaf8);
// the empty body was filled in after the .TCP snapshot. TestClass()==True still
// holds.
//
// Field offsets in comments are byte offsets into the shipped object; the
// MechWeapon base occupies everything below +0x3F0. ProjectileWeapon members
// span +0x3F0..+0x447 (object size 0x448). Several member NAMES are taken from
// the binary's own attribute strings ("TotalTimeToEject"/"TimeToEject"/
// "PercentOfEject"/"EjectAmmo" @DAT_00512290) and resource tag strings
// ("TracerInterval","AmmoBin","MinTimeOfFlight","MinVoltagePercentToFire",
// "MinJamChance"); the remainder are inferred from usage and flagged best-effort.
//
#if !defined(PROJWEAP_HPP)
# define PROJWEAP_HPP
#if !defined(MECHWEAP_HPP)
# include <mechweap.hpp>
#endif
//##################### Forward Class Declarations #######################
class Mech;
class Entity;
class AmmoBin;
class ResourceFile;
class NotationFile;
//###########################################################################
//################## ProjectileWeapon Model Resource ###################
//###########################################################################
//
// Extends the MechWeapon resource record. The ProjectileWeapon-specific fields
// begin at +0x1BC in the parsed record (see CreateStreamedSubsystem @004bc7cc).
// Total record size is 0x1D0 -- the parser stamps subsystemModelSize = 0x1D0.
//
struct ProjectileWeapon__SubsystemResource:
public MechWeapon::SubsystemResource
{
int tracerInterval; // +0x1BC "TracerInterval" (read as int/bool)
int ammoBinIndex; // +0x1C0 "AmmoBin" (subsystem-name -> index, +2 bias)
Scalar minTimeOfFlight; // +0x1C4 "MinTimeOfFlight"
Scalar minVoltagePercentToFire; // +0x1C8 "MinVoltagePercentToFire"
Scalar minJamChance; // +0x1CC "MinJamChance"
}; // sizeof == 0x1D0
//###########################################################################
//####################### ProjectileWeapon #########################
//###########################################################################
//
// Abstract MechWeapon specialisation for weapons that launch a physical
// projectile (a tracer round or a Missile) and draw rounds from an AmmoBin.
// Adds an eject/reload cycle, a heat-driven jam roll, a minimum time-of-flight
// lead solution, and per-shot projectile spawning.
// (vtable @0051242c, ctor @004bc3fc, dtor @004bc688.)
//
class ProjectileWeapon:
public MechWeapon
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support
//
public:
static Derivation ClassDerivations; // parent = MechWeapon::ClassDerivations,
// name = "ProjectileWeapon" (DAT_00512280)
static Receiver::MessageHandlerSet& GetMessageHandlers(); // inherits MechWeapon's (task #6)
static AttributeIndexSet AttributeIndex;
static SharedData DefaultData;
// Embedded ref-counted connection to the AmmoBin subsystem (resource
// AmmoBin index). The shipped layout is a 0xC-byte SharedData::Connection
// (ctor FUN_004bcbb0->FUN_004179d4; resolver FUN_00417ab4 two-level-derefs +8),
// NOT an empty proxy -- modeling it as 1 byte made ProjectileWeapon 8 bytes
// short (0x440 vs the binary 0x448) and slid MissileLauncher's missileCount off
// 0x448. Named access (Construct/Connect/Resolve/Destruct) is unchanged + inert
// in bring-up (no plug resolves). Same 0xC pattern as SubsystemConnection.
struct AmmoBinConnection {
void *linked; // +0x00 (SharedData::Connection; inert here)
int _reserved[2]; // pad to the binary 0xC connection
void Construct() { linked = 0; _reserved[0] = _reserved[1] = 0; }
template<class T> void Connect(T s) { linked = (void *)s; }
void Destruct() { linked = 0; }
void *Resolve() { return linked; }
};
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Weapon firing-state alarm levels (this+0x350 weaponAlarm, reported by
// PrintState @004bc6c8). Levels 0..4 are inherited from the MechWeapon /
// Emitter scheme (Firing/Loaded/Loading/TriggerDuringLoad); ProjectileWeapon
// adds the ammo-specific states:
//
public:
enum WeaponState {
JammedState = 5, // HandleMessage(2) -> SetLevel(5)
TriggerDuringJamState = 6,
NoAmmoState = 7
};
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Simulation Support (Performance member-function pointer; the per-frame
// driver lives at @004bbd04 and is installed by the ctor unless the model
// is flagged "no simulation").
//
public:
typedef void
(ProjectileWeapon::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
// @004bbd04 -- ProjectileWeapon per-frame update (Performance). Body not
// recovered by the decompiler (function prologue confirmed at 0x4bbd04);
// drives UpdateEject / DrawFiringCharge / CheckForJam / ComputeTimeOfFlight
// and triggers FireWeapon via the vtable.
void
ProjectileWeaponSimulation(Scalar time_slice);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Firing -- overrides MechWeapon's pure-virtual FireWeapon (vtable slot 18).
//
// MechWeapon's slot-18 body is the pure-virtual trap @004ba45c
// ("ERROR: Accessing Pure Virtual Method"). ProjectileWeapon provides the
// real spawn at @004bc104; MissileLauncher overrides again at @004bcc60.
//
protected:
void
FireWeapon(); // slot 18, @004bc104 (body not recovered)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Subsystem virtual overrides (slots on vtable @0051242c)
//
// Slot indices verified by decoding the raw vtable bytes (section_dump).
// SLOT MAP CORRECTED (task #51): 6=ReadUpdateRecord, 9=TakeDamage (the
// old attributions were swapped -- see mechweap.hpp for the evidence):
// slot 6 ReadUpdateRecord INHERITED MechWeapon @004b964c
// slot 7 WriteUpdateRecord INHERITED MechWeapon @004b9690
// slot 8 HandleMessage OVERRIDE @004bcabc
// slot 9 TakeDamage OVERRIDE @004bbae0 (thin -> MechWeapon)
// slot 10 ResetToInitialState OVERRIDE @004bbaf8
// slot 12 GetStatusFlags OVERRIDE @004bbf88 (body not recovered)
// slot 13 PrintState OVERRIDE @004bc6c8
// slot 16 (per-frame hook) OVERRIDE @004bbc20 (body not recovered)
// slot 18 FireWeapon OVERRIDE @004bc104 (body not recovered)
//
public:
LWord
GetStatusFlags(); // slot 12, @004bbf88
Logical
HandleMessage(int message); // slot 8, @004bcabc
void
ResetToInitialState(); // slot 10, @004bbaf8
void
PrintState(); // slot 13, @004bc6c8
protected:
// TASK #51 RENAME: slot 9 is TakeDamage (the old ReadUpdateRecord
// attribution was swapped; the record read is INHERITED from
// MechWeapon slot 6 @004b964c -- see mechweap.hpp for the evidence).
void
TakeDamage(Damage &damage); // slot 9, @004bbae0
// slot 16, @004bbc20 -- MechWeapon-family per-frame hook (overrides
// MechWeapon @004b0b5c). Body not recovered. TODO: confirm role.
virtual void
UpdateWeaponState();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Test Class Support
//
public:
static Logical
TestClass(Mech&); // @ PROJWEAP.TCP -> True
Logical
TestInstance() const; // @004bd1e0 family (IsDerivedFrom)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Internal model helpers (recovered)
//
protected:
// @004bbb50 -- advance the eject/reload countdown (TimeToEject), update
// PercentOfEject, and when complete request a fresh round from the AmmoBin
// (NoAmmo alarm if the bin is dry).
void
UpdateEject(Scalar time_slice);
// @004bbc78 -- bleed the firing charge (MechWeapon recoil @0x3E8) according
// to the available voltage / ammo-bin supply.
void
DrawFiringCharge(Scalar time_slice);
// @004bbfcc -- roll a heat-scaled jam chance: returns True if the weapon
// jams this shot. Jam probability = clamp((0.41*currentTemperature /
// failureTemperature), minJamChance, 1.0) compared against a uniform random.
Logical
CheckForJam();
// @004bc06c -- lead solution: project the target position by launchVelocity
// and return the time-of-flight (or minTimeOfFlight + bias when no target
// is in range).
Scalar
ComputeTimeOfFlight();
// --- cross-family isolation helpers (owning Mech subsystem roster +
// cockpit controls live in the mech/controls families). Declared
// here so the recovered bodies compile; the mech family wires the
// real roster/controls query. ---
int
OwnerSubsystemCount(Mech *owner) const; // Mech roster size
Subsystem *
OwnerSubsystem(Mech *owner, int index) const; // Mech roster lookup
Logical
LiveFireEnabled() const; // controls "live fire" flag
// Resolve the linked AmmoBin and pull one round (AmmoBin::FeedAmmo).
// Returns True when a round was dispensed; on a dry / not-ready / missing
// bin it raises the NoAmmo alarm and returns False. Shared by the
// ProjectileWeapon and MissileLauncher FireWeapon spawn paths.
Logical
ConsumeRound();
// True when the weapon has recovered its firing charge (recoil counts
// down toward 0 in DrawFiringCharge; 0 == ready) and is not mid-eject.
Logical
ReadyToFire() const
{ return (recoil <= 0.0f) && (ejectState != 0); }
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
public:
typedef ProjectileWeapon__SubsystemResource SubsystemResource;
ProjectileWeapon(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
);
~ProjectileWeapon();
static int
CreateStreamedSubsystem( // @004bc7cc
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 projectile-weapon class.
// Offsets are byte offsets into the shipped object (MechWeapon occupies
// everything below +0x3F0). Object size is 0x448.
//
protected:
// --- eject / reload cycle (attribute names from DAT_00512290) ---
Scalar totalTimeToEject; // @0x3F0 "TotalTimeToEject" init 3.0
Scalar timeToEject; // @0x3F4 "TimeToEject" countdown
Scalar percentOfEject; // @0x3F8 "PercentOfEject" 0..1 progress (HUD)
Scalar minJamChance; // @0x3FC resource MinJamChance (+0x1CC)
int ejectState; // @0x400 -1 = idle, 0 = ejecting (init -1)
Scalar minVoltagePercentToFire;// @0x404 resource MinVoltagePercentToFire (+0x1C8)
int tracerCounter; // @0x408 init 0 (best-effort: tracer cadence)
// --- launch / lead solution ---
Scalar minTimeOfFlight; // @0x40C resource MinTimeOfFlight (+0x1C4)
Vector3D launchVelocity; // @0x410 init {0,0,-effectiveRange/speed}
// (MissileLauncher overwrites w/ MuzzleVelocity)
int tracerModelHandle; // @0x41C spawned tracer/explosion model handle
Point3D leadPosition; // @0x420 computed target lead point (init origin)
Vector3D tracerOrigin; // @0x42C best-effort: runtime muzzle origin (sim scratch)
// --- ammunition ---
int tracerInterval; // @0x438 resource TracerInterval (+0x1BC)
AmmoBinConnection
ammoBinLink; // @0x43C embedded 0xC connection to the AmmoBin subsystem
friend void *BTWeaponAmmoBin(void *weapon); // panel ammo-bin bridge (AFC100-counter fix)
// (ctor @004bcbb0, dtor @004bcbcf, link vtable
// @00512424; resolved via the resource AmmoBin
// index at +0x1C0) -- LAST field, object ends 0x448
friend struct ProjectileWeaponLayoutCheck;
}; // sizeof == 0x448
struct ProjectileWeaponLayoutCheck {
static_assert(offsetof(ProjectileWeapon, totalTimeToEject) == 0x3F0, "ProjectileWeapon::totalTimeToEject @0x3F0 (MechWeapon must end 0x3F0)");
static_assert(offsetof(ProjectileWeapon, launchVelocity) == 0x410, "ProjectileWeapon::launchVelocity @0x410");
static_assert(offsetof(ProjectileWeapon, leadPosition) == 0x420, "ProjectileWeapon::leadPosition @0x420");
static_assert(offsetof(ProjectileWeapon, tracerInterval) == 0x438, "ProjectileWeapon::tracerInterval @0x438");
static_assert(offsetof(ProjectileWeapon, ammoBinLink) == 0x43C, "ProjectileWeapon::ammoBinLink @0x43C");
static_assert(sizeof(ProjectileWeapon) == 0x448, "sizeof(ProjectileWeapon) 0x448 (== factory alloc)");
};
#endif