TASK #6 -- the pod's in-cockpit weapon regrouping, fully reconstructed and live-verified (hold-configure -> tap fire button -> toggle membership -> release-commit): - MechWeapon handlers id 9 ConfigureMappables @004b9550 / id 10 ChooseButton @004b95b8 (binary table @0x511860; the old "Myomers fns" mislabel swept) + the GetMessageHandlers() accessor chain through Emitter/ProjectileWeapon/ MissileLauncher/GAUSS/PPC (empty per-class sets swallowed dispatch). - Mapper vtable truth: +0x38 EnterConfiguration / +0x3C ExitConfiguration / +0x40,+0x44 AddOrErase evt/dir. No "secondary vtable @0050f498", no "CreateTemporaryEventMappings" virtual (RP-name drift; swept incl CLASSMAP). L4 Enter/Exit rebuilt complete: StartMappableButtonsConfigure (the NonMapping 0x10000 <-> Mapping 0x8000 mode flip + the gauge's active-weapon latch) + the held-button re-arm + the 4 fire-button temp maps; only the RIO mapper implements the toggle (Thrustmaster no-ops = can't regroup). - MechSubsystem +0xE8/+0xEC corrected to controlDestination/controlMessageID (ex hostEntity/subsystemId2 mislabel); MechWeapon ctor defaults the destination to &fireImpulse (@004b99a8). - ConfigMapGauge state loop reconstructed (PE-recovered DAT_00518eb4 table; buttonGroup GetMapState sampler -- the "needs ModeManager" guard rationale was wrong). Finding [T1]: the shipped binary NEVER enables this gauge (no SetColor caller) -- authentically dormant; BT_CONFIGMAP=1 is the dev enable. - Dev harness: HOLD 'G' opens the session (BT_CONFIG_SLOT picks the weapon); BT_CONFIG_TEST scripts a headless verify. @004afbc4 corrected to its real Fail-trap body (the guessed AddOrErase(NULL) body would corrupt groups). - Bonus [T1]: the binary MechWeapon ATTRIBUTE table has ELEVEN entries (PercentDone..WeaponState) -- "TriggerState is the only one" was wrong. TASK #11 -- the user-reported live-play regressions (both real bugs): - PHANTOM FIRE: the mech4 bring-up shot block painted an explosion at the victim on its OWN 0.3s cadence whenever fire was held -- desynced once the authentic recharges landed. Retired (the real impact visual flows from each discharge via the messmgr SubmitExplosion). - WEAPON BRICKING (the "cuts out" bug): the Loading->Loaded snap window (+-0.01 around seekV) assumes the pod's LOCKED 60 fps; one port dt-spike jumps it, the byte-verified >1.0 clamp (_DAT_004ba830=0) zeroes readiness, and the weapon sticks in Loading at level ~10000 forever (observed live). Fixed with pod-frame (1/60s) sub-stepping of the binary's own Loading tick -- also fixes the I^2R integral over-heating generators on big steps. NEW GOTCHA CLASS recorded: reconstruction-gotchas #12 (frame-pacing trap). - Duty-cycle measured (max-rate autofire): generators equilibrate in the degradation band (recharge stretch ~3.5-4.4x), the FailureHeat breaker never trips solo, PPCs sustain ~11.8-damage full-charge fire. The slow kill pacing and recharge gaps are the AUTHENTIC heat economy; the pilot's counters (this regrouping UI, generator reassignment, coolant valves) are the reconstruction queue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
845 lines
30 KiB
C++
845 lines
30 KiB
C++
//===========================================================================//
|
|
// File: mechweap.cpp //
|
|
// Project: BattleTech Brick: Entity Manager //
|
|
// Contents: MechWeapon -- abstract base for every mounted Mech weapon //
|
|
//---------------------------------------------------------------------------//
|
|
// Date Who Modification //
|
|
// -------- --- ---------------------------------------------------------- //
|
|
// --/--/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 @004b95ec-@004b9d10 (file=bt/mechweap.cpp)
|
|
// plus the pure-virtual stub @004ba45c (file=bt/mechweap.hpp). Method and
|
|
// member names follow the surviving derived sources (PPC.CPP, GAUSS.CPP,
|
|
// MISLANCH.HPP), the resource-parser tag strings, and the RP WEAPSYS.h analog.
|
|
// Each non-trivial method cites the originating @ADDR.
|
|
//
|
|
// Resolved read-only constants (4 LE bytes from section_dump CODE):
|
|
// _DAT_004b9648 = 00 00 00 00 = 0.0f (fire-edge threshold)
|
|
// _DAT_004b9b98 = 00 00 80 bf = -1.0f (default pip-colour component)
|
|
// _DAT_004b9c98 = 00 00 80 3f = 1.0f (full-heat reference)
|
|
// _DAT_004ba37c = 00 00 80 bf = -1.0f (resource "unset" sentinel)
|
|
//
|
|
// Helper-function name mapping (engine internals referenced by the decomp):
|
|
// FUN_004b0f74 PoweredSubsystem base constructor
|
|
// FUN_004b115c PoweredSubsystem destructor
|
|
// FUN_004b0e6c PoweredSubsystem::ResetToInitialState (vtable slot 10)
|
|
// FUN_004b0efc PoweredSubsystem (vtable slot 9 -- ReadUpdateRecord)
|
|
// FUN_004b0f48 PoweredSubsystem::GetStatusFlags (vtable slot 12)
|
|
// FUN_004b1224 PoweredSubsystem::PrintState (vtable slot 13)
|
|
// FUN_004b13ac PoweredSubsystem::CreateStreamedSubsystem
|
|
// FUN_0041bd34 Subsystem::TakeDamage (vtable slot 6, base)
|
|
// FUN_0041c500 Subsystem::WriteUpdateRecord (vtable slot 7, base)
|
|
// FUN_0041b9ec AlarmIndicator(levels) FUN_0041baa4 ~AlarmIndicator
|
|
// FUN_0041bbd8 AlarmIndicator::SetLevel(n)
|
|
// FUN_0041db7c Damage::Damage() / init
|
|
// FUN_0041a1a4 IsDerivedFrom(classDerivations)
|
|
// FUN_00408440 Vector copy FUN_0040a7f4 Point copy
|
|
// FUN_00408644 Vector subtract (out = a - b)
|
|
// FUN_004087f4 Vector negate/copy
|
|
// FUN_0040aadc Matrix identity FUN_0040ad2c Matrix from angles
|
|
// FUN_00409a70 apply transform FUN_00409968 Vector set
|
|
// FUN_004dd138 sqrtf FUN_004dc8ec atan2f(double,double)
|
|
// FUN_00408110 angle normalise
|
|
// FUN_0041f216 / FUN_0041f235 segment-iterator construct / destruct
|
|
// FUN_00424da8 resolve segment world point
|
|
// FUN_0049b6d8 cockpit HUD: register pip / fire marker
|
|
// FUN_004b13ac etc. (resource parser primitives):
|
|
// FUN_00404118 NotationFile::Read(scalar) FUN_00404088 Read(string)
|
|
// FUN_004040d8 Read(int/bool) FUN_00404190 Read(int)
|
|
// FUN_00406824 parse colour FUN_00406ff8 ResourceFile::FindResource
|
|
// FUN_004d4b58 strcmp FUN_004d4b14 memcmp FUN_004d4c78 strstr
|
|
// FUN_004dbb24 DebugStream << FUN_004d9c38 DebugStream flush
|
|
// FUN_0040385c Verify()/assert(msg,file,line)
|
|
//
|
|
|
|
#include <bt.hpp>
|
|
#include <messmgr.hpp> // SubsystemMessageManager (task #8 damage submission)
|
|
#include <mechmppr.hpp> // MechControlsMapper -- the config-session virtuals (task #6)
|
|
#pragma hdrstop
|
|
|
|
#if !defined(MECHWEAP_HPP)
|
|
# include <mechweap.hpp>
|
|
#endif
|
|
#if !defined(APP_HPP)
|
|
# include <app.hpp>
|
|
#endif
|
|
#if !defined(TESTBT_HPP)
|
|
# include <testbt.hpp>
|
|
#endif
|
|
|
|
|
|
//#############################################################################
|
|
// Shared Data Support
|
|
//
|
|
Derivation
|
|
MechWeapon::ClassDerivations(
|
|
PoweredSubsystem::GetClassDerivations(),
|
|
"MechWeapon"
|
|
);
|
|
|
|
// task #6 -- the binary's MechWeapon handler table @0x511860 (section_dump:
|
|
// {9, "ConfigureMappables"@0x511994, @004b9550}, {10, "ChooseButton"@0x5119A7,
|
|
// @004b95b8}); chained onto the PoweredSubsystem set.
|
|
const Receiver::HandlerEntry
|
|
MechWeapon::MessageHandlerEntries[]=
|
|
{
|
|
MESSAGE_ENTRY(MechWeapon, ConfigureMappables), // id 9 @004b9550
|
|
MESSAGE_ENTRY(MechWeapon, ChooseButton) // id 10 @004b95b8
|
|
};
|
|
|
|
Receiver::MessageHandlerSet&
|
|
MechWeapon::GetMessageHandlers()
|
|
{
|
|
static Receiver::MessageHandlerSet messageHandlers(
|
|
ELEMENTS(MechWeapon::MessageHandlerEntries),
|
|
MechWeapon::MessageHandlerEntries,
|
|
PoweredSubsystem::GetMessageHandlers()
|
|
);
|
|
return messageHandlers;
|
|
}
|
|
|
|
Simulation::AttributeIndexSet MechWeapon::AttributeIndex; // legacy empty member (superseded by GetAttributeIndex)
|
|
|
|
// gauge data-binding wave: OutputVoltage/PercentDone -> rechargeLevel (1.0 ==
|
|
// ready; ComputeOutputVoltage/the charge state machine drive it, so both animate
|
|
// as the weapon recharges). Chained to PoweredSubsystem's dense index.
|
|
const MechWeapon::IndexEntry
|
|
MechWeapon::AttributePointers[]=
|
|
{
|
|
// The authentic entry (binary table @0x511890): the streamed controls maps
|
|
// bind the pod fire buttons directly onto this Scalar -- fireImpulse IS the
|
|
// binary's "TriggerState" (the edge-detected fire channel destination).
|
|
ATTRIBUTE_ENTRY(MechWeapon, TriggerState, fireImpulse), // @0x31C id 0x13
|
|
ATTRIBUTE_ENTRY(MechWeapon, OutputVoltage, rechargeLevel), // @0x320 (charge/ready level)
|
|
ATTRIBUTE_ENTRY(MechWeapon, PercentDone, rechargeLevel) // @0x320 (recharge dial fraction)
|
|
};
|
|
|
|
MechWeapon::AttributeIndexSet&
|
|
MechWeapon::GetAttributeIndex()
|
|
{
|
|
static MechWeapon::AttributeIndexSet attributeIndex(
|
|
ELEMENTS(MechWeapon::AttributePointers),
|
|
MechWeapon::AttributePointers,
|
|
PoweredSubsystem::GetAttributeIndex()
|
|
);
|
|
return attributeIndex;
|
|
}
|
|
|
|
MechWeapon::SharedData
|
|
MechWeapon::DefaultData(
|
|
&MechWeapon::ClassDerivations,
|
|
MechWeapon::GetMessageHandlers(),
|
|
MechWeapon::GetAttributeIndex(),
|
|
MechWeapon::StateCount
|
|
);
|
|
|
|
|
|
//#############################################################################
|
|
// The CONFIG-MODE session handlers (task #6)
|
|
//
|
|
|
|
//
|
|
// @004b9550 -- id 9 "ConfigureMappables": the press-and-HOLD session bracket.
|
|
// dataContents = +-(element+1) (press/release, the L4 button value convention).
|
|
// Press: open the session on THIS weapon -- the roster-slot-0 mapper flips the
|
|
// global mode NonMapping->Mapping, latches &fireImpulse as the active configure
|
|
// target, and temp-maps the fire buttons to send ChooseButton (id 10) here.
|
|
// Release: close the session (mode restored, temp layer stripped).
|
|
// The +0x110 write (0 open / -1 idle) is the binary's config-session flag
|
|
// (ctor default -1, part_012.c:15775); the port field carries the older name
|
|
// vitalSubsystemIndex -- no confirmed reader yet (read-site survey pending).
|
|
//
|
|
void
|
|
MechWeapon::ConfigureMappablesMessageHandler(
|
|
ReceiverDataMessageOf<ControlsButton> *message
|
|
)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(message);
|
|
|
|
int value = message->dataContents; // +-(element+1)
|
|
int element = ((value < 0) ? -value : value) - 1;
|
|
|
|
MechControlsMapper *mapper = ((Mech *)owner)->MappingMapper(); // **(mech+0x128)
|
|
if (mapper == 0)
|
|
{
|
|
return; // (binary assumes slot 0 is live)
|
|
}
|
|
|
|
if (value < 1)
|
|
{
|
|
mapper->ExitConfiguration(element); // vtbl+0x3C
|
|
vitalSubsystemIndex = -1; // this+0x110 -> idle
|
|
}
|
|
else
|
|
{
|
|
vitalSubsystemIndex = 0; // this+0x110 -> session open
|
|
mapper->EnterConfiguration( // vtbl+0x38
|
|
element,
|
|
(ControlsButton *)&fireImpulse, // this+0x31C (TriggerState)
|
|
this,
|
|
ChooseButtonMessageID, // 10
|
|
ConfigureMappablesMessageID, // 9
|
|
0); // active_message_id (direct bind)
|
|
}
|
|
}
|
|
|
|
//
|
|
// @004b95b8 -- id 10 "ChooseButton": while the session is open, a tapped fire
|
|
// button sends its RAW 1-based value here; the mapper's direct AddOrErase
|
|
// TOGGLES buttonGroup[value-1]'s permanent (0x10000) mapping onto
|
|
// &fireImpulse -- join the group if absent, leave it if present. Press only.
|
|
//
|
|
void
|
|
MechWeapon::ChooseButtonMessageHandler(
|
|
ReceiverDataMessageOf<ControlsButton> *message
|
|
)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(message);
|
|
|
|
if (message->dataContents > 0)
|
|
{
|
|
MechControlsMapper *mapper = ((Mech *)owner)->MappingMapper();
|
|
if (mapper != 0)
|
|
{
|
|
mapper->AddOrErase( // vtbl+0x44 (direct overload)
|
|
(unsigned int)message->dataContents, // RAW 1-based
|
|
(ControlsButton *)&fireImpulse);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Firing -- abstract base body
|
|
//
|
|
// @004b95ec -- MechWeapon is abstract; the base FireWeapon() must never run.
|
|
// Exact analog of RP GenericGun::Fire()'s "Should not be here!" stub. Real
|
|
// firing lives in Emitter::FireWeapon (PPC/Gauss) and ProjectileWeapon
|
|
// (MissileLauncher). Assert tag: MECHWEAP.CPP : 0xC9.
|
|
//
|
|
void
|
|
MechWeapon::FireWeapon()
|
|
{
|
|
Fail("MechWeapon abstract class should not be fired");
|
|
}
|
|
|
|
//
|
|
// @004ba45c -- a second pure-virtual placeholder emitted from MECHWEAP.HPP:343
|
|
// ("ERROR: Accessing Pure Virtual Method"). Compiled into the next translation
|
|
// unit (Emitter); reproduced here for completeness. A human should reconcile
|
|
// which header-declared pure virtual this guards.
|
|
//
|
|
// void MechWeapon::<pure-virtual>() {
|
|
// Verify(False, "ERROR: Accessing Pure Virtual Method",
|
|
// "d:\\tesla\\bt\\bt\\mechweap.hpp", 0x157);
|
|
// }
|
|
|
|
|
|
//#############################################################################
|
|
// Construction / Destruction
|
|
//
|
|
// @004b99a8 -- chains to the PoweredSubsystem ctor (@004b0f74), installs the
|
|
// MechWeapon vtable (PTR_FUN_00511d2c), constructs the alarm indicator and the
|
|
// Damage member, then copies the weapon parameters out of the resource record.
|
|
//
|
|
MechWeapon::MechWeapon(
|
|
Mech *owner,
|
|
int subsystem_ID,
|
|
SubsystemResource *subsystem_resource,
|
|
SharedData &shared_data
|
|
):
|
|
PoweredSubsystem(owner, subsystem_ID, subsystem_resource, shared_data),
|
|
weaponAlarm(/*levels=*/ 3), // FUN_0041b9ec(this+0xd4, levels)
|
|
damageData() // FUN_0041db7c(this+0xea)
|
|
{
|
|
Check(owner);
|
|
Check_Pointer(subsystem_resource);
|
|
|
|
// pip colour defaults to (-1,-1,-1) -- "use the configured/team colour".
|
|
pipColor.Red = -1.0f; // _DAT_004b9b98
|
|
pipColor.Green = -1.0f;
|
|
pipColor.Blue = -1.0f;
|
|
|
|
// --- copy weapon parameters out of the resource ---
|
|
rechargeRate = subsystem_resource->rechargeRate; // +0x190
|
|
explosionResourceID = subsystem_resource->explosionResourceID; // +0x198
|
|
damageData.damageType = subsystem_resource->damageType; // +0x1A0
|
|
damageData.damageAmount = subsystem_resource->damageAmount; // +0x19C
|
|
segmentPageIndex = subsystem_resource->segmentIndex; // +0x28
|
|
weaponRange = subsystem_resource->weaponRange; // +0x194
|
|
heatCostToFire = subsystem_resource->heatCostToFire; // +0x1A4
|
|
pipPosition = subsystem_resource->pipPosition; // +0x1A8
|
|
pipColor = subsystem_resource->pipColor; // +0x1AC..+0x1B4
|
|
pipExtendedRange = subsystem_resource->pipExtendedRange; // +0x1B8
|
|
effectiveRange = weaponRange; // 0x328 primed to weaponRange (recomputed in UpdateTargeting)
|
|
|
|
simulationFlags |= 0x8; // this[10] |= 8 (mark "weapon" subsystem)
|
|
|
|
rangeToTarget = 0.0f; // 0x324
|
|
targetWithinRange = False; // 0x34C
|
|
// segmentReference/pipSegment were phantom tail members (past the binary 0x3F0 end);
|
|
// removed. The muzzle segment index is the inherited base slot this+0xdc (used by
|
|
// GetMuzzlePoint); the pip mount is gated by useConfiguredPip in DrawWeaponPip.
|
|
|
|
// usesExternalModel: True if the model name contains the marker substring
|
|
// (DAT_00511aa2); selects whether the configured pip is drawn.
|
|
usesExternalModel = (strstr(GetName(), "EXT") != 0) ? True : False;
|
|
|
|
fireImpulse = 0.0f; // 0x31C
|
|
// task #6: the binary MechWeapon ctor defaults its controls-destination
|
|
// pass-through to &fireImpulse when the caller passes NULL (@004b99a8
|
|
// `if (param_7==0) param_7 = this+0x31C`); the chain stores it at
|
|
// MechSubsystem+0xE8/+0xEC where ConfigMapGauge's GetMapState reads it.
|
|
controlDestination = (void *)&fireImpulse; // 0xE8
|
|
controlMessageID = 0; // 0xEC
|
|
rechargeLevel = 1.0f; // 0x320 (fully charged)
|
|
previousFireImpulse = 0.0f; // 0x3A4
|
|
recoil = 0.0f; // 0x3E8
|
|
useConfiguredPip = (usesExternalModel == 0); // 0x3E0
|
|
pipState = 0; // 0x330
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
// @004b9b9c -- destroys the alarm indicator, chains to ~PoweredSubsystem, and
|
|
// (if the deleting-dtor bit is set) frees the object.
|
|
//
|
|
MechWeapon::~MechWeapon()
|
|
{
|
|
Check(this);
|
|
// weaponAlarm (@0xd4) destroyed by its own dtor (FUN_0041baa4(this+0xd4, 2));
|
|
// base chain (FUN_004b115c) handles the rest.
|
|
Check_Fpu();
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Test Class Support
|
|
//
|
|
Logical
|
|
MechWeapon::TestClass(Mech &)
|
|
{
|
|
return True;
|
|
}
|
|
|
|
Logical
|
|
MechWeapon::TestInstance() const
|
|
{
|
|
return IsDerivedFrom(ClassDerivations);
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Subsystem virtual overrides
|
|
//
|
|
|
|
//
|
|
// @004b96d4 -- slot 9 (TASK #51 RENAME: this body was mislabeled slot-6
|
|
// ReadUpdateRecord; @004b96d4 chains @004b0efc = PoweredSubsystem::TakeDamage,
|
|
// which dispatches on *param==4 == Damage::damageType -- damage semantics).
|
|
// The PoweredSubsystem::TakeDamage body (electrical-short handling) is not yet
|
|
// reconstructed; chain to the engine Subsystem base, and keep the damage-zone
|
|
// alarm tracking this port has shipped with (best-effort; the binary's alarm
|
|
// display on weapon damage flows through the unreconstructed @004b0efc chain).
|
|
//
|
|
void
|
|
MechWeapon::TakeDamage(Damage &damage)
|
|
{
|
|
Subsystem::TakeDamage(damage); // engine base (real chain: @004b0efc)
|
|
|
|
// Port behavior (kept): a destroyed weapon clears its display alarm;
|
|
// otherwise the alarm tracks the resolved damage-zone state.
|
|
::DamageZone *dz = this->Subsystem::damageZone;
|
|
if (dz != 0
|
|
&& dz->GetGraphicState() == DamageZone::DestroyedGraphicState)
|
|
{
|
|
weaponAlarm.SetLevel(0);
|
|
}
|
|
else
|
|
{
|
|
weaponAlarm.SetLevel(
|
|
(dz != 0 && dz->damageLevel > 0.0f) ? 1 : 0
|
|
);
|
|
}
|
|
}
|
|
|
|
//
|
|
// @004b9690 -- slot 7. Append the weapon's replication fields to the update
|
|
// record. DISASM CORRECTIONS (task #51): `*message = 0x18` writes the record
|
|
// LENGTH (record[0] = recordLength), not recordID; and message[5] derives from
|
|
// this+0x360 == weaponAlarm+0x10 (LevelCountB), not from the level. BASE-TYPED
|
|
// param: with the class's own UpdateRecord typedef this silently failed to
|
|
// override the engine virtual, so the engine base ran and no weapon field
|
|
// ever serialized.
|
|
//
|
|
void
|
|
MechWeapon::WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model)
|
|
{
|
|
Subsystem::WriteUpdateRecord(message, update_model); // FUN_0041c500 (header + subsystemID)
|
|
|
|
MechWeapon__UpdateRecord *rec = (MechWeapon__UpdateRecord *)message;
|
|
rec->recordLength = sizeof(MechWeapon__UpdateRecord); // *message = 0x18
|
|
rec->alarmState = weaponAlarm.GetLevel(); // message[4] = *(this+0x364)
|
|
rec->weaponIdle = (weaponAlarm.LevelCountB() == 0) ? 1 : 0; // message[5] = (*(this+0x360)==0)
|
|
}
|
|
|
|
//
|
|
// @004b964c -- slot 6 (TASK #51 RENAME: was mislabeled TakeDamage). Apply a
|
|
// weapon update record on the REPLICANT: chain the Simulation base
|
|
// (@0041bd34 -- lastUpdate + simulationState), then the weapon alarm --
|
|
// reset-through-0 first when the record's weaponIdle flag is set, then the
|
|
// replicated alarm level.
|
|
//
|
|
void
|
|
MechWeapon::ReadUpdateRecord(Simulation__UpdateRecord *message)
|
|
{
|
|
Simulation::ReadUpdateRecord(message); // FUN_0041bd34
|
|
|
|
MechWeapon__UpdateRecord *rec = (MechWeapon__UpdateRecord *)message;
|
|
if (rec->weaponIdle != 0) // record+0x14
|
|
{
|
|
weaponAlarm.SetLevel(0);
|
|
}
|
|
weaponAlarm.SetLevel(rec->alarmState); // record+0x10
|
|
}
|
|
|
|
//
|
|
// @004b96ec -- slot 10. Reset the firing/recharge state, then chain to base.
|
|
//
|
|
void
|
|
MechWeapon::ResetToInitialState()
|
|
{
|
|
PoweredSubsystem::ResetToInitialState(); // FUN_004b0e6c
|
|
recoil = 0.0f; // this[0xfa] = 0
|
|
rechargeLevel = 1.0f; // this[200] = 0x3f800000
|
|
fireImpulse = 0.0f; // this[199] = 0
|
|
previousFireImpulse = 0.0f; // this[0xe9] = 0
|
|
}
|
|
|
|
//
|
|
// @004b9d00 -- slot 13. Defers to the PoweredSubsystem print routine.
|
|
//
|
|
void
|
|
MechWeapon::PrintState()
|
|
{
|
|
PoweredSubsystem::PrintState(); // FUN_004b1224
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Targeting / aiming helpers
|
|
//
|
|
|
|
//
|
|
// @004b9bdc -- recompute the range to the owning Mech's current target and the
|
|
// heat-degraded effective range, then decide whether the target is in range.
|
|
//
|
|
// no target (entity+0x388 == 0):
|
|
// rangeToTarget = -1; targetWithinRange = False
|
|
// else:
|
|
// v = entity->targetPoint(+0x37c) - entity->muzzlePoint(+0x100)
|
|
// rangeToTarget = |v|
|
|
// effectiveRange = (1.0 - heatSource->heatLoad(+0x158)) * weaponRange
|
|
// targetWithinRange = (effectiveRange > rangeToTarget)
|
|
//
|
|
Logical
|
|
MechWeapon::UpdateTargeting()
|
|
{
|
|
if (!HasActiveTarget()) // *(entity+0x388) == 0
|
|
{
|
|
rangeToTarget = -1.0f; // 0xbf800000
|
|
targetWithinRange = False;
|
|
}
|
|
else
|
|
{
|
|
Point3D target, muzzle;
|
|
GetTargetPosition(target);
|
|
GetMuzzlePoint(muzzle);
|
|
|
|
Vector3D delta;
|
|
delta.Subtract(target, muzzle); // FUN_00408644
|
|
rangeToTarget = (Scalar)Sqrt( // FUN_004dd138
|
|
delta.x * delta.x + delta.y * delta.y + delta.z * delta.z);
|
|
|
|
// 1.0f == _DAT_004b9c98; heatLoad is the inherited HeatableSubsystem load.
|
|
effectiveRange = (1.0f - heatLoad) * weaponRange; // *0x32c
|
|
|
|
targetWithinRange = (effectiveRange > rangeToTarget) ? True : False;
|
|
}
|
|
return targetWithinRange;
|
|
}
|
|
|
|
//
|
|
// @004b9608 -- rising-edge detector on fireImpulse (threshold _DAT_004b9648 == 0.0f).
|
|
// Returns True the frame fireImpulse becomes positive while the previous sample
|
|
// was non-positive, then stores the current sample for next time.
|
|
//
|
|
// ⚠ BIT-PATTERN COMPARE (task #8 root-cause): TriggerState carries the raw
|
|
// ControlsButton INT bit-copied by the streamed direct mapping (+(button+1)
|
|
// press / -(button+1) release) -- the release value (e.g. -65 = 0xFFFFFFBF)
|
|
// reads as a NEGATIVE NaN in float terms. The binary's x87 compare (FNSTSW/
|
|
// SAHF + JBE: unordered sets CF|ZF => branch TAKEN) effectively treated the
|
|
// NaN as "released", so its edge re-armed; an IEEE-correct float compare
|
|
// makes NaN<=0 FALSE and the detector LATCHES SHUT after the first release
|
|
// (observed live: one startup edge, then never again). Compare the BIT
|
|
// PATTERNS as signed ints instead -- sign-correct for the button ints AND
|
|
// for genuine float values (0.0f = 0x0, positives > 0, negatives/NaNs < 0),
|
|
// reproducing the binary's effective behavior exactly.
|
|
//
|
|
Logical
|
|
MechWeapon::CheckFireEdge()
|
|
{
|
|
int current = *(int *)&fireImpulse;
|
|
int previous = *(int *)&previousFireImpulse;
|
|
Logical edge = (current > 0 && previous <= 0) ? True : False;
|
|
previousFireImpulse = fireImpulse;
|
|
return edge;
|
|
}
|
|
|
|
//
|
|
// @004b9cbc -- copy the owning Mech's current target position (entity+0x1c4).
|
|
//
|
|
void
|
|
MechWeapon::GetTargetPosition(Point3D &position)
|
|
{
|
|
// E3: read the owning Mech's current target position (mech+0x37c, set by mech4's
|
|
// targeting step) -- the local `targetPoint` member isn't refreshed in the port.
|
|
char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech)
|
|
if (o != 0)
|
|
position = *(Point3D *)(o + 0x37c); // MECH_TARGET_POS
|
|
// (no owner -> leave position unchanged; the phantom `targetPoint` member is gone --
|
|
// binary GetTargetPosition @004b9cbc unconditionally reads the owner target slot.)
|
|
}
|
|
|
|
// Faithful FUN_004b9948 muzzle resolve, defined in mech4.cpp (a complete-Mech TU);
|
|
// mechweap.cpp reaches the owner Mech only as a raw pointer, so it bridges via void*.
|
|
extern void BTResolveWeaponMuzzle(void *ownerMech, int segIndex, Point3D &out);
|
|
|
|
//
|
|
// @004b9cdc -- direction from a supplied point to the weapon mount (entity+0x37c).
|
|
//
|
|
void
|
|
MechWeapon::GetVectorToWeapon(const Point3D &from, Vector3D &direction)
|
|
{
|
|
Point3D muzzle;
|
|
GetMuzzlePoint(muzzle); // resolve live (phantom muzzlePoint member removed)
|
|
direction.Subtract(muzzle, from); // FUN_00408644
|
|
}
|
|
|
|
//
|
|
// @004b9948 -- resolve the weapon's muzzle/site world position by looking up its
|
|
// segment in the owning Mech's segment table and transforming it.
|
|
//
|
|
void
|
|
MechWeapon::GetMuzzlePoint(Point3D &point)
|
|
{
|
|
// @004b9948 -- resolve the weapon's mount segment (its index is the inherited
|
|
// subsystem slot at this+0xdc, == binary param_1+0xdc) in the owning Mech's
|
|
// segment table and transform it to world (FUN_00424da8). Reads live each call
|
|
// (the phantom cached `muzzlePoint` member is removed -- it collided with the
|
|
// Emitter subclass's own fields at 0x3F0+).
|
|
int segIndex = *(int *)((char *)this + 0xdc); // inherited segment index (byte-exact base)
|
|
BTResolveWeaponMuzzle(owner, segIndex, point); // mech4.cpp bridge (null-guarded)
|
|
}
|
|
|
|
//
|
|
// @004b9864 -- build an aim orientation from a direction vector (yaw/pitch via
|
|
// atan2), producing a rotation matrix.
|
|
//
|
|
void
|
|
MechWeapon::ComputeAimOrientation(LinearMatrix &orientation, const Vector3D &direction)
|
|
{
|
|
// Build a yaw/pitch aim orientation from a direction vector (atan2), roll 0.
|
|
Scalar yaw = (Scalar)atan2(-direction.x, -direction.z); // FUN_004dc8ec
|
|
Scalar len = (Scalar)Sqrt(direction.z * direction.z + direction.x * direction.x);
|
|
Scalar pitch = (Scalar)atan2(direction.y, len); // FUN_004dc8ec
|
|
|
|
EulerAngles angles(
|
|
Radian(Radian::Normalize(pitch)),
|
|
Radian(Radian::Normalize(yaw)),
|
|
Radian(0.0f)
|
|
);
|
|
orientation = angles; // AffineMatrix::operator=(EulerAngles)
|
|
}
|
|
|
|
//
|
|
// ⚠ IDENTITY CORRECTION (task #7): @004b9728 is MechWeapon::SendDamageMessage
|
|
// -- the weapon-fire damage submission into the owning mech's
|
|
// SubsystemMessageManager (mech+0x434 -> AddDamageMessage @0049b6d8), NOT a
|
|
// HUD pip. The "HUD element record" reading below was the same 0x434
|
|
// misattribution the mapper suffered. The authentic weapon-side submission
|
|
// (per-weapon damageData + inflictingSubsystemID) is pending the
|
|
// damage-economy reconciliation (authored 0.25-scale amounts vs the
|
|
// bring-up kShotDamage=12); the interim submitter is the mech4 fire block
|
|
// routing through AddDamageMessage. This body stays dormant meanwhile.
|
|
//
|
|
void
|
|
MechWeapon::SendDamageMessage(Entity *target)
|
|
{
|
|
//
|
|
// @004b9728 -- package this weapon's just-fired damageData as an
|
|
// Entity::TakeDamageMessage and submit it to the owning mech's
|
|
// SubsystemMessageManager for per-frame consolidation (AddDamageMessage
|
|
// @0049b6d8) -- the authentic beam damage delivery (task #8; the caller
|
|
// Emitter::FireWeapon fills damageAmount/damageForce/impactPoint first).
|
|
//
|
|
// Binary gate (part_013.c:6746): a Mech victim with NO designated zone
|
|
// (owner+0x38C == -1) is SKIPPED. The port PASSES -1 through [T3
|
|
// divergence]: our Mech::TakeDamageMessageHandler cylinder-resolves the
|
|
// unaimed zone (STEP 6), preserving the verified aiming model.
|
|
//
|
|
Mech *mech = (Mech *)GetEntity(); // weapon+0xD0
|
|
if (mech == 0 || target == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
extern int BTMechTargetZone(void *mech); // mech+0x38C (mech4 TU)
|
|
int zone = BTMechTargetZone(mech);
|
|
|
|
Entity::TakeDamageMessage message(
|
|
Entity::TakeDamageMessageID,
|
|
sizeof(Entity::TakeDamageMessage),
|
|
mech->GetEntityID(), // inflicting = owner (+0x184)
|
|
zone, // designated zone (or -1 -> cylinder)
|
|
damageData, // the 12-dword Damage @0x3A8
|
|
subsystemID); // inflictingSubsystemID (+0xD8) --
|
|
// lights up the messmgr's per-weapon
|
|
// explosion bundling (weapon+0x3E4)
|
|
SubsystemMessageManager *manager =
|
|
(SubsystemMessageManager *)mech->GetMessageManager(); // mech+0x434
|
|
if (manager != 0)
|
|
{
|
|
manager->AddDamageMessage(target, &message); // FUN_0049b6d8
|
|
}
|
|
else
|
|
{
|
|
target->Dispatch(&message); // no manager: direct (bring-up)
|
|
}
|
|
|
|
// PORT bookkeeping (score lived in the retired mech4 fire block): credit
|
|
// the delivered amount to the shooter-side score exactly as before.
|
|
extern void BTPostDamageScore(Entity *victim, Scalar damage);
|
|
extern Logical BTIsRegisteredMech(Entity *e);
|
|
if (BTIsRegisteredMech(target)
|
|
&& !((Mech *)target)->IsMechDestroyed())
|
|
{
|
|
BTPostDamageScore(target, damageData.damageAmount);
|
|
}
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// CreateStreamedSubsystem
|
|
//
|
|
// @004b9d10 -- parse the MechWeapon-specific resource fields after the
|
|
// PoweredSubsystem base parser. On the first pass every field is primed to the
|
|
// "unset" sentinel (-1.0f == _DAT_004ba37c, or -1 for ints); a field is an error
|
|
// only if both the read fails AND the field is still unset. Stamps
|
|
// subsystemModelSize = 0x1BC and classID = MechWeaponClassID.
|
|
//
|
|
int
|
|
MechWeapon::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 (
|
|
!PoweredSubsystem::CreateStreamedSubsystem( // FUN_004b13ac
|
|
model_file, model_name, subsystem_name,
|
|
subsystem_resource, subsystem_file, directories, passes
|
|
)
|
|
)
|
|
{
|
|
return False;
|
|
}
|
|
|
|
subsystem_resource->subsystemModelSize = sizeof(*subsystem_resource); // 0x1BC
|
|
subsystem_resource->classID = (RegisteredClass::ClassID)Mech::MechWeaponClassID; // 0xBCD
|
|
|
|
if (passes == 1)
|
|
{
|
|
// prime all weapon fields to "unset"
|
|
subsystem_resource->rechargeRate = -1.0f;
|
|
subsystem_resource->weaponRange = -1.0f;
|
|
subsystem_resource->explosionResourceID = -1;
|
|
subsystem_resource->damageAmount = -1.0f;
|
|
subsystem_resource->heatCostToFire = -1.0f;
|
|
subsystem_resource->pipPosition = -1;
|
|
subsystem_resource->pipColor.Red = -1.0f;
|
|
subsystem_resource->pipColor.Green = -1.0f;
|
|
subsystem_resource->pipColor.Blue = -1.0f;
|
|
subsystem_resource->pipExtendedRange = -1;
|
|
}
|
|
|
|
// "SegmentPageName" must be present on streaming passes after the first.
|
|
if (subsystem_resource->segmentIndex == -1 && passes > 1) // +0x28
|
|
{
|
|
DEBUG_STREAM << subsystem_name << " missing SegmentPageName!";
|
|
return False;
|
|
}
|
|
|
|
if (
|
|
!model_file->GetEntry(subsystem_name, "DamageAmount", &subsystem_resource->damageAmount)
|
|
&& subsystem_resource->damageAmount == -1.0f
|
|
)
|
|
{
|
|
DEBUG_STREAM << model_name << " Missing DamageAmount!" << std::endl;
|
|
return False;
|
|
}
|
|
|
|
if (
|
|
!model_file->GetEntry(subsystem_name, "HeatCostToFire", &subsystem_resource->heatCostToFire)
|
|
&& subsystem_resource->heatCostToFire == -1.0f
|
|
)
|
|
{
|
|
DEBUG_STREAM << subsystem_name << " missing HeatCostToFire!";
|
|
return False;
|
|
}
|
|
|
|
if (
|
|
!model_file->GetEntry(subsystem_name, "PipPosition", &subsystem_resource->pipPosition)
|
|
&& subsystem_resource->pipPosition == -1
|
|
)
|
|
{
|
|
DEBUG_STREAM << subsystem_name << " missing PipPosition!";
|
|
return False;
|
|
}
|
|
|
|
//
|
|
// PipColor: optional named colour; "Unspecified" leaves the resource value,
|
|
// otherwise it is parsed. A missing/short value is a hard error.
|
|
//
|
|
const char *pipColorName = "Unspecified";
|
|
if (!model_file->GetEntry(subsystem_name, "PipColor", &pipColorName))
|
|
{
|
|
// no value supplied -- require the resource already to hold one
|
|
RGBColor probe(-1.0f, -1.0f, -1.0f);
|
|
if (probe == subsystem_resource->pipColor)
|
|
{
|
|
DEBUG_STREAM << subsystem_name << " missing PipColor!";
|
|
return False;
|
|
}
|
|
}
|
|
if (strcmp(pipColorName, "Unspecified") != 0)
|
|
{
|
|
// FUN_00406824 -- the shipped build mapped a colour token to an RGB triple;
|
|
// accept an explicit "R G B" triple here (named-colour table is data-driven).
|
|
float r = -1.0f, g = -1.0f, b = -1.0f;
|
|
if (sscanf(pipColorName, "%f %f %f", &r, &g, &b) == 3)
|
|
{
|
|
subsystem_resource->pipColor.Red = r;
|
|
subsystem_resource->pipColor.Green = g;
|
|
subsystem_resource->pipColor.Blue = b;
|
|
}
|
|
}
|
|
|
|
if (
|
|
!model_file->GetEntry(subsystem_name, "PipExtendedRange", &subsystem_resource->pipExtendedRange)
|
|
&& subsystem_resource->pipExtendedRange == -1
|
|
)
|
|
{
|
|
DEBUG_STREAM << subsystem_name << " missing PipExtendedRange!" << std::endl;
|
|
return False;
|
|
}
|
|
|
|
if (
|
|
!model_file->GetEntry(subsystem_name, "RechargeRate", &subsystem_resource->rechargeRate)
|
|
&& subsystem_resource->rechargeRate == -1.0f
|
|
)
|
|
{
|
|
DEBUG_STREAM << model_name << " Missing RechargeRate!" << std::endl;
|
|
return False;
|
|
}
|
|
|
|
if (
|
|
!model_file->GetEntry(subsystem_name, "WeaponRange", &subsystem_resource->weaponRange)
|
|
&& subsystem_resource->weaponRange == -1.0f
|
|
)
|
|
{
|
|
DEBUG_STREAM << model_name << " Missing WeaponRange!" << std::endl;
|
|
return False;
|
|
}
|
|
|
|
//
|
|
// DamageType: maps a name onto the Damage::DamageType enum.
|
|
//
|
|
const char *damageTypeName = "Unspecified";
|
|
if (
|
|
!model_file->GetEntry(subsystem_name, "DamageType", &damageTypeName)
|
|
&& subsystem_resource->damageAmount == -1.0f
|
|
)
|
|
{
|
|
DEBUG_STREAM << model_name << " missing DamageType!";
|
|
return False;
|
|
}
|
|
if (strcmp(damageTypeName, "Unspecified") != 0)
|
|
{
|
|
if (strcmp(damageTypeName, "CollisionDamage") == 0)
|
|
subsystem_resource->damageType = Damage::CollisionDamageType; // 0
|
|
else if (strcmp(damageTypeName, "BallisticDamage") == 0)
|
|
subsystem_resource->damageType = Damage::BallisticDamageType; // 1
|
|
else if (strcmp(damageTypeName, "ExplosiveDamage") == 0)
|
|
subsystem_resource->damageType = Damage::ExplosiveDamageType; // 2
|
|
else if (strcmp(damageTypeName, "LaserDamage") == 0)
|
|
subsystem_resource->damageType = Damage::LaserDamageType; // 3
|
|
else if (strcmp(damageTypeName, "EnergyDamage") == 0)
|
|
subsystem_resource->damageType = Damage::EnergyDamageType; // 4
|
|
else
|
|
{
|
|
DEBUG_STREAM << model_name << " has an unknown DamageType of "
|
|
<< damageTypeName << std::endl;
|
|
return False;
|
|
}
|
|
}
|
|
|
|
//
|
|
// ExplosionModelFile: optional; "Unspecified" leaves it unset, otherwise the
|
|
// named model is looked up in the resource file.
|
|
//
|
|
const char *explosionModelName = "Unspecified";
|
|
if (
|
|
!model_file->GetEntry(subsystem_name, "ExplosionModelFile", &explosionModelName)
|
|
&& subsystem_resource->explosionResourceID == -1
|
|
)
|
|
{
|
|
DEBUG_STREAM << model_name << " missing ExplosionModelFile!";
|
|
return False;
|
|
}
|
|
|
|
ResourceDescription *explosion = 0;
|
|
if (strcmp(explosionModelName, "Unspecified") != 0)
|
|
{
|
|
explosion = resource_file->FindResourceDescription( // FUN_00406ff8
|
|
explosionModelName, (ResourceDescription::ResourceType)1, -1);
|
|
}
|
|
|
|
if (explosion == 0 && subsystem_resource->explosionResourceID == -1)
|
|
{
|
|
DEBUG_STREAM << model_name << " cannot find " << explosionModelName
|
|
<< " in resource file!";
|
|
return False;
|
|
}
|
|
|
|
if (explosion != 0)
|
|
{
|
|
subsystem_resource->explosionResourceID = explosion->resourceID;
|
|
}
|
|
|
|
return True;
|
|
}
|