diff --git a/restoration/source410/BT/DMGTABLE.CPP b/restoration/source410/BT/DMGTABLE.CPP new file mode 100644 index 00000000..eef4a923 --- /dev/null +++ b/restoration/source410/BT/DMGTABLE.CPP @@ -0,0 +1,333 @@ +//===========================================================================// +// File: dmgtable.cpp // +// Project: BattleTech Brick: Mech damage // +// Contents: The cylinder hit-location table (unaimed damage -> zone) // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// +// +// RECONSTRUCTED from the binary via the BT411 reversal (byte-verified +// against the real type-29 streams, 18 tables). STREAM FORMAT: +// +// Table { i32 rowCount; Row[rowCount] } +// Row { i32 rotateWithTorso; i32 cellCount; Cell[cellCount] } +// Cell { i32 nameLen; char name[nameLen]; u8 0; i32 entryCount; +// Entry[entryCount] } +// Entry { f32 cumulativeThreshold; i32 zoneIndex } +// +// Real sample (ava1, 7 rows x 8 cells): rows 0-2 fixed to the chassis +// (feet/legs), rows 3-6 rotate with the torso; a foot cell = +// {0.5->16, 0.8->10, 1.0->5}. +// + +#include +#pragma hdrstop + +#if !defined(DMGTABLE_HPP) +# include +#endif + +#if !defined(MECH_HPP) +# include +#endif + +#if !defined(RANDOM_HPP) +# include +#endif + +#include + +static const Scalar kTwoPi = 6.2831855f; // _DAT_0049e810 +static const Scalar kDegenerateEps = 1.0e-4f; // _DAT_0049ed04 + +// +// STAGED: the binary reads the mech's collision-cylinder height +// (mech+0x2ec -> +0xc); the collision volume isn't reconstructed yet, so a +// fixed standing height maps impacts onto the rows. (bhk1 torso mounts sit +// at y~4; 10u spans feet->cockpit plausibly.) +// +static const Scalar kStagedMechHeight = 10.0f; + +// +//############################################################################# +// DamageZonePercentTable -- one angular cell (binary ctor @0049deb0; the +// entry fill @0049e5e4). The cell NAME string ([i32 len][chars][NUL]) is +// consumed and discarded, exactly as the binary does. +//############################################################################# +// +DamageZonePercentTable::DamageZonePercentTable(MemoryStream *stream) +{ + Check(stream); + + int name_length = 0; + *stream >> name_length; + if (name_length > 0) + { + stream->AdvancePointer(name_length); + } + stream->AdvancePointer(1); // the NUL + + *stream >> entryCount; + thresholds = NULL; + zoneIndices = NULL; + if (entryCount > 0) + { + thresholds = new Scalar[entryCount]; + Register_Pointer(thresholds); + zoneIndices = new int[entryCount]; + Register_Pointer(zoneIndices); + for (int ee = 0; ee < entryCount; ++ee) + { + *stream >> thresholds[ee]; + *stream >> zoneIndices[ee]; + } + } +} + +DamageZonePercentTable::~DamageZonePercentTable() +{ + if (thresholds != NULL) + { + Unregister_Pointer(thresholds); + delete [] thresholds; + } + if (zoneIndices != NULL) + { + Unregister_Pointer(zoneIndices); + delete [] zoneIndices; + } +} + +// +//############################################################################# +// SelectZone (binary @0049de14): uniform roll, first cumulative threshold +// ABOVE the roll wins (entries ascend); the last entry backstops. +//############################################################################# +// +int + DamageZonePercentTable::SelectZone() +{ + Check(this); + + if (entryCount <= 0) + { + return -1; + } + Scalar roll = (Scalar)Random; + for (int ee = 0; ee < entryCount; ++ee) + { + if (thresholds[ee] > roll) + { + return zoneIndices[ee]; + } + } + return zoneIndices[entryCount - 1]; +} + +// +//############################################################################# +// PieSlice -- one height row (binary ctor @0049e740). +//############################################################################# +// +PieSlice::PieSlice(Mech *mech, MemoryStream *stream) +{ + Check(mech); + Check(stream); + + owner = mech; + *stream >> rotateWithTorso; + *stream >> cellCount; + cells = NULL; + if (cellCount > 0) + { + cells = new DamageZonePercentTable *[cellCount]; + Register_Pointer(cells); + for (int cc = 0; cc < cellCount; ++cc) + { + cells[cc] = new DamageZonePercentTable(stream); + Register_Object(cells[cc]); + } + } +} + +PieSlice::~PieSlice() +{ + if (cells != NULL) + { + for (int cc = 0; cc < cellCount; ++cc) + { + Unregister_Object(cells[cc]); + delete cells[cc]; + } + Unregister_Pointer(cells); + delete [] cells; + } +} + +// +//############################################################################# +// SelectSlice (binary @0049e678): rotate-with-torso rows add the LIVE torso +// twist to the impact angle (an upper-body ring follows the twist; legs +// stay chassis-fixed), wrap to [0,2pi), pick the cell by angular fraction. +//############################################################################# +// +DamageZonePercentTable* + PieSlice::SelectSlice(Scalar angle) +{ + Check(this); + + if (cellCount <= 0) + { + return NULL; + } + + Scalar a = angle; + if (rotateWithTorso) + { + a += owner->CurrentTorsoTwist(); + while (a >= kTwoPi) + { + a -= kTwoPi; + } + while (a < 0.0f) + { + a += kTwoPi; + } + } + + int index = (int)((Scalar)cellCount * a / kTwoPi); + if (index < 0) + { + index = 0; + } + if (index >= cellCount) + { + index = cellCount - 1; + } + return cells[index]; +} + +// +//############################################################################# +// DamageLookupTable (binary ctor @0049ea48). +//############################################################################# +// +DamageLookupTable::DamageLookupTable(Mech *mech, MemoryStream *stream) +{ + Check(mech); + Check(stream); + + owner = mech; + *stream >> rowCount; + rows = NULL; + if (rowCount > 0) + { + rows = new PieSlice *[rowCount]; + Register_Pointer(rows); + for (int rr = 0; rr < rowCount; ++rr) + { + rows[rr] = new PieSlice(mech, stream); + Register_Object(rows[rr]); + } + } +} + +DamageLookupTable::~DamageLookupTable() +{ + if (rows != NULL) + { + for (int rr = 0; rr < rowCount; ++rr) + { + Unregister_Object(rows[rr]); + delete rows[rr]; + } + Unregister_Pointer(rows); + delete [] rows; + } +} + +// +//############################################################################# +// ResolveHit (binary @0049eb54 + the glue @0049ed0c): world impact -> +// mech-local; clamp local height onto the cylinder -> row; impact angle = +// atan2(z, x) wrapped [0,2pi) (a degenerate on-axis hit rolls a random +// angle); row/angle -> cell -> the distribution roll -> a hull zone. +//############################################################################# +// +int + DamageLookupTable::ResolveHit(const Point3D &world_impact) +{ + Check(this); + + if (rowCount <= 0) + { + return -1; + } + + // + // World -> mech-local. + // + LinearMatrix to_world; + to_world = owner->localToWorld; + LinearMatrix to_local; + to_local.Invert(to_world); + Point3D local; + local.Multiply(world_impact, to_local); + + // + // Height -> row. + // + Scalar height = kStagedMechHeight; + Scalar y = local.y; + if (y < 0.0f) + { + y = 0.0f; + } + if (y > height) + { + y = height; + } + int row = (int)((Scalar)rowCount * y / height); + if (row < 0) + { + row = 0; + } + if (row >= rowCount) + { + row = rowCount - 1; + } + + // + // Impact angle. + // + Scalar angle; + Scalar ax = (local.x < 0.0f) ? -local.x : local.x; + Scalar az = (local.z < 0.0f) ? -local.z : local.z; + if (ax > kDegenerateEps || az > kDegenerateEps) + { + angle = (Scalar)atan2((double)local.z, (double)local.x); + if (angle < 0.0f) + { + angle += kTwoPi; + } + } + else + { + angle = (Scalar)Random * kTwoPi; + } + + DamageZonePercentTable *cell = rows[row]->SelectSlice(angle); + int zone = (cell != NULL) ? cell->SelectZone() : -1; + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[cyl] impact local=(" << local.x + << "," << local.y << "," << local.z + << ") row=" << row << "/" << rowCount + << " angle=" << angle + << " -> zone " << zone << endl << flush; + } + return zone; +} diff --git a/restoration/source410/BT/DMGTABLE.HPP b/restoration/source410/BT/DMGTABLE.HPP new file mode 100644 index 00000000..ca921d4a --- /dev/null +++ b/restoration/source410/BT/DMGTABLE.HPP @@ -0,0 +1,105 @@ +//===========================================================================// +// File: dmgtable.hpp // +// Project: BattleTech // +// Contents: The cylinder hit-location table (unaimed damage -> zone) // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// +// +// STAGED header -- no dmgtable.hpp survives; the interface is reconstructed +// from the binary (BT411, byte-verified vs the real type-29 .RES streams): +// a passive nested height x angle grid of three container classes, +// +// DamageLookupTable (binary ctor @0049ea48, vtable @0050bd84, 0x2c) +// -> PieSlice ROWS by height (@0049e740, @0050bd90, 0x30) +// -> DamageZonePercentTable CELLS by angle (@0049deb0, @0050bd9c, 0x2c) +// -> the cumulative hit-distribution (roll -> zone index). +// +// Class names follow the BT411 reconstruction (no 1995 names survive). +// The binary keys rows/cells into refcounted keyed lists; direct-indexed +// arrays are equivalent (the float key (i+1)*2pi/count === cells[i]). +// + +#if !defined(DMGTABLE_HPP) +# define DMGTABLE_HPP + +//##################### Forward Class Declarations ####################### + class Mech; + +//########################################################################### +//##################### DamageZonePercentTable ########################## +//########################################################################### + + // + // One angular CELL: the cumulative hit-distribution. Entries ascend; + // SelectZone (binary @0049de14) rolls uniform [0,1) and returns the + // first entry whose threshold exceeds the roll -- the classic + // BattleTech dice hit-scatter. + // + class DamageZonePercentTable + { + public: + DamageZonePercentTable(MemoryStream *stream); + ~DamageZonePercentTable(); + + int + SelectZone(); + + int entryCount; + Scalar *thresholds; // cumulative, ascending + int *zoneIndices; // -> Entity::damageZones[] + }; + +//########################################################################### +//############################ PieSlice ################################# +//########################################################################### + + // + // One height ROW: the ring of angular cells. rotateWithTorso rows + // (upper body) add the live torso twist to the impact angle before the + // cell pick (binary @0049e678). + // + class PieSlice + { + public: + PieSlice(Mech *mech, MemoryStream *stream); + ~PieSlice(); + + DamageZonePercentTable* + SelectSlice(Scalar angle); + + Mech *owner; + int rotateWithTorso; + int cellCount; + DamageZonePercentTable + **cells; + }; + +//########################################################################### +//######################## DamageLookupTable ############################ +//########################################################################### + + // + // The table: rows stacked by height over the mech's cylinder. + // ResolveHit (binary @0049eb54 + glue @0049ed0c) maps a WORLD impact + // point -> mech-local -> (height row, impact angle) -> cell -> the + // distribution roll -> a hull zone index. + // + class DamageLookupTable + { + public: + DamageLookupTable(Mech *mech, MemoryStream *stream); + ~DamageLookupTable(); + + int + ResolveHit(const Point3D &world_impact); + + Mech *owner; + int rowCount; + PieSlice + **rows; + }; + +#endif diff --git a/restoration/source410/BT/DMGTABLE.NOTES.md b/restoration/source410/BT/DMGTABLE.NOTES.md new file mode 100644 index 00000000..70d0d7b0 --- /dev/null +++ b/restoration/source410/BT/DMGTABLE.NOTES.md @@ -0,0 +1,32 @@ +# DMGTABLE.CPP — the cylinder hit-location table + +**Status: RECONSTRUCTED (5.3.23) — loads the authentic type-29 streams and +resolves unaimed hits live (fight-verified).** + +No 1995 header survives; class names (DamageLookupTable / PieSlice / +DamageZonePercentTable) follow the BT411 reversal, which byte-verified the +stream against the real .RES (18 tables). Binary anatomy: TABLE ctor +@0049ea48 (vtable @0050bd84) → ROW @0049e740 (@0050bd90) → CELL @0049deb0 +(@0050bd9c); lookup @0049eb54 (height→row, atan2 angle) + @0049e678 +(rotate-with-torso, angle→cell) + @0049de14 (the cumulative-distribution +roll); glue @0049ed0c. The binary's refcounted keyed lists are replaced by +direct-indexed arrays (the float key (i+1)·2π/count ≡ cells[i]). + +Stream (type 29, found by the mech's DamageZoneStream member NAME): +`Table{i32 rows; Row[]} Row{i32 rotateWithTorso; i32 cells; Cell[]} +Cell{i32 nameLen; chars; NUL; i32 entries; {f32 cumThreshold; i32 zone}[]}`. + +Wiring: the Mech ctor Pass-3 tail loads it (mech+0x444, kept locked); +Mech::TakeDamageMessageHandler resolves `invalidDamageZone` hits through it +before chaining the base. The MISSILE fuse is the authentic unaimed +producer: zone −1 + the round's world position as impactPoint (the +ENTITY3.HPP warning: "damage zones are only valid via reticle based +weapons"); victims without a table keep the interim random pick. + +Verified (bhk1, 7 rows): flat-flying SRMs strike low → row 0 → feet/leg +zones, side-correct by impact angle (+x hits right, −x left), identical +geometry spread across the cell's distribution by the roll. Zero faults. + +STAGED: mech cylinder height = 10.0 constant (binary reads the collision +cylinder mech+0x2ec→+0xc — not reconstructed); torso twist feeds +rotate-with-torso rows via Mech::CurrentTorsoTwist (Torso::CurrentTwist). diff --git a/restoration/source410/BT/MECH.CPP b/restoration/source410/BT/MECH.CPP index e661575d..03ba7034 100644 --- a/restoration/source410/BT/MECH.CPP +++ b/restoration/source410/BT/MECH.CPP @@ -1,1077 +1,1153 @@ -//===========================================================================// -// 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 -#pragma hdrstop - -#if !defined(MECH_HPP) -# include -#endif - -#if !defined(APP_HPP) -# include -#endif -#if !defined(MEMSTRM_HPP) -# include -#endif -#if !defined(RESOURCE_HPP) -# include -#endif - -// The subsystem roster the segment walk instantiates. -#include // HeatableSubsystem, HeatSink, HeatWatcher, Condenser -#include // PoweredSubsystem, PowerWatcher, Generator -#include // Reservoir -#include // Sensor -#include // Gyroscope -#include // Torso -#include // Myomers -#include // HUD -#include // Searchlight -#include // ThermalSight -#include // MechTech -#include // SubsystemMessageManager -#include // MechWeapon -#include // Emitter -#include // PPC -#include // GaussRifle -#include // ProjectileWeapon -#include // MissileLauncher -#include // AmmoBin -#include // MechControlsMapper -- the drive reads its demands -#include // Joint / JointSubsystem -- ResolveJoint -#include // EntitySegment -- the skeleton segment table -#include // Mech::DamageZone -- the hull zone fill (Pass 3) - -// -//############################################################################# -//############################################################################# -// -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 - ); - -Mech::SharedData - Mech::DefaultData( - Mech::ClassDerivations, - Mech::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; - targetEntity = NULL; - lastInflictingID = EntityID::Null; - - // - // 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 < 201; ++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; - } - } - - // - // 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; - - 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; -} - -// -//############################################################################# -// 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 press( - Condenser::MoveValveMessageID, - sizeof(ReceiverDataMessageOf), - 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 press( - Reservoir::InjectCoolantMessageID, - sizeof(ReceiverDataMessageOf), - 1 - ); - sub->Dispatch(&press); - break; - } - } - } - // - // BT_PRESS_GEN=1..4: SelectGenerator 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 press( - PoweredSubsystem:: - SelectGeneratorAMessageID - + (n - 1), - sizeof(ReceiverDataMessageOf), - 1 - ); - sub->Dispatch(&press); - } - } - if (press_seek != NULL) - { - ReceiverDataMessageOf press( - Emitter::ToggleSeekVoltageMessageID, - sizeof(ReceiverDataMessageOf), - 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(); -} +//===========================================================================// +// 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 +#pragma hdrstop + +#if !defined(MECH_HPP) +# include +#endif + +#if !defined(APP_HPP) +# include +#endif +#if !defined(MEMSTRM_HPP) +# include +#endif +#if !defined(RESOURCE_HPP) +# include +#endif + +// The subsystem roster the segment walk instantiates. +#include // HeatableSubsystem, HeatSink, HeatWatcher, Condenser +#include // PoweredSubsystem, PowerWatcher, Generator +#include // Reservoir +#include // Sensor +#include // Gyroscope +#include // Torso +#include // Myomers +#include // HUD +#include // Searchlight +#include // ThermalSight +#include // MechTech +#include // SubsystemMessageManager +#include // MechWeapon +#include // Emitter +#include // PPC +#include // GaussRifle +#include // ProjectileWeapon +#include // MissileLauncher +#include // AmmoBin +#include // MechControlsMapper -- the drive reads its demands +#include // Joint / JointSubsystem -- ResolveJoint +#include // EntitySegment -- the skeleton segment table +#include // Mech::DamageZone -- the hull zone fill (Pass 3) +#include // DamageLookupTable -- the cylinder hit table + +// +//############################################################################# +//############################################################################# +// +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 + ); + +Mech::SharedData + Mech::DefaultData( + Mech::ClassDerivations, + Mech::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; + targetEntity = NULL; + lastInflictingID = EntityID::Null; + damageLookupTable = NULL; + + // + // 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; + + // + // 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; +} + +// +//############################################################################# +// 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); + + // + //----------------------------------------------------------------------- + // 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 press( + Condenser::MoveValveMessageID, + sizeof(ReceiverDataMessageOf), + 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 press( + Reservoir::InjectCoolantMessageID, + sizeof(ReceiverDataMessageOf), + 1 + ); + sub->Dispatch(&press); + break; + } + } + } + // + // BT_PRESS_GEN=1..4: SelectGenerator 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 press( + PoweredSubsystem:: + SelectGeneratorAMessageID + + (n - 1), + sizeof(ReceiverDataMessageOf), + 1 + ); + sub->Dispatch(&press); + } + } + if (press_seek != NULL) + { + ReceiverDataMessageOf press( + Emitter::ToggleSeekVoltageMessageID, + sizeof(ReceiverDataMessageOf), + 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(); +} diff --git a/restoration/source410/BT/MECH.HPP b/restoration/source410/BT/MECH.HPP index c828ca0e..20473b7a 100644 --- a/restoration/source410/BT/MECH.HPP +++ b/restoration/source410/BT/MECH.HPP @@ -1,455 +1,468 @@ -//===========================================================================// -// File: mech.hpp // -// 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 // -//===========================================================================// - -#if !defined(MECH_HPP) -# define MECH_HPP - -# if !defined(JMOVER_HPP) -# include -# endif - -# if !defined(RETICLE_HPP) -# include -# endif - -# if !defined(ALARM_HPP) -# include -# endif - -# if !defined(STATE_HPP) -# include -# endif - -# if !defined(CHAIN_HPP) -# include -# endif - -# if !defined(AVERAGE_HPP) -# include -# endif - -# if !defined(CSTR_HPP) -# include -# endif - -# if !defined(SEQCTL_HPP) -# include -# endif - -# if !defined(NAMEFILT_HPP) -# include -# endif - -# if !defined(ROTATION_HPP) -# include -# endif - -//##################### Forward Class Declarations ####################### - class Mech; - class Subsystem; - class SubsystemMessageManager; - class ControlsMapping; - class PlatformTool; - class MechControlsMapper; - class Mech__DamageZone; - class Joint; - -//########################################################################### -//######################### Mech Model Resource ######################### -//########################################################################### - - struct Mech__ModelResource: - public JointedMover::ModelResource - { - char animationPrefix[4]; - Scalar maxAcceleration; - Scalar superStopAcceleration; - Scalar throttleAdjustment; - Scalar lookLeftAngle; - Scalar lookRightAngle; - Scalar lookFrontAngle; - Scalar lookBackAngle; - Scalar walkingTurnRate; - Scalar runningTurnRate; - Scalar reticleX; - Scalar reticleY; - Scalar relativeMechValue; - int deathEffectResourceID; - Scalar deathSplashDamage; - Scalar deathSplashRadius; - Scalar maxUnstableAcceleration; - Scalar unstableAccelerationEffect; - Scalar unstableGunTheEngineEffect; - Scalar unstableSuperStopEffect; - Scalar unstableHighVelocityEffect; - Scalar unstableStopedTurnEffect; - Scalar updatePositionDiffrence; - Scalar updateTurnVelocityDiffrence; - Scalar updateTurnDegreeDiffrence; - Scalar timeDelay; - Vector3D cameraOffset; - char shadowJointName[20]; - }; - -//########################################################################### -//########################## Mech Make Message ########################## -//########################################################################### - - class Mech__MakeMessage: - public JointedMover::MakeMessage - { - public: - char resourceNameA[20]; - char resourceNameB[20]; - char resourceNameC[20]; - - Mech__MakeMessage( - Receiver::MessageID message_ID, - size_t length, - const EntityID &entity_ID, - Entity::ClassID class_ID, - const EntityID &owner_ID, - ResourceDescription::ResourceID resource_ID, - LWord instance_flags, - const Origin &origin, - const Motion &velocity, - const Motion &acceleration, - const char *badge, - const char *color, - const char *patch - ): - JointedMover::MakeMessage( - message_ID, length, entity_ID, class_ID, owner_ID, - resource_ID, instance_flags, origin, velocity, acceleration - ) - { - Str_Copy(resourceNameA, badge, sizeof(resourceNameA)); - Str_Copy(resourceNameB, color, sizeof(resourceNameB)); - Str_Copy(resourceNameC, patch, sizeof(resourceNameC)); - } - }; - -//########################################################################### -//############################## Mech ############################### -//########################################################################### - - class Mech: - public JointedMover - { - // - // The per-zone damage code walks the roster / segment table / alarms - // directly (the 1995 arrangement, mirrored by the reconstruction). - // - friend class Mech__DamageZone; - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Shared Data Support - // - public: - static Derivation ClassDerivations; - static SharedData DefaultData; - - static const HandlerEntry - MessageHandlerEntries[]; - static MessageHandlerSet - MessageHandlers; - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Test Class Support - // - public: - static Logical - TestClass(Mech &); - Logical - TestInstance() const; - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Construction and Destruction - // - public: - typedef Mech__ModelResource ModelResource; - typedef Mech__MakeMessage MakeMessage; - typedef Mech__DamageZone DamageZone; - - static Mech* - Make(MakeMessage *creation_message); - - Mech( - MakeMessage *creation_message, - SharedData &shared_data = DefaultData - ); - ~Mech(); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // App interface - // - public: - void - SetMappingSubsystem(Subsystem *mapper); - - // - // Resolve a skeleton joint by name (the shared resolver the subsystems - // use to bind their animated joints -- Torso twist, etc.): - // GetSegment(name) -> segment jointIndex -> JointSubsystem::GetJoint. - // - Joint* - ResolveJoint(const char *joint_name); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Per-frame simulation (the mech's body Performance; installed by the ctor - // and dispatched each frame from Simulation::PerformAndWatch). - // - public: - typedef void - (Mech::*Performance)(Scalar time_slice); - void - SetPerformance(Performance performance) - { - Check(this); - activePerformance = (Simulation::Performance)performance; - } - - void - Simulate(Scalar time_slice); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Resource creation - // - public: - static void - CreateMakeMessage( - MakeMessage *creation_message, - NotationFile *model_file, - const ResourceDirectories *directories - ); - - static ResourceDescription::ResourceID - CreateModelResource( - ResourceFile *resource_file, - const char *model_name, - NotationFile *model_file, - const ResourceDirectories *directories, - ModelResource *model = 0 - ); - - static ResourceDescription::ResourceID - CreateSubsystemStream( - ResourceFile *resource_file, - const char *model_name, - NotationFile *model_file, - const ResourceDirectories *directories - ); - - static ResourceDescription::ResourceID - CreateDamageZoneStream( - ResourceFile *resource_file, - const char *model_name, - NotationFile *model_file, - const ResourceDirectories *directories - ); - - static ResourceDescription::ResourceID - CreateSkeletonStream( - ResourceFile *resource_file, - const char *model_name, - NotationFile *model_file, - const ResourceDirectories *directories - ); - - static ResourceDescription::ResourceID - CreateExplosionTableStream( - ResourceFile *resource_file, - const char *model_name, - NotationFile *model_file, - const ResourceDirectories *directories - ); - - typedef Logical (*FindNameFunction)( - const char *control_name, ControlsMapping *mapping); - - static ResourceDescription::ResourceID - CreateControlMappingStream( - const char *mapping_name, - NotationFile *mapping_file, - FindNameFunction find_name, - ResourceFile *resource_file, - const char *model_name, - NotationFile *model_file, - const ResourceDirectories *directories, - PlatformTool *current_tool - ); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Subsystem roster accessors (the roster itself -- subsystemArray / - // subsystemCount -- lives in the base Entity) - // - public: - Subsystem* - GetGyroSubsystem() { Check(this); return gyroSubsystem; } - Subsystem* - GetTorsoSubsystem() { Check(this); return sinkSourceSubsystem; } - Subsystem* - GetHudSubsystem() { Check(this); return hudSubsystem; } - Subsystem* - GetSensorSubsystem() { Check(this); return sensorSubsystem; } - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Locomotion parameters (read by MechControlsMapper::InterpretControls to - // shape the demands, and by the drive in Simulate). reverseStrideLength is - // the top/run cycle speed the throttle scales (the naming is the 1995 - // field's; LoadLocomotionClips measures it from the run clips), walkStride - // Length the walk speed. Turn rates are radians/sec. - // - public: - Scalar GetReverseStrideLength() const { Check(this); return reverseStrideLength; } - Scalar GetWalkStrideLength() const { Check(this); return walkStrideLength; } - Scalar GetForwardThrottleScale() const { Check(this); return forwardThrottleScale; } - void SetForwardThrottleScale(Scalar s){ Check(this); forwardThrottleScale = s; } - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Eyepoint / aim-ray composition. The pilot's torso-elevation aim (stick - // pitch) does NOT tilt any skeleton joint on this mech family -- it pitches - // the COCKPIT EYE and the weapon boresight (pixel-calibrated in the BT411 - // reverse-engineering: "pitch does not work" turned out to mean nothing - // consumed Torso's currentElevation, not that a joint was un-animated). - // Composed each frame in Simulate; DPLEyeRenderable / the aim ray read it. - // - public: - const EulerAngles& - GetEyepointRotation() const { Check(this); return eyepointRotation; } - - // - // The current target (the binary's mech target slot @0x388): read by - // the weapons' HasActiveTarget / UpdateTargeting and the fire path. - // The authentic writer is the reticle targeting step (mech4, the - // render wave); the dev harness sets it directly. - // - Entity* - GetTargetEntity() const { Check(this); return targetEntity; } - void - SetTargetEntity(Entity *target) { Check(this); targetEntity = target; } - - // - // Damage entry (overrides the Entity handler): latches the attacker - // (mech+0x43c, read by the zone LOD router) and feeds the gyro - // cockpit bounce before chaining to the base zone routing. The - // cylinder unaimed-hit resolver (type-0x1d table) is a later wave. - // - void - TakeDamageMessageHandler(TakeDamageMessage *message); - - // - // Damage-side death flag: the body graphic alarm (statusAlarm, - // binary @0x714) at level >= 9 = the death/fall state. - // - Logical - IsMechDestroyed() { Check(this); return statusAlarm.GetLevel() >= 9; } - - // - // Look-state commit (called by the controls mapper on a look-button - // state change): re-aim the eyepoint from the model's authored look - // angles. look_state = MechControlsMapper::LookState. - // - void - CommitLookState(int look_state); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Local Data - // - protected: - // - // Cached subsystem back-pointers (also held in the base roster). - // - Subsystem *sensorSubsystem; - Subsystem *gyroSubsystem; - Subsystem *sinkSourceSubsystem; // the real Torso - Subsystem *hudSubsystem; - SubsystemMessageManager *messageManager; - int weaponCount; - - // - // Capability sub-rosters (Socket views onto the subsystem roster; - // populated per-frame -- phase 5). - // - ChainOf controllableSubsystems; - ChainOf watchedSubsystems; - ChainOf heatableSubsystems; - ChainOf weaponRoster; - ChainOf damageableSubsystems; - - // - // Embedded status / animation / naming state. - // - NameFilter mechNameFilter; - AlarmIndicator masterAlarm; - AlarmIndicator heatAlarm; - AlarmIndicator stabilityAlarm; - AlarmIndicator statusAlarm; - Reticle targetReticle; - StateIndicator animationState; - StateIndicator replicantAnimationState; - StateIndicator collisionState; - SequenceController legAnimation; - SequenceController bodyAnimation; - AverageOf telemetryFilter[5]; - CString resourceNameA; - CString resourceNameB; - CString resourceNameC; - - // - // Locomotion state (Phase 5.3). Turn rates in rad/s, speeds/strides in - // world-units/s. reverseStrideLength = top (run) speed; walkStrideLength - // = walk speed; reverseSpeedMax = the low-speed turn-rate gate; forward - // ThrottleScale multiplies the forward demand; bodyTargetSpeed = the - // demanded speed; currentBodySpeed = the accel-tracked actual speed. - // - Scalar walkingTurnRate; - Scalar runningTurnRate; - Scalar reverseStrideLength; - Scalar walkStrideLength; - Scalar reverseSpeedMax; - Scalar forwardThrottleScale; - Scalar maxBodyAcceleration; - Scalar bodyTargetSpeed; - Scalar currentBodySpeed; - - // - // Composed each frame in Simulate: the look-state eye component - // (lookPitch/lookYaw, set by CommitLookState from the authored look - // angles) plus the Torso elevation. - // - EulerAngles eyepointRotation; - Scalar lookPitch; - Scalar lookYaw; - Entity *targetEntity; - EntityID lastInflictingID; // binary mech+0x43c -- last - // attacker (zone LOD router) - - // - // The model's authored look-view angles (rad; deg in the resource). - // - Scalar lookLeftAngle; - Scalar lookRightAngle; - Scalar lookFrontAngle; - Scalar lookBackAngle; - - // - // Remaining per-frame targeting / animation state, advanced by the - // mech2/mech3/mech4 simulation. Reserved until that path is - // reconstructed with named fields. - // - int reservedState[199]; - }; - -#endif +//===========================================================================// +// File: mech.hpp // +// 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 // +//===========================================================================// + +#if !defined(MECH_HPP) +# define MECH_HPP + +# if !defined(JMOVER_HPP) +# include +# endif + +# if !defined(RETICLE_HPP) +# include +# endif + +# if !defined(ALARM_HPP) +# include +# endif + +# if !defined(STATE_HPP) +# include +# endif + +# if !defined(CHAIN_HPP) +# include +# endif + +# if !defined(AVERAGE_HPP) +# include +# endif + +# if !defined(CSTR_HPP) +# include +# endif + +# if !defined(SEQCTL_HPP) +# include +# endif + +# if !defined(NAMEFILT_HPP) +# include +# endif + +# if !defined(ROTATION_HPP) +# include +# endif + +//##################### Forward Class Declarations ####################### + class Mech; + class Subsystem; + class SubsystemMessageManager; + class ControlsMapping; + class PlatformTool; + class MechControlsMapper; + class Mech__DamageZone; + class Joint; + class DamageLookupTable; + +//########################################################################### +//######################### Mech Model Resource ######################### +//########################################################################### + + struct Mech__ModelResource: + public JointedMover::ModelResource + { + char animationPrefix[4]; + Scalar maxAcceleration; + Scalar superStopAcceleration; + Scalar throttleAdjustment; + Scalar lookLeftAngle; + Scalar lookRightAngle; + Scalar lookFrontAngle; + Scalar lookBackAngle; + Scalar walkingTurnRate; + Scalar runningTurnRate; + Scalar reticleX; + Scalar reticleY; + Scalar relativeMechValue; + int deathEffectResourceID; + Scalar deathSplashDamage; + Scalar deathSplashRadius; + Scalar maxUnstableAcceleration; + Scalar unstableAccelerationEffect; + Scalar unstableGunTheEngineEffect; + Scalar unstableSuperStopEffect; + Scalar unstableHighVelocityEffect; + Scalar unstableStopedTurnEffect; + Scalar updatePositionDiffrence; + Scalar updateTurnVelocityDiffrence; + Scalar updateTurnDegreeDiffrence; + Scalar timeDelay; + Vector3D cameraOffset; + char shadowJointName[20]; + }; + +//########################################################################### +//########################## Mech Make Message ########################## +//########################################################################### + + class Mech__MakeMessage: + public JointedMover::MakeMessage + { + public: + char resourceNameA[20]; + char resourceNameB[20]; + char resourceNameC[20]; + + Mech__MakeMessage( + Receiver::MessageID message_ID, + size_t length, + const EntityID &entity_ID, + Entity::ClassID class_ID, + const EntityID &owner_ID, + ResourceDescription::ResourceID resource_ID, + LWord instance_flags, + const Origin &origin, + const Motion &velocity, + const Motion &acceleration, + const char *badge, + const char *color, + const char *patch + ): + JointedMover::MakeMessage( + message_ID, length, entity_ID, class_ID, owner_ID, + resource_ID, instance_flags, origin, velocity, acceleration + ) + { + Str_Copy(resourceNameA, badge, sizeof(resourceNameA)); + Str_Copy(resourceNameB, color, sizeof(resourceNameB)); + Str_Copy(resourceNameC, patch, sizeof(resourceNameC)); + } + }; + +//########################################################################### +//############################## Mech ############################### +//########################################################################### + + class Mech: + public JointedMover + { + // + // The per-zone damage code walks the roster / segment table / alarms + // directly (the 1995 arrangement, mirrored by the reconstruction). + // + friend class Mech__DamageZone; + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Shared Data Support + // + public: + static Derivation ClassDerivations; + static SharedData DefaultData; + + static const HandlerEntry + MessageHandlerEntries[]; + static MessageHandlerSet + MessageHandlers; + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Test Class Support + // + public: + static Logical + TestClass(Mech &); + Logical + TestInstance() const; + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Construction and Destruction + // + public: + typedef Mech__ModelResource ModelResource; + typedef Mech__MakeMessage MakeMessage; + typedef Mech__DamageZone DamageZone; + + static Mech* + Make(MakeMessage *creation_message); + + Mech( + MakeMessage *creation_message, + SharedData &shared_data = DefaultData + ); + ~Mech(); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // App interface + // + public: + void + SetMappingSubsystem(Subsystem *mapper); + + // + // Resolve a skeleton joint by name (the shared resolver the subsystems + // use to bind their animated joints -- Torso twist, etc.): + // GetSegment(name) -> segment jointIndex -> JointSubsystem::GetJoint. + // + Joint* + ResolveJoint(const char *joint_name); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Per-frame simulation (the mech's body Performance; installed by the ctor + // and dispatched each frame from Simulation::PerformAndWatch). + // + public: + typedef void + (Mech::*Performance)(Scalar time_slice); + void + SetPerformance(Performance performance) + { + Check(this); + activePerformance = (Simulation::Performance)performance; + } + + void + Simulate(Scalar time_slice); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Resource creation + // + public: + static void + CreateMakeMessage( + MakeMessage *creation_message, + NotationFile *model_file, + const ResourceDirectories *directories + ); + + static ResourceDescription::ResourceID + CreateModelResource( + ResourceFile *resource_file, + const char *model_name, + NotationFile *model_file, + const ResourceDirectories *directories, + ModelResource *model = 0 + ); + + static ResourceDescription::ResourceID + CreateSubsystemStream( + ResourceFile *resource_file, + const char *model_name, + NotationFile *model_file, + const ResourceDirectories *directories + ); + + static ResourceDescription::ResourceID + CreateDamageZoneStream( + ResourceFile *resource_file, + const char *model_name, + NotationFile *model_file, + const ResourceDirectories *directories + ); + + static ResourceDescription::ResourceID + CreateSkeletonStream( + ResourceFile *resource_file, + const char *model_name, + NotationFile *model_file, + const ResourceDirectories *directories + ); + + static ResourceDescription::ResourceID + CreateExplosionTableStream( + ResourceFile *resource_file, + const char *model_name, + NotationFile *model_file, + const ResourceDirectories *directories + ); + + typedef Logical (*FindNameFunction)( + const char *control_name, ControlsMapping *mapping); + + static ResourceDescription::ResourceID + CreateControlMappingStream( + const char *mapping_name, + NotationFile *mapping_file, + FindNameFunction find_name, + ResourceFile *resource_file, + const char *model_name, + NotationFile *model_file, + const ResourceDirectories *directories, + PlatformTool *current_tool + ); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Subsystem roster accessors (the roster itself -- subsystemArray / + // subsystemCount -- lives in the base Entity) + // + public: + Subsystem* + GetGyroSubsystem() { Check(this); return gyroSubsystem; } + Subsystem* + GetTorsoSubsystem() { Check(this); return sinkSourceSubsystem; } + Subsystem* + GetHudSubsystem() { Check(this); return hudSubsystem; } + Subsystem* + GetSensorSubsystem() { Check(this); return sensorSubsystem; } + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Locomotion parameters (read by MechControlsMapper::InterpretControls to + // shape the demands, and by the drive in Simulate). reverseStrideLength is + // the top/run cycle speed the throttle scales (the naming is the 1995 + // field's; LoadLocomotionClips measures it from the run clips), walkStride + // Length the walk speed. Turn rates are radians/sec. + // + public: + Scalar GetReverseStrideLength() const { Check(this); return reverseStrideLength; } + Scalar GetWalkStrideLength() const { Check(this); return walkStrideLength; } + Scalar GetForwardThrottleScale() const { Check(this); return forwardThrottleScale; } + void SetForwardThrottleScale(Scalar s){ Check(this); forwardThrottleScale = s; } + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Eyepoint / aim-ray composition. The pilot's torso-elevation aim (stick + // pitch) does NOT tilt any skeleton joint on this mech family -- it pitches + // the COCKPIT EYE and the weapon boresight (pixel-calibrated in the BT411 + // reverse-engineering: "pitch does not work" turned out to mean nothing + // consumed Torso's currentElevation, not that a joint was un-animated). + // Composed each frame in Simulate; DPLEyeRenderable / the aim ray read it. + // + public: + const EulerAngles& + GetEyepointRotation() const { Check(this); return eyepointRotation; } + + // + // The current target (the binary's mech target slot @0x388): read by + // the weapons' HasActiveTarget / UpdateTargeting and the fire path. + // The authentic writer is the reticle targeting step (mech4, the + // render wave); the dev harness sets it directly. + // + Entity* + GetTargetEntity() const { Check(this); return targetEntity; } + void + SetTargetEntity(Entity *target) { Check(this); targetEntity = target; } + + // + // Damage entry (overrides the Entity handler): latches the attacker + // (mech+0x43c, read by the zone LOD router) and feeds the gyro + // cockpit bounce before chaining to the base zone routing. The + // cylinder unaimed-hit resolver (type-0x1d table) is a later wave. + // + void + TakeDamageMessageHandler(TakeDamageMessage *message); + + // + // Damage-side death flag: the body graphic alarm (statusAlarm, + // binary @0x714) at level >= 9 = the death/fall state. + // + Logical + IsMechDestroyed() { Check(this); return statusAlarm.GetLevel() >= 9; } + + // + // The live torso twist (the cylinder table's rotate-with-torso rows + // follow it). NULL-safe: 0 with no torso subsystem. + // + Scalar + CurrentTorsoTwist(); + + DamageLookupTable* + GetDamageLookupTable() const { Check(this); return damageLookupTable; } + + // + // Look-state commit (called by the controls mapper on a look-button + // state change): re-aim the eyepoint from the model's authored look + // angles. look_state = MechControlsMapper::LookState. + // + void + CommitLookState(int look_state); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Local Data + // + protected: + // + // Cached subsystem back-pointers (also held in the base roster). + // + Subsystem *sensorSubsystem; + Subsystem *gyroSubsystem; + Subsystem *sinkSourceSubsystem; // the real Torso + Subsystem *hudSubsystem; + SubsystemMessageManager *messageManager; + int weaponCount; + + // + // Capability sub-rosters (Socket views onto the subsystem roster; + // populated per-frame -- phase 5). + // + ChainOf controllableSubsystems; + ChainOf watchedSubsystems; + ChainOf heatableSubsystems; + ChainOf weaponRoster; + ChainOf damageableSubsystems; + + // + // Embedded status / animation / naming state. + // + NameFilter mechNameFilter; + AlarmIndicator masterAlarm; + AlarmIndicator heatAlarm; + AlarmIndicator stabilityAlarm; + AlarmIndicator statusAlarm; + Reticle targetReticle; + StateIndicator animationState; + StateIndicator replicantAnimationState; + StateIndicator collisionState; + SequenceController legAnimation; + SequenceController bodyAnimation; + AverageOf telemetryFilter[5]; + CString resourceNameA; + CString resourceNameB; + CString resourceNameC; + + // + // Locomotion state (Phase 5.3). Turn rates in rad/s, speeds/strides in + // world-units/s. reverseStrideLength = top (run) speed; walkStrideLength + // = walk speed; reverseSpeedMax = the low-speed turn-rate gate; forward + // ThrottleScale multiplies the forward demand; bodyTargetSpeed = the + // demanded speed; currentBodySpeed = the accel-tracked actual speed. + // + Scalar walkingTurnRate; + Scalar runningTurnRate; + Scalar reverseStrideLength; + Scalar walkStrideLength; + Scalar reverseSpeedMax; + Scalar forwardThrottleScale; + Scalar maxBodyAcceleration; + Scalar bodyTargetSpeed; + Scalar currentBodySpeed; + + // + // Composed each frame in Simulate: the look-state eye component + // (lookPitch/lookYaw, set by CommitLookState from the authored look + // angles) plus the Torso elevation. + // + EulerAngles eyepointRotation; + Scalar lookPitch; + Scalar lookYaw; + Entity *targetEntity; + EntityID lastInflictingID; // binary mech+0x43c -- last + // attacker (zone LOD router) + DamageLookupTable *damageLookupTable; // binary mech+0x444 -- the + // cylinder hit-location table + + // + // The model's authored look-view angles (rad; deg in the resource). + // + Scalar lookLeftAngle; + Scalar lookRightAngle; + Scalar lookFrontAngle; + Scalar lookBackAngle; + + // + // Remaining per-frame targeting / animation state, advanced by the + // mech2/mech3/mech4 simulation. Reserved until that path is + // reconstructed with named fields. + // + int reservedState[198]; + }; + +#endif diff --git a/restoration/source410/BT/MISSILE.CPP b/restoration/source410/BT/MISSILE.CPP index be875d04..3f4d071b 100644 --- a/restoration/source410/BT/MISSILE.CPP +++ b/restoration/source410/BT/MISSILE.CPP @@ -313,8 +313,27 @@ void if (range <= fuseRadius && seeker->targetEntity != NULL) { Entity *victim = seeker->targetEntity; + // + // The AUTHENTIC unaimed delivery: zone -1 (invalidDamageZone) with + // the round's world position as the impact point -- the victim's + // cylinder hit-location table resolves WHERE the round physically + // struck ("For BattleTech, damage zones are only valid via reticle + // based weapons" -- the surviving ENTITY3.HPP warning). Without a + // table the base handler drops the hit, so victims with no + // type-29 stream fall back to the interim random-zone pick. + // int zone = -1; - if (victim->damageZoneCount > 0) + damageData.impactPoint = localOrigin.linearPosition; + if (victim->IsDerivedFrom(Mech::ClassDerivations)) + { + Mech *mech_victim = (Mech *)victim; + if (mech_victim->GetDamageLookupTable() == NULL + && victim->damageZoneCount > 0) + { + zone = Random(victim->damageZoneCount); + } + } + else if (victim->damageZoneCount > 0) { zone = Random(victim->damageZoneCount); }