Files
TeslaRel410/restoration/source410/BT/MECHWEAP.CPP
T
CydandClaude Fable 5 110ecec2ca BT410 Phase 5.3.18: flying missiles -- spawn/home/detonate LIVE, teardown open
The Missile entity family is reconstructed: Projectile : Mover gets a real
make-path ctor; Missile hosts the Seeker + MissleThruster roster (the
AUTHENTIC surviving SEEKER.HPP / MISTHRST.HPP interfaces -- the 1995
misspelling included) and flies the binary's guided integrator (@004bef78):
age/lifetime, seeker re-lead with the decoded loft constants
(200/50/300/0.1), thruster burn, steering (gain 4.0, deadband 1e-4), and
the proximity fuse delivering the cluster damage ONCE with the launcher as
inflictor.  Live-verified end-to-end: LAUNCH -> flight telemetry (thruster
visibly accelerating) -> DETONATE on the target ~0.9s out.

OPEN: the first ~Missile off the death row completes, then the frame
page-faults with EIP in the heap -- the first entity/subsystem teardown the
reconstructed sim has ever run.  The spawn path is therefore OPT-IN
(BT_MISSILE_FLIGHT=1); default SRM delivery stays the 5.3.18a instant-hit
cluster (verified 1:1, zero exceptions).  Forensics + engine truths
(SearchList crashes on non-directory ids; MakeMessages need FAMILY resource
ids; explosionModelFile is a model-list INDEX) in MISSILE.NOTES.md.

Also fixed on the way: the Subsystem NAME ctor left damageZone
UNINITIALIZED (latent garbage for every name-built subsystem).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 11:58:40 -05:00

395 lines
12 KiB
C++

