Myomers::MyomersSimulation (@004b8d18) reconstructed, five blocks in the binary's order, and it answers a question left open since 5.3.94: runSpeedMax (+0x7a0) IS WRITTEN BY THE MYOMERS. Each myomer's RegisterMaxOutput (@004b8ef0) raises the mech's run-cycle cap to its full-throttle AvailableOutput -- so the mech's true top speed is a property of its MUSCLE ASSEMBLY, and it degrades live as heat crosses the degradation band and as the myomer's own zone accumulates damage. The 1e9 ctor sentinel now stands only until the first myomer registers. The drive computation (@004b8ac0): base = the mech base speed (reverseStrideLength, mech+0x34C) scaled by the gear ratio -- input voltage over the RECOMMENDED gear -- with a quadratic thermal bleed across the degradation band, zero past failure, and the subsystem's wear taken off the top. speedEffect republishes per frame as a 0..1 fraction of full drive: input clamped to the SELECTED gear then the top gear, through AvailableOutput, normalised by the base speed. AND THE MUSCLES HEAL: while a myomer has NO VOLTAGE and its zone is not destroyed, it hands its own zone a NEGATIVE explosive tick (-0.011, 0xbc343958) -- unloaded muscle knits itself back together. The donor's history records this block as the fix for "torso twist dead after respawn" (the Torso mirrors this subsystem's electrical level; with the base machine never running, a myomer that lost power in the death window sat at NoVoltage forever) -- and our runs respawn now, so the fix is live where it matters. Registered on the master (the sibling gate); the base electrical machine runs FIRST and unconditionally, per the donor's hard-learned ordering note. Not carried over: the port's BT_FORCE_SEEK / repair-test bench scaffolding. Live run clean; the gait's speed sequence is unchanged (7.31972, 26.6726, ...) -- the muscle cap sits above the demanded 26.9 on a healthy Mad Cat, exactly as it should. Stub census: 18 across 12 files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
934 lines
34 KiB
C++
934 lines
34 KiB
C++
//===========================================================================//
|
|
// 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 <jmover.hpp>
|
|
# endif
|
|
|
|
# if !defined(RETICLE_HPP)
|
|
# include <reticle.hpp>
|
|
# endif
|
|
|
|
# if !defined(ALARM_HPP)
|
|
# include <alarm.hpp>
|
|
# endif
|
|
|
|
# if !defined(STATE_HPP)
|
|
# include <state.hpp>
|
|
# endif
|
|
|
|
# if !defined(CHAIN_HPP)
|
|
# include <chain.hpp>
|
|
# endif
|
|
|
|
# if !defined(AVERAGE_HPP)
|
|
# include <average.hpp>
|
|
# endif
|
|
|
|
# if !defined(CSTR_HPP)
|
|
# include <cstr.hpp>
|
|
# endif
|
|
|
|
# if !defined(SEQCTL_HPP)
|
|
# include <seqctl.hpp>
|
|
# endif
|
|
|
|
# if !defined(NAMEFILT_HPP)
|
|
# include <namefilt.hpp>
|
|
# endif
|
|
|
|
# if !defined(ROTATION_HPP)
|
|
# include <rotation.hpp>
|
|
# 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
|
|
{
|
|
//
|
|
// 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; }
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Locomotion parameters (read by MechControlsMapper::InterpretControls to
|
|
// shape the demands, and by the drive in Simulate). reverseStrideLength is
|
|
// the top/run cycle speed the throttle scales (the naming is the 1995
|
|
// field's; LoadLocomotionClips measures it from the run clips), walkStride
|
|
// Length the walk speed. Turn rates are radians/sec.
|
|
//
|
|
public:
|
|
Scalar GetReverseStrideLength() const { Check(this); return reverseStrideLength; }
|
|
Scalar GetWalkStrideLength() const { Check(this); return walkStrideLength; }
|
|
Scalar GetForwardThrottleScale() const { Check(this); return forwardThrottleScale; }
|
|
void SetForwardThrottleScale(Scalar s){ Check(this); forwardThrottleScale = s; }
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Eyepoint / aim-ray composition. The pilot's torso-elevation aim (stick
|
|
// pitch) does NOT tilt any skeleton joint on this mech family -- it pitches
|
|
// the COCKPIT EYE and the weapon boresight (pixel-calibrated in the BT411
|
|
// reverse-engineering: "pitch does not work" turned out to mean nothing
|
|
// consumed Torso's currentElevation, not that a joint was un-animated).
|
|
// Composed each frame in Simulate; DPLEyeRenderable / the aim ray read it.
|
|
//
|
|
public:
|
|
const EulerAngles&
|
|
GetEyepointRotation() const { Check(this); return eyepointRotation; }
|
|
|
|
//
|
|
// The current target (the binary's mech target slot @0x388): read by
|
|
// the weapons' HasActiveTarget / UpdateTargeting and the fire path.
|
|
// The authentic writer is the reticle targeting step (mech4, the
|
|
// render wave); the dev harness sets it directly.
|
|
//
|
|
Entity*
|
|
GetTargetEntity() const { Check(this); return targetEntity; }
|
|
void
|
|
SetTargetEntity(Entity *target) { Check(this); targetEntity = target; }
|
|
|
|
//
|
|
// Damage entry (overrides the Entity handler): latches the attacker
|
|
// (mech+0x43c, read by the zone LOD router) and feeds the gyro
|
|
// cockpit bounce before chaining to the base zone routing. The
|
|
// cylinder unaimed-hit resolver (type-0x1d table) is a later wave.
|
|
//
|
|
void
|
|
TakeDamageMessageHandler(TakeDamageMessage *message);
|
|
|
|
//
|
|
// Damage-side death flag: the body graphic alarm (statusAlarm,
|
|
// binary @0x714) at level >= 9 = the death/fall state.
|
|
//
|
|
Logical
|
|
IsMechDestroyed() { Check(this); return statusAlarm.GetLevel() >= 9; }
|
|
|
|
//
|
|
// The live torso twist (the cylinder table's rotate-with-torso rows
|
|
// follow it). NULL-safe: 0 with no torso subsystem.
|
|
//
|
|
Scalar
|
|
CurrentTorsoTwist();
|
|
|
|
DamageLookupTable*
|
|
GetDamageLookupTable() const { Check(this); return damageLookupTable; }
|
|
|
|
//
|
|
// Look-state commit (called by the controls mapper on a look-button
|
|
// state change): re-aim the eyepoint from the model's authored look
|
|
// angles. look_state = MechControlsMapper::LookState.
|
|
//
|
|
void
|
|
CommitLookState(int look_state);
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Local Data
|
|
//
|
|
protected:
|
|
//
|
|
// Cached subsystem back-pointers (also held in the base roster).
|
|
//
|
|
Subsystem *sensorSubsystem;
|
|
Subsystem *gyroSubsystem;
|
|
Subsystem *sinkSourceSubsystem; // the real Torso
|
|
Subsystem *hudSubsystem;
|
|
SubsystemMessageManager *messageManager;
|
|
int weaponCount;
|
|
|
|
//
|
|
// Capability sub-rosters (Socket views onto the subsystem roster;
|
|
// populated per-frame -- phase 5).
|
|
//
|
|
ChainOf<Subsystem*> controllableSubsystems;
|
|
ChainOf<Subsystem*> watchedSubsystems;
|
|
ChainOf<Subsystem*> heatableSubsystems;
|
|
ChainOf<Subsystem*> weaponRoster;
|
|
ChainOf<Subsystem*> 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
|
|
AverageOf<Scalar> telemetryFilter[5];
|
|
CString resourceNameA;
|
|
CString resourceNameB;
|
|
CString resourceNameC;
|
|
|
|
//
|
|
// Locomotion state (Phase 5.3). Turn rates in rad/s, speeds/strides in
|
|
// world-units/s. reverseStrideLength = top (run) speed; walkStrideLength
|
|
// = walk speed; reverseSpeedMax = the low-speed turn-rate gate; forward
|
|
// ThrottleScale multiplies the forward demand; bodyTargetSpeed = the
|
|
// demanded speed; currentBodySpeed = the accel-tracked actual speed.
|
|
//
|
|
Scalar walkingTurnRate;
|
|
Scalar runningTurnRate;
|
|
Scalar reverseStrideLength;
|
|
Scalar walkStrideLength;
|
|
Scalar reverseSpeedMax;
|
|
Scalar forwardThrottleScale;
|
|
Scalar maxBodyAcceleration;
|
|
Scalar bodyTargetSpeed;
|
|
Scalar currentBodySpeed;
|
|
|
|
//
|
|
// Composed each frame in Simulate: the look-state eye component
|
|
// (lookPitch/lookYaw, set by CommitLookState from the authored look
|
|
// angles) plus the Torso elevation.
|
|
//
|
|
EulerAngles eyepointRotation;
|
|
Scalar lookPitch;
|
|
Scalar lookYaw;
|
|
Entity *targetEntity;
|
|
EntityID lastInflictingID; // binary mech+0x43c -- last
|
|
// attacker (zone LOD router)
|
|
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;
|
|
//
|
|
// 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<T> 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
|