//===========================================================================// // File: emitter.hpp // // Project: BattleTech Brick: Entity Manager // // Contents: Emitter -- energy/beam weapon base (PPC, GaussRifle derive) // //---------------------------------------------------------------------------// // 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 // @004ba478-@004bb478, file=bt/emitter.cpp; the abstract-stub @004ba45c is // emitted at the head of this translation unit but belongs to mechweap.hpp). // Cross-referenced HEAVILY against: // * the SURVIVING derived sources PPC.HPP/PPC.CPP and GAUSS.HPP/GAUSS.CPP // (both `: public Emitter`; both chain ctor + FireWeapon to Emitter), and // * the SURVIVING EMITTER.TCP test (names: dischargeVoltage, outputVoltage, // currentLevel, EmitterSimulation, ResetToInitialState), and // * the reconstructed base mechweap.hpp/mechweap.cpp (MechWeapon layout). // See emitter.cpp for per-method @ADDR evidence and the resolved constants. // // Inheritance chain (from CLASSMAP.md + the ctor chain @004bb120 -> @004b99a8): // ... -> PoweredSubsystem -> MechWeapon (vtable @00511d2c, ctor @004b99a8) // -> Emitter (vtable @00512078, ctor @004bb120) // ├─ PPC (vtable @0051214c, ctor @004bb888) // └─ GaussRifle (vtable @00512934) // // The Emitter ctor @004bb120 installs PTR_FUN_00512078 and chains to the // MechWeapon ctor @004b99a8. PPC::FireWeapon (@004bb878) is a one-liner that // tail-calls Emitter::FireWeapon (@004bace8) -- exactly as PPC.CPP shows. // GaussRifle::FireWeapon overrides instead (sets outputVoltage = 0). // // Field offsets in comments are byte offsets into the shipped object. Member // names come from EMITTER.TCP / GAUSS.HPP where available, otherwise inferred // from usage and flagged best-effort. PoweredSubsystem occupies everything // below +0x31c; MechWeapon occupies +0x31c..+0x3ef; Emitter starts at +0x3f0. // #if !defined(EMITTER_HPP) # define EMITTER_HPP #if !defined(MECHWEAP_HPP) # include #endif //##################### Forward Class Declarations ####################### class Mech; class ResourceFile; class NotationFile; //########################################################################### //#################### Emitter Model Resource ####################### //########################################################################### // // Extends the MechWeapon resource record. The Emitter-specific fields begin // at +0x1BC in the parsed record (see CreateStreamedSubsystem @004bb478). // The parser stamps subsystemModelSize = 0x1DC and classID = EmitterClassID // (0xBC8). // struct Emitter__SubsystemResource: public MechWeapon::SubsystemResource { Scalar graphicLength; // +0x1BC "GraphicLength" (beam visual length) Scalar dischargeTime; // +0x1C0 "DischargeTime" (beam-on duration) Scalar seekVoltage[5]; // +0x1C4 "SeekVoltage" (per-index charge curve) int seekVoltageRecommendedIndex;// +0x1D8 "SeekVoltageRecommendedIndex" }; // sizeof == 0x1DC //########################################################################### //######################### Emitter ########################## //########################################################################### // // Base class for charge-and-discharge beam weapons. It accumulates charge // (currentLevel) toward a per-index target (seekVoltage), exposing a // normalised outputVoltage; when fully charged and triggered it discharges, // converting the stored energy into damage + self-heat and projecting a beam // at the owning Mech's current target. (vtable @00512078, ctor @004bb120, // dtor @004bb3c8.) // class Emitter: public MechWeapon { //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Shared Data Support // public: static Derivation ClassDerivations; static Receiver::MessageHandlerSet& GetMessageHandlers(); // inherits MechWeapon's (task #6) // // The AUTHENTIC Emitter attribute table (binary @0x511dd4, 9 records // {id, name, offset|1, 0}, ids 0x1D..0x25 chained after MechWeapon's // 0x1C WeaponState -- dumped issue #11). The Seek* rows feed the // SeekVoltageGraph engineering-page widget (ctor @004c6798 resolves // them by NAME); OutputVoltage binds the RAW charge voltage // currentLevel@0x414 (the graph's live cursor feed) -- NOT the 0..1 // rechargeLevel (the retired MechWeapon port alias). // enum { LaserOnAttributeID = MechWeapon::NextAttributeID, // 0x1D @0x418 firingActive LaserScaleAttributeID, // 0x1E @0x42C beamScale LaserRotationAttributeID, // 0x1F @0x41C beamOrientation CurrentSeekVoltageIndexAttributeID, // 0x20 @0x3F0 RecommendedSeekVoltageIndexAttributeID, // 0x21 @0x3F4 MinSeekVoltageIndexAttributeID, // 0x22 @0x3F8 MaxSeekVoltageIndexAttributeID, // 0x23 @0x3FC SeekVoltageAttributeID, // 0x24 @0x400 (array base) OutputVoltageAttributeID, // 0x25 @0x414 currentLevel (RAW volts) NextAttributeID // 0x26 }; static AttributeIndexSet& GetAttributeIndex(); static SharedData DefaultData; // Update record (replication) -- extends the MechWeapon record with the // Emitter beam-replication fields appended by WriteUpdateRecord (@004ba65c). struct Emitter__UpdateRecord: public MechWeapon::UpdateRecord { int firingActive; // rec+0x18 <- 0x418 Point3D beamEndpoint; // rec+0x1c <- 0x460 int beamFlag; // rec+0x28 <- 0x46c int targetLocalFlag; // rec+0x2c <- 0x470 // DISASM CORRECTION (task #51, @004ba65c/@004ba568): rec+0x30 is the // TARGET's EntityID -- FUN_00420ef4 (an EntityID copy) from // targetEntity+0x184 (the 1995 Entity::entityID) or DAT_00522524 // (EntityID::Null) -- NOT an RGB colour. The reader resolves it // through the entity index and DROPS the record when a non-null id // is unknown on this host. EntityID targetID; // rec+0x30 int payloadCount; // (AdvanceSeekVoltage) }; typedef Emitter__UpdateRecord UpdateRecord; // (task #10: the old compact "VoltageSource" proxy struct is RETIRED -- // the +0x1d0 plug resolves to the real Generator, whose named members // carry the decomp offsets: outputVoltage@0x1dc, ratedVoltage@0x1d8, // state@0x210, pendingHeat@0x1c8.) protected: // State / flag accessors the recovered bodies read (mapped onto the // MechWeapon weaponAlarm + Simulation flags). // (Gitea #12 / gotcha #20: the old SetDirty() { simulationFlags |= 0x1 } // helper is RETIRED -- it conflated TWO different binary writes and its // bit 0 is the engine DelayWatchersFlag (mutes the audio watchers // forever). The binary's `or word [this+0x18],1` sites are the engine // ForceUpdate(); its `or dword [this+0x28],2` sites are ExecuteOnUpdate().) int GetWeaponState(); int GetVoltageState(); LWord GetFlags(); int GetFaultState(); void ClearDirty(); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Simulation Support // // The per-frame step runs through the Simulation "activePerformance" // member-function pointer (see GAUSS.HPP SetPerformance). EmitterSimulation // is the installed performance for this class. // public: typedef void (Emitter::*Performance)(Scalar time_slice); void SetPerformance(Performance performance) { Check(this); activePerformance = (Simulation::Performance)performance; } // @004baa88 -- the installed per-frame performance: drive the // Loading->Loaded->Firing state machine, recharge, and fire on edge. void EmitterSimulation(Scalar time_slice); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Firing // // The real discharge implementation. PPC::FireWeapon chains straight here // (@004bb878); GaussRifle::FireWeapon overrides it. // protected: virtual void FireWeapon(); // vtable slot 18, @004bace8 //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Subsystem / Simulation virtual overrides (vtable @00512078) // // SLOT MAP CORRECTED (task #51): slot 6 is ReadUpdateRecord, not TakeDamage // (@004ba568 resolves an EntityID at rec+0x30 through the entity index and // applies alarm/beam fields -- record semantics; it chains @004b964c, the // MechWeapon slot-6 read). The real Emitter::TakeDamage is slot 9 // @004bafc8 (undecoded -- inherit MechWeapon::TakeDamage until recovered). // slot 6 ReadUpdateRecord @004ba568 (MechWeapon base @004b964c) // slot 7 WriteUpdateRecord @004ba65c (MechWeapon base @004b9690) // slot 9 TakeDamage @004bafc8 (NOT yet reconstructed) // slot 10 ResetToInitialState (MechWeapon base @004b96ec) // slot 13 PrintState (MechWeapon base @004b9d00) // slot 15 SeekVoltageResponse @004bb42c (the SeekVoltageGraph sampler; // Myomers' analog @004b8f94 shares the slot) // slot 16 ReadyToDischarge (Simulation interface) // slot 17 ComputeOutputVoltage (Simulation interface) // slot 18 FireWeapon // public: void ReadUpdateRecord(Simulation__UpdateRecord *message); // slot 6, @004ba568 void ResetToInitialState(); // slot 10, @004ba4d0 void PrintState(); // slot 13, @004bb014 // @004bb42c (vtable slot 15) -- the SeekVoltageGraph's voltage-response // sampler: sqrt((SeekPower(v) - 0) / (2.0e8f - 0)) [FUN_004dd138 = sqrt; // consts from the capstone disasm, issue #11]. Maps a supply voltage to // the graph's 0..1 x-axis (normalised sqrt of the discharge power). Scalar SeekVoltageResponse(Scalar voltage); // slot 15, @004bb42c // // Beam-render accessors (the visible-beam layer draws each weapon's own // beam from this live sim state -- the authentic per-weapon fire look). // int BeamOn() const { return beamFlag; } // 0x46c Scalar DischargeTimer() const { return dischargeTimer; } // forensics // REPLICANT-side beam aging (port, 2026-07-13): replicated discharge // state arrives via update records, but replicant subsystems never // Execute -- a lost/undelivered beam-END record left the peer's beam // ON forever, drawn at its stale fire-time endpoint (the phantom // "4th gold beam", user-reported). Run the master's deterministic // countdown locally: no alarms, no upstream records; a live master // refreshes the state with every real record anyway. void ReplicantServiceBeam(Scalar dt) { if (beamFlag == 0) return; dischargeTimer -= dt; if (dischargeTimer <= 0.0f) { beamFlag = 0; firingActive = 0; dischargeTimer = dischargeTime; } } const Point3D &BeamEndpoint() const { return beamEndpoint; } // 0x460 public: void WriteUpdateRecord( // slot 7, @004ba65c Simulation__UpdateRecord *message, int update_model ); protected: // @004ba6e0 -- True when the supplied (or linked) voltage source can // satisfy the current seekVoltage index and is in the "ready" state (2). Logical ReadyToDischarge(Generator *source); // slot 16 // @004ba738 -- recompute outputVoltage = currentLevel / seekVoltage[idx], // snapped to 1.0 near full and clamped to [0,1]. void ComputeOutputVoltage(); // slot 17 //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Internal charge / discharge helpers (non-virtual; called by the sim) // protected: // @004bb3f4 -- the seek discharge power at a supply voltage: // damageFraction * v^2 * 0.5 [_DAT_004bb428] * energyCoefficient. // (== the E = 1/2 k V^2 discharge energy scaled by the damage split.) Scalar SeekPower(Scalar voltage); // @004bb3f4 // @004ba838 -- track the linked voltage source: derive a seek rate and // integrate currentLevel; dump the I^2R heat back into the source. void TrackSeekVoltage(Scalar time_slice); // @004ba8d0 -- count the beam-on dischargeTimer down; when it expires // (or the firing message is refused) drop back to the Loaded state. void ServiceDischarge(Scalar time_slice); // @004ba9a8 -- reset the transient firing/beam fields and re-arm to the // Loading state (alarm level 3). void ResetFiringState(); // @004baa20 -- keep an active beam alive for another frame (re-send the // beam message, or tear it down if the target subsystem is gone). void ContinueDischarge(); public: // @004ba478 -- "ToggleSeekVoltage": the ENERGY-WEAPON handler table // @0x511DB8 {0xb, "ToggleSeekVoltage"}. Issue #19 (2026-07-21): the // faithful body existed (as AdvanceSeekVoltage) but the id was never // registered, so the ADV-mode weapon seek-gear press was silently // swallowed. Now the registered message handler; cycles // seekVoltageIndex@0x3F0 modulo (maxSeekVoltageIndex@0x3FC + 1) and // re-arms (ResetFiringState) when not wrapping. (The AMMO-weapon // table @0x512210 binds id 0xb to EjectAmmo instead -- ProjectileWeapon // branch, unaffected.) enum { ToggleSeekVoltageMessageID = 0x0b }; void ToggleSeekVoltageMessageHandler(ReceiverDataMessageOf *message); // @004ba478 protected: //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Test Class Support (EMITTER.TCP) // public: static Logical TestClass(Mech&); Logical TestInstance() const; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Construction and Destruction // public: typedef Emitter__SubsystemResource SubsystemResource; Emitter( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data = DefaultData ); ~Emitter(); static int CreateStreamedSubsystem( // @004bb478 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 energy-weapon base class. // Byte offsets into the shipped object; MechWeapon ends at +0x3EF. // protected: // Byte-exact own fields, ctor @004bb120 (param_1[N] == byte N*4), 0x3F0..0x478. // The former "additional firing/beam scratch" block (beamHitPoint/beamImpact/ // beamImpactScalar/beamColor/beamHitData/firingArmed/energyRampTime) + outputVoltage // + beamLengthRatio were phantom/duplicate members that over-sized Emitter to 0x4B0; // removed (see the .cpp for where each really lives / became a local): // * outputVoltage -> inherited MechWeapon::rechargeLevel@0x320 (ComputeOutputVoltage writes it) // * beamHitData -> a LinearMatrix local in FireWeapon (aim transform) // * beamHitPoint/beamImpact/beamImpactScalar -> binary writes into the inherited // Damage damageData (0x3C8/0x3B0/0x3AC); the recon assignments were dead -> deleted // * beamColor -> never read (WriteUpdateRecord builds colour from targetEntity) // * firingArmed -> the Loaded->Firing gate reads inherited viewFireEnable@0x3E0 // * energyRampTime -> binary writes the base slot voltageScale@0x310; recon computes a local // * beamLengthRatio -> is beamScale.z@0x434 int seekVoltageIndex; // @0x3F0 (param_1[0xfc]) current charge-curve index int seekVoltageRecommendedIndex;// @0x3F4 (param_1[0xfd]) recommended index // issue #11 IDENTITY CORRECTION (binary attribute table @0x511dd4): // @0x3F8 is published as "MinSeekVoltageIndex" and @0x3FC as // "MaxSeekVoltageIndex" -- the old names (seekStepCounter, "reset 0" / // seekVoltageCount, "count-1") were best-effort guesses that happened // to carry exactly those values (min is always 0; max = count-1, the // AdvanceSeekVoltage modulus-1). Renamed to the attribute truth -- // the exact analog of Myomers @0x328/@0x32C. int minSeekVoltageIndex; // @0x3F8 (param_1[0xfe]) always 0 int maxSeekVoltageIndex; // @0x3FC (param_1[0xff]) count-1 (index modulus - 1) Scalar seekVoltage[5]; // @0x400 (param_1[0x100..0x104]) per-index target voltages Scalar currentLevel; // @0x414 (param_1[0x105]) accumulated charge int firingActive; // @0x418 (param_1[0x106]) reset 0; FireWeapon sets 1 Quaternion beamOrientation; // @0x41C (param_1[0x107..0x10a], 16B) aim orientation Vector3D beamScale; // @0x42C (param_1[0x10b..0x10d]) beam scale; .z = length ratio Scalar graphicLength; // @0x438 (param_1[0x10e]) resource GraphicLength Scalar dischargeTime; // @0x43C (param_1[0x10f]) resource DischargeTime Scalar dischargeTimer; // @0x440 (param_1[0x110]) beam-on countdown Scalar damageFraction; // @0x444 (param_1[0x111]) damageAmount / (damageAmount + heatCostToFire) Scalar energyTotal; // @0x448 (param_1[0x112]) (damageAmount + heatCostToFire) * 1e7 Scalar damagePortion; // @0x44C (param_1[0x113]) per-shot damage energy Scalar heatPortion; // @0x450 (param_1[0x114]) per-shot self-heat energy Scalar energyCoefficient; // @0x454 (param_1[0x115]) energyTotal / (seekVoltage^2 * k) Scalar seekReserved; // @0x458 (param_1[0x116]) reset 0 Scalar seekRate; // @0x45C (param_1[0x117]) d(level)/dt working value Point3D beamEndpoint; // @0x460 (param_1[0x118..0x11a]) last hit / beam-end point int beamFlag; // @0x46C (param_1[0x11b]) beam-active flag int targetLocalFlag; // @0x470 (param_1[0x11c]) target-relative vs world Entity *targetEntity; // @0x474 (param_1[0x11d]) current beam target -- LAST, ends 0x478 friend struct EmitterLayoutCheck; }; struct EmitterLayoutCheck { static_assert(offsetof(Emitter, seekVoltageIndex) == 0x3F0, "Emitter::seekVoltageIndex @0x3F0 (MechWeapon must end 0x3F0)"); // issue #11: the four attribute-published Seek* slots (table @0x511dd4) static_assert(offsetof(Emitter, minSeekVoltageIndex) == 0x3F8, "Emitter::minSeekVoltageIndex @0x3F8 (attr 0x22)"); static_assert(offsetof(Emitter, maxSeekVoltageIndex) == 0x3FC, "Emitter::maxSeekVoltageIndex @0x3FC (attr 0x23)"); static_assert(offsetof(Emitter, seekVoltage) == 0x400, "Emitter::seekVoltage @0x400 (attr 0x24)"); static_assert(offsetof(Emitter, firingActive) == 0x418, "Emitter::firingActive @0x418 (attr 0x1D LaserOn)"); static_assert(offsetof(Emitter, currentLevel) == 0x414, "Emitter::currentLevel @0x414"); static_assert(offsetof(Emitter, beamOrientation) == 0x41C, "Emitter::beamOrientation @0x41C (Quaternion 16B)"); static_assert(offsetof(Emitter, beamScale) == 0x42C, "Emitter::beamScale @0x42C"); static_assert(offsetof(Emitter, targetEntity) == 0x474, "Emitter::targetEntity @0x474 (last)"); static_assert(sizeof(Emitter) == 0x478, "sizeof(Emitter) 0x478 (== factory alloc)"); }; #endif