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>
352 lines
11 KiB
C++
352 lines
11 KiB
C++
//===========================================================================//
|
|
// File: mechsub.cpp //
|
|
// Project: BattleTech Brick: Mech subsystems //
|
|
// Contents: Implementation details for the MechSubsystem base //
|
|
//---------------------------------------------------------------------------//
|
|
// 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(MECHSUB_HPP)
|
|
# include <mechsub.hpp>
|
|
#endif
|
|
|
|
#if !defined(MECH_HPP)
|
|
# include <mech.hpp>
|
|
#endif
|
|
|
|
#if !defined(DAMAGE_HPP)
|
|
# include <damage.hpp>
|
|
#endif
|
|
|
|
#if !defined(BTPLAYER_HPP)
|
|
# include <btplayer.hpp>
|
|
#endif
|
|
|
|
#if !defined(BTMSSN_HPP)
|
|
# include <btmssn.hpp>
|
|
#endif
|
|
|
|
//
|
|
//#############################################################################
|
|
// Shared data support -- MechSubsystem adds no message handlers or attributes
|
|
// that are boot-critical, so it reuses the base Subsystem sets (which are the
|
|
// inherited Simulation sets).
|
|
//#############################################################################
|
|
//
|
|
Derivation
|
|
MechSubsystem::ClassDerivations(
|
|
Subsystem::ClassDerivations,
|
|
"MechSubsystem"
|
|
);
|
|
|
|
const MechSubsystem::IndexEntry
|
|
MechSubsystem::AttributePointers[]=
|
|
{
|
|
ATTRIBUTE_ENTRY(MechSubsystem, ConfigureActivePress, configureActivePress)
|
|
};
|
|
|
|
MechSubsystem::AttributeIndexSet
|
|
MechSubsystem::AttributeIndex(
|
|
ELEMENTS(MechSubsystem::AttributePointers),
|
|
MechSubsystem::AttributePointers,
|
|
Subsystem::AttributeIndex
|
|
);
|
|
|
|
MechSubsystem::SharedData
|
|
MechSubsystem::DefaultData(
|
|
MechSubsystem::ClassDerivations,
|
|
Subsystem::MessageHandlers,
|
|
MechSubsystem::AttributeIndex,
|
|
Subsystem::StateCount
|
|
);
|
|
|
|
//
|
|
//#############################################################################
|
|
// Light constructor (name + classID). For a subsystem created without a
|
|
// streamed resource; builds a trivial (armour-free) damage zone named by the
|
|
// base.
|
|
//#############################################################################
|
|
//
|
|
MechSubsystem::MechSubsystem(
|
|
Mech *owner,
|
|
int subsystem_ID,
|
|
const char *subsystem_name,
|
|
RegisteredClass::ClassID class_id,
|
|
SharedData &shared_data
|
|
):
|
|
Subsystem(owner, subsystem_ID, subsystem_name, class_id, shared_data)
|
|
{
|
|
this->owner = owner;
|
|
vitalSubsystem = False;
|
|
alarmModel = ResourceDescription::NullResourceID;
|
|
criticalReference = 0.0f;
|
|
collisionCriticalHitWeight = 0.0f;
|
|
printSimulationState = False;
|
|
configureActivePress = -1;
|
|
resource = NULL;
|
|
statusAlarm.SetLevel(0);
|
|
|
|
//
|
|
// The base Subsystem ctor leaves damageZone NULL; give this subsystem a
|
|
// trivial (armour-free) zone.
|
|
//
|
|
damageZone = new DamageZone(owner, 0);
|
|
Register_Object(damageZone);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// Resource constructor. Chains the base Subsystem resource ctor and seeds the
|
|
// mech-specific fields from the subsystem resource.
|
|
//
|
|
// The 4.10 damage model is per-damage-TYPE: the damage zone owns
|
|
// defaultArmorPoints + damageScale[5] and streams them itself (a
|
|
// Mech__DamageZone built from the model's damage-zone stream). Wiring that
|
|
// stream through from the resource is deferred (see MECHSUB.NOTES.md); until
|
|
// then the subsystem gets a trivial armour-free zone so it constructs and takes
|
|
// damage with default (unscaled) behaviour.
|
|
//#############################################################################
|
|
//
|
|
MechSubsystem::MechSubsystem(
|
|
Mech *owner,
|
|
int subsystem_ID,
|
|
SubsystemResource *subsystem_resource,
|
|
SharedData &shared_data
|
|
):
|
|
Subsystem(owner, subsystem_ID, subsystem_resource, shared_data)
|
|
{
|
|
Check_Pointer(subsystem_resource);
|
|
|
|
this->owner = owner;
|
|
resource = subsystem_resource;
|
|
vitalSubsystem = (subsystem_resource->vitalSubsystemIndex == 1);
|
|
alarmModel = subsystem_resource->alarmModel;
|
|
printSimulationState = subsystem_resource->printSimulationState;
|
|
criticalReference = subsystem_resource->criticalReference;
|
|
collisionCriticalHitWeight = subsystem_resource->collisionCriticalHitWeight;
|
|
configureActivePress = -1;
|
|
statusAlarm.SetLevel(0);
|
|
|
|
//
|
|
// The subsystem's PRIVATE damage zone (binary @4abf28: `new
|
|
// DamageZone(this, 0)` -- owned by the SUBSYSTEM, index 0, NOT an entry
|
|
// in the entity's hull-zone array). The resource authors its armor the
|
|
// same way the hull stream does -- structureReference points +
|
|
// armorByFacing[5] per-type values -- normalized with the same rule as
|
|
// Mech::DamageZone (scale = 1/(raw x points); already-normalized cells
|
|
// pass through). This is what makes critical hits MEASURABLE
|
|
// (ApplyDamageAndMeasure) and the crit-allotment push meaningful.
|
|
//
|
|
damageZone = new DamageZone(this, 0);
|
|
Register_Object(damageZone);
|
|
damageZone->damageZoneName = GetName();
|
|
if (subsystem_resource->structureReference > 0.0f)
|
|
{
|
|
damageZone->defaultArmorPoints = subsystem_resource->structureReference;
|
|
for (int tt = 0; tt < Damage::DamageTypeCount; ++tt)
|
|
{
|
|
Scalar raw = subsystem_resource->armorByFacing[tt];
|
|
Scalar even = 1.0f / subsystem_resource->structureReference;
|
|
Scalar diff = raw - even;
|
|
if (diff < 0.0f)
|
|
{
|
|
diff = -diff;
|
|
}
|
|
if (diff > 1.0e-4f && raw > 0.0f)
|
|
{
|
|
damageZone->damageScale[tt] =
|
|
1.0f / (raw * subsystem_resource->structureReference);
|
|
}
|
|
else
|
|
{
|
|
damageZone->damageScale[tt] = raw;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
MechSubsystem::~MechSubsystem()
|
|
{
|
|
if (damageZone)
|
|
{
|
|
Unregister_Object(damageZone);
|
|
delete damageZone;
|
|
damageZone = NULL;
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
Logical
|
|
MechSubsystem::TestClass(Mech &)
|
|
{
|
|
return True;
|
|
}
|
|
|
|
Logical
|
|
MechSubsystem::TestInstance() const
|
|
{
|
|
return IsDerivedFrom(ClassDerivations);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// Damage support -- the 4.10 damage zone tracks damageLevel in [0,1]
|
|
// (0 = intact, 1 = destroyed).
|
|
//#############################################################################
|
|
//
|
|
Scalar
|
|
MechSubsystem::GetSubsystemDamageLevel() const
|
|
{
|
|
Check(this);
|
|
return damageZone ? damageZone->damageLevel : 0.0f;
|
|
}
|
|
|
|
void
|
|
MechSubsystem::SetSubsystemDamageLevel(Scalar level)
|
|
{
|
|
Check(this);
|
|
if (damageZone)
|
|
{
|
|
damageZone->damageLevel = level;
|
|
}
|
|
}
|
|
|
|
void
|
|
MechSubsystem::ForceCriticalFailure()
|
|
{
|
|
Check(this);
|
|
//
|
|
// Binary @4ac8xx family: the status alarm's DESTROYED display level is 1
|
|
// (the TechStatusType enum's Destroyed=0 is the display CATEGORY, not
|
|
// the alarm level -- BT411 disasm: "+0x2C: 1 = Destroyed").
|
|
//
|
|
statusAlarm.SetLevel(1);
|
|
//
|
|
// DestroyedState is the hard-failure gate the weapon simulations poll
|
|
// (`GetSimulationState() == 1`): a destroyed weapon drops its charge /
|
|
// pins the unavailable alarm from the next frame on.
|
|
//
|
|
SetSimulationState(DestroyedState);
|
|
if (damageZone)
|
|
{
|
|
damageZone->SetDamageZoneState(DamageZone::BurningState);
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// DeathReset -- the respawn restore (the engine base virtual pair's reset
|
|
// half; Mech::Reset @0049fb74 loops every subsystem through it). The base:
|
|
// heal the private zone, clear the Destroyed display level and -- the
|
|
// load-bearing part -- clear DestroyedState so the weapon hard gates
|
|
// (GetSimulationState() == 1) release.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
MechSubsystem::DeathReset(Logical /*full_reset*/)
|
|
{
|
|
Check(this);
|
|
|
|
if (damageZone != NULL)
|
|
{
|
|
damageZone->damageLevel = 0.0f;
|
|
damageZone->SetDamageZoneState(0);
|
|
}
|
|
statusAlarm.SetLevel(0);
|
|
SetSimulationState(Simulation::DefaultState);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// ApplyDamageAndMeasure (binary @4ac07c): apply through the virtual
|
|
// TakeDamage and report the zone-level delta -- the critical accountant
|
|
// (Mech::DamageZone::CriticalHit) charges the delta against the entry's
|
|
// damagePercentage allotment.
|
|
//#############################################################################
|
|
//
|
|
Scalar
|
|
MechSubsystem::ApplyDamageAndMeasure(Damage &damage)
|
|
{
|
|
Check(this);
|
|
Scalar before = (damageZone != NULL) ? damageZone->damageLevel : 0.0f;
|
|
TakeDamage(damage);
|
|
Scalar after = (damageZone != NULL) ? damageZone->damageLevel : 0.0f;
|
|
return after - before;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// TakeDamage Forward to the damage zone (per-type scaling happens there).
|
|
//#############################################################################
|
|
//
|
|
void
|
|
MechSubsystem::TakeDamage(Damage &damage)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(&damage);
|
|
if (damageZone)
|
|
{
|
|
damageZone->TakeDamage(damage);
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// CreateStreamedSubsystem Model-load-time construction of the subsystem
|
|
// resource + its damage-zone stream. Not yet reconstructed (see
|
|
// MECHSUB.NOTES.md -- the 4.10 per-type damage-zone stream format).
|
|
//#############################################################################
|
|
//
|
|
int
|
|
MechSubsystem::CreateStreamedSubsystem(
|
|
NotationFile *,
|
|
const char *,
|
|
const char *,
|
|
SubsystemResource *,
|
|
NotationFile *,
|
|
const ResourceDirectories *,
|
|
int
|
|
)
|
|
{
|
|
Fail("MechSubsystem::CreateStreamedSubsystem -- mechsub.cpp not yet reconstructed");
|
|
return 0;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// NoviceLockout -- the novice-experience lockout predicate (binary @4ac9c8):
|
|
// owner mech -> Entity::playerLink -> the BTPlayer's experience level == 0.
|
|
// Locks the advanced cockpit controls (condenser valves, coolant flush,
|
|
// cooling toggles). An unlinked mech reads UNLOCKED (dev-permissive).
|
|
//#############################################################################
|
|
//
|
|
Logical
|
|
MechSubsystem::NoviceLockout()
|
|
{
|
|
Check(this);
|
|
|
|
if (owner == NULL)
|
|
{
|
|
return False;
|
|
}
|
|
BTPlayer *player = Cast_Object(BTPlayer *, owner->GetPlayerLink());
|
|
if (player == NULL)
|
|
{
|
|
return False;
|
|
}
|
|
return (player->GetExperienceLevel() == BTMission::NoviceMode)
|
|
? True : False;
|
|
}
|