Files
BT411/game/reconstructed/projweap.cpp
T
arcattackandClaude Opus 4.8 89ddd49283 Add [ammo] fire trace (BT_AMMO_LOG); reproduces the missile FailureHeat->NoAmmo brick
Playtest: "lost missiles mid-fight, no pips, sad noise -- out of ammo?"  The default
build didn't trace ammo, so it couldn't be answered from the log.  Added a [ammo]
trace (env BT_AMMO_LOG) at the four ProjectileWeaponSimulation decision points --
FIRED+rounds-left, gate1/gate2 dry-transition edge, dry-fire blip, and denial -- plus
a public AmmoBin::GetAmmoCount() const getter.

Reproduced the actual cause: NOT ammo depletion.  The SRM6 trips gate 1 FailureHeat
(heatLvl=2) after ~5 volleys and latches into the permanent NoAmmo roach-motel with
19 rounds still in the bin:
  [ammo] SRM6_2 FIRED, rounds left=19
  [ammo] SRM6_2 -> NoAmmo (gate1): destroyed=0 failHeat=1 heatLvl=2 mechDisabled=0
Logged as the concrete instance of the heat-economy open question (open-questions.md).
Trace-only; no behavior change (all output gated on BT_AMMO_LOG).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 21:59:46 -05:00

1098 lines
45 KiB
C++

