The gap found in 5.3.131 is closed. HeatWatcher has always streamed a
watchedSubsystem index and read it into a member; nothing ever turned it
into the link UpdateWatch resolves, so the entire watcher family --
PowerWatcher and the Gyroscope / Torso / HUD / AmmoBin leaves included --
resolved NULL and reported its target dead.
The Mech ctor now binds them in a post-walk pass, which is where it has
to be: a watcher may legally watch a subsystem with a higher roster id,
which does not exist yet while the segment walk is still building.
Out-of-range and self-referencing indices are skipped rather than
trusted.
THE BINDINGS PROVE THEMSELVES -- every index lands on a semantically
right target, which a wrong offset could not manage:
Gyroscope -> Avionics Torso -> Myomers
HUD -> Avionics Searchlight -> Avionics
AmmoBinAFC100 -> AFC100 AmmoBinLRM15_1 -> LRM15_1
AmmoBinLRM15_2 -> LRM15_2
The gyro and HUD watch the avionics bus, the torso watches the muscles
that move it, and every ammo bin watches its own gun.
With the link live the gyro's watchdog moved off the NULL fallback (0 ->
1) but has not reached Ready. UpdateWatch can produce 1 two ways --
NoVoltage, or a brownout on a target that IS ready -- and since Avionics
is a Sensor reporting voltState 4, the brownout branch is the suspect.
Recorded as the next question with the experiment that separates the two
causes, rather than quieting an alarm by moving a threshold.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3293 lines
105 KiB
C++
3293 lines
105 KiB
C++
//===========================================================================//
|
|
// File: mech.cpp //
|
|
// Project: BattleTech Brick: Entity Manager //
|
|
// Contents: Implementation details for the Mech entity //
|
|
//---------------------------------------------------------------------------//
|
|
// Date Who Modification //
|
|
// -------- --- ---------------------------------------------------------- //
|
|
// //
|
|
//---------------------------------------------------------------------------//
|
|
// 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(MECH_HPP)
|
|
# include <mech.hpp>
|
|
#endif
|
|
|
|
#if !defined(APP_HPP)
|
|
# include <app.hpp>
|
|
#endif
|
|
#if !defined(MEMSTRM_HPP)
|
|
# include <memstrm.hpp>
|
|
#endif
|
|
#if !defined(RESOURCE_HPP)
|
|
# include <resource.hpp>
|
|
#endif
|
|
|
|
// The subsystem roster the segment walk instantiates.
|
|
#include <heat.hpp> // HeatableSubsystem, HeatSink, HeatWatcher, Condenser
|
|
#include <powersub.hpp> // PoweredSubsystem, PowerWatcher, Generator
|
|
#include <reservr.hpp> // Reservoir
|
|
#include <sensor.hpp> // Sensor
|
|
#include <gyro.hpp> // Gyroscope
|
|
#include <torso.hpp> // Torso
|
|
#include <myomers.hpp> // Myomers
|
|
#include <hud.hpp> // HUD
|
|
#include <searchlt.hpp> // Searchlight
|
|
#include <thermsgt.hpp> // ThermalSight
|
|
#include <mechtech.hpp> // MechTech
|
|
#include <messmgr.hpp> // SubsystemMessageManager
|
|
#include <mechweap.hpp> // MechWeapon
|
|
#include <emitter.hpp> // Emitter
|
|
#include <ppc.hpp> // PPC
|
|
#include <gauss.hpp> // GaussRifle
|
|
#include <projweap.hpp> // ProjectileWeapon
|
|
#include <mislanch.hpp> // MissileLauncher
|
|
#include <ammobin.hpp> // AmmoBin
|
|
#include <mechmppr.hpp> // MechControlsMapper -- the drive reads its demands
|
|
#include <joint.hpp> // Joint / JointSubsystem -- ResolveJoint
|
|
#include <segment.hpp> // EntitySegment -- the skeleton segment table
|
|
#include <mechdmg.hpp> // Mech::DamageZone -- the hull zone fill (Pass 3)
|
|
#if !defined(BOXSOLID_HPP)
|
|
# include <boxsolid.hpp> // BoxedSolid extents -- the ground-snap probe
|
|
#endif
|
|
#if !defined(RANDOM_HPP)
|
|
# include <random.hpp> // Random -- the collision-rattle draw
|
|
#endif
|
|
#if !defined(CULTURAL_HPP)
|
|
# include <cultural.hpp> // CulturalIcon -- the crunch / crushable sentinel
|
|
#endif
|
|
#include <dmgtable.hpp> // DamageLookupTable -- the cylinder hit table
|
|
#include <player.hpp> // Player::VehicleDeadMessage -- the death notification
|
|
#include <btplayer.hpp> // BTPlayer::ScoreMessage -- the kill credit
|
|
#include <hostmgr.hpp> // HostManager::GetEntityPointer -- killer resolve
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
Derivation
|
|
Mech::ClassDerivations(
|
|
JointedMover::ClassDerivations,
|
|
"Mech"
|
|
);
|
|
|
|
//
|
|
// The Mech's OWN handler table, chained to the JointedMover set. The
|
|
// TakeDamage entry OVERRIDES Entity's by message ID (Build overlays the
|
|
// inherited slot -- no gap risk, the parent chain already registered the
|
|
// ID); every other inherited message still routes to its base handler.
|
|
//
|
|
const Receiver::HandlerEntry
|
|
Mech::MessageHandlerEntries[] =
|
|
{
|
|
MESSAGE_ENTRY(Mech, TakeDamage),
|
|
MESSAGE_ENTRY(Mech, RealMaxSpeed),
|
|
MESSAGE_ENTRY(Mech, SetBurningState),
|
|
MESSAGE_ENTRY(Mech, ClearBurningState),
|
|
MESSAGE_ENTRY(Mech, BalanceCoolant),
|
|
MESSAGE_ENTRY(Mech, EjectPilot),
|
|
MESSAGE_ENTRY(Mech, DuckRequest)
|
|
};
|
|
|
|
Mech::MessageHandlerSet
|
|
Mech::MessageHandlers(
|
|
ELEMENTS(Mech::MessageHandlerEntries),
|
|
Mech::MessageHandlerEntries,
|
|
JointedMover::MessageHandlers
|
|
);
|
|
|
|
//
|
|
//#############################################################################
|
|
// The entity-level attribute table (cockpit binding by name).
|
|
//#############################################################################
|
|
//
|
|
const Mech::IndexEntry
|
|
Mech::AttributePointers[]=
|
|
{
|
|
ATTRIBUTE_ENTRY(Mech, RadarRange, radarRange),
|
|
ATTRIBUTE_ENTRY(Mech, RadarLinearPosition, radarLinearPosition),
|
|
ATTRIBUTE_ENTRY(Mech, RadarAngularPosition, radarAngularPosition),
|
|
ATTRIBUTE_ENTRY(Mech, LinearSpeed, currentBodySpeed),
|
|
ATTRIBUTE_ENTRY(Mech, MaxRunSpeed, reverseStrideLength),
|
|
ATTRIBUTE_ENTRY(Mech, DuckState, duckState),
|
|
ATTRIBUTE_ENTRY(Mech, EyepointRotation, eyepointRotation),
|
|
ATTRIBUTE_ENTRY(Mech, UnstablePercentage, unstablePercentage),
|
|
ATTRIBUTE_ENTRY(Mech, CollisionSpeed, collisionSpeed),
|
|
ATTRIBUTE_ENTRY(Mech, DistanceToMissile, distanceToMissile),
|
|
ATTRIBUTE_ENTRY(Mech, FootStep, footStep),
|
|
ATTRIBUTE_ENTRY(Mech, IncomingLock, incomingLock),
|
|
ATTRIBUTE_ENTRY(Mech, CollisionState, collisionState),
|
|
ATTRIBUTE_ENTRY(Mech, CollisionNormal, collisionNormal),
|
|
ATTRIBUTE_ENTRY(Mech, AnimationState, animationState),
|
|
ATTRIBUTE_ENTRY(Mech, ReduceButton, reduceButton)
|
|
};
|
|
|
|
Mech::AttributeIndexSet
|
|
Mech::AttributeIndex(
|
|
ELEMENTS(Mech::AttributePointers),
|
|
Mech::AttributePointers,
|
|
JointedMover::AttributeIndex
|
|
);
|
|
|
|
Mech::SharedData
|
|
Mech::DefaultData(
|
|
Mech::ClassDerivations,
|
|
Mech::MessageHandlers,
|
|
Mech::AttributeIndex,
|
|
33,
|
|
(Entity::MakeHandler)Mech::Make
|
|
);
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
Mech*
|
|
Mech::Make(MakeMessage *creation_message)
|
|
{
|
|
return new Mech(creation_message);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// Mech ctor -- the heart of the entity (walks the model segment table and
|
|
// instantiates the full subsystem roster: power, heat, weapons, actuators,
|
|
// controls, tech, damage zones). This is the largest single function in the
|
|
// game and the current reconstruction frontier; see MECH.NOTES.md. Chains to
|
|
// the JointedMover base so the skeleton/segments stream before it Fails.
|
|
//#############################################################################
|
|
//
|
|
Mech::Mech(
|
|
MakeMessage *creation_message,
|
|
SharedData &shared_data
|
|
):
|
|
JointedMover(creation_message, shared_data),
|
|
controllableSubsystems(this),
|
|
watchedSubsystems(this),
|
|
heatableSubsystems(this),
|
|
myomerSubsystems(this),
|
|
weaponRoster(this),
|
|
damageableSubsystems(this)
|
|
{
|
|
Check_Pointer(creation_message);
|
|
|
|
//
|
|
// Cached subsystem back-pointers -- filled by the segment walk below.
|
|
//
|
|
sensorSubsystem = NULL;
|
|
gyroSubsystem = NULL;
|
|
sinkSourceSubsystem = NULL;
|
|
hudSubsystem = NULL;
|
|
messageManager = NULL;
|
|
weaponCount = 0;
|
|
|
|
//
|
|
// Embedded status / animation / naming state.
|
|
//
|
|
mechNameFilter.Initialize();
|
|
masterAlarm.Initialize(0x21);
|
|
heatAlarm.Initialize(3);
|
|
stabilityAlarm.Initialize(2);
|
|
statusAlarm.Initialize(0x21);
|
|
|
|
targetReticle.reticleState = Reticle::ReticleOn;
|
|
targetReticle.pickPointingOn = True;
|
|
targetReticle.reticleElementMask = Reticle::AllEnabledGroup;
|
|
|
|
animationState = StateIndicator(0x21);
|
|
animationState.SetState(0);
|
|
replicantAnimationState = StateIndicator(0x21);
|
|
replicantAnimationState.SetState(0);
|
|
collisionState = StateIndicator(4);
|
|
collisionState.SetState(0);
|
|
|
|
{
|
|
for (int i = 0; i < 5; ++i)
|
|
{
|
|
telemetryFilter[i].SetSize(15, 0.0f);
|
|
}
|
|
}
|
|
|
|
legAnimation.Init(this);
|
|
bodyAnimation.Init(this);
|
|
|
|
//
|
|
// Locomotion parameters. BRING-UP DEFAULTS: the authentic values come from
|
|
// the Mech model resource (WalkingTurnRate / RunningTurnRate / MaxAcceleration)
|
|
// and LoadLocomotionClips (the stride/top speeds measured from the walk/run
|
|
// animation clips). Wiring those in is a later refinement (needs the model-
|
|
// resource pointer + clip loader); until then these sane defaults make the
|
|
// mech drivable with the authentic control-interpretation + drive math.
|
|
//
|
|
walkingTurnRate = 50.0f * RAD_PER_DEG; // rad/s (walk / turn-in-place)
|
|
runningTurnRate = 25.0f * RAD_PER_DEG; // rad/s (at run speed)
|
|
reverseStrideLength = 30.0f; // top/run speed (u/s)
|
|
walkStrideLength = 12.0f; // walk speed (u/s)
|
|
reverseSpeedMax = 2.0f; // low-speed turn-rate gate (u/s)
|
|
forwardThrottleScale= 1.0f;
|
|
maxBodyAcceleration = 30.0f; // u/s^2
|
|
bodyTargetSpeed = 0.0f;
|
|
currentBodySpeed = 0.0f;
|
|
|
|
//
|
|
// Gait channel state (mech2.cpp). The measured constants (standSpeed,
|
|
// gimpSpeedMax, gimpStrideLength and the four limp figures) get their
|
|
// real values in LoadLocomotionClips below; these defaults keep every
|
|
// divide in the transition machines finite if a clip set is missing.
|
|
// globalTimeScale MUST default to 1 -- zero would silence every clip
|
|
// advance. Unfilled clip slots hold NullResourceID, which SelectSequence
|
|
// resolves to an empty, inert controller.
|
|
//
|
|
legCycleSpeed = 0.0f;
|
|
bodyCycleSpeed = 0.0f;
|
|
forwardCycleRate = 1.0f;
|
|
gimpCycleRate = 1.0f;
|
|
standSpeed = 1.0f;
|
|
gimpSpeedMax = 1.0f;
|
|
gimpStrideLength = -1.0f; // the measured value is negative too
|
|
globalTimeScale = 1.0f;
|
|
hasGimpClips = 0;
|
|
squatCapable = 0;
|
|
turnCapable = 0;
|
|
usingExteriorClips = 0;
|
|
gimpLeftSpeedMax = 1.0f;
|
|
gimpRightSpeedMax = 1.0f;
|
|
gimpLeftStrideLength = 1.0f;
|
|
gimpRightStrideLength = 1.0f;
|
|
gyroRumbleTimer = 0.0f;
|
|
idleStrideScale = 1.0f;
|
|
runSpeedMax = 1.0e9f; // UNSOURCED cap -- never binds until
|
|
// its real source is found
|
|
motionEventArmed = 0;
|
|
deathAnimationLatched = 0;
|
|
legResetLatch = 0;
|
|
bodyResetLatch = 0;
|
|
limpModeOverride = 0;
|
|
collisionTemporaryState = 0;
|
|
{
|
|
const char *fl = getenv("BT_FORCE_LIMP");
|
|
if (fl != NULL && (*fl == '3' || *fl == '4'))
|
|
{
|
|
limpModeOverride = *fl - '0';
|
|
}
|
|
}
|
|
{
|
|
int i;
|
|
for (i = 0; i < AnimationSlotCount; ++i)
|
|
{
|
|
animationClips[i] = ResourceDescription::NullResourceID;
|
|
}
|
|
}
|
|
|
|
eyepointRotation = EulerAngles::Identity;
|
|
lookPitch = 0.0f;
|
|
lookYaw = 0.0f;
|
|
targetEntity = NULL;
|
|
lastInflictingID = EntityID::Null;
|
|
damageLookupTable = NULL;
|
|
deathTransitionDone = 0;
|
|
//
|
|
// Cockpit-published state: the radar follows our own live transform.
|
|
//
|
|
radarRange = 4000.0f; // the authored map() max range
|
|
radarLinearPosition = &localOrigin.linearPosition;
|
|
radarAngularPosition = &localOrigin.angularPosition;
|
|
duckState = 0;
|
|
|
|
//
|
|
// The duck system's rest state (binary ctor tail @004a1674: the volume
|
|
// alarm at +0x4c4 initialises to level 1 = STANDING, and the two
|
|
// cylinder heights capture template maxY and 0.6 x it -- the heights
|
|
// are seeded after the collision template resolves, below).
|
|
//
|
|
duckRequestLatch = 0;
|
|
duckPhaseRequest = 0;
|
|
mobilityScale = 1.0f; // [T1] held at "legs healthy" until the
|
|
// +0x7ac leg-roster scan lands; the
|
|
// binary re-derives it every frame
|
|
defaultStateRequests = 0; // the streamed mode-request counters
|
|
gimpLeftRequests = 0; // (binary ctor @004a1674 zeroes
|
|
gimpRightRequests = 0; // +0x334/+0x338/+0x33c)
|
|
runSpeedMaxKnown = 0; // set by the Myomers sweep (see MECH.HPP)
|
|
runSpeedMaxSent = 0;
|
|
superStopping = 0;
|
|
bodyAccelRate = 0.0f; // all three seeded from the model
|
|
forwardAccelRate = 0.0f; // resource below (rec+0x44 / rec+0x48)
|
|
superStopRate = -1.1f; // the DISABLED sentinel until read
|
|
superStopShudderTimer = 0.0f;
|
|
bodyAcceleration = Vector3D(0.0f, 0.0f, 0.0f);
|
|
bodyTurnAcceleration = 0.0f;
|
|
previousOlympicVelocityZ = 0.0f;
|
|
previousMeanVelocityX = 0.0f;
|
|
previousMeanTurnRate = 0.0f;
|
|
previousMeanVelocityZ = 0.0f;
|
|
maxUnstableAcceleration = 0.0f; // the instability constants, likewise
|
|
unstableAccelerationEffect = 0.0f;
|
|
unstableGunTheEngineEffect = 0.0f;
|
|
unstableSuperStopEffect = 0.0f;
|
|
unstableHighVelocityEffect = 0.0f;
|
|
unstableStopedTurnEffect = 0.0f;
|
|
combatIneffective = 0; // the @0049fa1c census cell (+0x414)
|
|
minimumWeaponCount = 2; // AUTHENTIC ctor constant (+0x448,
|
|
// part_012.c:15749: param_1[0x112] = 2)
|
|
collisionVolumeState.Initialize(2);
|
|
collisionVolumeState.SetLevel(1);
|
|
standingVolumeHeight = 0.0f;
|
|
duckedVolumeHeight = 0.0f;
|
|
if (collisionTemplate != NULL)
|
|
{
|
|
standingVolumeHeight = collisionTemplate->maxY;
|
|
duckedVolumeHeight = 0.6f * standingVolumeHeight;
|
|
}
|
|
unstablePercentage = 0.0f; // staged: no instability model yet
|
|
collisionSpeed = 0.0f; // staged: audio watcher set (see MECH.HPP)
|
|
distanceToMissile = 0.0f;
|
|
footStep = 0;
|
|
incomingLock = 0;
|
|
reduceButton = 0;
|
|
lastInflictingDamage = 0.0f;
|
|
|
|
//
|
|
// Look-view angles: defaults until the GameModel read below overrides them
|
|
// with the authored per-mech values.
|
|
//
|
|
lookLeftAngle = 90.0f * RAD_PER_DEG;
|
|
lookRightAngle = -90.0f * RAD_PER_DEG;
|
|
lookFrontAngle = -30.0f * RAD_PER_DEG;
|
|
lookBackAngle = 0.0f;
|
|
|
|
{
|
|
for (int i = 0; i < ELEMENTS(reservedState); ++i)
|
|
{
|
|
reservedState[i] = 0;
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// Segment-table walk: instantiate one Subsystem per streamed segment,
|
|
// dispatching on its classID. The subsystem roster (subsystemArray /
|
|
// subsystemCount) lives in the base Entity.
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
ResourceDescription::ResourceID modelResourceID = creation_message->resourceID;
|
|
|
|
ResourceDescription *subsystemDesc =
|
|
application->GetResourceFile()->SearchList(
|
|
modelResourceID,
|
|
ResourceDescription::SubsystemModelStreamResourceType
|
|
);
|
|
Check(subsystemDesc);
|
|
subsystemDesc->Lock();
|
|
|
|
//
|
|
// Copy the raw stream into a padded buffer: reading a SubsystemResource
|
|
// struct off the tail segment can over-read the raw resource, so pad it.
|
|
//
|
|
size_t rawSize = (size_t)subsystemDesc->resourceSize;
|
|
size_t padSize = rawSize + 0x400;
|
|
void *padBuffer = (void *)new char[padSize];
|
|
memcpy(padBuffer, subsystemDesc->resourceAddress, rawSize);
|
|
|
|
MemoryStream subsystemStream(padBuffer, padSize);
|
|
|
|
int streamedSubsystemCount = *(int *)subsystemStream.GetPointer();
|
|
subsystemStream.AdvancePointer(sizeof(int));
|
|
|
|
//
|
|
// Slot 0 = the (later-installed) control mapper, slot 1 = the voltage bus
|
|
// sentinel; the streamed subsystems fill from slot 2.
|
|
//
|
|
subsystemCount = streamedSubsystemCount + 2;
|
|
subsystemArray = new Subsystem *[subsystemCount];
|
|
{
|
|
for (int z = 0; z < subsystemCount; ++z)
|
|
{
|
|
subsystemArray[z] = NULL;
|
|
}
|
|
}
|
|
|
|
for (int id = 2; id < subsystemCount; ++id)
|
|
{
|
|
Subsystem::SubsystemResource *seg =
|
|
(Subsystem::SubsystemResource *)subsystemStream.GetPointer();
|
|
|
|
Subsystem *made = NULL;
|
|
|
|
switch (seg->classID)
|
|
{
|
|
case CondenserClassID:
|
|
made = new Condenser(this, id, (Condenser::SubsystemResource *)seg);
|
|
break;
|
|
case HeatSinkClassID:
|
|
//
|
|
// The 0x0BBE stream class is the mech's heat-sink BANK
|
|
// (AggregateHeatSink) -- there is no streamed plain-HeatSink; our
|
|
// VDATA enum name simply predates that discovery.
|
|
//
|
|
made = new AggregateHeatSink(this, id, (AggregateHeatSink::SubsystemResource *)seg);
|
|
break;
|
|
case HeatWatcherClassID:
|
|
made = new HeatWatcher(this, id, (HeatWatcher::SubsystemResource *)seg);
|
|
break;
|
|
case ReservoirClassID:
|
|
made = new Reservoir(this, id, (Reservoir::SubsystemResource *)seg);
|
|
break;
|
|
case GeneratorClassID:
|
|
made = new Generator(this, id, (Generator::SubsystemResource *)seg);
|
|
break;
|
|
case PoweredSubsystemClassID:
|
|
made = new PoweredSubsystem(this, id, (PoweredSubsystem::SubsystemResource *)seg);
|
|
break;
|
|
case SensorClassID:
|
|
made = new Sensor(this, id, (Sensor::SubsystemResource *)seg);
|
|
sensorSubsystem = made;
|
|
break;
|
|
case GyroscopeClassID:
|
|
made = new Gyroscope(this, id, (Gyroscope::SubsystemResource *)seg);
|
|
gyroSubsystem = made;
|
|
break;
|
|
case TorsoClassID:
|
|
made = new Torso(this, id, (Torso::SubsystemResource *)seg);
|
|
sinkSourceSubsystem = made;
|
|
break;
|
|
case MyomersClassID:
|
|
made = new Myomers(this, id, (Myomers::SubsystemResource *)seg);
|
|
break;
|
|
case EmitterClassID:
|
|
made = new Emitter(this, id, (Emitter::SubsystemResource *)seg);
|
|
++weaponCount;
|
|
break;
|
|
case PPCClassID:
|
|
made = new PPC(this, id, (PPC::SubsystemResource *)seg, PPC::DefaultData);
|
|
++weaponCount;
|
|
break;
|
|
case AmmoBinClassID:
|
|
made = new AmmoBin(this, id, (AmmoBin::SubsystemResource *)seg);
|
|
break;
|
|
case ProjectileWeaponClassID:
|
|
made = new ProjectileWeapon(this, id, (ProjectileWeapon::SubsystemResource *)seg);
|
|
++weaponCount;
|
|
break;
|
|
case GaussRifleClassID:
|
|
made = new GaussRifle(this, id, (GaussRifle::SubsystemResource *)seg);
|
|
++weaponCount;
|
|
break;
|
|
case MissileLauncherClassID:
|
|
made = new MissileLauncher(this, id, (MissileLauncher::SubsystemResource *)seg);
|
|
++weaponCount;
|
|
break;
|
|
case SubsystemMessageManagerClassID:
|
|
made = new SubsystemMessageManager(this, id, (SubsystemMessageManager::SubsystemResource *)seg);
|
|
messageManager = (SubsystemMessageManager *)made;
|
|
break;
|
|
case HUDClassID:
|
|
made = new HUD(this, id, (HUD::SubsystemResource *)seg);
|
|
hudSubsystem = made;
|
|
break;
|
|
case SearchlightClassID:
|
|
made = new Searchlight(this, id, (Searchlight::SubsystemResource *)seg);
|
|
break;
|
|
case ThermalSightClassID:
|
|
made = new ThermalSight(this, id, (ThermalSight::SubsystemResource *)seg);
|
|
break;
|
|
case MechTechClassID:
|
|
made = new MechTech(this, id, (MechTech::SubsystemResource *)seg);
|
|
break;
|
|
case EmitterClassID + 1: // LaserClassID -- an Emitter energy weapon
|
|
case EmitterClassID + 2: // ParticleCannonClassID -- an Emitter energy weapon
|
|
made = new Emitter(this, id, (Emitter::SubsystemResource *)seg);
|
|
++weaponCount;
|
|
break;
|
|
|
|
default:
|
|
//
|
|
// Unrecognised / not-yet-reconstructed subsystem class (Capacitor,
|
|
// AmmoFeeder, Radar, Turret, ...): give the slot a base
|
|
// MechSubsystem so control/damage bindings that resolve this
|
|
// subsystemID find a real (if generic) subsystem rather than a NULL
|
|
// plug. The roster stays aligned.
|
|
//
|
|
made = new MechSubsystem(
|
|
this, id,
|
|
(MechSubsystem::SubsystemResource *)seg,
|
|
MechSubsystem::DefaultData
|
|
);
|
|
break;
|
|
}
|
|
|
|
subsystemArray[id] = made;
|
|
|
|
subsystemStream.AdvancePointer(seg->subsystemModelSize);
|
|
}
|
|
|
|
subsystemDesc->Unlock();
|
|
delete [] (char *)padBuffer;
|
|
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[mech] segment walk done: subsystemCount=" << subsystemCount
|
|
<< " weaponCount=" << weaponCount << endl << flush;
|
|
|
|
//
|
|
// Skeleton summary: confirm the JointedMover base streamed the segment /
|
|
// joint tables (so joint-driven aim / animation / damage has something to
|
|
// bind to). BT_SKEL_DUMP additionally lists every segment name + joint
|
|
// index (used to identify the twist / gun / leg joints).
|
|
//
|
|
JointSubsystem *joints = GetJointSubsystem();
|
|
DEBUG_STREAM << "[skel] jointSubsystem=" << (void *)joints
|
|
<< " jointCount=" << (joints ? joints->GetJointCount() : -1) << endl << flush;
|
|
if (getenv("BT_SKEL_DUMP"))
|
|
{
|
|
EntitySegment::SegmentTableIterator it(segmentTable);
|
|
EntitySegment *seg;
|
|
int i = 0;
|
|
while ((seg = it.ReadAndNext()) != NULL && i < 60)
|
|
{
|
|
DEBUG_STREAM << "[skel] seg[" << i << "] name=" << seg->GetName()
|
|
<< " jointIdx=" << seg->GetJointIndex() << endl;
|
|
++i;
|
|
}
|
|
DEBUG_STREAM << "[skel] segments=" << i << endl << flush;
|
|
|
|
//
|
|
// The subsystem roster map (slot -> name), for cross-referencing the
|
|
// streamed index fields (voltage source / linked sink / ammo bin).
|
|
//
|
|
for (int r = 2; r < subsystemCount; ++r)
|
|
{
|
|
if (subsystemArray[r] != NULL)
|
|
{
|
|
DEBUG_STREAM << "[roster] slot " << r << " = "
|
|
<< subsystemArray[r]->GetName() << endl;
|
|
}
|
|
}
|
|
DEBUG_STREAM << flush;
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// THE HEATABLE CHAIN (binary ctor sweep, part_012.c:15774: every roster
|
|
// subsystem derived from HeatableSubsystem -- GUID 0x50e4fc -- joins the
|
|
// mech+0x7cc chain), then the first coolant shares (the binary calls the
|
|
// redistribute at the very ctor end, after the "Mechs" group add; the
|
|
// ordering between is inert). The weapon (+0x7bc) and Myomers (+0x7ac)
|
|
// sweeps land with their consumers.
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
{
|
|
for (int hs = 2; hs < subsystemCount; ++hs)
|
|
{
|
|
if (
|
|
subsystemArray[hs] != NULL &&
|
|
subsystemArray[hs]->IsDerivedFrom(
|
|
HeatableSubsystem::ClassDerivations)
|
|
)
|
|
{
|
|
heatableSubsystems.Add(subsystemArray[hs]);
|
|
}
|
|
}
|
|
}
|
|
RedistributeCoolantShares();
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// THE WATCH BINDS (5.3.132). Every HeatWatcher descendant -- which is
|
|
// the whole watcher family, PowerWatcher and the Gyroscope / Torso / HUD
|
|
// leaves included -- streams the roster index of the subsystem it
|
|
// watches, and `UpdateWatch` resolves a link built from it. Nothing
|
|
// ever built that link, so every watcher resolved NULL and reported its
|
|
// target unpowered / stone cold.
|
|
//
|
|
// This runs POST-WALK, not in the watcher ctor: a watcher may legally
|
|
// watch a subsystem with a higher roster id, which does not exist yet
|
|
// while the walk is still building. Out-of-range and self-referencing
|
|
// indices are skipped rather than trusted.
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
{
|
|
for (int wb = 2; wb < subsystemCount; ++wb)
|
|
{
|
|
Subsystem *watcher = subsystemArray[wb];
|
|
if (
|
|
watcher == NULL ||
|
|
!watcher->IsDerivedFrom(HeatWatcher::ClassDerivations)
|
|
)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int index = ((HeatWatcher *)watcher)->WatchedSubsystemIndex();
|
|
Subsystem *watched =
|
|
(index >= 2 && index < subsystemCount && index != wb)
|
|
? subsystemArray[index]
|
|
: NULL;
|
|
|
|
((HeatWatcher *)watcher)->BindWatchedSubsystem(watched);
|
|
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[watch] '" << watcher->GetName()
|
|
<< "' watches index " << index << " -> "
|
|
<< ((watched != NULL) ? watched->GetName() : "(unbound)")
|
|
<< endl << flush;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// THE MYOMER CHAIN (binary ctor sweep, part_012.c:15871: the mech+0x7ac
|
|
// chain). Walking it is also what tells the mech it has computed its
|
|
// OWN top speed: the binary calls the cap-raise per myomer and then
|
|
// sets +0x7a4 (part_012.c:15874), which is what makes this mech the one
|
|
// that BROADCASTS its real max speed instead of adopting a streamed one.
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
{
|
|
for (int my = 2; my < subsystemCount; ++my)
|
|
{
|
|
if (
|
|
subsystemArray[my] != NULL &&
|
|
subsystemArray[my]->IsDerivedFrom(Myomers::ClassDerivations)
|
|
)
|
|
{
|
|
myomerSubsystems.Add(subsystemArray[my]);
|
|
((Myomers *)subsystemArray[my])->RegisterMaxOutput();
|
|
runSpeedMaxKnown = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// Source the authentic per-mech locomotion params from the GameModel
|
|
// resource (mech.cpp @~1430: walkingTurnRate/runningTurnRate deg->rad,
|
|
// maxAcceleration, throttleAdjustment). The reconstructed ModelResource
|
|
// struct layout is only partially verified (BT411 flags it mis-decoded in
|
|
// places), so every read is SANITY-GUARDED: a value outside a sane band
|
|
// leaves the bring-up default in place. The stride/top speeds still come
|
|
// from the bring-up defaults (their authentic source is LoadLocomotionClips,
|
|
// which measures them from the walk/run animation clips -- a later wave).
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
{
|
|
ResourceDescription *modelDesc =
|
|
application->GetResourceFile()->SearchList(
|
|
modelResourceID,
|
|
ResourceDescription::GameModelResourceType
|
|
);
|
|
if (modelDesc != NULL)
|
|
{
|
|
modelDesc->Lock();
|
|
ModelResource *model = (ModelResource *)modelDesc->resourceAddress;
|
|
if (model != NULL)
|
|
{
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[mech] model params: walkTR="
|
|
<< model->walkingTurnRate << " runTR=" << model->runningTurnRate
|
|
<< " maxAcc=" << model->maxAcceleration
|
|
<< " throttleAdj=" << model->throttleAdjustment
|
|
<< " (deg,deg,u/s^2,scale)" << endl << flush;
|
|
DEBUG_STREAM << "[mech] look angles: L="
|
|
<< model->lookLeftAngle << " R=" << model->lookRightAngle
|
|
<< " F=" << model->lookFrontAngle << " B=" << model->lookBackAngle
|
|
<< " (deg)" << endl << flush;
|
|
}
|
|
if (model->walkingTurnRate > 1.0f && model->walkingTurnRate < 360.0f)
|
|
{
|
|
walkingTurnRate = model->walkingTurnRate * RAD_PER_DEG;
|
|
}
|
|
if (model->runningTurnRate > 1.0f && model->runningTurnRate < 360.0f)
|
|
{
|
|
runningTurnRate = model->runningTurnRate * RAD_PER_DEG;
|
|
}
|
|
if (model->maxAcceleration > 1.0f && model->maxAcceleration < 500.0f)
|
|
{
|
|
maxBodyAcceleration = model->maxAcceleration;
|
|
//
|
|
// The gait's slew rates ARE the authored acceleration
|
|
// (binary ctor: +0x344/+0x5b0/+0x5b8 all take rec+0x44).
|
|
// This is what makes the stride-driven speed model and
|
|
// the old acceleration model agree in feel: both slew at
|
|
// maxAcceleration toward the demand.
|
|
//
|
|
forwardCycleRate = model->maxAcceleration;
|
|
gimpCycleRate = model->maxAcceleration;
|
|
|
|
//
|
|
// The live accel rate and its forward figure are the
|
|
// same authored value (binary: +0x344, +0x5b0 and
|
|
// +0x5b8 ALL take rec+0x44).
|
|
//
|
|
bodyAccelRate = model->maxAcceleration;
|
|
forwardAccelRate = model->maxAcceleration;
|
|
}
|
|
|
|
//
|
|
// The super-stop rate (rec+0x48). -1.1 is the authored
|
|
// "no super stop on this chassis" sentinel and must pass
|
|
// through unguarded -- the whole system tests for it.
|
|
//
|
|
superStopRate = model->superStopAcceleration;
|
|
|
|
//
|
|
// The instability constants (rec+0x80..0x94), taken as
|
|
// authored: the model is a weighted sum, so a zero term
|
|
// simply contributes nothing.
|
|
//
|
|
maxUnstableAcceleration = model->maxUnstableAcceleration;
|
|
unstableAccelerationEffect = model->unstableAccelerationEffect;
|
|
unstableGunTheEngineEffect = model->unstableGunTheEngineEffect;
|
|
unstableSuperStopEffect = model->unstableSuperStopEffect;
|
|
unstableHighVelocityEffect = model->unstableHighVelocityEffect;
|
|
unstableStopedTurnEffect = model->unstableStopedTurnEffect;
|
|
if (model->throttleAdjustment > 0.05f && model->throttleAdjustment < 20.0f)
|
|
{
|
|
forwardThrottleScale = model->throttleAdjustment;
|
|
}
|
|
|
|
//
|
|
// The authored look-view angles (deg->rad), same insanity band
|
|
// as the rest of the guarded reads.
|
|
//
|
|
{
|
|
Scalar a;
|
|
a = model->lookLeftAngle;
|
|
if (a > -360.0f && a < 360.0f) lookLeftAngle = a * RAD_PER_DEG;
|
|
a = model->lookRightAngle;
|
|
if (a > -360.0f && a < 360.0f) lookRightAngle = a * RAD_PER_DEG;
|
|
a = model->lookFrontAngle;
|
|
if (a > -360.0f && a < 360.0f) lookFrontAngle = a * RAD_PER_DEG;
|
|
a = model->lookBackAngle;
|
|
if (a > -360.0f && a < 360.0f) lookBackAngle = a * RAD_PER_DEG;
|
|
}
|
|
|
|
//
|
|
// The gait clip loader -- resolves every animation clip by
|
|
// the model's prefix and MEASURES the stride/speed constants
|
|
// from the clips themselves, replacing the bring-up defaults
|
|
// above. Guarded on the prefix looking like text because
|
|
// this ModelResource layout is only partially verified; a
|
|
// garbage prefix would just probe nonsense names (soft), but
|
|
// the log line makes a layout miss visible.
|
|
//
|
|
if (
|
|
model->animationPrefix[0] >= 'a' &&
|
|
model->animationPrefix[0] <= 'z'
|
|
)
|
|
{
|
|
//
|
|
// The view dispatch (binary ctor tail @0x4a1674 region):
|
|
// a REPLICANT loads the exterior gait clips -- you see
|
|
// it from outside -- while the MASTER loads the INTERIOR
|
|
// 'i' set, the gait as seen from the cockpit. L4VIEWEXT
|
|
// (a 1995 dev switch) forces the exterior set on a
|
|
// master for external-camera work, which is exactly what
|
|
// the render-bridge verification rigs want.
|
|
//
|
|
if (getenv("L4VIEWEXT") == NULL)
|
|
{
|
|
if (GetInstance() == ReplicantInstance)
|
|
{
|
|
LoadLocomotionClips(model);
|
|
}
|
|
else
|
|
{
|
|
LoadLocomotionClipsExt(model);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
usingExteriorClips = 1;
|
|
LoadLocomotionClips(model);
|
|
}
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[mech] clips '"
|
|
<< model->animationPrefix << "': standSpeed="
|
|
<< standSpeed << " walkStride=" << walkStrideLength
|
|
<< " revStride=" << reverseStrideLength
|
|
<< " revSpeedMax=" << reverseSpeedMax
|
|
<< " gimpSpeedMax=" << gimpSpeedMax
|
|
<< " gimpStride=" << gimpStrideLength
|
|
<< " limpSet=" << hasGimpClips
|
|
<< endl << flush;
|
|
}
|
|
}
|
|
else if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[mech] animationPrefix not text ("
|
|
<< (int)(unsigned char)model->animationPrefix[0]
|
|
<< ") -- clip loader SKIPPED, layout suspect"
|
|
<< endl << flush;
|
|
}
|
|
}
|
|
modelDesc->Unlock();
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// Fill the hull damage-zone array. The Entity base ctor only READS the
|
|
// zone count and allocates the raw POINTER array (entity.cpp:1032) -- the
|
|
// derived entity must construct the streamed DamageZone objects into the
|
|
// slots itself (the CulturalIcon ctor is the surviving reference,
|
|
// cultural.cpp:349). LOAD-BEARING: an unfilled slot is heap garbage, and
|
|
// the first TakeDamageMessage that lands on it calls through a trash
|
|
// vtable (crash = EIP inside the heap). Runs AFTER the segment walk
|
|
// because the streamed DamageZone ctor resolves its effect-site segments
|
|
// via GetSegment() on JointedMover-derived owners.
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
if (damageZones != NULL && damageZoneCount > 0)
|
|
{
|
|
ResourceDescription *dmg_res =
|
|
application->GetResourceFile()->SearchList(
|
|
resourceID,
|
|
ResourceDescription::DamageZoneStreamResourceType
|
|
);
|
|
Check(dmg_res);
|
|
dmg_res->Lock();
|
|
DynamicMemoryStream damage_stream(
|
|
dmg_res->resourceAddress,
|
|
dmg_res->resourceSize
|
|
);
|
|
damage_stream.AdvancePointer(sizeof(damageZoneCount));
|
|
//
|
|
// Every entry is the MECH-specific zone subclass (the class-scope
|
|
// DamageZone typedef = Mech__DamageZone, binary ctor @0049ce50): its
|
|
// streamed ctor chains the engine base parse and consumes the BT
|
|
// per-zone TAIL (flags / criticals / LOD redirects) -- a base-class
|
|
// fill parses zone 0 short and skews every following zone.
|
|
//
|
|
{
|
|
for (int dz = 0; dz < damageZoneCount; ++dz)
|
|
{
|
|
damageZones[dz] = new DamageZone(this, dz, &damage_stream);
|
|
Register_Object(damageZones[dz]);
|
|
}
|
|
for (int dp = 0; dp < damageZoneCount; ++dp)
|
|
{
|
|
((DamageZone *)damageZones[dp])->SetLODParentPointers();
|
|
}
|
|
}
|
|
dmg_res->Unlock();
|
|
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[mech] damage zones streamed: " << damageZoneCount;
|
|
for (int zz = 0; zz < damageZoneCount; ++zz)
|
|
{
|
|
DEBUG_STREAM << (zz ? "," : " [") << damageZones[zz]->damageZoneName;
|
|
}
|
|
DEBUG_STREAM << "]" << endl << flush;
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// The cylinder hit-location table (binary Pass-3 tail, cached at
|
|
// mech+0x444): found by the DamageZoneStream member's NAME in the
|
|
// type-29 DamageLookupTableStream directory. Resolves UNAIMED hits
|
|
// (zone -1 + impact point -- missiles, splash, rams) onto hull zones by
|
|
// impact geometry. Kept locked (resident) for the mech's life.
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
{
|
|
ResourceDescription *zone_stream =
|
|
application->GetResourceFile()->SearchList(
|
|
modelResourceID,
|
|
ResourceDescription::DamageZoneStreamResourceType
|
|
);
|
|
if (zone_stream != NULL)
|
|
{
|
|
ResourceDescription *cyl_desc =
|
|
application->GetResourceFile()->FindResourceDescription(
|
|
zone_stream->resourceName,
|
|
ResourceDescription::DamageLookupTableStreamResourceType
|
|
);
|
|
if (cyl_desc != NULL)
|
|
{
|
|
cyl_desc->Lock();
|
|
MemoryStream cyl_stream(
|
|
cyl_desc->resourceAddress,
|
|
cyl_desc->resourceSize
|
|
);
|
|
damageLookupTable = new DamageLookupTable(this, &cyl_stream);
|
|
Register_Object(damageLookupTable);
|
|
}
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[cyl] table '"
|
|
<< zone_stream->resourceName << "' "
|
|
<< ((damageLookupTable != NULL) ? "LOADED rows=" : "ABSENT rows=")
|
|
<< ((damageLookupTable != NULL)
|
|
? damageLookupTable->rowCount : 0)
|
|
<< endl << flush;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Install the per-frame body Performance. Until now the mech ran the base
|
|
// DoNothingOnce; from here the engine dispatches Mech::Simulate every frame
|
|
// (Mover -> Entity -> Simulation::PerformAndWatch) once the mission is
|
|
// RunningMission.
|
|
//
|
|
SetPerformance(&Mech::Simulate);
|
|
|
|
//
|
|
// The entity is complete -- mark it VALID, like every 1995 entity ctor tail
|
|
// (CamShip / DoorFrame / DropZone / ...). LOAD-BEARING: Entity::Dispatch
|
|
// routes messages to an INVALID entity into the deferred event queue, so
|
|
// without this the mech never receives a directly-dispatched message --
|
|
// the PlayerLink bind (and with it every player-experience gate) silently
|
|
// never lands.
|
|
//
|
|
SetValidFlag();
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
Mech::~Mech()
|
|
{
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// InDeathTransition -- binary @0049fb54 (32 bytes): simulation state 2 or 9.
|
|
//#############################################################################
|
|
//
|
|
Logical
|
|
Mech::InDeathTransition(Entity *vehicle)
|
|
{
|
|
Check(vehicle);
|
|
return
|
|
vehicle->GetSimulationState() == 2 ||
|
|
vehicle->GetSimulationState() == 9;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// DuckRequestMessageHandler -- binary @0049fa00 (authentic name "DuckRequest"
|
|
// from the handler table @0x10bc7c). A positive request latches one duck
|
|
// evaluation for the next master frame; the frame consumes the latch either
|
|
// way.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::DuckRequestMessageHandler(Receiver::Message *message)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(message);
|
|
|
|
if (((int *)(message + 1))[0] > 0)
|
|
{
|
|
duckRequestLatch = 1;
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// RealMaxSpeedMessageHandler -- binary @0049f604 (authentic name from the
|
|
// handler table). Adopt the streamed real max speed only while our own is
|
|
// unknown. The wire shape is the entity-stream family: 12-byte header,
|
|
// sender EntityID at +0xc (stamped by Entity's network send @0041f640),
|
|
// reserved pair, payload at +0x1c.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::RealMaxSpeedMessageHandler(Receiver::Message *message)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(message);
|
|
|
|
if (runSpeedMaxKnown == 0)
|
|
{
|
|
runSpeedMax = *(Scalar *)((char *)message + 0x1c);
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// SetBurningStateMessageHandler -- binary @0049f674. The burning wreck is
|
|
// parked in a per-host off-field slot: x = localHostID x 10000, y =
|
|
// localHostID x -1000 (the data words @0049f6fc/@0049f6f8; the id read is
|
|
// HostManager::GetLocalHostID -- the accessor the entity-instance dispatch
|
|
// compares against ownerID), z untouched. The mode alarm drops to 2 --
|
|
// BURNING, the second InDeathTransition state (its missing writer, found)
|
|
// -- the position dirty mark fires, and execution is forced back on so the
|
|
// state runs even on a DontExecute replicant.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::SetBurningStateMessageHandler(Receiver::Message *message)
|
|
{
|
|
Check(this);
|
|
|
|
HostID
|
|
local_host = application->GetHostManager()->GetLocalHostID();
|
|
localOrigin.linearPosition.x = (Scalar)local_host * 10000.0f;
|
|
localOrigin.linearPosition.y = (Scalar)local_host * -1000.0f;
|
|
localToWorld = localOrigin;
|
|
SetSimulationState(2);
|
|
ForceUpdate(1);
|
|
AlwaysExecute();
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// ClearBurningStateMessageHandler -- binary @0049f700: back to
|
|
// DefaultState, dirty the position, re-arm execution.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::ClearBurningStateMessageHandler(Receiver::Message *message)
|
|
{
|
|
Check(this);
|
|
|
|
SetSimulationState(0);
|
|
ForceUpdate(1);
|
|
AlwaysExecute();
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// BalanceCoolantMessageHandler -- binary @0049f728 (authentic name from the
|
|
// handler table). A positive press resets every heatable's coolant
|
|
// priority to 1 -- the equal split -- and renormalizes the shares.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::BalanceCoolantMessageHandler(Receiver::Message *message)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(message);
|
|
|
|
if (((int *)(message + 1))[0] > 0)
|
|
{
|
|
ChainIteratorOf<Subsystem*> mark_walk(heatableSubsystems);
|
|
HeatableSubsystem *heatable;
|
|
while ((heatable = (HeatableSubsystem *)mark_walk.ReadAndNext()) != NULL)
|
|
{
|
|
heatable->coolantPriority = 1;
|
|
}
|
|
RedistributeCoolantShares();
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// UpdateCombatEffectiveness -- binary @0049fa1c, the master perf's opening
|
|
// call. The binary keys the bank on classID 0xBBE and the generators on
|
|
// 0xBC1; we use the equivalent derivations ([T1] cosmetic). A dry missile
|
|
// launcher (fire state 7 = NoAmmoState, the binary's +0x364 read) does not
|
|
// count as a working weapon. BT_FORCE_CRIPPLED=1 pins the flag for the
|
|
// eject harness.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::UpdateCombatEffectiveness()
|
|
{
|
|
Check(this);
|
|
|
|
int working_weapons = 0;
|
|
int working_generators = 0;
|
|
Scalar bank_fraction = 0.0f;
|
|
|
|
for (int s = 2; s < subsystemCount; ++s)
|
|
{
|
|
Subsystem *sub = subsystemArray[s];
|
|
if (sub == NULL)
|
|
{
|
|
continue;
|
|
}
|
|
if (sub->IsDerivedFrom(AggregateHeatSink::ClassDerivations))
|
|
{
|
|
HeatSink *bank = (HeatSink *)sub;
|
|
if (bank->GetThermalCapacity() > 0.0f)
|
|
{
|
|
bank_fraction =
|
|
bank->GetCoolantLevel() / bank->GetThermalCapacity();
|
|
}
|
|
}
|
|
else if (sub->IsDerivedFrom(Generator::ClassDerivations))
|
|
{
|
|
if (
|
|
(int)sub->GetSimulationState() != 1 &&
|
|
((Generator *)sub)->GeneratorStateOf()
|
|
!= Generator::GeneratorFailed
|
|
)
|
|
{
|
|
++working_generators;
|
|
}
|
|
}
|
|
else if (sub->IsDerivedFrom(MechWeapon::ClassDerivations))
|
|
{
|
|
if ((int)sub->GetSimulationState() != 1)
|
|
{
|
|
if (
|
|
sub->IsDerivedFrom(MissileLauncher::ClassDerivations) &&
|
|
((MechWeapon *)sub)->GetWeaponState()
|
|
== MechWeapon::NoAmmoState
|
|
)
|
|
{
|
|
// a dry launcher is not a working weapon
|
|
}
|
|
else
|
|
{
|
|
++working_weapons;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int limping_uncrouched = 0;
|
|
{
|
|
int mode = MovementMode();
|
|
if (mode == 3 || mode == 4)
|
|
{
|
|
MechControlsMapper *census_mapper =
|
|
(MechControlsMapper *)subsystemArray[0];
|
|
if (census_mapper == NULL || census_mapper->DuckCommand() == 0)
|
|
{
|
|
limping_uncrouched = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
int ineffective =
|
|
(
|
|
working_weapons < minimumWeaponCount ||
|
|
working_generators == 0 ||
|
|
bank_fraction < 0.05f ||
|
|
limping_uncrouched
|
|
) ? 1 : 0;
|
|
|
|
{
|
|
static int s_forceCrippled = -1;
|
|
if (s_forceCrippled < 0)
|
|
{
|
|
s_forceCrippled = (getenv("BT_FORCE_CRIPPLED") != NULL) ? 1 : 0;
|
|
}
|
|
if (s_forceCrippled)
|
|
{
|
|
ineffective = 1;
|
|
}
|
|
}
|
|
|
|
if (ineffective != combatIneffective && getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[census] combatIneffective -> " << ineffective
|
|
<< " (weapons=" << working_weapons
|
|
<< " generators=" << working_generators
|
|
<< " bank=" << bank_fraction << ")" << endl << flush;
|
|
}
|
|
combatIneffective = ineffective;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// UpdateSuperStop -- binary master perf @0x4aa353-0x4aa393. Hauling the
|
|
// throttle negative while the mech still rolls FORWARD is a hard brake: the
|
|
// gait's acceleration rate swaps to the authored super-stop figure and the
|
|
// gyro throws the cockpit. Entry is an EDGE (one lurch, random sign, both
|
|
// gyro channels at 0.4); while it holds above the walk threshold the pitch
|
|
// kick repeats every 0.4s -- the shudder of a mech standing on its heels.
|
|
// The -1.1 rate sentinel disables the whole system for a chassis.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::UpdateSuperStop(Scalar time_slice, Scalar speed_demand)
|
|
{
|
|
Check(this);
|
|
|
|
if (superStopRate != -1.1f)
|
|
{
|
|
if (currentBodySpeed <= 0.0f || speed_demand >= 0.0f)
|
|
{
|
|
if (superStopping != 0)
|
|
{
|
|
superStopping = 0;
|
|
ForceUpdate(0x100);
|
|
bodyAccelRate = forwardAccelRate;
|
|
}
|
|
}
|
|
else if (superStopping == 0)
|
|
{
|
|
superStopping = 1;
|
|
ForceUpdate(0x100);
|
|
bodyAccelRate = superStopRate;
|
|
|
|
if (gyroSubsystem != NULL)
|
|
{
|
|
//
|
|
// Two rolls: the magnitude, then a coin flip for which way
|
|
// the mech lurches (binary: 0.5 @0x4ab170).
|
|
//
|
|
Scalar
|
|
lurch = (Scalar)Random,
|
|
flip = (Scalar)Random;
|
|
if (flip > 0.5f)
|
|
{
|
|
lurch = -lurch;
|
|
}
|
|
((Gyroscope *)gyroSubsystem)->
|
|
ApplyDamageTorque(0.0f, 0.0f, lurch, 0.4f);
|
|
((Gyroscope *)gyroSubsystem)->
|
|
ApplyDamageImpulse(0.0f, 0.0f, lurch, 0.4f);
|
|
}
|
|
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[superstop] engaged: speed="
|
|
<< currentBodySpeed << " demand=" << speed_demand
|
|
<< " accelRate " << forwardAccelRate << " -> "
|
|
<< superStopRate << endl << flush;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// The sustained shudder: only while the brake holds AND the mech is
|
|
// still moving faster than a walk.
|
|
//
|
|
if (
|
|
superStopping != 0 &&
|
|
currentBodySpeed > standSpeed &&
|
|
gyroSubsystem != NULL
|
|
)
|
|
{
|
|
if (superStopShudderTimer > 0.0f)
|
|
{
|
|
superStopShudderTimer -= time_slice;
|
|
}
|
|
else
|
|
{
|
|
((Gyroscope *)gyroSubsystem)->
|
|
ApplyDamageImpulse(0.0f, 1.0f, 0.0f, 0.2f);
|
|
superStopShudderTimer = 0.4f;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// UpdateInstability -- binary master perf tail @0x4aab8e-0x4aaf42. Three
|
|
// weighted terms, each named by the model resource's own field:
|
|
//
|
|
// ACCELERATION |acceleration| / maxUnstableAcceleration, clamped to 1,
|
|
// times unstableAccelerationEffect, SQUARED.
|
|
// GUNNING THE ENGINE how far the throttle demand outruns the actual
|
|
// speed, as a fraction of the demand (the demand first clamped into
|
|
// [reverse cap, runSpeedMax]), clamped to 1, times
|
|
// unstableGunTheEngineEffect.
|
|
// STOPPED TURN a flat unstableStopedTurnEffect while the legs are in
|
|
// the turn-in-place gait.
|
|
//
|
|
// Sum clamped to 1 -> unstablePercentage (the published cockpit attribute)
|
|
// -> republished to the gyro. unstableSuperStopEffect and
|
|
// unstableHighVelocityEffect are streamed but NOT summed here; their
|
|
// consumers are elsewhere (OPEN).
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::UpdateInstability(Scalar speed_demand)
|
|
{
|
|
Check(this);
|
|
|
|
//
|
|
// Term 1: the acceleration the ring-buffer tail derives. That tail is
|
|
// not landed yet, so bodyAcceleration holds 0 and this term is inert
|
|
// ([T1] -- it lights up with the rings brick).
|
|
//
|
|
Scalar unstable = 0.0f;
|
|
if (maxUnstableAcceleration > 0.0f)
|
|
{
|
|
Scalar accel_ratio = bodyAcceleration.Length() / maxUnstableAcceleration;
|
|
if (accel_ratio > 1.0f)
|
|
{
|
|
accel_ratio = 1.0f;
|
|
}
|
|
unstable =
|
|
(accel_ratio * unstableAccelerationEffect) *
|
|
(accel_ratio * unstableAccelerationEffect);
|
|
}
|
|
|
|
//
|
|
// Term 2: gunning the engine.
|
|
//
|
|
{
|
|
Scalar demand = speed_demand;
|
|
if (demand > runSpeedMax)
|
|
{
|
|
demand = runSpeedMax;
|
|
}
|
|
if (demand < reverseSpeedMax)
|
|
{
|
|
demand = reverseSpeedMax;
|
|
}
|
|
|
|
Scalar shortfall = 0.0f;
|
|
if (demand > 0.0f)
|
|
{
|
|
shortfall = (demand - currentBodySpeed) / demand;
|
|
}
|
|
else if (demand < 0.0f)
|
|
{
|
|
//
|
|
// Reversing: the same fraction taken on magnitudes.
|
|
//
|
|
Scalar gap = demand - currentBodySpeed;
|
|
if (gap < 0.0f)
|
|
{
|
|
gap = -gap;
|
|
}
|
|
shortfall = gap / -demand;
|
|
}
|
|
if (shortfall > 1.0f)
|
|
{
|
|
shortfall = 1.0f;
|
|
}
|
|
unstable += shortfall * unstableGunTheEngineEffect;
|
|
}
|
|
|
|
//
|
|
// Term 3: turning on the spot.
|
|
//
|
|
if ((int)legStateAlarm.GetLevel() == 4)
|
|
{
|
|
unstable += unstableStopedTurnEffect;
|
|
}
|
|
|
|
if (unstable > 1.0f)
|
|
{
|
|
unstable = 1.0f;
|
|
}
|
|
unstablePercentage = unstable;
|
|
|
|
if (gyroSubsystem != NULL)
|
|
{
|
|
((Gyroscope *)gyroSubsystem)->SetSwayBias(unstablePercentage);
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// UpdateMobilityScale -- binary master perf @0x4a9cf0-0x4a9da2. The scale
|
|
// is the BEST surviving myomer's speed effect (max over the chain), and it
|
|
// multiplies the pilot's throttle command outright: shot-up muscles mean a
|
|
// mech that will not run however hard the throttle is pushed. A scale
|
|
// inside the 1e-4 deadzone also zeroes the turn command and is what the
|
|
// duck driver reads as "the legs still work".
|
|
//
|
|
// The scan runs ONLY when the mech actually has myomers (the binary's
|
|
// count guard) -- a chassis without them keeps a scale of 1 and is
|
|
// unaffected.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::UpdateMobilityScale()
|
|
{
|
|
Check(this);
|
|
|
|
int myomer_count = 0;
|
|
Scalar best = 0.0f;
|
|
|
|
ChainIteratorOf<Subsystem*> scan(myomerSubsystems);
|
|
Subsystem *muscle;
|
|
while ((muscle = scan.ReadAndNext()) != NULL)
|
|
{
|
|
++myomer_count;
|
|
Scalar effect = ((Myomers *)muscle)->SpeedEffectOf();
|
|
if (effect > best)
|
|
{
|
|
best = effect;
|
|
}
|
|
}
|
|
|
|
if (myomer_count > 0)
|
|
{
|
|
mobilityScale = best;
|
|
|
|
MechControlsMapper *scale_mapper =
|
|
(MechControlsMapper *)subsystemArray[0];
|
|
if (scale_mapper != NULL)
|
|
{
|
|
scale_mapper->ApplyMobilityScale(mobilityScale);
|
|
}
|
|
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
static Scalar s_lastScale = -1.0f;
|
|
if (mobilityScale != s_lastScale)
|
|
{
|
|
s_lastScale = mobilityScale;
|
|
DEBUG_STREAM << "[mobility] scale " << mobilityScale
|
|
<< " over " << myomer_count << " myomers"
|
|
<< endl << flush;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// UpdateTelemetryRings -- binary master perf tail @0x4aafba-0x4ab04a. Push
|
|
// this frame's velocity / turn rate / slice into the five filters, then
|
|
// derive the acceleration as the change in the FILTERED values over the
|
|
// filtered slice -- which is why a mech's instability reacts to a sustained
|
|
// shove and not to one noisy frame.
|
|
//
|
|
// Two faithful oddities, kept verbatim because the binary is explicit:
|
|
// * the Y filter is fed every frame and NEVER reduced;
|
|
// * the acceleration's three components are NOT x/y/z of one quantity --
|
|
// x comes from the mean velocity-X, y from the MEAN velocity-Z and z
|
|
// from the OLYMPIC (min/max-trimmed) velocity-Z. Only its LENGTH is
|
|
// consumed (by the instability model), so the mongrel basis is
|
|
// immaterial to behaviour; do not "fix" it into a tidy vector.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::UpdateTelemetryRings(Scalar time_slice)
|
|
{
|
|
Check(this);
|
|
|
|
telemetryFilter[VelocityZFilter].Add(localVelocity.linearMotion.z);
|
|
telemetryFilter[VelocityYFilter].Add(localVelocity.linearMotion.y);
|
|
telemetryFilter[VelocityXFilter].Add(localVelocity.linearMotion.x);
|
|
//
|
|
// The binary pushes its computed turn rate (+0x1d4); ours lives in the
|
|
// body angular motion the turn dispatcher writes.
|
|
//
|
|
telemetryFilter[TurnRateFilter].Add(localVelocity.angularMotion.y);
|
|
telemetryFilter[TimeSliceFilter].Add(time_slice);
|
|
|
|
Scalar
|
|
olympic_z = telemetryFilter[VelocityZFilter].CalculateOlympicAverage(),
|
|
mean_x = telemetryFilter[VelocityXFilter].CalculateAverage(),
|
|
mean_z = telemetryFilter[VelocityZFilter].CalculateAverage(),
|
|
mean_turn = telemetryFilter[TurnRateFilter].CalculateAverage(),
|
|
mean_dt = telemetryFilter[TimeSliceFilter].CalculateAverage();
|
|
|
|
bodyAcceleration = Vector3D(0.0f, 0.0f, 0.0f);
|
|
if (mean_dt > 0.0f)
|
|
{
|
|
bodyAcceleration.x = (mean_x - previousMeanVelocityX) / mean_dt;
|
|
bodyAcceleration.y = (mean_z - previousMeanVelocityZ) / mean_dt;
|
|
bodyAcceleration.z = (olympic_z - previousOlympicVelocityZ) / mean_dt;
|
|
bodyTurnAcceleration = (mean_turn - previousMeanTurnRate) / mean_dt;
|
|
}
|
|
|
|
previousOlympicVelocityZ = olympic_z;
|
|
previousMeanVelocityZ = mean_z;
|
|
previousMeanVelocityX = mean_x;
|
|
previousMeanTurnRate = mean_turn;
|
|
|
|
if (getenv("BT_TELEMETRY_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[telem] vel=(" << localVelocity.linearMotion.x
|
|
<< "," << localVelocity.linearMotion.z
|
|
<< ") meanDt=" << mean_dt
|
|
<< " accel=" << bodyAcceleration.Length()
|
|
<< " unstable=" << unstablePercentage << endl << flush;
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// EjectPilotMessageHandler -- binary @0049f854 (authentic name from the
|
|
// handler table). The pilot can only punch out of a CRIPPLED mech
|
|
// (mech+0x414, the census) that is not already dying. The eject: mode 10
|
|
// (EJECTED -- the reason IsMechDestroyed tests >= 9), the mapper's eject
|
|
// latch, and a self-dispatched explosive TakeDamage (type 2, zone -1 ->
|
|
// the cylinder resolver). STAGED pieces, recorded: the binary's damage
|
|
// amount reads *(*(mapper+0x208) + 0x1c) -- a bound cell whose identity
|
|
// needs the mapper ctor's binding-loop decode -- we use a guaranteed-kill
|
|
// constant; and the score notify (@00429078 accessor + the app+0x20 vcall,
|
|
// event code 5) lands with the score-plumbing wave.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::EjectPilotMessageHandler(Receiver::Message *message)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(message);
|
|
|
|
if (((int *)(message + 1))[0] <= 0 || combatIneffective == 0)
|
|
{
|
|
return;
|
|
}
|
|
if (Mech::InDeathTransition(this))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[eject] pilot out -- mech " << GetEntityID()
|
|
<< " ejects (crippled)" << endl << flush;
|
|
}
|
|
|
|
SetSimulationState(10);
|
|
|
|
MechControlsMapper *eject_mapper =
|
|
(MechControlsMapper *)subsystemArray[0];
|
|
if (eject_mapper != NULL)
|
|
{
|
|
eject_mapper->ejectLatch = 1;
|
|
}
|
|
|
|
Damage ejection;
|
|
ejection.damageType = Damage::ExplosiveDamageType; // binary: 2
|
|
ejection.damageAmount = 1.0e6f; // [T1] staged kill amount
|
|
ejection.damageForce = Vector3D(0.0f, 0.0f, 0.0f);
|
|
ejection.impactPoint = localOrigin.linearPosition;
|
|
|
|
Entity::TakeDamageMessage
|
|
eject_hit(
|
|
Entity::TakeDamageMessageID,
|
|
sizeof(Entity::TakeDamageMessage),
|
|
GetEntityID(),
|
|
-1,
|
|
ejection
|
|
);
|
|
Dispatch(&eject_hit);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// RedistributeCoolantShares -- binary @0049f788: total the priorities, give
|
|
// each heatable share = priority / total (0.0 when the total is empty --
|
|
// the data word @0049f850), blip its balance alarm with the direction
|
|
// (2 = shrank or held, 1 = grew, then back to 0), store the share. The
|
|
// reservoir draw weights each heatable's coolant feed by the share.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::RedistributeCoolantShares()
|
|
{
|
|
Check(this);
|
|
|
|
int total = 0;
|
|
{
|
|
ChainIteratorOf<Subsystem*> sum_walk(heatableSubsystems);
|
|
HeatableSubsystem *heatable;
|
|
while ((heatable = (HeatableSubsystem *)sum_walk.ReadAndNext()) != NULL)
|
|
{
|
|
total += heatable->coolantPriority;
|
|
}
|
|
}
|
|
|
|
int share_count = 0;
|
|
Scalar first_share = 0.0f;
|
|
|
|
ChainIteratorOf<Subsystem*> share_walk(heatableSubsystems);
|
|
HeatableSubsystem *heatable;
|
|
while ((heatable = (HeatableSubsystem *)share_walk.ReadAndNext()) != NULL)
|
|
{
|
|
Scalar share = 0.0f;
|
|
if (total > 0)
|
|
{
|
|
share = (Scalar)heatable->coolantPriority / (Scalar)total;
|
|
}
|
|
heatable->balanceAlarm.SetLevel(
|
|
(share <= heatable->coolantShare) ? 2 : 1);
|
|
heatable->coolantShare = share;
|
|
heatable->balanceAlarm.SetLevel(0);
|
|
|
|
if (share_count == 0)
|
|
{
|
|
first_share = share;
|
|
}
|
|
++share_count;
|
|
}
|
|
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[coolant] shares renormalized: " << share_count
|
|
<< " heatables, total priority " << total
|
|
<< ", first share " << first_share << endl << flush;
|
|
}
|
|
|
|
//
|
|
// The conduction-flow bridge (see HEAT.HPP RecomputeValves): the
|
|
// fight-verified condenser pump consumes a CONDENSER-relative
|
|
// fraction; refresh it whenever the shares move.
|
|
//
|
|
Condenser::RecomputeValves(this);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// The collision-volume swap (binaries @004ac04c / @004ac064): write the
|
|
// template's maxY -- the box TOP; the ground probe's minY is untouched.
|
|
// Level 1 (standing) restores the captured height; level 0 (ducked) the
|
|
// 0.6x height.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::SetStandingCollisionVolume()
|
|
{
|
|
Check(this);
|
|
Check(collisionTemplate);
|
|
collisionTemplate->maxY = standingVolumeHeight;
|
|
}
|
|
|
|
void
|
|
Mech::SetDuckedCollisionVolume()
|
|
{
|
|
Check(this);
|
|
Check(collisionTemplate);
|
|
collisionTemplate->maxY = duckedVolumeHeight;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// TakeDamageMessageHandler -- the Mech override of the Entity damage entry
|
|
// (binary hub @004a0230 via the handler glue @0049ed0c).
|
|
//
|
|
// Order in the binary: (1) feed the RAW Damage record to the gyro cockpit
|
|
// bounce FIRST -- even an invalid-zone hit shakes the cockpit (STAGED: the
|
|
// gyro bounce math is the gyro/feel wave; a gated log marks the feed site);
|
|
// (2) latch the attacker (mech+0x43c -- the zone LOD router keys its
|
|
// same-attacker redirect reuse off it); (3) resolve an unaimed hit's zone
|
|
// (invalidDamageZone) from the cylinder hit-location table -- DEFERRED, the
|
|
// type-0x1d table is not loaded yet, so unaimed hits fall through to the
|
|
// base handler's zone==-1 drop (authentic base behavior); (4) chain to the
|
|
// Entity handler which routes damageZones[zone]->TakeDamage.
|
|
//#############################################################################
|
|
//
|
|
//
|
|
//#############################################################################
|
|
// ProcessCollision (binary @004abb40) -- the per-contact collision
|
|
// responder, overriding the engine's protected virtual. The base head is
|
|
// Mover::ProcessCollision verbatim (MOVER.CPP:1354); what the Mech adds is
|
|
// WHO it hit:
|
|
//
|
|
// * A separating contact -- one whose relative velocity points away from
|
|
// the surface -- is skipped entirely (the -1e-4 gate). No damage while
|
|
// pulling apart.
|
|
//
|
|
// * Another MECH takes the collision damage too (zone -1: the victim's
|
|
// own cylinder resolves it). Both sides of a ram hurt.
|
|
//
|
|
// * A CulturalIcon takes the crunch, and a CRUSHABLE one (no
|
|
// StoppingCollisionVolume flag) then sets the amount to the 0.00123f
|
|
// WALK-THROUGH SENTINEL -- Simulate's response block reads exactly that
|
|
// value as "the move stands": the mech drives THROUGH the prop while
|
|
// the prop takes the damage. Trees fall, walls do not.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::ProcessCollision(
|
|
Scalar time_slice,
|
|
BoxedSolidCollision &collision,
|
|
const Point3D &old_position,
|
|
Damage *damage
|
|
)
|
|
{
|
|
Check(this);
|
|
Verify(time_slice > 0.0f);
|
|
Check(&collision);
|
|
Check_Pointer(damage);
|
|
|
|
Scalar
|
|
penetration;
|
|
|
|
if (
|
|
!collisionVolume->ProcessCollision(
|
|
collision,
|
|
worldLinearVelocity,
|
|
lastCollisionList,
|
|
&damage->surfaceNormal,
|
|
&penetration)
|
|
)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Max_Clamp(penetration, time_slice);
|
|
Scalar
|
|
ratio = penetration / time_slice,
|
|
elasticity = elasticityCoefficient,
|
|
friction = frictionCoefficient;
|
|
|
|
Simulation
|
|
*owner_simulation = collision.GetTreeVolume()->GetOwningSimulation();
|
|
|
|
damage->damageAmount =
|
|
StaticBounce(
|
|
old_position,
|
|
time_slice,
|
|
ratio,
|
|
damage->surfaceNormal,
|
|
&elasticity,
|
|
minimumBounceSpeed,
|
|
&friction);
|
|
|
|
Entity
|
|
*other = (Entity *)owner_simulation;
|
|
|
|
if (other != NULL && other->IsDerivedFrom(Mover::ClassDerivations))
|
|
{
|
|
Vector3D
|
|
relative;
|
|
relative.Subtract(
|
|
worldLinearVelocity,
|
|
((Mover *)other)->GetWorldLinearVelocity());
|
|
if (damage->surfaceNormal * relative < -1.0e-4f)
|
|
{
|
|
return;
|
|
}
|
|
if (other->IsDerivedFrom(Mech::ClassDerivations))
|
|
{
|
|
Damage
|
|
ram = *damage;
|
|
ram.impactPoint = Point3D(
|
|
(collision.collisionSlice.minX + collision.collisionSlice.maxX) * 0.5f,
|
|
(collision.collisionSlice.minY + collision.collisionSlice.maxY) * 0.5f,
|
|
(collision.collisionSlice.minZ + collision.collisionSlice.maxZ) * 0.5f);
|
|
|
|
Entity::TakeDamageMessage
|
|
hit(
|
|
Entity::TakeDamageMessageID,
|
|
sizeof(Entity::TakeDamageMessage),
|
|
GetEntityID(),
|
|
-1,
|
|
ram
|
|
);
|
|
other->Dispatch(&hit);
|
|
}
|
|
}
|
|
|
|
if (other != NULL && other->IsDerivedFrom(CulturalIcon::ClassDerivations))
|
|
{
|
|
Logical
|
|
stopping =
|
|
((CulturalIcon *)other)->IsStoppingCollisionVolume();
|
|
|
|
//
|
|
// The separating gate against a STATIC icon (its velocity is zero,
|
|
// which the 1995 compiler inlined away).
|
|
//
|
|
if (damage->surfaceNormal * worldLinearVelocity < -1.0e-4f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
//
|
|
// The crunch goes out BEFORE the sentinel overwrites the amount.
|
|
//
|
|
{
|
|
Damage
|
|
crunch = *damage;
|
|
crunch.impactPoint = Point3D(
|
|
(collision.collisionSlice.minX + collision.collisionSlice.maxX) * 0.5f,
|
|
(collision.collisionSlice.minY + collision.collisionSlice.maxY) * 0.5f,
|
|
(collision.collisionSlice.minZ + collision.collisionSlice.maxZ) * 0.5f);
|
|
|
|
Entity::TakeDamageMessage
|
|
hit(
|
|
Entity::TakeDamageMessageID,
|
|
sizeof(Entity::TakeDamageMessage),
|
|
GetEntityID(),
|
|
-1,
|
|
crunch
|
|
);
|
|
other->Dispatch(&hit);
|
|
}
|
|
|
|
if (!stopping)
|
|
{
|
|
damage->damageAmount = 0.00123f;
|
|
}
|
|
}
|
|
|
|
//
|
|
// The contact-state accumulator. [T3 simplification, per the decomp's
|
|
// dominant branch: the authentic InitialHit(1)/Slide(2) split keys the
|
|
// StaticBounce OUT coefficients against an unidentified literal
|
|
// (@0x4ac048); a resolved contact IS an impact, so accumulate 1 and
|
|
// capture the impact speed for the audio scales.]
|
|
//
|
|
if (collisionTemporaryState == 0)
|
|
{
|
|
collisionTemporaryState = 1;
|
|
collisionSpeed = worldLinearVelocity.Length();
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// DistributeCollisionDamage (binary @0049ffcc, reached ONLY from the damage
|
|
// hub's type-0 divert) -- the collision-damage economy.
|
|
//
|
|
// Collision damage is the manual's "collision damage" TECHNICIAN SETTING:
|
|
// gated on the owning player's advanced-damage flag, priced against a
|
|
// 100 km/h reference impact, and applied as 0.5-point RATTLE CRITS to random
|
|
// internal subsystems. Armor is never touched by a collision.
|
|
//
|
|
// scale = (2000 / mass) / (100 km/h)^2 / (1 - elasticity^2)
|
|
// amount = raw * scale; under 0.5 the tap is FREE
|
|
// n = Round(amount * 2) sub-hits of amount/n each
|
|
//
|
|
// Each sub-hit lands on ONE roster subsystem drawn by cumulative
|
|
// collisionCriticalHitWeight against a [0,1) roll, restricted to the
|
|
// HeatSink / Gyroscope / Torso families. An un-won roll lands nowhere --
|
|
// faithful: the binary does not normalize the weights.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::DistributeCollisionDamage(Damage *damage)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(damage);
|
|
|
|
BTPlayer
|
|
*player = (BTPlayer *)GetPlayerLink();
|
|
if (player == NULL || !player->IsAdvancedDamageOn())
|
|
{
|
|
return;
|
|
}
|
|
|
|
Scalar
|
|
reference = 100.0f * 0.27777779f; // 100 km/h in world u/s
|
|
Scalar
|
|
denominator =
|
|
1.0f - elasticityCoefficient * elasticityCoefficient;
|
|
Scalar
|
|
scale = ((2000.0f / moverMass) / (reference * reference)) / denominator;
|
|
|
|
damage->damageAmount *= scale;
|
|
if (damage->damageAmount < 0.5f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int
|
|
sub_hits = (int)(damage->damageAmount * 2.0f + 0.5f);
|
|
if (sub_hits < 1)
|
|
{
|
|
sub_hits = 1;
|
|
}
|
|
damage->damageAmount /= (Scalar)sub_hits;
|
|
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[colldmg] rattle "
|
|
<< (damage->damageAmount * sub_hits) << " pts in "
|
|
<< sub_hits << " sub-hits (scale=" << scale << ")"
|
|
<< endl << flush;
|
|
}
|
|
|
|
int
|
|
hit,
|
|
slot;
|
|
for (hit = 0; hit < sub_hits; ++hit)
|
|
{
|
|
Scalar
|
|
threshold = (Scalar)Random,
|
|
accumulated = 0.0f;
|
|
for (slot = 0; slot < GetSubsystemCount(); ++slot)
|
|
{
|
|
Subsystem
|
|
*subsystem = GetSubsystem(slot);
|
|
if (subsystem == NULL)
|
|
{
|
|
continue;
|
|
}
|
|
if (
|
|
!subsystem->IsDerivedFrom(HeatSink::ClassDerivations) &&
|
|
!subsystem->IsDerivedFrom(Gyroscope::ClassDerivations) &&
|
|
!subsystem->IsDerivedFrom(Torso::ClassDerivations)
|
|
)
|
|
{
|
|
continue;
|
|
}
|
|
accumulated +=
|
|
((MechSubsystem *)subsystem)->CollisionCritWeight();
|
|
if (accumulated < threshold)
|
|
{
|
|
continue;
|
|
}
|
|
((MechSubsystem *)subsystem)->ApplyDamageAndMeasure(*damage);
|
|
break;
|
|
}
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
void
|
|
Mech::TakeDamageMessageHandler(TakeDamageMessage *message)
|
|
{
|
|
Check(this);
|
|
Check(message);
|
|
|
|
//
|
|
// The RAW record feeds the gyro FIRST -- even an invalid-zone hit shakes
|
|
// the cockpit (binary hub order step 1).
|
|
//
|
|
if (gyroSubsystem != NULL)
|
|
{
|
|
((Gyroscope *)gyroSubsystem)->ApplyDamageResponse(message->damageData);
|
|
}
|
|
|
|
lastInflictingID = message->inflictingEntity;
|
|
lastInflictingDamage = message->damageData.damageAmount;
|
|
|
|
//
|
|
// THE COLLISION DIVERT (binary @0x4a0368 -> @0049ffcc): type-0 Collision
|
|
// damage NEVER reaches the armor zones. It is priced against a 100 km/h
|
|
// reference impact and applied as internal RATTLE crits -- which is the
|
|
// answer to the raw kinetic numbers being thousands of points where a
|
|
// PPC is ~12. Without this divert a hard wall crash one-shots a vital
|
|
// zone through the cylinder lottery (observed live, 5.3.99).
|
|
//
|
|
if (message->damageData.damageType == Damage::CollisionDamageType)
|
|
{
|
|
DistributeCollisionDamage(&message->damageData);
|
|
return;
|
|
}
|
|
|
|
//
|
|
// The cylinder resolve (binary @0x4a0264 tail): an UNAIMED hit arrives
|
|
// with invalidDamageZone set -- map its world impact point onto a hull
|
|
// zone through the height x angle table, then let the base route it.
|
|
//
|
|
if (message->invalidDamageZone && damageLookupTable != NULL)
|
|
{
|
|
int zone = damageLookupTable->ResolveHit(
|
|
message->damageData.impactPoint);
|
|
if (zone >= 0 && zone < damageZoneCount)
|
|
{
|
|
message->damageZone = zone;
|
|
message->invalidDamageZone = False;
|
|
}
|
|
}
|
|
|
|
Entity::TakeDamageMessageHandler(message);
|
|
}
|
|
|
|
void
|
|
Mech::SetMappingSubsystem(Subsystem *subsystem)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(subsystemArray);
|
|
|
|
//
|
|
// The control mapper lives in roster slot 0 (the streamed control-mapping
|
|
// resource binds its DirectMappings to subsystemID 0, so it must resolve
|
|
// there via Entity::GetSimulation(0)). On a re-spawn, drop the old one.
|
|
//
|
|
if (subsystemArray[0] != NULL)
|
|
{
|
|
Unregister_Object(subsystemArray[0]);
|
|
delete subsystemArray[0];
|
|
}
|
|
subsystemArray[0] = subsystem;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// Reset -- the respawn heal-and-move (binary @0049fb74). REUSES the same
|
|
// entity (the sever-and-recreate respawn was the source of the 1995 port's
|
|
// "two mechs / camera inside / on-fire respawn" glitch family):
|
|
// 1. reposition at the drop zone (origin + transform + update base)
|
|
// 2. kill all motion (a respawn is a TELEPORT, no dead-reckon lerp back)
|
|
// 3. clear the death latch (the alarm trigger + the once-per-death flag)
|
|
// 4. heal every hull zone (structure 0, intact skin, not burning) --
|
|
// the crit-allotment accountant (damagePercentageUsed) PERSISTS by
|
|
// design: nothing in the recovered binary resets it across lives
|
|
// (BT411-observed; spent crit budgets stay spent)
|
|
// 5. sweep the roster through DeathReset (heat/power/ammo/charge restore)
|
|
// 6. revalidate for the sim (PreRun).
|
|
// The ForceUpdate(0x1f) re-broadcast + warp/alarm effects join the render /
|
|
// cockpit waves.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::Reset(const Origin &origin, Logical full_reset)
|
|
{
|
|
//
|
|
// The binary Reset (@0049fb74) drops any pending duck latch with the
|
|
// rest of the per-life state.
|
|
//
|
|
duckRequestLatch = 0;
|
|
|
|
Check(this);
|
|
|
|
localOrigin = origin;
|
|
localToWorld = origin;
|
|
updateOrigin = origin;
|
|
|
|
worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f);
|
|
localVelocity = Motion::Identity;
|
|
updateVelocity.linearMotion = Vector3D(0.0f, 0.0f, 0.0f);
|
|
updateVelocity.angularMotion = Vector3D(0.0f, 0.0f, 0.0f);
|
|
currentBodySpeed = 0.0f;
|
|
bodyTargetSpeed = 0.0f;
|
|
|
|
//
|
|
// The binary Reset (@0049fb74): the movement-mode alarm drops to
|
|
// DefaultState (FUN_0041bbd8(mech+0x2c, 0)) and the streamed mode
|
|
// request counters clear with it (+0x334/+0x338/+0x33c = 0).
|
|
//
|
|
SetSimulationState(0);
|
|
defaultStateRequests = 0;
|
|
gimpLeftRequests = 0;
|
|
gimpRightRequests = 0;
|
|
|
|
statusAlarm.SetLevel(0);
|
|
deathTransitionDone = 0;
|
|
|
|
{
|
|
for (int dz = 0; dz < damageZoneCount; ++dz)
|
|
{
|
|
if (damageZones[dz] != NULL)
|
|
{
|
|
::DamageZone *zone = (::DamageZone *)damageZones[dz];
|
|
zone->damageLevel = 0.0f;
|
|
zone->SetGraphicState(0);
|
|
zone->SetDamageZoneState(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
for (int ss = 0; ss < subsystemCount; ++ss)
|
|
{
|
|
if (subsystemArray[ss] != NULL)
|
|
{
|
|
subsystemArray[ss]->DeathReset(full_reset);
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// The binary Reset (@0049fb74 tail) renormalizes the coolant shares
|
|
// after the roster's per-subsystem resets restored every priority.
|
|
//
|
|
RedistributeCoolantShares();
|
|
|
|
SetPreRunFlag();
|
|
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[reset] mech " << GetEntityID()
|
|
<< " HEALED + placed at ("
|
|
<< origin.linearPosition.x << ","
|
|
<< origin.linearPosition.y << ","
|
|
<< origin.linearPosition.z << ")" << endl << flush;
|
|
}
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// CurrentTorsoTwist -- the live torso twist for the cylinder table's
|
|
// rotate-with-torso rows (binary reads torso+0x1d8). NULL-safe.
|
|
//#############################################################################
|
|
//
|
|
Scalar
|
|
Mech::CurrentTorsoTwist()
|
|
{
|
|
Check(this);
|
|
Torso *torso = (Torso *)sinkSourceSubsystem;
|
|
return (torso != NULL) ? torso->CurrentTwist() : 0.0f;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// ResolveJoint -- the shared skeleton-joint resolver (mech.cpp @00424b60).
|
|
// A subsystem hands us the joint NAME from its resource; we look up the
|
|
// skeleton segment of that name, read its joint index, and fetch the animated
|
|
// Joint from the JointSubsystem. NULL for an empty/unknown name or a mech with
|
|
// no skeleton/joint subsystem.
|
|
//#############################################################################
|
|
//
|
|
Joint*
|
|
Mech::ResolveJoint(const char *joint_name)
|
|
{
|
|
Check(this);
|
|
|
|
if (joint_name == NULL || joint_name[0] == '\0')
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
EntitySegment *segment = GetSegment(CString(joint_name));
|
|
if (segment == NULL)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
JointSubsystem *joints = GetJointSubsystem();
|
|
if (joints == NULL)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
//
|
|
// A segment that HAS no joint reports index -1, and GetJoint indexes a
|
|
// table with it unchecked -- TableIterator::GetNthImplementation walks to
|
|
// [base + -1*4] and dies (guest 00426A1D, ECX=FFFFFFFF). Torso never hit
|
|
// this because it only ever asks for its own authored twist-joint name;
|
|
// the skeleton walk asks for EVERY page, and most .SKL pages are sites or
|
|
// static segments with no joint at all.
|
|
//
|
|
int
|
|
joint_index = segment->GetJointIndex();
|
|
if (joint_index < 0 || joint_index >= joints->GetJointCount())
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
return joints->GetJoint(joint_index);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// CommitLookState -- the look-button eyepoint commit (the binary's five-state
|
|
// look machine tail, controls mapper part_013.c:396-459). Re-aims the eyepoint
|
|
// from the model's authored look angles: side looks yaw by lookLeft/RightAngle,
|
|
// look-behind is yaw pi with lookBackAngle pitch, look-down pitches by
|
|
// lookFrontAngle, forward is identity. The committed pitch/yaw are stored in
|
|
// lookPitch/lookYaw so the per-frame compose in Simulate can keep adding the
|
|
// live Torso elevation on top.
|
|
//
|
|
// Also part of the authentic commit, deferred to the weapon wave: re-arming
|
|
// each weapon's view-fire enable (forward view = the non-rear-mounted weapons,
|
|
// look-back = the rear-mounted ones, side/down = none) and flipping the HUD pip
|
|
// group mask (forward = front group, look-back = rear group) -- both need the
|
|
// MechWeapon viewFireEnable/rearFiring members, not yet reconstructed.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::CommitLookState(int look_state)
|
|
{
|
|
Check(this);
|
|
|
|
Scalar pitch = 0.0f;
|
|
Scalar yaw = 0.0f;
|
|
|
|
switch (look_state)
|
|
{
|
|
case MechControlsMapper::LookLeftState:
|
|
yaw = lookLeftAngle;
|
|
break;
|
|
case MechControlsMapper::LookRightState:
|
|
yaw = lookRightAngle;
|
|
break;
|
|
case MechControlsMapper::LookBehindState:
|
|
yaw = PI;
|
|
pitch = lookBackAngle;
|
|
break;
|
|
case MechControlsMapper::LookDownState:
|
|
pitch = lookFrontAngle;
|
|
break;
|
|
default:
|
|
break; // LookNone: identity
|
|
}
|
|
|
|
lookPitch = pitch;
|
|
lookYaw = yaw;
|
|
eyepointRotation = EulerAngles(
|
|
Radian(Radian::Normalize(pitch)),
|
|
Radian(Radian::Normalize(yaw)),
|
|
Radian(0.0f)
|
|
);
|
|
|
|
//
|
|
// Re-arm each weapon's view-fire enable: the forward view arms the
|
|
// non-rear-mounted weapons, LOOK-BACK arms the rear-mounted ones, and the
|
|
// side/down views arm none.
|
|
//
|
|
{
|
|
for (int id = 2; id < subsystemCount; ++id)
|
|
{
|
|
Subsystem *sub = subsystemArray[id];
|
|
if (sub == NULL || !sub->IsDerivedFrom(MechWeapon::ClassDerivations))
|
|
{
|
|
continue;
|
|
}
|
|
MechWeapon *weapon = (MechWeapon *)sub;
|
|
Logical arm;
|
|
if (look_state == MechControlsMapper::LookNone)
|
|
{
|
|
arm = (weapon->IsRearFiring() == False);
|
|
}
|
|
else if (look_state == MechControlsMapper::LookBehindState)
|
|
{
|
|
arm = weapon->IsRearFiring();
|
|
}
|
|
else
|
|
{
|
|
arm = False;
|
|
}
|
|
weapon->SetViewFireEnable(arm);
|
|
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[look] weapon '" << weapon->GetName()
|
|
<< "' rear=" << (int)weapon->IsRearFiring()
|
|
<< " armed=" << (int)arm << endl << flush;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// The HUD reticle weapon-pip group: the forward view shows the FRONT pip
|
|
// group, look-back shows the REAR group; side/down views leave the mask.
|
|
//
|
|
if (look_state == MechControlsMapper::LookNone)
|
|
{
|
|
targetReticle.reticleElementMask = (Reticle::ReticleElements)
|
|
(((int)targetReticle.reticleElementMask | Reticle::FrontFiringWeaponsOn)
|
|
& ~Reticle::RearFiringWeaponsOn);
|
|
}
|
|
else if (look_state == MechControlsMapper::LookBehindState)
|
|
{
|
|
targetReticle.reticleElementMask = (Reticle::ReticleElements)
|
|
(((int)targetReticle.reticleElementMask | Reticle::RearFiringWeaponsOn)
|
|
& ~Reticle::FrontFiringWeaponsOn);
|
|
}
|
|
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[look] state=" << look_state
|
|
<< " yaw=" << yaw << " pitch=" << pitch
|
|
<< " pipMask=0x" << hex << (int)targetReticle.reticleElementMask << dec
|
|
<< endl << flush;
|
|
}
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// Simulate -- the mech's per-frame body Performance (the Mover locomotion tick).
|
|
//
|
|
// FUNCTIONAL MOTION CORE (Phase 5.3, increment 1). A Mech is a Mover; its
|
|
// per-frame job is to advance its Origin and commit it to the world transform.
|
|
// This reconstructs the load-bearing spine of the 1995 Mech::Simulate
|
|
// (mech4.cpp @004ab430): integrate the body velocity into localOrigin, then
|
|
// rebuild localToWorld with the engine's own idiom (ENTITY.CPP:988 /
|
|
// MOVER.CPP:850, `localToWorld = localOrigin`).
|
|
//
|
|
// Still layered on top of this (next increments):
|
|
// * the gait-cycle self-propulsion that FEEDS worldLinearVelocity -- the
|
|
// authentic model drives forward speed from the walk/run animation cycle
|
|
// (IntegrateMotion -> AdvanceBodyAnimation -> cycleDistance) steered by the
|
|
// control-mapper demands (throttle/turn), not a raw velocity;
|
|
// * heading integration (rotate localOrigin.angularPosition by the turn rate);
|
|
// * the terrain-height drop (BoundingBoxTreeNode::FindBoundingBoxUnder) that
|
|
// rests the feet on the ground;
|
|
// * the cockpit telemetry FilteredScalars (head/aim/leg/torso angular rates).
|
|
//
|
|
// With no locomotion layer yet, worldLinearVelocity is zero for a freshly
|
|
// spawned mech, so it holds its pose -- identical to the prior DoNothing, but
|
|
// now on the real Simulate path. DEV hook BT_DRIVE="vx,vy,vz" injects a
|
|
// constant world velocity so the integrate + transform path is verifiable
|
|
// headlessly (no RIO/controls needed). See MECH.NOTES.md.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::Simulate(Scalar time_slice)
|
|
{
|
|
Check(this);
|
|
|
|
//
|
|
// The effectiveness census is the master perf's FIRST call
|
|
// (@0049fa1c, before everything else, dead or alive).
|
|
//
|
|
UpdateCombatEffectiveness();
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// DEATH (binary UpdateDeathState @mech4, the once-per-death transition):
|
|
// a destroyed mech FREEZES -- no drive, no eyepoint, no dev harness (the
|
|
// weapon hard gates silence the guns state-side). The one-shot: sweep
|
|
// the roster through DeathShutdown(1) and dispatch the VehicleDead
|
|
// death notification (deathCount = -1, the ctor default) to the owning
|
|
// player -- only the owner master has a live playerLink; an unowned
|
|
// wreck (the dev enemy) just settles.
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
if (IsMechDestroyed())
|
|
{
|
|
if (!deathTransitionDone)
|
|
{
|
|
deathTransitionDone = 1;
|
|
|
|
//
|
|
// A wreck is STILL: kill the broadcastable motion so replicant
|
|
// wrecks don't dead-reckon away at the last drive vector.
|
|
//
|
|
worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f);
|
|
updateVelocity.linearMotion = Vector3D(0.0f, 0.0f, 0.0f);
|
|
updateVelocity.angularMotion = Vector3D(0.0f, 0.0f, 0.0f);
|
|
currentBodySpeed = 0.0f;
|
|
bodyTargetSpeed = 0.0f;
|
|
|
|
for (int ds = 0; ds < subsystemCount; ++ds)
|
|
{
|
|
if (subsystemArray[ds] != NULL)
|
|
{
|
|
subsystemArray[ds]->DeathShutdown(1);
|
|
}
|
|
}
|
|
|
|
//
|
|
// The death notification is MASTER-only: our
|
|
// InitializePlayerLink binds replicant mechs to replicant
|
|
// players too, and a replicant dispatch would run a second,
|
|
// non-authoritative death cycle on every remote pod.
|
|
//
|
|
Player *pilot = (GetInstance() != Entity::ReplicantInstance)
|
|
? GetPlayerLink() : NULL;
|
|
if (pilot != NULL)
|
|
{
|
|
Player::VehicleDeadMessage
|
|
dead(
|
|
Player::VehicleDeadMessageID,
|
|
sizeof(Player::VehicleDeadMessage)
|
|
);
|
|
pilot->Dispatch(&dead);
|
|
}
|
|
|
|
//
|
|
// The KILL CREDIT: resolve the last attacker and post the
|
|
// type-2 ScoreMessage to the KILLER's pilot, carrying the
|
|
// killing-blow magnitude (the whole award derives from it --
|
|
// binary emitter in the unexported master-perf writer; the
|
|
// kill-bonus bias rides scoreAward, staged 0). MASTER-only,
|
|
// and an unpiloted killer (the dev enemy) earns nothing.
|
|
//
|
|
if (GetInstance() != Entity::ReplicantInstance
|
|
&& !(lastInflictingID == EntityID::Null))
|
|
{
|
|
Entity *killer = (Entity *)application->GetHostManager()->
|
|
GetEntityPointer(lastInflictingID);
|
|
if (killer != NULL && killer != (Entity *)this
|
|
&& killer->IsDerivedFrom(Mech::ClassDerivations)
|
|
&& killer->GetPlayerLink() != NULL)
|
|
{
|
|
BTPlayer::ScoreMessage
|
|
kill_score(
|
|
Player::ScoreMessageID,
|
|
sizeof(BTPlayer::ScoreMessage),
|
|
0.0f,
|
|
BTPlayer::KillScore,
|
|
lastInflictingDamage,
|
|
GetEntityID()
|
|
);
|
|
killer->GetPlayerLink()->Dispatch(&kill_score);
|
|
}
|
|
}
|
|
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[death] mech " << GetEntityID()
|
|
<< " WRECKED (pilot "
|
|
<< ((pilot != NULL) ? "notified" : "none")
|
|
<< ")" << endl << flush;
|
|
}
|
|
}
|
|
Check_Fpu();
|
|
return;
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// Read the control-mapper locomotion demands. The mapper lives at roster
|
|
// slot 0 and its InterpretControls Performance ticks in the Entity::Perform
|
|
// AndWatch roster walk BEFORE this (the mech's own Performance runs last),
|
|
// so speedDemand/turnDemand are this frame's.
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
Scalar speedDemand = 0.0f;
|
|
Scalar turnDemand = 0.0f;
|
|
if (subsystemArray != NULL && subsystemArray[0] != NULL)
|
|
{
|
|
MechControlsMapper *mapper = (MechControlsMapper *)subsystemArray[0];
|
|
speedDemand = mapper->GetSpeedDemand();
|
|
turnDemand = mapper->GetTurnDemand();
|
|
}
|
|
bodyTargetSpeed = speedDemand;
|
|
|
|
//
|
|
// THE SUPER STOP (master perf @0x4aa353): the brake latch, the accel
|
|
// swap and the cockpit lurch, evaluated from this frame's demand.
|
|
//
|
|
//
|
|
// THE MOBILITY SCAN (master perf @0x4a9cf0), BEFORE the demands are
|
|
// consumed: damaged myomers scale the throttle the pilot actually gets.
|
|
//
|
|
UpdateMobilityScale();
|
|
speedDemand = 0.0f;
|
|
turnDemand = 0.0f;
|
|
if (subsystemArray != NULL && subsystemArray[0] != NULL)
|
|
{
|
|
MechControlsMapper *rescan = (MechControlsMapper *)subsystemArray[0];
|
|
speedDemand = rescan->GetSpeedDemand();
|
|
turnDemand = rescan->GetTurnDemand();
|
|
}
|
|
bodyTargetSpeed = speedDemand;
|
|
|
|
UpdateSuperStop(time_slice, speedDemand);
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// THE GAIT (mech2.cpp). The leg channel poses the skeleton from the live
|
|
// demand -- this is what makes the legs actually move; the body channel
|
|
// runs as a pure stride measurement (move_joints 0) so the two never
|
|
// fight over the same joints.
|
|
//
|
|
// The body channel's measured stride IS the mech's speed (the binary:
|
|
// IntegrateMotion @004ab1c8 sets local velocity z = -distance/dt). The
|
|
// gait slews its cycle speed at forwardCycleRate == the authored
|
|
// maxAcceleration, so this replaces the earlier explicit acceleration
|
|
// model at the same rate -- but the speed now rises THROUGH the gait:
|
|
// zero while standing, the walk band during the walk cycle, stepping to
|
|
// the run band at a clip boundary.
|
|
//
|
|
// STILL STAGED from IntegrateMotion: the airborne flavour pick, the
|
|
// dead-reckon latency fold, and the turn-in-place dispatcher (mech4).
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
//
|
|
// The limp pick (IntegrateMotion @004ab1c8): movement modes 3/4 are the
|
|
// left/right leg limps, and while limping (and the model HAS limp clips)
|
|
// the Gimp advancers replace the normal pair -- they must, because the
|
|
// normal ones treat the limp states as their reset group.
|
|
//
|
|
// BT_FORCE_LIMP=3|4 is a DEV hook that forces the pick without touching
|
|
// the simulation state, so the limp gait can be verified before the
|
|
// damage model's limp hook (leg zone >= 0.5 -> mode 3/4) is
|
|
// reconstructed.
|
|
//
|
|
//
|
|
// BT_FORCE_DUCK=1: one authentic duck press every ~4s on its own
|
|
// clock (independent of the one-shot button harness below) -- the
|
|
// DuckRequest message, plus the held cell and analog demand every
|
|
// frame. Press one ducks, the next (from the hold) stands: the
|
|
// full-machine cycle soak.
|
|
//
|
|
{
|
|
static int s_forceDuck = -1;
|
|
static Scalar s_duckClock = 0.0f;
|
|
if (s_forceDuck < 0)
|
|
{
|
|
s_forceDuck = (getenv("BT_FORCE_DUCK") != NULL) ? 1 : 0;
|
|
}
|
|
if (s_forceDuck)
|
|
{
|
|
MechControlsMapper
|
|
*pulse_mapper = (MechControlsMapper *)subsystemArray[0];
|
|
if (pulse_mapper != NULL)
|
|
{
|
|
pulse_mapper->duckCommand = 1;
|
|
}
|
|
mobilityScale = 1.0f; // hold the legs-healthy gate open
|
|
s_duckClock += time_slice;
|
|
if (s_duckClock >= 4.0f)
|
|
{
|
|
s_duckClock = 0.0f;
|
|
ReceiverDataMessageOf<int>
|
|
duck_press(
|
|
Mech::DuckRequestMessageID,
|
|
sizeof(ReceiverDataMessageOf<int>),
|
|
1);
|
|
Dispatch(&duck_press);
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// THE MODE-REQUEST COUNTERS (master perf @004a9b5c -- the function IS
|
|
// in the BT411 decomp after all, part_013.c:3059; the old "never
|
|
// decompiled" note was wrong). Three STREAMED request counters
|
|
// (+0x334/+0x338/+0x33c); while one is positive the master perf
|
|
// re-asserts the movement-mode alarm every frame, tracing the
|
|
// authentic state name to the debug stream. They are fed only by the
|
|
// network stream appliers (creation record +0x1c0.., update records);
|
|
// the LOCAL damage path writes the mode directly (MECHDMG,
|
|
// @0x49c8fb/@0x49c926 half-leg limps, @0x49c88f/@0x49c8c5/@0x49c83c
|
|
// kills) -- the staged statusAlarm->mode "promotion" that lived here
|
|
// was a misconstruction and is retired.
|
|
//
|
|
if (defaultStateRequests > 0)
|
|
{
|
|
DEBUG_STREAM << "DefaultState" << endl << flush;
|
|
SetSimulationState(0);
|
|
}
|
|
if (gimpLeftRequests > 0)
|
|
{
|
|
DEBUG_STREAM << "GimpLeft" << endl << flush;
|
|
SetSimulationState(3);
|
|
}
|
|
if (gimpRightRequests > 0)
|
|
{
|
|
DEBUG_STREAM << "GimpRight" << endl << flush;
|
|
SetSimulationState(4);
|
|
}
|
|
|
|
//
|
|
// THE DUCK DRIVER (master perf @004a9f61..@004aa155; 5.3.121 confirmed
|
|
// line-by-line against the decomp C, part_013.c:3059, and MOVED here --
|
|
// the binary runs it right after the mode-request counters, BEFORE the
|
|
// leg advance, so a duck arm is seen by the same frame's advance).
|
|
//
|
|
// Phase pick, every frame: not standing (raw state != 0) or duck button
|
|
// released -> no request. Standing with the button held: from gait
|
|
// state 0/4 (stand / turn-in-place), request duck-DOWN while the analog
|
|
// demand clears the 1e-4 deadzone; from gait state 1 (ducked hold),
|
|
// request duck-UP; anything else, none.
|
|
//
|
|
// The arms fire only on a DuckRequest-latched frame of a squat-capable
|
|
// mech, bind the LEG channel only (squat is a leg figure), and set the
|
|
// volume alarm: down -> level 0 (ducked), up -> level 1 (standing).
|
|
// The latch is consumed unconditionally. A mech that STARTS LIMPING
|
|
// while ducked is stood up by force (modes 3/4 with gait state 1).
|
|
// On a volume-alarm edge the collision template's top swaps between
|
|
// the captured heights; any other level is the authentic
|
|
// "Whoa! Bad Collision Volume State!" Fail. The dirty marks (binary
|
|
// MarkUpdate(8)+MarkUpdate(1) per arm) ride ForceUpdate -- still a
|
|
// no-op here until the update-record feed lands.
|
|
//
|
|
{
|
|
int
|
|
raw_mode = (int)GetSimulationState();
|
|
//
|
|
// The mapper lives in roster slot 0 (the binary caches it at
|
|
// mech+0x190; our tree resolves the same object through the roster).
|
|
//
|
|
MechControlsMapper
|
|
*duck_mapper = (MechControlsMapper *)subsystemArray[0];
|
|
int
|
|
duck_held =
|
|
(duck_mapper != NULL && duck_mapper->DuckCommand() != 0);
|
|
|
|
if (raw_mode != 0 || !duck_held)
|
|
{
|
|
duckPhaseRequest = 0;
|
|
}
|
|
else
|
|
{
|
|
int
|
|
leg_state = (int)legStateAlarm.GetLevel();
|
|
if (leg_state == 0 || leg_state == 4)
|
|
{
|
|
//
|
|
// The binary gate here is |mobilityScale| > 1e-4: "the
|
|
// legs still work" (see MECH.HPP) -- NOT a crouch analog.
|
|
//
|
|
duckPhaseRequest =
|
|
(mobilityScale <= 0.0001f && mobilityScale >= -0.0001f) ? 0 : 1;
|
|
}
|
|
else if (leg_state == 1)
|
|
{
|
|
duckPhaseRequest = 2;
|
|
}
|
|
else
|
|
{
|
|
duckPhaseRequest = 0;
|
|
}
|
|
}
|
|
|
|
{
|
|
static int s_duckEvalLog = -1;
|
|
if (s_duckEvalLog < 0)
|
|
{
|
|
s_duckEvalLog = (getenv("BT_MECH_LOG") != NULL);
|
|
}
|
|
if (s_duckEvalLog && duckRequestLatch != 0)
|
|
{
|
|
DEBUG_STREAM << "[duck] eval mode=" << raw_mode
|
|
<< " held=" << duck_held
|
|
<< " leg=" << legStateAlarm.GetLevel()
|
|
<< " phase=" << duckPhaseRequest
|
|
<< " capable=" << squatCapable << endl;
|
|
}
|
|
}
|
|
if (duckRequestLatch != 0 && squatCapable != 0)
|
|
{
|
|
if (duckPhaseRequest == 1)
|
|
{
|
|
SetLegAnimation(2); // the sqd (squat-down) clip slot -- the
|
|
// SLOT map diverges from the enum's walk
|
|
// names here (MECH2.NOTES.md)
|
|
ForceUpdate(8);
|
|
ForceUpdate(1);
|
|
collisionVolumeState.SetLevel(0);
|
|
}
|
|
else if (duckPhaseRequest == 2)
|
|
{
|
|
SetLegAnimation(3); // the squ (squat-up) clip slot
|
|
ForceUpdate(8);
|
|
ForceUpdate(1);
|
|
collisionVolumeState.SetLevel(1);
|
|
}
|
|
}
|
|
duckRequestLatch = 0;
|
|
|
|
//
|
|
// Limping stands you up (modes 3/4 with the legs still in the
|
|
// ducked hold).
|
|
//
|
|
if (
|
|
(raw_mode == 3 || raw_mode == 4) &&
|
|
(int)legStateAlarm.GetLevel() == 1
|
|
)
|
|
{
|
|
SetLegAnimation(3); // squ -- forced stand-up
|
|
ForceUpdate(8);
|
|
ForceUpdate(1);
|
|
collisionVolumeState.SetLevel(1);
|
|
}
|
|
|
|
//
|
|
// The volume swap on an alarm edge, and the DuckState publish.
|
|
//
|
|
if (collisionVolumeState.GetState() != collisionVolumeState.GetOldState())
|
|
{
|
|
static int s_duckLog = -1;
|
|
if (s_duckLog < 0)
|
|
{
|
|
s_duckLog = (getenv("BT_MECH_LOG") != NULL);
|
|
}
|
|
if (s_duckLog)
|
|
{
|
|
DEBUG_STREAM << "[duck] volume level "
|
|
<< collisionVolumeState.GetOldState() << " -> "
|
|
<< collisionVolumeState.GetState()
|
|
<< " legState=" << legStateAlarm.GetLevel()
|
|
<< " maxY " << standingVolumeHeight
|
|
<< "/" << duckedVolumeHeight << endl;
|
|
}
|
|
switch (collisionVolumeState.GetLevel())
|
|
{
|
|
case 1:
|
|
SetStandingCollisionVolume();
|
|
duckState = 0;
|
|
break;
|
|
case 0:
|
|
SetDuckedCollisionVolume();
|
|
duckState = 1;
|
|
break;
|
|
default:
|
|
Fail("Whoa! Bad Collision Volume State!\n");
|
|
break;
|
|
}
|
|
}
|
|
collisionVolumeState.SetLevel(collisionVolumeState.GetLevel());
|
|
}
|
|
|
|
int limping;
|
|
{
|
|
int mode = MovementMode();
|
|
limping = (mode == 3 || mode == 4) && hasGimpClips;
|
|
}
|
|
|
|
//
|
|
// The master performance clears the leg reset latch at the top of every
|
|
// frame (@0x4a9bff); the wind-down inside the advancer may set it again,
|
|
// and the turn-in-place dispatcher below honours it -- so a walk that
|
|
// wound down THIS frame cannot re-enter as a turn until the next.
|
|
//
|
|
legResetLatch = 0;
|
|
|
|
//
|
|
// DEATH-GUARDED (master perf: FUN_0049fb54 right before the pick) -- a
|
|
// mech in the death transition (modes 2/9) does not advance the leg
|
|
// gait; the crash clips run on the body machine's clock. (The binary
|
|
// leaves its advance-distance stack slot stale in that case; we skip.)
|
|
//
|
|
if (!Mech::InDeathTransition(this))
|
|
{
|
|
if (limping)
|
|
{
|
|
AdvanceLegAnimationGimp(time_slice);
|
|
}
|
|
else
|
|
{
|
|
AdvanceLegAnimation(time_slice);
|
|
}
|
|
}
|
|
|
|
//
|
|
// The gyro's joint writes (binary master-perf tail @0x4aaf74/83, AFTER
|
|
// the animation pass): the idle sway onto the EyeJoint, and the
|
|
// integrated eye offset + body tip onto 'jointeye' -- the joint the
|
|
// cockpit eyepoint rides. Gated off during the death clips exactly as
|
|
// the tail is.
|
|
//
|
|
//
|
|
// THE INSTABILITY ACCUMULATOR (master perf tail @0x4aab8e): the three
|
|
// weighted terms, clamped, published to the cockpit attribute and
|
|
// republished to the gyro -- computed BEFORE the gyro writes its
|
|
// joints, as the binary orders it.
|
|
//
|
|
UpdateTelemetryRings(time_slice);
|
|
UpdateInstability(speedDemand);
|
|
|
|
if (gyroSubsystem != NULL && MovementMode() < 5)
|
|
{
|
|
((Gyroscope *)gyroSubsystem)->WriteEyeJoint();
|
|
((Gyroscope *)gyroSubsystem)->WriteMechJoint();
|
|
}
|
|
|
|
//
|
|
// THE TURN-IN-PLACE DISPATCHER (master perf @0x4aa505-0x4aa588, decoded
|
|
// from raw disasm; 5.3.121: confirmed against the decomp C, which DOES
|
|
// carry the master perf at part_013.c:3059). From
|
|
// Standing, arm the trn clip when the mech is TURNING (the binary tests
|
|
// |angularVelocity| > 1e-4; at a standstill the turn rate is exactly
|
|
// walkingTurnRate, so the operand is turnDemand * walkingTurnRate), the
|
|
// speed demand sits in the FULL sub-walk band [0, standSpeed], the model
|
|
// has a trn clip, and the wind-down debounce is clear.
|
|
//
|
|
// BOTH channels arm on the same frame -- the lockstep weld. Arming only
|
|
// the leg lets the body enter its next cycle frames apart and the two
|
|
// walk cycles run permanently out of phase. Each trn clip plays one
|
|
// turn figure and drops back to Standing (the case-4 group), so a held
|
|
// turn re-arms clip after clip.
|
|
//
|
|
if (!limping && turnCapable != 0 && legResetLatch == 0)
|
|
{
|
|
Scalar
|
|
standstill_turn_rate = turnDemand * walkingTurnRate;
|
|
if (
|
|
legStateAlarm.GetLevel() == 0 &&
|
|
bodyStateAlarm.GetLevel() == 0 &&
|
|
speedDemand >= 0.0f && speedDemand <= standSpeed &&
|
|
(standstill_turn_rate > 1.0e-4f || standstill_turn_rate < -1.0e-4f)
|
|
)
|
|
{
|
|
SetLegAnimation(4);
|
|
SetBodyAnimation(4);
|
|
}
|
|
}
|
|
{
|
|
Scalar stride = limping
|
|
? AdvanceBodyAnimationGimp(time_slice, 0)
|
|
: AdvanceBodyAnimation(time_slice, 0);
|
|
|
|
if (animationClips[5] != ResourceDescription::NullResourceID)
|
|
{
|
|
currentBodySpeed = stride / time_slice;
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// [T3 bring-up] A model with NO gait clips would otherwise never
|
|
// move. Keep the old acceleration model for those until every
|
|
// fleet mech's clip set is verified.
|
|
//
|
|
Scalar dv = bodyTargetSpeed - currentBodySpeed;
|
|
Scalar maxStep = maxBodyAcceleration * time_slice;
|
|
if (dv > maxStep) dv = maxStep;
|
|
if (dv < -maxStep) dv = -maxStep;
|
|
currentBodySpeed += dv;
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// Authentic per-mech turn rate: lerp(walkingTurnRate, runningTurnRate) by
|
|
// ground speed, with a runningTurnRate/t^2 over-run falloff past top speed;
|
|
// clamp >= 0. (mech4.cpp master-perf @0x4aa3d3.)
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
Scalar authTurnRate = walkingTurnRate;
|
|
{
|
|
Scalar spd = (currentBodySpeed < 0.0f) ? -currentBodySpeed : currentBodySpeed;
|
|
if (spd >= reverseSpeedMax)
|
|
{
|
|
Scalar den = reverseStrideLength - walkStrideLength;
|
|
Scalar t = (den != 0.0f) ? (spd - walkStrideLength) / den : 0.0f;
|
|
if (t <= 1.0f)
|
|
{
|
|
authTurnRate = walkingTurnRate + (runningTurnRate - walkingTurnRate) * t;
|
|
}
|
|
else
|
|
{
|
|
authTurnRate = runningTurnRate / (t * t);
|
|
}
|
|
}
|
|
if (authTurnRate < 0.0f)
|
|
{
|
|
authTurnRate = 0.0f;
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// Integrate heading (yaw) into the body orientation quaternion via the
|
|
// engine's rotation-integrate op (Quaternion::Add(source, omega*dt)), then
|
|
// rebuild the world transform so the facing axis below is current.
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
{
|
|
Vector3D angStep;
|
|
angStep.x = 0.0f;
|
|
angStep.y = turnDemand * authTurnRate * time_slice;
|
|
angStep.z = 0.0f;
|
|
Quaternion prevPose = localOrigin.angularPosition;
|
|
localOrigin.angularPosition.Add(prevPose, angStep);
|
|
}
|
|
localToWorld = localOrigin;
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// Forward step: the mech faces local -Z (gun ports / eyepoint at -Z). Take
|
|
// the world Z basis and negate for the facing direction; move at the current
|
|
// body speed. (The animation-exact per-frame advance from the gait clip is
|
|
// the deferred fidelity layer; this is the procedural equivalent.)
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
UnitVector zAxis;
|
|
localToWorld.GetFromAxis(Z_Axis, &zAxis);
|
|
worldLinearVelocity.x = -zAxis.x * currentBodySpeed;
|
|
worldLinearVelocity.y = -zAxis.y * currentBodySpeed;
|
|
worldLinearVelocity.z = -zAxis.z * currentBodySpeed;
|
|
|
|
//
|
|
// DEV cockpit-button harness: BT_PRESS_VALVE / BT_PRESS_FLUSH dispatch the
|
|
// REAL cockpit messages (MoveValve to Condenser1, InjectCoolant to the
|
|
// Reservoir) once, ~6 s in -- exercising the authentic Receiver dispatch ->
|
|
// handler-table path headlessly.
|
|
//
|
|
{
|
|
static Scalar pressClock = 0.0f;
|
|
static int pressed = 0;
|
|
pressClock += time_slice;
|
|
if (!pressed && pressClock >= 6.0f)
|
|
{
|
|
pressed = 1;
|
|
if (getenv("BT_PRESS_VALVE"))
|
|
{
|
|
for (int s = 2; s < subsystemCount; ++s)
|
|
{
|
|
Subsystem *sub = subsystemArray[s];
|
|
if (sub != NULL
|
|
&& sub->IsDerivedFrom(Condenser::ClassDerivations))
|
|
{
|
|
ReceiverDataMessageOf<int> press(
|
|
Condenser::MoveValveMessageID,
|
|
sizeof(ReceiverDataMessageOf<int>),
|
|
1
|
|
);
|
|
sub->Dispatch(&press);
|
|
break; // one press, the first condenser
|
|
}
|
|
}
|
|
}
|
|
if (getenv("BT_PRESS_FLUSH"))
|
|
{
|
|
for (int s = 2; s < subsystemCount; ++s)
|
|
{
|
|
Subsystem *sub = subsystemArray[s];
|
|
if (sub != NULL
|
|
&& sub->IsDerivedFrom(Reservoir::ClassDerivations))
|
|
{
|
|
ReceiverDataMessageOf<int> press(
|
|
Reservoir::InjectCoolantMessageID,
|
|
sizeof(ReceiverDataMessageOf<int>),
|
|
1
|
|
);
|
|
sub->Dispatch(&press);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
//
|
|
// BT_PRESS_BALANCE: one mech-level BalanceCoolant press (0x16)
|
|
// through the normal dispatch -- the equal-split button.
|
|
//
|
|
if (getenv("BT_PRESS_BALANCE"))
|
|
{
|
|
ReceiverDataMessageOf<int> balance_press(
|
|
Mech::BalanceCoolantMessageID,
|
|
sizeof(ReceiverDataMessageOf<int>),
|
|
1
|
|
);
|
|
Dispatch(&balance_press);
|
|
}
|
|
//
|
|
// BT_PRESS_EJECT: one EjectPilot press (0x19) through the normal
|
|
// dispatch (pair with BT_FORCE_CRIPPLED=1 -- a healthy mech
|
|
// refuses the eject).
|
|
//
|
|
if (getenv("BT_PRESS_EJECT"))
|
|
{
|
|
ReceiverDataMessageOf<int> eject_press(
|
|
Mech::EjectPilotMessageID,
|
|
sizeof(ReceiverDataMessageOf<int>),
|
|
1
|
|
);
|
|
Dispatch(&eject_press);
|
|
}
|
|
//
|
|
// BT_PRESS_GEN=1..4: SelectGenerator<N> at the first Emitter
|
|
// (the generator panel re-tap); BT_PRESS_SEEK: ToggleSeekVoltage
|
|
// at the first Emitter (the seek dial).
|
|
//
|
|
{
|
|
const char *press_gen = getenv("BT_PRESS_GEN");
|
|
const char *press_seek = getenv("BT_PRESS_SEEK");
|
|
if (press_gen != NULL || press_seek != NULL)
|
|
{
|
|
for (int s = 2; s < subsystemCount; ++s)
|
|
{
|
|
Subsystem *sub = subsystemArray[s];
|
|
if (sub != NULL
|
|
&& sub->IsDerivedFrom(Emitter::ClassDerivations))
|
|
{
|
|
if (press_gen != NULL)
|
|
{
|
|
int n = atoi(press_gen);
|
|
if (n >= 1 && n <= 4)
|
|
{
|
|
ReceiverDataMessageOf<int> press(
|
|
PoweredSubsystem::
|
|
SelectGeneratorAMessageID
|
|
+ (n - 1),
|
|
sizeof(ReceiverDataMessageOf<int>),
|
|
1
|
|
);
|
|
sub->Dispatch(&press);
|
|
}
|
|
}
|
|
if (press_seek != NULL)
|
|
{
|
|
ReceiverDataMessageOf<int> press(
|
|
Emitter::ToggleSeekVoltageMessageID,
|
|
sizeof(ReceiverDataMessageOf<int>),
|
|
1
|
|
);
|
|
sub->Dispatch(&press);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// DEV override: BT_DRIVE forces a raw world velocity (bypasses the demands,
|
|
// for the pure integrate/transform test).
|
|
//
|
|
{
|
|
const char *drive = getenv("BT_DRIVE");
|
|
if (drive != NULL)
|
|
{
|
|
float dx = 0.0f, dy = 0.0f, dz = 0.0f;
|
|
if (sscanf(drive, "%f,%f,%f", &dx, &dy, &dz) == 3)
|
|
{
|
|
worldLinearVelocity.x = dx;
|
|
worldLinearVelocity.y = dy;
|
|
worldLinearVelocity.z = dz;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// Eyepoint / aim-ray composition (mech4.cpp @~5219, pixel-calibrated in the
|
|
// BT411 reverse-engineering). The pilot's torso-elevation aim does NOT tilt
|
|
// any skeleton joint on this mech family -- it pitches the cockpit eye and
|
|
// the weapon boresight directly. Compose the committed look-state pitch/yaw
|
|
// (CommitLookState, driven by the look buttons) with the Torso's live
|
|
// currentElevation. DPLEyeRenderable / the aim ray read this each frame.
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
{
|
|
Scalar elevation = 0.0f;
|
|
if (sinkSourceSubsystem != NULL)
|
|
{
|
|
elevation = ((Torso *)sinkSourceSubsystem)->CurrentElevation();
|
|
}
|
|
eyepointRotation = EulerAngles(
|
|
Radian(Radian::Normalize(lookPitch + elevation)),
|
|
Radian(Radian::Normalize(lookYaw)),
|
|
Radian(0.0f)
|
|
);
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// Integrate position and commit the Origin to the world transform.
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
Point3D
|
|
frame_start_position = localOrigin.linearPosition;
|
|
|
|
localOrigin.linearPosition.AddScaled(
|
|
localOrigin.linearPosition,
|
|
worldLinearVelocity,
|
|
time_slice
|
|
);
|
|
localToWorld = localOrigin;
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// THE GROUND SNAP -- the probe half of the master performance's ground
|
|
// model (binary @4aa630-4aa6cc, decoded from raw asm; there is NO
|
|
// gravity anywhere in the mech). Place the collision volume, drop a
|
|
// probe from the volume's authored bottom, ask the zone's box tree for
|
|
// the surface under it, and place the origin ON that surface exactly.
|
|
// Walking up-slope rides the lift window; walking off a roof drops
|
|
// instantly; a probe MISS (h == -1) holds Y, so a runaway is
|
|
// structurally impossible.
|
|
//
|
|
// The COLLISION half (frame rejection, crush sentinel, crash clip) is a
|
|
// separate increment -- it hangs off ProcessCollisionList.
|
|
//
|
|
// Gated on the engine having built a collision volume for this model
|
|
// (Mover's ctor does when the resource carries one); logged once if
|
|
// absent so a silent no-op is visible.
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
if (
|
|
GetCollisionVolumeCount() > 0 &&
|
|
collisionVolume != NULL &&
|
|
collisionTemplate != NULL
|
|
)
|
|
{
|
|
MoveCollisionVolume();
|
|
|
|
BoundingBoxTreeNode
|
|
*ground_node = GetMoverCollisionRoot();
|
|
if (ground_node != NULL)
|
|
{
|
|
Point3D
|
|
probe = localOrigin.linearPosition;
|
|
probe.y += collisionTemplate->minY;
|
|
|
|
Scalar
|
|
height = -1.0f;
|
|
ground_node->FindBoundingBoxUnder(probe, &height);
|
|
|
|
if (height > 0.0001f)
|
|
{
|
|
Scalar
|
|
drop = height - collisionTemplate->minY;
|
|
localOrigin.linearPosition.y -= drop;
|
|
collisionVolume->minY -= drop;
|
|
collisionVolume->maxY -= drop;
|
|
localToWorld = localOrigin;
|
|
}
|
|
}
|
|
}
|
|
else if (getenv("BT_MECH_LOG"))
|
|
{
|
|
static int noVolumeOnce = 0;
|
|
if (!noVolumeOnce++)
|
|
{
|
|
DEBUG_STREAM << "[ground] mech has NO collision volume -- "
|
|
<< "snap inactive (model streams none?)" << endl << flush;
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// COLLISIONS -- the response half of the master performance's ground
|
|
// model (binary @4aa6cf-4aab0b). Walls block by FULL FRAME REJECTION,
|
|
// never by slide or climb: any blocking contact restores the
|
|
// start-of-frame position and zeroes the velocity. The crash itself
|
|
// HURTS -- the accumulated collision Damage is dispatched at OUR OWN
|
|
// mech, zone -1 (the cylinder lottery), which is what makes wall-
|
|
// grinding self-limiting. And a hard enough hit (|v|^2 > 40, the
|
|
// binary's @0x4ab184 threshold) staggers the mech: both gait channels
|
|
// bind the bump clip (slot 0x20) and recover to Standing at its end.
|
|
//
|
|
// Guarded on the collision assistant: the engine's GetCurrentCollisions
|
|
// walks it unchecked, and only the viewpoint mech gets one
|
|
// (StartCollisionAssistant, BTL4APP.CPP:409).
|
|
//
|
|
// STAGED deltas, named: the crushable-CulturalIcon sentinel (0.00123f
|
|
// -- the move stands, gyro crunch) needs the Mech::ProcessCollision
|
|
// override, a later increment; the gyro crunch feed itself is with it;
|
|
// and the collisionState audio push waits on the same override's
|
|
// contact accumulation.
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
if (
|
|
collisionAssistant != NULL &&
|
|
GetCollisionVolumeCount() > 0 &&
|
|
collisionVolume != NULL
|
|
)
|
|
{
|
|
Vector3D
|
|
impact_velocity = worldLinearVelocity;
|
|
Vector3D
|
|
post_snap_velocity = worldLinearVelocity;
|
|
Point3D
|
|
post_snap_position = localOrigin.linearPosition;
|
|
BoxedSolidCollisionList
|
|
*collisions = GetCurrentCollisions();
|
|
Damage
|
|
collision_damage;
|
|
|
|
collisionTemporaryState = 0;
|
|
|
|
if (collisions != NULL)
|
|
{
|
|
ProcessCollisionList(
|
|
collisions, time_slice, frame_start_position,
|
|
&collision_damage);
|
|
}
|
|
|
|
//
|
|
// 0->1 on a fresh contact fires the impact sound; SetState only
|
|
// fires on change, so a pressed-against contact plays once.
|
|
//
|
|
collisionState.SetState((unsigned)collisionTemporaryState);
|
|
|
|
if (collision_damage.damageAmount == 0.00123f)
|
|
{
|
|
//
|
|
// The crushable-icon WALK-THROUGH sentinel (ProcessCollision):
|
|
// the move STANDS -- restore the post-snap saves and undo the
|
|
// bounce. The prop took its crunch in the dispatch; the mech
|
|
// drives on through. (The gyro crunch feed -- torque 0.4 along
|
|
// the normalized force, upward impulse 0.2, binary
|
|
// @4aa81e/@4aa86c -- is STAGED with the gyro feel wave.)
|
|
//
|
|
worldLinearVelocity = post_snap_velocity;
|
|
localOrigin.linearPosition = post_snap_position;
|
|
localToWorld = localOrigin;
|
|
MoveCollisionVolume();
|
|
|
|
//
|
|
// The gyro CRUNCH (binary @4aa7ce-4aa871): torque 0.4 along the
|
|
// normalized contact force, upward impulse 0.2. The Normalize
|
|
// is unguarded in the binary too.
|
|
//
|
|
if (gyroSubsystem != NULL)
|
|
{
|
|
Vector3D
|
|
crunch_normal;
|
|
crunch_normal.Normalize(collision_damage.damageForce);
|
|
((Gyroscope *)gyroSubsystem)->ApplyDamageTorque(
|
|
crunch_normal.x, crunch_normal.y, crunch_normal.z, 0.4f);
|
|
((Gyroscope *)gyroSubsystem)->ApplyDamageImpulse(
|
|
0.0f, 1.0f, 0.0f, 0.2f);
|
|
}
|
|
|
|
collision_damage.damageAmount = 0.0f;
|
|
|
|
static int s_crunchLog = -1;
|
|
if (s_crunchLog < 0)
|
|
{
|
|
s_crunchLog = (getenv("BT_MECH_LOG") != NULL);
|
|
}
|
|
if (s_crunchLog)
|
|
{
|
|
DEBUG_STREAM << "[crash] CRUNCH (crushable) at ("
|
|
<< post_snap_position.x << ","
|
|
<< post_snap_position.z << ")" << endl << flush;
|
|
}
|
|
}
|
|
else if (collision_damage.damageAmount > 0.0f)
|
|
{
|
|
worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f);
|
|
localOrigin.linearPosition = frame_start_position;
|
|
localToWorld = localOrigin;
|
|
MoveCollisionVolume();
|
|
|
|
Entity::TakeDamageMessage
|
|
crash(
|
|
Entity::TakeDamageMessageID,
|
|
sizeof(Entity::TakeDamageMessage),
|
|
GetEntityID(),
|
|
-1,
|
|
collision_damage
|
|
);
|
|
Dispatch(&crash);
|
|
|
|
Scalar
|
|
impact_squared =
|
|
impact_velocity.x * impact_velocity.x +
|
|
impact_velocity.y * impact_velocity.y +
|
|
impact_velocity.z * impact_velocity.z;
|
|
if (
|
|
impact_squared > 40.0f &&
|
|
animationClips[0x20] != ResourceDescription::NullResourceID &&
|
|
legStateAlarm.GetLevel() != 0x20
|
|
)
|
|
{
|
|
SetLegAnimation(0x20);
|
|
SetBodyAnimation(0x20);
|
|
ForceUpdate(1);
|
|
ForceUpdate(0x20);
|
|
}
|
|
|
|
//
|
|
// Log the CONTACT EDGE only. This site once printed every
|
|
// blocked frame -- 5487 lines in one wall-grinding run, each
|
|
// formatted and pushed through the COM3 serial redirect, which
|
|
// is what made the build feel vastly slower than the shipped
|
|
// exe. The collisionState 0->1 edge is the same gate the audio
|
|
// uses.
|
|
//
|
|
static int s_mechLog = -1;
|
|
if (s_mechLog < 0)
|
|
{
|
|
s_mechLog = (getenv("BT_MECH_LOG") != NULL);
|
|
}
|
|
if (s_mechLog && collisionState.GetState() == 0)
|
|
{
|
|
DEBUG_STREAM << "[crash] BLOCK dmg="
|
|
<< collision_damage.damageAmount
|
|
<< " iv2=" << impact_squared
|
|
<< (impact_squared > 40.0f ? " KNOCKDOWN" : "")
|
|
<< endl << flush;
|
|
}
|
|
}
|
|
}
|
|
|
|
static int s_simLog = -1;
|
|
if (s_simLog < 0)
|
|
{
|
|
s_simLog = (getenv("BT_MECH_LOG") != NULL);
|
|
}
|
|
if (s_simLog)
|
|
{
|
|
static Scalar reportAccum = 0.0f;
|
|
reportAccum += time_slice;
|
|
if (reportAccum >= 1.0f)
|
|
{
|
|
reportAccum = 0.0f;
|
|
EulerAngles ypr;
|
|
ypr = localOrigin.angularPosition;
|
|
DEBUG_STREAM << "[sim] pos=("
|
|
<< localOrigin.linearPosition.x << ","
|
|
<< localOrigin.linearPosition.y << ","
|
|
<< localOrigin.linearPosition.z << ")"
|
|
<< " yaw=" << (Scalar)ypr.yaw
|
|
<< " spd=" << currentBodySpeed
|
|
<< " eyePitch=" << (Scalar)eyepointRotation.pitch
|
|
<< " eyeYaw=" << (Scalar)eyepointRotation.yaw
|
|
<< endl << flush;
|
|
}
|
|
}
|
|
|
|
Check_Fpu();
|
|
}
|