Files
TeslaRel410/restoration/source410/BT/MECH.CPP
T
CydandClaude Fable 5 4ace2571d9 BT410 5.3.99: walls are real -- frame rejection, crash self-damage, the knockdown stagger, and a pricing question left honestly open
The response half of the ground model (binary @4aa6cf-4aab0b), after the
5.3.98 snap:

  WALLS BLOCK BY FULL FRAME REJECTION, never slide or climb: any blocking
  contact restores the start-of-frame position and zeroes the velocity.

  THE CRASH HURTS: the accumulated collision Damage is dispatched at our own
  mech, zone -1 -- through the same cylinder lottery as enemy fire -- which
  is what makes wall-grinding self-limiting in the original.

  A HARD HIT STAGGERS: past the binary's |v|^2 > 40 threshold (@0x4ab184),
  both gait channels bind the bump clip (slot 0x20, the reason the clip
  array outgrows the name table) and recover to Standing at its end.  Both
  channels together, per the donor's foot-slip finding: staggering only the
  leg channel desyncs the two cycles permanently.

LIVE, one arena run, no fault -- and the run turned into an unplanned
full-system exercise: 8 wall contacts (three at knockdown force, iv2
745-932; one low-speed bump correctly blocked without stagger), and the
self-damage cascaded through vital zones to MECH KILL, pilot notification,
respawn HEAL-and-place, four times over.  The death and respawn machinery
all held.

OPEN, stated rather than tuned away: THE PRICE IS TOO HOT.  Wall crashes
land 14k-24k damage points against a game where a PPC is ~12 and an upper
torso carries ~124 -- a hard crash one-shots vital zones through the
lottery.  BT411's economy audit hit the identical symptom ("tapped a wall
and died instantly") and traced part of it to compounded StaticBounce
reflections across multi-solid frames.  The fix lives in the
Mech::ProcessCollision override (with the crushable-icon sentinel and the
gyro crunch), which is the NEXT increment -- the pricing stays as the
engine computes it until that override is reconstructed from the binary,
because guessing a divisor would be tuning, not archaeology.  The [crash]
log line prints amount + iv2 under BT_MECH_LOG as the audit trail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 13:55:47 -05:00

1707 lines
57 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
#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)
};
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),
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;
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;
{
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;
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;
}
}
//
//-----------------------------------------------------------------------
// 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;
}
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'
)
{
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;
}
}
}
//
// Seed the condenser flow shares: every valve spawns at 1, so the initial
// recompute yields equal shares across the bank (a MoveValve press
// re-shares them).
//
Condenser::RecomputeValves(this);
//
// 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()
{
}
//
//#############################################################################
// 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.
//#############################################################################
//
void
Mech::TakeDamageMessageHandler(TakeDamageMessage *message)
{
Check(this);
Check(message);
if (gyroSubsystem != NULL && getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[gyro] hit feed staged (type="
<< (int)message->damageData.damageType
<< " amt=" << message->damageData.damageAmount << ")"
<< endl << flush;
}
lastInflictingID = message->inflictingEntity;
lastInflictingDamage = message->damageData.damageAmount;
//
// 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)
{
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;
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);
}
}
}
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);
//
//-----------------------------------------------------------------------
// 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 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.
//
//
// STAGED (the master performance's job, mech4 @004a9b5c -- never
// decompiled): promote the damage model's limp signal (statusAlarm level
// 3/4, MECHDMG's leg-half write to mech+0x2c) into the movement mode the
// gait machinery keys on. The binary's damage code READS mode 3/4 to
// decide "already limping", so the promotion demonstrably exists; the
// master perf is the only unaccounted-for writer. Guarded so a death
// (mode 5+) is never overwritten.
//
{
unsigned status = statusAlarm.GetLevel();
int mode = MovementMode();
if ((status == 3 || status == 4) && mode < 3)
{
SetSimulationState(status);
}
}
int limping;
{
int mode = MovementMode();
limping = (mode == 3 || mode == 4) && hasGimpClips;
}
if (limping)
{
AdvanceLegAnimationGimp(time_slice);
}
else
{
AdvanceLegAnimation(time_slice);
}
{
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_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;
BoxedSolidCollisionList
*collisions = GetCurrentCollisions();
Damage
collision_damage;
if (collisions != NULL)
{
ProcessCollisionList(
collisions, time_slice, frame_start_position,
&collision_damage);
}
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);
}
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[crash] BLOCK dmg="
<< collision_damage.damageAmount
<< " iv2=" << impact_squared
<< (impact_squared > 40.0f ? " KNOCKDOWN" : "")
<< endl << flush;
}
}
}
if (getenv("BT_MECH_LOG"))
{
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();
}