//===========================================================================//
// File: projweap.cpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: ProjectileWeapon -- MechWeapon that spawns physical projectiles //
//---------------------------------------------------------------------------//
// 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. Behaviour follows the Ghidra pseudo-C
// for the module cluster @004bbae0-@004bcff0 (file=bt/projweap.cpp). Method and
// member names follow the surviving MISLANCH.HPP / PROJWEAP.TCP, the binary's own
// attribute strings (DAT_00512290) and resource tag strings, and the base
// mechweap.cpp. Each non-trivial method cites the originating @ADDR.
//
// CONFIDENCE
// confident (full body recovered & translated):
// ctor @004bc3fc, dtor @004bc688, deleting-dtor @004bcbcf,
// ammoBinLink ctor @004bcbb0, ReadUpdateRecord @004bbae0,
// ResetToInitialState @004bbaf8, HandleMessage @004bcabc,
// PrintState @004bc6c8, CreateStreamedSubsystem @004bc7cc,
// UpdateEject @004bbb50, DrawFiringCharge @004bbc78,
// CheckForJam @004bbfcc, ComputeTimeOfFlight @004bc06c,
// ProjectileWeaponSimulation @004bbd04 (Performance -- FULLY RECOVERED
// by capstone disasm, Gitea #12 2026-07-19; see context/decomp-reference.md
// s5 -- the old RivetGun-modeled body is retired).
// best-effort (vtable slot proven, function prologue present, BODY NOT
// recovered by the decompiler -- bodies below are reconstructed from context
// and clearly marked):
// FireWeapon @004bc104 (slot 18), GetStatusFlags @004bbf88 (slot 12),
// UpdateWeaponState @004bbc20 (slot 16).
// excluded (belong to sibling/derived classes, NOT ProjectileWeapon):
// Emitter family @004bb120/@004bb888/@004ba4d0-@004bb478 (energy weapons),
// MissileLauncher @004bcff0/@004bd060/@004bd08c & FireWeapon @004bcc60,
// AmmoBin (HeatWatcher-derived) @004bd5c4/@004bd6b0/@004bd6f0 and its
// helpers @004bd2c0-@004bd4f4.
//
// Resolved read-only constants (4 LE bytes from section_dump CODE):
// _DAT_004bc100 = 0000803f = 1.0f
// _DAT_004bc064 = 85ebd13e = 0.41f (heat->jam-chance coefficient)
// _DAT_004bc068 = 0000803f = 1.0f (jam-chance clamp ceiling)
// _DAT_004bb3b0 = 000080bf = -1.0f (resource "unset" sentinel)
// _DAT_004bb3b4 = 0000003f = 0.5f
// DAT_004e0f74 = {0,0,0} (zero vector)
// DAT_004e0fd4 = {0,0,0} (zero point)
// 0x40400000 = 3.0f (TotalTimeToEject default)
//
// Helper-function name mapping (engine internals referenced by the decomp):
// FUN_004b99a8 MechWeapon base constructor
// FUN_004b9b9c ~MechWeapon
// FUN_004b964c MechWeapon::TakeDamage (vtable slot 6, inherited)
// FUN_004b9690 MechWeapon::WriteUpdateRecord (slot 7, inherited)
// FUN_004add6c Subsystem::HandleMessage (slot 8 base)
// FUN_004b96d4 MechWeapon::ReadUpdateRecord (slot 9 base)
// FUN_004b96ec MechWeapon::ResetToInitialState (slot 10 base)
// FUN_004b9d00 MechWeapon::PrintState (slot 13 base)
// FUN_004b9948 MechWeapon::GetMuzzlePoint FUN_004b9bdc MechWeapon::UpdateTargeting
// FUN_004b9cbc MechWeapon::GetTargetPosition FUN_004b0d50 voltage helper
// FUN_004ad7d4 heat-model experience gate (player+0x260; HeatModelActive) FUN_00417ab4 Connection::Resolve
// FUN_0041bbd8 AlarmIndicator::SetLevel(n) FUN_0041db7c Damage init
// FUN_004179d4 / FUN_004179f8 SharedData::Connection ctor/dtor
// FUN_004022d0 operator delete FUN_004215b0 resolve subsystem name->index
// FUN_00408440 Vector copy FUN_0040a7f4 Point copy FUN_00408050 uniform random
// FUN_004040d8 Read(int/bool) FUN_00404088 Read(string) FUN_00404118 Read(scalar)
// FUN_004084fc vectors-close? FUN_00408944 parse vector
// FUN_004dbb24 DebugStream << FUN_004d9c38 DebugStream flush
// FUN_004b9d10 MechWeapon::CreateStreamedSubsystem
// FUN_004bd588 AmmoBin::request-eject/dry FUN_004bf8bc spawn projectile entity
//
#include <bt.hpp>
#pragma hdrstop
#if !defined(PROJWEAP_HPP)
# include <projweap.hpp>
#endif
#if !defined(AMMOBIN_HPP)
# include <ammobin.hpp>
#endif
#if !defined(PROJTILE_HPP)
# include <projtile.hpp>
#endif
#if !defined(TESTBT_HPP)
# include <testbt.hpp>
#endif
#include <math.h>
// WAVE 7 Phase B -- the port-side flying-projectile service (defined in mech4.cpp).
// SIGNATURE MUST MATCH mech4.cpp EXACTLY (the missile-arc wave added
// launch_velocity): a stale extern here mangles to a nonexistent symbol, /FORCE
// fills the call with garbage, and the first autocannon shot AVs (user-hit
// crash, 2026-07-12 -- gotcha #3).
extern void BTPushProjectile(const Point3D &muzzle, void *shooter, void *target,
const Point3D &targetPos, Scalar speed, Scalar damage,
const Vector3D *launch_velocity = 0, int guided = 1,
int weapon_subsys = -1, int splash_burst = 0, int muzzle_seg = -1); // AC: no cluster splash (default 0)
//#############################################################################
// FIRE-VISUAL REPLICATION STATE (task #61) -- the AC twin of MissileLauncher's
// BTSalvoState (mislanch.cpp). Port-side table keyed by the weapon pointer
// (the 0x1D0 object is byte-locked): the master counts its shots + remembers
// the aim; the replicant edge-detects the counter and mirrors ONE visual round
// + DAFC muzzle flash so the enemy's cannon is visible on the peer.
//#############################################################################
namespace {
struct BTAcFireState
{
const void *owner;
int fired; // master: shots fired
int seen; // replicant: last counter applied (-1 = unsynced)
Point3D target; // master: the last shot's aim point
};
BTAcFireState gAcFireTable[64];
BTAcFireState &BTAcFireOf(const void *weapon)
{
int freeSlot = -1;
for (int i = 0; i < 64; ++i)
{
if (gAcFireTable[i].owner == weapon)
return gAcFireTable[i];
if (gAcFireTable[i].owner == 0 && freeSlot < 0)
freeSlot = i;
}
BTAcFireState &s = gAcFireTable[(freeSlot >= 0) ? freeSlot : 0];
s.owner = weapon;
s.fired = 0;
s.seen = -1;
s.target = Point3D(0.0f, 0.0f, 0.0f);
return s;
}
}
//#############################################################################
// Resolved read-only constants + unrecovered-helper stand-ins (see banner).
//
static const Scalar _DAT_004bc100 = 1.0f; // time-of-flight bias
static const Scalar _DAT_004bc064 = 0.41f; // heat -> jam-chance coefficient
static const Scalar _DAT_004bc068 = 1.0f; // jam-chance clamp ceiling
static const Scalar _DAT_004bc678 = 1.0f; // muzzle-speed term (unresolved)
static const Scalar _DAT_004bc67c = 1.0f; // muzzle-speed term (unresolved)
// FUN_00408050 -- uniform [0,1) random (jam roll). The old `return 0.0f` stub ALWAYS
// jammed (0 < any positive jam chance), so a projectile weapon could never fire. A real
// pseudo-random (LCG) restores authentic behavior: it fires ~(1-jamChance) of the time.
static Scalar UniformRandom()
{
static unsigned s = 0x12345678u;
s = s * 1103515245u + 12345u;
return (Scalar)((s >> 16) & 0x7fff) / 32768.0f;
}
static int ResolveTracerModel(void*) { return 0; } // explosion/tracer model handle
static int ResolveSubsystemName(const ResourceDirectories*, const char*) { return -1; } // FUN_004215b0
//#############################################################################
// Shared Data Support
//
Derivation
ProjectileWeapon::ClassDerivations(
&MechWeapon::ClassDerivations,
"ProjectileWeapon" // DAT_00512280
);
// task #6: inherit MechWeapon's handler set (see emitter.cpp note).
Receiver::MessageHandlerSet&
ProjectileWeapon::GetMessageHandlers()
{
static Receiver::MessageHandlerSet messageHandlers(
MechWeapon::GetMessageHandlers()); // copy-inherit
return messageHandlers;
}
Simulation::AttributeIndexSet ProjectileWeapon::AttributeIndex;
ProjectileWeapon::SharedData
ProjectileWeapon::DefaultData(
&ProjectileWeapon::ClassDerivations,
ProjectileWeapon::GetMessageHandlers(),
MechWeapon::GetAttributeIndex(), // gauge wave: inherit temps/InputVoltage/OutputVoltage/PercentDone (was empty)
ProjectileWeapon::StateCount
);
//#############################################################################
// Test Class Support (PROJWEAP.TCP)
//
Logical
ProjectileWeapon::TestClass(Mech &)
{
return True;
}
Logical
ProjectileWeapon::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
//#############################################################################
// Cross-family isolation helpers (mech subsystem roster + cockpit controls).
// The owning Mech's subsystem roster and the cockpit "live fire" control flag
// are owned by the mech / controls families; these stubs let the recovered
// bodies compile and are wired up there.
//
int
ProjectileWeapon::OwnerSubsystemCount(Mech *owner) const
{
// Real roster (binary reads owner->subsystemCount @0x124). The AmmoBin (0xBCB)
// constructs before the projectile weapons (0xBCD/0xBD0) in the factory loop, so
// its roster slot is populated by the time this weapon's ctor resolves it.
return (owner != 0) ? owner->GetSubsystemCount() : 0;
}
Subsystem *
ProjectileWeapon::OwnerSubsystem(Mech *owner, int index) const
{
// Real roster lookup (binary: subsystemArray[index] @0x128).
return (owner != 0) ? owner->GetSubsystem(index) : 0;
}
Logical
ProjectileWeapon::LiveFireEnabled() const
{
// CheckForJam @004bbfcc's early-out [T1]: the owning BTPlayer's +0x25c
// "sim live" experience flag (mech+0x190 playerLink) -- 0 only for NOVICE
// experience, whose ballistics never jam. Issue #2: the old permissive
// `return True` stub let every tier roll jams; routed through the
// complete-type bridge in btplayer.cpp (databinding rule).
extern int BTPlayerExperienceSimLive(void *owner_mech); // btplayer.cpp (player+0x25c)
return BTPlayerExperienceSimLive(owner) ? True : False;
}
//
// Panel bridge (AFC100-counter fix, 2026-07-12): resolve the weapon's ammo
// bin through the TYPED connection -- the gauge cluster's hand-rolled raw walk
// (*(plug+8) twice at subsystem+0x43c) assumed the 1995 connection-object
// internals and returned NULL/garbage on our layout, so the panel's ammo
// digits never bound (fired shells consumed rounds invisibly).
//
// (Gitea #12: the old `ConsumeRound()` helper is RETIRED -- the binary has no
// such method. The ammo pull is `bin->FeedAmmo()` @004bd4f4 called by the
// SIMULATION's Loaded case @4bbee6, and a failed pull leaves the weapon Loaded
// SILENTLY -- the helper's SetLevel(NoAmmo) on failure was port invention; the
// authentic NoAmmo pin is gate 2 of the state machine, from the BIN's alarm.)
void *
BTWeaponAmmoBin(void *weapon)
{
return (weapon != 0)
? (void *)((ProjectileWeapon *)weapon)->ammoBinLink.Resolve() : 0;
}
//#############################################################################
// Construction / Destruction
//
// @004bc3fc -- chain to the MechWeapon ctor (@004b99a8, StateCount 8), install
// vtable PTR_FUN_0051242c, construct the embedded AmmoBin connection, copy the
// projectile parameters out of the resource record, build the launch-velocity
// seed from effectiveRange, prime the eject cycle, install the Performance
// simulation (unless the model is flagged non-simulated), and link to the
// AmmoBin subsystem named in the resource.
//
ProjectileWeapon::ProjectileWeapon(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
MechWeapon(owner, subsystem_ID, subsystem_resource, shared_data)
{
Check(owner);
Check_Pointer(subsystem_resource);
ammoBinLink.Construct(); // FUN_004bcbb0(this+0x43c, 0)
tracerInterval = subsystem_resource->tracerInterval; // +0x1BC -> 0x438
minTimeOfFlight = subsystem_resource->minTimeOfFlight; // +0x1C4 -> 0x40C
minJamChance = subsystem_resource->minJamChance; // +0x1CC -> 0x3FC
leadPosition = Point3D(0,0,0); // FUN_0040a7f4(this+0x420, DAT_004e0fd4)
minVoltagePercentToFire = subsystem_resource->minVoltagePercentToFire; // +0x1C8 -> 0x404
tracerCounter = 0; // 0x408
// launchVelocity seed = (0,0,0) then z = -(effectiveRange / muzzleSpeed)
launchVelocity = Vector3D(0,0,0); // FUN_00408440(this+0x410, DAT_004e0f74)
Scalar muzzleSpeed = (Scalar)Sqrt(_DAT_004bc67c * _DAT_004bc678); // FUN_004dd138
tracerOrigin = Vector3D(0,0,0); // FUN_00408440(this+0x42c, DAT_004e0f74)
launchVelocity.z = -(effectiveRange / muzzleSpeed); // this[0x106] = -(this[0xca]/speed)
totalTimeToEject = 3.0f; // 0x3F0 (0x40400000)
timeToEject = totalTimeToEject; // 0x3F4
percentOfEject = 0.0f; // 0x3F8
ejectState = -1; // 0x400 (idle)
// Install Performance unless the owning Mech is a non-authoritative ghost.
// @004bc3fc reads the OWNER's flags (param_2+0x28 == owner->simulationFlags),
// NOT the resource flags -- the same authoritative-simulation gate the AmmoBin
// uses (owner&0xC==0 && owner&0x100). This is what arms the per-frame tick on
// the bring-up's authoritative mech.
Logical installPerf = (((owner->simulationFlags & 0xC) == 0) && ((owner->simulationFlags & 0x100) != 0));
if (installPerf)
{
SetPerformance(&ProjectileWeapon::ProjectileWeaponSimulation); // member-ptr {0x4bbd04,..}
}
else
{
simulationFlags |= 0x2; // this[10] |= 2 (mark "not simulated")
}
if (getenv("BT_PROJ_LOG"))
DEBUG_STREAM << "[projweap] CTOR name=" << GetName() << " ownerFlags="
<< owner->simulationFlags << " installPerf=" << (int)installPerf << "\n" << std::flush;
// Link to the AmmoBin subsystem referenced by the resource and copy the
// initial display fields out of it. (The Mech subsystem-roster lookup lives
// in the mech family; isolated behind OwnerSubsystemCount/OwnerSubsystem.)
if (subsystem_resource->ammoBinIndex < OwnerSubsystemCount(owner)) // +0x124
{
Subsystem *roster_bin = OwnerSubsystem(owner, subsystem_resource->ammoBinIndex); // +0x128 table
ammoBinLink.Connect(roster_bin); // (**bin.vtbl[1])(...)
// CROSS-FAMILY (mech/ammobin): the shipped ctor also primed the bin's HUD
// display block (iVar1+0x1f0/0x1f4/0x1f8/0x210) from this weapon; wired in
// the AmmoBin family.
}
AmmoBin *bin = (AmmoBin*)ammoBinLink.Resolve(); // FUN_00417ab4(this+0x43c)
if (bin == 0)
{
DebugStream << GetName() << " Could not get ammo bin!" << endl; // DAT_005122c0
}
// Look up / ref-count the explosion (tracer) model for the round.
tracerModelHandle = ResolveTracerModel(bin); // FUN_00407064(...,bin+0x1e8,0xf) -> this[0x107]
Check_Fpu();
}
//
// @004bc688 -- destroy the embedded AmmoBin connection, chain to ~MechWeapon,
// and (if the deleting-dtor bit is set) free the object.
//
ProjectileWeapon::~ProjectileWeapon()
{
Check(this);
ammoBinLink.Destruct(); // FUN_004bcbcf(this+0x43c, 2)
// base chain (FUN_004b9b9c) handles the rest.
Check_Fpu();
}
//#############################################################################
// Subsystem virtual overrides
//
//
// @004bcabc -- slot 8. A "jam" event (message 2) latches the weapon alarm to
// the Jammed level; everything else defers to the Subsystem base handler.
//
Logical
ProjectileWeapon::HandleMessage(int message)
{
if (message == 2)
{
weaponAlarm.SetLevel(JammedState); // FUN_0041bbd8(this+0x350, 5)
return True;
}
return PoweredSubsystem::HandleMessage(message); // FUN_004add6c
}
//
// @004bbae0 -- slot 9 (TASK #51 RENAME: slot 9 is TakeDamage, not
// ReadUpdateRecord -- see the corrected slot map in mechweap.hpp).
// ProjectileWeapon adds nothing of its own; defers to the MechWeapon
// implementation (@004b96d4 chains @004b0efc = PoweredSubsystem::TakeDamage).
//
void
ProjectileWeapon::TakeDamage(Damage &damage)
{
MechWeapon::TakeDamage(damage); // FUN_004b96d4
}
//
// @004bbaf8 -- slot 10. Reset the firing/eject state, re-arm the alarm to the
// "Loading" level, restore the eject countdown, and clear the jam flag, then
// chain to MechWeapon. (PROJWEAP.TCP showed an empty early stub; this is the
// shipped body.)
//
void
ProjectileWeapon::ResetToInitialState()
{
MechWeapon::ResetToInitialState(); // FUN_004b96ec
weaponAlarm.SetLevel(3); // FUN_0041bbd8(this+0x350, 3) -- "Loading"
recoil = rechargeRate; // this[0xfa] = this[0xf7]
timeToEject = totalTimeToEject; // this[0xfd] = this[0xfc]
percentOfEject = 0.0f; // this[0xfe] = 0
ejectState = -1; // this[0x100] = -1
// @4bbb47: `or word ptr [this+0x18], 1` == updateModel |= 1 == the engine
// ForceUpdate() (replication mark). The old transcription onto
// `simulationFlags |= 0x1` set the engine DelayWatchersFlag instead --
// permanently muting this subsystem's audio watchers (gotcha #20, Gitea #12).
ForceUpdate();
}
//
// @004bbf88 -- slot 12 (GetStatusFlags). Body NOT recovered by the decompiler
// (function prologue present at 0x4bbf88). Overrides MechWeapon's
// PoweredSubsystem::GetStatusFlags (@004b0f48); best-effort: ORs the weapon's
// ammo/jam status bits onto the base flags.
//
LWord
ProjectileWeapon::GetStatusFlags()
{
// BEST-EFFORT -- body unrecovered. Defer to base until a human disassembles
// 0x4bbf88.
return MechWeapon::GetStatusFlags();
}
//
// @004bc6c8 -- slot 13. Print the MechWeapon state, then append the
// ammo-specific weapon-alarm state.
//
void
ProjectileWeapon::PrintState()
{
MechWeapon::PrintState(); // FUN_004b9d00
switch (weaponAlarm.GetState()) // *(this+0x364)
{
case JammedState: DebugStream << GetName() << " = Jammed"; break; // 5
case TriggerDuringJamState: DebugStream << GetName() << " = TriggerDuringJam"; break; // 6
case NoAmmoState: DebugStream << GetName() << " = NoAmmo"; break; // 7
default: DebugStream << GetName() << "Unknown weapon state!"; break;
}
DebugStream.Flush();
}
//
// @004bbc20 -- slot 16, MechWeapon-family per-frame hook (overrides MechWeapon
// @004b0b5c). Body NOT recovered (prologue present at 0x4bbc20).
//
void
ProjectileWeapon::UpdateWeaponState()
{
// BEST-EFFORT -- body unrecovered. TODO: disassemble 0x4bbc20.
}
//#############################################################################
// Internal model helpers
//
//
// @004bbb50 -- advance the eject/reload cycle. While ejecting (ejectState==0)
// run the TimeToEject countdown (clamped >= 0) and recompute PercentOfEject;
// when the countdown reaches ~0 ask the AmmoBin for the next round, raising the
// NoAmmo alarm and parking the weapon if the bin is dry.
//
void
ProjectileWeapon::UpdateEject(Scalar time_slice)
{
if (ejectState == 0)
{
timeToEject -= time_slice; // this+0x3f4
if (timeToEject < 0.0f) // _DAT_004bbc18 == 0.0f
timeToEject = 0.0f;
percentOfEject = (totalTimeToEject - timeToEject) / totalTimeToEject; // this+0x3f8
}
if (Close_Enough(timeToEject, 0.0f)) // FUN_004dcd00 .. _DAT_004bbc1c
{
AmmoBin *bin = (AmmoBin*)ammoBinLink.Resolve(); // FUN_00417ab4(this+0x43c)
if (bin != 0)
{
bin->DumpAmmo(); // FUN_004bd588
weaponAlarm.SetLevel(NoAmmoState); // FUN_0041bbd8(this+0x350, 7)
}
ejectState = -1; // this+0x400 = -1 (done)
}
}
//
// @004bbc78 -- bleed the firing charge (MechWeapon recoil @0x3E8) at a rate set
// by the available bus voltage and the AmmoBin's remaining supply ratio.
//
void
ProjectileWeapon::DrawFiringCharge(Scalar time_slice)
{
// bus voltage scale (FUN_004b0d50) -- clamp to >= 1.0
Scalar voltage = 1.0f;
if (voltage < 1.0f) // _DAT_004bbd00 == 1.0f
voltage = 1.0f;
// available supply ratio from the linked voltage source. The shipped code
// read source->outputVoltage / source->ratedVoltage off the resolved Generator
// segment; here we use the inherited PoweredSubsystem voltageScale, which is
// that same normalised available-voltage figure.
Scalar supply = voltageScale; // (+0x1dc)/(+0x1d8)
if (supply < minVoltagePercentToFire) // this+0x404
supply = 0.0f;
recoil -= (time_slice * supply) / voltage; // *(this+0x3e8)
}
//
// @004bbfcc -- roll a heat-scaled jam chance. Returns True (jam!) when a
// uniform random sample falls under the current jam probability:
// p = clamp(0.41 * currentTemperature / failureTemperature,
// minJamChance, 1.0)
// Gated on the weapon being heat-active (this+0x184) and the owning PLAYER's
// "sim live" experience flag (mech+0x190 playerLink -> player+0x25c; novice
// never jams -- issue #2).
//
Logical
ProjectileWeapon::CheckForJam()
{
if (getenv("BT_JAM_LOG"))
DEBUG_STREAM << "[jam] " << GetName()
<< " T=" << currentTemperature
<< " degT=" << degradationTemperature
<< " failT=" << failureTemperature
<< " alarm=" << (int)heatAlarm.GetLevel()
<< " liveFire=" << (int)LiveFireEnabled()
<< " p=" << ((_DAT_004bc064 * currentTemperature) / failureTemperature)
<< "\n" << std::flush;
if (!LiveFireEnabled()) // *(*(this+0xd0)+400)+0x25c
return False;
// BRING-UP: the reconstructed heat model holds currentTemperature at a constant ~77
// (thermalMass is huge, so temp doesn't yet rise/fall with fire), which makes this
// heat-scaled jam roll fire spuriously EVERY shot -- and since JammedState clears only
// on ResetToInitialState, a weapon that jams STAYS jammed for the whole mission (you'd
// fire one salvo, then only lasers). The authentic rule is "jam risk scales with heat",
// so gate the roll on the weapon actually being in a degraded HEAT STATE (heatAlarm
// level >= 1) instead of the raw heatLoad. In bring-up the alarm stays 0 (temp <<
// degradation threshold) -> weapons fire reliably; real jams return once the heat model
// drives true temps. (FOLLOW-UP: make JammedState recoverable via an unjam delay so
// even a genuine overheat jam clears.)
if (heatAlarm.GetLevel() < HeatSink::DegradationHeat) // was: if (heatLoad <= 0.0f)
return False;
Scalar p = (_DAT_004bc064 * currentTemperature) / failureTemperature; // 0.41 * temp/failTemp
if (minJamChance <= p) // this+0x3fc
{
if (_DAT_004bc068 < p) // clamp to 1.0
p = _DAT_004bc068;
}
else
{
p = minJamChance;
}
return (p > UniformRandom()) ? True : False; // FUN_00408050
}
//
// @004bc06c -- lead-solution time-of-flight. Sample the current target
// position, advance it by launchVelocity, and re-run targeting. With no target
// in range, return minTimeOfFlight plus a small bias; otherwise return
// rangeToTarget / projectileSpeed.
//
Scalar
ProjectileWeapon::ComputeTimeOfFlight()
{
GetTargetPosition(leadPosition); // FUN_004b9cbc(this, this+0x420)
leadPosition.x += launchVelocity.x; // this+0x420 += this+0x410
leadPosition.y += launchVelocity.y;
leadPosition.z = launchVelocity.z; // (z copied, not accumulated)
if (!UpdateTargeting()) // FUN_004b9bdc -- target in range?
{
return minTimeOfFlight + _DAT_004bc100; // +0x40c + 1.0
}
Scalar speed = Sqrt( // FUN_004dd138
leadPosition.z * leadPosition.z +
leadPosition.y * leadPosition.y +
leadPosition.x * leadPosition.x);
return rangeToTarget / speed; // this+0x324 / speed
}
//#############################################################################
// Simulation / Firing
//
//
// @004bbd04 -- Performance: per-frame ProjectileWeapon update. FULLY RECOVERED
// (capstone disasm via tools/disas2.py, Gitea #12, 2026-07-19 -- every branch
// below cites its binary address). The old RivetGun-modeled body is retired;
// its divergences were the #12 incident mechanics: denied shots faked a full
// Loaded->Firing->Loading pip cycle (binary: a 4-blip that STAYS Loaded, no
// ammo pull), the fault gate read simulationFlags (binary: simulationState),
// recoil bled every frame unconditionally and ran to -114 (binary: Loading-only
// + clamp at 0), and slot 17 (the recharge-dial writer) was never called.
//
void
ProjectileWeapon::ProjectileWeaponSimulation(Scalar time_slice)
{
Check(this);
// @4bbd12: the PoweredSubsystem step FIRST (task #10) -- HeatSink absorb ->
// temperature -> conduction, and the electrical state machine.
PoweredSubsystem::PoweredSubsystemSimulation(time_slice); // call 0x4b0bd0
// @4bbd1b: the trigger edge is sampled ONCE, before the gates ([ebp-4]).
// AUTHENTIC TRIGGER (task #5): fireImpulse = the TriggerState attribute the
// streamed controls map binds to the pod fire buttons.
Logical trigger = CheckFireEdge(); // call 0x4b9608
// @4bbd28: advance any in-progress magazine eject.
UpdateEject(time_slice); // call 0x4bbb50
// [ammo] fire trace (env BT_AMMO_LOG): surfaces the round count + the exact
// dry transition / dry-fire blip / denial, so "did I run out of missiles?" is
// answerable from the log instead of inferred. Cached once.
static int s_ammoLog = -1;
if (s_ammoLog < 0) s_ammoLog = getenv("BT_AMMO_LOG") ? 1 : 0;
// GATE 1 @4bbd36-4bbd6e: weapon DESTROYED (`simulationState@0x40 == 1` --
// NOT simulationFlags, the historic mis-transcription of gotcha #20), own
// heatAlarm at FailureHeat (this+0x184 == 2 -- THE consumer of the authored
// FailureTemperature for the ballistic family), or the owning mech disabled
// (FUN_0049fb54) -> pin recoil at the full rechargeRate + latch alarm 7.
// No return: the frame continues into the state machine.
{
int gate1Destroyed = (simulationState == 1);
int gate1FailHeat = (heatAlarm.GetLevel() == HeatSink::FailureHeat);
int gate1Disabled = (owner != 0 && owner->IsDerivedFrom(*Mech::GetClassDerivations())
&& ((Mech *)owner)->IsDisabled());
if (gate1Destroyed || gate1FailHeat || gate1Disabled)
{
// Edge log: the FIRST frame gate 1 bricks the weapon into the NoAmmo
// roach-motel (destroyed / FailureHeat / mech disabled) -- distinguishes
// a true dry bin (gate 2) from a HEAT trip (the open heat-economy question).
if (s_ammoLog && weaponAlarm.GetState() != 7)
DEBUG_STREAM << "[ammo] " << GetName() << " -> NoAmmo (gate1): destroyed="
<< gate1Destroyed << " failHeat=" << gate1FailHeat
<< " heatLvl=" << heatAlarm.GetLevel()
<< " mechDisabled=" << gate1Disabled << "\n" << std::flush;
recoil = rechargeRate; // [this+0x3E8] = [this+0x3DC]
weaponAlarm.SetLevel(7); // call 0x41bbd8(this+0x350, 7)
}
}
// GATE 2 @4bbd71-4bbda6: the linked AmmoBin. Feed alarm Empty(2)/level-3 or
// bin destroyed -> re-pin alarm 7 EVERY FRAME while dry. (The binary
// dereferences the resolved bin UNGUARDED @4bbd80; the port keeps a null
// guard -- shipped content always links a bin, the ctor logs when it can't.)
AmmoBin *bin = (AmmoBin *)ammoBinLink.Resolve(); // call 0x417ab4(this+0x43c)
if (bin != 0
&& (bin->GetAmmoState() == 2 || bin->GetAmmoState() == 3 // [bin+0x1A8]
|| bin->GetSimulationState() == 1)) // [bin+0x40] (destroyed)
{
// Edge: log the FIRST frame the weapon is pinned dry (not the per-frame re-pin).
if (s_ammoLog && weaponAlarm.GetState() != 7)
DEBUG_STREAM << "[ammo] " << GetName() << " -> NoAmmo (gate2): rounds="
<< bin->GetAmmoCount() << " binState=" << bin->GetAmmoState()
<< " binDestroyed=" << (int)(bin->GetSimulationState() == 1)
<< "\n" << std::flush;
weaponAlarm.SetLevel(7);
}
static int s_pwtick = 0;
if (getenv("BT_PROJ_LOG") && (((++s_pwtick) % 240) == 0 || trigger))
DEBUG_STREAM << "[projweap] tick " << GetName() << " trig=" << (int)trigger
<< " trigState=" << (float)fireImpulse
<< " state=" << weaponAlarm.GetState() << " recoil=" << recoil
<< " binState=" << (bin != 0 ? bin->GetAmmoState() : -1) << "\n" << std::flush;
// THE FIRING STATE MACHINE @4bbda9: switch on weaponAlarm state ([this+0x364]).
// Only states 2/3/5/7 carry a case body; 0/1/4/6 are transient audio blips.
switch (weaponAlarm.GetState())
{
case 2: // Loaded @4bbec2 -- armed; everything happens on the trigger edge
if (trigger)
{
if (viewFireEnable != 0 && HasActiveTarget()) // [this+0x3E0] && [owner+0x388]
{
// @4bbee6: the ammo pull happens HERE, in the caller -- a failed
// pull (bin feeding/not-ready) just stays Loaded, silently.
if (bin != 0 && bin->FeedAmmo() != 0) // call 0x4bd4f4(bin)
{
weaponAlarm.SetLevel(0); // @4bbef4 -> Firing
ForceUpdate(); // @4bbf05 `or word [this+0x18],1` (updateModel)
FireWeapon(); // @4bbf0d vtbl+0x48 (slot 18)
if (s_ammoLog)
DEBUG_STREAM << "[ammo] " << GetName() << " FIRED, rounds left="
<< bin->GetAmmoCount()
<< (bin->GetAmmoState() == 2 ? " (bin EMPTY -> NoAmmo)" : "")
<< "\n" << std::flush;
if (bin->GetAmmoState() == 2) // @4bbf11 bin went Empty on the pull
weaponAlarm.SetLevel(7); // -> unavailable (gate 2 re-pins it)
else if (CheckForJam()) // @4bbf2a call 0x4bbfcc (heat-scaled roll)
weaponAlarm.SetLevel(JammedState); // @4bbf34 -> 5
else
weaponAlarm.SetLevel(3); // @4bbf41 -> Loading (reload)
ForceUpdate(); // @4bbf4c second updateModel mark
recoil = rechargeRate; // @4bbf51 begin the recharge cooldown
}
else if (s_ammoLog)
DEBUG_STREAM << "[ammo] " << GetName()
<< " trigger but FeedAmmo not ready: rounds="
<< (bin != 0 ? bin->GetAmmoCount() : -1)
<< " binState=" << (bin != 0 ? bin->GetAmmoState() : -1)
<< "\n" << std::flush;
}
else
{
if (s_ammoLog)
DEBUG_STREAM << "[ammo] " << GetName()
<< " DENIED (no target / view gate): viewFire=" << (int)viewFireEnable
<< " hasTarget=" << (int)HasActiveTarget()
<< " rounds=" << (bin != 0 ? bin->GetAmmoCount() : -1)
<< " (pips stay, no ammo pulled)\n" << std::flush;
// @4bbf5f THE DENIAL BLIP (Gitea #12 fix): a shot denied by the
// look-view gate or by having NO TARGET does SetLevel(4);
// SetLevel(2) -- a one-frame audio blip, the pip STAYS LOADED and
// NO ammo is pulled. (The old port routed these gates through
// FireWeapon early-returns while the caller cycled the full
// Firing->Loading alarm anyway -- a denied missile shot faked a
// perfect firing cycle and launched nothing: the reported
// "missiles cycle but never launch" phenomenology, verbatim.)
weaponAlarm.SetLevel(4);
weaponAlarm.SetLevel(2);
}
}
break;
case 3: // Loading @4bbdd2 -- recharging toward Loaded
if (trigger)
{
weaponAlarm.SetLevel(4); // @4bbdd9 TriggerDuringLoad blip
weaponAlarm.SetLevel(3); // @4bbdea back to Loading
}
if (electricalStateAlarm.GetLevel() == 4) // @4bbdf5 [this+0x278] == Ready
{
// @4bbe04: recoil bleeds ONLY here (Loading + electrical Ready) --
// the old port bled it unconditionally every frame with no clamp,
// running it to -114 across a mission.
DrawFiringCharge(time_slice); // call 0x4bbc78
if (recoil < 0.0f) // @4bbe0c fcomp 0.0 (_DAT_004bbf84)
{
recoil = 0.0f; // @4bbe1d clamp AT zero
if (bin != 0 && bin->GetAmmoState() == 1) // @4bbe25 bin Loaded (round chambered)
weaponAlarm.SetLevel(2); // @4bbe30 -> Loaded
}
}
ComputeOutputVoltage(); // @4bbe44 vtbl+0x44 (slot 17 @4b9c9c):
// rechargeLevel = (rate - recoil)/rate
// -- the recharge dial ANIMATES
break;
case 5: // Jammed @4bbe8e -- cleared only by ResetToInitialState / msg 2
if (trigger)
{
weaponAlarm.SetLevel(TriggerDuringJamState); // @4bbe95 blip 6
weaponAlarm.SetLevel(JammedState); // @4bbea6 back to 5
}
recoil = rechargeRate; // @4bbeb1 held at full recoil
break;
case 7: // unavailable/NoAmmo @4bbe4d -- THE ROACH MOTEL (binary-faithful:
// the case re-asserts 7 UNCONDITIONALLY; nothing in the machine
// ever leaves it. Entered on: weapon destroyed, FailureHeat, mech
// disabled (gate 1) or bin empty/destroyed (gate 2). Recovery
// paths that EXIST in the binary: ResetToInitialState (slot 10,
// mission reset) and an inbound update record overwriting the
// alarm on a replicant. A launcher that trips FailureHeat
// mid-mission stays dead until mission end -- harsh but authentic;
// whether FailureHeat is reachable at authentic play intensity is
// the open heat-economy question (open-questions.md, MP section).
if (trigger)
{
if (s_ammoLog)
DEBUG_STREAM << "[ammo] " << GetName()
<< " DRY-FIRE blip (NoAmmo roach-motel): rounds="
<< (bin != 0 ? bin->GetAmmoCount() : -1)
<< " binState=" << (bin != 0 ? bin->GetAmmoState() : -1)
<< "\n" << std::flush;
weaponAlarm.SetLevel(1); // @4bbe54 dry-trigger blip
}
weaponAlarm.SetLevel(7); // @4bbe65 re-assert
recoil = rechargeRate; // @4bbe76
ComputeOutputVoltage(); // @4bbe85 vtbl+0x44 -- dial pinned at 0
break;
default: // 0/1/4/6 @4bbdcd: no case body (transient audio-blip states)
break;
}
Check_Fpu();
}
//
// @004bc104 -- slot 18 FireWeapon (overrides MechWeapon's pure-virtual trap
// @004ba45c). Body NOT recovered by the decompiler (prologue confirmed at
// 0x4bc104, immediately following the 1.0f literal @0x4bc100). Spawns the
// generic projectile/tracer. MissileLauncher overrides this again at @004bcc60
// to launch Missile entities.
//
// Gitea #12: the view/target gate, the ammo pull and the recoil set are
// STRIPPED from this body -- they belong to the CALLER (the recovered
// ProjectileWeaponSimulation Loaded case @4bbec2/@4bbee6/@4bbf51; the sibling
// @004bcc60 confirms a FireWeapon body is heat + spawn ONLY). The old
// early-returns here, combined with the caller's unconditional alarm cycling,
// were the "denied shot fakes a full firing cycle" incident mechanic.
//
void
ProjectileWeapon::FireWeapon()
{
Check(this);
// Refresh the lead solution (leadPosition / targeting) for the spawn below
// (@004bc06c; the recovered sim no longer calls it -- best-effort placement
// inside the unrecovered @4bc104 body, which is where the lead data is used).
ComputeTimeOfFlight(); // @004bc06c
// THE FIRING HEAT (task #9; was "a separate pre-existing gap"): the
// binary adds heatCostToFire RAW to the weapon's own pendingHeat --
// projectile resources author PRE-scaled 1e7-unit heat (AFC100 = 6.5e7)
// -- gated on the heatable check (part_013.c:8744-8747 idiom).
if (HeatModelActive()) // FUN_004ad7d4 (heat.hpp -> player+0x260 experience gate; issue #2)
{
pendingHeat += heatCostToFire; // HeatSink @0x1C8, raw
}
// Binary @4bc136-4bc19c (task #56): the firing RECOIL kick. Gate: per-shot
// damage > 3.0f (@0x4bc3f4) && the owner mech has a gyro. Direction
// (0, 0.6, -1.5) (0x3f19999a/0xbfc00000), magnitude = damageAmount / 16
// (@0x4bc3f8).
if (damageData.damageAmount > 3.0f && owner != 0)
{
extern void GyroApplyDamageImpulse(Subsystem *, Scalar, Scalar, Scalar, Scalar);
Subsystem *g = ((Mech *)owner)->GetGyroSubsystem(); // mech+0x528
if (g != 0)
GyroApplyDamageImpulse(g, 0.0f, 0.6f, -1.5f,
damageData.damageAmount * 0.0625f);
}
// Resolve the live muzzle.
Point3D muzzle;
GetMuzzlePoint(muzzle); // MechWeapon @004b9948
// GENUINE MUZZLE FLASH (task #61): the shipped DAFC fire-smoke blast at the
// gun-port -- BTDPL.INI psfx 6/14 "the effect used on all projectile guns".
// Fires on every AC shot (all ACs author TracerInterval=1); one ~0.2s
// auto-expiring burst. Default ON (BT_MUZZLE=0 disables for diagnostics).
{
const char *mz = getenv("BT_MUZZLE"); // default ON; =0 disables
if (mz == 0 || mz[0] != '0')
{
extern void BTFlashMuzzle(void *ownerMech, int seg_index, float mx, float my, float mz);
BTFlashMuzzle(owner, GetSegmentIndex(),
(float)muzzle.x, (float)muzzle.y, (float)muzzle.z);
}
}
// WAVE 7 Phase B: launch one flying ballistic round toward the owner's target (port
// reconstruction; the byte-exact world-entity Projectile is blocked by the 2007 engine
// Entity base mismatch -- see BTPushProjectile / mech4.cpp). Speed = |launchVelocity|.
char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech)
// The owner's target slots (raw 0x388/0x37c -- the same raw offsets mech4's
// targeting block writes; a consistent producer/consumer pair on our
// compiled object). Task #41: the slots hold the WORLD PICK -- the enemy
// mech under the boresight, or the terrain downrange (a missile fired at
// the scenery flies to the ground point and detonates without damage), or
// null at the sky (BTPushProjectile refuses; the weapon's own 0x388 gate
// wouldn't have fired either).
void *target = (o != 0) ? *(void **)(o + 0x388) : 0;
Point3D targetPos = (o != 0) ? *(Point3D *)(o + 0x37c) : muzzle;
Scalar speed = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
+ launchVelocity.y*launchVelocity.y
+ launchVelocity.z*launchVelocity.z);
BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount,
0 /*aim straight at the pick*/, 0 /*BALLISTIC: no seeker on a shell*/,
subsystemID /*messmgr explosion bundling at impact (task #7)*/);
// task #61: mark this shot for REPLICATION so the peer sees the enemy's
// cannon. ++fireCounter; the CALLER's Loaded case makes the two
// `updateModel |= 1` marks (@4bbf05/@4bbf4c == ForceUpdate) that serialize
// this weapon's record (WriteUpdateRecord) to the peer's replicant.
// Gitea #12 / gotcha #20: the old extra `simulationFlags |= 0x1` here set
// the engine DelayWatchersFlag -- permanently muting this weapon's audio
// watchers after the first shot; removed.
{
BTAcFireState &fs = BTAcFireOf(this);
++fs.fired;
fs.target = targetPos;
}
Check_Fpu();
}
//#############################################################################
// FIRE-VISUAL REPLICATION (task #61) -- the AC twin of MissileLauncher's salvo
// mirror. Master serializes the fire counter + aim after the MechWeapon base
// fields; the replicant edge-detects the counter and mirrors ONE visual round
// + DAFC muzzle flash from its OWN resolved muzzle -- so the enemy's autocannon
// is visible on the peer (the port's local flying round otherwise never left
// the firing node).
//#############################################################################
void
ProjectileWeapon::WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model)
{
MechWeapon::WriteUpdateRecord(message, update_model); // @004b9690 (alarm fields + subsystemID)
ProjectileWeapon__UpdateRecord *rec = (ProjectileWeapon__UpdateRecord *)message;
BTAcFireState &s = BTAcFireOf(this);
rec->recordLength = sizeof(ProjectileWeapon__UpdateRecord);
rec->fireCounter = s.fired;
rec->fireTarget = s.target;
}
void
ProjectileWeapon::ReadUpdateRecord(Simulation__UpdateRecord *message)
{
MechWeapon::ReadUpdateRecord(message); // @004b964c (alarm apply)
ProjectileWeapon__UpdateRecord *rec = (ProjectileWeapon__UpdateRecord *)message;
BTAcFireState &s = BTAcFireOf(this);
if (s.seen < 0)
{
s.seen = rec->fireCounter; // first sync: adopt silently
return;
}
if (rec->fireCounter == s.seen)
return;
s.seen = rec->fireCounter;
// Mirror ONE shot (a missed record collapses to one -- no phantom replays):
// a VISUAL-ONLY round (damage 0; the master's round delivers cross-pod
// damage) from this replicant's own muzzle toward the master's aim point,
// plus the DAFC muzzle flash.
Point3D mz;
GetMuzzlePoint(mz); // @004b9948
Scalar spd = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
+ launchVelocity.y*launchVelocity.y
+ launchVelocity.z*launchVelocity.z);
// Fly toward the master's aim if it replicated a real pick; else straight
// out the barrel (the shooter-frame launchVelocity, as the missile mirror
// does) so an untargeted shot still streaks forward, not toward the origin.
const int haveAim = (rec->fireTarget.x != 0.0f
|| rec->fireTarget.y != 0.0f
|| rec->fireTarget.z != 0.0f);
BTPushProjectile(mz, owner, 0 /*aim point only, no entity*/, rec->fireTarget,
spd, 0.0f /*VISUAL*/,
haveAim ? 0 : &launchVelocity, 0 /*ballistic*/, subsystemID,
0, GetSegmentIndex() /*task #67 mount frame*/);
const char *mzf = getenv("BT_MUZZLE"); // default ON; =0 disables
if (mzf == 0 || mzf[0] != '0')
{
extern void BTFlashMuzzle(void *ownerMech, int seg_index, float, float, float);
BTFlashMuzzle(owner, GetSegmentIndex(), (float)mz.x, (float)mz.y, (float)mz.z);
}
if (getenv("BT_PROJ_LOG"))
DEBUG_STREAM << "[projectile] REPLICANT AC shot at(" << rec->fireTarget.x
<< "," << rec->fireTarget.y << "," << rec->fireTarget.z << ")\n" << std::flush;
}
//#############################################################################
// CreateStreamedSubsystem
//
// @004bc7cc -- parse the ProjectileWeapon-specific resource fields after the
// MechWeapon base parser. Stamps subsystemModelSize = 0x1D0. (The classID
// stamp here equals the MechWeapon class id 0xBCD -- ProjectileWeapon is an
// abstract base with no separately-registered ClassID; the concrete
// MissileLauncher::CreateStreamedSubsystem @004bd08c overwrites the stamp with
// 0x1E0 / classID 0xBD0.)
//
int
ProjectileWeapon::CreateStreamedSubsystem(
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
)
{
if (
!MechWeapon::CreateStreamedSubsystem( // FUN_004b9d10
resource_file, model_file, model_name, subsystem_name,
subsystem_resource, subsystem_file, directories, passes
)
)
{
return False;
}
subsystem_resource->subsystemModelSize = sizeof(*subsystem_resource); // 0x1D0
subsystem_resource->classID = (RegisteredClass::ClassID)Mech::MechWeaponClassID; // 0xBCD (abstract)
if (passes == 1)
{
// prime all projectile fields to "unset"
subsystem_resource->tracerInterval = -1; // +0x1BC
subsystem_resource->ammoBinIndex = -1; // +0x1C0
subsystem_resource->minTimeOfFlight = -1.0f; // +0x1C4
subsystem_resource->minVoltagePercentToFire = -1.0f; // +0x1C8
subsystem_resource->minJamChance = -1.0f; // +0x1CC
}
if (
!model_file->GetEntry(subsystem_name, "TracerInterval", &subsystem_resource->tracerInterval)
&& subsystem_resource->tracerInterval == -1
)
{
DebugStream << subsystem_name << " missing TracerInterval!" << endl;
return False;
}
//
// AmmoBin: a subsystem name; "Unspecified" leaves the resource value,
// otherwise it is resolved to a subsystem index (+2 bias applied when not
// "Unspecified").
//
const char *ammoBinName = "Unspecified";
if (
!model_file->GetEntry(subsystem_name, "AmmoBin", &ammoBinName)
&& subsystem_resource->ammoBinIndex == -1
)
{
DebugStream << subsystem_name << " missing AmmoBin!" << endl;
return False;
}
if (strcmp(ammoBinName, "Unspecified") != 0)
{
subsystem_resource->ammoBinIndex = ResolveSubsystemName(directories, ammoBinName); // FUN_004215b0
}
if (subsystem_resource->ammoBinIndex < 0)
{
DebugStream << subsystem_name << " has an invalid Ammo Bin!" << endl;
return False;
}
if (strcmp(ammoBinName, "Unspecified") != 0)
{
subsystem_resource->ammoBinIndex += 2; // segment-table +2 bias
}
if (
!model_file->GetEntry(subsystem_name, "MinTimeOfFlight", &subsystem_resource->minTimeOfFlight)
&& subsystem_resource->minTimeOfFlight == -1.0f // _DAT_004bca9c
)
{
DebugStream << subsystem_name << " missing MinTimeOfFlight !" << endl;
return False;
}
if (
!model_file->GetEntry(subsystem_name, "MinJamChance", &subsystem_resource->minJamChance)
&& subsystem_resource->minJamChance == -1.0f
)
{
DebugStream << subsystem_name << " missing MinJamChance !" << endl;
return False;
}
if (
!model_file->GetEntry(subsystem_name, "MinVoltagePercentToFire", &subsystem_resource->minVoltagePercentToFire)
&& subsystem_resource->minVoltagePercentToFire == -1.0f
)
{
DebugStream << subsystem_name << " missing MinVoltagePercentToFire !" << endl;
return False;
}
return True;
}
//===========================================================================//
// WAVE 7 factory bridge -- ProjectileWeapon (factory case 0xBCD, "MechWeapon"
// mislabel). The real class at 0xBCD (ctor @004bc3fc) is ProjectileWeapon
// (the autocannon/ballistic ammo weapon); the factory built the base MechWeapon
// stub (whose FireWeapon is the pure-virtual trap @004ba45c). alloc 0x448 (==
// the byte-exact ProjectileWeapon sizeof). Constructs the AmmoBin connection +
// ticks its real ProjectileWeaponSimulation (charge/ammo/jam/eject); FireWeapon
// consumes a round + recoils but DEFERS the projectile spawn (Phase B).
//===========================================================================//
Subsystem *CreateProjectileWeaponSubsystem(Mech *owner, int id, void *seg)
{
Check(sizeof(ProjectileWeapon) <= 0x448);
return (Subsystem *) new (Memory::Allocate(0x448))
ProjectileWeapon(owner, id, (ProjectileWeapon::SubsystemResource *)seg, ProjectileWeapon::DefaultData);
}