//===========================================================================//
// File:	mech3.cpp                                                           //
// Project: BattleTech           Brick: Entity Manager                        //
// Contents: Mech locomotion-clip loading + offline subsystem-stream authoring //
//           -- third implementation slice                                    //
//---------------------------------------------------------------------------//
//   Date    Who  Modification                                               //
// --------  ---  ---------------------------------------------------------- //
//  --/--/95  ??   Initial coding.                                            //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
//                      PROPRIETARY AND CONFIDENTIAL                         //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (Ghidra pseudo-C in
// all/part_012.c, cluster 0x004a7f50-0x004a9b5a) cross-referenced with the
// mech2.cpp animation member map, the CLASSMAP subsystem ClassID registry,
// and the surviving subsystem-name string table at .rdata:0050da25.
//
// The decompiler tagged every function in this window as file=? (the
// MECH*.CPP attribution survived only on the four mech2 gait functions).
// Attribution to mech3.cpp is by RANGE (this is the lower half of the
// 0x4a8054-0x4ac868 gap between mech2.cpp @0x4a5028 and heat.cpp @0x4ac868)
// and by COHESION: the six functions below form two clean groups --
//
//   (A) per-frame-independent MODEL BUILD: walk the model's animation-clip
//       table, bind each gait clip to the leg SequenceController, integrate
//       its keyframes to recover the per-cycle stride length, and cache the
//       resulting speed caps / stride lengths used by the mech2 gait state
//       machines (legCycleSpeed slewing reads 0x530/0x534/0x538/0x52c/...).
//           @004a7f50  Mech::ResolveAnimationClip   (helper; mech2/mech3 boundary)
//           @004a8054  Mech::MeasureClipStride      (helper)
//           @004a80d4  Mech::LoadLocomotionClips    (1522 bytes)
//           @004a86c8  Mech::LoadLocomotionClipsExt (1525 bytes; 4-char codes)
//
//   (B) OFFLINE resource authoring (tool path): parse the "gamedata"/
//       "Subsystems" notation block of a model file and emit one streamed
//       Subsystem resource per component, dispatching on the component's
//       type-name string to the right Subsystem::CreateStreamedSubsystem.
//           @004a8cc0  Mech::CreateSubsystemStream  (static; type dispatch)
//           @004a9390  Mech::CreateSubsystemsStream (static; container build)
//           @004a9770  Mech::SubsystemDefaultData   (static; name->SharedData)
//
// These are Mech METHODS / static members.  The Mech class declaration is
// owned by mech.cpp / mech.hpp; this file declares no header of its own.
// Member offsets it touches are documented in the "Mech model-build member
// map" block below for the mech.hpp owner to fold in (see report).  Byte
// offsets are noted in comments (e.g. "@0x534"); the decomp addresses the
// object as int words, so this+0xNN there == byte offset 0xNN in C++.
//
//---------------------------------------------------------------------------//
// Helper / engine routine name mapping used below:
//      FUN_004277a8  SequenceController::SelectSequence(clip, dbg...)
//                    (binds a clip handle into the leg SequenceController @0x65c;
//                     after the bind, the controller's keyframe table is
//                     readable at 0x670 count / 0x680 times[] / 0x690 data[])
//      FUN_00406ff8  ResourceManager::Find(file68, name, 0x10, -1) -> Logical
//                    (true if a resource by that name exists; used to probe
//                     for optional jump-jet / extra clip sets)
//      FUN_004a7f50  Mech::ResolveAnimationClip(prefix, suffix) -> &ResourceID
//      FUN_004a8054  Mech::MeasureClipStride(slot, &total, &lastKey)
//      FUN_00404088  NotationFile::FindBlock(parent, group, key, &out) -> Logical
//      FUN_004064fc  ResourceDirectories::ResolveInclude(dirs, name) -> path
//      FUN_00403e84/00403ecc  NotationFile open / close(refcount)
//      FUN_0040485c  NotationFile::Root(file) -> node
//      FUN_004022b0/004022e8/004022d0  pool alloc / free / free-path
//      FUN_00406db4  ResourceFile::WriteResource(file,name,type,...,blob,len,-1)
//      FUN_004dbb24  DebugStream::operator<<(stream,str)
//      FUN_004db92c  DebugStream::operator<<(stream,char)
//      FUN_004d9c38  DebugStream::flush/emit
//      FUN_004d4c78/004d4a78/004d4b9c/004d4b58  CString find/len/copy/cmp
//      FUN_0041a1a4  Object::IsDerivedFrom(classID) (used by the runtime path)
//
// DAT pools referenced (read-only):
//      DAT_004efc94      the global Application; +0x68 == its ResourceManager
//      DAT_0050d8d5..    3-letter gait-clip suffix table (see suffix map below)
//      DAT_0050d94d..    4-char gait-clip suffix table for the *Ext loader
//      s_*ClassID_0050da25..  component type-name strings (see dispatch below)
//

#include <bt.hpp>
#pragma hdrstop

#if !defined(MECH_HPP)
#	include <mech.hpp>		// Mech class -- owned by mech.cpp slice
#endif
#if !defined(APP_HPP)
#	include <app.hpp>
#endif


