An 8-agent decomp-mapping workflow mapped every weapon-family ctor + fire/spawn path
(scratchpad/wave7_maps.txt). KEY IDENTITY CORRECTION via VDATA.h enum (base 3000=0xBB8):
the factory ctor-address comments were right but the built CLASS names were stubs/base --
0xBCD == ProjectileWeapon (was building the base MechWeapon stub)
0xBCE == GaussRifle (NOT MissileLauncher -- was a MissileLauncher stub)
0xBD0 == MissileLauncher (NOT BallisticWeapon -- was a BallisticWeapon stub)
Un-mislabeled + wired via Create<Class>Subsystem bridges (mirroring CreateEmitterSubsystem),
each static_assert-locked byte-exact on the now-locked MechWeapon(0x3F0)/Emitter(0x478) bases:
* ProjectileWeapon (0xBCD, @4bc3fc : MechWeapon, sizeof 0x448)
FIX: AmmoBinConnection was an empty 1-byte struct -> retyped to the binary's 0xC
SharedData::Connection (was making the object 8 bytes short, sliding
MissileLauncher's missileCount off 0x448).
* MissileLauncher (0xBD0, @4bcff0 : ProjectileWeapon, sizeof 0x44C)
FIX: deleted muzzleVelocity (ALIAS of inherited ProjectileWeapon launchVelocity@0x410)
and salvoCount (SHADOW of inherited MechWeapon damageData.burstCount@0x3d4),
keeping the single own field missileCount@0x448.
* GaussRifle (0xBCE, @4bdcb4 : Emitter, sizeof 0x484 = Emitter + Vector3D muzzleVelocity)
DEDICATED bridge (not CreateEmitter): its FireWeapon is a no-op (mov [eax+0x414],0) --
a non-functional weapon in this 1995 build. Also fixed GAUSS.CPP's discharge write
rechargeLevel -> currentLevel@0x414 (the binary writes this+0x414).
Phase A safe-stubs FireWeapon (consume round + recoil, NO spawn): the reconstructed
Projectile (@4be1bc, 0x340) + Missile (@4bf5b4, 0x368) ENTITY classes are NOT spawn-ready
(Entity-base phantom members overflow their New() alloc -> heap corruption; New() takes
(int)this instead of the 0xD4 descriptor; Entity base size unconfirmed 2007-vs-1995, the
Mech 0x638-vs-0x854 problem). The workflow's adversarial verify flagged the live spawn as a
heap-overflow hazard and recommended exactly this phasing. Phase B = the entity byte-exactness
+ descriptor build + New(MakeMessage*) so a fired shot becomes a flying entity that damages.
Factory now 18 of 20 cases wired to real ticking classes (remaining: SubsystemMessageManager
0xBD3 [WAVE 8], Gyroscope 0xBC4 [deferred]). Verified: Mad Cat (LRM/ballistic) + BLH construct
the real weapon classes + tick authentic charge/ammo/heat/recoil, DESTROYED-in-8, 0 crashes,
heapcheck-clean through construction. Energy weapons already damage via the mech4 beam path,
so combat is unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
716 lines
26 KiB
C++
716 lines
26 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.
|
|
// 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), ProjectileWeaponSimulation @004bbd04
|
|
// (Performance), 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 HeatableSubsystem present? 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
|
|
|
|
|
|
//#############################################################################
|
|
// 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)
|
|
|
|
static Scalar UniformRandom() { return 0.0f; } // FUN_00408050 (deterministic stand-in)
|
|
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
|
|
);
|
|
|
|
Receiver::MessageHandlerSet ProjectileWeapon::MessageHandlers;
|
|
Simulation::AttributeIndexSet ProjectileWeapon::AttributeIndex;
|
|
|
|
ProjectileWeapon::SharedData
|
|
ProjectileWeapon::DefaultData(
|
|
&ProjectileWeapon::ClassDerivations,
|
|
ProjectileWeapon::MessageHandlers,
|
|
ProjectileWeapon::AttributeIndex,
|
|
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
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
Subsystem *
|
|
ProjectileWeapon::OwnerSubsystem(Mech * /*owner*/, int /*index*/) const
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
Logical
|
|
ProjectileWeapon::LiveFireEnabled() const
|
|
{
|
|
return True;
|
|
}
|
|
|
|
//
|
|
// ConsumeRound -- pull one round from the linked AmmoBin (FUN_004bd4f4
|
|
// AmmoBin::FeedAmmo). Returns True when a round was dispensed; otherwise raises
|
|
// the NoAmmo alarm and returns False. Shared by both FireWeapon spawn paths.
|
|
//
|
|
Logical
|
|
ProjectileWeapon::ConsumeRound()
|
|
{
|
|
AmmoBin *bin = (AmmoBin*)ammoBinLink.Resolve(); // FUN_00417ab4(this+0x43c)
|
|
if (bin != 0 && bin->FeedAmmo() != 0) // FUN_004bd4f4 -- dispensed?
|
|
{
|
|
return True;
|
|
}
|
|
weaponAlarm.SetLevel(NoAmmoState); // FUN_0041bbd8(this+0x350, 7)
|
|
return False;
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// 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.
|
|
if (((owner->simulationFlags & 0xC) == 0) && ((owner->simulationFlags & 0x100) != 0))
|
|
{
|
|
SetPerformance(&ProjectileWeapon::ProjectileWeaponSimulation); // member-ptr {0x4bbd04,..}
|
|
}
|
|
else
|
|
{
|
|
simulationFlags |= 0x2; // this[10] |= 2 (mark "not simulated")
|
|
}
|
|
|
|
// 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. ProjectileWeapon adds nothing of its own; defers to the
|
|
// MechWeapon implementation.
|
|
//
|
|
void
|
|
ProjectileWeapon::ReadUpdateRecord(UpdateRecord *message)
|
|
{
|
|
MechWeapon::ReadUpdateRecord(message); // 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
|
|
|
|
simulationFlags |= 0x1; // this[6] |= 1 (state dirty)
|
|
}
|
|
|
|
//
|
|
// @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 Mech's
|
|
// "live fire" flag (mech.controls+0x25c).
|
|
//
|
|
Logical
|
|
ProjectileWeapon::CheckForJam()
|
|
{
|
|
if (!LiveFireEnabled()) // *(*(this+0xd0)+400)+0x25c
|
|
return False;
|
|
|
|
if (heatLoad <= 0.0f) // this+0x184 (heat-active gate)
|
|
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. Body NOT
|
|
// recovered by the decompiler (prologue confirmed at 0x4bbd04). From context it
|
|
// sequences DrawFiringCharge / UpdateEject / CheckForJam / ComputeTimeOfFlight
|
|
// and dispatches FireWeapon (vtable slot 18) when the trigger + charge + ammo
|
|
// conditions are met.
|
|
//
|
|
void
|
|
ProjectileWeapon::ProjectileWeaponSimulation(Scalar time_slice)
|
|
{
|
|
Check(this);
|
|
|
|
// 1. Recover the firing charge (recoil counts down toward 0 in
|
|
// DrawFiringCharge) and advance any in-progress magazine eject.
|
|
DrawFiringCharge(time_slice); // @004bbc78 -- bleed recoil (+0x3E8)
|
|
UpdateEject(time_slice); // @004bbb50 -- idle unless ejectState==0
|
|
|
|
// 2. Rising-edge trigger from the owning Mech's discharge signal.
|
|
Logical trigger = CheckFireEdge(); // MechWeapon @004b9608
|
|
|
|
// 3. Firing state machine (weaponAlarm @0x350). Modelled on the RP analog
|
|
// RivetGun::RivetGunSimulation (WEAPSYS.cpp): the Jammed / NoAmmo latches
|
|
// persist and only escalate on a trigger pull; the default (ready) branch
|
|
// rolls the heat-scaled jam chance and -- on a clean shot with charge and
|
|
// ammo -- dispatches FireWeapon through the vtable (slot 18) so the
|
|
// MissileLauncher override spawns Missiles.
|
|
switch (weaponAlarm.GetState()) // *(this+0x364)
|
|
{
|
|
case JammedState: // 5 -- cleared only by ResetToInitialState
|
|
if (trigger)
|
|
weaponAlarm.SetLevel(TriggerDuringJamState); // 6
|
|
break;
|
|
|
|
case NoAmmoState: // 7 -- re-assert on a dry trigger pull
|
|
if (trigger)
|
|
weaponAlarm.SetLevel(NoAmmoState);
|
|
break;
|
|
|
|
default: // Loaded / Loading / Firing / idle
|
|
if (trigger && ReadyToFire())
|
|
{
|
|
if (CheckForJam()) // @004bbfcc -- heat-scaled jam roll
|
|
{
|
|
weaponAlarm.SetLevel(JammedState); // 5
|
|
break;
|
|
}
|
|
|
|
// refresh the lead solution (updates leadPosition / targeting), then
|
|
// fire. FireWeapon() consumes a round + spawns the projectile/missile
|
|
// and (on a dry bin) latches NoAmmo itself.
|
|
ComputeTimeOfFlight(); // @004bc06c
|
|
FireWeapon(); // vtable slot 18 -> spawn path
|
|
if (weaponAlarm.GetState() != NoAmmoState && weaponAlarm.GetState() != JammedState)
|
|
weaponAlarm.SetLevel(0); // Firing
|
|
}
|
|
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 and starts the eject cycle. MissileLauncher
|
|
// overrides this again at @004bcc60 to launch Missile entities.
|
|
//
|
|
void
|
|
ProjectileWeapon::FireWeapon()
|
|
{
|
|
Check(this);
|
|
|
|
// Pull a round; a dry / not-ready bin latches NoAmmo and aborts the shot.
|
|
if (!ConsumeRound()) // FUN_004bd4f4 (AmmoBin::FeedAmmo)
|
|
return;
|
|
|
|
// Begin the per-shot recharge cooldown (recoil bleeds back down to 0 in
|
|
// DrawFiringCharge before the weapon is ReadyToFire again).
|
|
recoil = rechargeRate; // this[0xfa] = this[0xf7]
|
|
|
|
// Resolve the live muzzle (used to seed the projectile descriptor).
|
|
Point3D muzzle;
|
|
GetMuzzlePoint(muzzle); // MechWeapon @004b9948
|
|
|
|
// ⚠ SPAWN DEFERRED (WAVE 7 Phase B): the shipped code builds a ~0xD4-byte projectile
|
|
// MakeMessage here (muzzle transform + lead point + the 12-word Damage descriptor +
|
|
// owner/target) and hands it to Projectile::New (@004be384). The reconstructed
|
|
// Projectile/Missile ENTITY classes are NOT spawn-ready yet: they carry Entity-base
|
|
// phantom members that overflow their New() alloc (0x340/0x368), and New() currently
|
|
// takes (int)this instead of the descriptor -> a live spawn would corrupt the heap.
|
|
// So the weapon SUBSYSTEM ticks authentically (charge/ammo/heat/recoil) but does NOT
|
|
// spawn until the entity byte-exact base + descriptor build land. See scratchpad/
|
|
// wave7_maps.txt (Projectile/Missile maps) + docs/SUBSYS_PLAN.md.
|
|
// Projectile::New((int)this); // <-- Phase B
|
|
|
|
simulationFlags |= 0x1; // replication-dirty
|
|
Check_Fpu();
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// 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);
|
|
}
|