//===========================================================================// // File: mechweap.hpp // // Project: BattleTech Brick: Entity Manager // // Contents: MechWeapon -- abstract base for every mounted Mech weapon // //---------------------------------------------------------------------------// // Date Who Modification // // -------- --- ---------------------------------------------------------- // // --/--/95 JM Initial coding. // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved // // PROPRIETARY AND CONFIDENTIAL // //===========================================================================// // // RECONSTRUCTED from the shipped binary (Ghidra pseudo-C, module cluster // @004b95ec-@004b9d10, file=bt/mechweap.cpp; pure-virtual stub @004ba45c, // file=bt/mechweap.hpp). Cross-referenced HEAVILY against the SURVIVING // derived-class sources PPC.HPP/PPC.CPP and GAUSS.HPP/GAUSS.CPP (which derive // from Emitter : MechWeapon) and MISLANCH.HPP (MissileLauncher : ProjectileWeapon // : MechWeapon), plus the MUNGA SUBSYSTM.HPP base interface and the RP // WEAPSYS.h analog. See mechweap.cpp for per-method @ADDR evidence. // // Inheritance chain established from the decomp: // Simulation -> Subsystem -> PoweredSubsystem (powersub.cpp, ctor @004b0f74) // -> MechWeapon (this file, vtable @00511d2c) // -> Emitter / ProjectileWeapon -> PPC / GaussRifle / MissileLauncher // // The MechWeapon ctor (@004b99a8) installs vtable PTR_FUN_00511d2c and chains // to the PoweredSubsystem ctor @004b0f74, confirming the base. PPC and Gauss // FireWeapon() chain to Emitter::FireWeapon(); MechWeapon::FireWeapon() is the // pure-virtual default that asserts "MechWeapon abstract class should..." (@004b95ec) // -- the exact analog of RP GenericGun::Fire()'s "Should not be here!" stub. // // Field offsets in comments are byte offsets into the shipped object // (e.g. "@0x34c" == this word index 0xd3). Names taken from PPC.CPP // (targetWithinRange, damageData) and the resource-parser tag strings // (WeaponRange, DamageAmount, HeatCostToFire, RechargeRate, DamageType, // PipPosition/PipColor/PipExtendedRange, ExplosionModelFile); the remainder // are inferred from usage and flagged best-effort where uncertain. // #if !defined(MECHWEAP_HPP) # define MECHWEAP_HPP #if !defined(POWERSUB_HPP) # include #endif #if !defined(DAMAGE_HPP) # include #endif #if !defined(ALARM_HPP) # include #endif #include // RGBColor #include // LinearMatrix #include // Vector3D #include // Point3D //##################### Forward Class Declarations ####################### class Mech; class Entity; class ResourceFile; class NotationFile; class CockpitHud; //########################################################################### //#################### MechWeapon Model Resource ######################## //########################################################################### // // Extends the PoweredSubsystem resource record. The MechWeapon-specific // fields begin at +0x190 in the parsed record (see CreateStreamedSubsystem // @004b9d10). Total record size is 0x1BC (444 bytes) -- the parser stamps // subsystemModelSize = 0x1BC and classID = MechWeaponClassID (0xBCD). // struct MechWeapon__SubsystemResource: public PoweredSubsystem::SubsystemResource { Scalar rechargeRate; // +0x190 "RechargeRate" Scalar weaponRange; // +0x194 "WeaponRange" ResourceDescription::ResourceID explosionResourceID; // +0x198 "ExplosionModelFile" Scalar damageAmount; // +0x19C "DamageAmount" (-> damageData) Enumeration damageType; // +0x1A0 "DamageType" (Collision..Energy) Scalar heatCostToFire; // +0x1A4 "HeatCostToFire" int pipPosition; // +0x1A8 "PipPosition" RGBColor pipColor; // +0x1AC "PipColor" (3 floats, init -1) int pipExtendedRange; // +0x1B8 "PipExtendedRange" }; // sizeof == 0x1BC //########################################################################### //######################### MechWeapon ############################ //########################################################################### // // Abstract base for any subsystem that can be aimed at a target and fired, // dealing damage and (optionally) spawning an explosion model and a HUD pip. // (vtable @00511d2c, ctor @004b99a8, dtor @004b9b9c.) // class MechWeapon: public PoweredSubsystem { //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Shared Data Support // public: static Derivation ClassDerivations; // task #6: the binary registers TWO handlers ({9,"ConfigureMappables"}, // {10,"ChooseButton"} -- table @0x511860); the old empty static set // meant no pilot input could ever reach the config session. Table + // set both function-local statics in the accessor (init-order-proof). static Receiver::MessageHandlerSet& GetMessageHandlers(); static AttributeIndexSet AttributeIndex; static SharedData DefaultData; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Messaging Support -- the CONFIG-MODE session (task #6) // // Ids PINNED to the binary table @0x511860 (PoweredSubsystem owns 4-8, // the generator-select handlers). The pilot HOLDS a weapon's authored // configure button (a type-6 EventMapping, id 9, under the MFD-quadrant // aux-panel mode masks) to open the session; taps fire buttons to toggle // this weapon's group membership (id 10, temp-mapped by // EnterConfiguration); releases to close. // public: enum { ConfigureMappablesMessageID = 9, // @004b9550 (was mislabeled a Myomers fn) ChooseButtonMessageID = 10 // @004b95b8 }; void ConfigureMappablesMessageHandler( // @004b9550 ReceiverDataMessageOf *message ); void ChooseButtonMessageHandler( // @004b95b8 ReceiverDataMessageOf *message ); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Attribute Support (gauge data-binding wave) // // The energy/ballistic weapon engineering clusters read OutputVoltage (the // charge/ready level -- animates as the weapon recharges after firing) and // PercentDone (the recharge dial), both onto rechargeLevel (1.0 == ready). // GetAttributeIndex() chains to PoweredSubsystem's so InputVoltage + the // HeatSink temps also resolve; Emitter/PPC/ProjectileWeapon/MissileLauncher // point their DefaultData at MechWeapon::GetAttributeIndex() to inherit it // (they carried an EMPTY default-constructed index -> resolved NOTHING). // public: enum { // TriggerState ({0x13, "TriggerState"} @0x511890 -- fireImpulse // @0x31C). task #6 CORRECTION: the binary MechWeapon attribute // table has ELEVEN entries, not one -- 0x12 PercentDone@0x320, // 0x14 DistanceToTarget@0x324, 0x15 TargetWithinRange@0x34C, // 0x16 WeaponRange@0x328(=effectiveRange), 0x17 PipPosition, // 0x18 PipColor, 0x19 PipExtendedRange, 0x1A EstimatedReadyTime // @0x330, 0x1B RearFiring@0x334, 0x1C WeaponState@0x350 (future // gauge-wave feeds). The pinning below still holds: // The streamed per-mech controls maps (type-6 resources) bind the // fire buttons DIRECTLY onto it by this NUMERIC id, so it is // PINNED to the binary value: the binary's subsystem chain reaches // 0x12 before MechWeapon (ConfigureActivePress@MechSubsystem=2, // heat family 3..0xC, powered/aux-screen family 0xD..0x11 -- full // dump in the task #5 attr-table scan), while our reconstruction // publishes fewer base attrs (chain value 0xD). The id gap wastes // a few dense-index slots (Build sizes by max id) -- harmless. // OutputVoltage/PercentDone are port gauge additions // (name-resolved) renumbered after it. // task #16 (attribute parity): the FULL binary table @0x511890, // ids renumbered to binary truth. PercentDone was 0x15 and the // port-extra OutputVoltage alias squatted 0x14 -- both collided // with the binary's DistanceToTarget/TargetWithinRange ids. // (Numeric ids matter to the streamed type-6 maps -- which bind // ONLY TriggerState 0x13, unchanged -- and to binary parity; // gauge configs bind by NAME.) // // THE ID GAP MUST BE PADDED (task #16 crash): our reconstruction's // parent chain ends at PoweredSubsystem::NextAttributeID == 0x0D // (the binary's reaches 0x12 -- we publish fewer base attrs), and // AttributeIndexSet::Build leaves the slots between UNINITIALIZED // -- Find() strcmps EVERY slot, so a gap slot's garbage entryName // is a latent AV (gotcha #11; the old pinned table survived on // heap luck -- the renumber reshuffled allocations and it fired // live in WeaponCluster's "PercentDone" resolve). Pads below fill // ids 0x0D..0x11 (the mech.cpp attrPad idiom). MechWeaponPadFirstAttributeID = 0x0D, // == PoweredSubsystem::NextAttributeID (locked below) PercentDoneAttributeID = 0x12, // @0x320 rechargeLevel TriggerStateAttributeID, // 0x13 @0x31C (PINNED -- the fire-button binding) DistanceToTargetAttributeID, // 0x14 @0x324 rangeToTarget TargetWithinRangeAttributeID, // 0x15 @0x34C WeaponRangeAttributeID, // 0x16 @0x328 -- the HEAT-DEGRADED effectiveRange, // not the configured max (binary truth) PipPositionAttributeID, // 0x17 @0x338 PipColorAttributeID, // 0x18 @0x33C PipExtendedRangeAttributeID, // 0x19 @0x348 EstimatedReadyTimeAttributeID, // 0x1A @0x330 RearFiringAttributeID, // 0x1B @0x334 WeaponStateAttributeID, // 0x1C @0x350 weaponAlarm OutputVoltageAttributeID, // 0x1D PORT-EXTRA alias -> rechargeLevel (the // engineering clusters bind it by name) NextAttributeID }; private: static const IndexEntry AttributePointers[]; public: static AttributeIndexSet& GetAttributeIndex(); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Update record (replication) -- extends the base Simulation record with // the weapon-specific alarm/idle state appended by WriteUpdateRecord. // public: struct MechWeapon__UpdateRecord: public PoweredSubsystem::UpdateRecord { Enumeration alarmState; // weaponAlarm state int weaponIdle; // 1 == idle (alarm level 0) }; typedef MechWeapon__UpdateRecord UpdateRecord; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Firing -- the central abstract operation // // Pure-virtual in spirit: the base body @004b95ec just asserts // "MechWeapon abstract class should [not be fired]". Emitter (PPC, Gauss) // and ProjectileWeapon (MissileLauncher) provide the real implementations; // PPC::FireWeapon / GaussRifle::FireWeapon chain up to Emitter::FireWeapon. // protected: virtual void FireWeapon(); // @004b95ec (abstract stub) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Subsystem virtual overrides (slots on vtable @00511d2c) // // SLOT MAP CORRECTED (task #51): the Read/TakeDamage attributions were // SWAPPED. By symmetry with the Entity hierarchy (Mech slot 6 = // ReadUpdateRecord @004a1232, slot 7 = WriteUpdateRecord @004a0c2c, both // verified) and by body semantics (@004b964c reads record+0x10/+0x14 into // the weapon alarm after chaining @0041bd34 = Simulation::ReadUpdateRecord; // @004b0efc dispatches on *param==4 = Damage::damageType): // slot 6 ReadUpdateRecord(...) @004b964c (base @0041bd34 = Simulation::ReadUpdateRecord) // slot 7 WriteUpdateRecord(...) @004b9690 (base @0041c500 = Subsystem::WriteUpdateRecord) // slot 9 TakeDamage(Damage&) @004b96d4 (chains @004b0efc = PoweredSubsystem::TakeDamage) // slot 10 ResetToInitialState() (PoweredSubsystem base @004b0e6c) // slot 13 PrintState() (PoweredSubsystem base @004b1224) // // NOTE the ENGINE-OVERRIDE signature rule: the virtuals are declared on // Simulation with Simulation::UpdateRecord* (SIMULATE.h:127-131). Declaring // them here with THIS class's shadowing `UpdateRecord` typedef silently // fails to override (the engine base runs instead and none of the weapon // fields ever serialize) -- base-typed params, cast inside. // public: void TakeDamage(Damage &damage); // slot 9, @004b96d4 void ResetToInitialState(); // slot 10, @004b96ec void PrintState(); // slot 13, @004b9d00 void WriteUpdateRecord( // slot 7, @004b9690 Simulation__UpdateRecord *message, int update_model ); void ReadUpdateRecord(Simulation__UpdateRecord *message); // slot 6, @004b964c //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Test Class Support // public: static Logical TestClass(Mech&); Logical TestInstance() const; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Targeting / aiming helpers (used by FireWeapon and the per-frame update) // protected: // E3: read the owning Mech's CURRENT target slot (mech+0x388, the Entity* that // mech4.cpp's targeting step writes) directly -- the old `hasTarget` member was // never set (the Mech per-frame refresh isn't reconstructed) so the fire gate was // permanently false. GetOwningSimulation is non-const, hence the cast. Logical HasActiveTarget() const { char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech) return (o != 0 && *(void **)(o + 0x388) != 0) ? True : False; } // @004b9bdc -- recompute rangeToTarget and the heat-degraded // effectiveRange, then set targetWithinRange. No target => range -1. // Returns the resulting targetWithinRange flag (consumed by Emitter / // ProjectileWeapon simulation). Logical UpdateTargeting(); // @004b9608 -- rising-edge detector on fireImpulse (best-effort name); // returns True the frame the weapon's discharge signal goes positive. Logical CheckFireEdge(); // TODO: confirm semantic (recharge/discharge edge) // @004b9cbc -- copy the owning Mech's current target position. void GetTargetPosition(Point3D &position); // @004b9cdc -- direction from a supplied point to the weapon mount. void GetVectorToWeapon(const Point3D &from, Vector3D &direction); // @004b9948 -- resolve the weapon's muzzle/site world position from the // owning Mech's segment table. void GetMuzzlePoint(Point3D &point); public: // // Beam-render accessors (the visible-beam layer draws each weapon's beam // from its own live state: real muzzle + the authored per-weapon colour). // void MuzzlePoint(Point3D &p) { GetMuzzlePoint(p); } const RGBColor &PipColor() const { return pipColor; } // // Reticle/HUD accessors (the pip registration wiring @part_014.c:5390: // per weapon the 1996 build reads TargetWithinRange / WeaponRange / // PipPosition / PipColor / PipExtendedRange / SimulationState). // Scalar WeaponRange() const { return weaponRange; } int PipPosition() const { return pipPosition; } int PipExtendedRange() const { return pipExtendedRange; } void *WithinRangePtr() { return &targetWithinRange; } void *WeaponAlarmPtr() { return &weaponAlarm; } void *SimulationStatePtr() { return &simulationState; } // attr 1 ("SimulationState" -- damage state; 1 = Destroyed) Scalar *RechargeLevelPtr() { return &rechargeLevel; } // the pip ready source (1.0 == loaded/ready) protected: // @004b9864 -- build an aim orientation (atan2 of a direction vector). void ComputeAimOrientation(LinearMatrix &orientation, const Vector3D &direction); // @004b9728 -- register the targeting "pip" / fire marker with the // owning Mech's cockpit HUD manager. void SendDamageMessage(Entity *target); // @004b9728 (ex "DrawWeaponPip" -- the // weapon-fire damage submission into the // owner's SubsystemMessageManager, task #8) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Construction and Destruction // public: typedef MechWeapon__SubsystemResource SubsystemResource; MechWeapon( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data = DefaultData ); ~MechWeapon(); static int CreateStreamedSubsystem( // @004b9d10 ResourceFile *resource_file, NotationFile *model_file, const char *model_name, const char *subsystem_name, SubsystemResource *subsystem_resource, NotationFile *subsystem_file, const ResourceDirectories *directories, int passes = 1 ); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Local data for the weapon base class. // Offsets are byte offsets into the shipped object (PoweredSubsystem // occupies everything below +0x31c). // protected: // --- recharge / firing state --- Scalar fireImpulse; // @0x31C reset 0 (edge-detected; best-effort name) Scalar rechargeLevel; // @0x320 reset 1.0 (1.0 == fully charged/ready) Scalar rangeToTarget; // @0x324 distance to current target (-1 if none) Scalar effectiveRange; // @0x328 (1 - heatLoad) * weaponRange (heat-degraded) Scalar weaponRange; // @0x32C resource WeaponRange (configured max) int estimatedReadyTime; // @0x330 reset 0 (binary attr 0x1A "EstimatedReadyTime" // names this slot -- resolves the old pipState TODO) Logical usesExternalModel; // @0x334 the "EXT" model-name marker; the binary attr // 0x1B names this slot "RearFiring" (an EXT/rear-mount // weapon fires backward) int pipPosition; // @0x338 resource PipPosition RGBColor pipColor; // @0x33C resource PipColor (3 floats, init -1,-1,-1) int pipExtendedRange; // @0x348 resource PipExtendedRange Logical targetWithinRange; // @0x34C PPC.CPP: "if (targetWithinRange)" // --- damage / display alarm --- GaugeAlarm54 weaponAlarm; // @0x350 0x54 AlarmIndicator (ctor FUN_0041b9ec); level @0x364 -> ends 0x3A4 Scalar previousFireImpulse; // @0x3A4 reset 0 (fire-edge history, see @004b9608) Damage damageData; // @0x3A8 damageType @0x3A8, damageAmount @0x3AC // PPC.CPP: Damage energy_damage(damageData) Scalar heatCostToFire; // @0x3D8 resource HeatCostToFire Scalar rechargeRate; // @0x3DC resource RechargeRate Logical useConfiguredPip; // @0x3E0 = (usesExternalModel == 0) (best-effort) ResourceDescription::ResourceID explosionResourceID; // @0x3E4 resource ExplosionModelFile public: // task #7: the SubsystemMessageManager bundles the FIRING weapon's // explosion per consolidated damage record (ENTITY3.h:143). ResourceDescription::ResourceID GetExplosionResourceID() const { return explosionResourceID; } protected: Scalar recoil; // @0x3E8 reset 0 (Emitter::firingArmed aliases this slot) int segmentPageIndex; // @0x3EC resource segmentIndex (+0x28) -- LAST field, binary ends 0x3F0 // The binary MechWeapon ends at 0x3F0 (segmentPageIndex is the last ctor write, // @004b99a8 param_1[0xfb]). The former tail fields segmentReference/pipSegment/ // hasTarget/targetPoint/muzzlePoint were PHANTOM (no binary storage) and, worse, // collided with the Emitter subclass's own fields at 0x3F0+; removed: // * segmentReference -> the muzzle segment is the inherited base slot this+0xdc // (GetMuzzlePoint resolves it live via the mech4 BTResolveWeaponMuzzle bridge) // * pipSegment -> DrawWeaponPip gates on useConfiguredPip // * hasTarget -> dead (HasActiveTarget reads owner+0x388) // * targetPoint -> GetTargetPosition reads owner+0x37c // * muzzlePoint -> resolved live by GetMuzzlePoint friend struct MechWeaponLayoutCheck; }; struct MechWeaponLayoutCheck { static_assert(offsetof(MechWeapon, weaponAlarm) == 0x350, "MechWeapon::weaponAlarm @0x350 (0x54 alarm)"); static_assert(offsetof(MechWeapon, previousFireImpulse) == 0x3A4, "MechWeapon::previousFireImpulse @0x3A4 (alarm ends here)"); static_assert(offsetof(MechWeapon, recoil) == 0x3E8, "MechWeapon::recoil @0x3E8"); static_assert(offsetof(MechWeapon, segmentPageIndex) == 0x3EC, "MechWeapon::segmentPageIndex @0x3EC (last field)"); static_assert(sizeof(MechWeapon) == 0x3F0, "sizeof(MechWeapon) 0x3F0"); }; #endif