Under the documented rig (launch_pod.ps1 with the GL bridge up) the 'couldn't load object' count went to ZERO. .BGF loading was never a reconstruction problem, only a missing bridge. What blocks now is a series of authored AttributeWatchers: BTL4.RES binds them BY NAME and the engine Fails outright on any that does not resolve, so each is a name our subsystems must publish. Five rungs climbed, each run-verified: UnstablePercentage, SpeedEffect (Myomers table), AnimationState + CollisionState/CollisionNormal + ReduceButton (Mech), and the full Torso table (all six authentic names mapped onto existing members). The method that makes this cheap is banked: the shipped binary's string pool carries each class's attribute names CONTIGUOUSLY in ID ORDER after the class name, and in every case so far our member declarations sit in the same order -- which confirms the layout and lets ids be pinned rather than guessed. Intersecting those names with BTL4.RES gives the exact work list. A THIRD miswired SharedData found on the way: Myomers carried Subsystem's tables where it derives from PoweredSubsystem, the same defect as MissileLauncher (5.3.33). Worth a sweep across every subsystem. Staged member TYPES are provisional and documented as such: AttributeWatcherOf reads *(T*)attributePointer and the instantiation comes from the resource, which the string pool does not reveal. Nothing drives them yet, so settle the types with the models that write them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1375 lines
46 KiB
C++
1375 lines
46 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)
|
|
#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;
|
|
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
return joints->GetJoint(segment->GetJointIndex());
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// 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;
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// Accelerate the actual body speed toward the demand (bounded per frame by
|
|
// the mech's max acceleration).
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
{
|
|
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.
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
localOrigin.linearPosition.AddScaled(
|
|
localOrigin.linearPosition,
|
|
worldLinearVelocity,
|
|
time_slice
|
|
);
|
|
localToWorld = localOrigin;
|
|
|
|
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();
|
|
}
|