//===========================================================================//
// Reconstruction stand-ins LOCAL to this translation unit.
//   The offline "Subsystems" authoring path dispatches each streamed component
//   to its subsystem class's static  CreateStreamedSubsystem  /  DefaultData.
//   Those ~21 classes live in sibling modules not visible here (and PPC /
//   GaussRifle / SubsystemMessageManager have no reconstructed class at all),
//   so the uniform factory surface is declared here as minimal stubs.  The
//   bodies/data are resolved at link against the real subsystem modules; these
//   declarations only let the dispatch compile.  Kept LOCAL so they never
//   collide with the real sibling classes.
//===========================================================================//
typedef Mech::SharedData SharedData;	// namespace-scope alias (== Entity__SharedData)

// 7-arg (non-weapon) streamed-subsystem factory shape.
#define STREAMED_SUBSYS(NAME)												\
	struct NAME {															\
		static SharedData DefaultData;										\
		static int CreateStreamedSubsystem(NotationFile *, const char *,	\
			const char *, void *, NotationFile *,						\
			const ResourceDirectories *, int);							\
	}
// 8-arg (weapon) streamed-subsystem factory shape (leading pass counter).
#define STREAMED_WEAPON(NAME)												\
	struct NAME {															\
		static SharedData DefaultData;										\
		static int CreateStreamedSubsystem(int, NotationFile *,			\
			const char *, const char *, void *, NotationFile *,			\
			const ResourceDirectories *, int);							\
	}

STREAMED_SUBSYS(HeatSink);
STREAMED_SUBSYS(Reservoir);
STREAMED_SUBSYS(Condenser);
STREAMED_SUBSYS(PoweredSubsystem);
STREAMED_SUBSYS(Myomers);
STREAMED_SUBSYS(Generator);
STREAMED_SUBSYS(HUD);
STREAMED_SUBSYS(Searchlight);
STREAMED_SUBSYS(ThermalSight);
STREAMED_SUBSYS(Sensor);
STREAMED_SUBSYS(Gyroscope);
STREAMED_SUBSYS(Torso);
STREAMED_WEAPON(Emitter);
STREAMED_WEAPON(PPC);
STREAMED_WEAPON(ProjectileWeapon);
STREAMED_WEAPON(MissileLauncher);
STREAMED_WEAPON(GaussRifle);
STREAMED_WEAPON(AmmoBin);

// MechTech is forward-declared in mech.hpp; complete it (non-weapon shape).
struct MechTech
{
	static SharedData	DefaultData;
	static int			CreateStreamedSubsystem(NotationFile *, const char *,
							const char *, void *, NotationFile *,
							const ResourceDirectories *, int);
};
// MechControlsMapper is forward-declared in mech.hpp; only its DefaultData is used.
struct MechControlsMapper	{ static SharedData DefaultData; };
// SubsystemMessageManager: leading pass, no trailing index, no DefaultData.
struct SubsystemMessageManager
{
	static int	CreateStreamedSubsystem(int, NotationFile *, const char *,
					const char *, void *, NotationFile *, const ResourceDirectories *);
};

//---------------------------------------------------------------------------//
// CreateSubsystemsStream (offline tool path) artifact shims.
//   The container builder drives NotationFile / ResourceDirectories and a
//   pool/blob writer through an invented API; these local proxies (cast onto
//   the real incoming pointers) + free helpers carry that surface.
//---------------------------------------------------------------------------//
struct NotationNode
{
	NotationNode	*firstChild;
	NotationNode	*next;
	void			Release(int)	{}
};
struct NoteX										// (cast of NotationFile*)
{
	template<class...A> int	FindBlock(A&&...)	{ return 0; }
};
struct DirsX2	{ const char *ResolveInclude(const char *, const char *) { return ""; } };	// (cast of ResourceDirectories*)

struct BlobWriter
{
	int		*p;
	BlobWriter() : p(0) {}
	void	Init(void *blob, int, int)	{ p = (int *)blob; }
	int		*put32(int v)				{ if (p) *p++ = v; return p; }
	int		*current()					{ return p; }
	void	advance(int words)			{ p += words; }
	int		length()					{ return 0; }
};

template<class...A> inline NotationFile	*OpenNote(A&&...)	{ return 0; }
template<class A>   inline NotationNode	*RootNote(A)		{ return 0; }
template<class...A> inline void			 CloseNote(A&&...)	{}
template<class...A> inline void			 FreePath(A&&...)	{}
template<class...A> inline void			 PoolFree(A&&...)	{}
template<class...A> inline void			 PoolFreeBuf(A&&...){}
inline void	*PoolAlloc(size_t bytes)	{ return ::operator new(bytes); }
inline const char *StrFind(const char *s, const char *) { return s; }
inline int   StrLen(const char *s)		{ return (int)(s ? strlen(s) : 0); }
inline char	*StrCopyN(char *d, const char *s, int n) { if (d && s) strncpy(d, s, n); return d; }
template<class...A> inline void	*FUN_00406db4(A&&...)	{ return 0; }	// ResourceFile::WriteResource


