The subsystem-family cockpit buttons now work through the authentic Receiver dispatch -> per-class handler-table path. The id space decodes cleanly: Receiver::NextMessageID == 3, so ToggleCooling = 3 on the HeatSink chain and the per-class id 4 is MoveValve on a Condenser / InjectCoolant on the Reservoir -- same number, different class, the binary's per-receiver-class convention. - HeatSink::ToggleCoolingMessageHandler (@004ad6f8, id 3): novice-locked, press-only; toggles coolantAvailable + coolantFlowScale together. - Condenser::MoveValveMessageHandler (@4ae464, id 4): novice-locked; cycles the valve 1->5->50->0->1 and calls Condenser::RecomputeValves (@0049f788): every condenser's coolantFlowScale = valve / sum-of-valves. The ctor now streams the AUTHENTIC flowScale=0; the Mech ctor seeds equal shares once at spawn -- the flowScale=1 interim is retired. - Reservoir::InjectCoolantMessageHandler (@4aee70, id 4): novice-locked; press arms the flush when the tank holds charge, release drops it. - MechSubsystem::NoviceLockout() (@4ac9c8): owner -> playerLink -> experience == novice; unlinked mechs read unlocked. - DEV harness BT_PRESS_VALVE / BT_PRESS_FLUSH: dispatch the REAL messages ~6s into the mission from Mech::Simulate. VERIFIED: spawn shares 6 x 0.166667; one MoveValve press -> Condenser1 valve=5 flow=0.5, others 0.1 (valve/sum exact); flush arms via the real button message; NOVICE locks both presses (valve lines stay spawn-only, zero flush). Zero Fail throughout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
913 lines
29 KiB
C++
913 lines
29 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
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
Derivation
|
|
Mech::ClassDerivations(
|
|
JointedMover::ClassDerivations,
|
|
"Mech"
|
|
);
|
|
|
|
Mech::SharedData
|
|
Mech::DefaultData(
|
|
Mech::ClassDerivations,
|
|
JointedMover::MessageHandlers,
|
|
JointedMover::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;
|
|
|
|
//
|
|
// 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 < 202; ++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:
|
|
made = new HeatSink(this, id, (HeatSink::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();
|
|
}
|
|
}
|
|
|
|
//
|
|
// 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()
|
|
{
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// 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);
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// 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();
|
|
}
|