The dispatcher (master perf @0x4aa505-0x4aa588, from the raw disasm -- the
function Ghidra never decompiled), re-hosted at the gait seam in Simulate:
From Standing, arm the trn clip when the mech is TURNING (the binary's
operand is |angularVelocity| > 1e-4; at a standstill the turn rate is
exactly walkingTurnRate, so the test is turnDemand * walkingTurnRate),
the speed demand sits in the FULL sub-walk band [0, standSpeed], the
model carries a trn clip (turnCapable, real since 5.3.103), 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 (the donor's rhythmic-gait-skip
finding). Each trn clip plays one figure and drops back to Standing via
the case-4 group, so a held turn re-arms clip after clip.
The debounce is the master perf's own shape: legResetLatch cleared at the
top of every frame (@0x4a9bff), set by the advancer's wind-down, honoured
here -- a walk that wound down THIS frame cannot re-enter as a turn until
the next.
LIVE (new pod_render_trn.conf: throttle 0, turn 0.5, exterior clips):
pos PINNED at (-641.043, 0, -243.722) across the whole run
yaw sweeping the full circle
spd oscillating +/-0.001347
That last number is the proof of authenticity: it is the trn clip's authored
root translation -- the foot shuffle. The mech turns by STEPPING, not by
having its yaw slid underneath it, and the stride-driven speed model shows
the steps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2089 lines
68 KiB
C++
2089 lines
68 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)
|
|
};
|
|
|
|
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;
|
|
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;
|
|
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'
|
|
)
|
|
{
|
|
//
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// 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.
|
|
//#############################################################################
|
|
//
|
|
//
|
|
//#############################################################################
|
|
// 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);
|
|
|
|
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 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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
//
|
|
// 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;
|
|
|
|
if (limping)
|
|
{
|
|
AdvanceLegAnimationGimp(time_slice);
|
|
}
|
|
else
|
|
{
|
|
AdvanceLegAnimation(time_slice);
|
|
}
|
|
|
|
//
|
|
// THE TURN-IN-PLACE DISPATCHER (master perf @0x4aa505-0x4aa588, decoded
|
|
// from raw disasm -- Ghidra never decompiled the function). 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_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();
|
|
collision_damage.damageAmount = 0.0f;
|
|
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
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);
|
|
}
|
|
|
|
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();
|
|
}
|