Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.
Layout:
engine/ MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
models) + image codec; the minimal rp/ headers the audio HAL needs
game/ reconstructed BT logic + surviving-original BT source + fwd shims
+ WinMain launcher
content/ full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
docs/ format specs + reconstruction ledgers
reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
tools/ MP console emulator + map/resource scanners
One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
752 lines
38 KiB
C++
752 lines
38 KiB
C++
//===========================================================================//
|
|
// 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.
|
|
//===========================================================================//
|