Files
TeslaRel410/restoration/source410/BT/MECHWEAP.CPP
T
CydandClaude Fable 5 4168f1ad4d BT410 5.3.47: the whole authored watcher set is published -- cockpit renders in the pod
Ladder rungs, each run-verified on the pod rig: ReportLeak (HeatSink),
GeneratorOn (Generator -- member existed, publication missing),
ConfigureActivePress (MechSubsystem -- likewise), AmmoState +
FireCountdownStarted + the rest of the AmmoBin table.

THE PINNED RANGE IS NOW THE AUTHENTIC SHAPE.  Publishing ReportLeak on
HeatSink and ConfigureActivePress on MechSubsystem shifts the chain down two,
and MechWeapon's bridge to its binary-pinned PercentDone (0x12) collapses
from THREE guessed pads to ONE -- which is exactly the arithmetic the shipped
string pool predicts (MechSubsystem 1, HeatableSubsystem 3, HeatSink 6,
PoweredSubsystem 5).  Three pads of guesswork replaced by a real attribute
and a real base-class row.  HeatableSubsystem and Torso rebase onto
MechSubsystem::AttributeIndex; MechControlsMapper does not (it derives from
Subsystem directly).

Types were chosen deliberately this time, per the AudioWatcher families the
engine instantiates (Motion / Hinge / Scalar / StateIndicator): every *State
name resolves to a StateIndicator, ReportLeak is a Scalar.

RESULT: the pod run now gets past every authored watcher and DRAWS THE FULL
COCKPIT -- sensor cluster, myomers, cooling, the weapon panels, kills/deaths
-- with the board booted and audio running.  It then exits without flushing
its redirected stdout, so the exit reason is not yet known; next step is a
run with the redirect removed so the console is readable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 08:09:56 -05:00

506 lines
16 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
#if !defined(BTPLAYER_HPP)
# include <btplayer.hpp>
#endif
Derivation
MechWeapon::ClassDerivations(
PoweredSubsystem::ClassDerivations,
"MechWeapon"
);
//
//#############################################################################
// Message handlers (binary ids 9/10 @004b9550/@004b95b8) -- the weapon-group
// config buttons, chained onto the PoweredSubsystem generator panel (4-8),
// which chains HeatSink (3): a weapon's dispatch reaches the whole cockpit
// column. (The set MUST be a real defined object: the surviving CODE
// PPC.CPP binds the inherited name into PPC::DefaultData, and a
// declared-but-undefined static links as a zero-filled common block.)
//#############################################################################
//
const MechWeapon::HandlerEntry
MechWeapon::MessageHandlerEntries[] =
{
MESSAGE_ENTRY(MechWeapon, ConfigureMappables), // id 9 @004b9550
MESSAGE_ENTRY(MechWeapon, ChooseButton) // id 10 @004b95b8
};
MechWeapon::MessageHandlerSet
MechWeapon::MessageHandlers(
ELEMENTS(MechWeapon::MessageHandlerEntries),
MechWeapon::MessageHandlerEntries,
PoweredSubsystem::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[]=
{
//
// TWO pads now, not three: HeatSink grew ReportLeak, so
// PoweredSubsystem::NextAttributeID moved 0x0F -> 0x10 and the bridge
// to the binary-pinned PercentDone (0x12) is one slot shorter.
//
MECHWEAP_PAD( 0, "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,
PoweredSubsystem::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();
}
//
//#############################################################################
// DeathReset -- the weapon half of the respawn restore.
//#############################################################################
//
void
MechWeapon::DeathReset(Logical full_reset)
{
Check(this);
//
// The FULL powered-chain restore (base heal + thermal + electrical
// re-init): a mech that died hot must NOT respawn with guns still at
// FailureHeat (the hard gate + the jam roll read the carried
// temperature). The electrical FSM re-enters Starting and lifts to
// Ready on the next sim frame (startTimer holds startTime).
//
PoweredSubsystem::DeathReset(full_reset);
fireImpulse = 0.0f;
previousFireImpulse = 0.0f;
rechargeLevel = 0.0f;
recoil = 0.0f;
rangeToTarget = 0.0f;
targetWithinRange = False;
weaponAlarm.SetLevel(LoadingState);
}
//
//#############################################################################
// The weapon-group config buttons (ids 9/10). STAGED: the binary bodies
// (@004b9550/@004b95b8) drive the group-assign flow with the HUD config
// page; until that page exists the press is latched (configureActivePress)
// and logged -- the dispatch path itself is the live, authentic part.
//#############################################################################
//
void
MechWeapon::ConfigureMappablesMessageHandler(
ReceiverDataMessageOf<int> *message)
{
Check(this);
configureActivePress = message->dataContents;
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[btn] '" << GetName()
<< "' ConfigureMappables press=" << message->dataContents
<< " (staged latch)" << endl << flush;
}
}
void
MechWeapon::ChooseButtonMessageHandler(
ReceiverDataMessageOf<int> *message)
{
Check(this);
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[btn] '" << GetName()
<< "' ChooseButton press=" << message->dataContents
<< " (staged)" << endl << flush;
}
}
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)");
}
//
//#############################################################################
// GetMuzzlePoint (binary @004b9948) -- the mount segment's world-frame
// position: segmentToEntity x localToWorld (GetSegmentToWorld), translation
// extracted by transforming the segment-space origin. This is what makes
// fire leave FROM THE GUN and follow the torso twist.
//#############################################################################
//
void
MechWeapon::GetMuzzlePoint(Point3D &point)
{
Check(this);
point = owner->localOrigin.linearPosition;
if (GetSegmentIndex() >= 0)
{
EntitySegment *segment = owner->GetSegment(GetSegmentIndex());
if (segment != NULL)
{
LinearMatrix to_world;
owner->GetSegmentToWorld(*segment, &to_world);
point.Multiply(Point3D(0.0f, 0.0f, 0.0f), to_world);
}
}
}
//
//#############################################################################
// 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);
//
// The DAMAGE SCORE: post the type-0 ScoreInflicted to OUR pilot (the
// authentic producer sits in the unexported master-perf writer; the
// port shape posts per delivered hit). senderMechID = the VICTIM.
//
if (owner != NULL && owner->GetInstance() != Entity::ReplicantInstance
&& owner->GetPlayerLink() != NULL)
{
BTPlayer::ScoreMessage
damage_score(
BTPlayer::ScoreInflictedMessageID,
sizeof(BTPlayer::ScoreMessage),
0.0f,
BTPlayer::DamageInflictedScore,
damage.damageAmount,
target->GetEntityID()
);
owner->GetPlayerLink()->Dispatch(&damage_score);
}
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;
}