//===========================================================================//
// 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
// 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)
#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),
	};

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)
{
	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
	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::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
}

//
// 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,          attrPad),				// 0x18
	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,            attrPad),				// 0x2e
	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,              attrPad),				// 0x32
	ATTRIBUTE_ENTRY(Mech, RequestDuckAnimation,    attrPad),				// 0x33
	ATTRIBUTE_ENTRY(Mech, UnstablePercentage,      attrPad),				// 0x34
	ATTRIBUTE_ENTRY(Mech, SuperStop,               attrPad),				// 0x35
	ATTRIBUTE_ENTRY(Mech, IncomingLock,            attrPad),				// 0x36
	ATTRIBUTE_ENTRY(Mech, DuckState,               duckState),			// 0x37 (crouch posture, int)
	ATTRIBUTE_ENTRY(Mech, DistanceToMissile,       attrPad)				// 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)
	stateFlags    = 0;									// Wword(0xff)
	maxSpeed      = DAT_005209d0;						// FLT_MAX -> Wword(0x100)
	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).
	//
	resourceNameA = NewRefCounted();					// Wword(0x211)
	resourceNameB = NewRefCounted();					// Wword(0x212)
	resourceNameC = NewRefCounted();					// Wword(0x213)

	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)
	footStepDecay           = 0;
	{ extern void *g_btFootStepAddr; g_btFootStepAddr = &footStep; }	// DIAG: watcher-poll tracer target
	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

	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
	}

	maxSpeed     = 0x447a0000;							// 1000.0f -> Wword(0x101) (override)
	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)
	{
		SubProxy *s = (SubProxy *)subsystemArray[id];
		if (s != 0 && s->IsDerivedFrom(0x511830))
		{
			weaponRoster.Add((Subsystem *)s);
			stateFlags |= s->capabilityFlags;			// |= *(s+0x334) -> Wword(0x104)
		}
	}
	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
	Wword(0x1e1) = model->maxUnstableAcceleration;		// rec+0x80-0x94 -> mech+0x784..0x798
	Wword(0x1e2) = model->unstableAccelerationEffect;	//  (the six Unstable* effects)
	Wword(0x1e3) = model->unstableGunTheEngineEffect;
	Wword(0x1e4) = model->unstableSuperStopEffect;
	Wword(0x1e5) = model->unstableHighVelocityEffect;
	Wword(0x1e6) = model->unstableStopedTurnEffect;

	// Angle fields converted from degrees to radians.
	Wword(0x159) = (int)(model->lookLeftAngle  * DegreesToRadians);	// +0x50
	Wword(0x15a) = (int)(model->lookRightAngle * DegreesToRadians);	// +0x54
	Wword(0x15b) = (int)(model->lookFrontAngle * DegreesToRadians);	// +0x58
	Wword(0x15c) = (int)(model->lookBackAngle  * DegreesToRadians);	// +0x5c
	// 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;
	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.
	//
	resourceNameA = creation_message->resourceNameA;	// param_2+0x7c -> Wword(0x211)
	resourceNameB = creation_message->resourceNameB;	// param_2+0x90 -> Wword(0x212)
	resourceNameC = creation_message->resourceNameC;	// 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
	}

	ReleaseRefCounted(resourceNameC);					// Wword(0x213)
	ReleaseRefCounted(resourceNameB);					// Wword(0x212)
	ReleaseRefCounted(resourceNameA);					// Wword(0x211)

	//
	// ⚠ 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.
//===========================================================================//