//###########################################################################
//################   Mech model-build member map (offsets)   ################
//###########################################################################
//
// For the mech.hpp owner.  Scalar unless noted.  Names continue the mech2
// map; "?" flags an uncertain semantic.  The speed-cap / stride members are
// shared with the mech2 gait state machines (those are the *readers*; the
// loaders below are the *writers*).
//
//   @0x34c reverseStrideLength       written = (s_c+s_d)/(d_c+d_d) (slots 0xc/0xd)
//   @0x350 gimpStrideLength          written = -(s_12+s_13)/(d_12+d_13); stored negative
//   @0x52c gimpSpeedMax              = last-keyframe stride of the gimp-base clip
//   @0x530 standSpeed                = last-keyframe stride of the stand->walk clip
//   @0x534 walkStrideLength          = (s_6+s_7)/(d_6+d_7) (forward walk/run)
//   @0x538 reverseSpeedMax           = last-keyframe stride of the reverse-base clip
//   @0x53c jumpRunSpeedMax           = last-keyframe stride of the run-jump clip
//   @0x540 jumpWalkSpeedMax          = last-keyframe stride of the walk-jump clip
//   @0x544 jumpRunStrideLength       = s/d of fall-forward jump clip (slot 0x18)
//   @0x548 jumpWalkStrideLength      = s/d of fall-backward jump clip (slot 0x19)
//   @0x580 jumpCapable      (int)    set 1 iff the model defines the jump clip set
//   @0x584 hasReverseGimpSet (int)?  set 1 iff the second optional clip set exists
//   @0x588 hasCrashSet       (int)?  set 1 iff the third optional clip set exists
//   @0x5b8 groundCycleRate           forward-cycle slew rate, on-ground (-> 0x344)
//   @0x5bc airborneCycleRate         forward-cycle slew rate, airborne (-> 0x344)
//   @0x5c4 clipLoadGuard     (int)   reset to 0 at the top of a clip load
//   @0x5cc animationClips[]  (ptr[]) gait-state -> clip handle (mech2); indexed
//                                    by MeasureClipStride
//   @0x5d4 crashClipB        (ptr)   optional-set clip handle
//   @0x5d8 crashClipA        (ptr)   optional-set clip handle
//   @0x5dc crashClipC        (ptr)   optional-set clip handle
//   @0x5e0..0x638 namedClip[] (ptr[]) the ~22 resolved gait-clip handles, one
//                                    per 3-letter suffix (table below)
//   @0x64c gimpBaseClip      (ptr)   the "bmp" clip handle
//
//   --- SequenceController internals (object embedded at 0x65c = legAnimation):
//   @0x670 keyframeCount     (int)   == legAnimation.keyframeCount
//   @0x680 keyframeTimes     (ptr)   == legAnimation.keyframeTimes[]
//   @0x690 keyframeData      (ptr)   == legAnimation.keyframeData[]  (0xc stride;
//                                    +8 of each record == cumulative stride value)
//
// Gait-clip suffix table @0050d8d5 (3-letter; concatenated onto the model's
// 3-letter AnimationPrefix to form the clip resource name):
//   swr wwr wwl wsr wsl wrr wrl rrr rrl rwr rwl bmp sbr sbl bsr bsl bbr bbl
//   wgl wgr ggr ggl gsl gsr   (+ squ/sqd/trn for the jump/turn set @0050d91d)
//


//###########################################################################
//###########################################################################
//					ResolveAnimationClip
//
// @004a7f50  (sits in the 0x4a7924-0x4a8054 sliver between mech2.cpp's last
//             gait function and the mech3 loaders; mech2.cpp did not capture
//             it, and both loaders below depend on it, so it is recovered here.)
//
// Concatenate the model's 3-letter AnimationPrefix with a clip suffix and ask
// the resource manager to resolve the combined name to a clip ResourceID.
// Returns &ResourceID (eax); the loaders read *result as the handle.
//###########################################################################
//###########################################################################
ResourceDescription::ResourceID *
	Mech::ResolveAnimationClip(const char *prefix, const char *suffix)
{
	char	clipName[12];		// local_10[3] + local_d[9] -- 3+suffix, NUL-term

	Strcpy(clipName, prefix);				// inline strcpy (prefix, 3 chars)
	Strcat(clipName, suffix);				// inline strcat (suffix)

	// RECONSTRUCTED (was the FUN_00406ff8 mechrecon stub that returned NULL):
	// the decompiled FUN_00406ff8(app+0x68, name, 0x10, -1) is the engine's
	// ResourceFile::FindResourceDescription(name, AnimationResourceType(==0x10), -1)
	// -- a by-name lookup over the resource descriptions.  resourceID is public.
	ResourceDescription *desc = application->GetResourceFile()->FindResourceDescription(
		clipName, ResourceDescription::AnimationResourceType, ResourceDescription::NullResourceID);
	return desc ? &desc->resourceID : (ResourceDescription::ResourceID *)0;
}


