User report: lasers only visible on the window firing them. The peer's replicant emitters never learned the master fired. THE AUTHENTIC PIPE (decomp-verified): - FUN_0041c350 (the "beam keepalive" ServiceDischarge/ContinueDischarge call) does TWO things: queue the LOCAL deferred beam-effect callback (@0x4bac0c) on the app+0x34 manager -- our per-weapon render walk already plays that role -- and set the subsystem DIRTY bit, which maps to the 2007 engine's updateModel / ForceUpdate(). - Replication rides SUBSYSTEM UPDATE RECORDS inside the mech's update message: the roster walk already hands the entity's stream to every subsystem's PerformAndWatch; Simulation::WriteSimulationUpdate serializes each requested updateModel bit; Entity::UpdateMessageHandler routes received records by subsystemID to the subsystem's ReadUpdateRecord. All engine machinery -- the missing pieces were the Emitter's serialize/apply pair + the triggers. CORRECTIONS to the dormant task-33-era transcriptions (never exercised -- nothing ever set updateModel -- so the latent misreads never surfaced): - The weapon-family VTABLE SLOT MAP was swapped: slot 6 = ReadUpdateRecord, 7 = WriteUpdateRecord, 9 = TakeDamage (evidence: Mech hierarchy symmetry + body semantics; @004ba568 resolves an EntityID at rec+0x30 through the entity index -- record semantics, not Damage). Renamed across MechWeapon / Emitter / ProjectileWeapon; the real Emitter::TakeDamage @004bafc8 is undecoded (inherits MechWeapon for now). - Emitter/MechWeapon Write: `*record = 0x38/0x18` is the record LENGTH, not recordID; rec+0x30 is the TARGET's EntityID (GetEntityID()), not a colour -- the old `CopyColor(targetEntity+0x184)` was also a databinding trap. - OVERRIDE-SIGNATURE TRAP: the decls used each class's own shadowing UpdateRecord typedef as the param type, silently NOT overriding the engine virtual (the base ran instead; nothing would ever have serialized). Base-typed params (Simulation__UpdateRecord*), casts inside. - Emitter::ReadUpdateRecord reconstructed (@004ba568): target EntityID resolve (drop unknown non-null targets), MechWeapon alarm apply chain, beam fields. - ServiceDischarge/ContinueDischarge: ForceUpdate() per keepalive tick + one final record at beam end (turns the peer's beam off). - Mech::DrawWeaponBeams extracted from the player-only drive block so the walk runs for REPLICANTS (+ per-mech gun-port cache -- the process-wide statics would have served the player's segment pointers as the replicant's muzzles). VERIFIED 2-node: A fires 57 volleys -> 225 emitter records -> B applies all 225 -> B draws 414 beams (PPC blue / laser red, from A's replicant's own gun ports). Solo un-regressed (150 beams, kill chain, no crash). Also preserved: the full Mech::WriteUpdateRecord @0x4a0c2c recovery (reference/decomp/mech_writeupdate_004a0c2c.disasm.txt) with all 9 record types decoded (pose/alarm/leg-state+heat with the body-channel write-through re-sync, knockdown, death, impact, movementMode) -- transcription deferred; it replicates remote knockdown/death/heat and was not needed for beams. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
340 lines
15 KiB
C++
340 lines
15 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 MessageHandlers;
|
|
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
|
|
// (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
|