Files
BT411/game/reconstructed/mech.hpp
T
CydandClaude Opus 5 1debbeb240 Mech::DuckRequest -- the CROUCH button, and a dead-button census that lied
The 2026-07-20 audit listed 8 panel buttons dispatching a streamed message with
no reconstructed handler.  Only ONE was still missing: generator on/off,
ToggleSeekVoltage, EjectAmmo, ToggleCooling and BalanceCoolant had all landed
between 07-20 and 07-25 while pod-hardware.md and open-questions.md still called
them dead.  docs/INPUT_PATH_AUDIT.md had already flagged the census as "stale in
both directions" and was right -- swept both files, and recorded the rule: check
the code, not the census.

DuckRequest @0049fa00 (id 0x1a, RIO 0x13 -- the manual gives crouch a whole
section).  The binary's entire body:

    if (0 < *(int *)(param_2 + 0xc)) { *(undefined4 *)(param_1 + 0x398) = 1; }

Press-only; sets duckState (mech+0x398, attribute 0x37).  A one-shot REQUEST
flag, not a posture toggle -- the handler never clears it and the only other
writer in the whole binary is the mech reset (part_012.c:9439, the same reset
that zeroes incomingLock, which is how that region was already mapped).

WHY THE FLAG HAS NO READER, AND WHY THAT IS CORRECT.  duckState has ZERO readers
anywhere in the decomp, because it is published as an ATTRIBUTE and consumed
through DATABINDING: content/GAUGE/L4GAUGE.CFG runs a 3-frame bduck.pcc
oneOfSeveralPixInt bound to DuckState -- "crouch mode: button 4" on the map's
legend column.  Verified live by capturing the Secondary/Radar window before and
after a 0x13 press: the crouch icon goes grey -> orange.  So the handler is
COMPLETE.  A crouch pose invented here would be a stand-in for data we have not
found (no SQUAT clip name survives in the decomp or in content/, only
DuckServo01.wav in AUDIO1.RES).

Bonus: those gauge widgets sit at offsets 537/430/322/215/108 -- a 107 pitch,
independently corroborating the map legend grid measured from pixels.

Still open from that census: MechRIOMapper's own Keypress @004d2514 (id 0x19).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 02:23:58 -05:00

