//===========================================================================// // 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)); } }; //########################################################################### //####################### MechAnimationState ############################ //########################################################################### // // The gait clip a mech is currently playing. Recovered VERBATIM from the // 0x3c-byte-stride name table at .data:0050cfe8 -- the table the binary's // "Unsupported mech animation" assert indexes -- so the names and their order // are the original's, not a reading of behaviour. // // The cycle alternates LEFT and RIGHT because each clip is one stride: a // walk is Right -> Left -> Right forever, and the transitions in and out of // stand / run / reverse each have their own handed pair so the mech always // leaves a cycle on the correct foot. // enum MechAnimationState { StandingAnimation = 0x00, RightStandToWalkAnimation = 0x01, RightWalkForwardAnimation = 0x02, LeftWalkForwardAnimation = 0x03, RightWalkToStandAnimation = 0x04, LeftWalkToStandAnimation = 0x05, RightWalkToRunAnimation = 0x06, LeftWalkToRunAnimation = 0x07, RightRunAnimation = 0x08, LeftRunAnimation = 0x09, RightRunToWalkAnimation = 0x0a, LeftRunToWalkAnimation = 0x0b, RightStandToReverseAnimation = 0x0c, LeftStandToReverseAnimation = 0x0d, RightReverseAnimation = 0x0e, LeftReverseAnimation = 0x0f, RightReverseToStandAnimation = 0x10, LeftReverseToStandAnimation = 0x11, LeftWalkToGimpAnimation = 0x12, RightWalkToGimpAnimation = 0x13, LeftGimpAnimation = 0x14, RightGimpAnimation = 0x15, LeftGimpToStandAnimation = 0x16, RightGimpToStandAnimation = 0x17, FallForwardAnimation = 0x18, FallBackwardAnimation = 0x19, FallLeftAnimation = 0x1a, FallRightAnimation = 0x1b, CrashAnimation = 0x1c, AnimationCount = 0x1d, // // The clip ARRAY is larger than the name table. Slot 0x20 holds the // bump/crash clip (mech+0x64c) bound on a hard wall impact, so the // array runs to 0x21 entries even though only 0x1d are named. // AnimationSlotCount = 0x21 }; // // A WARNING ABOUT THE NAMES ABOVE, which are verbatim from the binary's table // and therefore authoritative as NAMES -- but are NOT a reliable guide to what // each slot actually plays. // // The clip loader's slot assignments (LoadLocomotionClips) disagree with them. // Slot 0x0e takes the "rwr" run-to-walk clip while the table calls 0x0e // RightReverseAnimation; the reverse gait actually lives at 0x10-0x15 // (sbr/sbl entry, bbr/bbl cycle, bsr/bsl exit); and the states the gait // machine alternates between while walking forward are 6 and 7, not the // pair the "WalkForward" names suggest. // // Read the slot map in MECH2.NOTES.md when the question is "what does this // state play"; read the enum when the question is "what did the original call // this index". Conflating the two is how BT411 shipped a reverse gait that // played its coming-to-a-halt frames mid-cycle. // //########################################################################### //############################## Mech ############################### //########################################################################### class Mech: public JointedMover { //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Message Support -- the authentic roster, read from the binary's own // handler table (ids and NAME STRINGS live at 0x10bbf0../0x50c146..): // 0x12 TakeDamage (Entity overlay), 0x14 PlayerLink (Entity overlay), // then the mech's own block from Entity::NextMessageID. // public: enum { RealMaxSpeedMessageID = Entity::NextMessageID, // 0x15 @0049f604 BalanceCoolantMessageID, // 0x16 @0049f728 SetBurningStateMessageID, // 0x17 @0049f674 ClearBurningStateMessageID, // 0x18 @0049f700 EjectPilotMessageID, // 0x19 @0049f854 DuckRequestMessageID, // 0x1A @0049fa00 NextMessageID }; // // 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; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Attribute Support -- the ENTITY-level rows the cockpit binds without a // subsystem prefix (the radar trio the map() primitive reads, the speed // pair the speedometer arc reads, and the duck/crouch state selector). // Chained onto JointedMover's index. // public: enum { RadarRangeAttributeID = JointedMover::NextAttributeID, RadarLinearPositionAttributeID, RadarAngularPositionAttributeID, LinearSpeedAttributeID, MaxRunSpeedAttributeID, DuckStateAttributeID, // // The 3-D renderer reads this BY NAME: DPLRenderer::SetupCull // (CODE/RP/MUNGA_L4/L4VIDEO.CPP:4990) fetches // "EyepointRotation" off the viewpoint entity and, for a // JointedMover, composes it with the siteeyepoint segment to // build worldToEyeMatrix. Without it the engine Fails every // frame. The member already existed -- only the publication // was missing. // EyepointRotationAttributeID, // // Bound BY NAME by an authored AttributeWatcher (BTL4.RES); the // shipped binary carries the same string in its pool. // UnstablePercentageAttributeID, CollisionSpeedAttributeID, DistanceToMissileAttributeID, FootStepAttributeID, IncomingLockAttributeID, // // The remaining names BTL4.RES binds watchers to. The two // state indicators already existed as members -- only the // publication was missing, same as EyepointRotation. // AnimationState is bound 307 times in the resource: it is how // the audio system follows the gait. // CollisionStateAttributeID, CollisionNormalAttributeID, AnimationStateAttributeID, ReduceButtonAttributeID, NextAttributeID }; static const IndexEntry AttributePointers[]; static AttributeIndexSet AttributeIndex; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // 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); // //-------------------------------------------------------------------- // The gait transition machine (mech2.cpp). // // Set*Animation binds a channel's clip player to the clip for a state // and records the state; *Transition is the shared tail every handler // ends in (bind the next state, then spend the leftover time in it). // // *ClipFinished are the callbacks the clip players invoke at end of // clip -- hence static, taking the mech explicitly. They are // RE-ENTRANT BY CONTRACT: each re-arms its channel and advances the // carryover itself, returning the distance that carryover covered, so // SequenceController::Advance folds the result into its own return. //-------------------------------------------------------------------- // void SetLegAnimation(int state); void SetBodyAnimation(int state); Scalar LegTransition(int next_state, Scalar advance_time, int move_joints); Scalar BodyTransition(int next_state, Scalar advance_time, int move_joints); static Scalar LegClipFinished( Mech *mech, unsigned callback_arg, Scalar carryover, int move_joints); // // The collision-damage economy (binary @0049ffcc): type-0 damage is // diverted here by the damage hub and rattles internal subsystems -- // armor is never touched by a collision. // void DistributeCollisionDamage(Damage *damage); // // The per-contact collision responder (binary @004abb40) -- the // override of the engine's protected virtual. Base resolution plus: // the separating-contact gate, the mech-vs-mech ram dispatch, the // CulturalIcon crunch, and the 0.00123f walk-through sentinel for // crushable props. See MECH4.NOTES.md for the decode. // void ProcessCollision( Scalar time_slice, BoxedSolidCollision &collision, const Point3D &old_position, Damage *damage); int collisionTemporaryState; // binary mech+0x44c -- the frame's // contact accumulator; pushed into // collisionState once per frame static Scalar BodyClipFinished( Mech *mech, unsigned callback_arg, Scalar carryover, int move_joints); // //-------------------------------------------------------------------- // The gait clip loader (mech2.cpp). ResolveAnimationClip maps the // model's animation prefix + a 3-char suffix to a clip resource; // MeasureClipStride binds a loaded slot and integrates its keyframe // strides; LoadLocomotionClips fills animationClips[] and measures // every gait constant from the clips themselves. //-------------------------------------------------------------------- // ResourceDescription::ResourceID * ResolveAnimationClip(const char *prefix, const char *suffix); void MeasureClipStride(int slot, Scalar *total, Scalar *last_key); void LoadLocomotionClips(ModelResource *model); // // The INTERIOR twin (binary @004a86c8): the identical loader over the // 'i'-suffixed clip table (swri, wwri, ...). The interior clips // animate the inside-view skeleton -- they never bind the hip -- and // the MASTER (viewpoint) mech loads them; replicants load the // exterior set; the L4VIEWEXT env (a 1995 dev switch) forces the // exterior set on a master for external-camera work. // void LoadLocomotionClipsExt(ModelResource *model); int LoadClipSlot(int slot, const char *prefix, const char *suffix); // //-------------------------------------------------------------------- // The myomer drive contract (myomers.cpp). The mech's BASE speed // scalar is reverseStrideLength (binary mech+0x34C -- the measured // run-cycle speed); the myomers normalise their output by it and // RAISE the run cap (runSpeedMax, +0x7a0) to their available output // at the top gear. //-------------------------------------------------------------------- // Scalar BaseSpeedOf() { Check(this); return reverseStrideLength; } Scalar RunSpeedMaxOf() { Check(this); return runSpeedMax; } void RaiseRunSpeedMax(Scalar candidate) { Check(this); if (candidate > runSpeedMax || runSpeedMax >= 1.0e8f) { runSpeedMax = candidate; } } // //-------------------------------------------------------------------- // The per-frame gait entry points (mech2.cpp). Each advances its // channel's clip by the frame's time slice -- slewing the channel's // cycle speed toward its demand, driving the state machine at clip // boundaries through the *ClipFinished callbacks -- and returns the // cycle DISTANCE covered, which is what locomotion integrates. // // The leg version always moves the joints; the body version takes // move_joints explicitly so the caller can run it as a pure stride // measurement. (The airborne flavours @004a5bf8/@004a71f4 are a // later increment.) //-------------------------------------------------------------------- // Scalar AdvanceLegAnimation(Scalar time_slice); Scalar AdvanceBodyAnimation(Scalar time_slice, int move_joints); // //-------------------------------------------------------------------- // The LIMP flavours (mech2.cpp). Selected instead of the pair above // while the movement mode is 3 (left-leg limp) or 4 (right) and the // model carries the limp clip set. SETTLED from the binary after two // conflicting donor readings: the mode test is mech+0x40 -- the same // field the damage model documents as "limp gait graphic (left 3 / // right 4)" -- and NOT a jump-jet system. // // The normal advancers treat states 0x16-0x1b as the RESET group, so // the limp flavours must be selected while limping or the limp figure // gets its skeleton neutralized mid-cycle. //-------------------------------------------------------------------- // Scalar AdvanceLegAnimationGimp(Scalar time_slice); Scalar AdvanceBodyAnimationGimp(Scalar time_slice, int move_joints); Scalar GimpLegClipFinished(Scalar carryover); Scalar GimpBodyClipFinished(Scalar carryover, int move_joints); // // The respawn heal-and-move (binary @0049fb74): reposition the SAME // entity at the drop-zone origin, kill all motion, clear the death // latch, heal every hull zone and sweep the roster through // DeathReset. Shared by the initial drop-in and the respawn. // void Reset(const Origin &origin, Logical full_reset); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // 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; } // // The death-transition test (binary @0049fb54, mech.cpp): True while // the simulation state is 2 or 9 -- the two death-transition states // in the MovementMode ledger. Multiple families key off it: update // records strip their motion bits while it holds (@004a4c54), motion // stops integrating (mech4 dead-reckon head), and the spectator // camera director cuts AWAY from a mech the moment it starts dying // (BeABTDirector @004c1230). Takes the ENTITY view because callers // hold vehicles as Entity* (the state field is the Simulation base's). // static Logical InDeathTransition(Entity *vehicle); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // 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 MOVEMENT MODE (the entity mode alarm, // binary mech+0x2c, level cell +0x40 -- entity-base machinery, the // engine seeds it from the class descriptor @0041bdf0) at level >= 9 // = killed. 5.3.121 CORRECTION: the old claim that the damage code // wrote the @0x714 body-graphic alarm was a mis-anchor; the binary // TakeDamage kill writes are FUN_0041bbd8(mech+0x2c, 9) at // @0x49c88f/@0x49c8c5/@0x49c83c. Death-SEQUENCE membership is the // narrower Mech::InDeathTransition (modes 2/9, @0049fb54); the fall // variants 5-8 sit between (armed as crash clips by the body // machine) -- their writer is still undecoded. // Logical IsMechDestroyed() { Check(this); return (int)GetSimulationState() >= 9; } // // The live torso twist (the cylinder table's rotate-with-torso rows // follow it). NULL-safe: 0 with no torso subsystem. // Scalar CurrentTorsoTwist(); // // The coolant-share renormalizer (binary @0049f788): share = // priority / total across the heatable chain, blipping each // heatable's balance alarm with the change direction. PUBLIC: // Condenser::MoveValve calls it (binary callers: the 0x16 handler, // the ctor tail, Reset, and the valve stepper @004ae464). // void RedistributeCoolantShares(); // // The combat-effectiveness census (binary @0049fa1c; see the // member block). Runs at the top of every Simulate frame. // void UpdateCombatEffectiveness(); // // The super stop (master perf @0x4aa353) and the instability // accumulator (@0x4aa5f6 tail); both run inside Simulate. // void UpdateSuperStop(Scalar time_slice, Scalar speed_demand); void UpdateInstability(Scalar speed_demand); void UpdateTelemetryRings(Scalar time_slice); void UpdateMobilityScale(); 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 myomerSubsystems; // binary mech+0x7ac 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; // // GAIT ANIMATION (mech2.cpp). Two parallel channels, each a clip // player plus a state alarm holding the current MechAnimationState: // // LEG channel -- the locally-simulated gait. Its transitions read // the LIVE commanded speed from the controls mapper. // BODY channel -- the displayed-motion gait, whose advanced distance // is what feeds the mech's forward motion. Its // transitions read bodyTargetSpeed instead, so a // dead-reckoned or networked mech walks correctly // without a local controls mapper. // // The alarms ARE the state (binary mech+0x3b0 / +0x728 mirror the // alarm level); read them with GetLevel rather than keeping a copy. // AlarmIndicator legStateAlarm; AlarmIndicator bodyStateAlarm; Scalar legCycleSpeed; // channel-A cycle speed Scalar bodyCycleSpeed; // channel-B cycle speed Scalar forwardCycleRate; // cycle-speed slew rate Scalar gimpCycleRate; // slew rate while limping Scalar standSpeed; // "moving at all" threshold Scalar gimpSpeedMax; // limp cycle speed cap Scalar gimpStrideLength; // limp clip length (NEGATIVE // as authored -- the machines // fold the sign) Scalar globalTimeScale; // multiplies every increment // // Clip handle per gait SLOT (binary mech+0x5cc, indexed by state). // // SIZED 0x21, NOT AnimationCount. The name table stops at 0x1d but // the array does not: slot 0x20 (mech+0x64c) is the bump/crash clip // the mech binds on a hard wall impact. Sizing this by the enum // leaves that slot off the end. // int animationClips[AnimationSlotCount]; // // The OPTIONAL limp clip set (slots 22-27). LoadLocomotionClips // probes for the "wgl" clip; a model without it leaves hasGimpClips 0 // and those slots unfilled, so the limp machine must never be entered // for such a mech. (These four measured figures ARE the limp's -- // unlike gimpSpeedMax/gimpStrideLength above, which despite the names // are measured from the REVERSE clips. See MECH2.NOTES.md.) // int hasGimpClips; int squatCapable; // +0x584: squ/sqd resolved int turnCapable; // +0x588: trn resolved -- the // turn-in-place dispatcher's gate int usingExteriorClips; // +0x57c: L4VIEWEXT forced // the exterior set on a master Scalar gimpLeftSpeedMax; Scalar gimpRightSpeedMax; Scalar gimpLeftStrideLength; Scalar gimpRightStrideLength; Scalar gyroRumbleTimer; // binary mech+0x5c4; the // clip loader zeroes it // // Per-frame gait state (the Advance* entry points). // // idleStrideScale scales ONLY the idle/transition clip group's // advance rate (binary +0x5ac); the cycles use their own // cycle/stride ratio instead. UNSOURCED -- nothing observed sets // it yet; defaulted 1. // // runSpeedMax (+0x7a0) caps the run cycle's upward slew the way // walkStrideLength caps the walk's. SOURCED (2026-08-03): the // MYOMERS write it -- each myomer's RegisterMaxOutput raises it to // AvailableOutput(top gear), so the mech's true top speed is a // property of its muscle assembly, degrading with heat and damage. // The huge ctor default stands until the first myomer registers. // // The two reset latches and the death latch are one-shots the // fall/reset clip group clears; motionEventName/Armed are cleared // there too (their consumer is a later TU). // Scalar idleStrideScale; Scalar runSpeedMax; CString motionEventName; int motionEventArmed; int deathAnimationLatched; int legResetLatch; int bodyResetLatch; // // STAGED (binary @004a4c54, inline in the original mech.hpp): the // action-request bits are the update-record request mask -- setting // bit 3 (mask 8) asks the replication layer to emit the leg-state / // stability record. Our replication emitter is not reconstructed, // so the request has nowhere to go yet; the call sites keep the // binary's shape. // void ForceUpdate(int /*record_mask*/) { Check(this); } // // The movement mode (binary mech+0x40 == the simulation state): // 3/4 = left/right leg limp, 5-8 = the falls/deaths, 2||9 = the // death transition. Every gait read goes through here so the // BT_FORCE_LIMP bring-up hook (set once in the ctor) can exercise // the limp machinery before the damage model's limp hook exists. // int MovementMode() const { Check(this); return (limpModeOverride != 0) ? limpModeOverride : (int)((Mech *)this)->GetSimulationState(); } int limpModeOverride; // DEV: 3/4 from BT_FORCE_LIMP; 0 = off // // THE TELEMETRY RINGS (binary mech+0x7e0/+0x7ec/+0x7f8/+0x804/ // +0x810). Pushed once per master frame from the local velocity, // the turn rate and the frame slice; reduced into the derived // acceleration below. Indexed by TelemetryFilter. // enum TelemetryFilter { VelocityZFilter, // +0x7e0 VelocityYFilter, // +0x7ec -- FED BUT NEVER REDUCED (verbatim) VelocityXFilter, // +0x7f8 TurnRateFilter, // +0x804 TimeSliceFilter, // +0x810 TelemetryFilterCount }; AverageOf telemetryFilter[TelemetryFilterCount]; // // The previous frame's reduced values (binary +0x81c/+0x820/ // +0x824/+0x828), the other half of the difference quotient. // Scalar previousOlympicVelocityZ; Scalar previousMeanVelocityX; Scalar previousMeanTurnRate; Scalar previousMeanVelocityZ; // // The derived angular acceleration (binary +0x1ec), alongside // bodyAcceleration (+0x1dc, copied to +0x82c for the instability). // Scalar bodyTurnAcceleration; 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) Scalar lastInflictingDamage; // the killing-blow // magnitude (kill score) int deathTransitionDone; // the once-per-death latch // (binary movementMode 2||9) DamageLookupTable *damageLookupTable; // binary mech+0x444 -- the // cylinder hit-location table // // Cockpit-published state (bound by name from L4GAUGE.CFG): the radar // scale + the pointers the map gauge follows, and the duck selector. // The position/angle pointers are the 1995 shape -- the gauge holds a // pointer TO the pointer and re-reads the live transform every frame. // Scalar radarRange; Point3D *radarLinearPosition; Quaternion *radarAngularPosition; int duckState; // // THE DUCK SYSTEM (master perf @004a9f61..@004aa155, disasm-decoded). // // duckRequestLatch (+0x398) ONE-SHOT: set by the DuckRequest // message handler (@0049fa00, request > 0), consumed -- and // CLEARED -- every master frame after the arm check; also // cleared by Reset. Only a latched frame may bind squat clips. // duckPhaseRequest (+0x3f8) 0 none / 1 duck-down / 2 duck-up, // recomputed every frame from the raw simulation state, the // mapper's duck button cell, the leg gait state and the duck // analog demand (deadzone 1e-4 @0x4ab16c). // mobilityScale (+0x79c) NOT an analog input (5.3.121, from // the master perf C): recomputed each frame as the MAX leg // effectiveness (item +0x31c) over the +0x7ac leg roster, // it scales the throttle command, zeroes the turn command // when ~0, and the duck-down pick reads it as the "legs // still work" gate. DERIVED since 5.3.129 (the [T1] hold is // retired): Mech::UpdateMobilityScale maxes the myomer // chain's speedEffect and applies it to the mapper. // collisionVolumeState (+0x4c4) the volume alarm: level 1 = // STANDING, 0 = DUCKED. On an edge the collision template's // maxY (BoxedSolid +0xc -- the box TOP; the ground probe's // minY is untouched) swaps between the two ctor-captured // heights; any other level Fails ("Whoa! Bad Collision // Volume State!", MECH4.CPP line 417). // standing/duckedVolumeHeight (+0x518/+0x51c) captured at ctor // time: template maxY, and 0.6 x it (the double @004a2d38). // int duckRequestLatch; int duckPhaseRequest; Scalar mobilityScale; AlarmIndicator collisionVolumeState; Scalar standingVolumeHeight; Scalar duckedVolumeHeight; // // The streamed movement-mode request counters (binary +0x334 // DefaultState / +0x338 GimpLeft / +0x33c GimpRight -- the // authentic state names, traced by the master perf as it consumes // them). They are fed ONLY over the wire (the creation-stream // ctor @004b3778 reads them at record +0x1c0.., the update-record // appliers likewise); the LOCAL damage path writes the mode alarm // directly. Zeroed by the ctor (@004a1674) and Reset (@0049fb74). // int defaultStateRequests; int gimpLeftRequests; int gimpRightRequests; // // The real-max-speed share (binary +0x7a4/+0x7a8; the VALUE itself // is +0x7a0 == our existing runSpeedMax -- 5.3.127 CORRECTION, the // 5.3.122 'realMaxSpeed' member was a duplicate of that same cell). // A mech that computed its OWN cap has runSpeedMaxKnown set -- the // binary sets it in the ctor's Myomers sweep (part_012.c:15874: // walk mech+0x7ac, FUN_004b8ef0 per item, then +0x7a4 = 1), i.e. // exactly our Myomers -> RaiseRunSpeedMax path -- and it SENDS the // value once (message 0x15, latch +0x7a8) while ignoring incoming // shares; a mech without myomers ADOPTS the streamed value // (handler @0049f604). The send itself rides the EntityManager // stream path (@0041f640) and waits on the update-record feed. // int runSpeedMaxKnown; int runSpeedMaxSent; // // The combat-effectiveness census (binary @0049fa1c -> mech+0x414, // recomputed at the top of every master frame). INEFFECTIVE when: // working weapons < minimumWeaponCount (mech+0x448, ctor-hardcoded // 2; a weapon works when not destroyed and not a dry launcher -- // fire state NoAmmoState), OR no working generator (not destroyed, // generator state != GeneratorFailed -- the binary's +0x210 != 4), // OR the bank's coolant fraction < 0.05 (@0049fb50), OR limping // without the crouch cell held. Consumer: the EjectPilot gate -- // you can only punch out of a crippled mech. // int combatIneffective; int minimumWeaponCount; // // THE SUPER STOP (binary +0x3f4 / +0x344 / +0x5b8 / +0x5bc / // +0x5c4, master perf @0x4aa353-0x4aa393). Hauling the throttle // negative while still rolling forward is a hard brake, not a // reverse: the mech swaps its acceleration rate from the authored // forward figure to the authored superStopAcceleration and the // gyro throws the cockpit about. // // superStopping +0x3f4, the latch (an EDGE drives the kick) // bodyAccelRate +0x344, the live rate the gait slews by // forwardAccelRate +0x5b8, model rec+0x44 (== maxAcceleration) // superStopRate +0x5bc, model rec+0x48; the authored -1.1 // sentinel (@0x4ab174) means THE WHOLE SYSTEM IS DISABLED // superStopShudderTimer +0x5c4, the 0.4s pitch-kick cadence // int superStopping; Scalar bodyAccelRate; Scalar forwardAccelRate; Scalar superStopRate; Scalar superStopShudderTimer; // // THE INSTABILITY MODEL (binary +0x784..+0x798, all streamed from // the model resource rec+0x80..0x94 -- and the resource's own field // names say what each term is). unstablePercentage (+0x3f0) is the // sum, clamped to 1, republished to the gyro every frame. // // // The derived acceleration vector (binary +0x82c, written by the // master perf's ring-buffer tail). Holds zero until that brick // lands, which leaves the acceleration instability term inert. // Vector3D bodyAcceleration; Scalar maxUnstableAcceleration; Scalar unstableAccelerationEffect; Scalar unstableGunTheEngineEffect; Scalar unstableSuperStopEffect; Scalar unstableHighVelocityEffect; Scalar unstableStopedTurnEffect; void DuckRequestMessageHandler(Receiver::Message *message); void RealMaxSpeedMessageHandler(Receiver::Message *message); void SetBurningStateMessageHandler(Receiver::Message *message); void ClearBurningStateMessageHandler(Receiver::Message *message); void BalanceCoolantMessageHandler(Receiver::Message *message); void EjectPilotMessageHandler(Receiver::Message *message); void SetStandingCollisionVolume(); // binary @004ac04c... see .CPP void SetDuckedCollisionVolume(); // // STAGED VALUE (the instability model is not reconstructed). // The mech streams the authored unstable* effect constants in its // resource (maxUnstableAcceleration and friends) but nothing yet // computes how close the mech is to going over. It is PUBLISHED // because an authored AttributeWatcher in BTL4.RES binds // "UnstablePercentage" by name and the engine Fails outright when // the name does not resolve (WATCHER.CPP:141). Holds 0 = stable // until the model lands. // Scalar unstablePercentage; // // The AUDIO watcher set, staged alongside unstablePercentage. // BTL4.RES binds exactly six attribute watchers by name -- // UnstablePercentage, CollisionSpeed, FootStep, IncomingLock, // DistanceToMissile (Mech) and SpeedEffect (Myomers) -- and the // engine Fails outright on any that does not resolve. // These are the sound triggers: impact volume, footfalls, the // missile-lock warning, the myomer whine. // // TYPES ARE PROVISIONAL. AttributeWatcherOf reads // *(T*)attributePointer (WATCHER.HPP:288) and the instantiation // comes from the resource, which we cannot read off the string // pool. Nothing drives these yet, so nothing ever changes and no // watcher can fire -- settle the types WITH the models that write // them (collision response, the gait FSM, the missile threat // track), not before. // Scalar collisionSpeed; Scalar distanceToMissile; int footStep; int incomingLock; UnitVector collisionNormal; // staged with collisionSpeed int reduceButton; // staged: cockpit button feed // // 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. // // // 60 ints carved out for the gait channel above (2 AlarmIndicators = 4, // 8 Scalars, animationClips[0x21] = 33, the optional limp set = 6, the // per-frame gait state = 9 counting CString loosely); 191 -> 128. The // block is a layout BUDGET, not offset-exact -- MECH-LAYOUT.md holds // the real offsets. // int reservedState[128]; }; #endif