Files
BT411/game/reconstructed/mech.cpp
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

3025 lines
135 KiB
C++

//===========================================================================//
// File: mech.cpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: Mech -- BattleMech entity construction, subsystem assembly, and //
// the per-frame replication update. (mech.cpp slice 1 of 4.) //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// --/--/95 ?? Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary BTL4OPT.EXE. Behaviour follows the
// Ghidra pseudo-C for the cluster 0x004a1232 .. 0x004a5027 (the lone function
// the disassembler tagged "bt/mech.cpp" is the 5690-byte ctor @004a1674; the
// surrounding contiguous functions up to mech2.cpp @004a5028 form this slice).
// The string "d:\tesla\bt\bt\MECH.CPP" embedded at @004a1674+0x936 confirms
// the source file.
//
// Names follow RP/VTV.cpp (the structural analog -- player vehicle entity on
// the same JointedMover base) and the surviving BT headers (MECHDMG.HPP,
// MECHTECH.HPP). Member offsets cite this[word] / byte-offset. Each method
// cites its originating @ADDR.
//
// Hex-float / data constants recovered from the image:
// _DAT_004a1670 = 10.0f (re-sync speed threshold)
// _DAT_004a2d38 = 0.6 (double) (heat-capacity fraction)
// _DAT_004a2d40 = 0.05f (update-position scale)
// _DAT_004a2d44 = 0.0174533f (degrees -> radians)
// DAT_005209d0 = FLT_MAX (initial maxSpeed)
// DAT_0052140c = <runtime time-scale global> (frame divisor)
//
// Engine-helper name map (referenced by the decomp):
// FUN_00424e38 JointedMover base constructor
// FUN_00425550 JointedMover base destructor
// FUN_00402298 operator new (Memory::New) FUN_004022d0 delete
// FUN_004023f4 RefCounted::RefCounted FUN_004024d8 Release
// FUN_00402460 CString(const char*) FUN_00402a98 CString assign
// FUN_004022b0 Memory::Allocate
// FUN_0041a1a4 IsDerivedFrom(Derivation*) FUN_0041a058 Enum::Lookup
// FUN_00417ab4 SharedData::Resolve()
// FUN_0041b9ec AlarmIndicator(levels) FUN_0041bbd8 ::SetLevel
// FUN_0041baa4 ~AlarmIndicator
// FUN_0043ad4f FilteredScalar::Initialize(n,v) FUN_0043adb5 ~FilteredScalar
// FUN_00427768 ChainOf<>::ChainOf(owner)
// FUN_00408440 Vector::Copy FUN_0040a7f4 Quat::Copy FUN_0040a938 Quat::Mul
// FUN_0040ab44 Matrix::FromQuat FUN_00408e90 CString::Copy
// FUN_00407064/00406ff8 ResourceFile::Find(segment,id)
// FUN_004032dc/004030dc MemoryStream(open) FUN_00403310 ~MemoryStream
// FUN_00404088/00404118 NotationFile::ReadString/ReadFloat
// FUN_00406db4/00406f3c ResourceFile::WriteResource
// FUN_004dbb24 DebugStream << FUN_0040385c Verify(msg,file,line)
//
#include <bt.hpp>
#pragma hdrstop
#if !defined(MECH_HPP)
# include <mech.hpp>
#endif
#if !defined(MECHSUB_HPP)
# include <mechsub.hpp>
#endif
#if !defined(MECHDMG_HPP)
# include <mechdmg.hpp>
#endif
#if !defined(MECHTECH_HPP)
# include <mechtech.hpp>
#endif
#include <matchlog.hpp> // MP match forensics (DMG event line)
// AUTHENTIC GROUND MODEL ctor half (task #15): complete BoxedSolid type for the
// collisionTemplate/collisionVolume extent reads + the template bottom lift.
#include <BOXSOLID.hpp>
#include <line.hpp> // Line/Ray -- the PickRayHit slab test (task #36)
// STEP 6 cylinder hit-location table. dmgtable.hpp pulls in no subsystem
// headers (only Plug + mechrecon + <vector>), so it is safe here -- unlike the
// real subsystem headers, whose classes collide with mech.cpp's local stubs.
// The torso twist is reached via a BRIDGE (BTGetTorsoTwist, defined in torso.cpp)
// for the same reason.
#include "dmgtable.hpp"
extern Scalar BTGetTorsoTwist(Subsystem *torso); // torso.cpp (Torso complete there)
// heat-bank ambient bridge (heatfamily_reslice.cpp, AggregateHeatSink complete
// there) -- mech.cpp cannot include the subsystem headers (local-stub collision)
extern void BTSetBankAmbientTemperature(Subsystem *bank, Scalar temperature);
#if !defined(BTMSSN_HPP)
# include <btmssn.hpp> // BTMission::GetMissionTemperature (PlayerLink)
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#endif
//
// Tuning constants recovered from .data (see banner).
//
static const Scalar ReSyncSpeedThreshold = 10.0f; // _DAT_004a1670
static const Scalar HeatCapacityFraction = 0.6f; // _DAT_004a2d38
static const Scalar UpdatePositionScale = 0.05f; // _DAT_004a2d40
static const Scalar DegreesToRadians = 0.0174533f; // _DAT_004a2d44
extern Scalar FrameTimeScale; // DAT_0052140c (runtime)
//===========================================================================//
// Reconstruction stand-ins LOCAL to this translation unit.
// The ctor wires ~20 streamed Subsystem subclasses and several resource /
// registry helpers whose real definitions live in sibling modules not
// visible here (and a few -- Cockpit / Actuator / JumpJet / LegSubsystem /
// HeatSinkSource / MechDisplay -- have NO reconstructed class at all). These
// minimal stubs let the factory + construction logic compile faithfully and
// are flagged in the port notes. Kept LOCAL (not in mechrecon.hpp) so they
// never collide with the real sibling classes that include that header.
//===========================================================================//
// Subsystem behaviour the recovered ctor reaches through a Subsystem* that the
// modern engine Subsystem does not expose (located by class-ID / raw offset).
struct SubProxy
{
int IsDerivedFrom(int) { return 0; }
void Start() {}
int capabilityFlags; // +0x334
char *linkTarget; // gyro -> sink coupling
};
// One streamed subsystem segment record.
struct SubsystemSegment
{
int flags; // +0x10
int classID; // streamed component ClassID
int recordSize; // piVar1[9] advance
};
// Resource-segment stream cursor (ResourceFile::Find -> stream).
struct ResourceStream
{
template<class A> ResourceStream(const A &) {}
int SegmentCount() { return 0; }
SubsystemSegment *FirstSegment() { return 0; }
const char *Name() { return ""; }
};
// Memory-stream cursor for the offline streamers (engine MemoryStream ctor
// shape differs; this local proxy carries only the verbs the decomp calls).
struct MemStreamX
{
MemStreamX() {}
template<class...A> MemStreamX(A&&...) {}
void SetMode(int) {}
template<class...A> void Write(A&&...) {}
};
// Streamed-Subsystem stub base.
//
// TODO(bring-up): these stand in for the per-class subsystems whose own
// reconstructed bodies are not yet wired into this TU (and a few -- Cockpit /
// Actuator / JumpJet / LegSubsystem / HeatSinkSource / MechDisplay -- have no
// reconstructed class at all). They now DERIVE FROM THE REAL MechSubsystem
// base (proven: MechTech uses the same ctor) so each streamed roster entry is a
// genuine, registered, vtable-valid Subsystem with a DamageZone -- walkable by
// MechTech / the engine control-mapping & per-frame paths. Their class-specific
// behaviour is still minimal; flagged for per-class reconstruction.
struct ReconSubsystemStub : public MechSubsystem
{
ReconSubsystemStub(Mech *owner, int id, void *res)
: MechSubsystem(owner, id, (MechSubsystem::SubsystemResource *)res) {}
static Recon DefaultData; // ignored extra ctor arg (<Class>::DefaultData)
void AttachToBody() {}
};
Recon ReconSubsystemStub::DefaultData;
// owner/id/resource forwarded to the MechSubsystem base; trailing args
// (e.g. <Class>::DefaultData) are absorbed by the variadic tail.
#define RECON_SUBSYS(NAME) \
struct NAME : ReconSubsystemStub { \
template<class...A> NAME(Mech *o, int id, void *res, A&&...) \
: ReconSubsystemStub(o, id, res) {} }
RECON_SUBSYS(Cockpit);
RECON_SUBSYS(Sensor);
RECON_SUBSYS(Condenser);
RECON_SUBSYS(Generator);
RECON_SUBSYS(PoweredSubsystem);
RECON_SUBSYS(Myomers);
RECON_SUBSYS(Gyro);
RECON_SUBSYS(HeatSinkSource);
RECON_SUBSYS(Actuator);
RECON_SUBSYS(EmitterWeapon);
RECON_SUBSYS(JumpJet);
RECON_SUBSYS(MissileLauncher);
RECON_SUBSYS(BallisticWeapon);
RECON_SUBSYS(GaussRifle);
RECON_SUBSYS(LegSubsystem);
RECON_SUBSYS(HeatableSubsystem);
RECON_SUBSYS(MechDisplay);
// MechWeapon / MechControlsMapper are forward-declared in mech.hpp; complete
// them here as stubs (real bodies live in mechweap.cpp / mechmppr.cpp).
struct MechWeapon : ReconSubsystemStub { template<class...A> MechWeapon(Mech *o, int id, void *res, A&&...) : ReconSubsystemStub(o, id, res) {} };
struct MechControlsMapper : ReconSubsystemStub { template<class...A> MechControlsMapper(Mech *o, int id, void *res, A&&...) : ReconSubsystemStub(o, id, res) {} };
// WAVE 2 factory bridges -- defined in each real subsystem's own .cpp (heat.cpp,
// heatfamily_reslice.cpp, hud.cpp, mechtech.cpp) so the real headers are not pulled
// into this TU (where they would collide with the RECON_SUBSYS stubs above). Each
// constructs the real class with the binary's alloc size and returns a Subsystem*.
extern Subsystem *CreateCondenserSubsystem(Mech *, int, void *); // 0xBBD
extern Subsystem *CreateHeatSinkBankSubsystem(Mech *, int, void *); // 0xBBE
extern Subsystem *CreateReservoirSubsystem(Mech *, int, void *); // 0xBC0
extern void BTRecomputeCondenserValves(Entity *); // @0049f788 (post-init valve distribution)
extern Subsystem *CreateHUDSubsystem(Mech *, int, void *); // 0xBD6
extern Subsystem *CreateMechTechSubsystem(Mech *, int, void *); // 0xBDC
extern Subsystem *CreateGeneratorSubsystem(Mech *, int, void *); // 0xBC1 (WAVE 3a)
extern Subsystem *CreatePoweredSubsystem(Mech *, int, void *); // 0xBC2 (WAVE 3a)
extern Subsystem *CreateEmitterSubsystem(Mech *, int, void *); // 0xBC8/0xBD4 (WAVE 3b)
extern Subsystem *CreateTorsoSubsystem(Mech *, int, void *); // 0xBC5 (WAVE 4 -- base re-based, layout locked)
extern Subsystem *CreateSensorSubsystem(Mech *, int, void *); // 0xBC3 (WAVE 4 readouts -- de-shimmed, layout locked)
extern Subsystem *CreateSearchlightSubsystem(Mech *, int, void *); // 0xBD8 (WAVE 4 readouts -- de-shimmed + gate fixed)
extern Subsystem *CreateThermalSightSubsystem(Mech *, int, void *); // 0xBDE (WAVE 4 readouts -- de-shimmed + gate fixed)
extern Subsystem *CreateAmmoBinSubsystem(Mech *, int, void *); // 0xBCB (WAVE 4 -- AmmoBin : HeatWatcher, layout locked)
extern Subsystem *CreateMyomersSubsystem(Mech *, int, void *); // 0xBC6 (WAVE 6 -- mover-coupled drive; INERT, gated BT_MYOMERS)
extern Subsystem *CreateProjectileWeaponSubsystem(Mech *, int, void *); // 0xBCD (WAVE 7 -- ammoBinLink 0xC, layout locked; FireWeapon no-spawn Phase A)
extern Subsystem *CreateMissileLauncherSubsystem(Mech *, int, void *); // 0xBD0 (WAVE 7 -- shadows deleted, layout locked; FireWeapon no-spawn Phase A)
extern Subsystem *CreateGaussRifleSubsystem(Mech *, int, void *); // 0xBCE (WAVE 7 -- Emitter subclass, no-op FireWeapon; dedicated bridge)
extern Subsystem *CreateGyroSubsystem(Mech *, int, void *); // 0xBC4 (task #56 -- ctor/integrators byte-exact, ENABLED)
extern void GyroFrameJointWrite(Subsystem *, Scalar, int, int); // task #56 -- mech-performance joint-write dispatch (mech4.cpp)
extern void GyroBindExternalPitch(Subsystem *, Scalar *); // task #56 -- post-stream gyro+0x258 = &torso twist
extern void GyroApplyDamage(Subsystem *, const Damage &); // task #56 -- the @0x4a0264 hit-bounce fan-out
extern Scalar *BTGetTorsoTwistAddr(Subsystem *); // torso.cpp bridge (complete-type TU)
// Post-stream entity glue + cosmetic objects.
struct StandingAnimation { template<class...A> StandingAnimation(A&&...) {} };
// MechDeathHandler is now the REAL class (mechdmg.hpp) -- the local stub was removed.
struct StabilityMessage { StabilityMessage() {} };
// "Mechs" object directory in the global registry (decomp registry verbs).
// (Named ReconRegistryT to avoid the real engine Registry class.)
struct MechDirectory { template<class A> void Add(A) {} };
struct MechDirectoryIterator
{
template<class A> MechDirectoryIterator(A) {}
void *Current() { return 0; }
void Remove() {}
};
struct ReconRegistryT
{
MechDirectory *Find(const char *) { return 0; }
MechDirectory *Create(const char *) { return 0; }
void Destroy(MechDirectory *){}
};
inline ReconRegistryT *ReconRegistry() { static ReconRegistryT r; return &r; }
// Free helpers (engine routines with no modern analog under these names).
// NB: ResolveJoint is now a real Mech member (below) backed by the engine
// segment/joint API -- it is no longer a free stand-in.
template<class...A> inline Recon LookupVideoObject(A&&...) { return Recon(); }
template<class...A> inline int ResourceFindByName(A&&...) { return 0; }
// (LoadLowDetailBody / LoadHighDetailBody removed task #59 -- they were no-op
// stubs mislabeling the FUN_004a80d4/86c8 GAIT-CLIP loaders as a body-LOD
// pair; the authentic clip-set gate now calls the real loaders.)
template<class...A> inline void FinishConstruction(A&&...) {}
template<class...A> inline void ResetPose(A&&...) {}
template<class...A> inline void SetStatusState(A&&...) {}
template<class...A> inline void BroadcastToTeam(A&&...) {}
//---------------------------------------------------------------------------//
// Mech::ResolveJoint (FUN_00424b60) -- the shared "name -> live Joint*" resolver
// the Torso ctor (@004b6b0c, part_013.c:5055-5080) and Gyroscope ctor (@004b3778,
// part_013.c:2904-2920) inline verbatim. It walks the inherited JointedMover
// segment table by name (GetSegment == FUN_00424b60/JMOVER.cpp:68), then fetches
// the live Joint by the segment's PUBLIC joint index -- so the protected
// EntitySegment::jointPointer is never touched. Returns NULL when the node is
// absent (the binary trusts a valid name; callers guard on NULL for bring-up).
//---------------------------------------------------------------------------//
Joint*
Mech::ResolveJoint(const char *joint_name)
{
Check(this);
EntitySegment *segment = GetSegment(CString(joint_name)); // inherited, FUN_00424b60
if (segment == NULL)
{
return NULL;
}
JointSubsystem *joints = GetJointSubsystem(); // inherited (JMOVER.h:80)
Check_Pointer(joints);
return joints->GetJoint(segment->GetJointIndex()); // SEGMENT.h:163 + JOINT.h:217
}
//---------------------------------------------------------------------------//
// UpdateShadowJoint (task #20) -- pose the flat *_tshd shadow proxy to the
// ground. The binary's dormant Simulate wrote the ShadowJointName joint
// ('jointshadow', balltranslate under ROOT; SKL comment: "apply terrain angle
// to pitch and roll") from the terrain-follow query's surface normal; torso
// yaw is a SEPARATE channel (child 'jointtshadow', written by Torso).
// PLUMBING NOTE: until the ground decode (task #15) supplies real surface
// normals, the caller passes the flat up-normal -> pitch/roll = 0 -> the
// write is a faithful no-op on flat ground. Yaw is preserved (left 0 here).
//---------------------------------------------------------------------------//
void
Mech::UpdateShadowJoint(const Vector3D &ground_normal)
{
if (shadowJointNode == NULL) // mech has no shadow proxy
{
return;
}
// APPLY THE TERRAIN ANGLE (task #49b -- the SKL's own contract for this
// joint: "apply terrain angle to pitch and roll"). ground_normal is the
// surface normal in the MECH-LOCAL frame (the caller rotates the world
// gradient by -yaw); pose the quad so its up (+Y) aligns with it and the
// quad LIES ON the slope. A flat quad + a big depth-bias was the previous
// workaround for slope burial, but a bias big enough to beat the terrain
// also beat the mech's FEET (the shadow painted OVER them); with the tilt
// the bias drops to a decal epsilon and the feet layer correctly.
// Cliff guard: cap the tilt (~35 deg) so a bogus gradient from probing
// across a cliff edge can't wrench the quad vertical.
Vector3D n = ground_normal;
if (n.y < 0.82f) // cap ~35 deg
{
Scalar xz = (Scalar)sqrtf((float)(n.x * n.x + n.z * n.z));
if (xz > 1e-6f)
{
const Scalar s = 0.5735f / xz; // sin(35 deg) / |xz|
n.x *= s; n.z *= s; n.y = 0.8192f; // cos(35 deg)
}
else
{
n.x = 0.0f; n.z = 0.0f; n.y = 1.0f;
}
}
// Convention-proof: build the tilted ORTHONORMAL BASIS (quad up = n) and let
// the ENGINE's own LinearMatrix -> EulerAngles conversion produce the angles
// (hand-derived Euler signs are exactly how the original tilt attempt "dug
// into the hillside"). Basis: Y = n; Z = world Z projected off n (so the
// tilt carries no yaw; jointtshadow handles torso-twist yaw separately);
// X = Y x Z (right-handed, matches the identity basis).
{
Vector3D zx(0.0f - n.x * n.z, 0.0f - n.y * n.z, 1.0f - n.z * n.z);
Scalar zl = (Scalar)sqrtf((float)(zx.x * zx.x + zx.y * zx.y + zx.z * zx.z));
if (zl < 1e-6f) { zx.x = 0.0f; zx.y = 0.0f; zx.z = 1.0f; zl = 1.0f; }
zx.x /= zl; zx.y /= zl; zx.z /= zl;
Vector3D xx(n.y * zx.z - n.z * zx.y,
n.z * zx.x - n.x * zx.z,
n.x * zx.y - n.y * zx.x);
LinearMatrix tiltM(1); // identity ctor
tiltM.SetFromAxis(X_Axis, UnitVector(xx.x, xx.y, xx.z));
tiltM.SetFromAxis(Y_Axis, UnitVector(n.x, n.y, n.z));
tiltM.SetFromAxis(Z_Axis, UnitVector(zx.x, zx.y, zx.z));
EulerAngles tilt;
tilt = tiltM; // the engine's own conversion
shadowJointNode->SetRotation(tilt);
}
}
//---------------------------------------------------------------------------//
// Offline-tool path proxies (CreateModelResource / CreateControlMappingStream /
// CreateDamageZoneStream). The recovered authoring code drives ResourceFile /
// ResourceDirectories / NotationFile through an invented verb/field set that
// the modern engine spells differently; these local proxies (cast onto the
// real incoming pointers) carry that surface so the recovered logic compiles.
//---------------------------------------------------------------------------//
struct RStr // CString-like path builder (dir + name)
{
RStr() {}
template<class A> RStr(const A &) {}
template<class A> RStr operator+(const A &) const { return RStr(); }
operator CString() const { return CString(); }
operator const char *() const { return ""; }
};
struct DirsX { RStr skeletonDir, gameDataDir, modelDir; }; // (cast of ResourceDirectories*)
struct RDesc { ResourceDescription::ResourceID id; }; // resource-write result
struct RFileX // (cast of ResourceFile*)
{
template<class...A> RDesc *Find(A&&...) { return 0; }
template<class...A> RDesc *WriteResource(A&&...) { return 0; }
};
struct Releasable { void Release() {} };
struct NotationEntry { const char *Name() { return ""; } NotationEntry *Next() { return 0; } };
struct NotationList { int Count() { return 0; } NotationEntry *First() { return 0; } void Release() {} };
struct NotationIterator { template<class A> NotationIterator(const A &) {} void Release() {} };
struct NoteX // (cast of NotationFile*) invented verbs
{
template<class...A> int Count(A&&...) { return 0; }
template<class...A> int ReadString(A&&...) { return 0; }
template<class...A> NotationList *List(A&&...) { return 0; }
};
struct CMapTable // control-mapping table blob
{
int count;
ControlsMapping entries[1];
int byteSize() { return 0; }
};
template<class...A> inline NotationFile *OpenNotation(A&&...) { return 0; }
template<class...A> inline void CloseNotation(A&&...) {}
template<class...A> inline void ParseVector(A&&...) {}
template<class...A> inline int ParseJointResource(A&&...) { return 0; }
template<class...A> inline int CreateModelResourceBase(A&&...){ return 0; }
template<class...A> inline void StreamDamageZone(A&&...) {}
// task #7: the factory-case enum under its true name (0xBD3 was mislabeled
// MechControlsMapperID; the mapper never streams).
enum { SubsystemMessageManagerID = 0xBD3 };
class SubsystemMessageManager;
extern Subsystem *CreateMessageManagerSubsystem(Mech *owner, int id, void *seg);
template<class...A> inline void StreamDamageLookup(A&&...) {}
//---------------------------------------------------------------------------//
// REAL resource streaming (bring-up). Replaces the mechrecon no-op ResourceFind
// shim on the construction path with the genuine engine idiom used by the
// JointedMover base ctor and by RP/VTV::VTV:
// application->GetResourceFile()->SearchList(resourceID, type)
// then a MemoryStream over the locked resource's address/size. The mech's
// model resource ID is the creation_message->resourceID the base ctor already
// used to stream the skeleton (so we know the .res list is populated for it).
//
// FUN_00407064 / FUN_00406ff8 ResourceFile::Find(segment,id) == SearchList
//---------------------------------------------------------------------------//
static ResourceDescription *
MechFindResource(
ResourceDescription::ResourceID resource_id,
ResourceDescription::ResourceType type
)
{
ResourceFile *resource_file = application->GetResourceFile();
Check(resource_file);
return resource_file->SearchList(resource_id, type); // NULL for absent optional segments
}
//---------------------------------------------------------------------------//
// Mech::Zone -- typed access to the inherited Entity::damageZones[]. The engine
// stores base DamageZone*; every entry the Mech ctor builds is a Mech__DamageZone
// (single-inheritance subclass), so the downcast is identity-pointer-safe.
//---------------------------------------------------------------------------//
Mech__DamageZone *
Mech::Zone(int i) const
{
return static_cast<Mech__DamageZone *>(damageZones[i]);
}
//###########################################################################
//#############################################################################
// Shared Data Support
//
Derivation
Mech::ClassDerivations( // @0050bdb4
JointedMover::GetClassDerivations(),
"Mech"
);
// Mech's OWN handler table, chained to the parent's (Entity's, which Mover/
// JointedMover use directly -- they add none). It OVERRIDES Entity's TakeDamage
// with the cylinder hit-location resolver (STEP 6): Build() overlays the derived
// entry onto the inherited table by message ID (RECEIVER.cpp), so every other
// inherited message still routes to its base handler. (Previously this was a
// bare copy of Entity's set -- base TakeDamage dropped unaimed/-1 zone hits.)
const Receiver::HandlerEntry
Mech::MessageHandlerEntries[] =
{
MESSAGE_ENTRY(Mech, TakeDamage),
MESSAGE_ENTRY(Mech, PlayerLink),
MESSAGE_ENTRY(Mech, BalanceCoolant), // id 0x16 @0049f728 (issue #20)
MESSAGE_ENTRY(Mech, DuckRequest), // id 0x1a @0049fa00 (CROUCH, 2026-07-26)
};
//
// @0049f728 -- BalanceCoolant (id 0x16): the auto-cooling-balance button.
// Disasm-verified: press-only guard (NO novice lockout in the binary), then
// equal-weight every condenser valve + redistribute via the condenser-
// complete bridge. Issue #20 fix, 2026-07-21.
//
void
Mech::BalanceCoolantMessageHandler(ReceiverDataMessageOf<int> *message)
{
if (message->dataContents <= 0) // press only (msg+0xc)
{
return;
}
extern void BTBalanceCondenserValves(Entity *owner); // heatfamily_reslice.cpp
BTBalanceCondenserValves(this);
if (getenv("BT_VALVE_LOG"))
DEBUG_STREAM << "[valve] BALANCE (all valves -> 1)" << std::endl;
}
//
// @0049fa00 -- DuckRequest (id 0x1a): the CROUCH button (RIO 0x13). The
// binary's whole body, verbatim:
//
// if (0 < *(int *)(message + 0xc)) *(undefined4 *)(this + 0x398) = 1;
//
// this+0x398 is duckState (attribute 0x37) -- the mech reset is the only other
// writer in the binary (part_012.c:9439, the same reset that zeroes
// incomingLock and sets distanceToMissile = FLT_MAX, which is how we already
// map that region). Nothing in the decompiled code READS it: it is published
// as an attribute, so its consumer is a databinding (the SQUAT clips), not a
// call. So this handler is complete as-is -- the flag IS the feature's code
// side, and a crouch implementation here would be a stand-in for data we have
// not yet identified. See context/pod-hardware.md.
//
void
Mech::DuckRequestMessageHandler(ReceiverDataMessageOf<int> *message)
{
if (message->dataContents <= 0) // press only (msg+0xc)
{
return;
}
duckState = 1;
DEBUG_STREAM << "[duck] DuckRequest: duckState -> 1" << std::endl << std::flush;
}
Receiver::MessageHandlerSet
Mech::MessageHandlers(
ELEMENTS(Mech::MessageHandlerEntries),
Mech::MessageHandlerEntries,
Entity::GetMessageHandlers());
//#############################################################################
// STEP 6 -- cylinder hit-location support (consumed by dmgtable.cpp)
//#############################################################################
//
// World -> mech-local point transform (binary FUN_00408bf8 reading owner+0xd0).
// Uses the inherited Entity::localToWorld (the render/collision object matrix);
// MultiplyByInverse gives the world->local change of coordinates.
//
Point3D
Mech::WorldToLocal(const Point3D &world)
{
Point3D local;
local.MultiplyByInverse(world, localToWorld);
return local;
}
//
// Ray pick against this mech (task #36 -- the engine Reticle's "pick point
// intersection testing"): transform the world ray into the mech's local frame
// (rotation part only for the direction), slab-test it against the collision
// template's ExtentBox via the engine's own BoundingBox::HitBy (which clips
// the Line's length to the entry distance), and return the world hit point.
// The same collision volume the movement/collision code uses; the hit point
// then drives the cylinder hit-location table at damage delivery (aimed
// shots resolve to the zone under the crosshair).
//
Logical
Mech::PickRayHit(const Point3D &start, const Vector3D &dir,
Scalar max_range, Point3D *hit_world)
{
BoxedSolid *tmpl = GetCollisionTemplate();
if (tmpl == 0)
return False;
// world -> local: point via the full inverse, direction via the inverse
// rotation (transform a second point and difference -- exact for the
// orthonormal localToWorld, no scale).
Point3D localStart;
localStart.MultiplyByInverse(start, localToWorld);
Point3D worldTip(start.x + dir.x, start.y + dir.y, start.z + dir.z);
Point3D localTip;
localTip.MultiplyByInverse(worldTip, localToWorld);
Vector3D localDir(localTip.x - localStart.x,
localTip.y - localStart.y,
localTip.z - localStart.z);
Scalar dlen = (Scalar)Sqrt(localDir.x*localDir.x + localDir.y*localDir.y
+ localDir.z*localDir.z);
if (dlen < 1e-6f)
return False;
localDir.x /= dlen; localDir.y /= dlen; localDir.z /= dlen;
BoundingBox box(*(const ExtentBox *)tmpl); // the template's extents
Line ray(localStart, UnitVector(localDir.x, localDir.y, localDir.z), max_range);
const Logical hitOK = box.HitBy(&ray); // clips ray.length to the entry
if (BTEnvOn("BT_AIM_LOG", 0))
{
static int s_n = 0;
if ((++s_n % 60) == 1)
DEBUG_STREAM << "[pick] box=(" << tmpl->minX << ".." << tmpl->maxX
<< ", " << tmpl->minY << ".." << tmpl->maxY
<< ", " << tmpl->minZ << ".." << tmpl->maxZ
<< ") lstart=(" << localStart.x << "," << localStart.y << "," << localStart.z
<< ") ldir=(" << localDir.x << "," << localDir.y << "," << localDir.z
<< ") hit=" << (int)hitOK << " len=" << ray.length << "\n" << std::flush;
}
if (!hitOK)
return False;
if (hit_world != 0)
{
Point3D localHit;
ray.Project(ray.length, &localHit); // start + length*dir (local)
hit_world->Multiply(localHit, localToWorld); // local -> world
}
return True;
}
//
// World-STRUCTURE boresight pick. Ray-test the zone's STATIC collision solid
// tree (the world geometry -- garages, walls, props -- that the mech's walk
// already collides against) along the boresight, and hand back the entry point
// of the closest structure the ray strikes. This is the authentic non-mech
// world pick: Mover::FindStaticSolidHitBy is the "test against the static world"
// tail of the engine's own FindBoxedSolidHitBy (MOVER.cpp), which is what the
// mover-vs-world collision uses -- so a shot lands on EXACTLY the geometry that
// blocks the walk (the user's "structures block the mech, but weapons don't").
//
// The ray is WORLD-space (the static tree is world-space, unlike a mech's local
// collision template). FindBoundingBoxHitBy clips the Line's length to the
// entry distance (BoundingBox::HitByBounded -> line->length = enter), so
// line->FindEnd yields the world entry point on the struck solid.
//
Logical
Mech::WorldStructurePick(const Point3D &start, const Vector3D &dir,
Scalar max_range, Point3D *hit_world,
Entity **hit_owner)
{
Scalar dlen = (Scalar)Sqrt(dir.x*dir.x + dir.y*dir.y + dir.z*dir.z);
if (dlen < 1e-6f)
return False;
Line ray(start, UnitVector(dir.x/dlen, dir.y/dlen, dir.z/dlen), max_range);
BoxedSolid *solid = FindStaticSolidHitBy(&ray); // static world only
if (solid == 0)
return False;
if (hit_world != 0)
ray.FindEnd(hit_world); // clipped length -> entry point
if (hit_owner != 0)
{
// The struck solid's owning ENTITY (issue #3, item b). Every static solid
// carries its owner Simulation in the tag pointer (BoxedSolid ctor;
// TERRAIN.cpp:107/246 pass the Terrain/CulturalIcon `this`, DOOR.cpp:395
// the Door) and Entity derives from Simulation at offset 0, so this is the
// struck map entity. The binary designates/damages the struck ENTITY:
// the weapon dispatch @004b9728 (part_013.c:6765) gates ONLY "target not
// derived from Mech@0x50bdb4 OR aimed zone set" -- a non-Mech target is
// damaged unconditionally -- and the missile contact @004be078 dispatches
// a zone=-1 TakeDamageMessage at the struck solid's owner with no class
// test at all.
*hit_owner = (Entity *)solid->GetOwningSimulation();
}
return True;
}
//
// The roster torso (sinkSourceSubsystem @0x438, ClassID 0xBC5) -- parity with
// the binary's *(mech+0x438).
//
void *
Mech::TorsoOrientationSource()
{
return GetTorsoSubsystem();
}
//
// Live torso twist (yaw) in radians -- binary torso+0x1d8. Reached via the
// torso.cpp bridge (Torso is a complete type there; including torso.hpp here
// would collide with mech.cpp's local subsystem stubs). 0 for no/fixed torso.
//
Scalar
Mech::TorsoHeading()
{
return BTGetTorsoTwist(GetTorsoSubsystem());
}
//
// Height reference the cylinder table normalises impact height against -- the
// binary's *(mech+0x2ec)+0xc, a stance height written from mech+0x518 (standing)
// / +0x51c (ducked). standingTemplateMaxY is that value (collisionTemplate->maxY
// captured at ctor); fall back to the live template if the ground model was off
// when the ctor ran (standingTemplateMaxY left 0).
//
Scalar
Mech::CylinderReferenceHeight()
{
if (standingTemplateMaxY > 0.0f)
{
return standingTemplateMaxY;
}
BoxedSolid *tmpl = GetCollisionTemplate();
return tmpl ? tmpl->maxY : 0.0f; // ResolveHit guards <= 0
}
//
// Mech override of Entity::PlayerLinkMessageHandler (binary @0049f624). The
// engine base resolves the FORWARD link (mech->playerLink @0x190 from the
// message's playerID); BT then:
// 1. sets the REVERSE link player->playerVehicle = this -- on EVERY node,
// replicants included. This is how the 1995 camera director
// (BeABTDirector's GetPlayerByIndex -> GetPlayerVehicle) and the
// scoreboard find a REMOTE player's mech; without it a spectator seat
// parks forever ("no goal entity", camera-seat bring-up 2026-07-18).
// 2. clears the player's NonScoringPlayerFlag (0x4000 == bit 14): a pilot
// with a vehicle is a SCORING player -- this is what admits them to the
// ranking pass (Player::IsScoringPlayer gates RankPlayers).
// 3. master only: seeds the heat bank's ambientTemperature (@0x1d4 on the
// 0xBBE AggregateHeatSink at mech+0x7dc) from the mission's [mission]
// temperature (BTMission+0xf4) -- resolving the heat family's
// "frozen 300" deviation note: it was never frozen, THIS writes it.
//
void
Mech::PlayerLinkMessageHandler(PlayerLinkMessage *message)
{
Check(message);
Entity::PlayerLinkMessageHandler(message); // FUN_0041fd18: mech+0x190
Player *owner = GetPlayerLink();
if (getenv("BT_CAM_LOG"))
DEBUG_STREAM << "[cam] Mech::PlayerLink inst="
<< (int)(GetInstance() == ReplicantInstance)
<< " owner=" << (void*)owner << "\n" << std::flush;
//
// #45 observability: did the one-shot PlayerLink actually RESOLVE on this node?
// `Entity::playerLink` is written only here and rides no update record, so a
// replicant with resolved=0 is the "barrier 1" hazard -- it would silently skip
// that pilot's kill credit and leave no other trace. One row per link message,
// so this cannot spam the matchlog.
//
if (BTMatchLogActive())
BTMatchLog("PLAYER_LINK",
"mech=%d:%d inst=%c playerID=%d:%d resolved=%d",
BTMatchHostOf(GetEntityID()), (int)GetEntityID(),
(GetInstance() == ReplicantInstance) ? 'R' : 'M',
BTMatchHostOf(message->playerID), (int)message->playerID,
(int)(owner != 0));
if (owner != NULL)
{
owner->SetPlayerVehicle(this); // player+0x1fc = this
owner->simulationFlags &= ~Player::NonScoringPlayerFlag; // &= ~0x4000
if (GetInstance() != ReplicantInstance && sensorSubsystem != NULL)
{
Mission *mission = owner->GetMission();
if (mission != NULL)
{
BTSetBankAmbientTemperature(sensorSubsystem,
((BTMission *)mission)->GetMissionTemperature());
}
}
}
Check_Fpu();
}
//
// Mech override of Entity::TakeDamageMessageHandler (binary @0x4a037a, the two
// call sites into the glue @0x49ed0c). An unaimed hit arrives with
// invalidDamageZone set (damageZone < 0); resolve its zone from the cylinder
// hit-location table (mech[0x111]) using the impact point, clear the flag, then
// hand off to the base handler which routes damageZones[zone]->TakeDamage. Aimed
// (reticle) hits carry a valid zone and pass straight through.
//
void
Mech::TakeDamageMessageHandler(TakeDamageMessage *message)
{
Check(message);
// MP DIAGNOSTIC (task #47): confirm the handler runs on the OWNING MASTER
// for a cross-pod (network-delivered) TakeDamage + what state it carries.
if (getenv("BT_MP_NET"))
DEBUG_STREAM << "[mp-hdlr] TakeDamageHandler this=" << (void*)this
<< " inst=" << (int)GetInstance()
<< " invalidZone=" << (int)message->invalidDamageZone
<< " amount=" << message->damageData.damageAmount
<< " type=" << (int)message->damageData.damageType
<< " from=" << message->inflictingEntity
<< " at(" << message->damageData.impactPoint.x
<< "," << message->damageData.impactPoint.y
<< "," << message->damageData.impactPoint.z << ")"
<< " table=" << (void*)damageLookupTable
<< " zones=" << damageZoneCount << "\n" << std::flush;
// Binary @0x4a0264-0x4a0300 (hub FUN_004a0230): the cockpit hit-BOUNCE.
// Feed the gyro the raw Damage record FIRST -- before inflictor
// bookkeeping, threat feed and zone resolution (an invalid-zone hit still
// bounces). Sole gate: non-null gyro (mech+0x528). The fan-out no-ops
// CollisionDamageType(0) and damageAmount==0 itself (@4b298c/@4b299e); on
// a replicant the binary only WARNS ("Replicant Mech recieving
// takedamagemessage!", MECH.CPP:986) and proceeds -- no replicant gate.
if (gyroSubsystem != 0)
GyroApplyDamage(gyroSubsystem, message->damageData);
// Maintain the last-attacker bookkeeping (mech[0x43c]): read by the
// DamageZone LOD router (same-attacker redirect reuse, mechdmg.cpp:374)
// and by the damage-band effect orientation. Was declared but never
// WRITTEN -- a reconstruction gap; the natural authentic write site is
// this handler (every damage message carries the inflictor). [T3]
lastInflictingID = message->inflictingEntity;
// task #60: latch the killing-blow magnitude alongside the attacker id so the
// kill score carries the REAL finishing-hit damage (mirrors the per-hit path
// mech4.cpp:1207 that already passes p.damage), not the flat kShotDamage=12.
lastInflictingDamage = message->damageData.damageAmount;
// THREAT feed (task #37, the recovered reticle Execute's ThreatVector
// attr 0xC): when the PLAYER takes a hit, push the attack direction --
// world (x,z) from the mech toward the impact point -- onto the HUD's
// compass-rose threat trail (fresh marks red, fading over 6s).
if (application != 0 && (Entity *)this == application->GetViewpointEntity())
{
extern void BTPushHudThreat(float wx, float wz);
BTPushHudThreat(
(float)(message->damageData.impactPoint.x - localOrigin.linearPosition.x),
(float)(message->damageData.impactPoint.z - localOrigin.linearPosition.z));
}
DamageLookupTable *table = (DamageLookupTable *)damageLookupTable; // named member (Wword absorbs!)
if (message->invalidDamageZone && table != 0)
{
int zone = table->ResolveHit(message->damageData.impactPoint);
message->damageZone = zone;
message->invalidDamageZone = False;
if (BTEnvOn("BT_CYL_LOG", 0))
{
DEBUG_STREAM << "[cyl] unaimed hit -> zone " << zone
<< " (impact " << message->damageData.impactPoint.x << ","
<< message->damageData.impactPoint.y << ","
<< message->damageData.impactPoint.z << ")\n" << std::flush;
}
}
Entity::TakeDamageMessageHandler(message); // base: damageZones[zone]->TakeDamage
// MP MATCH FORENSICS (matchlog.hpp): the victim-side authoritative damage
// application -- one line per applied TakeDamage with the resolved zone
// and the zone's post-application level. tools/matchcheck.py reconciles
// these against every peer's shooter-side FIRE/PROJ/SPLASH/RAM records.
if (BTMatchLogActive())
{
int zone = message->damageZone;
Scalar level = -1.0f;
if (zone >= 0 && zone < damageZoneCount
&& damageZones != 0 && damageZones[zone] != 0)
level = damageZones[zone]->damageLevel;
BTMatchLog("DMG",
"victim=%d:%d inst=%c from=%d:%d type=%d amt=%.3f burst=%d "
"zone=%d lvl=%.4f pos=%.1f,%.1f,%.1f",
BTMatchHostOf(GetEntityID()), (int)GetEntityID(),
(GetInstance() == ReplicantInstance) ? 'R' : 'M',
BTMatchHostOf(message->inflictingEntity),
(int)message->inflictingEntity,
(int)message->damageData.damageType,
(float)message->damageData.damageAmount,
(int)message->damageData.burstCount,
zone, (float)level,
(float)message->damageData.impactPoint.x,
(float)message->damageData.impactPoint.y,
(float)message->damageData.impactPoint.z);
}
}
//
// Mech attribute table. DENSE PREFIX 0x15..0x21 (JointedMover::NextAttributeID
// through LinearSpeed): the built index is a dense array and Find strcmps every
// slot, so the table must be contiguous from the parent's NextAttributeID up to
// the highest id published -- no gaps. The ids the BLH cockpit does not consume
// (collision/eyepoint/reticle/footstep/anim state) bind to the shared read-only
// attrPad so the slot is a valid, named, non-crashing entry; CurrentSpeed and
// MaxRunSpeed bind to the existing gait members; LinearSpeed binds to the live
// forward-speed member the speed gauges read. (Ids 0x22..0x38 remain declared in
// the enum for later extension -- e.g. RadarRange/DuckState when the map/duck
// widgets are reconstructed -- but are NOT published yet.)
//
const Mech::IndexEntry
Mech::AttributePointers[]=
{
ATTRIBUTE_ENTRY(Mech, MaxAcceleration, attrPad), // 0x15
ATTRIBUTE_ENTRY(Mech, CollisionState, collisionState), // 0x16 real StateIndicator (audio impact/scrape watcher)
ATTRIBUTE_ENTRY(Mech, CollisionNormal, attrPad), // 0x17
ATTRIBUTE_ENTRY(Mech, CollisionSpeed, collisionSpeed), // 0x18 (F17: |impact velocity|, binary @0x4B4)
ATTRIBUTE_ENTRY(Mech, CollisionMaterialType, attrPad), // 0x19
ATTRIBUTE_ENTRY(Mech, CurrentSpeed, legCycleSpeed), // 0x1a (existing @0x348)
ATTRIBUTE_ENTRY(Mech, MaxRunSpeed, reverseStrideLength), // 0x1b (existing @0x34c = run/top speed)
ATTRIBUTE_ENTRY(Mech, EyepointRotation, eyepointRotation), // 0x1c (real member -- the eye reads it per frame)
ATTRIBUTE_ENTRY(Mech, TargetReticle, targetReticle), // 0x1d (real Reticle struct -- task #36)
ATTRIBUTE_ENTRY(Mech, FootStep, footStep), // 0x1e real pulse (AudioLogicalTrigger; one rising edge per foot plant)
ATTRIBUTE_ENTRY(Mech, AnimationState, animationState), // 0x1f real StateIndicator (audio state-watcher binds + AddAudioWatcher)
ATTRIBUTE_ENTRY(Mech, ReplicantAnimationState, replicantAnimationState), // 0x20 real StateIndicator
ATTRIBUTE_ENTRY(Mech, LinearSpeed, linearSpeed), // 0x21 (live forward speed)
ATTRIBUTE_ENTRY(Mech, ClimbRate, attrPad), // 0x22
ATTRIBUTE_ENTRY(Mech, AccelerationLastFrame, attrPad), // 0x23
ATTRIBUTE_ENTRY(Mech, AngularSpeed, attrPad), // 0x24
ATTRIBUTE_ENTRY(Mech, ArmorDamageLevel, attrPad), // 0x25
ATTRIBUTE_ENTRY(Mech, SubsystemDamageLevel, attrPad), // 0x26
ATTRIBUTE_ENTRY(Mech, MyomerDamageLevel, attrPad), // 0x27
ATTRIBUTE_ENTRY(Mech, TestButton1, attrPad), // 0x28
ATTRIBUTE_ENTRY(Mech, TestButton2, attrPad), // 0x29
ATTRIBUTE_ENTRY(Mech, TestButton3, attrPad), // 0x2a
ATTRIBUTE_ENTRY(Mech, TestButton4, attrPad), // 0x2b
ATTRIBUTE_ENTRY(Mech, TestButton5, attrPad), // 0x2c
ATTRIBUTE_ENTRY(Mech, TestButton6, attrPad), // 0x2d
ATTRIBUTE_ENTRY(Mech, ReduceButton, reduceButton), // 0x2e (F17: cab REDUCE, binary @0x340)
ATTRIBUTE_ENTRY(Mech, RadarRange, radarRange), // 0x2f (radar scale)
ATTRIBUTE_ENTRY(Mech, RadarLinearPosition, radarLinearPosition), // 0x30 (Point3D* -> localOrigin)
ATTRIBUTE_ENTRY(Mech, RadarAngularPosition, radarAngularPosition),// 0x31 (Quaternion* -> localOrigin)
ATTRIBUTE_ENTRY(Mech, RearFiring, rearFiring), // 0x32 (task #68: binary id 50 @0x410)
ATTRIBUTE_ENTRY(Mech, RequestDuckAnimation, attrPad), // 0x33
ATTRIBUTE_ENTRY(Mech, UnstablePercentage, unstablePercentage), // 0x34 (task #66: the instability alarm; binary id 52 @0x3F0)
ATTRIBUTE_ENTRY(Mech, SuperStop, attrPad), // 0x35
// (AUDIO_FIDELITY F7) real missile-alarm backing: binary ids 54/56 @0x3fc/
// 0x400 [T1 table @0x50c0b0/0x50c0d0]; the authored beeper (match 1/0) +
// tempo scale (range 100..800 -> tempo 600..10) read these. Driven by
// Missile::MoveAndCollide via BTReportIncomingMissile (mech4.cpp).
ATTRIBUTE_ENTRY(Mech, IncomingLock, incomingLock), // 0x36
ATTRIBUTE_ENTRY(Mech, DuckState, duckState), // 0x37 (crouch posture, int)
ATTRIBUTE_ENTRY(Mech, DistanceToMissile, distanceToMissile) // 0x38
};
Mech::AttributeIndexSet&
Mech::GetAttributeIndex()
{
static Mech::AttributeIndexSet attributeIndex(
ELEMENTS(Mech::AttributePointers),
Mech::AttributePointers,
JointedMover::GetAttributeIndex()
);
return attributeIndex;
}
Mech::SharedData
Mech::DefaultData( // @0050bde4
&Mech::ClassDerivations,
Mech::MessageHandlers,
Mech::GetAttributeIndex(),
Mech::StateCount,
(Entity::MakeHandler)Mech::Make // Entity__SharedData adds the make-callback
);
//###########################################################################
//###########################################################################
// Construction
//###########################################################################
//###########################################################################
//#############################################################################
// Mech::Make -- factory. @004a2d48
//
// Allocates a 0x854-byte Mech and runs the constructor with DefaultData.
//
Mech*
Mech::Make(MakeMessage *creation_message)
{
Mech *mech = (Mech *)Memory::Allocate(sizeof(Mech)); // FUN_00402298(0x854)
if (mech == 0)
{
return 0;
}
new (mech) Mech(creation_message, DefaultData); // FUN_004a1674
// P6 bring-up (multiplayer): the port's MakeReady/CheckLoad validity handshake
// is a partial implementation, so a mech is never authentically validated. An
// INVALID entity DEFERS every dispatched message forever (ENTITY.cpp
// Entity::Receive -> event->Defer), which silently drops cross-pod TakeDamage
// at the owning MASTER (task #47: B receives + resolves the hit to its own mech,
// classID 3001, but Entity::Receive sees valid=0 and defers it). It also stalls
// a REPLICANT's network UPDATES and churns the peer's CreatingMission queue. The
// reconstructed ctor builds the whole mech synchronously, so validating here is
// safe for BOTH instances (a master must be valid to take cross-pod damage; a
// replicant to apply updates). The authentic MakeReady flow is future work.
if (mech != 0)
{
mech->SetValidFlag();
}
return mech;
}
//#############################################################################
// Mech::Mech -- the heart of the entity. @004a1674 (5690 bytes)
//
// Chains to the JointedMover base ctor, installs the Mech vtable, primes
// every member, then walks the model's segment table to instantiate and wire
// up the full subsystem roster (power, heat, weapons, actuators, controls,
// tech, damage zones). This is faithful to the decomp; raw offsets are kept
// in comments where a member name is uncertain.
//
Mech::Mech(
MakeMessage *creation_message,
SharedData &shared_data
):
JointedMover(creation_message, shared_data) // FUN_00424e38
{
Check_Pointer(creation_message);
//
// The resource handle the decomp reads from this[0x6f] is the model's
// resource list ID -- identical to the resourceID the (real engine)
// JointedMover base ctor just used to stream the skeleton/segments. Use it
// directly (REAL streaming) instead of the scratch-bank Wword(0x6f).
//
ResourceDescription::ResourceID modelResourceID = creation_message->resourceID;
//
// Initialise the current-target slots (mech+0x37c pos / +0x388 entity / +0x38c
// sub-zone) to "no target". mech4's targeting step sets these for the PLAYER; an
// un-targeted mech (e.g. a spawned enemy) must read 0 here, else weapons that gate
// on HasActiveTarget (mech+0x388) would fire at a garbage pointer.
*(void **)((char *)this + 0x388) = 0; // target entity
*(Scalar *)((char *)this + 0x37c) = 0.0f; // target pos (Point3D)
*(Scalar *)((char *)this + 0x380) = 0.0f;
*(Scalar *)((char *)this + 0x384) = 0.0f;
*(int *)((char *)this + 0x38c) = -1; // targeted sub-zone
// LIVE-MECH REGISTRY (task #46, MP targeting): every Mech -- player, the
// solo dummy, AND every peer replicant -- registers here so the boresight
// world-pick can test ALL of them, not just the solo gEnemyMech. The dtor
// deregisters (rare -- wrecks stay; replicants drop when a peer leaves).
extern void BTRegisterMech(Entity *m);
BTRegisterMech((Entity *)this);
//
// Install the Mech vtable and construct the embedded member objects.
//
vtable = &PTR_FUN_0050cfa8; // *param_1 = Mech vtable
mechNameFilter.Initialize(); // FUN_00435a7c(this+0xdb)
masterAlarm = AlarmIndicator(0x21); // FUN_0041b9ec(this+0xe7,0x21)
rearFiring = 0; // (task #68) ORed from the weapons below
// (F7 correction) the binary's 0x400 = FLT_MAX init is DistanceToMissile's
// "no missile" far default (attr id 56), NOT a maxSpeed -- the old member
// is retired; distanceToMissile (init below) owns the slot's meaning.
Wword(0x104) = 0;
Wword(0x106).Construct(0); // FUN_004a4d60 (slot member)
Wword(0x10f).LinkTo(this + 0x61); // FUN_00420ea4
heatAlarm = AlarmIndicator(3); // FUN_0041b9ec(this+0x114,3)
Wword(0x12a) = 0;
Wword(0x12b) = 0;
Wword(0x12c) = 0x3f800000; // 1.0f
stabilityAlarm = AlarmIndicator(2); // FUN_0041b9ec(this+0x131,2)
controllableSubsystems.SetOwner(this); // FUN_00427768(this+0x197)
watchedSubsystems.SetOwner(this); // FUN_00427768(this+0x1af)
statusAlarm = AlarmIndicator(0x21); // FUN_0041b9ec(this+0x1c5,0x21)
Wword(0x1e8) = Wword(0x1e9) = Wword(0x1ea) = 0;
heatableSubsystems.Construct(0); // FUN_004a4dab(this+0x1eb)
weaponRoster.Construct(0); // FUN_004a4df6(this+0x1ef)
damageableSubsystems.Construct(0); // FUN_004a4e41(this+499)
// The target reticle (task #36): armed + pick-testing. The Reticle ctor
// zeroes position/target; the pilot's slew + the pick feed it per frame.
targetReticle.reticleState = Reticle::ReticleOn;
targetReticle.pickPointingOn = True;
targetReticle.reticleElementMask = Reticle::AllEnabledGroup;
for (int i = 0; i < 5; ++i) // FUN_0043ad4f x5
{
telemetryFilter[i].Initialize(15, 0.0f); // this[0x1f8..0x204]
}
//
// Three ref-counted creation-name holders (badge/color/insignia).
// CString members now: their default ctors run in the member-init phase and
// allocate the empty CStringRepresentation + addref -- exactly the binary's
// new(0x10)/FUN_004023f4/refs++ triple @part_012.c:9841-9871. Nothing to
// do here.
//
instanceFlags |= 0x10; // this[10] |= 0x10
// Zero the optional-subsystem back-pointers up front (binary @9873-9875).
// These MUST hit the real members (not the Wword scratch bank) -- they are
// later tested for NULL (e.g. the gyro<->sink cross-link below); left as
// debug-fill 0xcdcdcdcd they pass the != 0 guard and fault.
gyroSubsystem = 0; // this[0x14a] (0xBC4)
sinkSourceSubsystem = 0; // this[0x10e] (0xBC5)
hudSubsystem = 0; // this[0x16d] (0xBD6 -> HUD; was misnamed mechTechSubsystem)
forwardThrottleScale = 1.0f; // @0x5c0 mapper speed scale (writer not in decomp; neutral)
sensorSubsystem = 0; // this[0x1f7] (0xBBE) -- same hazard, zero it too
deathAnimationLatched = 0; // @0x650 -- same 0xCDCDCDCD hazard: the gyro joint-write
// dispatch (task #56) gates on it every frame; garbage
// nonzero silently disabled the writers until death anims ran
legResetLatch = 0; // @0x654 -- same latch family, same fill hazard
legAnimationState = 0; // @0x3b0 -- same hazard (found 2026-07-14, the real-clock
// crash): the type-3 record writer re-dispatches
// SetBodyAnimation(legAnimationState) on itself; a record
// emitted before the leg SM's first tick passed the raw
// 0xCDCDCDCD as a clip index -> AV in SetBodyAnimation.
// (The stub call-counter clock had merely re-ordered record
// emission so the window was never hit.) Standing = 0.
attrPad = 0.0f; // shared read pad for un-populated gauge attributes
linearSpeed = 0.0f; // live forward ground speed (LinearSpeed gauge); set per frame
// Size the animation StateIndicators to cover clips 0..0x20 (MechAnimationState,
// death variants past AnimationCount=0x1d included) so SetState(state) never trips
// Verify(state<stateCount). operator= copies only the counts (keeps the ctor-built
// watcher sockets); then seat the initial standing state. [T2]
animationState = StateIndicator(0x21);
animationState.SetState(0); // StandingAnimation
replicantAnimationState = StateIndicator(0x21);
replicantAnimationState.SetState(0);
// CollisionState: 4 states (NoCollision/InitialHit/Slide/Rest) driven per frame
// from the collision walk (ProcessCollision) so impact audio fires on contact.
collisionState = StateIndicator(4); // CollisionStateCount
collisionState.SetState(0); // NoCollisionState
collisionTemporaryState = 0;
footStep = 0; // FootStep pulse (audio trigger)
footStepRootJoint = 0; // (F5) resolved on first contact eval
footStepRootResolved = 0;
// (F19) the acceleration-feed rings (binary: size 0xF, fill 0)
for (int vr = 0; vr < 15; ++vr)
velRingFwd[vr] = velRingVert[vr] = velRingDt[vr] = 0.0f;
velRingCursor = 0;
accelPrevFwdMean = 0.0f;
accelPrevVertMean = 0.0f;
accelPrevPos = localOrigin.linearPosition;
// (task #66) instability accumulator: the binary reset zeroes 0x3F0
// (part_012.c:9420); the tuning members are set from the model record below.
unstablePercentage = 0.0f;
fwdSpeedFiltered = 0.0f; // smoothed published velocity (see mech4 derive)
vertSpeedFiltered = 0.0f;
{ extern void *g_btFootStepAddr; g_btFootStepAddr = &footStep; } // DIAG: watcher-poll tracer target
{ extern void *g_btAccelAddr; g_btAccelAddr = &localAcceleration; } // DIAG: accel poll/change tracer
radarRange = 1000.0f; // radar display scale (SetTargetRange is stubbed;
// 1km default zoom so contacts within ~500m show on
// the radar, vs the config maximum_range=4000 edge)
radarLinearPosition = &localOrigin.linearPosition; // map reads the mech's live world position...
radarAngularPosition= &localOrigin.angularPosition; // ...and orientation (pointers into the base origin)
duckState = 0; // not crouching
// (AUDIO_FIDELITY F7) missile alarm: the binary reset writes 0 / FLT_MAX
// (part_012.c:9446-9447; FLT_MAX = "no missile" far default)
incomingLock = 0;
distanceToMissile = FLT_MAX;
incomingLockNext = 0;
distanceToMissileNext = FLT_MAX;
// (F17) impact-speed scale + cab REDUCE button
collisionSpeed = 0.0f;
reduceButton = 0;
ZeroBlock(this + 0xca, 6); // this[0xca..0xcf] = 0
ZeroBlock(this + 0x153, 6); // this[0x153..0x158] = 0
Wword(0xd0) = 0;
creationTime = Now(); // FUN_00414b60 -> Wword(0x1de)
Wword(0xd2) = 0;
Wword(0x1ae) = 0;
bodyTargetSpeed = 0.0f; // @0x6b4 (was absorbed Wword(0x1ad))
//
// Seed the pose + the dead-reckon baselines (task #1 corrections: the
// binary's this[0x98]/this[0x4b] Origin copies are projectedOrigin@0x260
// and updateOrigin@0x12c seeded from localOrigin@0x100 -- the ENGINE
// inherited members, not mech-local "torso" fields).
//
projectedOrigin = localOrigin; // FUN_0040a938(this+0x98,this+0x40)
updateOrigin = localOrigin; // FUN_0040a938(this+0x4b,this+0x40)
Wword(0x163) = Wword(0x40); // FUN_00408440(this+0x163,this+0x40)
turretBase.SetIdentity(); // FUN_0040a7f4(this+0x71, IdentityQuat)
torsoAimCurrent.SetIdentity(); // this+0xa6
torsoAimTarget.SetIdentity(); // this+0xb2
Wword(0x77).SetIdentity();
legAngle = ZeroVector; // FUN_00408440(this+0x7d,ZeroVec)
hipAngle = ZeroVector; // this+0x80
orientation.FromRotation(bodyRotation); // FUN_0040ab44(this+0x34,this+0x40)
Wword(0x166) = ZeroVector;
Wword(0x169) = 0;
Wword(0xe6) = 0;
poseSyncLatch = 0; // @0x77c (was absorbed Wword(0x1df))
heatLevelSnapshot = 0; // @0x780 (type-7 deadband baseline)
mirrorBodyAdv = 0.0f; // master send-mirror gait travel
peerMirrorSpeed = -1.0f; // peer true-mirror cadence (disabled by default)
angMirrorYaw = 0.0f; // scalar peer-yaw mirror (port addition)
angMirrorRate = 0.0f;
angMirrorValid = 0;
angSyncLatch = 0; // peer heading re-anchor (type-4 receive)
Wword(0xfe) = Wword(0xfc) = Wword(0x105) = 0;
mechName.Copy(&DAT_004e0f8c); // FUN_00408e90(this+0xd8,"")
Wword(0x160) = Wword(0x15f) = 0;
Wword(0x194) = 0;
Wword(0x207) = Wword(0x208) = Wword(0x20a) = 0;
Wword(0x20b).SetIdentity();
Wword(0x1e7) = Wword(0x1dd) = Wword(0x209) = 0;
Wword(0xd5) = Wword(0xd6) = Wword(0xd7) = 0;
fallScalar = 0.0f; // @0x4b4 (was absorbed Wword(0x12d))
fallDirection = Vector3D(0.0f, 0.0f, 1.0f); // @0x4a8 fall/impact record payload
throttleState = 2; // Wword(0x129) = 2
Wword(0x1e0) = Wword(0x119);
stabilityAlarm.SetLevel(1); // FUN_0041bbd8(this+0x131,1)
stabilityAlarm.SetLevel(Wword(0x136));
//
// Derive heat capacity from the engine segment record (Wword(0xbb)).
//
// TODO(bring-up): Wword(0xbb) is the model's "engine" geometry/segment record,
// populated by the base-ctor model-segment walk that the object-layout
// reconstruction has not wired up yet (Wword() is a scratch-bank shim, so this
// reads 0). Guard the NULL deref so construction proceeds; heat capacity falls
// back to a neutral default until the subsystem factory maps this member.
int engineSeg = Wword(0xbb);
heatLevel = engineSeg ? *(int *)(engineSeg + 0xc) : 0; // Wword(0x146)
heatCapacity = (int)(HeatCapacityFraction * (float)heatLevel); // Wword(0x147)
Wword(0x16a) = Wword(0x16b) = 0x3f800000; // 1.0f, 1.0f
Wword(0x94) = (int)WorldTransform() + 0x2c; // FUN_00433ed4(...)
//
// Pick the Simulate "Performance" thunk by instance role
// (copy vs. master) -- this[10] & 0xc == 4 means a network copy.
//
if ((instanceFlags & 0xc) == 4)
{
SetPerformance(PTR_LAB_0050c0e8); // copy performance
}
else
{
SetPerformance(PTR_LAB_0050c0f4); // master performance
}
// (F7 correction) the old "maxSpeed = 1000.0f override" here targeted
// Wword(0x101) = +0x404 -- the binary table says that is RadarRange
// (id 47), already initialized as radarRange = 1000.0f above.
Wword(0x102) = 0;
Wword(0x103) = (int)(this + 0x43);
//
// ---- Pass 1: stream the SUBSYSTEM segment list (resource segment 0x11) ----
//
// REAL streaming (mirrors VTV::VTV @1379-1395): the segment stream begins
// with an int subsystem-count, followed by one Subsystem::SubsystemResource
// blob per subsystem (each self-sized via subsystemModelSize). The Mech
// reserves 2 leading sentinel slots (0 = NULL, 1 = power/voltage bus).
//
ResourceDescription *subsystemResourceDesc =
MechFindResource(modelResourceID,
ResourceDescription::SubsystemModelStreamResourceType); // segment 0x11
Check(subsystemResourceDesc);
subsystemResourceDesc->Lock();
// Stream over a zero-padded COPY of the segment, not the locked resource directly.
// The MechSubsystem base ctor reads each record's fields up to res+0xe0; for the
// last / shorter records (e.g. the controls mapper) that runs a few dozen bytes past
// the segment's exact size. In the shipped game the streamed records were always the
// full in-memory SubsystemResource size so this was safe; our reconstructed stream
// isn't padded -> the read ran off the buffer and corrupted the heap (the intermittent
// ~1-in-6 startup crash, exit 0xC0000374; caught precisely via PageHeap at
// MechSubsystem::MechSubsystem mechsub.cpp:165 reading res+0xCC). A zeroed +0x100 tail
// makes any in-record read land in mapped, zeroed memory.
// TODO(bring-up): remove once the per-subsystem SubsystemResource layouts are
// reconstructed to their exact streamed sizes.
size_t subsysRawSize = (size_t)subsystemResourceDesc->resourceSize;
size_t subsysPadSize = subsysRawSize + 0x100;
void *subsysPadBuf = Memory::Allocate(subsysPadSize);
memset(subsysPadBuf, 0, subsysPadSize);
memcpy(subsysPadBuf, subsystemResourceDesc->resourceAddress, subsysRawSize);
MemoryStream subsystemStream(subsysPadBuf, subsysPadSize);
int streamedSubsystemCount = *(int *)subsystemStream.GetPointer();
subsystemStream.AdvancePointer(sizeof(int));
subsystemCount = streamedSubsystemCount + 2; // Wword(0x49)
subsystemArray = (Subsystem **)Memory::Allocate(subsystemCount * sizeof(Subsystem *)); // Wword(0x4a)
// Zero the WHOLE table up front: subsystem ctors created mid-loop (e.g.
// MechTech) walk the owner's full roster via Entity::GetSubsystem, so every
// not-yet-filled slot must read NULL rather than uninitialised heap garbage.
for (int z = 0; z < subsystemCount; ++z)
{
subsystemArray[z] = 0;
}
subsystemArray[1] = (Subsystem *)voltageBus.Resolve(); // FUN_00417ab4(this+199)
Wword(0x12e) = (int)((Wword(0xba).max - Wword(0xba).min) * UpdatePositionScale);
Wword(0xbb).field8 += (float)Wword(0x12e);
weaponCount = 0; // Wword(0x112)
messageManager = 0; // Wword(0x10d)
//
// Instantiate one Subsystem per segment, dispatching on its streamed
// ClassID (see Mech::SubsystemClassID). The capability sub-rosters are
// wired up afterward. Segment 0/1 are reserved sentinels (loop from 2).
//
for (int id = 2; id < subsystemCount; ++id)
{
Subsystem::SubsystemResource *seg =
(Subsystem::SubsystemResource *)subsystemStream.GetPointer();
subsystemArray[id] = 0;
Logical skipCopy =
((seg->subsystemFlags & 4) != 0) && ((instanceFlags & 0xc) == 4);
if (!skipCopy)
{
switch (seg->classID)
{
case CockpitClassID: // 0xBBD -> real class Condenser (label mislabeled)
subsystemArray[id] = CreateCondenserSubsystem(this, id, seg); // FUN_004ae568
break;
case SensorClassID: // 0xBBE -> real class HeatSink bank (NOT a sensor)
subsystemArray[id] = CreateHeatSinkBankSubsystem(this, id, seg); // FUN_004ae8d0
sensorSubsystem = subsystemArray[id]; // Wword(0x1f7) = the heat bank
break;
case CondenserClassID: // 0xBC0 -> real class Reservoir (label mislabeled)
subsystemArray[id] = CreateReservoirSubsystem(this, id, seg); // FUN_004af408
break;
case GeneratorClassID: // 0xBC1 -> real Generator (power bus)
subsystemArray[id] = CreateGeneratorSubsystem(this, id, seg); // FUN_004b225c
break;
case PoweredSubsystemClassID: // 0xBC2 -> real PoweredSubsystem (power bus)
subsystemArray[id] = CreatePoweredSubsystem(this, id, seg); // FUN_004b0f74
break;
case MyomersClassID: // 0xBC3 -> real class Sensor (ctor @004b1d18, "Myomers" mislabel)
// WAVE 4: the real class at 0xBC3 is Sensor (string pool @0050fae0 +
// SENSOR.HPP); MyomersClassID is a factory-enum mislabel (real Myomers
// is 0xBC6). De-shimmed + layout-locked (radarPercent@0x31C, sizeof==0x328).
subsystemArray[id] = CreateSensorSubsystem(this, id, seg); // FUN_004b1d18
break;
case GyroClassID: // 0xBC4 -> real class Gyroscope (ctor @004b3778)
// RE-ENABLED (task #56). The old NaN revert's root causes are fixed
// byte-exact against @004b3778/@004b2ec0/@004b30ec: the ctor field map
// (springConstant@0x1E8 / dampingConstant@0x1F4 were mislabelled, the
// 0x254-0x2B3 block was missing so everything after was mis-offset,
// clamps/accumulators were left as 0xCD fill) and the integrator
// semantics (state-minus-target displacements, componentwise damping
// OVERWRITE, IntegrateBody X/Z crossings) -- all static_assert-locked.
// The joint writers now dispatch from the MECH performance tail
// (GyroFrameJointWrite bridge; binary calls @0x4aaf74/0x4aaf83), NOT
// from GyroscopeSimulation.
subsystemArray[id] = CreateGyroSubsystem(this, id, seg); // ctor @004b3778 (real)
gyroSubsystem = subsystemArray[id]; // Wword(0x14a)
break;
case SinkSourceClassID: // 0xBC5 -> real class Torso (ctor @004b6b0c, mislabeled)
// WAVE 4 (base chain re-based; currentTwist@0x1D8 compile-time locked
// by TorsoLayoutCheck, so the gyro cross-link sinkSourceSubsystem+0x1D8
// now reads the live torso twist). Build the real Torso.
subsystemArray[id] = CreateTorsoSubsystem(this, id, seg); // FUN_004b6b0c
sinkSourceSubsystem = subsystemArray[id]; // Wword(0x10e) -- gyro links to +0x1D8
break;
case ActuatorClassID: // 0xBC6 -> real class Myomers (ctor @004b8fec, "Actuator" stub)
// WAVE 6: the real class at 0xBC6 is Myomers (the artificial-muscle
// drive). GATED BT_MYOMERS (default ON): the real class constructs +
// ticks but is INERT w.r.t. locomotion/heat (no-op mover feed + the
// advanced-damage sim gate is False), so it can't regress the gait.
// BT_MYOMERS=0 falls back to the Actuator stub. Authentic mover/heat
// coupling is a follow-up (needs real Mech accessors + messmgr 0xBD3,
// and must reconcile the mover feed with the gait cutover first).
if (BTEnvOn("BT_MYOMERS", 1))
subsystemArray[id] = CreateMyomersSubsystem(this, id, seg); // FUN_004b8fec
else
subsystemArray[id] = (Subsystem *)
new (Memory::Allocate(0x358)) Actuator(this, id, seg);
break;
case WeaponEmitterClassID: // 0xBC8 -> real Emitter beam weapon
subsystemArray[id] = CreateEmitterSubsystem(this, id, seg); // FUN_004bb120
++weaponCount;
break;
case JumpJetClassID: // 0xBCB -> real class AmmoBin (ctor @004bd5c4, mislabeled)
// WAVE 4: AmmoBin : HeatWatcher : MechSubsystem; own block @0x180,
// sizeof 0x22C compile-time locked. Feeds a ProjectileWeapon/
// MissileLauncher (0xBCD/0xBD0) via a SharedData connection (still stubbed).
subsystemArray[id] = CreateAmmoBinSubsystem(this, id, seg); // FUN_004bd5c4
break;
case MechWeaponClassID: // 0xBCD -> real class ProjectileWeapon (ctor @004bc3fc, mislabeled)
// WAVE 7: the autocannon/ballistic ammo weapon (: MechWeapon, sizeof 0x448).
// Was building the base MechWeapon stub (pure-virtual FireWeapon trap).
subsystemArray[id] = CreateProjectileWeaponSubsystem(this, id, seg); // FUN_004bc3fc
++weaponCount;
break;
case MissileWeaponClassID: // 0xBCE -> real class GaussRifle (ctor @004bdcb4, mislabeled)
// WAVE 7: VDATA.h enum -> 0xBCE == GaussRifleClassID (NOT MissileWeapon).
// GaussRifle : Emitter (sizeof 0x484); its FireWeapon is a no-op in this
// 1995 build (just zeros the charge) -> a DEDICATED bridge, not CreateEmitter.
subsystemArray[id] = CreateGaussRifleSubsystem(this, id, seg); // FUN_004bdcb4
++weaponCount;
break;
case BallisticWeaponClassID: // 0xBD0 -> real class MissileLauncher (ctor @004bcff0, mislabeled)
// WAVE 7: VDATA.h enum -> 0xBD0 == MissileLauncherClassID. : ProjectileWeapon
// (sizeof 0x44c). Was building a BallisticWeapon stub.
subsystemArray[id] = CreateMissileLauncherSubsystem(this, id, seg); // FUN_004bcff0
++weaponCount;
break;
case SubsystemMessageManagerID: // 0xBD3 -> the REAL class (task #7):
// the per-mech damage/explosion consolidation hub (ctor
// @0049bca4, 0x130 bytes, ConsolidateAndSendDamage Performance
// @0049b784). The old MechControlsMapper build here was the
// mapper/messmgr conflation -- the mapper never streams; it is
// installed into roster slot 0 by SetMappingSubsystem.
subsystemArray[id] = CreateMessageManagerSubsystem(this, id, seg); // FUN_0049bca4
messageManager = (SubsystemMessageManager *)subsystemArray[id]; // Wword(0x10d)
break;
case GaussWeaponClassID: // 0xBD4 -> real Emitter (PPC, mislabeled)
subsystemArray[id] = CreateEmitterSubsystem(this, id, seg); // FUN_004bb888
++weaponCount;
break;
case MechTechClassID: // 0xBD6 -> real class HUD (mislabeled; MechTech is 0xBDC)
subsystemArray[id] = CreateHUDSubsystem(this, id, seg); // FUN_004b7f94 (alloc 0x2a4)
hudSubsystem = subsystemArray[id]; // raw part_012.c:10164 param_1[0x16d] = HUD
break;
case LegSubsystemClassID: // 0xBD8 -> real class Searchlight (ctor @004b84dc, mislabeled)
// WAVE 4: de-shimmed + GATE FIXED (was gating on the shadow
// segmentFlags -> Performance never installed). Searchlight :
// PowerWatcher; own fields layout-locked at 0x1D8+.
subsystemArray[id] = CreateSearchlightSubsystem(this, id, seg); // FUN_004b84dc
break;
case HeatableClassID: // 0xBDC -> real class MechTech (mislabeled).
// (Was: built the HeatableSubsystem base and DISCARDED the pointer -- bug.)
subsystemArray[id] = CreateMechTechSubsystem(this, id, seg); // FUN_004ad228 (binary alloc 0x104)
// NO cache write here: raw case 0xbdc (part_012.c:10176-10185) stores roster-only;
// the 0x5b4 cache belongs to case 0xbd6 (the HUD) -- earlier recon had it swapped.
break;
case DisplayClassID: // 0xBDE -> real class ThermalSight (ctor @004b8718, "MechDisplay" stub)
// WAVE 4: de-shimmed + GATE FIXED. ThermalSight : PowerWatcher;
// own fields layout-locked at 0x1D8+. The cockpit IR view-mode
// render hooks (pvision/viewport) are clearly-marked no-ops.
subsystemArray[id] = CreateThermalSightSubsystem(this, id, seg); // FUN_004b8718
break;
default:
// @004a1674+0x936: "Unknown subsystem resource ->classID"
Verify(False, "Unknown subsystem resource ->classID",
"d:\\tesla\\bt\\bt\\MECH.CPP", 0x936);
break;
}
}
subsystemStream.AdvancePointer(seg->subsystemModelSize); // next blob
}
subsystemResourceDesc->Unlock();
Memory::Free(subsysPadBuf);
weaponCount = 2; // Wword(0x112) reset to base index (sentinels 0,1)
//
// Cross-link the gyro to the torso twist (task #56, binary bt_mech tail:
// gyro+0x258 = torso+0x1D8 == Torso::currentTwist; the gyro damage-response
// @004b2980 reads it per hit). Done through the two complete-type-TU bridges
// (GyroBindExternalPitch / BTGetTorsoTwistAddr) -- the old raw
// SubProxy::linkTarget write landed at gyro+4 (an engine base field) and would
// have stomped the live gyro base; that landmine is retired.
//
if (sinkSourceSubsystem != 0 && gyroSubsystem != 0)
GyroBindExternalPitch(gyroSubsystem, BTGetTorsoTwistAddr(sinkSourceSubsystem));
//
// Build the capability sub-rosters by IsDerivedFrom() class tests.
//
// Loop 1 = the watcher CONNECT pass (task #57). The binary calls vtable
// slot +0x38 on every HeatWatcher-derived subsystem (@0x50e604 test);
// the slot bodies (FUN_004aee2c, PowerWatcher/Torso override @004b1a40,
// byte-identical -- Ghidra missed both starts, recovered from raw bytes)
// bind watchedLink to roster[watchedSubsystem], gated on the owner being
// the live master node ((flags & 0xC)==0 && (flags & 0x100)). The gate is
// hoisted out of the loop unchanged (constant per mech). This replaces
// the old SubProxy::Start() no-op that left every watchdogAlarm at 0 --
// which held the Torso's ElectricalStateLevel() below Ready and zeroed the
// twist rate forever.
//
if ((simulationFlags & 0xC) == 0 // SegmentCopyMask
&& (simulationFlags & 0x100) != 0) // MasterHeatSinkFlag
{
extern int BTWatcherWatchedIndex(Subsystem *sub); // heatfamily_reslice.cpp
extern void BTWatcherBindTarget(Subsystem *sub, Subsystem *target);
for (int id = 2; id < subsystemCount; ++id)
{
int watched = BTWatcherWatchedIndex(subsystemArray[id]); // -1 = not a HeatWatcher
if (watched < 0)
continue;
if (watched < subsystemCount && subsystemArray[watched] != 0)
{
BTWatcherBindTarget(subsystemArray[id], subsystemArray[watched]);
DEBUG_STREAM << "[watch] subsystem " << id << " watches "
<< watched << std::endl;
}
else
{
// The binary has no range check (CreateStreamed validated the
// name); flag data drift honestly instead of a wild read.
DEBUG_STREAM << "[watch] subsystem " << id
<< " BAD WatchedSubsystem index " << watched << std::endl;
}
}
}
for (int id = 2; id < subsystemCount; ++id) // heatable roster
{
SubProxy *s = (SubProxy *)subsystemArray[id];
if (s != 0 && s->IsDerivedFrom(0x51155c))
{
heatableSubsystems.Add((Subsystem *)s);
}
}
for (int id = 2; id < subsystemCount; ++id) // weapon roster (0x511830 = MechWeapon)
{
// (task #68) REAL derivation test via the mechweap bridge -- the old
// SubProxy::IsDerivedFrom stub returned 0, so this loop never ran
// (empty weaponRoster, no capability OR).
extern int BTWeaponIsRearFiring(Subsystem *sub); // -1 = not a weapon
int rf = BTWeaponIsRearFiring(subsystemArray[id]);
if (rf >= 0)
{
weaponRoster.Add(subsystemArray[id]);
// mech.RearFiring |= weapon.rearFiring (binary ctor
// @part_012.c:10220: mech[0x104] |= *(weapon+0x334))
rearFiring |= rf;
}
}
for (int id = 2; id < subsystemCount; ++id) // damageable roster
{
SubProxy *s = (SubProxy *)subsystemArray[id];
if (s != 0 && s->IsDerivedFrom(0x50e4fc))
{
damageableSubsystems.Add((Subsystem *)s);
}
}
//
// ---- Pass 2: stream the MODEL record (resource segment 0xf) ----
//
// REAL streaming (mirrors VTV::VTV @1297-1309): the GameModel resource's
// resourceAddress IS the ModelResource record. Kept locked for the mech's
// lifetime because the body below stores pointers into it (model->base).
//
ResourceDescription *modelResourceDesc =
MechFindResource(modelResourceID,
ResourceDescription::GameModelResourceType); // segment 0xf
Check(modelResourceDesc);
modelResourceDesc->Lock();
ModelResource *model = (ModelResource *)modelResourceDesc->resourceAddress;
Check_Pointer(model);
Wword(0x68) = model->cameraOffset; // FUN_00408440(this+0x1a0, rec+0xA8)
Wword(0x130) = model->deathEffectResourceID; // rec+0x74 -> mech+0x4c0
Wword(0x148) = model->deathSplashDamage; // rec+0x78 -> mech+0x520
Wword(0x149) = model->deathSplashRadius; // rec+0x7C -> mech+0x524
Wword(0x195) = Wword(0x196) = 0;
Wword(0xfd) = 0;
masterAlarm.SetLevel(0); // FUN_0041bbd8(this+0xe7,0)
statusAlarm.SetLevel(0); // FUN_0041bbd8(this+0x1c5,0)
// GAIT SLEW RATES -> REAL MEMBERS (task #15; was Wword scratch-bank writes, so
// the authentic per-mech acceleration never reached the state machine and a
// bring-up forwardCycleRate=1000 stand-in made every speed change instant):
// model->maxAcceleration (+0x44) is the cycle slew rate; 0x5b0/0x5b8 hold the
// gimp/ground copies (ResetToInitialState restores 0x344 from 0x5b8 -- raw
// part_012.c:9441); +0x48 -> airborneCycleRate; +0x4c -> forwardThrottleScale
// (the ctor's "=1.0, writer not in decomp" note is resolved: THIS is the writer).
forwardCycleRate = model->maxAcceleration; // @0x344 (this[0xd1] = rec+0x44 = 30
// for the madcat -- the struct skew
// used to land this on
// ThrottleAdjustment ~1.0)
// (The "floor 25" era is over: the old struct skew landed this read on
// ThrottleAdjustment ~1.0 -- the authentic MaxAcceleration (madcat: 30)
// now reads directly. task #4)
gimpCycleRate = forwardCycleRate; // @0x5b0 (this[0x16c])
groundCycleRate = forwardCycleRate; // @0x5b8 (this[0x16e])
airborneCycleRate = model->superStopAcceleration; // @0x5bc (this[0x16f] = rec+0x48)
forwardThrottleScale = model->throttleAdjustment; // @0x5c0 (this[0x170] = rec+0x4C;
// the old "decodes to 0" was the
// out-of-bounds struct read)
if (getenv("BT_REPL_LOG"))
DEBUG_STREAM << "[model] accel=" << forwardCycleRate
<< " superStop=" << airborneCycleRate
<< " throttleAdj=" << forwardThrottleScale << std::endl;
Wword(0x12f) = model->relativeMechValue; // rec+0x70 -> mech+0x4bc
// (task #66) the six Unstable* tuning fields are REAL members now (the
// instability model consumes them); binary rec+0x80-0x94 -> mech+0x784..0x798.
maxUnstableAcceleration = model->maxUnstableAcceleration;
unstableAccelerationEffect = model->unstableAccelerationEffect;
unstableGunTheEngineEffect = model->unstableGunTheEngineEffect;
unstableSuperStopEffect = model->unstableSuperStopEffect;
unstableHighVelocityEffect = model->unstableHighVelocityEffect;
unstableStopedTurnEffect = model->unstableStopedTurnEffect;
// Angle fields converted from degrees to radians. (task #68: REAL members
// now -- the look-state commit consumes them; were Wword scratch parks.)
lookLeftAngle = model->lookLeftAngle * DegreesToRadians; // +0x50 -> @0x564
lookRightAngle = model->lookRightAngle * DegreesToRadians; // +0x54 -> @0x568
lookFrontAngle = model->lookFrontAngle * DegreesToRadians; // +0x58 -> @0x56C
lookBackAngle = model->lookBackAngle * DegreesToRadians; // +0x5c -> @0x570
// The update-record deadbands (task #3; the struct is now the verified
// 200-byte overlay, so the named fields ARE the raw offsets).
updateTurnAngleDeadband = model->updateTurnDegreeDiffrence * DegreesToRadians; // @0x770
updateTurnVelocityDeadband = model->updateTurnVelocityDiffrence; // @0x76c
updatePositionDeadband = model->updatePositionDiffrence; // @0x768
if (getenv("BT_REPL_LOG"))
DEBUG_STREAM << "[deadband] pos=" << updatePositionDeadband
<< " turnVel=" << updateTurnVelocityDeadband
<< " turnAngle=" << updateTurnAngleDeadband << " (rad)" << std::endl;
Wword(0x10c) = model->timeDelay; // rec+0xA4 -> mech+0x430
// The AUTHENTIC per-mech turn rates (rec+0x60/0x64, deg->rad -> mech+0x574/0x578).
// The decomp writes word slots this[0x15d]/this[0x15e] (== 0x574/0x578); stored in
// real named members now (task #64b) -- read by the master-perf turn-rate lerp in
// the drive (mech4.cpp) that yaws at lerp(walkingTurnRate, runningTurnRate) by speed
// instead of the old bring-up constant kDriveTurnRate.
walkingTurnRate = model->walkingTurnRate * DegreesToRadians;
runningTurnRate = model->runningTurnRate * DegreesToRadians;
//
// SHADOW JOINT resolve (task #20; CORRECTED -- the earlier draft misread
// this block as a death-effect video-object lookup). Raw part_012.c:
// 10285-10310: the record's ShadowJointName string at record+0xB4 ->
// CString (FUN_00402298/FUN_00402460) -> GetSegment (FUN_00424b60) ->
// segment->GetJointIndex() (*(seg+0xc0)) -> JointSubsystem::GetJoint
// (the FUN_0041d3b3 accessor + virtual +0x34 call) -> this[0x10b] @0x42c.
// Our public Mech::ResolveJoint() is exactly that chain. The name is read
// at the raw byte offset (the ModelResource struct layout is the known
// mis-decoded one -- see the maxAcceleration/field4c notes above).
//
{
const char *shadow_name = (const char *)model + 0xB4;
shadowJointNode = NULL;
if (shadow_name[0] != '\0'
&& strcmp(shadow_name, "Unspecified") != 0
&& memchr(shadow_name, 0, 20) != NULL) // sane record string
{
shadowJointNode = ResolveJoint(shadow_name); // FUN_00424b60 chain
DEBUG_STREAM << "[shadow] ShadowJointName='" << shadow_name
<< "' -> joint " << (void *)shadowJointNode << "\n" << std::flush;
}
}
//
// CLIP-SET / VIEW gate (@part_012.c:10308-10320). CORRECTION (task #59):
// the two branch targets here are FUN_004a80d4 / FUN_004a86c8 -- which are
// the GAIT-CLIP loaders (LoadLocomotionClips / LoadLocomotionClipsExt), NOT
// a "body LOD" pair (the old LoadLowDetailBody/LoadHighDetailBody names were
// no-op stubs, so this authentic gate did NOTHING and every mech silently
// fell back to the unconditional exterior load below -> the local cockpit
// mech played the EXTERIOR clips and leaned -8deg into every walk). The
// only real side effect at THIS point is the L4VIEWEXT flag; the clip LOAD
// itself is deferred to the gait-clip site below (after animationClips[] is
// zeroed), where the same three-way selection is applied.
//
if (getenv("L4VIEWEXT") != 0) // FUN_004dee74
Wword(0x15f) = 1;
//
// Tell every heatable subsystem to attach to the body model.
//
for (HeatableSubsystem *h = (HeatableSubsystem *)heatableSubsystems.First(); // FUN_004a4f9e iterator
h != 0;
h = (HeatableSubsystem *)heatableSubsystems.Next())
{
h->AttachToBody(); // FUN_004b8ef0
Wword(0x1e9) = 1;
}
controllableSubsystems.Reset(); // FUN_004283b8(this+0x197,1)
//
// Resolve the "jointlocal" joint and a couple of cosmetic flags.
// FUN_00424b60 caches the resolved Joint* into an object field whose recon
// offset is not yet mapped (it had been parked in the Wword scratch bank);
// run the real resolve now via the Mech member -- storage slot TBD.
//
ResolveJoint("jointlocal"); // FUN_00424b60 (Mech::ResolveJoint)
// P3 GAIT CUTOVER: initialise the two-channel gait controllers now that the
// skeleton/jointSubsystem is loaded (Init resolves owner + jointSubsystem, the
// binary ctor @00427768). Inert until the cutover drives them (BT_GAIT_CUTOVER).
legAnimation.Init(this);
bodyAnimation.Init(this);
// P6 MULTIPLAYER: choose the dead reckoner (the VTV pattern, RP/VTV.cpp:1280).
// Masters use it to decide when to ForceUpdate (self-prediction error);
// replicants use it inside Mover::DeadReckon to move between updates.
SetDeadReckoner(&Mover::AcceleratedDeadReckoner);
// AUTHENTIC GROUND MODEL -- ctor half (task #15, ground-model-decode).
// Binary part_012.c:9938-9940 + 9974-9975: duck presets from the template,
// then the TEMPLATE BOTTOM LIFT: raise collisionTemplate->minY by 5% of the
// volume's X width (const _DAT_004a2d40 = 0.05f). The lifted bottom is the
// ground-probe height AND the wall-vs-floor separation: surfaces closer than
// the lift below the probe answer the height query (walkable), anything the
// cylinder itself strikes is a wall. Blackhawk (BLH_CV.SLD, verified):
// cylinder Y[2.0, 7.10156], width 7.59376 -> lift 0.37969 -> minY 2.37969.
// Runs for EVERY mech instance (the binary does it unconditionally).
// (_DAT_004a2d38 is stored as a DOUBLE 0.6 in the binary; 0.6f differs by
// at most 1 ulp in the stored float result.)
// Guard on a real volume: defensive addition (a volume-less mech would have
// crashed in 1995); gated with the frame half via the SHARED GroundReal().
standingTemplateMaxY = 0.0f;
duckedTemplateMaxY = 0.0f;
templateBottomLift = 0.0f;
frameEntryWorldVelocity = Vector3D(0.0f, 0.0f, 0.0f); // collision-damage guard snapshot
ramLastVictim = 0; // ram contact-edge state
ramContactLinger = 0.0f;
lastInflictingDamage = 0.0f; // task #60: killing-blow magnitude (set on hit)
if (GroundReal() && GetCollisionVolumeCount() > 0
&& collisionTemplate != 0 && collisionVolume != 0)
{
standingTemplateMaxY = collisionTemplate->maxY; // @0x518
duckedTemplateMaxY = 0.6f * standingTemplateMaxY; // @0x51c, _DAT_004a2d38
templateBottomLift = // @0x4b8, _DAT_004a2d40
(collisionVolume->maxX - collisionVolume->minX) * 0.05f;
collisionTemplate->minY += templateBottomLift; // part_012.c:9975
if (GroundLog())
{
DEBUG_STREAM << "[ground] ctor: volume width="
<< (collisionVolume->maxX - collisionVolume->minX)
<< " lift=" << templateBottomLift
<< " template minY=" << collisionTemplate->minY
<< " maxY=" << collisionTemplate->maxY << "\n" << std::flush;
}
}
// namedClip aliases &animationClips[5] (binary: they are ONE array; namedClip@0x5e0
// == animationClips[5]@0x5cc+0x14) -- so LoadLocomotionClips' writes are what the gait
// state machine (SetBodyAnimation/MeasureClipStride) reads. Always set (cheap).
namedClip = &animationClips[5];
// Zero the whole clip table first: the debug heap fills fresh allocs
// 0xCDCDCDCD, and unresolved slots must read as "no clip" (the crash-anim
// trigger guards on animationClips[0x20] != 0 before SetLegAnimation(0x20)).
for (int ci = 0; ci < (int)(sizeof(animationClips) / sizeof(animationClips[0])); ++ci)
animationClips[ci] = 0;
// P3 STEP 7 (state-machine path): load the full gait clip set + speed caps into
// animationClips[5..26]. GATED (BT_GAIT_CUTOVER) so the default STEP-1/2 path is
// untouched while the AdvanceBodyAnimation state machine is brought up (it derefs
// ResolveAnimationClip unconditionally -> will fault on any clip this mech's content
// lacks; observe under the gate first).
//
// AUTHENTIC clip-set gate (@part_012.c:10308-10320, task #59): the local cockpit
// MASTER gets the INTERIOR clip set (LoadLocomotionClipsExt @004a86c8, 4-char 'i'
// suffix) whose clips shake `jointshakey` and OMIT the `jointhip` walk lean; network
// REPLICANTS and the forced L4VIEWEXT external view get the EXTERIOR set
// (LoadLocomotionClips @004a80d4, 3-char) carrying the authored -8deg lean. This is
// the SINGLE loader call the decomp ctor makes (the earlier no-op body-LOD gate was
// this same decomp branch mis-reconstructed -- see the L4VIEWEXT block above).
//
// PORT NOTE (task #59): the authentic discriminator is the replicant COPY bit
// (instanceFlags & 0xC == 4), which the 1995 game sets when it constructs a
// network replicant. The port does NOT set that bit at construction (all MP
// mechs are built as local masters; replication rides the mech registry +
// update-records, not the copy-performance path) -- so BOTH the local mech
// and the peer's replica currently take the ELSE (interior) branch. That is
// correct for the LOCAL cockpit (level, the reported bug) but means the peer's
// mech does not carry the exterior -8deg lean in the OTHER pod's view yet;
// that lights up automatically once the replicant copy bit is set at
// construction (tracked in open-questions -- do NOT fake it with a viewpoint
// test here: the clips load once at ctor, before the viewpoint is assigned).
int loadedInterior = 0;
if (BTEnvOn("BT_GAIT_CUTOVER", 1)) // default ON (=0 to disable)
{
if (getenv("L4VIEWEXT") != 0 || (instanceFlags & 0xc) == 4)
{
LoadLocomotionClips(model); // exterior (3-char): replicant / forced-ext view
}
else
{
LoadLocomotionClipsExt(model); // interior (4-char 'i'): the local cockpit master
loadedInterior = 1;
}
}
// Register this mech's model + which clip set it currently holds, so the
// per-frame MaintainViewClipSet() (mech4 PerformAndWatch) can flip it to the
// authentic set once the viewpoint is assigned (the copy bit is never set in
// the port, so the ctor gate lands everyone on interior -- see the note above).
extern void BTStashClipState(const Mech *m, Mech__ModelResource *model, int interior);
BTStashClipState(this, model, loadedInterior);
Wword(0xe5) = 0;
Wword(0xdd) = Wword(0xde) = 1;
Wword(0xdb) = model->reticleX; // rec+0x68 -> mech+0x36c
Wword(0xdc) = model->reticleY; // rec+0x6C -> mech+0x370
//
// ---- Pass 3: build the per-zone Mech::DamageZone objects (resource type 0x14) ----
// The Entity base ctor (ENTITY.cpp:961-987 == binary FUN_0041ff38) already opened
// this SAME type-0x14 resource, did `stream >> damageZoneCount` (==20) and allocated
// the INHERITED Entity::damageZones[count] array -- leaving the entries uninitialised
// (0xCDCDCDCD). We re-open the resource and build a fresh stream positioned PAST the
// 4-byte count the base consumed (DynamicMemoryStream initial_offset=4 == the binary's
// AdvancePointer(4)), then populate that inherited array. Faithful to FUN_004a1674
// (part_012.c ~10343-10399). The engine DamageZone base ctor (DAMAGE.cpp:200, shared
// MUNGA == binary FUN_0041df5c) consumes name/effect-sites/armor/damageScale/materials,
// then our Mech__DamageZone subclass reads its scalars+crit array+redirect table --
// alignment is exact because it is the same engine source.
//
ResourceDescription *dzRes =
MechFindResource(creation_message->resourceID,
ResourceDescription::DamageZoneStreamResourceType); // SearchList(id, type=0x14)
Check_Pointer(dzRes);
dzRes->Lock(); // FUN_00406cd0 load-on-first-lock
{
DynamicMemoryStream dzStream( // FUN_004032dc
dzRes->resourceAddress, dzRes->resourceSize, 4); // initial_offset=4: skip count word
for (int z = 0; z < damageZoneCount; ++z) // INHERITED Entity::damageZoneCount (==20)
{
damageZones[z] = // INHERITED Entity::damageZones[]
new (Memory::Allocate(0x1b8)) Mech__DamageZone(this, z, &dzStream); // FUN_0049ce50
}
for (int z = 0; z < damageZoneCount; ++z)
{
Zone(z)->SetLODParentPointers(); // FUN_0049d1d0
}
}
dzRes->Unlock();
// BRING-UP verify: confirm the inherited array is now populated (was 0xCDCDCDCD).
DEBUG_STREAM << "[zonebuild] damageZoneCount=" << damageZoneCount
<< " zone[0]=" << (void *)(damageZoneCount > 0 ? damageZones[0] : 0)
<< " zone[last]=" << (void *)(damageZoneCount > 0 ? damageZones[damageZoneCount - 1] : 0)
<< "\n" << std::flush;
//
// ---- Critical-subsystem table (resource type 0x1e) + death handler ----
// Faithful to FUN_004a1674 (part_012.c ~10388-10410); conditional on the segment.
// (Raw 0x1e: BT's crit-subsystem table. Do NOT use the RP411 enum name for 0x1e --
// it reuses that number for an unrelated table; only type 0x14 is a confirmed match.)
//
deathHandler = 0; // this[0x214]
ResourceDescription *critRes =
MechFindResource(creation_message->resourceID, 0x1e);
if (critRes != 0)
{
critRes->Lock();
{
// Load each zone's damage-state descriptor table from the type-0x1e
// stream, sequentially (binary loops mech[0x47] subsystems == our zones,
// calling FUN_0041e4a8 per object over the same stream). This is what
// MechDeathHandler walks to fire destroyed-skins + explosions on damage.
DynamicMemoryStream critStream(critRes->resourceAddress, critRes->resourceSize);
for (int z = 0; z < damageZoneCount; ++z)
{
Zone(z)->LoadCriticalSubsystems(&critStream); // FUN_0041e4a8 (now the real loader)
}
}
// The REAL MechDeathHandler (mechdmg.hpp), not the binary's 0x18 placement --
// plain new (our class carries a std::vector cache). Ticked from PerformAndWatch.
deathHandler = (int)new MechDeathHandler(this); // FUN_0042a984
critRes->Unlock();
}
wreckSmokeTimer = 0.0f;
matchlogDeathLogged = 0;
eyepointRotation = EulerAngles(Radian(0.0f), Radian(0.0f), Radian(0.0f));
//
// Cylinder hit-location table (resource type 0x1d = DamageLookupTableStream) --
// the per-impact zone resolver consumed by Mech::TakeDamageMessageHandler for
// unaimed/-1 hits (STEP 6). Faithful to the ctor @part_012.c:10411-10425: the
// table is found by the mech's DamageZoneStream (type-0x14) NAME -- the offline
// builder writes BOTH the 0x14 and 0x1d resources under the model name (see
// CreateDamageZoneStream ~line 1930), so they share it -- then streamed and
// cached at mech[0x111] (byte 0x444). (Was an empty-name StandingAnimation
// stub -> 0 rows; the real class is dmgtable.cpp.)
//
damageLookupTable = 0; // named member (Wword absorbs!)
ResourceDescription *dzForName =
MechFindResource(creation_message->resourceID,
ResourceDescription::DamageZoneStreamResourceType); // type 0x14 (for its name)
ResourceDescription *cylRes = (dzForName != 0)
? application->GetResourceFile()->FindResourceDescription(
dzForName->resourceName,
ResourceDescription::DamageLookupTableStreamResourceType) // FUN_00406ff8, type 0x1d
: 0;
if (cylRes != 0)
{
cylRes->Lock();
{
DynamicMemoryStream cylStream( // FUN_004032dc, offset 0
cylRes->resourceAddress, cylRes->resourceSize, 0);
DamageLookupTable *table = new DamageLookupTable(this, &cylStream); // FUN_0049ea48
damageLookupTable = (int)table; // named member (Wword absorbs!)
DEBUG_STREAM << "[cyl] table '" << dzForName->resourceName
<< "' layers=" << table->LayerCount() << "\n" << std::flush;
}
cylRes->Unlock();
}
else
{
DEBUG_STREAM << "[cyl] no DamageLookupTable (type 0x1d) for '"
<< (dzForName ? dzForName->resourceName : "?") << "'\n" << std::flush;
}
//
// Bind the three creation-name strings from the MakeMessage. CString::
// operator=(const char*) releases the empty ctor rep and allocates a fresh
// rep COPYING the message's inline char[20] -- byte-for-byte FUN_00402a98
// (part_012.c:10427-10441); the names survive the transient message.
//
resourceNameA = creation_message->resourceNameA; // badge param_2+0x7c -> Wword(0x211)
resourceNameB = creation_message->resourceNameB; // color param_2+0x90 -> Wword(0x212)
resourceNameC = creation_message->resourceNameC; // patch param_2+0xa4 -> Wword(0x213)
//
// Register this Mech in the global "Mechs" directory.
//
ReconRegistryT *registry = ReconRegistry(); // DAT_004efc94+0x24
MechDirectory *dir = registry->Find("Mechs"); // FUN_00403ad0
if (dir == 0)
{
dir = registry->Create("Mechs"); // FUN_004212b0
}
dir->Add(this); // (**(dir[4]+4))(dir+4,this)
// SLOT-0 DEMAND LATCH (task #7; see BTBuildMapperDemandLatch): every mech
// gets a base mapper in roster slot 0 so the drive harness + replicant
// gait have a demand latch; the viewpoint mech's is replaced by the real
// device mapper (SetMappingSubsystem).
{
extern Subsystem *BTBuildMapperDemandLatch(Mech *mech);
if (GetSubsystemCount() > 0 && GetSubsystem(0) == 0)
{
subsystemArray[0] = BTBuildMapperDemandLatch(this);
}
}
// @0049f788 -- distribute coolant flow across the condensers (post-init pass).
// The real RecomputeCondenserValves; sets each condenser's coolantFlowScale to
// valveState/sum(valveState) so the ValveSetting gauge reads the authentic 1/N
// (was a no-op stub -> the valve gauge showed 0).
BTRecomputeCondenserValves(this);
Check_Fpu();
}
//###########################################################################
// ~Mech -- @004a452c (vtable slot 0)
//
// Releases the animation object, removes the Mech from the "Mechs" registry
// (destroying the directory when it empties), tears down the death handler
// and the three name holders, then destructs every embedded container in
// reverse order and chains to ~JointedMover.
//
Mech::~Mech()
{
Check(this);
extern void BTDeregisterMech(Entity *m); // task #46 live-mech registry
BTDeregisterMech((Entity *)this);
extern void BTUnstashClipState(const Mech *m); // task #59 clip-set registry
BTUnstashClipState(this);
vtable = &PTR_FUN_0050cfa8;
// Teardown-health probe (env-gated): collisionLists@0x2e4 must be LIVE at ~Mech
// ENTRY. (Historical note: the old EXIT twin of this probe sat after an explicit
// `JointedMover::~JointedMover()` -- decomp epilogue glue wrongly reconstructed as
// source, since removed -- so its "FREED by a member dtor" verdict was really the
// first of a DOUBLED base-dtor chain. That double-run was the entire P5 crash.)
if (getenv("BT_ENABLE_TEARDOWN"))
{
void *cl = *(void **)((char *)this + 0x2e4);
unsigned probe = (cl != 0) ? *(unsigned *)cl : 0u;
DEBUG_STREAM << "[dtor] ~Mech ENTRY this=" << (void *)this
<< " collisionLists@0x2e4=" << cl << " *cl=0x" << std::hex << probe << std::dec
<< ((probe & 0xffffff00u) == 0xdddddd00u ? " <== ALREADY FREED (0xDD)" : " (live)")
<< "\n" << std::flush;
}
if (damageLookupTable != 0) // cylinder hit-location table (STEP 6)
{
delete (DamageLookupTable *)damageLookupTable; // frees layers/slices/entries
}
//
// Deregister from the "Mechs" directory.
//
ReconRegistryT *registry = ReconRegistry(); // DAT_004efc94+0x24
MechDirectory *dir = registry->Find("Mechs"); // FUN_00403ad0
if (dir != 0)
{
MechDirectoryIterator it(dir); // FUN_00421414
if (it.Current() == this)
{
it.Remove(); // slot 0x48
if (it.Current() == 0)
{
registry->Destroy(dir); // FUN_00421308
}
}
}
if (deathHandler != 0) // Wword(0x214)
{
delete (MechDeathHandler *)deathHandler; // real class -> plain delete
}
// resourceNameC/B/A: the binary's inline refcount-decrement/delete triple
// (part_012.c:11388-11403) is the CString member dtors -- implicit at the
// closing brace now that the members are real CStrings (dtor-epilogue rule:
// do NOT also write them out here).
//
// ⚠ RECONSTRUCTION RULE (root cause of the P5 death-row crash AND the app-exit
// crash -- forensic workflow, adversarially confirmed): in a Ghidra-decompiled
// DESTRUCTOR, the trailing member-dtor calls (FUN_xxx(this+N, 2): the binary's
// FUN_0043adb5 x5 telemetryFilter, ~ReconChain x5, ~AlarmIndicator x4,
// FUN_004a4d7f) and the final base-dtor call FUN_00425550(this, 0) are
// COMPILER-EMITTED epilogue glue, NOT source statements. C++ re-emits all of
// them implicitly at this closing brace; writing them out ran the whole
// ~JointedMover -> ~Mover -> ~Entity chain TWICE per Mech: the second pass
// re-delete[]d Mover::collisionLists (count word read from 0xDD-freed fill =
// the observed 0xdddddddb) and re-ran DeletePlugs over the destroyed segment
// table (= the P5 "EntitySegment already freed" 0xC0000374). Reconstruct only
// the dtor BODY above; let the compiler emit member + base destruction once.
// (Binary-oracle note: FUN_004278d4 at this+0x1af/0x197 is really
// ~SequenceController on bodyAnimation@0x6bc/legAnimation@0x65c -- their single
// implicit dtor run Unlocks the held clip correctly; see seqctl.cpp.)
//
Check_Fpu();
// (the byte-1 flag controls operator delete in the scalar-deleting dtor)
}
//###########################################################################
// TestInstance -- @004a4c7c
//
Logical
Mech::TestInstance() const
{
return IsDerivedFrom(ClassDerivations); // FUN_0041a1a4(**this[3],0x50bdb4)
}
//###########################################################################
//###########################################################################
// Simulation / replication
//###########################################################################
//###########################################################################
//###########################################################################
// ReadUpdateRecord -- @004a1232 (vtable slot 6)
//
// Applies one Mech-level replication record. The switch key is the
// inherited Simulation::UpdateRecord recordID (u16 @+6) == the updateModel
// bit that requested the record on the master. Byte-exact against
// part_012.c:9576-9692, with every binary offset mapped to the NAMED
// engine/port member (task #1, 2026-07-11; the old Wword-absorber body is
// gone). Cross-checked field-for-field against the writer @004a0c2c. [T1]
//
// Base chains: case 0 / default -> the JointedMover-level pair
// (FUN_0042249c = Mover::ReadUpdateRecord); cases 2..8 -> ONLY the
// Simulation base (FUN_0041bd34: lastUpdate + SetSimulationState) -- the
// old reconstruction chained JointedMover everywhere, which misparsed the
// short records.
//
// simulationState rides EVERY record header (+0xC) and the base reader
// applies it -- so the replicant's MovementMode() (death 9 / limbo 2 /
// airborne 3,4) tracks the master automatically; the case-0 / tail edge
// tests below then see the old->current transition.
//
void
Mech::ReadUpdateRecord(Simulation::UpdateRecord *message)
{
// RECORD-CADENCE probe (BT_RXJIT): quantify how EVENLY records arrive (wall-clock
// ms between arrivals) -- the jitter that makes the peer's corrections random.
if (getenv("BT_RXJIT") && GetInstance() == ReplicantInstance)
{
LARGE_INTEGER _now, _freq;
QueryPerformanceCounter(&_now); QueryPerformanceFrequency(&_freq);
static double s_lastMs = 0.0, s_min = 1e9, s_max = 0.0, s_acc = 0.0;
static int s_n = 0, s_rep = 0;
const double nowMs = (double)_now.QuadPart * 1000.0 / (double)_freq.QuadPart;
if (s_lastMs > 0.0)
{
const double gap = nowMs - s_lastMs;
s_n++; s_acc += gap;
if (gap > s_max) s_max = gap;
if (gap < s_min) s_min = gap;
}
s_lastMs = nowMs;
if (++s_rep >= 120 && s_n > 0)
{
DEBUG_STREAM << "[rxjit] records/win=" << s_n
<< " gapMs min=" << s_min << " avg=" << (s_acc / s_n)
<< " max=" << s_max
<< " burstiness(max/avg)=" << (s_max / (s_acc / s_n))
<< "\n" << std::flush;
s_rep = 0; s_n = 0; s_acc = 0.0; s_min = 1e9; s_max = 0.0;
}
}
static const int s_mrecLog = getenv("BT_REPL_LOG") ? 1 : 0;
if (s_mrecLog && message->recordID != 0)
DEBUG_STREAM << "[mrec-rx] type=" << (int)message->recordID
<< " ent=" << GetEntityID() << " len=" << (int)message->recordLength
<< " simState=" << (int)message->simulationState << "\n" << std::flush;
switch (message->recordID) // u16 @+6
{
case 0: // full pose (Mover record + speedDemand tail)
{
Mech__PoseUpdateRecord *record = (Mech__PoseUpdateRecord *)message;
// The mech OWNS its replicated-orientation channel (the type-4
// resync record): save updateOrigin's rotation (quat->euler
// roundtrip, exactly the binary's FUN_00408f44/FUN_00409a00) and
// the update angular velocity around the base reader, which would
// otherwise overwrite both from the pose record.
EulerAngles savedRot;
savedRot = updateOrigin.angularPosition; // FUN_00408f44 (quat->euler)
Vector3D savedAngV = updateVelocity.angularMotion; // FUN_00408440
Mover::ReadUpdateRecord(message); // FUN_0042249c
updateOrigin.angularPosition = savedRot; // FUN_00409a00 (euler->quat)
updateVelocity.angularMotion = savedAngV; // FUN_00408440
poseSyncLatch = 1; // @0x77c -- arm the dead-reckon re-base
if (getenv("BT_WIRE"))
DEBUG_STREAM << "[rx0] ent=" << GetEntityID()
<< " uvLin=(" << updateVelocity.linearMotion.x << ","
<< updateVelocity.linearMotion.z << ")"
<< " uPos=(" << updateOrigin.linearPosition.x << ","
<< updateOrigin.linearPosition.z << ")\n" << std::flush;
if (bodyAnimationState == 2 || bodyAnimationState == 3) // @0x728
{
projectedVelocity.linearMotion = ZeroVector; // FUN_0040a7f4(0x298, zeroMotion)
projectedVelocity.angularMotion = ZeroVector;
}
// Enter/leave simulation state 2 (disabled/limbo): re-base the
// local + projected origins onto the freshly replicated
// updateOrigin and drop the re-base latch.
if (GetSimulationState() != GetOldSimulationState()
&& (GetOldSimulationState() == 2 || GetSimulationState() == 2))
{
localOrigin = updateOrigin; // FUN_0040a938(0x100 <- 0x12c)
projectedOrigin = updateOrigin; // FUN_0040a938(0x260 <- 0x12c)
projectedVelocity = updateVelocity; // FUN_0040a7f4(0x298 <- 0x2c8)
poseSyncLatch = 0;
if ((instanceFlags & 0x4000) == 0) // byte(this+0x29)&0x40 [T4 bit name]
{
ResetPose(); // vcall vtable+0x34 (slot 13) [T4]
}
}
bodyTargetSpeed = record->speedDemand; // @0x6b4 <- rec+0x74
}
break;
case 2: // commanded-speed update
{
Mech__SpeedUpdateRecord *record = (Mech__SpeedUpdateRecord *)message;
{
// CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now()
// on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no
// pose, but that stamp shrinks the dead-reckoner's projection span
// (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the
// peer's position target jumps backward toward the stale origin, then the
// next pose record yanks it forward: target oscillation every frame while
// records churn. Keyboard driving churns them CONSTANTLY (the throttle
// lever sweeps every frame of a key-hold -> a type-2 speed record per
// frame), which autodrive's pinned throttle never does -- the 'skips when
// I drive, smooth when you drive' report. Preserve the clock; keep the
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
} // FUN_0041bd34
bodyTargetSpeed = record->speedDemand; // @0x6b4 <- rec+0x10
}
break;
case 3: // leg/body state + stability
{
Mech__StateUpdateRecord *record = (Mech__StateUpdateRecord *)message;
if (getenv("BT_GAITEV"))
DEBUG_STREAM << "[t3rx] legState=" << record->legState
<< " reset=" << record->legResetLatch
<< " myLegState=" << (int)legStateAlarm.GetLevel()
<< " myLegFrm=" << legAnimation.currentFrame
<< " spd=" << record->speedDemand << "\n" << std::flush;
{
// CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now()
// on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no
// pose, but that stamp shrinks the dead-reckoner's projection span
// (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the
// peer's position target jumps backward toward the stale origin, then the
// next pose record yanks it forward: target oscillation every frame while
// records churn. Keyboard driving churns them CONSTANTLY (the throttle
// lever sweeps every frame of a key-hold -> a type-2 speed record per
// frame), which autodrive's pinned throttle never does -- the 'skips when
// I drive, smooth when you drive' report. Preserve the clock; keep the
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
}
bodyResetLatch = record->legResetLatch; // @0x658 <- rec+0x10
stabilityAlarm.SetLevel(record->stability); // @0x4c4 <- rec+0x18
if (record->legState == 0)
{
legAnimation.Reset(1); // FUN_004283b8(0x65c, 1)
bodyStateAlarm.SetLevel(record->legState); // @0x714
}
else if (record->legState == 1)
{
bodyStateAlarm.SetLevel(record->legState);
}
else
{
SetBodyAnimation(record->legState); // FUN_004a800c
}
bodyTargetSpeed = record->speedDemand; // rec+0x1c
}
break;
case 4: // orientation + angular-velocity re-sync
{
Mech__ResyncUpdateRecord *record = (Mech__ResyncUpdateRecord *)message;
Simulation::ReadUpdateRecord(message);
creationTime = Now(); // @0x778 -- the dead-reckon ref time
{
EulerAngles e(Radian(record->eulerX), Radian(record->eulerY),
Radian(record->eulerZ));
updateOrigin.angularPosition = e; // FUN_00409a00 (euler->quat)
}
updateVelocity.angularMotion = record->angularVelocity; // @0x2d4 <- rec+0x1c
nextUpdate = Now(); // @0x2e0
{
// extrapolate the next-update horizon unless the record is stale
Scalar age = (Scalar)(nextUpdate.ticks - lastUpdate.ticks) / FrameTimeScale;
if (age < ReSyncSpeedThreshold) // 10.0f (_DAT_004a1670)
{
nextUpdate.ticks += (nextUpdate.ticks - lastUpdate.ticks);
}
}
bodyTargetSpeed = record->speedDemand; // rec+0x28
angSyncLatch = 1; // arm the peer heading re-anchor
}
break;
case 5: // knockdown
{
Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
{
// CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now()
// on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no
// pose, but that stamp shrinks the dead-reckoner's projection span
// (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the
// peer's position target jumps backward toward the stale origin, then the
// next pose record yanks it forward: target oscillation every frame while
// records churn. Keyboard driving churns them CONSTANTLY (the throttle
// lever sweeps every frame of a key-hold -> a type-2 speed record per
// frame), which autodrive's pinned throttle never does -- the 'skips when
// I drive, smooth when you drive' report. Preserve the clock; keep the
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
}
heatAlarm.SetLevel(record->heatLevel); // @0x450 <- rec+0x10
throttleState = record->throttleState; // @0x4a4 <- rec+0x14
fallDirection = record->fallDirection; // @0x4a8 <- rec+0x18
fallScalar = record->fallScalar; // @0x4b4 <- rec+0x24
SetBodyAnimation(0x20); // FUN_004a800c -- the knockdown clip
projectedVelocity.linearMotion = ZeroVector; // FUN_0040a7f4(0x298, zeroMotion)
projectedVelocity.angularMotion = ZeroVector;
bodyTargetSpeed = record->speedDemand; // rec+0x28
}
break;
case 6: // death
{
Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
{
// CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now()
// on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no
// pose, but that stamp shrinks the dead-reckoner's projection span
// (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the
// peer's position target jumps backward toward the stale origin, then the
// next pose record yanks it forward: target oscillation every frame while
// records churn. Keyboard driving churns them CONSTANTLY (the throttle
// lever sweeps every frame of a key-hold -> a type-2 speed record per
// frame), which autodrive's pinned throttle never does -- the 'skips when
// I drive, smooth when you drive' report. Preserve the clock; keep the
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
}
heatAlarm.SetLevel(record->heatLevel);
throttleState = record->throttleState;
fallDirection = record->fallDirection;
fallScalar = record->fallScalar;
bodyStateAlarm.SetLevel(0); // @0x714
legResetLatch = 1; // @0x654 (reader sets BOTH latches;
bodyResetLatch = 1; // @0x658 the writer only 0x658)
projectedVelocity.linearMotion = ZeroVector;
projectedVelocity.angularMotion = ZeroVector;
bodyTargetSpeed = record->speedDemand;
}
break;
case 7: // impact (fields only, no side effects)
{
Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
{
// CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now()
// on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no
// pose, but that stamp shrinks the dead-reckoner's projection span
// (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the
// peer's position target jumps backward toward the stale origin, then the
// next pose record yanks it forward: target oscillation every frame while
// records churn. Keyboard driving churns them CONSTANTLY (the throttle
// lever sweeps every frame of a key-hold -> a type-2 speed record per
// frame), which autodrive's pinned throttle never does -- the 'skips when
// I drive, smooth when you drive' report. Preserve the clock; keep the
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
}
heatAlarm.SetLevel(record->heatLevel);
throttleState = record->throttleState;
fallDirection = record->fallDirection;
fallScalar = record->fallScalar;
bodyTargetSpeed = record->speedDemand;
}
break;
case 8: // airborne/reverse cycle-rate selector
{
Mech__AirborneUpdateRecord *record = (Mech__AirborneUpdateRecord *)message;
{
// CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now()
// on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no
// pose, but that stamp shrinks the dead-reckoner's projection span
// (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the
// peer's position target jumps backward toward the stale origin, then the
// next pose record yanks it forward: target oscillation every frame while
// records churn. Keyboard driving churns them CONSTANTLY (the throttle
// lever sweeps every frame of a key-hold -> a type-2 speed record per
// frame), which autodrive's pinned throttle never does -- the 'skips when
// I drive, smooth when you drive' report. Preserve the clock; keep the
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
}
airborneSelect = record->airborne; // @0x3f4 <- rec+0x10
bodyTargetSpeed = record->speedDemand; // rec+0x14
}
break;
default: // type 1 (damage zones) + anything unknown -> the base chain
Mover::ReadUpdateRecord(message); // FUN_0042249c
break;
}
//
// Tail (shared with the writer @004a11d9): when the mech just LEFT
// simulation state 2 (re-activated from limbo), the binary rebuilt this
// entity's floating name bitmap (FUN_00436668 NameBitmapEntry +
// FUN_004364e4 RendererManager::ReplaceNameBitmap). That per-entry API
// belonged to the Division IG renderer and was removed in the WinTesla
// port -- the bulk SortAndReloadNameBitmaps refresh subsumes it (port
// precedent: btl4pb.cpp:560). The state edge is then explicitly
// acknowledged (SetState(current) collapses oldState = currentState).
//
if (GetOldSimulationState() != GetSimulationState()
&& GetOldSimulationState() == 2)
{
SetSimulationState(GetSimulationState()); // FUN_0041bbd8(this+0x2c, cur)
}
}
//###########################################################################
// WriteUpdateRecord -- @004a0c2c (vtable slot 7)
//
// Serializes one Mech-level record; the jump-table type == the updateModel
// bit that requested it (see Mech::ForceUpdate and the senders in
// mech2/mech4). Transcribed instruction-for-instruction from the recovered
// disasm (reference/decomp/mech_writeupdate_004a0c2c.disasm.txt), offsets
// mapped to named members (task #1, 2026-07-11). [T1]
//
// Conventions the engine requires: recordLength must equal each type's
// exact size (the receiver frames the stream on it, ENTITY.cpp:390) and
// subsystemID = 0 routes the record to the entity itself. The trailing
// speedDemand stamp mirrors into bodyTargetSpeed@0x6b4 -- the master-side
// "last replicated" baseline the perf-loop deadbands diff against.
//
void
Mech::WriteUpdateRecord(Simulation::UpdateRecord *message, int record_type)
{
// The binary stamps *(subsystemArray[0] + 0x128) = the controls mapper's
// speedDemand into every record tail -- roster slot 0, the mapper's one
// true home (task #7 untangle; the old [0x10d] cache is the message
// manager). (Bridge fn: this TU carries a local recon stub under the
// mapper's name, so the bridge is type-erased.)
extern Scalar BTMapperSpeedDemandRaw(void *mapper);
Scalar speedDemand = BTMapperSpeedDemandRaw((void *)MappingMapper());
static const int s_mrecLog = getenv("BT_REPL_LOG") ? 1 : 0;
if (s_mrecLog && record_type != 0)
DEBUG_STREAM << "[mrec-tx] type=" << record_type
<< " ent=" << GetEntityID() << " state=" << (int)GetSimulationState()
<< "\n" << std::flush;
switch (record_type)
{
case 0: // full pose @4a0c79
{
Mech__PoseUpdateRecord *record = (Mech__PoseUpdateRecord *)message;
EulerAngles savedRot;
savedRot = updateOrigin.angularPosition; // FUN_00408f44 (quat->euler)
Vector3D savedAngV = updateVelocity.angularMotion; // FUN_00408440
Mover::WriteUpdateRecord(message, record_type); // FUN_004225a4
record->subsystemID = 0; // word @+4
poseSyncLatch = 1; // @0x77c = 1
updateOrigin.angularPosition = savedRot; // FUN_00409a00 restore
updateVelocity.angularMotion = savedAngV; // FUN_00408440 restore
if (legAnimationState == 2 || legAnimationState == 3) // @0x3b0
{
projectedVelocity.linearMotion = ZeroVector; // FUN_0040a7f4(0x298, zero)
projectedVelocity.angularMotion = ZeroVector;
}
if (GetOldSimulationState() != GetSimulationState()
&& (GetOldSimulationState() == 2 || GetSimulationState() == 2))
{
projectedOrigin = updateOrigin; // FUN_0040a938(0x260 <- 0x12c)
projectedVelocity = updateVelocity; // FUN_0040a7f4(0x298 <- 0x2c8)
poseSyncLatch = 0;
}
record->speedDemand = speedDemand; // rec+0x74
bodyTargetSpeed = speedDemand; // @0x6b4
record->recordLength = 0x78;
}
break;
case 3: // leg/body state + stability @4a0d84
{
Mech__StateUpdateRecord *record = (Mech__StateUpdateRecord *)message;
Simulation::WriteUpdateRecord(message, record_type); // FUN_0041bd60
record->recordLength = 0x20;
record->subsystemID = 0;
record->legResetLatch = legResetLatch; // rec+0x10 <- @0x654
record->legState = legAnimationState; // rec+0x14 <- @0x3b0
record->stability = (int)stabilityAlarm.GetLevel(); // rec+0x18 <- @0x4d8
bodyResetLatch = record->legResetLatch; // @0x658 <- rec+0x10
// the writer runs the SAME state dispatch on itself:
if (record->legState == 0)
{
legAnimation.Reset(1); // FUN_004283b8(0x65c, 1)
bodyStateAlarm.SetLevel(record->legState);
}
else if (record->legState == 1)
{
bodyStateAlarm.SetLevel(record->legState);
}
else
{
SetBodyAnimation(record->legState); // FUN_004a800c
}
record->speedDemand = speedDemand; // rec+0x1c
bodyTargetSpeed = speedDemand;
}
break;
case 2: // commanded speed @4a0e5b
{
Mech__SpeedUpdateRecord *record = (Mech__SpeedUpdateRecord *)message;
Simulation::WriteUpdateRecord(message, record_type);
record->recordLength = 0x14;
record->subsystemID = 0;
record->speedDemand = speedDemand; // rec+0x10
bodyTargetSpeed = speedDemand;
}
break;
case 4: // orientation + angular-velocity re-sync @4a0e9e
{
Mech__ResyncUpdateRecord *record = (Mech__ResyncUpdateRecord *)message;
Simulation::WriteUpdateRecord(message, record_type);
record->recordLength = 0x2c;
record->subsystemID = 0;
localOrigin.angularPosition.Normalize(); // FUN_0040a138(0x10c)
{
EulerAngles e;
e = localOrigin.angularPosition; // FUN_00408f44 (quat->euler)
record->eulerX = e.pitch; // rec+0x10
record->eulerY = e.yaw;
record->eulerZ = e.roll;
}
record->angularVelocity = localVelocity.angularMotion; // rec+0x1c <- @0x1d0
creationTime = Now(); // @0x778
nextUpdate = Now(); // @0x2e0
{
Scalar age = (Scalar)(nextUpdate.ticks - lastUpdate.ticks) / FrameTimeScale;
if (age < ReSyncSpeedThreshold) // 10.0f
{
nextUpdate.ticks += (nextUpdate.ticks - lastUpdate.ticks);
}
}
updateOrigin.angularPosition = localOrigin.angularPosition; // FUN_00409968(0x138 <- 0x10c)
updateVelocity.angularMotion = localVelocity.angularMotion; // FUN_00408440(0x2d4 <- 0x1d0)
// RE-BASE the master's peer-estimate mirror (2026-07-14, replicant-chop
// fix): projectedOrigin/projectedVelocity model what the PEER will now
// extrapolate from this record. The send gate compares local vs projected
// (the peer's drift); without the re-base the mirror went stale, the gate
// fired EVERY frame, and the per-frame records re-based the replicant into
// the stall/snap chop. (The binary maintains this in the un-decompiled
// master perf; the writer-side re-base is the coherent reconstruction.)
projectedOrigin.angularPosition = localOrigin.angularPosition;
projectedVelocity.angularMotion = localVelocity.angularMotion;
// SCALAR peer-yaw mirror re-base (see mech.hpp): what the peer will now
// extrapolate -- this yaw, at this rate, from this moment.
{
YawPitchRoll _my; _my = localOrigin.angularPosition;
angMirrorYaw = (Scalar)_my.yaw;
angMirrorRate = (Scalar)localVelocity.angularMotion.y;
angMirrorTime = lastPerformance;
angMirrorValid = 1;
}
record->speedDemand = speedDemand; // rec+0x28
bodyTargetSpeed = speedDemand;
}
break;
case 5: // knockdown @4a0fab
{
Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
Simulation::WriteUpdateRecord(message, record_type);
record->recordLength = 0x2c;
record->subsystemID = 0;
record->heatLevel = (int)heatAlarm.GetLevel(); // rec+0x10 <- @0x464
record->throttleState = throttleState; // rec+0x14 <- @0x4a4
record->fallDirection = fallDirection; // rec+0x18 <- @0x4a8
record->fallScalar = fallScalar; // rec+0x24 <- @0x4b4
SetBodyAnimation(0x20); // FUN_004a800c(this, 0x20)
projectedVelocity.linearMotion = ZeroVector; // FUN_0040a7f4(0x298, zero)
projectedVelocity.angularMotion = ZeroVector;
record->speedDemand = speedDemand; // rec+0x28
bodyTargetSpeed = speedDemand;
}
break;
case 6: // death @4a104d
{
Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
Simulation::WriteUpdateRecord(message, record_type);
record->recordLength = 0x2c;
record->subsystemID = 0;
record->heatLevel = (int)heatAlarm.GetLevel();
record->throttleState = throttleState;
record->fallDirection = fallDirection;
record->fallScalar = fallScalar;
bodyStateAlarm.SetLevel(0); // @0x714
bodyResetLatch = 1; // @0x658 (writer sets ONLY this one)
projectedVelocity.linearMotion = ZeroVector;
projectedVelocity.angularMotion = ZeroVector;
record->speedDemand = speedDemand;
bodyTargetSpeed = speedDemand;
}
break;
case 7: // impact @4a10ff (fields only)
{
Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
Simulation::WriteUpdateRecord(message, record_type);
record->recordLength = 0x2c;
record->subsystemID = 0;
record->heatLevel = (int)heatAlarm.GetLevel();
record->throttleState = throttleState;
record->fallDirection = fallDirection;
record->fallScalar = fallScalar;
record->speedDemand = speedDemand;
bodyTargetSpeed = speedDemand;
}
break;
case 8: // airborne/reverse selector @4a117f
{
Mech__AirborneUpdateRecord *record = (Mech__AirborneUpdateRecord *)message;
Simulation::WriteUpdateRecord(message, record_type);
record->recordLength = 0x18;
record->subsystemID = 0;
record->airborne = airborneSelect; // rec+0x10 <- @0x3f4
record->speedDemand = speedDemand; // rec+0x14
bodyTargetSpeed = speedDemand;
}
break;
default: // type 1 (damage zones) + unknown @4a11cb
Mover::WriteUpdateRecord(message, record_type); // FUN_004225a4
break;
}
// Tail @4a11d9 -- identical to the reader's (see there for the name-
// bitmap note): acknowledge the leave-state-2 edge.
if (GetOldSimulationState() != GetSimulationState()
&& GetOldSimulationState() == 2)
{
SetSimulationState(GetSimulationState()); // FUN_0041bbd8(this+0x2c, cur)
}
}
// (The @004a4c54 body lives inline in mech.hpp as Mech::ForceUpdate -- the
// old "SetInstanceFlags" reading here was wrong twice over: +0x18 is
// Simulation::updateModel, not the entity instance flags, and FUN_0049fb54
// is IsDisabled (simulationState 2||9), not an is-network-copy test.)
//###########################################################################
//###########################################################################
// Resource authoring (offline tools)
//###########################################################################
//###########################################################################
//###########################################################################
// CreateModelResource -- @004a2da8
//
// Parses the Mech's "gamedata" notation block into a Mech::ModelResource and
// writes it to the resource file (segment 0xf). Faithful in structure; the
// long run of identical "Read field or fail" blocks is summarized -- each
// missing key logs "<model> missing <Key>!" and aborts. See Mech__ModelResource
// for the field->offset mapping.
//
ResourceDescription::ResourceID
Mech::CreateModelResource(
ResourceFile *resource_file,
const char *model_name,
NotationFile *model_file,
const ResourceDirectories *directories,
ModelResource *model
)
{
Logical owned = (model == 0);
if (owned)
{
model = (ModelResource *)Memory::Allocate(200); // 0x32 words
if (model == 0)
{
return (ResourceDescription::ResourceID)-1;
}
}
if (JointedMover::CreateModelResource( // FUN_004238bc
resource_file, model_name, model_file, directories, model) == -1
|| ParseJointResource( // FUN_00435ac8
resource_file, model_name, model_file, directories,
(char *)&model->reticleX) == -1) // rec words 0x1A-0x1B (Reticle level;
// the old 'skeletonName' block was
// fictitious -- no skl name in this record)
{
goto fail;
}
//
// AnimationPrefix (must be exactly 3 letters).
//
{
const char *prefix = 0;
if (!model_file->GetEntry("gamedata", "AnimationPrefix", &prefix))
{
DebugStream << model_name << " missing AnimationPrefix!";
goto fail;
}
if (strlen(prefix) != 3)
{
DebugStream << model_name << " must have 3 letter AnimationPrefix!";
goto fail_no_free;
}
strcpy(model->animationPrefix, prefix);
}
//
// Required scalar fields -- each aborts with a "missing <Key>!" diagnostic.
// (Order and keys exactly as in the decomp.)
//
if (!model_file->GetEntry("gamedata","MaxAcceleration", &model->maxAcceleration)) goto miss;
if (!model_file->GetEntry("gamedata","RelativeMechValue", &model->relativeMechValue)) goto miss;
if (!model_file->GetEntry("gamedata","LookLeftAngle", &model->lookLeftAngle)) goto miss;
if (!model_file->GetEntry("gamedata","LookRightAngle", &model->lookRightAngle)) goto miss;
if (!model_file->GetEntry("gamedata","LookFrontAngle", &model->lookFrontAngle)) goto miss;
if (!model_file->GetEntry("gamedata","LookBackAngle", &model->lookBackAngle)) goto miss;
if (!model_file->GetEntry("gamedata","WalkingTurnRate", &model->walkingTurnRate)) goto miss;
if (!model_file->GetEntry("gamedata","RunningTurnRate", &model->runningTurnRate)) goto miss;
{
const char *cameraOffset = 0;
if (!model_file->GetEntry("gamedata","CameraOffset",&cameraOffset))
{
DebugStream << model_name << " missing CameraOffset!";
}
ParseVector(cameraOffset, &model->cameraOffset); // FUN_00408944
}
if (!model_file->GetEntry("gamedata","DeathSplashDamage", &model->deathSplashDamage)) goto miss;
if (!model_file->GetEntry("gamedata","DeathSplashRadius", &model->deathSplashRadius)) goto miss;
//
// DeathEffect -- resolved to a resource id in the same file.
//
{
const char *deathEffect = 0;
if (!model_file->GetEntry("gamedata","DeathEffect",&deathEffect))
{
DebugStream << model_name << " missing DeathEffect!";
goto fail;
}
RDesc *fx = ((RFileX *)resource_file)->Find(deathEffect, 1, -1);
if (fx == 0)
{
DebugStream << model_name << " cannot find " << deathEffect
<< " in resource file!";
goto fail;
}
model->deathEffectResourceID = fx->id;
}
if (!model_file->GetEntry("gamedata","MaxUnstableAcceleration", &model->maxUnstableAcceleration)) goto miss;
if (!model_file->GetEntry("gamedata","UnstableAccelerationEffect", &model->unstableAccelerationEffect)) goto miss;
if (!model_file->GetEntry("gamedata","UnstableGunTheEngineEffect", &model->unstableGunTheEngineEffect)) goto miss;
if (!model_file->GetEntry("gamedata","UnstableSuperStopEffect", &model->unstableSuperStopEffect)) goto miss;
if (!model_file->GetEntry("gamedata","UnstableHighVelocityEffect", &model->unstableHighVelocityEffect)) goto miss;
if (!model_file->GetEntry("gamedata","UnstableStopedTurnEffect", &model->unstableStopedTurnEffect)) goto miss;
if (!model_file->GetEntry("gamedata","SuperStopAcceleration", &model->superStopAcceleration)) goto miss;
if (!model_file->GetEntry("gamedata","ThrottleAdjustment", &model->throttleAdjustment)) goto miss;
if (!model_file->GetEntry("gamedata","UpdateTurnVelocityDiffrence",&model->updateTurnVelocityDiffrence)) goto miss;
if (!model_file->GetEntry("gamedata","UpdateTurnDegreeDiffrence", &model->updateTurnDegreeDiffrence)) goto miss;
if (!model_file->GetEntry("gamedata","UpdatePositionDiffrence", &model->updatePositionDiffrence)) goto miss;
if (!model_file->GetEntry("gamedata","TimeDelay", &model->timeDelay)) goto miss;
//
// ShadowJointName (optional "Unspecified"; otherwise < 20 chars).
//
{
const char *shadowJoint = "Unspecified";
if (!model_file->GetEntry("gamedata","ShadowJointName",&shadowJoint))
{
DebugStream << model_name << " missing ShadowJointName!";
goto fail;
}
if (strcmp(shadowJoint, "Unspecified") != 0)
{
if (strlen(shadowJoint) > 0x13)
{
DebugStream << model_name
<< " ShadowJointName must be less than 20 characters long!";
goto fail;
}
strcpy(model->shadowJointName, shadowJoint);
}
}
if (owned)
{
RDesc *out = ((RFileX *)resource_file)->WriteResource( // FUN_00406db4
model_name, 0xf, 1, 0, model, 200, -1);
Memory::Free(model);
return out->id;
}
return (ResourceDescription::ResourceID)-1;
miss:
DebugStream << model_name << " missing field!"; // (per-key text in decomp)
fail:
if (owned)
{
Memory::Free(model);
}
fail_no_free:
return (ResourceDescription::ResourceID)-1;
}
//###########################################################################
// CreateModelResourceStub -- @004a2d78 (best-effort)
//
// Tiny wrapper that runs the JointedMover resource builder and stamps the
// Mech entity ClassID (0xBB9) into the record. Exact owner uncertain (could
// be a RegisteredClass make-callback); folded here for completeness.
//
Logical
Mech::CreateModelResourceStub(ModelResource *model)
{
if (!CreateModelResourceBase(model)) // FUN_00423864
{
return False;
}
// (the 200-byte record has NO classID field -- the old struct appended a
// fictitious one; the class identity rides the resource TYPE/segment.)
return True;
}
//###########################################################################
// CreateControlMappingStream -- @004a3794 (best-effort)
//
// Builds the I/O control-mapping table (resource segment 0x13) from the
// "<tool>:IOMapping" entries: for each control it resolves the control type,
// its owning subsystem (or the ControlsMapper itself), the Mode, and either
// an EventMapping(MessageID) or a DirectMapping(AttributeID). The decomp is
// a large diagnostic-heavy parser; reproduced in summary form. Logged
// failures use ':'-separated "<map>:<control> <reason>" messages.
//
ResourceDescription::ResourceID
Mech::CreateControlMappingStream(
const char *mapping_name,
NotationFile *mapping_file,
FindNameFunction find_name,
ResourceFile *resource_file,
const char *model_name,
NotationFile *model_file,
const ResourceDirectories *directories,
PlatformTool *current_tool
)
{
NotationIterator section(mapping_file); // FUN_0040485c
int count = ((NoteX *)mapping_file)->Count();
if (count == 0)
{
DebugStream << mapping_name << " is missing control mappings!";
return (ResourceDescription::ResourceID)-1;
}
CMapTable *table = // (count*6 + 6) * 0x18
(CMapTable *)Memory::Allocate((count * 6 + 6) * 0x18);
table->count = count;
int written = 0;
for (int i = 0; i < count; ++i)
{
ControlsMapping *entry = &table->entries[written];
const char *controlName = 0;
if (!((NoteX *)mapping_file)->ReadString(section, "IOMapping", &controlName))
{
DebugStream << mapping_name << ":" << section << " has no IOMapping!";
Memory::Free(table);
goto cleanup_fail;
}
if (!find_name(controlName, entry)) // (*param_3)
{
DebugStream << mapping_name << " control type "
<< controlName << " does not exist!";
Memory::Free(table);
goto cleanup_fail;
}
// ... resolve Subsystem / Mode / Type / Event|Direct mapping ...
// (Each branch validates a key and, on failure, logs and bails.
// On success the entry's subsystem index, mode, and target
// MessageID/AttributeID are filled, then `written`/section advance.)
// TODO: verify the per-field detail against @004a3794; the control-
// flow there is heavily duplicated by the optimizer.
++written;
}
table->count = written;
{
RDesc *out = ((RFileX *)resource_file)->WriteResource( // FUN_00406db4
mapping_name, 0x13, 1, 0, table,
table->byteSize(), -1);
Memory::Free(table);
section.Release();
return out->id;
}
cleanup_fail:
section.Release();
return (ResourceDescription::ResourceID)-1;
}
//###########################################################################
// CreateDamageZoneStream -- @004a474c
//
// Streams the skeleton (.skl), damage-zone (.dmg) and damage-lookup (.tbl)
// files for the model. Verifies DZoneCount matches the number of zones
// actually present, builds each Mech::DamageZone (FUN_0049d304) into a
// MemoryStream, writes the damage-zone segment (0x14) and the damage-lookup
// segment (0x1d), and returns the damage-zone resource id.
//
ResourceDescription::ResourceID
Mech::CreateDamageZoneStream(
ResourceFile *resource_file,
const char *model_name,
NotationFile *model_file,
const ResourceDirectories *directories
)
{
const char *sklName = 0;
if (!model_file->GetEntry("video", "skeleton", &sklName))
{
DebugStream << model_name << " is missing .skl file specification!";
return (ResourceDescription::ResourceID)-1;
}
CString sklPath = ((DirsX *)directories)->skeletonDir + sklName; // FUN_004064fc
NotationFile *sklFile = OpenNotation(sklPath, 1); // FUN_00403e84
if (sklFile->IsEmpty())
{
DebugStream << sklPath << " is empty or missing!";
goto fail_skl;
}
{
const char *dmgName = 0;
if (!model_file->GetEntry("gamedata", "DamageZones", &dmgName))
{
DebugStream << model_name << " is missing .dmg file specification!";
goto fail_skl;
}
CString dmgPath = ((DirsX *)directories)->gameDataDir + dmgName;
NotationFile *dmgFile = OpenNotation(dmgPath, 1);
if (dmgFile->IsEmpty())
{
DebugStream << dmgPath << " is empty or missing!";
goto fail_dmg;
}
int dzoneCount = 0;
if (!sklFile->GetEntry("DZoneInfo", "DZoneCount", &dzoneCount))
{
DebugStream << model_name << " is missing DZoneCount!";
goto fail_dmg;
}
MemStreamX dzMem; // FUN_0040328c
dzMem.Write(&dzoneCount, 4);
NotationList *zones = ((NoteX *)sklFile)->List("DamageZones", ""); // FUN_00404720
if (zones->Count() == 0)
{
DebugStream << "No dZones listed in DamageZones Part!";
DebugStream.Flush();
goto fail_dmg;
}
int found = 0;
for (NotationEntry *z = zones->First(); z != 0; z = z->Next())
{
++found;
char zoneName[32];
strcpy(zoneName, z->Name());
StreamDamageZone( // FUN_0049d304
resource_file, model_file, model_name,
sklFile, zoneName, dmgFile, directories, &dzMem);
}
if (found != dzoneCount)
{
DebugStream << "DZoneCount != damage zones found!";
DebugStream.Flush();
goto fail_dmg;
}
//
// Damage-lookup table (.tbl).
//
const char *tblName = 0;
if (!model_file->GetEntry("gamedata", "DamageLookupTable", &tblName))
{
DebugStream << model_name << " is missing .tbl file specification!";
goto fail_dmg;
}
{
CString tblPath = ((DirsX *)directories)->gameDataDir + tblName;
NotationFile *tblFile = OpenNotation(tblPath, 1);
if (tblFile->IsEmpty())
{
DebugStream << tblPath << " is empty or missing!";
goto fail_tbl;
}
MemStreamX tblMem; // FUN_0040328c
StreamDamageLookup(model_file, model_name, tblFile, // FUN_0049ed28
directories, &tblMem);
RDesc *out = // segment 0x14
((RFileX *)resource_file)->WriteResource(model_name, 0x14, 1, 0, &dzMem, -1);
((RFileX *)resource_file)->WriteResource(model_name, 0x1d, 1, 0, &tblMem, -1); // segment 0x1d
// release everything and return the damage-zone resource id
CloseNotation(sklFile);
CloseNotation(dmgFile);
zones->Release();
CloseNotation(tblFile);
return out->id;
}
fail_tbl:
;
fail_dmg:
CloseNotation(dmgFile);
}
fail_skl:
CloseNotation(sklFile);
return (ResourceDescription::ResourceID)-1;
}
//===========================================================================//
// Embedded-container ctor/dtor/iterator thunks
//---------------------------------------------------------------------------//
// The following are compiler-emitted (de)constructors and iterator helpers
// for the Mech's member containers -- the subsystem roster ChainOf<> views
// and their Slot members. They are listed here only to document the vtable
// addresses; the actual bodies are template instantiations.
//
// @004a4d60 / @004a4d7f Slot member Wword(0x106) vtable 0050cfa0
// @004a4dab / @004a4dca ChainOf member Wword(0x1eb) vtable 0050cf98 (heatable)
// @004a4df6 / @004a4e15 ChainOf member Wword(0x1ef) vtable 0050cf90 (powered)
// @004a4e41 / @004a4e60 ChainOf member this[499] vtable 0050cf88 (damageable)
// @004a4e8c .. @004a4ee9 ChainNode iterator vtable 0050cf34
// @004a4f15 .. @004a4f72 ChainOf<>::Iterator vtable 0050cee4
// @004a4f9e .. @004a4ffb ChainOf<>::Iterator vtable 0050ce94 (used in ctor)
//===========================================================================//
//===========================================================================//
// CONTINUED IN mech2.cpp .. mech4.cpp
//---------------------------------------------------------------------------//
// mech2.cpp @004a5028 : MoveAndCollide / per-frame Simulate, WriteUpdateRecord,
// LoadLowDetailBody @004a80d4, LoadHighDetailBody @004a86c8,
// SetStatusState @004a800c, damage routing.
// mech3.cpp : status reporting / cockpit display feed (vtable
// slot 15 @004abb40 lands here), PrintState.
// mech4.cpp : AI / pathing / scoring, registry glue.
//
// Also uncaptured (mech.cpp gap 0x004a0c2c, vtable slot 7): the pose/state
// writer paired with ReadUpdateRecord above.
//===========================================================================//