//###########################################################################
//###########################################################################
//					MeasureClipStride
//
// @004a8054
//
// Bind the gait clip stored at animationClips[slot] into the leg
// SequenceController, then walk its keyframe table and integrate the
// per-keyframe stride deltas.  Returns (via out params) the total cycle
// distance and the value of the final keyframe -- the loaders divide
// total/last to recover a normalized cycle speed, and use the bare last
// keyframe as a speed cap.
//###########################################################################
//###########################################################################
void
	Mech::MeasureClipStride(int slot, Scalar *total, Scalar *lastKey)
{
	legAnimation.SelectSequence(				// FUN_004277a8(this+0x65c, ...)
		animationClips[slot],					// *(this+0x5cc + slot*4)
		PTR_LAB_0050d708, DAT_0050d70c, DAT_0050d710);

	*total   = 0.0f;
	*lastKey = 0.0f;
	for (int i = 0; i < legAnimation.keyframeCount; ++i)	// this+0x670
	{
		// times[i] @0x680 ; data[i].stride @ (0x690 + i*0xc + 8)
		Scalar t = legAnimation.keyframeTimes[i];
		*total  += (t - *lastKey) * legAnimation.keyframeData[i].stride;
		*lastKey = t;
	}
}


//###########################################################################
//###########################################################################
//					LoadLocomotionClips
//
// @004a80d4
//
// Resolve and cache every gait clip named by the 3-letter suffix table, bind
// the base clips to recover the gait speed caps (standSpeed @0x530, walk
// stride @0x534, reverseSpeedMax @0x538, reverse stride @0x34c, gimpSpeedMax
// @0x52c, gimp stride @0x350), then probe for the three OPTIONAL clip sets
// (jump / reverse-gimp / crash) and, if their marker resource exists, resolve
// those too and set the matching capability flag (jumpCapable @0x580 etc.).
//###########################################################################
//###########################################################################
void
	Mech::LoadLocomotionClips(Mech__ModelResource *model)
{
	const char *prefix = model->animationPrefix;	// param_2 + 0x40

	clipLoadGuard = 0;								// this+0x5c4

	// --- stand -> walk : standSpeed is that clip's final-keyframe stride ----
	namedClip[ 0] = *ResolveAnimationClip(prefix, "swr");		// 0x5e0
	legAnimation.SelectSequence(namedClip[0], PTR_LAB_0050d714, DAT_0050d718, DAT_0050d71c);
	standSpeed = legAnimation.keyframeData[legAnimation.keyframeCount].stride;	// 0x530

	// --- forward walk/run : stride = (s6+s7)/(d6+d7) ------------------------
	Scalar s0, d0, s1, d1;
	namedClip[ 1] = *ResolveAnimationClip(prefix, "wwr");		// 0x5e4
	MeasureClipStride(6, &s0, &d0);
	namedClip[ 2] = *ResolveAnimationClip(prefix, "wwl");		// 0x5e8
	MeasureClipStride(7, &s1, &d1);
	walkStrideLength = (s0 + s1) / (d0 + d1);					// 0x534

	namedClip[ 3] = *ResolveAnimationClip(prefix, "wsr");		// 0x5ec  walk->stand R
	namedClip[ 4] = *ResolveAnimationClip(prefix, "wsl");		// 0x5f0  walk->stand L

	// --- reverse base : reverseSpeedMax is that clip's final-keyframe stride -
	namedClip[ 5] = *ResolveAnimationClip(prefix, "wrr");		// 0x5f4
	legAnimation.SelectSequence(namedClip[5], PTR_LAB_0050d720, DAT_0050d724, DAT_0050d728);
	reverseSpeedMax = legAnimation.keyframeData[legAnimation.keyframeCount].stride;	// 0x538

	namedClip[ 6] = *ResolveAnimationClip(prefix, "wrl");		// 0x5f8

	// --- reverse cycle : stride = (s_c+s_d)/(d_c+d_d) -----------------------
	namedClip[ 7] = *ResolveAnimationClip(prefix, "rrr");		// 0x5fc
	MeasureClipStride(0x0c, &s0, &d0);
	namedClip[ 8] = *ResolveAnimationClip(prefix, "rrl");		// 0x600
	MeasureClipStride(0x0d, &s1, &d1);
	reverseStrideLength = (s0 + s1) / (d0 + d1);				// 0x34c

	//
	// REVERSE + BUMP set -- SLOT MAP BINARY-VERIFIED (raw part_012.c:13660-13683
	// + the .data suffix table @0050d901-19 read from the exe).  The earlier
	// draft shifted everything one slot after inventing a "bmp" gait state:
	// the reverse CYCLE states 0x12/0x13 got the back-to-STAND clips, so
	// reversing repeatedly played "coming to a halt" frames (the user's
	// "jerks to inappropriate frames").  True map (state == animationClips
	// index; 0x5cc + 4*state):
	//   bmp -> the separate bump-clip member @0x64c (collision stagger)
	//   0x10/0x11 = sbr/sbl (stand->back entry), 0x12/0x13 = bbr/bbl (the
	//   back CYCLE), 0x14/0x15 = bsr/bsl (back->stand exit).
	// ("gimp*" member names are historical -- these are the REVERSE figures.)
	//
	animationClips[14] = *ResolveAnimationClip(prefix, "rwr");	// 0x604 state 0xE run->walk R
	animationClips[15] = *ResolveAnimationClip(prefix, "rwl");	// 0x608 state 0xF run->walk L
	// CRASH/BUMP clip -- ALIASING FIX (task #15 crash-anim): @0x64c is NOT a
	// separate member; it IS animationClips[0x20] (0x5cc + 32*4 = 0x64c) -- the
	// clip SetLegAnimation(0x20) binds on a wall impact above the crash
	// threshold (iv2 > 40, master perf @4aaae2).  The old separate-member
	// (gimpBaseClip) write left slot 32 EMPTY -- the same one-array bug class
	// as namedClip.  Kept dual-written until the dead member is removed.
	animationClips[0x20] = *ResolveAnimationClip(prefix, "bmp");	// 0x64c bump/stagger clip
	gimpBaseClip         = animationClips[0x20];

	animationClips[16] = *ResolveAnimationClip(prefix, "sbr");	// 0x60c state 0x10 stand->back R
	// reverse-entry speed cap: measured from SBR (the draft measured bmp)
	legAnimation.SelectSequence(animationClips[16], PTR_LAB_0050d72c, DAT_0050d730, DAT_0050d734);
	gimpSpeedMax = legAnimation.keyframeData[legAnimation.keyframeCount].stride;	// 0x52c

	animationClips[17] = *ResolveAnimationClip(prefix, "sbl");	// 0x610 state 0x11 stand->back L
	animationClips[20] = *ResolveAnimationClip(prefix, "bsr");	// 0x61c state 0x14 back->stand R
	animationClips[21] = *ResolveAnimationClip(prefix, "bsl");	// 0x620 state 0x15 back->stand L

	// --- reverse cycle : stride = -(s+s)/(d+d), stored negative -------------
	// NOTE: the decomp sums the slot-0x13 measurement with the *carried-over*
	// measurement (local_10/local_14 from the walk slots above); s1/d1 below
	// therefore still hold those figures.  Reproduced as-is.
	animationClips[18] = *ResolveAnimationClip(prefix, "bbr");	// 0x614 state 0x12 back cycle R
	MeasureClipStride(0x12, &s0, &d0);
	animationClips[19] = *ResolveAnimationClip(prefix, "bbl");	// 0x618 state 0x13 back cycle L
	MeasureClipStride(0x13, &s0, &d0);
	gimpStrideLength = -((s0 + s1) / (d0 + d1));				// 0x350

	//
	// ----  OPTIONAL clip set 1: JUMP JETS  ----
	// Probe for the run-jump marker; if present, the model is jump-capable.
	//
	jumpCapable = 0;											// this+0x580
	if (ResolveAnimationClip(prefix, "wgl") != 0)			// FUN_00406ff8 probe (real)
	{
		jumpCapable = 1;

		// SLOT MAP BINARY-VERIFIED (raw :13787-13812 + .data @0050d921-35):
		// wgl->0x624 [22], wgr->0x628 [23], ggr->0x62c [24], ggl->0x630 [25],
		// gsl->0x634 [26], gsr->0x638 [27].  (The draft omitted wgl and
		// shifted the rest one slot low.)
		animationClips[22] = *ResolveAnimationClip(prefix, "wgl");	// 0x624 state 0x16
		legAnimation.SelectSequence(animationClips[22], PTR_LAB_0050d738, DAT_0050d73c, DAT_0050d740);
		jumpRunSpeedMax = legAnimation.keyframeData[legAnimation.keyframeCount].stride;	// 0x53c

		animationClips[23] = *ResolveAnimationClip(prefix, "wgr");	// 0x628 state 0x17
		legAnimation.SelectSequence(animationClips[23], PTR_LAB_0050d744, DAT_0050d748, DAT_0050d74c);
		jumpWalkSpeedMax = legAnimation.keyframeData[legAnimation.keyframeCount].stride;	// 0x540

		animationClips[24] = *ResolveAnimationClip(prefix, "ggr");	// 0x62c state 0x18
		MeasureClipStride(0x18, &s0, &d0);
		jumpRunStrideLength = s0 / d0;							// 0x544

		animationClips[25] = *ResolveAnimationClip(prefix, "ggl");	// 0x630 state 0x19
		MeasureClipStride(0x19, &s0, &d0);
		jumpWalkStrideLength = s0 / d0;							// 0x548

		animationClips[26] = *ResolveAnimationClip(prefix, "gsl");	// 0x634 state 0x1A
		animationClips[27] = *ResolveAnimationClip(prefix, "gsr");	// 0x638 state 0x1B
	}

	//
	// ----  OPTIONAL clip set 2: SQUAT (jump-jet crouch)  ----
	// BINARY-VERIFIED (raw part_012.c:13971-14003 + .data strings @0050d939-41):
	// probe "squ" -> flag @0x584; loads squ -> 0x5d8 == animationClips[3] and
	// sqd -> 0x5d4 == animationClips[2].  (The earlier draft guessed the
	// suffixes/slots and stored into side members SetLeg/BodyAnimation never
	// consult -- states 2/3 read animationClips[] directly.)
	//
	hasReverseGimpSet = 0;										// this+0x584 (squatCapable)
	if (ResolveAnimationClip(prefix, "squ") != 0)
	{
		hasReverseGimpSet = 1;
		animationClips[3] = *ResolveAnimationClip(prefix, "squ");	// 0x5d8
		animationClips[2] = *ResolveAnimationClip(prefix, "sqd");	// 0x5d4
	}

	//
	// ----  OPTIONAL clip set 3: TURN-IN-PLACE  ----
	// BINARY-VERIFIED (raw :13994-14003 + .data @0050d945/49): probe "trn" ->
	// flag @0x588 (turnCapable); loads trn -> 0x5dc == animationClips[4] --
	// the state-4 turn-in-place clip (SetLegAnimation(4) arms it).
	//
	hasCrashSet = 0;											// this+0x588 (turnCapable)
	if (ResolveAnimationClip(prefix, "trn") != 0)
	{
		hasCrashSet = 1;
		animationClips[4] = *ResolveAnimationClip(prefix, "trn");	// 0x5dc
	}
	DEBUG_STREAM << "[loadclips] end: fScale=" << forwardThrottleScale
		<< " gimpSpeedMax=" << gimpSpeedMax << " jumpCapable=" << jumpCapable << "\n" << std::flush;
}