//===========================================================================//
// File: mechweap.cpp //
// Project: BattleTech Brick: Mech weapons //
// Contents: MechWeapon -- the base weapon subsystem //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
// All Rights reserved worldwide //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
//===========================================================================//
#include <bt.hpp>
#pragma hdrstop
#if !defined(MECHWEAP_HPP)
# include <mechweap.hpp>
#endif
#if !defined(MECH_HPP)
# include <mech.hpp>
#endif
#if !defined(SEGMENT_HPP)
# include <segment.hpp>
#endif
#include <string.h>
#include <stdlib.h>
#if !defined(RANDOM_HPP)
# include <random.hpp>
#endif
Derivation
MechWeapon::ClassDerivations(
PoweredSubsystem::ClassDerivations,
"MechWeapon"
);
//
//#############################################################################
// Message handlers. MechWeapon publishes no handlers of its own yet (the
// weapon fire/damage handler wave), but the set MUST be a real defined object:
// the surviving CODE PPC.CPP binds the inherited name (`PPC::MessageHandlers`)
// into PPC::DefaultData, and a declared-but-undefined static links as a
// zero-filled common block -- the same silent-NULL trap as the
// Emitter::AttributeIndex crash (see MECHWEAP.NOTES.md).
//#############################################################################
//
MechWeapon::MessageHandlerSet
MechWeapon::MessageHandlers(
0,
NULL,
Subsystem::MessageHandlers
);
//
//#############################################################################
// Attribute Support -- the 1995 binary table (@0x511890), IDs pinned so the
// streamed per-mech control mappings resolve: TriggerState (0x13) is what the
// authored fire buttons bind (the controls push then writes the trigger state
// straight into fireImpulse each frame). The pads bridge our chain gap
// 2..0x11 (parents publish nothing yet) -- REQUIRED, because Build leaves
// uncovered gap slots uninitialized; they target rechargeLevel, harmless if a
// stray mapping ever binds one (BT411 shipped the same). Pip*/WeaponRange
// pads-to-rechargeLevel where the member isn't reconstructed yet are noted.
//#############################################################################
//
#define MECHWEAP_PAD(n, name) \
{ (int)MechWeapon::MechWeaponPadFirstAttributeID + n, name, \
(Simulation::AttributePointer)&MechWeapon::rechargeLevel }
const MechWeapon::IndexEntry
MechWeapon::AttributePointers[]=
{
MECHWEAP_PAD( 0, "MechWeaponPad02"),
MECHWEAP_PAD( 1, "MechWeaponPad03"),
MECHWEAP_PAD( 2, "MechWeaponPad04"),
MECHWEAP_PAD( 3, "MechWeaponPad05"),
MECHWEAP_PAD( 4, "MechWeaponPad06"),
MECHWEAP_PAD( 5, "MechWeaponPad07"),
MECHWEAP_PAD( 6, "MechWeaponPad08"),
MECHWEAP_PAD( 7, "MechWeaponPad09"),
MECHWEAP_PAD( 8, "MechWeaponPad0A"),
MECHWEAP_PAD( 9, "MechWeaponPad0B"),
MECHWEAP_PAD(10, "MechWeaponPad0C"),
MECHWEAP_PAD(11, "MechWeaponPad0D"),
MECHWEAP_PAD(12, "MechWeaponPad0E"),
MECHWEAP_PAD(13, "MechWeaponPad0F"),
MECHWEAP_PAD(14, "MechWeaponPad10"),
MECHWEAP_PAD(15, "MechWeaponPad11"),
ATTRIBUTE_ENTRY(MechWeapon, PercentDone, rechargeLevel), // 0x12
ATTRIBUTE_ENTRY(MechWeapon, TriggerState, fireImpulse), // 0x13
ATTRIBUTE_ENTRY(MechWeapon, DistanceToTarget, rangeToTarget), // 0x14
ATTRIBUTE_ENTRY(MechWeapon, TargetWithinRange, targetWithinRange), // 0x15
ATTRIBUTE_ENTRY(MechWeapon, WeaponRange, effectiveRange), // 0x16
{ (int)MechWeapon::PipPositionAttributeID, "PipPosition", // 0x17 (pip
(Simulation::AttributePointer)&MechWeapon::rechargeLevel }, // members
{ (int)MechWeapon::PipColorAttributeID, "PipColor", // 0x18 not yet
(Simulation::AttributePointer)&MechWeapon::rechargeLevel }, // recon-
{ (int)MechWeapon::PipExtendedRangeAttributeID, "PipExtendedRange", // 0x19 structed)
(Simulation::AttributePointer)&MechWeapon::rechargeLevel },
ATTRIBUTE_ENTRY(MechWeapon, EstimatedReadyTime, estimatedReadyTime),// 0x1A
ATTRIBUTE_ENTRY(MechWeapon, RearFiring, rearFiring), // 0x1B
ATTRIBUTE_ENTRY(MechWeapon, WeaponState, weaponAlarm) // 0x1C
};
MechWeapon::AttributeIndexSet
MechWeapon::AttributeIndex(
ELEMENTS(MechWeapon::AttributePointers),
MechWeapon::AttributePointers,
Subsystem::AttributeIndex
);
MechWeapon::SharedData
MechWeapon::DefaultData(
MechWeapon::ClassDerivations,
MechWeapon::MessageHandlers,
MechWeapon::AttributeIndex,
Subsystem::StateCount
);
MechWeapon::MechWeapon(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
PoweredSubsystem(owner, subsystem_ID, subsystem_resource, shared_data)
{
Check(owner);
Check_Pointer(subsystem_resource);
rechargeRate = subsystem_resource->rechargeRate;
weaponRange = subsystem_resource->weaponRange;
damageAmount = subsystem_resource->damageAmount;
damageType = subsystem_resource->damageType;
heatCostToFire = subsystem_resource->heatCostToFire;
explosionResourceID = subsystem_resource->explosionModelFile;
targetWithinRange = False;
damageData.damageType = damageType;
damageData.damageAmount = damageAmount;
//
// Fire-machine state: spawn fully charged and Loaded (ready to fire).
//
fireImpulse = 0.0f;
previousFireImpulse = 0.0f;
rechargeLevel = 1.0f;
rangeToTarget = 0.0f;
effectiveRange = weaponRange;
estimatedReadyTime = 0;
recoil = 0.0f;
weaponAlarm.Initialize(WeaponStateCount);
weaponAlarm.SetLevel(LoadedState);
//
// REAR-FIRING resolve (the binary ctor tail @004b99a8): a weapon is
// rear-mounted when its MOUNT SEGMENT's site name carries the 'b' (back)
// marker -- the back gun ports (sitelbgunport / siterbgunport) are the only
// port names containing it. Rear-mounted weapons fire only into the
// LOOK-BACK view; everything else fires forward. The look-state commit
// (Mech::CommitLookState) re-arms viewFireEnable on every view change; a
// weapon spawns armed for the forward view.
//
rearFiring = False;
{
EntitySegment *mount = owner->GetSegment(GetSegmentIndex());
if (mount != NULL)
{
const char *mount_name = mount->GetName();
if (mount_name != NULL && strstr(mount_name, "b") != NULL)
{
rearFiring = True;
}
}
}
viewFireEnable = (rearFiring == False);
if (getenv("BT_MECH_LOG"))
{
EntitySegment *mount = owner->GetSegment(GetSegmentIndex());
DEBUG_STREAM << "[weap] '" << GetName()
<< "' seg=" << GetSegmentIndex() << " mount=";
if (mount != NULL)
{
DEBUG_STREAM << mount->GetName();
}
else
{
DEBUG_STREAM << "<none>";
}
DEBUG_STREAM << " rear=" << (int)rearFiring << endl << flush;
}
Check_Fpu();
}
MechWeapon::~MechWeapon()
{
}
Logical
MechWeapon::TestClass(Mech &)
{
return True;
}
//
//#############################################################################
// CheckFireEdge -- the rising-edge detector on fireImpulse (binary @004b9608).
// TriggerState (attr 0x13) carries the raw ControlsButton INT bit-copied by the
// streamed direct mapping (+(button+1) press / -(button+1) release), so the
// release value reads as a negative NaN in float terms; the binary's x87
// compare effectively treated it as "released". Comparing the BIT PATTERNS as
// signed ints is sign-correct for the button ints AND for genuine float values
// (0.0f = 0x0, positives > 0, negatives/NaNs < 0), reproducing the binary's
// behavior exactly.
//#############################################################################
//
Logical
MechWeapon::CheckFireEdge()
{
Check(this);
int current = *(int *)&fireImpulse;
int previous = *(int *)&previousFireImpulse;
Logical edge = (current > 0 && previous <= 0) ? True : False;
previousFireImpulse = fireImpulse;
return edge;
}
//
//#############################################################################
// ComputeOutputVoltage -- the recharge-dial writer (binary @004b9c9c, vtable
// slot the Emitter overrides with its own voltage form):
// rechargeLevel = (rechargeRate - recoil) / rechargeRate
// The Loading tick decays recoil from rechargeRate toward 0, so the dial rises
// 0 -> 1 over the authored RechargeRate seconds.
//#############################################################################
//
void
MechWeapon::ComputeOutputVoltage()
{
Check(this);
rechargeLevel = (rechargeRate - recoil) / rechargeRate;
}
Logical
MechWeapon::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
//
//#############################################################################
// FireWeapon Abstract -- each concrete weapon (Emitter/PPC, ProjectileWeapon/
// Gauss/MissileLauncher) overrides this. Reaching the base is a bug.
//#############################################################################
//
void
MechWeapon::FireWeapon()
{
Fail("MechWeapon::FireWeapon -- should not be here (abstract)");
}
//
//#############################################################################
// HasActiveTarget -- the owner mech's target slot (binary owner+0x388).
//#############################################################################
//
Logical
MechWeapon::HasActiveTarget()
{
Check(this);
return (owner != NULL && owner->GetTargetEntity() != NULL)
? True : False;
}
//
//#############################################################################
// UpdateTargeting -- refresh the range to the current target and the
// within-range flag (binary @004b9bdc). effectiveRange is the (heat-degraded,
// later) weapon range.
//#############################################################################
//
Logical
MechWeapon::UpdateTargeting()
{
Check(this);
Entity *target = (owner != NULL) ? owner->GetTargetEntity() : NULL;
if (target == NULL)
{
targetWithinRange = False;
return False;
}
Vector3D span;
span.Subtract(
target->localOrigin.linearPosition,
owner->localOrigin.linearPosition
);
rangeToTarget = span.Length();
targetWithinRange = (rangeToTarget <= effectiveRange) ? True : False;
return targetWithinRange;
}
//
//#############################################################################
// SendDamage Deliver this weapon's damage to the struck entity: build the
// engine TakeDamageMessage (inflictor = our mech) and dispatch it AT the
// target, whose handler routes damageZones[zone]->TakeDamage with the
// authored per-type scales. INTERIM zone pick: a uniform roll over the
// target's hull zones -- the authentic resolver is the reticle hit (aimed) or
// the cylinder hit-location table (unaimed), both in the render/mech3 wave.
//#############################################################################
//
void
MechWeapon::SendDamage(Entity *target, Damage &damage)
{
Check(this);
Check(target);
int zone = -1;
if (target->damageZoneCount > 0)
{
zone = Random(target->damageZoneCount);
//
// DEV hook BT_FORCE_ZONE=n: pin every hit to zone n (concentrated
// fire -- exercises the zone-death cascade headlessly).
//
{
const char *force_zone = getenv("BT_FORCE_ZONE");
if (force_zone != NULL)
{
int forced = atoi(force_zone);
if (forced >= 0 && forced < target->damageZoneCount)
{
zone = forced;
}
}
}
}
Entity::TakeDamageMessage
hit(
Entity::TakeDamageMessageID,
sizeof(Entity::TakeDamageMessage),
(owner != NULL) ? owner->GetEntityID() : EntityID::Null,
zone,
damage
);
target->Dispatch(&hit);
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[dmg] '" << GetName()
<< "' hit zone " << zone
<< " amount=" << damage.damageAmount
<< " type=" << damage.damageType;
if (zone >= 0 && zone < target->damageZoneCount
&& target->damageZones != NULL
&& target->damageZones[zone] != NULL)
{
DEBUG_STREAM << " -> zoneLevel="
<< target->damageZones[zone]->damageLevel;
}
DEBUG_STREAM << endl << flush;
}
}
//
//#############################################################################
// CreateStreamedSubsystem Model-load-time construction. Not yet reconstructed.
//#############################################################################
//
int
MechWeapon::CreateStreamedSubsystem(
ResourceFile *,
NotationFile *,
const char *,
const char *,
SubsystemResource *,
NotationFile *,
const ResourceDirectories *,
int
)
{
Fail("MechWeapon::CreateStreamedSubsystem -- mechweap.cpp not yet reconstructed");
return 0;
}