- dpl2d API fully recovered from the binary recorders (@487f34-488630): opcode model (points/lines/polyline/circle/color/width/matrix/push-pop), CallList = INLINE include (state persists to caller), centered coordinate frame (unit = half viewport height). game/reconstructed/dpl2d.cpp rework. - BTReticleRenderable ctor @004cc40c transcribed with the authentic calibration (originX .35, originY .25, scaleY .5, 0..1200m right range ladder, bottom heading tape, FUN_004cd938 tick ladders, lock rings, turn arrows); range caret slides from the live target range fed by the mech4 targeting step (BTSetHudTargetRange). - Weapon pips: the binary gate is IsDerivedFrom(0x511830 = MechWeapon::ClassDerivations) [T1: part_014.c:5386 hard-aborts on missing weapon attrs; part_012 counts + roster ORs capabilityFlags@+0x334] so ALL 7 BLH weapons register (3 lasers + 2 PPCs + 2 MissileLaunchers). Pip A (lit, authored PipColor) on TargetWithinRange, else dark ring B. - AddWeapon @004cdac0 store map corrected to the verified order (part_014.c:4827-4837); both state attrs are literally named "SimulationState" (strings @51d526/51d577) -> weapon simulationState. - Mech roster this[0x1ef] renamed poweredSubsystems -> weaponRoster (0x511830 is MechWeapon, not PoweredSubsystem=0x50f4bc); derivation-tag table added to context/decomp-reference.md. - Draw hook BTDrawReticle after the 3D scene, cockpit view only. Binary Execute @004cdcf0 is an un-exported gap -> Draw dynamics [T3], tracked in context/open-questions.md with the blx_cop canopy + PNAME pip meshes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2064 lines
87 KiB
C++
2064 lines
87 KiB
C++
//===========================================================================//
|
|
// File: mech.cpp //
|
|
// Project: BattleTech Brick: Entity Manager //
|
|
// Contents: Mech -- BattleMech entity construction, subsystem assembly, and //
|
|
// the per-frame replication update. (mech.cpp slice 1 of 4.) //
|
|
//---------------------------------------------------------------------------//
|
|
// Date Who Modification //
|
|
// -------- --- ---------------------------------------------------------- //
|
|
// --/--/95 ?? Initial coding. //
|
|
//---------------------------------------------------------------------------//
|
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
|
|
// PROPRIETARY AND CONFIDENTIAL //
|
|
//===========================================================================//
|
|
//
|
|
// RECONSTRUCTED from the shipped binary BTL4OPT.EXE. Behaviour follows the
|
|
// Ghidra pseudo-C for the cluster 0x004a1232 .. 0x004a5027 (the lone function
|
|
// the disassembler tagged "bt/mech.cpp" is the 5690-byte ctor @004a1674; the
|
|
// surrounding contiguous functions up to mech2.cpp @004a5028 form this slice).
|
|
// The string "d:\tesla\bt\bt\MECH.CPP" embedded at @004a1674+0x936 confirms
|
|
// the source file.
|
|
//
|
|
// Names follow RP/VTV.cpp (the structural analog -- player vehicle entity on
|
|
// the same JointedMover base) and the surviving BT headers (MECHDMG.HPP,
|
|
// MECHTECH.HPP). Member offsets cite this[word] / byte-offset. Each method
|
|
// cites its originating @ADDR.
|
|
//
|
|
// Hex-float / data constants recovered from the image:
|
|
// _DAT_004a1670 = 10.0f (re-sync speed threshold)
|
|
// _DAT_004a2d38 = 0.6 (double) (heat-capacity fraction)
|
|
// _DAT_004a2d40 = 0.05f (update-position scale)
|
|
// _DAT_004a2d44 = 0.0174533f (degrees -> radians)
|
|
// DAT_005209d0 = FLT_MAX (initial maxSpeed)
|
|
// DAT_0052140c = <runtime time-scale global> (frame divisor)
|
|
//
|
|
// Engine-helper name map (referenced by the decomp):
|
|
// FUN_00424e38 JointedMover base constructor
|
|
// FUN_00425550 JointedMover base destructor
|
|
// FUN_00402298 operator new (Memory::New) FUN_004022d0 delete
|
|
// FUN_004023f4 RefCounted::RefCounted FUN_004024d8 Release
|
|
// FUN_00402460 CString(const char*) FUN_00402a98 CString assign
|
|
// FUN_004022b0 Memory::Allocate
|
|
// FUN_0041a1a4 IsDerivedFrom(Derivation*) FUN_0041a058 Enum::Lookup
|
|
// FUN_00417ab4 SharedData::Resolve()
|
|
// FUN_0041b9ec AlarmIndicator(levels) FUN_0041bbd8 ::SetLevel
|
|
// FUN_0041baa4 ~AlarmIndicator
|
|
// FUN_0043ad4f FilteredScalar::Initialize(n,v) FUN_0043adb5 ~FilteredScalar
|
|
// FUN_00427768 ChainOf<>::ChainOf(owner)
|
|
// FUN_00408440 Vector::Copy FUN_0040a7f4 Quat::Copy FUN_0040a938 Quat::Mul
|
|
// FUN_0040ab44 Matrix::FromQuat FUN_00408e90 CString::Copy
|
|
// FUN_00407064/00406ff8 ResourceFile::Find(segment,id)
|
|
// FUN_004032dc/004030dc MemoryStream(open) FUN_00403310 ~MemoryStream
|
|
// FUN_00404088/00404118 NotationFile::ReadString/ReadFloat
|
|
// FUN_00406db4/00406f3c ResourceFile::WriteResource
|
|
// FUN_004dbb24 DebugStream << FUN_0040385c Verify(msg,file,line)
|
|
//
|
|
|
|
#include <bt.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(MECH_HPP)
|
|
# include <mech.hpp>
|
|
#endif
|
|
#if !defined(MECHSUB_HPP)
|
|
# include <mechsub.hpp>
|
|
#endif
|
|
#if !defined(MECHDMG_HPP)
|
|
# include <mechdmg.hpp>
|
|
#endif
|
|
#if !defined(MECHTECH_HPP)
|
|
# include <mechtech.hpp>
|
|
#endif
|
|
// AUTHENTIC GROUND MODEL ctor half (task #15): complete BoxedSolid type for the
|
|
// collisionTemplate/collisionVolume extent reads + the template bottom lift.
|
|
#include <BOXSOLID.hpp>
|
|
// 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)
|
|
// CreateGyroSubsystem (0xBC4) is reconstructed + ready in gyro.cpp (layout re-based,
|
|
// joint I/O real), but NOT wired -- the gyro ctor/integrator reconstruction is
|
|
// incomplete (garbage output); see the 0xBC4 case below.
|
|
|
|
// 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; }
|
|
template<class...A> inline void LoadLowDetailBody(A&&...) {}
|
|
template<class...A> inline void LoadHighDetailBody(A&&...) {}
|
|
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;
|
|
}
|
|
|
|
// The *_tshd proxy is a FLAT blob shadow (no light source, no projection). A flat
|
|
// quad cannot bend to a slope, so tilting it to the terrain angle only made its
|
|
// uphill edge dig into the hillside (and a nearby cliff wrenched it over). The
|
|
// renderer's depth-bias already draws it ON TOP of the terrain so it doesn't get
|
|
// buried/culled on elevation -- which was the reason the tilt was added. So keep
|
|
// the shadow FLAT (horizontal, riding the depth-bias) and only preserve the yaw
|
|
// channel (jointtshadow handles torso-twist yaw separately). ground_normal is now
|
|
// unused for pitch/roll; kept in the signature for the torso-yaw path + future use.
|
|
(void)ground_normal;
|
|
EulerAngles angles = shadowJointNode->GetEulerAngles(); // keep yaw
|
|
EulerAngles flat(0.0f, angles.yaw, 0.0f);
|
|
shadowJointNode->SetRotation(flat);
|
|
}
|
|
|
|
//---------------------------------------------------------------------------//
|
|
// 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 int IsNetworkCopy(A&&...) { return 0; }
|
|
template<class...A> inline void StreamDamageZone(A&&...) {}
|
|
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;
|
|
}
|
|
|
|
//
|
|
// 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);
|
|
// 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;
|
|
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, attrPad), // 0x16
|
|
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, attrPad), // 0x1d
|
|
ATTRIBUTE_ENTRY(Mech, FootStep, attrPad), // 0x1e
|
|
ATTRIBUTE_ENTRY(Mech, AnimationState, attrPad), // 0x1f
|
|
ATTRIBUTE_ENTRY(Mech, ReplicantAnimationState, attrPad), // 0x20
|
|
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): a REPLICANT never gets the master's MakeReady/
|
|
// CheckLoad validity handshake (the engine's interest layer is a partial
|
|
// implementation), and an INVALID entity defers every message as an event
|
|
// (ENTITY.cpp Receive/Dispatch) -- so its network UPDATES never apply AND the
|
|
// deferred-event churn keeps the peer's CreatingMission queue from ever going
|
|
// quiet. Validate replicants at creation (same fix the spawned test dummy
|
|
// needed); the authentic MakeReady flow is future work.
|
|
if (mech->GetInstance() == Entity::ReplicantInstance)
|
|
{
|
|
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
|
|
|
|
//
|
|
// 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)
|
|
|
|
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
|
|
attrPad = 0.0f; // shared read pad for un-populated gauge attributes
|
|
linearSpeed = 0.0f; // live forward ground speed (LinearSpeed gauge); set per frame
|
|
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;
|
|
Wword(0x1ad) = 0;
|
|
|
|
//
|
|
// Seed the pose: identity body rotation, identity aim quaternions.
|
|
//
|
|
torsoRotation = bodyRotation; // FUN_0040a938(this+0x98,this+0x40)
|
|
Wword(0x4b) = bodyRotation; // 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;
|
|
Wword(0x1df) = 0;
|
|
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;
|
|
Wword(0x12d) = 0;
|
|
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)
|
|
controlsMapper = 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)
|
|
// WAVE 5 DEFERRED: the layout re-base + joint I/O ARE done (gyro
|
|
// constructs, resolves real joints jointlocal/jointeye, WriteMechJoint
|
|
// fires -- CreateGyroSubsystem is ready in gyro.cpp). BUT the gyro's
|
|
// ctor field-init + integrator field-MAPPING are incomplete in the
|
|
// reconstruction (binary ctor @004b3778 puts springConstant@0x1E8,
|
|
// dampingConstant@0x1F4, etc.; the recon mislabels these and leaves the
|
|
// accumulators uninitialised) -> live output is 0xCDCDCDCD/NaN written
|
|
// to the ROOT joint. Keep the stub until the ctor+integrator are
|
|
// faithfully reconstructed from @004b3778. See CLAUDE.md §10d.
|
|
subsystemArray[id] = (Subsystem *)
|
|
new (Memory::Allocate(0x3d0)) Gyro(this, id, seg); // FUN_004b3778 (stub)
|
|
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 MechControlsMapperID: // 0xBD3
|
|
subsystemArray[id] = (Subsystem *)
|
|
new (Memory::Allocate(0x130)) MechControlsMapper(this, id, seg, MechControlsMapper::DefaultData); // FUN_0049bca4
|
|
controlsMapper = (MechControlsMapper *)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 (sinkSourceSubsystem+0x1D8 ==
|
|
// Torso::currentTwist) so the gyro can fold torso yaw into the mech joint.
|
|
// ⚠ DEFERRED (WAVE 5): the recon SubProxy::linkTarget sits at gyro+4 (an engine
|
|
// base field), NOT the real gyro torso-link offset (which the gyro layout has
|
|
// not yet mapped) -- writing here would STOMP the live gyro base. Skipped until
|
|
// the gyro's torso-link field is recovered; harmless meanwhile (the Blackhawk
|
|
// torso is disabled, so there is no twist to couple). See CLAUDE.md §10d.
|
|
//
|
|
// if (sinkSourceSubsystem != 0 && gyroSubsystem != 0)
|
|
// ((SubProxy *)gyroSubsystem)->linkTarget = (char *)sinkSourceSubsystem + 0x1d8;
|
|
|
|
//
|
|
// Build the capability sub-rosters by IsDerivedFrom() class tests.
|
|
//
|
|
for (int id = 2; id < subsystemCount; ++id) // "start" each subsystem
|
|
{
|
|
SubProxy *s = (SubProxy *)subsystemArray[id];
|
|
if (s != 0 && s->IsDerivedFrom(0x50e604)) // FUN_0041a1a4
|
|
{
|
|
s->Start(); // (**(s+0x38))(s)
|
|
}
|
|
}
|
|
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->base + 0xa8; // FUN_00408440(this+0x68,...)
|
|
Wword(0x130) = model->field74;
|
|
Wword(0x148) = model->field78;
|
|
Wword(0x149) = model->field7c;
|
|
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] = +0x44)
|
|
// FLOOR all cycle slew rates (the record field decodes to 1.0 -- see the
|
|
// maxAcceleration note): an unfloored gimpCycleRate made REVERSE accelerate
|
|
// at 1 u/s^2 (12+ seconds to back up -- the user's '10-20s to change
|
|
// directions'). Same floor as the mech4 forward floor (25 u/s^2).
|
|
if (!(forwardCycleRate >= 25.0f && forwardCycleRate < 10000.0f))
|
|
forwardCycleRate = 25.0f;
|
|
gimpCycleRate = forwardCycleRate; // @0x5b0 (this[0x16c])
|
|
groundCycleRate = forwardCycleRate; // @0x5b8 (this[0x16e])
|
|
airborneCycleRate = model->field48; // @0x5bc (this[0x16f])
|
|
if (!(airborneCycleRate >= 25.0f && airborneCycleRate < 10000.0f))
|
|
airborneCycleRate = 25.0f;
|
|
// ⚠ forwardThrottleScale: the record field at +0x4c decodes to 0 for the
|
|
// Blackhawk (same undecoded-ModelResource-layout problem as maxAcceleration
|
|
// == 1.0 above) -- a literal read ZEROES the forward speed demand
|
|
// (speedDemand = topSpeed * throttle * THIS) and the mech can never walk
|
|
// forward. Guard until the record layout is decoded against the raw parser.
|
|
forwardThrottleScale = model->field4c; // @0x5c0 (this[0x170])
|
|
if (!(forwardThrottleScale > 0.01f && forwardThrottleScale < 100.0f))
|
|
forwardThrottleScale = 1.0f;
|
|
Wword(0x12f) = model->field70;
|
|
Wword(0x1e1) = model->field80; Wword(0x1e2) = model->field84;
|
|
Wword(0x1e3) = model->field88; Wword(0x1e4) = model->field8c;
|
|
Wword(0x1e5) = model->field90; Wword(0x1e6) = model->field94;
|
|
|
|
// 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
|
|
Wword(0x1dc) = (int)(model->fieldA0 * DegreesToRadians);
|
|
Wword(0x1db) = model->field9c;
|
|
Wword(0x1da) = model->field98;
|
|
Wword(0x10c) = model->fieldA4;
|
|
// NOTE: the decomp writes word slots this[0x15d]/this[0x15e]. Routed to the
|
|
// scratch bank (unnamed pose-angle fields) -- writing through (float*)(this +
|
|
// 0x15d) would be typed-pointer arithmetic on Mech* (== this + 0x15d*sizeof(Mech)).
|
|
Wword(0x15d) = (int)(model->field60 * DegreesToRadians);
|
|
Wword(0x15e) = (int)(model->field64 * 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;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Choose the LOD / view variant of the body model.
|
|
//
|
|
if (getenv("L4VIEWEXT") != 0) // FUN_004dee74
|
|
{
|
|
Wword(0x15f) = 1;
|
|
LoadLowDetailBody(model); // FUN_004a80d4 [mech2]
|
|
}
|
|
else if ((instanceFlags & 0xc) == 4)
|
|
{
|
|
LoadLowDetailBody(model); // FUN_004a80d4 [mech2]
|
|
}
|
|
else
|
|
{
|
|
LoadHighDetailBody(model); // FUN_004a86c8 [mech2]
|
|
}
|
|
|
|
//
|
|
// 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;
|
|
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] via LoadLocomotionClips (mech3.cpp @004a80d4 -- real). GATED
|
|
// (BT_GAIT_CUTOVER) so the default STEP-1/2 path is untouched while the AdvanceBody
|
|
// Animation state machine is brought up (it derefs ResolveAnimationClip unconditionally
|
|
// -> will fault on any clip this mech's content lacks; observe under the gate first).
|
|
if (BTEnvOn("BT_GAIT_CUTOVER", 1)) // default ON (=0 to disable)
|
|
LoadLocomotionClips(model);
|
|
|
|
Wword(0xe5) = 0;
|
|
Wword(0xdd) = Wword(0xde) = 1;
|
|
Wword(0xdb) = model->field68;
|
|
Wword(0xdc) = model->field6c;
|
|
|
|
//
|
|
// ---- 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)
|
|
|
|
// @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);
|
|
|
|
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 a streamed update packet. The packet's 16-bit type field (+6)
|
|
// selects how much of the Mech's replicated state is overwritten. After the
|
|
// switch, a torso/leg state transition re-syncs the stability alarm.
|
|
//
|
|
// NOTE: the decomp recovers the body via the EBP frame (it is invoked through
|
|
// the vtable as a member fn); param shapes below are reconstructed.
|
|
//
|
|
void
|
|
Mech::ReadUpdateRecord(Simulation::UpdateRecord *message)
|
|
{
|
|
Mech__UpdateRecord *record = (Mech__UpdateRecord *)message;
|
|
|
|
switch (record->recordType) // *(u16*)(msg+6)
|
|
{
|
|
case 0: // full pose snapshot
|
|
{
|
|
Vector3D savedPos = worldPosition; // Wword(0x4e)
|
|
EulerAngles savedRot = netOrientation; // Wword(0xb5)
|
|
JointedMover::ReadUpdateRecord(message); // FUN_0042249c
|
|
worldPosition = savedPos;
|
|
netOrientation = savedRot;
|
|
Wword(0x1df) = 1;
|
|
if (throttleState == 2 || throttleState == 3) // Wword(0x1ca)
|
|
{
|
|
torsoAimCurrent.SetIdentity(); // this+0xa6
|
|
}
|
|
// torso/leg sync when the move state flips to/from "2"
|
|
if (Wword(0x10) != Wword(0xf) && (Wword(0xf) == 2 || Wword(0x10) == 2))
|
|
{
|
|
torsoRotation.Mul(Wword(0x4b));
|
|
torsoAimCurrent.Mul(Wword(0x4b));
|
|
torsoAimCurrent = torsoAimTarget;
|
|
Wword(0x1df) = 0;
|
|
if ((instanceFlags & 0x40) == 0)
|
|
{
|
|
ResetPose(); // (**(this+0x34))(this)
|
|
}
|
|
}
|
|
Wword(0x1ad) = record->sequence; // puVar1[0x1d]
|
|
}
|
|
break;
|
|
|
|
case 2: // alarm-only
|
|
JointedMover::ReadUpdateRecord(message); // FUN_0041bd34
|
|
Wword(0x1ad) = record->field4;
|
|
break;
|
|
|
|
case 3: // score / heat state
|
|
JointedMover::ReadUpdateRecord(message);
|
|
Wword(0x196) = record->field4;
|
|
stabilityAlarm.SetLevel(record->field6);
|
|
if (record->field5 == 0)
|
|
{
|
|
controllableSubsystems.Reset(); // FUN_004283b8
|
|
statusAlarm.SetLevel(record->field5);
|
|
}
|
|
else if (record->field5 == 1)
|
|
{
|
|
statusAlarm.SetLevel(record->field5);
|
|
}
|
|
else
|
|
{
|
|
SetStatusState(record->field5); // FUN_004a800c [mech2]
|
|
}
|
|
Wword(0x1ad) = record->field7;
|
|
break;
|
|
|
|
case 4: // pose + server-time re-sync
|
|
{
|
|
JointedMover::ReadUpdateRecord(message);
|
|
creationTime = Now(); // Wword(0x1de)
|
|
worldPosition = record->position; // this+0x4e <- msg+4
|
|
netOrientation = record->orientation; // this+0xb5 <- msg+7
|
|
Wword(0xb8) = Now();
|
|
Scalar age = (Scalar)(Wword(0xb8) - Wword(5)) / FrameTimeScale;
|
|
if (age < ReSyncSpeedThreshold) // 10.0f
|
|
{
|
|
Wword(0xb8) += (Wword(0xb8) - Wword(5)); // extrapolate
|
|
}
|
|
Wword(0x1ad) = record->fieldA;
|
|
}
|
|
break;
|
|
|
|
case 5: // subsystem alarm (variant A)
|
|
JointedMover::ReadUpdateRecord(message);
|
|
heatAlarm.SetLevel(record->field4); // this+0x114
|
|
Wword(0x129) = record->field5;
|
|
Wword(0x12a) = record->field6;
|
|
Wword(0x12d) = record->field9;
|
|
SetStatusState(0x20); // FUN_004a800c [mech2]
|
|
torsoAimCurrent.SetIdentity();
|
|
Wword(0x1ad) = record->fieldA;
|
|
break;
|
|
|
|
case 6: // subsystem alarm (variant B -- death)
|
|
JointedMover::ReadUpdateRecord(message);
|
|
heatAlarm.SetLevel(record->field4);
|
|
Wword(0x129) = record->field5;
|
|
Wword(0x12a) = record->field6;
|
|
Wword(0x12d) = record->field9;
|
|
statusAlarm.SetLevel(0);
|
|
Wword(0x195) = Wword(0x196) = 1;
|
|
torsoAimCurrent.SetIdentity();
|
|
Wword(0x1ad) = record->fieldA;
|
|
break;
|
|
|
|
case 7: // subsystem alarm (variant C)
|
|
JointedMover::ReadUpdateRecord(message);
|
|
heatAlarm.SetLevel(record->field4);
|
|
Wword(0x129) = record->field5;
|
|
Wword(0x12a) = record->field6;
|
|
Wword(0x12d) = record->field9;
|
|
Wword(0x1ad) = record->fieldA;
|
|
break;
|
|
|
|
case 8: // single scalar
|
|
JointedMover::ReadUpdateRecord(message);
|
|
Wword(0xfd) = record->field4;
|
|
Wword(0x1ad) = record->field5;
|
|
break;
|
|
|
|
default:
|
|
JointedMover::ReadUpdateRecord(message); // FUN_0042249c
|
|
break;
|
|
}
|
|
|
|
//
|
|
// Re-fire the stability alarm when the leg state transitions into "2".
|
|
//
|
|
if (Wword(0xf) != Wword(0x10) && Wword(0xf) == 2)
|
|
{
|
|
StabilityMessage msg; // FUN_00436668
|
|
BroadcastToTeam(msg); // FUN_004364e4
|
|
stabilityAlarm.SetLevel(Wword(0x10)); // FUN_0041bbd8(this+0xb,...)
|
|
}
|
|
}
|
|
|
|
|
|
//###########################################################################
|
|
// SetInstanceFlags -- @004a4c54
|
|
//
|
|
// OR a flag word into the entity instance flags (this+0x18); copies (network
|
|
// replicants) are stripped of all but the low/high reserved bits.
|
|
//
|
|
void
|
|
Mech::SetInstanceFlags(Word flags)
|
|
{
|
|
if (IsNetworkCopy()) // FUN_0049fb54
|
|
{
|
|
flags &= 0xfe03;
|
|
}
|
|
*(Word *)((char *)this + 0x18) |= flags;
|
|
}
|
|
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// 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,
|
|
model->skeletonName) == -1)
|
|
{
|
|
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;
|
|
}
|
|
model->classID = MechClassID; // model[7] = 0xBB9
|
|
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.
|
|
//===========================================================================//
|