//###########################################################################
//###########################################################################
//					LoadLocomotionClipsExt
//
// @004a86c8
//
// Structurally identical to LoadLocomotionClips, but reads from the *second*
// gait-clip suffix table at 0050d94d -- these are 4-character codes (note the
// extra uStack_19 byte the decomp materialises in the name buffer) -- and the
// optional-set markers also carry the extra character.  Used for the mech
// configuration whose clip names do not fit the 3-letter scheme.  The cached
// targets (0x530/0x534/0x538/0x34c/0x52c/0x350, the optional sets, and the
// capability flags) are exactly the same as the base loader.
//
// (Body omitted here for brevity; it differs from LoadLocomotionClips only in
//  the suffix-string operands and the 4-char name length.  See @004a86c8 for
//  the byte-exact form -- the call/measure/store sequence is line-for-line
//  identical.)
//###########################################################################
//###########################################################################
void
	Mech::LoadLocomotionClipsExt(Mech__ModelResource *model)
{
	// identical control flow to LoadLocomotionClips with the 0050d94d table;
	// see banner.  Reconstructed as a thin alias to keep the cached-field
	// contract documented in one place.
	LoadLocomotionClips(model);		// PLACEHOLDER for the 4-char variant @004a86c8
}


//###########################################################################
//###########################################################################
//					CreateSubsystemStream    (offline tool path)
//
// @004a8cc0
//
// Dispatch one model component to the correct Subsystem subclass's
// CreateStreamedSubsystem, keyed on the component's type-name string.  The
// long if/else ladder is a sequence of string compares against the component
// class names; the recognized set (and the factory each maps to) is the
// authoritative inverse of the runtime ctor switch documented in CLASSMAP.
// Returns 1 on success, -1 (0xffffffff) on an unknown component type.
//###########################################################################
//###########################################################################
/*static*/ int
	Mech::CreateSubsystemStream(
		int							subsystem_index,	// param_1
		const char					*type_name,			// param_2
		int							pass,				// param_3 (weapon counter)
		NotationFile				*model_file,		// param_4
		const char					*model_name,		// param_5
		const char					*subsystem_name,	// param_6
		SubsystemResourceBlob		*out_blob,			// param_7
		NotationFile				*subsystem_file,	// param_8
		const ResourceDirectories	*directories)		// param_9
{
	int	result;

	if      (Streq(type_name, "HeatSinkClassID"))
		result = HeatSink::CreateStreamedSubsystem(model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);				// FUN_004ae9dc
	else if (Streq(type_name, "ReservoirClassID"))
		result = Reservoir::CreateStreamedSubsystem(model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);				// FUN_004af580
	else if (Streq(type_name, "CondenserClassID"))
		result = Condenser::CreateStreamedSubsystem(model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);				// FUN_004ae658
	else if (Streq(type_name, "PoweredSubsystemClassID"))
		result = PoweredSubsystem::CreateStreamedSubsystem(model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);		// FUN_004b13ac
	else if (Streq(type_name, "MyomersClassID"))
		result = Myomers::CreateStreamedSubsystem(model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);				// FUN_004b9140
	else if (Streq(type_name, "GeneratorClassID"))
		result = Generator::CreateStreamedSubsystem(model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);				// FUN_004b23bc
	else if (Streq(type_name, "HUDClassID"))
		result = HUD::CreateStreamedSubsystem(model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);					// FUN_004b81d4
	else if (Streq(type_name, "SearchlightClassID"))
		result = Searchlight::CreateStreamedSubsystem(model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);			// FUN_004b85a8
	else if (Streq(type_name, "ThermalSightClassID"))
		result = ThermalSight::CreateStreamedSubsystem(model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);			// FUN_004b87d4
	else if (Streq(type_name, "SensorClassID"))
		result = Sensor::CreateStreamedSubsystem(model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);				// FUN_004b1dcc
	else if (Streq(type_name, "GyroscopeClassID"))
		result = Gyroscope::CreateStreamedSubsystem(model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);				// FUN_004b3eb4
	else if (Streq(type_name, "TorsoClassID"))
		result = Torso::CreateStreamedSubsystem(model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);					// FUN_004b6fec
	else if (Streq(type_name, "EmitterClassID"))
		result = Emitter::CreateStreamedSubsystem(pass, model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);			// FUN_004bb478
	else if (Streq(type_name, "PPCClassID"))
		result = PPC::CreateStreamedSubsystem(pass, model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);				// FUN_004bb8e0
	else if (Streq(type_name, "ProjectileWeaponClassID"))
		result = ProjectileWeapon::CreateStreamedSubsystem(pass, model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);// FUN_004bc7cc
	else if (Streq(type_name, "MissileLauncherClassID"))
		result = MissileLauncher::CreateStreamedSubsystem(pass, model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);	// FUN_004bd08c
	else if (Streq(type_name, "GaussRifleClassID"))
		result = GaussRifle::CreateStreamedSubsystem(pass, model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);		// FUN_004bdd14
	else if (Streq(type_name, "AmmoBinClassID"))
		result = AmmoBin::CreateStreamedSubsystem(pass, model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, subsystem_index);			// FUN_004bd6f0
	else if (Streq(type_name, "SubsystemMessageManagerClassID"))
		result = SubsystemMessageManager::CreateStreamedSubsystem(pass, model_file, model_name, subsystem_name, out_blob, subsystem_file, directories);			// FUN_0049be10
	else if (Streq(type_name, "MechTechClassID"))
		result = MechTech::CreateStreamedSubsystem(model_file, model_name, subsystem_name, out_blob, subsystem_file, directories, pass);							// FUN_004ad48c
	else
	{
		// Unknown component type -> log "<model>:<subsystem> has an unknown
		// component type of <type_name>" and fail the stream.
		DebugStream << model_name;
		DebugStream << ':' << subsystem_name
		            << " has an unknown component type of " << type_name;
		DebugStream.Emit();
		return -1;
	}

	return (result == 0) ? -1 : 1;
}