1266 lines
66 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 <cstr.hpp> // CString (the vehicle badge/color/patch creation names)
#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)
friend void BTCommitLookState(void *, int); // task #68: the look-state commit (mech4.cpp)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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; }
// The mapper's ONE true home (task #7): roster slot 0 -- the binary's
// SetMappingSubsystem @0049fe40 installs there, and every binary
// mapper consumer reads **(mech+0x128). Null-safe: the binary never
// ran these paths mapper-less; our reachability is wider.
MechControlsMapper *MappingMapper()
{
return (GetSubsystemCount() > 0)
? (MechControlsMapper *)GetSubsystem(0) : 0;
}
class SubsystemMessageManager *GetMessageManager()
{
return messageManager;
}
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)
// THE UNTANGLE (task #7): binary [0x10d]/mech+0x434 caches the
// SubsystemMessageManager (0xBD3, the per-mech damage/explosion
// consolidation hub, ctor @0049bca4) -- NOT a controls mapper. The
// binary-wide census found exactly ONE reader (@0x4b984b in
// MechWeapon::SendDamageMessage @004b9728) and it is pure
// message-manager semantics. The old `controlsMapper` name/type was
// the misread that made the whole drive lean on this cache. The
// REAL mapper's one true home is roster slot 0 (MappingMapper()).
class SubsystemMessageManager *messageManager; // @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)
// SCALAR peer-yaw mirror (port addition, replicant-spin work): the master's
// estimate of the yaw the PEER is currently rendering -- re-based on every
// type-4 send (writer case 4), advanced analytically as base + rate*elapsed.
// Replaces the quaternion projectedOrigin mirror for the ANGLE deadband: that
// one is recomputed by the master's own reckoner each frame from lastUpdate
// timing it does not control, and its false pi-drift waves FLOODED type-4
// resyncs (measured maxAng~=pi bursts). Scalars, wrap-safe, self-timed.
// Master send-mirror gait travel (BT_MASTER_GAITMIRROR): the invisible mj=0
// body channel's per-frame cycleDistance, stashed in the drive block and
// consumed by the send gate to advance projectedOrigin the way the PEER
// predicts (gait), instead of the constant-velocity deadReckoner. This is the
// authentic FUN_004a9b5c mirror = IntegrateMotion(mj=0) on projectedOrigin.
Scalar mirrorBodyAdv; // body cycleDistance this frame (master)
// Peer true-mirror cadence: the replicant sets this to the actual replicated
// ground speed (|updateVelocity|) each frame so the body clip advances at the
// master's real rate instead of the locally-slewed bodyCycleSpeed (which drifts
// and makes the animation phase wander -> the accel/decel hesitation). -1 =
// disabled (master/single-player always -1, so the authentic slew runs).
Scalar peerMirrorSpeed;
Scalar angMirrorYaw; // yaw at the last type-4 send (rad)
Scalar angMirrorRate; // yaw rate sent with it (rad/s)
Time angMirrorTime; // lastPerformance at that send
int angMirrorValid; // 0 until the first type-4 send
int angSyncLatch; // peer: type-4 arrived -> re-anchor heading (the
// angular analog of poseSyncLatch; consumed by
// the replicant's incremental heading integrator)
// 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)
// COLLISION-DAMAGE ECONOMY GUARD (the MP ram one-shot, 2026-07-12): the
// mech's TRUE motion at frame entry, snapshotted just before the frame's
// ProcessCollisionList walk. Each StaticBounce in that walk MUTATES
// worldLinearVelocity (the += delta_v reflection); with several solids in
// one frame's list (2007 WinTesla puts TERRAIN in the collision tree; the
// 1995 ground was a heightfield probe, never a list entry) the reflections
// COMPOUND x(1+e) per contact, and a mech-vs-mech contact later in the
// list priced its dispatched damage off a velocity amplified 4x-40x
// (62-pt bump economy -> 1074/112375-pt one-shots; mp_a.log:32651).
// Mech::ProcessCollision restores worldLinearVelocity from this snapshot
// per contact, so contact damage is always priced at the mech's REAL
// approach speed -- which is all the binary's StaticBounce ever saw.
// By-name access only; declared after the layout-locked fields.
Vector3D frameEntryWorldVelocity;
// RAM CONTACT EDGE (port analog of the binary's bounce-separation): in the
// 1995 mover, StaticBounce REVERSED worldLinearVelocity, so the frame after
// a bump read as separating and the damage gate stopped repeat dispatch.
// Our gait re-derives velocity every frame (still closing while the stick
// is held / while respawn-overlapped), so sustained contact re-priced a
// fresh full-speed ram EVERY frame (~60 dmg x 60fps = melt; the respawn
// explode-loop). One bump = one damage event; re-arms after the contact
// breaks for ramContactLinger seconds.
Entity *ramLastVictim;
Scalar ramContactLinger;
// 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);
// World-STRUCTURE boresight pick: ray-test the zone's STATIC collision solid
// tree (garages/walls/props -- the geometry that blocks the walk) along the
// boresight and return the entry point of the closest structure hit. The
// authentic non-mech world pick (Mover::FindStaticSolidHitBy, the static-world
// half of the engine's FindBoxedSolidHitBy). hit_owner (optional) receives the
// struck solid's OWNING ENTITY (BoxedSolid::GetOwningSimulation -- every static
// solid is built with its Terrain/CulturalIcon/Door entity as owner,
// TERRAIN.cpp:107/246, DOOR.cpp:395): the binary targets/damages the struck
// ENTITY, not a sentinel (weapon dispatch @004b9728 sends to any non-Mech
// target; missile contact @004be078 dispatches at the struck solid's owner).
// Defined in mech.cpp.
Logical WorldStructurePick(const Point3D &start, const Vector3D &dir,
Scalar max_range, Point3D *hit_world,
Entity **hit_owner = 0);
// 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)
// (F7) `Scalar maxSpeed @0x400` was a MISREAD -- the binary attribute
// table names 0x400 DistanceToMissile (id 56; FLT_MAX = far default);
// the member is retired in favor of distanceToMissile below.
// (task #68) REAR-FIRING (binary attr id 50 @0x410; the old name was
// `stateFlags`): OR of every mounted weapon's rearFiring flag -- "this
// mech carries a rear arsenal". The look-back view arms those weapons.
int rearFiring; // @0x410 this[0x104]
// (task #68) the authored look-view angles (model record +0x50..0x5c,
// ctor converts degrees->radians; binary @0x564..0x570 -- previously
// parked in the Wword scratch bank). Consumed by the look-state
// commit: side views yaw by these, look-back = yaw pi + lookBackAngle
// pitch, look-down pitches by lookFrontAngle.
Scalar lookLeftAngle; // @0x564
Scalar lookRightAngle; // @0x568
Scalar lookFrontAngle; // @0x56C
Scalar lookBackAngle; // @0x570
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;
int matchlogDeathLogged; // matchlog DEATH one-shot latch
// (self-resets while alive)
// 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 strings (badge/color/insignia).
// CString members: each slot is a CStringRepresentation* -- the exact
// 16-byte object {size,len,text@+8,refs@+0xc} the binary's Mech ctor
// allocates (part_012.c:9841-9871) and the make handler re-binds from
// the MakeMessage via CString::operator=(const char*) == FUN_00402a98
// (part_012.c:10427-10441). SetupMaterialSubstitutionList reads the
// backing text (*(char**)(this+0x844..84c)+8) for the vehicle-table
// color/badge/patch lookups.
CString resourceNameA; // @0x844 this[0x211] (vehicle badge)
CString resourceNameB; // @0x848 this[0x212] (vehicle color)
CString resourceNameC; // @0x84c this[0x213] (vehicle patch)
public:
// Paint-name accessors (SetupMaterialSubstitutionList). CString's
// operator char*() returns the rep's stringText -- NULL for a
// default-constructed (never-bound) name; callers must tolerate NULL.
const char *GetVehicleBadgeName() const { return (const char *)resourceNameA; }
const char *GetVehicleColorName() const { return (const char *)resourceNameB; }
const char *GetVehiclePatchName() const { return (const char *)resourceNameC; }
protected:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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;
// Real StateIndicators for the animation-state attributes (0x1f/0x20).
// The audio subsystem binds AudioStateWatchers to these BY NAME and calls
// AddAudioWatcher() on them -- so unlike the scalar gauge attrs they CANNOT
// share the read-only attrPad (a Scalar has no watcher chain -> the watcher
// register AV'd on 0xCDCDCDCD). Driven from SetBodyAnimation so footstep/
// gait sounds fire on animation transitions. [T2]
StateIndicator animationState; // 0x1f AnimationState
StateIndicator replicantAnimationState; // 0x20 ReplicantAnimationState
// 0x16 CollisionState -- audio binds an AudioStateWatcher here too (impact/
// scrape sounds). Driven from ProcessCollision via collisionTemporaryState:
// NoCollision(0)/InitialHit(1)/Slide(2)/Rest(3) (RP VTV CollisionStateCount).
StateIndicator collisionState;
// 0x1e FootStep -- audio binds an AudioLogicalTrigger (threshold-crossing
// with hysteresis: rise past threshold -> play the step, fall back -> re-arm).
// Backed by a real pulse: SetBodyAnimation raises it 1 on entering a cyclic
// stride clip (each walk/run/reverse alternation == one foot plant) and the
// per-frame motion tick decays it back to 0 a few frames later, giving one
// clean rising edge per step. [T2]
int footStep; // the Logical the matcher polls (1 = contact)
Scalar fwdSpeedFiltered; // smoothed |v| published via localVelocity (the raw
Scalar vertSpeedFiltered; // per-frame position derivative pulses per stride and
// flapped the audio's speed triggers -- engine-hum
// restarts on every footfall)
// (AUDIO_FIDELITY F5) footStep is the authored CONTACT LEVEL, not a
// clip-transition pulse: per frame (leg gait state not 0/1),
// footStep = (jointlocal.y <= *clip footStepThreshold) -- binary
// disasm @0x4a9e80-0x4a9eb6 (leg) / @0x4aba86-0x4abab9 (body); the
// threshold is the ANI header word[2] SelectSequence now captures.
// States 0/1 RETAIN the previous value (binary skip). The old 150 ms
// pulse + decay members are retired.
Joint *footStepRootJoint; // cached "jointlocal" (binary mech+0x5C8)
int footStepRootResolved;
// (AUDIO_FIDELITY F19, corrected) the binary's localAcceleration feed:
// FIVE 15-sample RING BUFFERS (ctor part_012.c:9836-9840, size 0xF
// fill 0) hold the RAW per-frame velocity components + dt;
// localAcceleration = (ringMean - prevRingMean) / ringMean(dt)
// (part_012.c:15169-15195). A moving-window mean JITTERS every frame
// as samples rotate, so the accel attribute changes every watcher
// poll -- which is what keeps the authored N=30 footstep smoother fed
// (an exponential-filter derivative goes epsilon-flat between strides
// and starved it: the 10-20 s footstep warm-up bug).
Scalar velRingFwd[15];
Scalar velRingVert[15];
Scalar velRingDt[15];
int velRingCursor;
Scalar accelPrevFwdMean;
Scalar accelPrevVertMean;
Point3D accelPrevPos; // raw position memory for the ring feed
// (task #66, the gyro-tail INSTABILITY MODEL -- byte-decoded from the
// master perf @0x4aad3d-0x4aaf14 [T1]):
// instab = (min(|AccelerationLastFrame| / maxUnstableAcceleration, 1)
// x unstableAccelerationEffect)^2
// instab += clamp((demand - legCycleSpeed)/demand, <=1) ["gun the
// engine": commanded speed the legs haven't reached]
// x unstableGunTheEngineEffect
// if (legAnimationState == 4 /*trn*/) instab += unstableStopedTurnEffect
// clamp to 1 -> mech+0x3F0, then gyro->swayBias (gyro+0x3A8) and the
// UnstablePercentage audio attribute (binary id 52 @0x3F0) read it.
// The six tuning fields come from the model record +0x80..0x94 (ctor
// @0x4a2593-0x4a25d5); previously parked in the Wword scratch bank.
Scalar unstablePercentage; // binary @0x3F0 (reset writes 0)
Scalar maxUnstableAcceleration; // @0x784 <- rec+0x80
Scalar unstableAccelerationEffect; // @0x788 <- rec+0x84
Scalar unstableGunTheEngineEffect; // @0x78C <- rec+0x88
Scalar unstableSuperStopEffect; // @0x790 <- rec+0x8C (writer not yet located; airborne perf?)
Scalar unstableHighVelocityEffect; // @0x794 <- rec+0x90 (writer not yet located)
Scalar unstableStopedTurnEffect; // @0x798 <- rec+0x94
// 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)
// (AUDIO_FIDELITY F7) the incoming-missile alarm attributes. Binary
// Mech table [T1]: IncomingLock id 54 @0x3fc (Logical; authored match
// ==1 Start / ==0 Stop of the looped beeper), DistanceToMissile id 56
// @0x400 (Scalar; authored scale maps range 100..800 -> TEMPO 600..10,
// the beep accelerating as the missile closes). The mech reset inits
// them 0 / FLT_MAX (part_012.c:9446-9447); the live writer is in an
// un-exported gap, so the drive is intent-level [T3]: every homing
// Missile reports its target + range per MoveAndCollide tick into the
// *Next accumulators; PerformAndWatch latches them each frame.
int incomingLock; // 0x36 IncomingLock (1 = missile inbound)
Scalar distanceToMissile; // 0x38 DistanceToMissile (FLT_MAX = none)
int incomingLockNext; // accumulator (missiles OR into it)
Scalar distanceToMissileNext; // accumulator (missiles MIN into it)
// (AUDIO_FIDELITY F17) CollisionSpeed (binary id 24 @0x4B4): |impact
// velocity| captured when the contact accumulator arms (0->1) -- the
// authored AttackVolume [0.9,1] / Brightness [0.7,1] scales over
// impact speed [0,25] read it, so impacts scale with how hard you hit.
// ReduceButton (id 46 @0x340): the pod cab's REDUCE button; a real,
// watchable member -- the keyboard rig simply never presses it.
Scalar collisionSpeed; // 0x18 CollisionSpeed
int reduceButton; // 0x2e ReduceButton
// --- 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); }
// (AUDIO_FIDELITY F7) called by each homing Missile per MoveAndCollide
// tick; PerformAndWatch latches the accumulators into the published
// IncomingLock/DistanceToMissile attributes each frame.
void ReportIncomingMissile(Scalar range)
{
incomingLockNext = 1;
if (range < distanceToMissileNext)
distanceToMissileNext = range;
}
// (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 walkingTurnRate; // @0x574 (WalkingTurnRate, rad/s) -- the master
Scalar runningTurnRate; // @0x578 (RunningTurnRate, rad/s) perf turn-rate lerp
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
void MaintainViewClipSet(); // task #59 port: interior(viewpoint)/exterior(others)
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);
// Mech override of Entity::PlayerLinkMessageHandler (@0049f624): the
// base resolves mech->player; BT adds the REVERSE link
// player->playerVehicle = this on EVERY node (replicants included --
// this is how the camera director / scoreboard find a remote player's
// mech), clears the player's NonScoringPlayerFlag (a pilot with a
// vehicle SCORES/ranks), and on the master seeds the heat bank's
// ambientTemperature from the mission's [mission] temperature.
void PlayerLinkMessageHandler(PlayerLinkMessage *message);
// @0049f728 -- "BalanceCoolant" (Mech handler table @0x50BDF8, id 0x16;
// the pod's auto-cooling-balance button). Issue #20 (2026-07-21): the
// id was never registered -- silently swallowed. Disasm-verified body:
// press-only (msg+0xc > 0; NO novice guard in the binary), set EVERY
// condenser's valveState@0x1D0 = 1 (equal weights), then the shared
// redistribute @0049f788 (== BTRecomputeCondenserValves: flow =
// valve/total + condenserAlarm pulse). Body lives in the condenser-
// complete TU (heatfamily_reslice.cpp bridge BTBalanceCondenserValves).
enum { BalanceCoolantMessageID = 0x16 };
void BalanceCoolantMessageHandler(ReceiverDataMessageOf<int> *message);
// @0049fa00 -- "DuckRequest" (Mech handler table @0x50BDF8, id 0x1a; the
// pod's CROUCH button, RIO address 0x13 -- the manual gives it a whole
// section). The last unregistered id in the Mech table; until now a
// press was silently swallowed by Receiver::Receive.
//
// The binary's ENTIRE body is two lines: press-only (msg+0xc > 0), then
// duckState@0x398 = 1. It is a one-shot REQUEST flag, not a posture
// toggle -- the handler never clears it, and the only other writer in
// the whole binary is the mech reset (0). duckState has NO code reader
// anywhere in the decomp; it is published as attribute 0x37, so whatever
// consumes it consumes it through DATABINDING (the SQUAT clips), not
// through a call. Reconstructed exactly: setting the flag is the
// authentic behaviour, and inventing a crouch here would be a stand-in.
enum { DuckRequestMessageID = 0x1a };
void DuckRequestMessageHandler(ReceiverDataMessageOf<int> *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;
// @0x4ac274 mech side (Gitea #46): the ammo-explosion fan-out -- split the
// total across the zones listing the subsystem as a critical subsystem and
// send one full TakeDamageMessage per zone (inflictingEntity = SELF,
// inflictingSubsystemID = the bin). Defined in mechdmg.cpp; reached from
// MechSubsystem::DistributeCriticalHit via the BTAmmoExplosionFanOut bridge.
void AmmoExplosionFanOut(::Subsystem *inflicting_subsystem,
int inflicting_subsystem_id, Damage &total_damage);
int flags; // entity flags word (this+0x28 region)
Scalar lastInflictingDamage; // task #60: killing-blow magnitude (port-only; reuses
// the retired phantom `stance` slot -- feeds the kill score)
int collisionTemporaryState; // @0x44c per-frame collision accumulator
// (0 none / 1 InitialHit / 2 Slide) -- CORRECTED from
// the "ammoState 0/1 leaking/2 dry" mislabel; the decomp
// (part_012.c:15406-15413) sets it per resolved contact,
// pushed into collisionState each frame. Never read as ammo.
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