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