//###########################################################################
//###########################################################################
//					CreateSubsystemsStream   (offline tool path)
//
// @004a9390
//
// Build the streamed-Subsystems container resource for a model.  Open the
// model's "gamedata"/"Subsystems" sub-file, iterate every subsystem entry,
// resolve each entry's "Type" (and optional "Include" file), and call
// CreateSubsystemStream once per component, packing each emitted resource into
// a contiguous blob (0x770 bytes reserved per subsystem -- the largest
// streamed Subsystem record).  Finally write the packed blob as resource type
// 0x11 via ResourceFile::WriteResource and return its ResourceID.
//###########################################################################
//###########################################################################
/*static*/ ResourceDescription::ResourceID
	Mech::CreateSubsystemsStream(
		NotationFile				*model_file,		// param_1 (host)
		const char					*model_name,		// param_2
		NotationFile				*model_notation,	// param_3
		const ResourceDirectories	*directories)		// param_4
{
	const char	*subFileName;

	if (!((NoteX *)model_notation)->FindBlock("gamedata", "Subsystems", &subFileName))	// FUN_00404088
	{
		DebugStream << model_name << " is missing  sub file specification.";
		return (ResourceDescription::ResourceID)-1;
	}

	const char	*subPath  = ((DirsX2 *)directories)->ResolveInclude(model_name, subFileName);	// FUN_004064fc
	NotationFile *subFile  = OpenNote(Alloc(0x20), subPath, 1);						// FUN_00403e84
	NotationNode *root     = RootNote(subFile);										// FUN_0040485c

	// Count subsystem entries.
	int	count = 0;
	for (NotationNode *n = root->firstChild; n; n = n->next)
		++count;

	if (count == 0)
	{
		DebugStream << model_name << " has no subsystems.";
		root->Release(3);
		FreePath(subPath);
		CloseNote(subFile, 3);
		return (ResourceDescription::ResourceID)-1;
	}

	// Reserve 0x770 bytes per subsystem; first word of the blob is the count.
	int		*blob   = (int *)PoolAlloc(count * 0x770);				// FUN_004022b0
	BlobWriter cursor; cursor.Init(blob, count * 0x770, 0);			// FUN_004030dc
	*cursor.put32(count);											// *local_50++ = count

	for (NotationNode *n = root->firstChild; n; n = n->next)
	{
		const char	*entryName = (const char *)(n + 2);				// puVar5 + 2
		const char	*typeNode;
		if (!((NoteX *)subFile)->FindBlock(entryName, "Type", &typeNode))		// FUN_00404088
		{
			DebugStream << model_name << ':' << entryName << " missing Type.";
			goto fail;
		}

		// Optional "Include" file -- if present, build a synthetic type name
		// "<Type without trailing ClassID>" and stream the include first.
		const char	*includeName;
		NotationFile *includeFile = 0;
		int			 pass = 1;
		if (((NoteX *)subFile)->FindBlock(entryName, "Include", &includeName))	// FUN_00404088
		{
			const char *inc = ((DirsX2 *)directories)->ResolveInclude(model_name, includeName);
			includeFile = OpenNote(Alloc(0x20), inc, 1);

			// Strip the "ClassID" suffix off the type name for the include pass.
			const char	*tail = StrFind(typeNode, "ClassID");		// FUN_004d4c78
			int			 len  = StrLen(typeNode);					// FUN_004d4a78
			char		*base = (char *)PoolAlloc(len + 1);
			int			 keep = (int)(tail - typeNode);
			base = StrCopyN(base, typeNode, keep);					// FUN_004d4b9c
			base[keep] = '\0';

			int rc = CreateSubsystemStream(pass++, typeNode, (int)model_file,
				model_notation, model_name, base,
				cursor.current(), includeFile, directories);	// FUN_004a8cc0
			PoolFree(base);
			if (rc == -1) goto fail;
		}

		// Stream the component itself.
		if (CreateSubsystemStream(pass, typeNode, (int)model_file,
				model_notation, model_name, entryName,
				cursor.current(), subFile, directories) == -1)
			goto fail;

		if (includeFile)
		{
			FreePath(includeName);
			CloseNote(includeFile, 3);
		}
		cursor.advance(cursor.current()[9]);		// += emitted record size
	}

	{
		ResourceDescription::ResourceID *id =
			(ResourceDescription::ResourceID *)FUN_00406db4(	// WriteResource
				model_file, model_name, 0x11, 1, 0,
				blob, cursor.length(), -1);
		PoolFreeBuf(blob);
		root->Release(3);
		FreePath(subPath);
		CloseNote(subFile, 3);
		return *id;
	}

fail:
	PoolFreeBuf(blob);
	root->Release(3);
	FreePath(subPath);
	CloseNote(subFile, 3);
	return (ResourceDescription::ResourceID)-1;
}


