//===========================================================================// // 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) #if !defined(BOXSOLID_HPP) # include // BoxedSolid extents -- the ground-snap probe #endif #if !defined(RANDOM_HPP) # include // Random -- the collision-rattle draw #endif #include // DamageLookupTable -- the cylinder hit table #include // Player::VehicleDeadMessage -- the death notification #include // BTPlayer::ScoreMessage -- the kill credit #include // HostManager::GetEntityPointer -- killer resolve // //############################################################################# //############################################################################# // Derivation Mech::ClassDerivations( JointedMover::ClassDerivations, "Mech" ); // // The Mech's OWN handler table, chained to the JointedMover set. The // TakeDamage entry OVERRIDES Entity's by message ID (Build overlays the // inherited slot -- no gap risk, the parent chain already registered the // ID); every other inherited message still routes to its base handler. // const Receiver::HandlerEntry Mech::MessageHandlerEntries[] = { MESSAGE_ENTRY(Mech, TakeDamage) }; Mech::MessageHandlerSet Mech::MessageHandlers( ELEMENTS(Mech::MessageHandlerEntries), Mech::MessageHandlerEntries, JointedMover::MessageHandlers ); // //############################################################################# // The entity-level attribute table (cockpit binding by name). //############################################################################# // const Mech::IndexEntry Mech::AttributePointers[]= { ATTRIBUTE_ENTRY(Mech, RadarRange, radarRange), ATTRIBUTE_ENTRY(Mech, RadarLinearPosition, radarLinearPosition), ATTRIBUTE_ENTRY(Mech, RadarAngularPosition, radarAngularPosition), ATTRIBUTE_ENTRY(Mech, LinearSpeed, currentBodySpeed), ATTRIBUTE_ENTRY(Mech, MaxRunSpeed, reverseStrideLength), ATTRIBUTE_ENTRY(Mech, DuckState, duckState), ATTRIBUTE_ENTRY(Mech, EyepointRotation, eyepointRotation), ATTRIBUTE_ENTRY(Mech, UnstablePercentage, unstablePercentage), ATTRIBUTE_ENTRY(Mech, CollisionSpeed, collisionSpeed), ATTRIBUTE_ENTRY(Mech, DistanceToMissile, distanceToMissile), ATTRIBUTE_ENTRY(Mech, FootStep, footStep), ATTRIBUTE_ENTRY(Mech, IncomingLock, incomingLock), ATTRIBUTE_ENTRY(Mech, CollisionState, collisionState), ATTRIBUTE_ENTRY(Mech, CollisionNormal, collisionNormal), ATTRIBUTE_ENTRY(Mech, AnimationState, animationState), ATTRIBUTE_ENTRY(Mech, ReduceButton, reduceButton) }; Mech::AttributeIndexSet Mech::AttributeIndex( ELEMENTS(Mech::AttributePointers), Mech::AttributePointers, JointedMover::AttributeIndex ); Mech::SharedData Mech::DefaultData( Mech::ClassDerivations, Mech::MessageHandlers, Mech::AttributeIndex, 33, (Entity::MakeHandler)Mech::Make ); // //############################################################################# //############################################################################# // Mech* Mech::Make(MakeMessage *creation_message) { return new Mech(creation_message); } // //############################################################################# // Mech ctor -- the heart of the entity (walks the model segment table and // instantiates the full subsystem roster: power, heat, weapons, actuators, // controls, tech, damage zones). This is the largest single function in the // game and the current reconstruction frontier; see MECH.NOTES.md. Chains to // the JointedMover base so the skeleton/segments stream before it Fails. //############################################################################# // Mech::Mech( MakeMessage *creation_message, SharedData &shared_data ): JointedMover(creation_message, shared_data), controllableSubsystems(this), watchedSubsystems(this), heatableSubsystems(this), weaponRoster(this), damageableSubsystems(this) { Check_Pointer(creation_message); // // Cached subsystem back-pointers -- filled by the segment walk below. // sensorSubsystem = NULL; gyroSubsystem = NULL; sinkSourceSubsystem = NULL; hudSubsystem = NULL; messageManager = NULL; weaponCount = 0; // // Embedded status / animation / naming state. // mechNameFilter.Initialize(); masterAlarm.Initialize(0x21); heatAlarm.Initialize(3); stabilityAlarm.Initialize(2); statusAlarm.Initialize(0x21); targetReticle.reticleState = Reticle::ReticleOn; targetReticle.pickPointingOn = True; targetReticle.reticleElementMask = Reticle::AllEnabledGroup; animationState = StateIndicator(0x21); animationState.SetState(0); replicantAnimationState = StateIndicator(0x21); replicantAnimationState.SetState(0); collisionState = StateIndicator(4); collisionState.SetState(0); { for (int i = 0; i < 5; ++i) { telemetryFilter[i].SetSize(15, 0.0f); } } legAnimation.Init(this); bodyAnimation.Init(this); // // Locomotion parameters. BRING-UP DEFAULTS: the authentic values come from // the Mech model resource (WalkingTurnRate / RunningTurnRate / MaxAcceleration) // and LoadLocomotionClips (the stride/top speeds measured from the walk/run // animation clips). Wiring those in is a later refinement (needs the model- // resource pointer + clip loader); until then these sane defaults make the // mech drivable with the authentic control-interpretation + drive math. // walkingTurnRate = 50.0f * RAD_PER_DEG; // rad/s (walk / turn-in-place) runningTurnRate = 25.0f * RAD_PER_DEG; // rad/s (at run speed) reverseStrideLength = 30.0f; // top/run speed (u/s) walkStrideLength = 12.0f; // walk speed (u/s) reverseSpeedMax = 2.0f; // low-speed turn-rate gate (u/s) forwardThrottleScale= 1.0f; maxBodyAcceleration = 30.0f; // u/s^2 bodyTargetSpeed = 0.0f; currentBodySpeed = 0.0f; // // Gait channel state (mech2.cpp). The measured constants (standSpeed, // gimpSpeedMax, gimpStrideLength and the four limp figures) get their // real values in LoadLocomotionClips below; these defaults keep every // divide in the transition machines finite if a clip set is missing. // globalTimeScale MUST default to 1 -- zero would silence every clip // advance. Unfilled clip slots hold NullResourceID, which SelectSequence // resolves to an empty, inert controller. // legCycleSpeed = 0.0f; bodyCycleSpeed = 0.0f; forwardCycleRate = 1.0f; gimpCycleRate = 1.0f; standSpeed = 1.0f; gimpSpeedMax = 1.0f; gimpStrideLength = -1.0f; // the measured value is negative too globalTimeScale = 1.0f; hasGimpClips = 0; gimpLeftSpeedMax = 1.0f; gimpRightSpeedMax = 1.0f; gimpLeftStrideLength = 1.0f; gimpRightStrideLength = 1.0f; gyroRumbleTimer = 0.0f; idleStrideScale = 1.0f; runSpeedMax = 1.0e9f; // UNSOURCED cap -- never binds until // its real source is found motionEventArmed = 0; deathAnimationLatched = 0; legResetLatch = 0; bodyResetLatch = 0; limpModeOverride = 0; { const char *fl = getenv("BT_FORCE_LIMP"); if (fl != NULL && (*fl == '3' || *fl == '4')) { limpModeOverride = *fl - '0'; } } { int i; for (i = 0; i < AnimationSlotCount; ++i) { animationClips[i] = ResourceDescription::NullResourceID; } } eyepointRotation = EulerAngles::Identity; lookPitch = 0.0f; lookYaw = 0.0f; targetEntity = NULL; lastInflictingID = EntityID::Null; damageLookupTable = NULL; deathTransitionDone = 0; // // Cockpit-published state: the radar follows our own live transform. // radarRange = 4000.0f; // the authored map() max range radarLinearPosition = &localOrigin.linearPosition; radarAngularPosition = &localOrigin.angularPosition; duckState = 0; unstablePercentage = 0.0f; // staged: no instability model yet collisionSpeed = 0.0f; // staged: audio watcher set (see MECH.HPP) distanceToMissile = 0.0f; footStep = 0; incomingLock = 0; reduceButton = 0; lastInflictingDamage = 0.0f; // // Look-view angles: defaults until the GameModel read below overrides them // with the authored per-mech values. // lookLeftAngle = 90.0f * RAD_PER_DEG; lookRightAngle = -90.0f * RAD_PER_DEG; lookFrontAngle = -30.0f * RAD_PER_DEG; lookBackAngle = 0.0f; { for (int i = 0; i < ELEMENTS(reservedState); ++i) { reservedState[i] = 0; } } // //----------------------------------------------------------------------- // Segment-table walk: instantiate one Subsystem per streamed segment, // dispatching on its classID. The subsystem roster (subsystemArray / // subsystemCount) lives in the base Entity. //----------------------------------------------------------------------- // ResourceDescription::ResourceID modelResourceID = creation_message->resourceID; ResourceDescription *subsystemDesc = application->GetResourceFile()->SearchList( modelResourceID, ResourceDescription::SubsystemModelStreamResourceType ); Check(subsystemDesc); subsystemDesc->Lock(); // // Copy the raw stream into a padded buffer: reading a SubsystemResource // struct off the tail segment can over-read the raw resource, so pad it. // size_t rawSize = (size_t)subsystemDesc->resourceSize; size_t padSize = rawSize + 0x400; void *padBuffer = (void *)new char[padSize]; memcpy(padBuffer, subsystemDesc->resourceAddress, rawSize); MemoryStream subsystemStream(padBuffer, padSize); int streamedSubsystemCount = *(int *)subsystemStream.GetPointer(); subsystemStream.AdvancePointer(sizeof(int)); // // Slot 0 = the (later-installed) control mapper, slot 1 = the voltage bus // sentinel; the streamed subsystems fill from slot 2. // subsystemCount = streamedSubsystemCount + 2; subsystemArray = new Subsystem *[subsystemCount]; { for (int z = 0; z < subsystemCount; ++z) { subsystemArray[z] = NULL; } } for (int id = 2; id < subsystemCount; ++id) { Subsystem::SubsystemResource *seg = (Subsystem::SubsystemResource *)subsystemStream.GetPointer(); Subsystem *made = NULL; switch (seg->classID) { case CondenserClassID: made = new Condenser(this, id, (Condenser::SubsystemResource *)seg); break; case HeatSinkClassID: // // The 0x0BBE stream class is the mech's heat-sink BANK // (AggregateHeatSink) -- there is no streamed plain-HeatSink; our // VDATA enum name simply predates that discovery. // made = new AggregateHeatSink(this, id, (AggregateHeatSink::SubsystemResource *)seg); break; case HeatWatcherClassID: made = new HeatWatcher(this, id, (HeatWatcher::SubsystemResource *)seg); break; case ReservoirClassID: made = new Reservoir(this, id, (Reservoir::SubsystemResource *)seg); break; case GeneratorClassID: made = new Generator(this, id, (Generator::SubsystemResource *)seg); break; case PoweredSubsystemClassID: made = new PoweredSubsystem(this, id, (PoweredSubsystem::SubsystemResource *)seg); break; case SensorClassID: made = new Sensor(this, id, (Sensor::SubsystemResource *)seg); sensorSubsystem = made; break; case GyroscopeClassID: made = new Gyroscope(this, id, (Gyroscope::SubsystemResource *)seg); gyroSubsystem = made; break; case TorsoClassID: made = new Torso(this, id, (Torso::SubsystemResource *)seg); sinkSourceSubsystem = made; break; case MyomersClassID: made = new Myomers(this, id, (Myomers::SubsystemResource *)seg); break; case EmitterClassID: made = new Emitter(this, id, (Emitter::SubsystemResource *)seg); ++weaponCount; break; case PPCClassID: made = new PPC(this, id, (PPC::SubsystemResource *)seg, PPC::DefaultData); ++weaponCount; break; case AmmoBinClassID: made = new AmmoBin(this, id, (AmmoBin::SubsystemResource *)seg); break; case ProjectileWeaponClassID: made = new ProjectileWeapon(this, id, (ProjectileWeapon::SubsystemResource *)seg); ++weaponCount; break; case GaussRifleClassID: made = new GaussRifle(this, id, (GaussRifle::SubsystemResource *)seg); ++weaponCount; break; case MissileLauncherClassID: made = new MissileLauncher(this, id, (MissileLauncher::SubsystemResource *)seg); ++weaponCount; break; case SubsystemMessageManagerClassID: made = new SubsystemMessageManager(this, id, (SubsystemMessageManager::SubsystemResource *)seg); messageManager = (SubsystemMessageManager *)made; break; case HUDClassID: made = new HUD(this, id, (HUD::SubsystemResource *)seg); hudSubsystem = made; break; case SearchlightClassID: made = new Searchlight(this, id, (Searchlight::SubsystemResource *)seg); break; case ThermalSightClassID: made = new ThermalSight(this, id, (ThermalSight::SubsystemResource *)seg); break; case MechTechClassID: made = new MechTech(this, id, (MechTech::SubsystemResource *)seg); break; case EmitterClassID + 1: // LaserClassID -- an Emitter energy weapon case EmitterClassID + 2: // ParticleCannonClassID -- an Emitter energy weapon made = new Emitter(this, id, (Emitter::SubsystemResource *)seg); ++weaponCount; break; default: // // Unrecognised / not-yet-reconstructed subsystem class (Capacitor, // AmmoFeeder, Radar, Turret, ...): give the slot a base // MechSubsystem so control/damage bindings that resolve this // subsystemID find a real (if generic) subsystem rather than a NULL // plug. The roster stays aligned. // made = new MechSubsystem( this, id, (MechSubsystem::SubsystemResource *)seg, MechSubsystem::DefaultData ); break; } subsystemArray[id] = made; subsystemStream.AdvancePointer(seg->subsystemModelSize); } subsystemDesc->Unlock(); delete [] (char *)padBuffer; if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[mech] segment walk done: subsystemCount=" << subsystemCount << " weaponCount=" << weaponCount << endl << flush; // // Skeleton summary: confirm the JointedMover base streamed the segment / // joint tables (so joint-driven aim / animation / damage has something to // bind to). BT_SKEL_DUMP additionally lists every segment name + joint // index (used to identify the twist / gun / leg joints). // JointSubsystem *joints = GetJointSubsystem(); DEBUG_STREAM << "[skel] jointSubsystem=" << (void *)joints << " jointCount=" << (joints ? joints->GetJointCount() : -1) << endl << flush; if (getenv("BT_SKEL_DUMP")) { EntitySegment::SegmentTableIterator it(segmentTable); EntitySegment *seg; int i = 0; while ((seg = it.ReadAndNext()) != NULL && i < 60) { DEBUG_STREAM << "[skel] seg[" << i << "] name=" << seg->GetName() << " jointIdx=" << seg->GetJointIndex() << endl; ++i; } DEBUG_STREAM << "[skel] segments=" << i << endl << flush; // // The subsystem roster map (slot -> name), for cross-referencing the // streamed index fields (voltage source / linked sink / ammo bin). // for (int r = 2; r < subsystemCount; ++r) { if (subsystemArray[r] != NULL) { DEBUG_STREAM << "[roster] slot " << r << " = " << subsystemArray[r]->GetName() << endl; } } DEBUG_STREAM << flush; } } // //----------------------------------------------------------------------- // Source the authentic per-mech locomotion params from the GameModel // resource (mech.cpp @~1430: walkingTurnRate/runningTurnRate deg->rad, // maxAcceleration, throttleAdjustment). The reconstructed ModelResource // struct layout is only partially verified (BT411 flags it mis-decoded in // places), so every read is SANITY-GUARDED: a value outside a sane band // leaves the bring-up default in place. The stride/top speeds still come // from the bring-up defaults (their authentic source is LoadLocomotionClips, // which measures them from the walk/run animation clips -- a later wave). //----------------------------------------------------------------------- // { ResourceDescription *modelDesc = application->GetResourceFile()->SearchList( modelResourceID, ResourceDescription::GameModelResourceType ); if (modelDesc != NULL) { modelDesc->Lock(); ModelResource *model = (ModelResource *)modelDesc->resourceAddress; if (model != NULL) { if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[mech] model params: walkTR=" << model->walkingTurnRate << " runTR=" << model->runningTurnRate << " maxAcc=" << model->maxAcceleration << " throttleAdj=" << model->throttleAdjustment << " (deg,deg,u/s^2,scale)" << endl << flush; DEBUG_STREAM << "[mech] look angles: L=" << model->lookLeftAngle << " R=" << model->lookRightAngle << " F=" << model->lookFrontAngle << " B=" << model->lookBackAngle << " (deg)" << endl << flush; } if (model->walkingTurnRate > 1.0f && model->walkingTurnRate < 360.0f) { walkingTurnRate = model->walkingTurnRate * RAD_PER_DEG; } if (model->runningTurnRate > 1.0f && model->runningTurnRate < 360.0f) { runningTurnRate = model->runningTurnRate * RAD_PER_DEG; } if (model->maxAcceleration > 1.0f && model->maxAcceleration < 500.0f) { maxBodyAcceleration = model->maxAcceleration; // // The gait's slew rates ARE the authored acceleration // (binary ctor: +0x344/+0x5b0/+0x5b8 all take rec+0x44). // This is what makes the stride-driven speed model and // the old acceleration model agree in feel: both slew at // maxAcceleration toward the demand. // forwardCycleRate = model->maxAcceleration; gimpCycleRate = model->maxAcceleration; } if (model->throttleAdjustment > 0.05f && model->throttleAdjustment < 20.0f) { forwardThrottleScale = model->throttleAdjustment; } // // The authored look-view angles (deg->rad), same insanity band // as the rest of the guarded reads. // { Scalar a; a = model->lookLeftAngle; if (a > -360.0f && a < 360.0f) lookLeftAngle = a * RAD_PER_DEG; a = model->lookRightAngle; if (a > -360.0f && a < 360.0f) lookRightAngle = a * RAD_PER_DEG; a = model->lookFrontAngle; if (a > -360.0f && a < 360.0f) lookFrontAngle = a * RAD_PER_DEG; a = model->lookBackAngle; if (a > -360.0f && a < 360.0f) lookBackAngle = a * RAD_PER_DEG; } // // The gait clip loader -- resolves every animation clip by // the model's prefix and MEASURES the stride/speed constants // from the clips themselves, replacing the bring-up defaults // above. Guarded on the prefix looking like text because // this ModelResource layout is only partially verified; a // garbage prefix would just probe nonsense names (soft), but // the log line makes a layout miss visible. // if ( model->animationPrefix[0] >= 'a' && model->animationPrefix[0] <= 'z' ) { LoadLocomotionClips(model); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[mech] clips '" << model->animationPrefix << "': standSpeed=" << standSpeed << " walkStride=" << walkStrideLength << " revStride=" << reverseStrideLength << " revSpeedMax=" << reverseSpeedMax << " gimpSpeedMax=" << gimpSpeedMax << " gimpStride=" << gimpStrideLength << " limpSet=" << hasGimpClips << endl << flush; } } else if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[mech] animationPrefix not text (" << (int)(unsigned char)model->animationPrefix[0] << ") -- clip loader SKIPPED, layout suspect" << endl << flush; } } modelDesc->Unlock(); } } // //----------------------------------------------------------------------- // Fill the hull damage-zone array. The Entity base ctor only READS the // zone count and allocates the raw POINTER array (entity.cpp:1032) -- the // derived entity must construct the streamed DamageZone objects into the // slots itself (the CulturalIcon ctor is the surviving reference, // cultural.cpp:349). LOAD-BEARING: an unfilled slot is heap garbage, and // the first TakeDamageMessage that lands on it calls through a trash // vtable (crash = EIP inside the heap). Runs AFTER the segment walk // because the streamed DamageZone ctor resolves its effect-site segments // via GetSegment() on JointedMover-derived owners. //----------------------------------------------------------------------- // if (damageZones != NULL && damageZoneCount > 0) { ResourceDescription *dmg_res = application->GetResourceFile()->SearchList( resourceID, ResourceDescription::DamageZoneStreamResourceType ); Check(dmg_res); dmg_res->Lock(); DynamicMemoryStream damage_stream( dmg_res->resourceAddress, dmg_res->resourceSize ); damage_stream.AdvancePointer(sizeof(damageZoneCount)); // // Every entry is the MECH-specific zone subclass (the class-scope // DamageZone typedef = Mech__DamageZone, binary ctor @0049ce50): its // streamed ctor chains the engine base parse and consumes the BT // per-zone TAIL (flags / criticals / LOD redirects) -- a base-class // fill parses zone 0 short and skews every following zone. // { for (int dz = 0; dz < damageZoneCount; ++dz) { damageZones[dz] = new DamageZone(this, dz, &damage_stream); Register_Object(damageZones[dz]); } for (int dp = 0; dp < damageZoneCount; ++dp) { ((DamageZone *)damageZones[dp])->SetLODParentPointers(); } } dmg_res->Unlock(); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[mech] damage zones streamed: " << damageZoneCount; for (int zz = 0; zz < damageZoneCount; ++zz) { DEBUG_STREAM << (zz ? "," : " [") << damageZones[zz]->damageZoneName; } DEBUG_STREAM << "]" << endl << flush; } } // //----------------------------------------------------------------------- // The cylinder hit-location table (binary Pass-3 tail, cached at // mech+0x444): found by the DamageZoneStream member's NAME in the // type-29 DamageLookupTableStream directory. Resolves UNAIMED hits // (zone -1 + impact point -- missiles, splash, rams) onto hull zones by // impact geometry. Kept locked (resident) for the mech's life. //----------------------------------------------------------------------- // { ResourceDescription *zone_stream = application->GetResourceFile()->SearchList( modelResourceID, ResourceDescription::DamageZoneStreamResourceType ); if (zone_stream != NULL) { ResourceDescription *cyl_desc = application->GetResourceFile()->FindResourceDescription( zone_stream->resourceName, ResourceDescription::DamageLookupTableStreamResourceType ); if (cyl_desc != NULL) { cyl_desc->Lock(); MemoryStream cyl_stream( cyl_desc->resourceAddress, cyl_desc->resourceSize ); damageLookupTable = new DamageLookupTable(this, &cyl_stream); Register_Object(damageLookupTable); } if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[cyl] table '" << zone_stream->resourceName << "' " << ((damageLookupTable != NULL) ? "LOADED rows=" : "ABSENT rows=") << ((damageLookupTable != NULL) ? damageLookupTable->rowCount : 0) << endl << flush; } } } // // Seed the condenser flow shares: every valve spawns at 1, so the initial // recompute yields equal shares across the bank (a MoveValve press // re-shares them). // Condenser::RecomputeValves(this); // // Install the per-frame body Performance. Until now the mech ran the base // DoNothingOnce; from here the engine dispatches Mech::Simulate every frame // (Mover -> Entity -> Simulation::PerformAndWatch) once the mission is // RunningMission. // SetPerformance(&Mech::Simulate); // // The entity is complete -- mark it VALID, like every 1995 entity ctor tail // (CamShip / DoorFrame / DropZone / ...). LOAD-BEARING: Entity::Dispatch // routes messages to an INVALID entity into the deferred event queue, so // without this the mech never receives a directly-dispatched message -- // the PlayerLink bind (and with it every player-experience gate) silently // never lands. // SetValidFlag(); Check_Fpu(); } Mech::~Mech() { } // //############################################################################# // TakeDamageMessageHandler -- the Mech override of the Entity damage entry // (binary hub @004a0230 via the handler glue @0049ed0c). // // Order in the binary: (1) feed the RAW Damage record to the gyro cockpit // bounce FIRST -- even an invalid-zone hit shakes the cockpit (STAGED: the // gyro bounce math is the gyro/feel wave; a gated log marks the feed site); // (2) latch the attacker (mech+0x43c -- the zone LOD router keys its // same-attacker redirect reuse off it); (3) resolve an unaimed hit's zone // (invalidDamageZone) from the cylinder hit-location table -- DEFERRED, the // type-0x1d table is not loaded yet, so unaimed hits fall through to the // base handler's zone==-1 drop (authentic base behavior); (4) chain to the // Entity handler which routes damageZones[zone]->TakeDamage. //############################################################################# // // //############################################################################# // DistributeCollisionDamage (binary @0049ffcc, reached ONLY from the damage // hub's type-0 divert) -- the collision-damage economy. // // Collision damage is the manual's "collision damage" TECHNICIAN SETTING: // gated on the owning player's advanced-damage flag, priced against a // 100 km/h reference impact, and applied as 0.5-point RATTLE CRITS to random // internal subsystems. Armor is never touched by a collision. // // scale = (2000 / mass) / (100 km/h)^2 / (1 - elasticity^2) // amount = raw * scale; under 0.5 the tap is FREE // n = Round(amount * 2) sub-hits of amount/n each // // Each sub-hit lands on ONE roster subsystem drawn by cumulative // collisionCriticalHitWeight against a [0,1) roll, restricted to the // HeatSink / Gyroscope / Torso families. An un-won roll lands nowhere -- // faithful: the binary does not normalize the weights. //############################################################################# // void Mech::DistributeCollisionDamage(Damage *damage) { Check(this); Check_Pointer(damage); BTPlayer *player = (BTPlayer *)GetPlayerLink(); if (player == NULL || !player->IsAdvancedDamageOn()) { return; } Scalar reference = 100.0f * 0.27777779f; // 100 km/h in world u/s Scalar denominator = 1.0f - elasticityCoefficient * elasticityCoefficient; Scalar scale = ((2000.0f / moverMass) / (reference * reference)) / denominator; damage->damageAmount *= scale; if (damage->damageAmount < 0.5f) { return; } int sub_hits = (int)(damage->damageAmount * 2.0f + 0.5f); if (sub_hits < 1) { sub_hits = 1; } damage->damageAmount /= (Scalar)sub_hits; if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[colldmg] rattle " << (damage->damageAmount * sub_hits) << " pts in " << sub_hits << " sub-hits (scale=" << scale << ")" << endl << flush; } int hit, slot; for (hit = 0; hit < sub_hits; ++hit) { Scalar threshold = (Scalar)Random, accumulated = 0.0f; for (slot = 0; slot < GetSubsystemCount(); ++slot) { Subsystem *subsystem = GetSubsystem(slot); if (subsystem == NULL) { continue; } if ( !subsystem->IsDerivedFrom(HeatSink::ClassDerivations) && !subsystem->IsDerivedFrom(Gyroscope::ClassDerivations) && !subsystem->IsDerivedFrom(Torso::ClassDerivations) ) { continue; } accumulated += ((MechSubsystem *)subsystem)->CollisionCritWeight(); if (accumulated < threshold) { continue; } ((MechSubsystem *)subsystem)->ApplyDamageAndMeasure(*damage); break; } } Check_Fpu(); } void Mech::TakeDamageMessageHandler(TakeDamageMessage *message) { Check(this); Check(message); if (gyroSubsystem != NULL && getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[gyro] hit feed staged (type=" << (int)message->damageData.damageType << " amt=" << message->damageData.damageAmount << ")" << endl << flush; } lastInflictingID = message->inflictingEntity; lastInflictingDamage = message->damageData.damageAmount; // // THE COLLISION DIVERT (binary @0x4a0368 -> @0049ffcc): type-0 Collision // damage NEVER reaches the armor zones. It is priced against a 100 km/h // reference impact and applied as internal RATTLE crits -- which is the // answer to the raw kinetic numbers being thousands of points where a // PPC is ~12. Without this divert a hard wall crash one-shots a vital // zone through the cylinder lottery (observed live, 5.3.99). // if (message->damageData.damageType == Damage::CollisionDamageType) { DistributeCollisionDamage(&message->damageData); return; } // // The cylinder resolve (binary @0x4a0264 tail): an UNAIMED hit arrives // with invalidDamageZone set -- map its world impact point onto a hull // zone through the height x angle table, then let the base route it. // if (message->invalidDamageZone && damageLookupTable != NULL) { int zone = damageLookupTable->ResolveHit( message->damageData.impactPoint); if (zone >= 0 && zone < damageZoneCount) { message->damageZone = zone; message->invalidDamageZone = False; } } Entity::TakeDamageMessageHandler(message); } void Mech::SetMappingSubsystem(Subsystem *subsystem) { Check(this); Check_Pointer(subsystemArray); // // The control mapper lives in roster slot 0 (the streamed control-mapping // resource binds its DirectMappings to subsystemID 0, so it must resolve // there via Entity::GetSimulation(0)). On a re-spawn, drop the old one. // if (subsystemArray[0] != NULL) { Unregister_Object(subsystemArray[0]); delete subsystemArray[0]; } subsystemArray[0] = subsystem; } // //############################################################################# // Reset -- the respawn heal-and-move (binary @0049fb74). REUSES the same // entity (the sever-and-recreate respawn was the source of the 1995 port's // "two mechs / camera inside / on-fire respawn" glitch family): // 1. reposition at the drop zone (origin + transform + update base) // 2. kill all motion (a respawn is a TELEPORT, no dead-reckon lerp back) // 3. clear the death latch (the alarm trigger + the once-per-death flag) // 4. heal every hull zone (structure 0, intact skin, not burning) -- // the crit-allotment accountant (damagePercentageUsed) PERSISTS by // design: nothing in the recovered binary resets it across lives // (BT411-observed; spent crit budgets stay spent) // 5. sweep the roster through DeathReset (heat/power/ammo/charge restore) // 6. revalidate for the sim (PreRun). // The ForceUpdate(0x1f) re-broadcast + warp/alarm effects join the render / // cockpit waves. //############################################################################# // void Mech::Reset(const Origin &origin, Logical full_reset) { Check(this); localOrigin = origin; localToWorld = origin; updateOrigin = origin; worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f); localVelocity = Motion::Identity; updateVelocity.linearMotion = Vector3D(0.0f, 0.0f, 0.0f); updateVelocity.angularMotion = Vector3D(0.0f, 0.0f, 0.0f); currentBodySpeed = 0.0f; bodyTargetSpeed = 0.0f; statusAlarm.SetLevel(0); deathTransitionDone = 0; { for (int dz = 0; dz < damageZoneCount; ++dz) { if (damageZones[dz] != NULL) { ::DamageZone *zone = (::DamageZone *)damageZones[dz]; zone->damageLevel = 0.0f; zone->SetGraphicState(0); zone->SetDamageZoneState(0); } } } { for (int ss = 0; ss < subsystemCount; ++ss) { if (subsystemArray[ss] != NULL) { subsystemArray[ss]->DeathReset(full_reset); } } } SetPreRunFlag(); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[reset] mech " << GetEntityID() << " HEALED + placed at (" << origin.linearPosition.x << "," << origin.linearPosition.y << "," << origin.linearPosition.z << ")" << endl << flush; } Check_Fpu(); } // //############################################################################# // CurrentTorsoTwist -- the live torso twist for the cylinder table's // rotate-with-torso rows (binary reads torso+0x1d8). NULL-safe. //############################################################################# // Scalar Mech::CurrentTorsoTwist() { Check(this); Torso *torso = (Torso *)sinkSourceSubsystem; return (torso != NULL) ? torso->CurrentTwist() : 0.0f; } // //############################################################################# // ResolveJoint -- the shared skeleton-joint resolver (mech.cpp @00424b60). // A subsystem hands us the joint NAME from its resource; we look up the // skeleton segment of that name, read its joint index, and fetch the animated // Joint from the JointSubsystem. NULL for an empty/unknown name or a mech with // no skeleton/joint subsystem. //############################################################################# // Joint* Mech::ResolveJoint(const char *joint_name) { Check(this); if (joint_name == NULL || joint_name[0] == '\0') { return NULL; } EntitySegment *segment = GetSegment(CString(joint_name)); if (segment == NULL) { return NULL; } JointSubsystem *joints = GetJointSubsystem(); if (joints == NULL) { return NULL; } // // A segment that HAS no joint reports index -1, and GetJoint indexes a // table with it unchecked -- TableIterator::GetNthImplementation walks to // [base + -1*4] and dies (guest 00426A1D, ECX=FFFFFFFF). Torso never hit // this because it only ever asks for its own authored twist-joint name; // the skeleton walk asks for EVERY page, and most .SKL pages are sites or // static segments with no joint at all. // int joint_index = segment->GetJointIndex(); if (joint_index < 0 || joint_index >= joints->GetJointCount()) { return NULL; } return joints->GetJoint(joint_index); } // //############################################################################# // CommitLookState -- the look-button eyepoint commit (the binary's five-state // look machine tail, controls mapper part_013.c:396-459). Re-aims the eyepoint // from the model's authored look angles: side looks yaw by lookLeft/RightAngle, // look-behind is yaw pi with lookBackAngle pitch, look-down pitches by // lookFrontAngle, forward is identity. The committed pitch/yaw are stored in // lookPitch/lookYaw so the per-frame compose in Simulate can keep adding the // live Torso elevation on top. // // Also part of the authentic commit, deferred to the weapon wave: re-arming // each weapon's view-fire enable (forward view = the non-rear-mounted weapons, // look-back = the rear-mounted ones, side/down = none) and flipping the HUD pip // group mask (forward = front group, look-back = rear group) -- both need the // MechWeapon viewFireEnable/rearFiring members, not yet reconstructed. //############################################################################# // void Mech::CommitLookState(int look_state) { Check(this); Scalar pitch = 0.0f; Scalar yaw = 0.0f; switch (look_state) { case MechControlsMapper::LookLeftState: yaw = lookLeftAngle; break; case MechControlsMapper::LookRightState: yaw = lookRightAngle; break; case MechControlsMapper::LookBehindState: yaw = PI; pitch = lookBackAngle; break; case MechControlsMapper::LookDownState: pitch = lookFrontAngle; break; default: break; // LookNone: identity } lookPitch = pitch; lookYaw = yaw; eyepointRotation = EulerAngles( Radian(Radian::Normalize(pitch)), Radian(Radian::Normalize(yaw)), Radian(0.0f) ); // // Re-arm each weapon's view-fire enable: the forward view arms the // non-rear-mounted weapons, LOOK-BACK arms the rear-mounted ones, and the // side/down views arm none. // { for (int id = 2; id < subsystemCount; ++id) { Subsystem *sub = subsystemArray[id]; if (sub == NULL || !sub->IsDerivedFrom(MechWeapon::ClassDerivations)) { continue; } MechWeapon *weapon = (MechWeapon *)sub; Logical arm; if (look_state == MechControlsMapper::LookNone) { arm = (weapon->IsRearFiring() == False); } else if (look_state == MechControlsMapper::LookBehindState) { arm = weapon->IsRearFiring(); } else { arm = False; } weapon->SetViewFireEnable(arm); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[look] weapon '" << weapon->GetName() << "' rear=" << (int)weapon->IsRearFiring() << " armed=" << (int)arm << endl << flush; } } } // // The HUD reticle weapon-pip group: the forward view shows the FRONT pip // group, look-back shows the REAR group; side/down views leave the mask. // if (look_state == MechControlsMapper::LookNone) { targetReticle.reticleElementMask = (Reticle::ReticleElements) (((int)targetReticle.reticleElementMask | Reticle::FrontFiringWeaponsOn) & ~Reticle::RearFiringWeaponsOn); } else if (look_state == MechControlsMapper::LookBehindState) { targetReticle.reticleElementMask = (Reticle::ReticleElements) (((int)targetReticle.reticleElementMask | Reticle::RearFiringWeaponsOn) & ~Reticle::FrontFiringWeaponsOn); } if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[look] state=" << look_state << " yaw=" << yaw << " pitch=" << pitch << " pipMask=0x" << hex << (int)targetReticle.reticleElementMask << dec << endl << flush; } Check_Fpu(); } // //############################################################################# // Simulate -- the mech's per-frame body Performance (the Mover locomotion tick). // // FUNCTIONAL MOTION CORE (Phase 5.3, increment 1). A Mech is a Mover; its // per-frame job is to advance its Origin and commit it to the world transform. // This reconstructs the load-bearing spine of the 1995 Mech::Simulate // (mech4.cpp @004ab430): integrate the body velocity into localOrigin, then // rebuild localToWorld with the engine's own idiom (ENTITY.CPP:988 / // MOVER.CPP:850, `localToWorld = localOrigin`). // // Still layered on top of this (next increments): // * the gait-cycle self-propulsion that FEEDS worldLinearVelocity -- the // authentic model drives forward speed from the walk/run animation cycle // (IntegrateMotion -> AdvanceBodyAnimation -> cycleDistance) steered by the // control-mapper demands (throttle/turn), not a raw velocity; // * heading integration (rotate localOrigin.angularPosition by the turn rate); // * the terrain-height drop (BoundingBoxTreeNode::FindBoundingBoxUnder) that // rests the feet on the ground; // * the cockpit telemetry FilteredScalars (head/aim/leg/torso angular rates). // // With no locomotion layer yet, worldLinearVelocity is zero for a freshly // spawned mech, so it holds its pose -- identical to the prior DoNothing, but // now on the real Simulate path. DEV hook BT_DRIVE="vx,vy,vz" injects a // constant world velocity so the integrate + transform path is verifiable // headlessly (no RIO/controls needed). See MECH.NOTES.md. //############################################################################# // void Mech::Simulate(Scalar time_slice) { Check(this); // //----------------------------------------------------------------------- // DEATH (binary UpdateDeathState @mech4, the once-per-death transition): // a destroyed mech FREEZES -- no drive, no eyepoint, no dev harness (the // weapon hard gates silence the guns state-side). The one-shot: sweep // the roster through DeathShutdown(1) and dispatch the VehicleDead // death notification (deathCount = -1, the ctor default) to the owning // player -- only the owner master has a live playerLink; an unowned // wreck (the dev enemy) just settles. //----------------------------------------------------------------------- // if (IsMechDestroyed()) { if (!deathTransitionDone) { deathTransitionDone = 1; // // A wreck is STILL: kill the broadcastable motion so replicant // wrecks don't dead-reckon away at the last drive vector. // worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f); updateVelocity.linearMotion = Vector3D(0.0f, 0.0f, 0.0f); updateVelocity.angularMotion = Vector3D(0.0f, 0.0f, 0.0f); currentBodySpeed = 0.0f; bodyTargetSpeed = 0.0f; for (int ds = 0; ds < subsystemCount; ++ds) { if (subsystemArray[ds] != NULL) { subsystemArray[ds]->DeathShutdown(1); } } // // The death notification is MASTER-only: our // InitializePlayerLink binds replicant mechs to replicant // players too, and a replicant dispatch would run a second, // non-authoritative death cycle on every remote pod. // Player *pilot = (GetInstance() != Entity::ReplicantInstance) ? GetPlayerLink() : NULL; if (pilot != NULL) { Player::VehicleDeadMessage dead( Player::VehicleDeadMessageID, sizeof(Player::VehicleDeadMessage) ); pilot->Dispatch(&dead); } // // The KILL CREDIT: resolve the last attacker and post the // type-2 ScoreMessage to the KILLER's pilot, carrying the // killing-blow magnitude (the whole award derives from it -- // binary emitter in the unexported master-perf writer; the // kill-bonus bias rides scoreAward, staged 0). MASTER-only, // and an unpiloted killer (the dev enemy) earns nothing. // if (GetInstance() != Entity::ReplicantInstance && !(lastInflictingID == EntityID::Null)) { Entity *killer = (Entity *)application->GetHostManager()-> GetEntityPointer(lastInflictingID); if (killer != NULL && killer != (Entity *)this && killer->IsDerivedFrom(Mech::ClassDerivations) && killer->GetPlayerLink() != NULL) { BTPlayer::ScoreMessage kill_score( Player::ScoreMessageID, sizeof(BTPlayer::ScoreMessage), 0.0f, BTPlayer::KillScore, lastInflictingDamage, GetEntityID() ); killer->GetPlayerLink()->Dispatch(&kill_score); } } if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[death] mech " << GetEntityID() << " WRECKED (pilot " << ((pilot != NULL) ? "notified" : "none") << ")" << endl << flush; } } Check_Fpu(); return; } // //----------------------------------------------------------------------- // Read the control-mapper locomotion demands. The mapper lives at roster // slot 0 and its InterpretControls Performance ticks in the Entity::Perform // AndWatch roster walk BEFORE this (the mech's own Performance runs last), // so speedDemand/turnDemand are this frame's. //----------------------------------------------------------------------- // Scalar speedDemand = 0.0f; Scalar turnDemand = 0.0f; if (subsystemArray != NULL && subsystemArray[0] != NULL) { MechControlsMapper *mapper = (MechControlsMapper *)subsystemArray[0]; speedDemand = mapper->GetSpeedDemand(); turnDemand = mapper->GetTurnDemand(); } bodyTargetSpeed = speedDemand; // //----------------------------------------------------------------------- // THE GAIT (mech2.cpp). The leg channel poses the skeleton from the live // demand -- this is what makes the legs actually move; the body channel // runs as a pure stride measurement (move_joints 0) so the two never // fight over the same joints. // // The body channel's measured stride IS the mech's speed (the binary: // IntegrateMotion @004ab1c8 sets local velocity z = -distance/dt). The // gait slews its cycle speed at forwardCycleRate == the authored // maxAcceleration, so this replaces the earlier explicit acceleration // model at the same rate -- but the speed now rises THROUGH the gait: // zero while standing, the walk band during the walk cycle, stepping to // the run band at a clip boundary. // // STILL STAGED from IntegrateMotion: the airborne flavour pick, the // dead-reckon latency fold, and the turn-in-place dispatcher (mech4). //----------------------------------------------------------------------- // // // The limp pick (IntegrateMotion @004ab1c8): movement modes 3/4 are the // left/right leg limps, and while limping (and the model HAS limp clips) // the Gimp advancers replace the normal pair -- they must, because the // normal ones treat the limp states as their reset group. // // BT_FORCE_LIMP=3|4 is a DEV hook that forces the pick without touching // the simulation state, so the limp gait can be verified before the // damage model's limp hook (leg zone >= 0.5 -> mode 3/4) is // reconstructed. // // // STAGED (the master performance's job, mech4 @004a9b5c -- never // decompiled): promote the damage model's limp signal (statusAlarm level // 3/4, MECHDMG's leg-half write to mech+0x2c) into the movement mode the // gait machinery keys on. The binary's damage code READS mode 3/4 to // decide "already limping", so the promotion demonstrably exists; the // master perf is the only unaccounted-for writer. Guarded so a death // (mode 5+) is never overwritten. // { unsigned status = statusAlarm.GetLevel(); int mode = MovementMode(); if ((status == 3 || status == 4) && mode < 3) { SetSimulationState(status); } } int limping; { int mode = MovementMode(); limping = (mode == 3 || mode == 4) && hasGimpClips; } if (limping) { AdvanceLegAnimationGimp(time_slice); } else { AdvanceLegAnimation(time_slice); } { Scalar stride = limping ? AdvanceBodyAnimationGimp(time_slice, 0) : AdvanceBodyAnimation(time_slice, 0); if (animationClips[5] != ResourceDescription::NullResourceID) { currentBodySpeed = stride / time_slice; } else { // // [T3 bring-up] A model with NO gait clips would otherwise never // move. Keep the old acceleration model for those until every // fleet mech's clip set is verified. // Scalar dv = bodyTargetSpeed - currentBodySpeed; Scalar maxStep = maxBodyAcceleration * time_slice; if (dv > maxStep) dv = maxStep; if (dv < -maxStep) dv = -maxStep; currentBodySpeed += dv; } } // //----------------------------------------------------------------------- // Authentic per-mech turn rate: lerp(walkingTurnRate, runningTurnRate) by // ground speed, with a runningTurnRate/t^2 over-run falloff past top speed; // clamp >= 0. (mech4.cpp master-perf @0x4aa3d3.) //----------------------------------------------------------------------- // Scalar authTurnRate = walkingTurnRate; { Scalar spd = (currentBodySpeed < 0.0f) ? -currentBodySpeed : currentBodySpeed; if (spd >= reverseSpeedMax) { Scalar den = reverseStrideLength - walkStrideLength; Scalar t = (den != 0.0f) ? (spd - walkStrideLength) / den : 0.0f; if (t <= 1.0f) { authTurnRate = walkingTurnRate + (runningTurnRate - walkingTurnRate) * t; } else { authTurnRate = runningTurnRate / (t * t); } } if (authTurnRate < 0.0f) { authTurnRate = 0.0f; } } // //----------------------------------------------------------------------- // Integrate heading (yaw) into the body orientation quaternion via the // engine's rotation-integrate op (Quaternion::Add(source, omega*dt)), then // rebuild the world transform so the facing axis below is current. //----------------------------------------------------------------------- // { Vector3D angStep; angStep.x = 0.0f; angStep.y = turnDemand * authTurnRate * time_slice; angStep.z = 0.0f; Quaternion prevPose = localOrigin.angularPosition; localOrigin.angularPosition.Add(prevPose, angStep); } localToWorld = localOrigin; // //----------------------------------------------------------------------- // Forward step: the mech faces local -Z (gun ports / eyepoint at -Z). Take // the world Z basis and negate for the facing direction; move at the current // body speed. (The animation-exact per-frame advance from the gait clip is // the deferred fidelity layer; this is the procedural equivalent.) //----------------------------------------------------------------------- // UnitVector zAxis; localToWorld.GetFromAxis(Z_Axis, &zAxis); worldLinearVelocity.x = -zAxis.x * currentBodySpeed; worldLinearVelocity.y = -zAxis.y * currentBodySpeed; worldLinearVelocity.z = -zAxis.z * currentBodySpeed; // // DEV cockpit-button harness: BT_PRESS_VALVE / BT_PRESS_FLUSH dispatch the // REAL cockpit messages (MoveValve to Condenser1, InjectCoolant to the // Reservoir) once, ~6 s in -- exercising the authentic Receiver dispatch -> // handler-table path headlessly. // { static Scalar pressClock = 0.0f; static int pressed = 0; pressClock += time_slice; if (!pressed && pressClock >= 6.0f) { pressed = 1; if (getenv("BT_PRESS_VALVE")) { for (int s = 2; s < subsystemCount; ++s) { Subsystem *sub = subsystemArray[s]; if (sub != NULL && sub->IsDerivedFrom(Condenser::ClassDerivations)) { ReceiverDataMessageOf 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. //----------------------------------------------------------------------- // Point3D frame_start_position = localOrigin.linearPosition; localOrigin.linearPosition.AddScaled( localOrigin.linearPosition, worldLinearVelocity, time_slice ); localToWorld = localOrigin; // //----------------------------------------------------------------------- // THE GROUND SNAP -- the probe half of the master performance's ground // model (binary @4aa630-4aa6cc, decoded from raw asm; there is NO // gravity anywhere in the mech). Place the collision volume, drop a // probe from the volume's authored bottom, ask the zone's box tree for // the surface under it, and place the origin ON that surface exactly. // Walking up-slope rides the lift window; walking off a roof drops // instantly; a probe MISS (h == -1) holds Y, so a runaway is // structurally impossible. // // The COLLISION half (frame rejection, crush sentinel, crash clip) is a // separate increment -- it hangs off ProcessCollisionList. // // Gated on the engine having built a collision volume for this model // (Mover's ctor does when the resource carries one); logged once if // absent so a silent no-op is visible. //----------------------------------------------------------------------- // if ( GetCollisionVolumeCount() > 0 && collisionVolume != NULL && collisionTemplate != NULL ) { MoveCollisionVolume(); BoundingBoxTreeNode *ground_node = GetMoverCollisionRoot(); if (ground_node != NULL) { Point3D probe = localOrigin.linearPosition; probe.y += collisionTemplate->minY; Scalar height = -1.0f; ground_node->FindBoundingBoxUnder(probe, &height); if (height > 0.0001f) { Scalar drop = height - collisionTemplate->minY; localOrigin.linearPosition.y -= drop; collisionVolume->minY -= drop; collisionVolume->maxY -= drop; localToWorld = localOrigin; } } } else if (getenv("BT_MECH_LOG")) { static int noVolumeOnce = 0; if (!noVolumeOnce++) { DEBUG_STREAM << "[ground] mech has NO collision volume -- " << "snap inactive (model streams none?)" << endl << flush; } } // //----------------------------------------------------------------------- // COLLISIONS -- the response half of the master performance's ground // model (binary @4aa6cf-4aab0b). Walls block by FULL FRAME REJECTION, // never by slide or climb: any blocking contact restores the // start-of-frame position and zeroes the velocity. The crash itself // HURTS -- the accumulated collision Damage is dispatched at OUR OWN // mech, zone -1 (the cylinder lottery), which is what makes wall- // grinding self-limiting. And a hard enough hit (|v|^2 > 40, the // binary's @0x4ab184 threshold) staggers the mech: both gait channels // bind the bump clip (slot 0x20) and recover to Standing at its end. // // Guarded on the collision assistant: the engine's GetCurrentCollisions // walks it unchecked, and only the viewpoint mech gets one // (StartCollisionAssistant, BTL4APP.CPP:409). // // STAGED deltas, named: the crushable-CulturalIcon sentinel (0.00123f // -- the move stands, gyro crunch) needs the Mech::ProcessCollision // override, a later increment; the gyro crunch feed itself is with it; // and the collisionState audio push waits on the same override's // contact accumulation. //----------------------------------------------------------------------- // if ( collisionAssistant != NULL && GetCollisionVolumeCount() > 0 && collisionVolume != NULL ) { Vector3D impact_velocity = worldLinearVelocity; BoxedSolidCollisionList *collisions = GetCurrentCollisions(); Damage collision_damage; if (collisions != NULL) { ProcessCollisionList( collisions, time_slice, frame_start_position, &collision_damage); } if (collision_damage.damageAmount > 0.0f) { worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f); localOrigin.linearPosition = frame_start_position; localToWorld = localOrigin; MoveCollisionVolume(); Entity::TakeDamageMessage crash( Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage), GetEntityID(), -1, collision_damage ); Dispatch(&crash); Scalar impact_squared = impact_velocity.x * impact_velocity.x + impact_velocity.y * impact_velocity.y + impact_velocity.z * impact_velocity.z; if ( impact_squared > 40.0f && animationClips[0x20] != ResourceDescription::NullResourceID && legStateAlarm.GetLevel() != 0x20 ) { SetLegAnimation(0x20); SetBodyAnimation(0x20); ForceUpdate(1); ForceUpdate(0x20); } if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[crash] BLOCK dmg=" << collision_damage.damageAmount << " iv2=" << impact_squared << (impact_squared > 40.0f ? " KNOCKDOWN" : "") << endl << flush; } } } if (getenv("BT_MECH_LOG")) { static Scalar reportAccum = 0.0f; reportAccum += time_slice; if (reportAccum >= 1.0f) { reportAccum = 0.0f; EulerAngles ypr; ypr = localOrigin.angularPosition; DEBUG_STREAM << "[sim] pos=(" << localOrigin.linearPosition.x << "," << localOrigin.linearPosition.y << "," << localOrigin.linearPosition.z << ")" << " yaw=" << (Scalar)ypr.yaw << " spd=" << currentBodySpeed << " eyePitch=" << (Scalar)eyepointRotation.pitch << " eyeYaw=" << (Scalar)eyepointRotation.yaw << endl << flush; } } Check_Fpu(); }