Files
TeslaRel410/restoration/source410/BT/MECH.CPP
T
CydandClaude Fable 5 3364b65ae8 BT410 Phase 5.3.1: Mech::Simulate motion core -- mech integrates motion per-frame
Installs Mech::Simulate as the mech's per-frame Performance (was DoNothingOnce).
Reconstructs the load-bearing spine of the 1995 Simulate (mech4.cpp @004ab430):
integrate the body velocity into localOrigin (linearPosition.AddScaled(..,
worldLinearVelocity, dt)) and commit the Origin to the world transform with the
engine idiom (localToWorld = localOrigin, per ENTITY.cpp:988 / MOVER.cpp:850).

Uses the NAMED engine base Mover fields (localOrigin/localToWorld/
worldLinearVelocity) -- BT411's MechBaseLayoutCheck proved the WinTesla decomp's
raw this+0x100/0x260 offsets actually stomp those base fields; the clean 1995
build addresses them by name.

- MECH.HPP: Mech Performance typedef + SetPerformance + Simulate() decl.
- MECH.CPP: SetPerformance(&Mech::Simulate) at ctor tail; Simulate body.

Verified headlessly (BT_MECH_LOG "[sim] mech pos=" 1Hz): BT_DRIVE="5,0,10"
advances the mech +5.0/s x, +10.0/s z (y fixed) = exactly the injected world
velocity; no BT_DRIVE -> worldLinearVelocity is 0, mech holds spawn pose. Zero
Fail/Exception either way. BT_DRIVE is a retained dev hook for headless motion.

Deferred (locomotion wave): gait-cycle self-propulsion feeding worldLinearVelocity
(IntegrateMotion->AdvanceBodyAnimation, steered by mapper throttle/turn), heading
integration, terrain-height drop, cockpit telemetry filters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 13:45:24 -05:00

424 lines
13 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
//
//#############################################################################
//#############################################################################
//
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);
{
for (int i = 0; i < 220; ++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;
}
//
// 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);
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;
}
//
//#############################################################################
// 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);
Vector3D velocity = worldLinearVelocity;
{
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)
{
velocity.x = dx;
velocity.y = dy;
velocity.z = dz;
}
}
}
//
// Integrate position (pos = pos + velocity*dt) and commit the Origin to the
// world transform.
//
localOrigin.linearPosition.AddScaled(
localOrigin.linearPosition,
velocity,
time_slice
);
localToWorld = localOrigin;
if (getenv("BT_MECH_LOG"))
{
static Scalar reportAccum = 0.0f;
reportAccum += time_slice;
if (reportAccum >= 1.0f)
{
reportAccum = 0.0f;
DEBUG_STREAM << "[sim] mech pos=("
<< localOrigin.linearPosition.x << ","
<< localOrigin.linearPosition.y << ","
<< localOrigin.linearPosition.z << ")"
<< endl << flush;
}
}
Check_Fpu();
}