//###########################################################################
//###########################################################################
//					SubsystemDefaultData     (offline tool path)
//
// @004a9770
//
// Map a component type-name string to the static SharedData/ClassDerivations
// object the streaming machinery should clone defaults from.  The very first
// match (DAT_0050dc1f) returns &Mech::DefaultData @0050bde4 -- i.e. the model
// itself is the first "component".  The remaining names return each
// subsystem class's static default-data block (addresses confirmed against
// CLASSMAP / the per-module reconstructions).  Returns 0 for an unknown name.
//###########################################################################
//###########################################################################
/*static*/ SharedData *
	Mech::SubsystemDefaultData(const char *type_name)
{
	if (Streq(type_name, ""))						return &Mech::DefaultData;					// 0050bde4 (DAT_0050dc1f == "")
	if (Streq(type_name, "ControlsMapper"))			return &MechControlsMapper::DefaultData;	// 0050ee00
	if (Streq(type_name, "AmmoBinClassID"))			return &AmmoBin::DefaultData;				// 005125bc
	if (Streq(type_name, "HeatSinkClassID"))		return &HeatSink::DefaultData;				// 0050e580
	if (Streq(type_name, "ReservoirClassID"))		return &Reservoir::DefaultData;				// 0050e640
	if (Streq(type_name, "CondenserClassID"))		return &Condenser::DefaultData;				// 0050e4ec
	if (Streq(type_name, "PoweredSubsystemClassID"))return &PoweredSubsystem::DefaultData;		// 0050f4ac
	if (Streq(type_name, "MyomersClassID"))			return &Myomers::DefaultData;				// 0051154c
	if (Streq(type_name, "GeneratorClassID"))		return &Generator::DefaultData;				// 0050fb50
	if (Streq(type_name, "HUDClassID"))				return &HUD::DefaultData;					// 00511078
	if (Streq(type_name, "SensorClassID"))			return &Sensor::DefaultData;				// 0050fa1c
	if (Streq(type_name, "SearchlightClassID"))		return &Searchlight::DefaultData;			// 00511198
	if (Streq(type_name, "ThermalSightClassID"))	return &ThermalSight::DefaultData;			// 00511228
	if (Streq(type_name, "GyroscopeClassID"))		return &Gyroscope::DefaultData;				// 0050fdb0
	if (Streq(type_name, "TorsoClassID"))			return &Torso::DefaultData;					// 00510af8
	if (Streq(type_name, "EmitterClassID"))			return &Emitter::DefaultData;				// 00511d78
	if (Streq(type_name, "PPCClassID"))				return &PPC::DefaultData;					// 005120c4
	if (Streq(type_name, "ProjectileWeaponClassID"))return &ProjectileWeapon::DefaultData;		// 00512198
	if (Streq(type_name, "MissileLauncherClassID"))	return &MissileLauncher::DefaultData;		// 00512478
	if (Streq(type_name, "GaussRifleClassID"))		return &GaussRifle::DefaultData;			// 00512478 (shares MissileLauncher table)
	if (Streq(type_name, "ControlsMapper"))			return &MechControlsMapper::DefaultData;	// 0050ee00 (2nd alias)
	if (Streq(type_name, "MechTech"))				return &MechTech::DefaultData;				// 0050e270
	return 0;
}

//===========================================================================//
// End of recovered mech3.cpp slice.
//===========================================================================//
