Two size errors (animationPrefix char[12] vs 4 bytes; a FICTITIOUS 64-byte skeletonName block -- the record has no skl name) skewed every member after word 0x10 by +2/+15 words and pushed the fieldXX block past the record end. Full writer map recovered (FUN_004a2da8 + Mover FUN_004238bc + Reticle FUN_00435ac8 chains) + runtime reader map (ctor pass @0x4a24d1-0x4a2968, capstone-verified). Struct rewritten as the exact overlay (static_assert- locked); all Pass-2 consumers renamed to real fields; retired: ctor + replicant + master-gait floor-25 hacks, the throttle guard, the fictitious classID stamp. Verified: accel=30 superStop=10 throttleAdj=1 (was ~1/OOB/0), deadbands byte-identical via named fields, full walk->run lifecycle clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
998 lines
50 KiB
C++
998 lines
50 KiB
C++
//===========================================================================//
|
|
// File: mech.hpp //
|
|
// Project: BattleTech Brick: Entity Manager //
|
|
// Contents: Mech -- the BattleMech player/AI entity (declaration) //
|
|
//---------------------------------------------------------------------------//
|
|
// Date Who Modification //
|
|
// -------- --- ---------------------------------------------------------- //
|
|
// --/--/95 ?? Initial coding. //
|
|
//---------------------------------------------------------------------------//
|
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
|
|
// PROPRIETARY AND CONFIDENTIAL //
|
|
//===========================================================================//
|
|
//
|
|
// RECONSTRUCTED from the shipped binary (Ghidra pseudo-C, BTL4OPT.EXE).
|
|
//
|
|
// The Mech class is the heart of the game and its implementation is split
|
|
// across mech.cpp .. mech4.cpp. This header declares the *whole* class so
|
|
// the recovered mech.cpp slice compiles; methods whose bodies live in the
|
|
// sibling translation units are marked [mech2] / [mech3] / [mech4].
|
|
//
|
|
// mech.cpp slice recovered here: 0x004a1232 .. 0x004a5027
|
|
// (bounded below by the inter-module gap 0x0049ffc8..0x004a1232 that
|
|
// separates it from btmssn.cpp, and above by mech2.cpp @0x004a5028.)
|
|
//
|
|
// Structural template: Red Planet's player vehicle entity VTV (RP/VTV.h),
|
|
// which -- like Mech -- derives from JointedMover and hosts a roster of
|
|
// streamed Subsystem objects. Surviving BT headers MECHDMG.HPP / MECHTECH.HPP
|
|
// supplied the DamageZone / MechTech shapes.
|
|
//
|
|
// Field offsets in comments are BYTE offsets into the shipped object
|
|
// (sizeof(Mech) == 0x854, see Mech::Make @004a2d48). The decomp addresses
|
|
// the object as int words, so this[0xNN] == byte offset 0xNN*4. Names are
|
|
// taken from VTV / the surviving BT headers where an analog exists and are
|
|
// inferred from usage otherwise (flagged TODO / "best-effort").
|
|
//
|
|
|
|
#if !defined(MECH_HPP)
|
|
# define MECH_HPP
|
|
|
|
#if !defined(JMOVER_HPP)
|
|
# include <jmover.hpp> // JointedMover -- engine base (MUNGA)
|
|
#endif
|
|
#if !defined(CONTROLS_HPP)
|
|
# include <controls.hpp>
|
|
#endif
|
|
#if !defined(DAMAGE_HPP)
|
|
# include <damage.hpp>
|
|
#endif
|
|
#if !defined(ALARM_HPP)
|
|
# include <alarm.hpp> // GaugeAlarm
|
|
#endif
|
|
#include <tool.hpp> // PlatformTool
|
|
#include <affnmtrx.hpp> // AffineMatrix
|
|
#include <average.hpp> // AverageOf<T>
|
|
#include <scalar.hpp> // Scalar
|
|
#include <reticle.hpp> // Reticle (the TargetReticle attribute struct)
|
|
|
|
#include "mechrecon.hpp" // reconstruction shim (proxies, artifact globals, type aliases)
|
|
|
|
//##################### Reconstruction type aliases #####################
|
|
// Names inferred by the decompiler mapped to the real engine types.
|
|
// (The actual aliases are now defined in mechrecon.hpp under the same guard.)
|
|
#if !defined(BT_RECON_TYPE_ALIASES)
|
|
# define BT_RECON_TYPE_ALIASES
|
|
typedef ReconMatrix Matrix34;
|
|
typedef ReconAlarm AlarmIndicator;
|
|
typedef ReconFiltered FilteredScalar;
|
|
#endif
|
|
|
|
//##################### Forward Class Declarations #######################
|
|
class Mech;
|
|
class Subsystem;
|
|
class MechSubsystem;
|
|
class MechWeapon;
|
|
class MechControlsMapper;
|
|
class MechTech;
|
|
class Mech__DamageZone; // see MECHDMG.HPP
|
|
class HitQuery; // weapon sweep query (mech4)
|
|
class Joint; // engine skeleton node (JOINT.h); ResolveJoint result
|
|
|
|
//
|
|
// Streamed-subsystem resource blob handed to each <Class>::CreateStreamedSubsystem.
|
|
// The recovered factory only passes it through, so an opaque alias suffices.
|
|
//
|
|
typedef void SubsystemResourceBlob;
|
|
|
|
//
|
|
// Per-shot impact descriptor consumed by Mech::ResolveWeaponImpact (mech4).
|
|
// Fields are those the recovered body reads.
|
|
//
|
|
struct ShotDescriptor
|
|
{
|
|
Vector3D dir; // +0x14 shot direction
|
|
Scalar damage; // computed impact damage
|
|
Scalar fieldAt(int) const { return Scalar(0); } // raw +0x240/0x244/0x248 reads
|
|
};
|
|
|
|
//###########################################################################
|
|
//######################### Mech::ModelResource #########################
|
|
//###########################################################################
|
|
//
|
|
// Parsed by Mech::CreateModelResource @004a2da8 from the "gamedata" notation
|
|
// block. Layout mirrors the float[] the decomp fills (pfVar3, a 200-byte /
|
|
// 0x32-word record). Indices in comments are the word index used by the
|
|
// parser (e.g. pfVar3+0x11 == MaxAcceleration).
|
|
//
|
|
struct Mech__ModelResource:
|
|
public JointedMover::ModelResource // == Mover__ModelResource, 0x40 bytes
|
|
{
|
|
//
|
|
// AUTHORITATIVE overlay of the 200-byte (0x32-word) segment-0xf record
|
|
// (task #4, 2026-07-11). Writer chain [T1]: FUN_004a2da8 (Mech) after
|
|
// FUN_004238bc (Mover, words 0x00-0x0F) + FUN_00435ac8 (Reticle, words
|
|
// 0x1A-0x1B). Runtime reader: the ctor pass @0x4a24d1-0x4a2968. The
|
|
// old struct had two size errors (animationPrefix char[12] vs 4 bytes;
|
|
// a FICTITIOUS char skeletonName[64]) that skewed every later member
|
|
// +2/+15 words and pushed the fieldXX block past the record end --
|
|
// every fieldXX read was out-of-bounds garbage (the forwardCycleRate
|
|
// "floor 25" era). There is NO skeleton-name block in this record.
|
|
//
|
|
char animationPrefix[4]; // 0x40 w0x10 "AnimationPrefix" (3 letters + NUL)
|
|
Scalar maxAcceleration; // 0x44 w0x11 "MaxAcceleration"
|
|
Scalar superStopAcceleration; // 0x48 w0x12 "SuperStopAcceleration"
|
|
Scalar throttleAdjustment; // 0x4C w0x13 "ThrottleAdjustment"
|
|
Scalar lookLeftAngle; // 0x50 w0x14 "LookLeftAngle" (degrees)
|
|
Scalar lookRightAngle; // 0x54 w0x15 "LookRightAngle"
|
|
Scalar lookFrontAngle; // 0x58 w0x16 "LookFrontAngle"
|
|
Scalar lookBackAngle; // 0x5C w0x17 "LookBackAngle"
|
|
Scalar walkingTurnRate; // 0x60 w0x18 "WalkingTurnRate" (degrees)
|
|
Scalar runningTurnRate; // 0x64 w0x19 "RunningTurnRate"
|
|
Scalar reticleX; // 0x68 w0x1A "ReticleX" (Reticle level; default 0)
|
|
Scalar reticleY; // 0x6C w0x1B "ReticleY"
|
|
Scalar relativeMechValue; // 0x70 w0x1C "RelativeMechValue"
|
|
int deathEffectResourceID; // 0x74 w0x1D "DeathEffect" (resolved id)
|
|
Scalar deathSplashDamage; // 0x78 w0x1E "DeathSplashDamage"
|
|
Scalar deathSplashRadius; // 0x7C w0x1F "DeathSplashRadius"
|
|
Scalar maxUnstableAcceleration; // 0x80 w0x20 "MaxUnstableAcceleration"
|
|
Scalar unstableAccelerationEffect; // 0x84 w0x21 "UnstableAccelerationEffect"
|
|
Scalar unstableGunTheEngineEffect; // 0x88 w0x22 "UnstableGunTheEngineEffect"
|
|
Scalar unstableSuperStopEffect; // 0x8C w0x23 "UnstableSuperStopEffect"
|
|
Scalar unstableHighVelocityEffect; // 0x90 w0x24 "UnstableHighVelocityEffect"
|
|
Scalar unstableStopedTurnEffect; // 0x94 w0x25 "UnstableStopedTurnEffect"
|
|
Scalar updatePositionDiffrence; // 0x98 w0x26 "UpdatePositionDiffrence"
|
|
Scalar updateTurnVelocityDiffrence;// 0x9C w0x27 "UpdateTurnVelocityDiffrence"
|
|
Scalar updateTurnDegreeDiffrence; // 0xA0 w0x28 "UpdateTurnDegreeDiffrence" (deg; ctor x pi/180)
|
|
Scalar timeDelay; // 0xA4 w0x29 "TimeDelay"
|
|
Vector3D cameraOffset; // 0xA8 w0x2A-0x2C "CameraOffset"
|
|
char shadowJointName[20]; // 0xB4 w0x2D-0x31 "ShadowJointName" (<=19 chars)
|
|
};
|
|
|
|
static_assert(sizeof(Mover::ModelResource) == 0x40, "Mover model record 0x40");
|
|
static_assert(sizeof(Mech__ModelResource) == 0xC8, "Mech model record 200 bytes");
|
|
static_assert(offsetof(Mech__ModelResource, maxAcceleration) == 0x44, "MaxAcceleration w0x11");
|
|
static_assert(offsetof(Mech__ModelResource, relativeMechValue) == 0x70, "RelativeMechValue w0x1C");
|
|
static_assert(offsetof(Mech__ModelResource, updatePositionDiffrence) == 0x98, "UpdatePositionDiffrence w0x26");
|
|
static_assert(offsetof(Mech__ModelResource, cameraOffset) == 0xA8, "CameraOffset w0x2A");
|
|
static_assert(offsetof(Mech__ModelResource, shadowJointName) == 0xB4, "ShadowJointName w0x2D");
|
|
|
|
//###########################################################################
|
|
//######################### Mech::UpdateRecord ##########################
|
|
//###########################################################################
|
|
//
|
|
// The Mech-level replication records (task #1, 2026-07-11). The record TYPE
|
|
// is the inherited Simulation::UpdateRecord `recordID` (u16 @+6) == the
|
|
// updateModel BIT INDEX that requested it (Simulation::WriteSimulationUpdate,
|
|
// SIMULATE.cpp:302-328). Writer @004a0c2c (slot 7, 9-case jump table) /
|
|
// reader @004a1232 (slot 6). Field layouts are byte-exact from the writer
|
|
// disasm (reference/decomp/mech_writeupdate_004a0c2c.disasm.txt) cross-mapped
|
|
// against the reader pseudocode (part_012.c:9576-9692). [T1]
|
|
//
|
|
// EVERY record's trailing Scalar is the controls-mapper's speedDemand
|
|
// (subsystemArray[0]+0x128), stored into mech+0x6b4 `bodyTargetSpeed` on BOTH
|
|
// sides -- the authentic replicant-gait feed (it is NOT a sequence number).
|
|
//
|
|
// type 0 (len 0x78): the full Mover pose record + the speedDemand tail.
|
|
struct Mech__PoseUpdateRecord:
|
|
public Mover::UpdateRecord // 0x74
|
|
{
|
|
Scalar speedDemand; // +0x74
|
|
};
|
|
|
|
// type 2 (len 0x14): speedDemand alone (sent when the commanded speed
|
|
// drifts from the last-replicated bodyTargetSpeed).
|
|
struct Mech__SpeedUpdateRecord:
|
|
public Simulation::UpdateRecord // 0x10
|
|
{
|
|
Scalar speedDemand; // +0x10
|
|
};
|
|
|
|
// type 3 (len 0x20): leg/body state + stability.
|
|
struct Mech__StateUpdateRecord:
|
|
public Simulation::UpdateRecord
|
|
{
|
|
int legResetLatch; // +0x10 <- 0x654 ; -> 0x658
|
|
int legState; // +0x14 <- legStateAlarm.currentLevel (0x3b0)
|
|
int stability; // +0x18 <- stabilityAlarm.currentLevel (0x4d8)
|
|
Scalar speedDemand; // +0x1c
|
|
};
|
|
|
|
// type 4 (len 0x2c): orientation + angular-velocity re-sync (pose delta).
|
|
struct Mech__ResyncUpdateRecord:
|
|
public Simulation::UpdateRecord
|
|
{
|
|
Scalar eulerX, eulerY, eulerZ; // +0x10 localOrigin.angularPosition as euler
|
|
Vector3D angularVelocity; // +0x1c live localVelocity.angularMotion (0x1d0)
|
|
Scalar speedDemand; // +0x28
|
|
};
|
|
|
|
// types 5 (knockdown) / 6 (death) / 7 (impact), all len 0x2c: heat +
|
|
// throttle + the ground-contact basis. Same payload, different side
|
|
// effects on each end.
|
|
struct Mech__FallUpdateRecord:
|
|
public Simulation::UpdateRecord
|
|
{
|
|
int heatLevel; // +0x10 <- heatAlarm.currentLevel (0x464)
|
|
int throttleState; // +0x14 <- 0x4a4
|
|
Vector3D fallDirection; // +0x18 <- 0x4a8 (ctor (0,0,1); name [T4])
|
|
Scalar fallScalar; // +0x24 <- 0x4b4 (name [T4])
|
|
Scalar speedDemand; // +0x28
|
|
};
|
|
|
|
// type 8 (len 0x18): the airborne/reverse cycle-rate selector flag.
|
|
struct Mech__AirborneUpdateRecord:
|
|
public Simulation::UpdateRecord
|
|
{
|
|
int airborne; // +0x10 <- 0x3f4
|
|
Scalar speedDemand; // +0x14
|
|
};
|
|
|
|
static_assert(sizeof(Simulation::UpdateRecord) == 0x10, "Simulation record header 0x10");
|
|
static_assert(sizeof(Mover::UpdateRecord) == 0x74, "Mover pose record 0x74");
|
|
static_assert(sizeof(Mech__PoseUpdateRecord) == 0x78, "type 0 record 0x78");
|
|
static_assert(sizeof(Mech__SpeedUpdateRecord) == 0x14, "type 2 record 0x14");
|
|
static_assert(sizeof(Mech__StateUpdateRecord) == 0x20, "type 3 record 0x20");
|
|
static_assert(sizeof(Mech__ResyncUpdateRecord) == 0x2c, "type 4 record 0x2c");
|
|
static_assert(sizeof(Mech__FallUpdateRecord) == 0x2c, "type 5/6/7 record 0x2c");
|
|
static_assert(sizeof(Mech__AirborneUpdateRecord) == 0x18, "type 8 record 0x18");
|
|
|
|
//###########################################################################
|
|
//########################## Mech::MakeMessage ##########################
|
|
//###########################################################################
|
|
//
|
|
// Creation message. Carries the three resource-name strings copied into the
|
|
// badge/color/insignia slots (this[0x211..0x213]) at +0x7c / +0x90 / +0xa4.
|
|
//
|
|
class Mech__MakeMessage:
|
|
public JointedMover::MakeMessage
|
|
{
|
|
public:
|
|
char resourceNameA[20]; // +0x7c -> this[0x211] (vehicle badge)
|
|
char resourceNameB[20]; // +0x90 -> this[0x212] (vehicle color)
|
|
char resourceNameC[20]; // +0xa4 -> this[0x213] (vehicle patch / insignia)
|
|
|
|
//
|
|
// Construction message, mirrored on VTV__MakeMessage (RP/VTV.h): the
|
|
// entity_ID form carrying the three resource-name strings copied into
|
|
// the badge/color/patch slots. Built by BTPlayer::CreatePlayerVehicle.
|
|
//
|
|
Mech__MakeMessage(
|
|
Receiver::MessageID message_ID,
|
|
size_t length,
|
|
const EntityID &entity_ID,
|
|
Entity::ClassID class_ID,
|
|
const EntityID &owner_ID,
|
|
ResourceDescription::ResourceID resource_ID,
|
|
LWord instance_flags,
|
|
const Origin &origin,
|
|
const Motion &velocity,
|
|
const Motion &acceleration,
|
|
const char *badge,
|
|
const char *color,
|
|
const char *patch
|
|
):
|
|
JointedMover::MakeMessage(
|
|
message_ID, length, entity_ID, class_ID, owner_ID,
|
|
resource_ID, instance_flags, origin, velocity, acceleration
|
|
)
|
|
{
|
|
Str_Copy(resourceNameA, badge, sizeof(resourceNameA));
|
|
Str_Copy(resourceNameB, color, sizeof(resourceNameB));
|
|
Str_Copy(resourceNameC, patch, sizeof(resourceNameC));
|
|
}
|
|
};
|
|
|
|
//###########################################################################
|
|
//############################## Mech ###############################
|
|
//###########################################################################
|
|
//
|
|
// The BattleMech entity. vtable @0050cfa8 (16 slots); slot 0 == ~Mech
|
|
// @004a452c. Engine-base slots (0x41xxxx / 0x42xxxx) are inherited from
|
|
// JointedMover/Mover/Entity and are NOT redefined here. In-module overrides:
|
|
// slot 0 (0050cfa8) ~Mech @004a452c [mech.cpp]
|
|
// slot 6 (0050cfc0) ReadUpdateRecord @004a122c [mech.cpp]
|
|
// slot 7 (0050cfc4) (pose write/simulate)@004a0c2c [mech.cpp gap]
|
|
// slot 15 (0050cfe4) (state/print) @004abb40 [mech3/4]
|
|
//
|
|
class Mech:
|
|
public JointedMover
|
|
{
|
|
friend class Mech__DamageZone; // per-zone damage code reads the roster / segment table
|
|
friend struct MechBaseLayoutCheck; // base-region layout locks (mech4.cpp; P3 STEP-6 audit)
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Shared Data Support
|
|
//
|
|
public:
|
|
static Derivation ClassDerivations; // @0050bdb4
|
|
static SharedData DefaultData; // @0050bde4
|
|
|
|
enum { MechClassID = 0xBB9 }; // stamped by CreateModelResource
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Subsystem roster -- streamed-class registry
|
|
//
|
|
// The constructor (@004a1674) walks the model's segment table and
|
|
// instantiates one Subsystem per segment, dispatching on the segment's
|
|
// streamed ClassID. ClassIDs confirmed against CLASSMAP.md are named;
|
|
// the rest are best-effort from the factory address + object size.
|
|
//
|
|
public:
|
|
enum SubsystemClassID {
|
|
CockpitClassID = 0xBBD, // ctor @004ae568 (size 0x230)
|
|
SensorClassID = 0xBBE, // ctor @004ae8d0 -> this[0x1f7]
|
|
CondenserClassID = 0xBC0, // ctor @004af408 (size 0x230)
|
|
GeneratorClassID = 0xBC1, // ctor @004b225c (CLASSMAP)
|
|
PoweredSubsystemClassID = 0xBC2, // ctor @004b0f74 (CLASSMAP)
|
|
MyomersClassID = 0xBC3, // ctor @004b1d18 (CLASSMAP)
|
|
GyroClassID = 0xBC4, // ctor @004b3778 -> this[0x14a]
|
|
SinkSourceClassID = 0xBC5, // ctor @004b6b0c -> this[0x10e]
|
|
ActuatorClassID = 0xBC6, // ctor @004b8fec
|
|
WeaponEmitterClassID = 0xBC8, // ctor @004bb120 (+weapon count)
|
|
JumpJetClassID = 0xBCB, // ctor @004bd5c4
|
|
MechWeaponClassID = 0xBCD, // ctor @004bc3fc (CLASSMAP)
|
|
MissileWeaponClassID = 0xBCE, // ctor @004bdcb4 (+weapon count)
|
|
BallisticWeaponClassID = 0xBD0, // ctor @004bcff0 (+weapon count)
|
|
MechControlsMapperID = 0xBD3, // ctor @0049bca4 -> this[0x10d]
|
|
GaussWeaponClassID = 0xBD4, // ctor @004bb888 (+weapon count)
|
|
MechTechClassID = 0xBD6, // ctor @004b7f94 -> this[0x16d]
|
|
LegSubsystemClassID = 0xBD8, // ctor @004b84dc
|
|
HeatableClassID = 0xBDC, // ctor @004ad228 (heat.cpp family; exact class TBD)
|
|
DisplayClassID = 0xBDE // ctor @004b8718
|
|
};
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Test Class Support
|
|
//
|
|
public:
|
|
Logical
|
|
TestInstance() const; // @004a4c7c
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// App / cockpit interface (called by the BT_L4 layer: btl4app/mppr/pb).
|
|
// Declarations only -- definitions live in mech.cpp (factory pass / bring-up).
|
|
//
|
|
public:
|
|
enum ResetMode { MissionReviewReset = 0 }; // reset-mode selector (Reset arg, value 0)
|
|
|
|
void SetMappingSubsystem(Subsystem *mapper); // btl4app.cpp:567
|
|
Logical GetMissionReviewMode(); // reads this+0x414 (btl4mppr.cpp:366)
|
|
void SetTargetRange(Scalar range); // writes this+0x404 (btl4mppr.cpp:407)
|
|
void Reset(const Origin &origin, int mode); // btl4pb.cpp:555 (FUN_0049fb74)
|
|
|
|
//~~~ Skeleton joint access (shared by Torso, Gyroscope) ~~~~~~~~~~~~~~~~
|
|
// Resolve a named skeleton node to its live Joint* via the PUBLIC segment
|
|
// path (GetSegment -> segment joint index -> JointSubsystem::GetJoint);
|
|
// the protected EntitySegment::jointPointer is never read. The Torso ctor
|
|
// (@004b6b0c) and Gyroscope ctor (@004b3778) inline this exact sequence
|
|
// (binary FUN_00424b60); hoisted here as the single reuse point.
|
|
Joint* ResolveJoint(const char *joint_name);
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Construction and Destruction
|
|
//
|
|
public:
|
|
typedef Mech__ModelResource ModelResource;
|
|
typedef Mech__MakeMessage MakeMessage;
|
|
typedef Mech__DamageZone DamageZone;
|
|
// (No UpdateRecord typedef: the Mech records are per-type structs --
|
|
// Mech__PoseUpdateRecord etc. above -- and a class-local UpdateRecord
|
|
// typedef is the silent-non-override trap: an override declared with
|
|
// the shadowing typedef does NOT override the engine virtual. See
|
|
// mechweap.hpp:174 / [[reconstruction-gotchas]].)
|
|
|
|
static Mech*
|
|
Make(MakeMessage *creation_message); // @004a2d48
|
|
|
|
Mech(
|
|
MakeMessage *creation_message,
|
|
SharedData &shared_data = DefaultData
|
|
); // @004a1674
|
|
~Mech(); // @004a452c (vtable slot 0)
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Resource authoring (offline tools)
|
|
//
|
|
public:
|
|
static ResourceDescription::ResourceID
|
|
CreateModelResource( // @004a2da8
|
|
ResourceFile *resource_file,
|
|
const char *model_name,
|
|
NotationFile *model_file,
|
|
const ResourceDirectories *directories,
|
|
ModelResource *model = 0
|
|
);
|
|
|
|
static Logical
|
|
CreateModelResourceStub( // @004a2d78 (best-effort)
|
|
ModelResource *model
|
|
); // stamps model->classID = MechClassID
|
|
|
|
typedef Logical (*FindNameFunction)(
|
|
const char *control_name, ControlsMapping *mapping);
|
|
|
|
static ResourceDescription::ResourceID
|
|
CreateControlMappingStream( // @004a3794
|
|
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
|
|
);
|
|
|
|
static ResourceDescription::ResourceID
|
|
CreateDamageZoneStream( // @004a474c
|
|
ResourceFile *resource_file,
|
|
const char *model_name,
|
|
NotationFile *model_file,
|
|
const ResourceDirectories *directories
|
|
);
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Simulation / replication (task #1: the Mech-level update records)
|
|
//
|
|
// The record type == the updateModel BIT INDEX (Simulation::
|
|
// WriteSimulationUpdate walks the mask, SIMULATE.cpp:302-328). Bits 0/1
|
|
// are the engine's (Mover pose / Entity damage-zone); 2..8 are Mech's.
|
|
// Request one with ForceUpdate(1<<bit); the emitter consumes-and-zeroes
|
|
// the mask, so each request produces exactly one record. [T1 senders
|
|
// mapped from the binary: 27 call sites, perf loop + events]
|
|
//
|
|
public:
|
|
enum {
|
|
MechSpeedUpdateModelBit = 2, // commanded-speed change
|
|
MechStateUpdateModelBit = 3, // leg/body state + stability
|
|
MechResyncUpdateModelBit = 4, // orientation/angular-vel delta
|
|
MechKnockdownUpdateModelBit= 5, // knockdown (SetBodyAnimation 0x20)
|
|
MechDeathUpdateModelBit = 6, // death
|
|
MechImpactUpdateModelBit = 7, // heat/impact state changed
|
|
MechAirborneUpdateModelBit = 8 // airborne/reverse cycle selector
|
|
};
|
|
|
|
//
|
|
// @004a4c54 -- Mech::ForceUpdate: OR record-request bits into the
|
|
// engine's Simulation::updateModel. A disabled/destroyed mech
|
|
// (simulationState 2 or 9) is masked to pose + damage-zone + high
|
|
// bits (mask &= 0xfe03): the event records 2..8 stop flowing from a
|
|
// wreck. SHADOWS the non-virtual Simulation::ForceUpdate. [T1]
|
|
// (Two earlier mislabels of the same binary fn are retired:
|
|
// "SetInstanceFlags(Word)" -- +0x18 is updateModel, NOT the entity
|
|
// instance flags -- and "RequestActionFlags(Word)" writing a
|
|
// side-member the emitter never read.)
|
|
//
|
|
void
|
|
ForceUpdate(Word mask = Simulation::DefaultUpdateModelFlag)
|
|
{
|
|
if (IsDisabled()) // FUN_0049fb54 (simState 2||9)
|
|
{
|
|
mask &= 0xfe03;
|
|
}
|
|
updateModel |= mask;
|
|
}
|
|
|
|
protected:
|
|
void
|
|
ReadUpdateRecord( // @004a1232 (vtable slot 6)
|
|
Simulation::UpdateRecord *message
|
|
);
|
|
void
|
|
WriteUpdateRecord( // @004a0c2c (vtable slot 7)
|
|
Simulation::UpdateRecord *message,
|
|
int record_type
|
|
);
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Subsystem roster (host pattern -- cf. VTV / CLASSMAP shared base)
|
|
//
|
|
// this[0x47] segmentCount / this[0x48] segment[] (JointedMover base)
|
|
// this[0x49] subsystemCount / this[0x4a] subsystem[] (Mech-owned table)
|
|
// The capability sub-rosters below are ChainOf<> views built in the ctor
|
|
// by IsDerivedFrom() tests against the streamed subsystem classes.
|
|
//
|
|
protected:
|
|
// NOTE: subsystemCount (this[0x49]) and the subsystem table (this[0x4a])
|
|
// are the INHERITED Entity base members `subsystemCount` / `subsystemArray`
|
|
// (Entity reads them in GetSimulation/GetSubsystem). They are NOT
|
|
// re-declared here -- doing so shadowed the base fields, so the engine's
|
|
// control-mapping / per-frame code never saw the streamed roster. The
|
|
// ctor streams into the inherited subsystemArray/subsystemCount directly.
|
|
|
|
// NOTE: damageZoneCount (this[0x47]) and the zone table (this[0x48]) are the
|
|
// INHERITED Entity base members `damageZoneCount` / `damageZones` -- the engine
|
|
// Entity ctor (ENTITY.cpp:961-987 == binary FUN_0041ff38) reads the count from
|
|
// the type-0x14 resource and allocates damageZones[count] (entries left
|
|
// uninitialised); the Mech ctor populates THAT inherited array. They are NOT
|
|
// re-declared here -- doing so shadowed the base fields (count read 0xCDCDCDCD,
|
|
// the populate loop never ran, and the engine damage router read the empty
|
|
// base array). Same lesson as subsystemCount/subsystemArray above. Use Zone(i)
|
|
// for typed (Mech__DamageZone*) access to the engine-stored DamageZone* entries.
|
|
|
|
ReconChain<Subsystem*> controllableSubsystems; // @0x65c this[0x197] (FUN_00427768)
|
|
ReconChain<Subsystem*> watchedSubsystems; // @0x6bc this[0x1af] (FUN_00427768)
|
|
ReconChain<Subsystem*> heatableSubsystems; // @0x7ac this[0x1eb] (class 0x51155c)
|
|
ReconChain<Subsystem*> weaponRoster; // @0x7bc this[0x1ef] (class 0x511830 =
|
|
// MechWeapon::ClassDerivations -- the
|
|
// reticle AddWeapon loop @part_014.c:5386
|
|
// asserts weapon attrs on every member.
|
|
// Was mislabeled "poweredSubsystems";
|
|
// PoweredSubsystem is 0x50f4bc.)
|
|
ReconChain<Subsystem*> damageableSubsystems; // @0x7cc this[499] (class 0x50e4fc)
|
|
|
|
int weaponCount; // @0x448 this[0x112]
|
|
Subsystem *sensorSubsystem; // @0x7dc this[0x1f7] (0xBBE)
|
|
Subsystem *gyroSubsystem; // @0x528 this[0x14a] (0xBC4)
|
|
public:
|
|
// Cache accessors for the controls mapper (@004afd10 reads mech+0x438/+0x5b4).
|
|
Subsystem *GetTorsoSubsystem() { return sinkSourceSubsystem; }
|
|
// Gyro accessor (task #56): the firing-recoil kick (projweap.cpp,
|
|
// binary @4bc194) reads mech+0x528.
|
|
Subsystem *GetGyroSubsystem() { return gyroSubsystem; }
|
|
Subsystem *GetHudSubsystem() { return hudSubsystem; }
|
|
// Reachable horizontal firing half-arc (radians) the mech's torso can bring
|
|
// its guns to bear -- the wider torso twist limit, or 0 for a fixed torso.
|
|
// Defined in mechmppr.cpp (which knows the full Torso type). Used by the
|
|
// weapon fire path (mech4.cpp) to derive the real, per-mech firing arc.
|
|
Scalar GetHorizontalFiringReach();
|
|
protected:
|
|
Subsystem *sinkSourceSubsystem; // @0x438 this[0x10e] (0xBC5 -> real Torso)
|
|
// The @0x5b4 cache holds the HUD, NOT the MechTech: raw factory (part_012.c:
|
|
// 10155-10164) case 0xbd6 (alloc 0x2a4, HUD ctor @004b7f94) writes param_1[0x16d];
|
|
// case 0xbdc (MechTech, alloc 0x104) stores NO cache. Also required by the
|
|
// controls mapper (@004afd10 writes cockpit+0x28c -- past MechTech's alloc,
|
|
// inside the HUD's). Was misnamed mechTechSubsystem.
|
|
Subsystem *hudSubsystem; // @0x5b4 this[0x16d] (0xBD6 -> real HUD)
|
|
MechControlsMapper *controlsMapper; // @0x434 this[0x10d] (0xBD3)
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Pose / kinematics (subset touched by the recovered slice)
|
|
//
|
|
protected:
|
|
Matrix34 orientation; // @0xd0 this[0x34] (FUN_0040ab44)
|
|
Quaternion bodyRotation; // @0x100 this[0x40]
|
|
// (task #1 corrections: the old `BTVal torsoRotation @0x130 [0x4b]`
|
|
// and `Vector3D worldPosition @0x138 [0x4e]` members were MISREADS of
|
|
// the ENGINE base fields -- word 0x4b = byte 0x12c = Entity::
|
|
// updateOrigin, and 0x138 = updateOrigin.angularPosition (Quaternion).
|
|
// The update-record reader/writer now uses the inherited engine
|
|
// members directly. The 24 bytes stay as PADDING so every following
|
|
// member keeps its layout-locked OUR-offset (mech4 static_asserts).)
|
|
int retiredUpdateRecordPad[6];
|
|
BTVal turretBase; // @0x1c4 this[0x71] (proxy: .SetIdentity)
|
|
Quaternion headPitch; // @0x1cc this[0x73] (look)
|
|
Quaternion torsoTwist; // @0x1dc this[0x77]
|
|
Vector3D legAngle; // @0x1f4 this[0x7d]
|
|
Vector3D hipAngle; // @0x200 this[0x80]
|
|
BTVal torsoAimCurrent; // @0x298 this[0xa6] (proxy: .Mul/.SetIdentity)
|
|
BTVal torsoAimTarget; // @0x2c8 this[0xb2] (proxy: .SetIdentity)
|
|
EulerAngles netOrientation; // @0x2d4 this[0xb5] (from update record)
|
|
// SHADOW JOINT (task #20): the model record's ShadowJointName (record+0xB4,
|
|
// 'jointshadow' on the BLH) resolved to the live skeleton Joint that tilts
|
|
// the flat *_tshd shadow proxy to the terrain angle. Binary: part_012.c:
|
|
// 10285 (record+0xb4 -> CString -> GetSegment -> GetJointIndex -> GetJoint
|
|
// -> this[0x10b] @0x42c). Accessed BY NAME only; declared after the locked
|
|
// fields so it does not shift them.
|
|
public:
|
|
Joint *shadowJointNode; // binary @0x42c this[0x10b]
|
|
void UpdateShadowJoint(const Vector3D &ground_normal);
|
|
// Update-record state (task #1; by-name access, declared after the locked
|
|
// fields so nothing shifts):
|
|
int poseSyncLatch; // binary @0x77c -- armed by every type-0 record
|
|
// on both sides; consumed by the dead-reckoner
|
|
// (FUN_004ab1c8: replicant motionEventVector =
|
|
// updateOrigin - localOrigin; master
|
|
// projectedOrigin re-base); cleared on the
|
|
// simState-2 edge
|
|
Vector3D fallDirection; // binary @0x4a8 -- knockdown/death/impact record
|
|
// payload; ctor (0,0,1) [T4 name]
|
|
Scalar fallScalar; // binary @0x4b4 -- same record family; ctor 0 [T4 name]
|
|
int heatLevelSnapshot; // binary @0x780 -- heatAlarm level at frame
|
|
// entry; the type-7 impact-record deadband
|
|
// The AUTHENTIC update-record deadbands (task #3): streamed by the ctor
|
|
// from the model record (disasm @0x4a26d7-0x4a26ef: 0x770 <- rec+0xa0 x
|
|
// pi/180 [const @0x4a2d44 = 0.01745329], 0x76c <- rec+0x9c raw, 0x768 <-
|
|
// rec+0x98 raw = UpdateTurnDegreeDiffrence / UpdateTurnVelocityDiffrence /
|
|
// UpdatePositionDiffrence). Consumed by the perf-loop senders. [T1]
|
|
Scalar updatePositionDeadband; // binary @0x768 -- type-0 pose trigger
|
|
Scalar updateTurnVelocityDeadband; // binary @0x76c -- type-4 yaw-rate trigger
|
|
Scalar updateTurnAngleDeadband; // binary @0x770 -- type-4 orientation trigger (rad)
|
|
// AUTHENTIC GROUND MODEL ctor products (task #15, ground-model-decode;
|
|
// binary part_012.c:9938-9940 + 9974-9975). By-name access only; declared
|
|
// after the layout-locked fields so nothing shifts.
|
|
Scalar standingTemplateMaxY; // binary @0x518 collisionTemplate->maxY at ctor
|
|
Scalar duckedTemplateMaxY; // binary @0x51c 0.6 x standing (duck preset)
|
|
Scalar templateBottomLift; // binary @0x4b8 0.05 x (volume maxX-minX)
|
|
|
|
// AUTHENTIC TARGETING (task #36): the engine Reticle struct (MUNGA/RETICLE.h
|
|
// [T0]) -- the mech's "TargetReticle" attribute (id 0x1d) binds to it, per the
|
|
// RP VTV analog (VTV.h: `Reticle targetReticle` + TargetReticleAttributeID).
|
|
// The pilot slews reticlePosition; the pick ray through it fills
|
|
// rayIntersection / targetEntity / targetDamageZone; the mech's engine-Entity
|
|
// target slots (+0x37c/+0x388/+0x38c) are fed from this each frame (the
|
|
// binary's writer is in an un-exported gap -- semantics per RETICLE.h).
|
|
// By-name access only; declared after the layout-locked fields.
|
|
Reticle targetReticle;
|
|
|
|
// Ray pick against this mech (the Reticle "pick point intersection"):
|
|
// world ray -> local frame -> slab test vs the collision template's
|
|
// ExtentBox (BoundingBox::HitBy clips the Line at the entry distance)
|
|
// -> world hit point. Defined in mech.cpp.
|
|
Logical PickRayHit(const Point3D &start, const Vector3D &dir,
|
|
Scalar max_range, Point3D *hit_world);
|
|
|
|
// Per-weapon beam render walk (task #33; extracted task #51 so it runs for
|
|
// EVERY mech -- player, dummy masters, and MP replicants whose emitters
|
|
// carry replicated discharge state). Defined in mech4.cpp.
|
|
void DrawWeaponBeams(Scalar dt);
|
|
protected:
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Alarms / indicators
|
|
//
|
|
protected:
|
|
AlarmIndicator masterAlarm; // @0x39c this[0xe7] (33 levels)
|
|
AlarmIndicator heatAlarm; // @0x450 this[0x114] (3 levels)
|
|
AlarmIndicator stabilityAlarm; // @0x4c4 this[0x131] (2 levels)
|
|
AlarmIndicator statusAlarm; // @0x714 this[0x1c5] (33 levels)
|
|
|
|
FilteredScalar telemetryFilter[5]; // @0x7e0 this[0x1f8..0x204] (15-sample)
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Misc recovered state (names best-effort)
|
|
//
|
|
protected:
|
|
BTVal mechName; // @0x360 this[0xd8] (proxy: .Copy)
|
|
Scalar maxSpeed; // @0x400 this[0x100] = FLT_MAX
|
|
int stateFlags; // @0x410 this[0x104]
|
|
int throttleState; // @0x4a4 this[0x129] = 2 (initial)
|
|
int heatLevel; // @0x518 this[0x146]
|
|
int heatCapacity; // @0x51c this[0x147] = 0.6 * heatLevel
|
|
Time creationTime; // @0x778 this[0x1de]
|
|
// Cylinder hit-location table (STEP 6) -- DamageLookupTable* cached by the
|
|
// ctor from the type-0x1d resource (FUN_0049ea48 = the table ctor), read by
|
|
// Mech::TakeDamageMessageHandler to resolve an unaimed (-1) hit's zone from
|
|
// its impact point. MUST be a real named member: the old home Wword(0x111)
|
|
// is the recon ABSORBER bank (BTVal stores nothing, reads 0) -> the cached
|
|
// table silently vanished and every unaimed hit no-op'd. (Was mislabeled
|
|
// "ammoExpended"; binary slot this[0x111] is this pointer.)
|
|
int damageLookupTable; // @0x444 this[0x111] (FUN_0049ea48)
|
|
int deathHandler; // @0x850 this[0x214] (FUN_0042a984)
|
|
// Wreck-smoke cadence (port addition, [T3]): re-fires the death/rubble
|
|
// smoke plume (psfx 1, DDTHSMK) every 10s (its authored emission window)
|
|
// while the mech is a destroyed wreck, so a dead mech keeps smoking.
|
|
Scalar wreckSmokeTimer;
|
|
// The cockpit eye-slew angles (the "EyepointRotation" attribute the
|
|
// binary's DPLEyeRenderable composes into the view every frame). Was
|
|
// bound to the shared junk attrPad -> the cockpit camera got rotated by
|
|
// garbage (the canted-horizon / black-screen views). Zero until the
|
|
// eye-slew system (HUD freeAimSlew / gyro eye chain) writes it.
|
|
EulerAngles eyepointRotation;
|
|
|
|
// Three ref-counted creation-name objects (badge/color/insignia).
|
|
void *resourceNameA; // @0x844 this[0x211]
|
|
void *resourceNameB; // @0x848 this[0x212]
|
|
void *resourceNameC; // @0x84c this[0x213]
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Reconstruction additions -- members & methods whose bodies live in the
|
|
// mech2 / mech3 / mech4 slices (declared here so the whole class is visible).
|
|
// Offsets cite the mech2/mech3 offset maps; types inferred from usage.
|
|
//
|
|
public:
|
|
static Receiver::MessageHandlerSet MessageHandlers; // SharedData ctor operand
|
|
// Mech's OWN handler table (overrides Entity's TakeDamage with the
|
|
// cylinder hit-location resolver); chained to Entity::GetMessageHandlers().
|
|
static const Receiver::HandlerEntry MessageHandlerEntries[];
|
|
enum { StateCount = 0x21 };
|
|
|
|
//
|
|
// Attribute Support -- the named attributes the cockpit gauges bind to.
|
|
// The config (content/GAUGE/L4GAUGE.CFG) binds the bare names LinearSpeed /
|
|
// MaxRunSpeed / DuckState / RadarRange / RadarLinearPosition /
|
|
// RadarAngularPosition, resolved by Simulation::GetAttributePointer ->
|
|
// activeAttributeIndex->Find(name). The reconstruction shipped an EMPTY,
|
|
// unchained Mech::AttributeIndex, so EVERY bare Mech gauge (and even the
|
|
// inherited attributes) resolved to NullAttribute.
|
|
//
|
|
// The full binary id set (0x15..0x38, table VA 0x50be84) is declared so the
|
|
// published table can be extended without renumbering. The chain starts at
|
|
// JointedMover::NextAttributeID (0x15); ids MUST stay contiguous, because the
|
|
// built index is a dense array sized to max(id) and AttributeIndexSet::Find
|
|
// strcmps EVERY slot -- an unfilled slot holds a garbage entryName -> AV.
|
|
//
|
|
enum {
|
|
MaxAccelerationAttributeID = JointedMover::NextAttributeID, // 0x15
|
|
CollisionStateAttributeID, // 0x16
|
|
CollisionNormalAttributeID, // 0x17
|
|
CollisionSpeedAttributeID, // 0x18
|
|
CollisionMaterialTypeAttributeID, // 0x19
|
|
CurrentSpeedAttributeID, // 0x1a
|
|
MaxRunSpeedAttributeID, // 0x1b
|
|
EyepointRotationAttributeID, // 0x1c
|
|
TargetReticleAttributeID, // 0x1d
|
|
FootStepAttributeID, // 0x1e
|
|
AnimationStateAttributeID, // 0x1f
|
|
ReplicantAnimationStateAttributeID, // 0x20
|
|
LinearSpeedAttributeID, // 0x21
|
|
ClimbRateAttributeID, // 0x22
|
|
AccelerationLastFrameAttributeID, // 0x23
|
|
AngularSpeedAttributeID, // 0x24
|
|
ArmorDamageLevelAttributeID, // 0x25
|
|
SubsystemDamageLevelAttributeID, // 0x26
|
|
MyomerDamageLevelAttributeID, // 0x27
|
|
TestButton1AttributeID, // 0x28
|
|
TestButton2AttributeID, // 0x29
|
|
TestButton3AttributeID, // 0x2a
|
|
TestButton4AttributeID, // 0x2b
|
|
TestButton5AttributeID, // 0x2c
|
|
TestButton6AttributeID, // 0x2d
|
|
ReduceButtonAttributeID, // 0x2e
|
|
RadarRangeAttributeID, // 0x2f
|
|
RadarLinearPositionAttributeID, // 0x30
|
|
RadarAngularPositionAttributeID, // 0x31
|
|
RearFiringAttributeID, // 0x32
|
|
RequestDuckAnimationAttributeID, // 0x33
|
|
UnstablePercentageAttributeID, // 0x34
|
|
SuperStopAttributeID, // 0x35
|
|
IncomingLockAttributeID, // 0x36
|
|
DuckStateAttributeID, // 0x37
|
|
DistanceToMissileAttributeID, // 0x38
|
|
NextAttributeID // 0x39
|
|
};
|
|
static const IndexEntry AttributePointers[];
|
|
static AttributeIndexSet& GetAttributeIndex();
|
|
|
|
// Cockpit-gauge attribute backing members (APPENDED to Mech's own region --
|
|
// never at a locked base/gait offset). attrPad is a shared read-only 0 for
|
|
// the dense-prefix ids the BLH cockpit does not bind; linearSpeed is the live
|
|
// forward ground speed the speed readout / arc / radar consume (binary @0x81c).
|
|
Scalar attrPad;
|
|
Scalar linearSpeed;
|
|
// Radar/map gauge attributes (binary @0x404/0x408/0x40c/0x3f8). The map
|
|
// widget reads position/angle as POINTERS into the mech's live origin, so
|
|
// radarLinearPosition/radarAngularPosition point at localOrigin.{linear,
|
|
// angular}Position (set in the ctor); radarRange is the radar scale.
|
|
Scalar radarRange; // 0x2f RadarRange (scale/max)
|
|
Point3D *radarLinearPosition; // 0x30 RadarLinearPosition
|
|
Quaternion *radarAngularPosition; // 0x31 RadarAngularPosition
|
|
int duckState; // 0x37 DuckState (crouch posture)
|
|
|
|
// --- raw / engine-shim members touched by the ctor slice -------------
|
|
void *vtable; // installed Mech vtable ptr
|
|
Word instanceFlags; // entity instance flags (this+0x18 region)
|
|
ReconFiltered mechNameFilter; // @0xdb name filter
|
|
ReconSlot voltageBus; // resolve-able power-bus slot
|
|
void *controlSource; // @0x128 controls subsystem handle
|
|
|
|
// --- locomotion / gait animation state (mech2/mech3 maps) ------------
|
|
// movementMode UNIFIED with the engine (task #1, 2026-07-11): binary
|
|
// mech+0x40 is Simulation::simulationState.currentState (StateIndicator
|
|
// @0x2c: +0x10 oldState, +0x14 currentState) -- the "gait/death
|
|
// selector" and the replicated simulation state are ONE field. Every
|
|
// update record carries it (header +0xC); Simulation::ReadUpdateRecord
|
|
// applies it on the replicant, so death (9) / limbo (2) / airborne
|
|
// (3/4) now replicate for free. The old parallel `int movementMode`
|
|
// member is GONE -- reads/writes go through the engine StateIndicator
|
|
// (SetState collapses old=current then fires only on change, exactly
|
|
// the binary's FUN_0041bbd8). [T1: writers in the binary are
|
|
// SetLevel(this+0x2c, n) calls -- FUN_0049f674 sets 2, death paths 9]
|
|
// (The old `Word actionRequestFlags @0x18` member is also gone: +0x18
|
|
// is Simulation::updateModel; see ForceUpdate above.)
|
|
public:
|
|
int MovementMode() { return (int)GetSimulationState(); }
|
|
void SetMovementMode(int m) { SetSimulationState((unsigned)m); }
|
|
// (the members below were and stay PUBLIC -- the recon TUs
|
|
// (btplayer/dmgtable/mechmppr/emitter) reach them directly)
|
|
int movementFlags;
|
|
int motionEventArmed; // @0x5a4
|
|
int motionEventPending;
|
|
int airborneSelect;
|
|
Scalar forwardCycleRate; // @0x344
|
|
Scalar legCycleSpeed; // @0x348
|
|
Scalar reverseStrideLength; // @0x34c
|
|
Scalar gimpStrideLength; // @0x350
|
|
AlarmIndicator legStateAlarm; // @0x39c
|
|
int legAnimationState; // @0x3b0
|
|
Scalar gimpSpeedMax; // @0x52c
|
|
Scalar standSpeed; // @0x530
|
|
Scalar walkStrideLength; // @0x534
|
|
Scalar reverseSpeedMax; // @0x538
|
|
Scalar jumpRunSpeedMax; // @0x53c
|
|
Scalar jumpWalkSpeedMax; // @0x540
|
|
Scalar jumpRunStrideLength; // @0x544
|
|
Scalar jumpWalkStrideLength; // @0x548
|
|
Scalar groundCycleRate; // @0x5b8
|
|
Scalar airborneCycleRate; // @0x5bc
|
|
// Forward-throttle scale read by the controls mapper (@004afd10:
|
|
// speedDemand = topSpeed@0x34c * throttle * THIS). No writer found in the
|
|
// decomp windows (likely model/status-driven); ctor inits 1.0 (neutral).
|
|
Scalar forwardThrottleScale; // @0x5c0
|
|
Scalar gyroRumbleTimer; // @0x5c4 binary: FLOAT rumble-period countdown
|
|
// (engaged-gait rumble @4aa2eb-4aa35f, task #56);
|
|
// reset at clip load. (Was mis-typed int clipLoadGuard.)
|
|
Scalar globalTimeScale; // @0x5a8
|
|
Scalar idleStrideScale; // @0x5ac
|
|
Scalar gimpCycleRate; // @0x5b0
|
|
int jumpCapable; // @0x580
|
|
int hasReverseGimpSet; // @0x584
|
|
int hasCrashSet; // @0x588
|
|
int deathAnimationLatched; // @0x650
|
|
int legResetLatch; // @0x654
|
|
int bodyResetLatch; // @0x658
|
|
ReconSeq legAnimation; // @0x65c channel-A controller
|
|
ReconSeq bodyAnimation; // @0x6bc channel-B controller
|
|
AlarmIndicator bodyStateAlarm; // @0x714
|
|
int bodyAnimationState; // @0x728
|
|
Scalar bodyTargetSpeed; // @0x6b4
|
|
Scalar bodyCycleSpeed; // @0x6b8
|
|
Scalar reverseSpeedMax2; // @0x7a0
|
|
Scalar arrivalTime;
|
|
Scalar simTime;
|
|
Scalar spinRate;
|
|
ResourceDescription::ResourceID animationClips[40]; // @0x5cc gait clips
|
|
// namedClip is NOT a separate array in the binary: namedClip@0x5e0 == &animationClips[5]
|
|
// (0x5cc + 5*4). LoadLocomotionClips writes namedClip[i]; SetBodyAnimation/MeasureClipStride
|
|
// read animationClips[i+5] -- SAME storage. Aliased via this pointer (set in the ctor) so the
|
|
// array syntax in mech3.cpp keeps working AND the writes land where the state machine reads.
|
|
ResourceDescription::ResourceID *namedClip; // -> &animationClips[5]
|
|
ResourceDescription::ResourceID crashClipA, crashClipB, crashClipC; // @0x5d8/0x5d4/0x5dc
|
|
ResourceDescription::ResourceID gimpBaseClip; // @0x64c
|
|
CString motionEventName; // @0x598 cleared to "" on fall/reset
|
|
|
|
// --- gait helpers (mech2) -------------------------------------------
|
|
void SetLegAnimation(int state); // @004a7fc4
|
|
void SetBodyAnimation(int state); // @004a800c
|
|
// (RequestActionFlags(Word) @004a4c54 retired: it was Mech::ForceUpdate
|
|
// -- the "action request bits" ARE the updateModel record requests.
|
|
// The gait/knockdown call sites now call ForceUpdate directly and
|
|
// their masks match the binary's senders byte-for-byte.)
|
|
// P3 STEP-7 body finished-callback (the real PTR_LAB_0050d6fc == FUN_004a6d8c):
|
|
// called by SequenceController::Advance at end-of-clip -- picks the next gait state
|
|
// from commanded speed vs the loaded caps, re-arms via SetBodyAnimation, recursively
|
|
// advances the carryover. Static (the cb is a plain fn ptr, owner Mech is arg1).
|
|
// BodyTransition = the shared tail (SetBodyAnimation(next) + bodyAnimation.Advance).
|
|
// LoopBodyClip = bring-up loop for the inline cutover path.
|
|
static Scalar BodyClipFinished(Mech *m, unsigned a2, Scalar carryover, int move_joints); // FUN_004a6d8c
|
|
static Scalar LoopBodyClip(Mech *m, unsigned a2, Scalar carryover, int move_joints);
|
|
Scalar BodyTransition(int next_state, Scalar adv_time, int move_joints);
|
|
// LEG-channel finished-callback (the real PTR_LAB_0050d6f0 == FUN_004a6928):
|
|
// same transition machine keyed on legAnimationState@0x3b0, but compares the
|
|
// LIVE mapper speedDemand (subsystemArray[0]+0x128) and slews legCycleSpeed.
|
|
static Scalar LegClipFinished(Mech *m, unsigned a2, Scalar carryover, int move_joints); // FUN_004a6928
|
|
Scalar LegTransition(int next_state, Scalar adv_time, int move_joints);
|
|
Scalar AdvanceLegAnimation(Scalar time_slice); // @004a5028
|
|
Scalar AdvanceBodyAnimation(Scalar time_slice, int loop);
|
|
Scalar AdvanceBodyAnimationAirborne(Scalar time_slice, int loop);
|
|
Scalar AdvanceLegAnimationAirborne(Scalar time_slice);
|
|
Logical IsDisabled(); // FUN_0049fb54
|
|
// --- death sequence (the un-exported master-perf death branch, rebuilt
|
|
// from its exported consumers + the RP VTV::DeathShutdown analog) ---
|
|
Logical IsMechDestroyed(); // damage-side death flag: graphicAlarm level >= 9
|
|
void UpdateDeathState(Scalar dt); // death freeze + subsystem DeathShutdown + wreck smoke
|
|
|
|
// --- clip loaders (mech3) -------------------------------------------
|
|
ResourceDescription::ResourceID *
|
|
ResolveAnimationClip(const char *prefix, const char *suffix); // @004a7f50
|
|
|
|
// Body-animation looping callback (bring-up gait). Non-virtual so adding
|
|
// it does NOT change object layout; passed to AnimationInstance::SetAnimation
|
|
// (reinterpret_cast to JointedMover::AnimationCallback) so the clip re-arms at
|
|
// frame 0 when it ends instead of dereferencing a NULL finishedCallback.
|
|
Scalar OnBodyAnimFinished(ResourceDescription::ResourceID animation_number,
|
|
Scalar carryover, Logical animate_joints);
|
|
void MeasureClipStride(int slot, Scalar *total, Scalar *lastKey); // @004a8054
|
|
void LoadLocomotionClips(Mech__ModelResource *model); // @004a80d4
|
|
void LoadLocomotionClipsExt(Mech__ModelResource *model); // @004a86c8
|
|
|
|
static int
|
|
CreateSubsystemStream(
|
|
int subsystem_index, const char *type_name, int pass,
|
|
NotationFile *model_file, const char *model_name,
|
|
const char *subsystem_name, SubsystemResourceBlob *out_blob,
|
|
NotationFile *subsystem_file,
|
|
const ResourceDirectories *directories);
|
|
static ResourceDescription::ResourceID
|
|
CreateSubsystemsStream(
|
|
NotationFile *model_file, const char *model_name,
|
|
NotationFile *model_notation,
|
|
const ResourceDirectories *directories);
|
|
static SharedData *
|
|
SubsystemDefaultData(const char *type_name);
|
|
|
|
// --- simulation / damage (mech4) ------------------------------------
|
|
void DeadReckonPose(Scalar fraction);
|
|
Logical IntegrateMotion(Scalar time_slice, int loop);
|
|
void Simulate(Scalar time_slice);
|
|
|
|
// --- BRING-UP locomotion (Tier 2: drivable player mech) -------------
|
|
// Overrides the engine per-frame Simulation::PerformAndWatch (virtual;
|
|
// the simulation director calls it every frame on every entity). The
|
|
// shipped game's per-frame tick (Simulate via activePerformance) is the
|
|
// uncaptured/reconstructed-but-unsafe chain (raw this+0xNN offset bugs,
|
|
// stubbed subsystems), so for the drivable bring-up we override
|
|
// PerformAndWatch directly and integrate the player's localOrigin /
|
|
// localToWorld from throttle+turn input. localToWorld is the matrix the
|
|
// render tree + chase camera (btl4vid.cpp) are bound to, so the mech
|
|
// walks and the camera follows. See mech4.cpp. MUST match the base
|
|
// signature exactly or it hides instead of overriding.
|
|
void PerformAndWatch(const Time& till, MemoryStream *update_stream);
|
|
// THE REAL per-contact collision responder (task #15, ground-model-decode):
|
|
// binary FUN_004abb40, Mech vtable slot +0x3c -- the override of the engine
|
|
// protected virtual Mover::ProcessCollision (MOVER.h:359-365, signature
|
|
// exact). The earlier draft misread this function as a weapon sweep
|
|
// ("ResolveWeaponImpact") -- it is the mech-vs-world collision policy:
|
|
// BoxedSolid resolver -> StaticBounce -> owner classification (Mover /
|
|
// CulturalIcon crush sentinel 0.00123f). Gated: falls through to the
|
|
// engine base when GroundReal() is off.
|
|
virtual void ProcessCollision(Scalar time_slice, BoxedSolidCollision &collision,
|
|
const Point3D &old_position, Damage *damage);
|
|
// The authentic per-frame ground block (binary FUN_004a9b5c @4aa630-4aab5f):
|
|
// MoveCollisionVolume -> ground probe/snap -> collisions -> response policy.
|
|
// old_position = the START-OF-FRAME position (the blocking-hit restore target);
|
|
// must NOT alias localOrigin.linearPosition.
|
|
void AuthenticGroundAndCollide(Scalar dt, const Point3D &old_position);
|
|
void FeedHeatCapacityGauge();
|
|
void FeedHeatLevelGauge();
|
|
static Logical
|
|
LookupDamageState(const char *keyword, int *out);
|
|
void RaiseStatusAlarm(int level); // subsystem -> mech status alarm
|
|
|
|
// --- damage-table support (dmgtable) --------------------------------
|
|
Point3D WorldToLocal(const Point3D &world); // FUN_00408bf8 (owner+0xd0)
|
|
void *TorsoOrientationSource(); // *(this+0x438)
|
|
// Height reference the cylinder table normalises impact height against --
|
|
// the binary's *(mech+0x2ec)+0xc, a stance height set from mech+0x518
|
|
// (standing) / +0x51c (ducked). We return the standing value
|
|
// (collisionTemplate->maxY captured at ctor).
|
|
Scalar CylinderReferenceHeight();
|
|
// Live torso twist (yaw) in radians -- torso+0x1d8, via the roster torso
|
|
// (sinkSourceSubsystem @0x438). 0 for a fixed-torso mech.
|
|
Scalar TorsoHeading();
|
|
// Mech override of Entity::TakeDamageMessageHandler: resolve an unaimed
|
|
// (invalidDamageZone) hit's zone via the cylinder table, then base-route.
|
|
void TakeDamageMessageHandler(TakeDamageMessage *message);
|
|
|
|
// --- damage-routing support (mechdmg / mech4) -----------------------
|
|
// Typed access to the inherited Entity::damageZones[] (engine stores DamageZone*;
|
|
// our entries are Mech__DamageZone, populated by the Mech ctor). Defined in
|
|
// mech.cpp where Mech__DamageZone is a complete type.
|
|
Mech__DamageZone *Zone(int i) const;
|
|
// First VITAL damage zone index (0 if none) -- for concentrated fire that kills.
|
|
// Defined in mech4.cpp (Mech__DamageZone::vitalDamageZone is protected; Mech has access).
|
|
int FirstVitalZone() const;
|
|
int flags; // entity flags word (this+0x28 region)
|
|
int stance; // posture state
|
|
int ammoState; // @0x44c 0 none / 1 leaking / 2 dry
|
|
AlarmIndicator graphicAlarm; // body graphic-state alarm
|
|
EntityID lastInflictingID; // @0x43c last attacker (read by DamageZone routing)
|
|
Logical IsAirborne();
|
|
|
|
// --- gait world-step accumulators (P3 STEP-6 base-region reconciliation) ---
|
|
// RELOCATED out of the engine base: IntegrateMotion used raw 1995 binary
|
|
// offsets that, in the 2007 engine layout, stomp live engine fields
|
|
// (0x260->projectedOrigin, 0x26c->previousOrigin, 0x298->projectedVelocity,
|
|
// 0x100->localOrigin, 0x12c->updateOrigin). Declared here in the Mech's OWN
|
|
// region (appended -- shifts no locked offset) and accessed BY NAME. See
|
|
// btbuild/P3_LOCOMOTION.md "BASE-REGION RECONCILIATION".
|
|
Quaternion motionDelta; // was raw @0x260 (orientation delta; FromQuat src)
|
|
Quaternion worldPose; // was raw @0x26c (integrated world pose)
|
|
Quaternion worldPoseBase; // was raw @0x138 (dead-reckon base pose; DeadReckonPose)
|
|
Quaternion angularAccum; // was raw @0x298 (per-frame angular accumulator)
|
|
Vector3D motionSourceA; // was raw @0x100 (motion-event source A)
|
|
Vector3D motionSourceB; // was raw @0x12c (motion-event source B)
|
|
Vector3D motionEventVector; // was raw @0x598 (motion-event delta = B - A)
|
|
Quaternion aimRate; // was raw @0x1dc (aim/torso angular-rate telemetry,
|
|
// cleared each frame; stomped localAcceleration)
|
|
|
|
//
|
|
// --- Everything past here is owned by mech2.cpp .. mech4.cpp ---
|
|
// [mech2] MoveAndCollide / Simulate / WriteUpdateRecord / damage routing
|
|
// [mech3] PrintState / status reporting / cockpit display feed
|
|
// [mech4] AI / pathing / scoring hooks
|
|
// The remaining members up to sizeof==0x854 are declared by those slices.
|
|
//
|
|
};
|
|
|
|
#endif
|