//===========================================================================// // File: emitter.cpp // // 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. Behaviour follows the Ghidra // pseudo-C for the module cluster @004ba478-@004bb478 (file=bt/emitter.cpp). // Method and member names follow the surviving sources EMITTER.TCP, PPC.CPP, // GAUSS.CPP/GAUSS.HPP and the reconstructed base mechweap.cpp; the remainder // are inferred from usage and flagged best-effort. Each non-trivial method // cites the originating @ADDR. // // Resolved read-only constants (4 LE bytes from section_dump CODE): // _DAT_004ba818 = 00 00 00 00 = 0.0f (zero / lower clamp) // _DAT_004ba81c = 17 b7 d1 38 = 1.0e-4 (currentLevel "non-zero" epsilon) // _DAT_004ba820 = 00 00 80 3f = 1.0f (full-charge reference / upper clamp) // _DAT_004ba824 = 0a d7 23 3c = 0.01f (snap-to-1.0 tolerance) // _DAT_004ba828 = 00 00 00 00 = 0.0f (below-range clamp result) // _DAT_004ba830 = 00 00 00 00 = 0.0f (above-range clamp result) // _DAT_004ba9a4 = 00 00 00 00 = 0.0f (dischargeTimer expiry threshold) // _DAT_004bac04 = 00 00 80 3f = 1.0f (outputVoltage "fully charged" test) // _DAT_004bac08 = 00 00 00 00 = 0.0f (firing dischargeTimer expiry threshold) // _DAT_004bafb8 = 00 00 00 3f = 0.5f (1/2 in the E = 1/2 k V^2 energy term) // _DAT_004bb3ac = 80 96 18 4b = 1.0e7 (energyTotal unit scale) // _DAT_004bb3b0 = 00 00 80 bf = -1.0f (SeekVoltage "unset" sentinel) // _DAT_004bb3b4 = 00 00 00 3f = 0.5f (energyCoefficient denominator term) // _DAT_004bb3c4 = 00 00 80 3f = 1.0f (voltage-curve constant) // _DAT_004bb428 = 00 00 00 3f = 0.5f // _DAT_004bb798 = 00 00 80 bf = -1.0f (resource "unset" sentinel) // (the firing-physics multipliers _DAT_004bafbc, _DAT_004bb3b8 sit inside // this TU's literal pool and resolve to non-finite singles in the dump; // they are part of larger double-precision operands and are left // symbolic below -- a human should re-derive them from the FPU stream.) // // Helper-function name mapping (engine internals referenced by the decomp): // FUN_004b99a8 MechWeapon base constructor FUN_004b9b9c ~MechWeapon // FUN_004b96ec MechWeapon::ResetToInitialState FUN_004b96d4 MechWeapon::ReadUpdateRecord // FUN_004b964c MechWeapon::TakeDamage FUN_004b9690 MechWeapon::WriteUpdateRecord // FUN_004b9d00 MechWeapon::PrintState FUN_004b9d10 MechWeapon::CreateStreamedSubsystem // FUN_004b9608 MechWeapon::CheckFireEdge FUN_004b9bdc MechWeapon::UpdateTargeting // FUN_004b9948 MechWeapon::GetMuzzlePoint FUN_004b9864 MechWeapon::ComputeAimOrientation // FUN_004b9728 MechWeapon::DrawWeaponPip FUN_004b9cbc MechWeapon::GetTargetPosition // FUN_004b0bd0 PoweredSubsystem simulation step FUN_004b0d50 PoweredSubsystem scalar query // FUN_004ad7d4 HeatableSubsystem "is online/active" FUN_004ac9c8 HeatableSubsystem heat-state query // FUN_00417ab4 follow a SharedData link (voltage source / ammo bin) // FUN_0049fb54 Mech::IsDestroyed (movementMode 2||9) FUN_0041a1a4 IsDerivedFrom(classDerivations) // FUN_0041bbd8 AlarmIndicator::SetLevel(n) (weaponAlarm @0x350, state @0x364) // FUN_0041c350 Subsystem::SendMessage(template, a, b) // FUN_00408440 Vector copy FUN_00409968 Vector set FUN_0040a968 Vector negate/copy // FUN_00408644 Vector subtract FUN_00408bf8 Vector subtract (pt - pt) FUN_0040a138 normalise // FUN_0040aadc Matrix identity FUN_00424da8 resolve segment world point // FUN_0041f216 / FUN_0041f235 segment-iterator construct / destruct // FUN_004dd138 sqrtf FUN_004dce24 expf/logf FUN_004dcd00 fabsf // FUN_00404118 Read(scalar) FUN_00404720 Read(list) FUN_00403b60 list-empty? // FUN_004dbb24 DebugStream<< FUN_004d9c38 DebugStream flush FUN_0040385c Verify() // #include #pragma hdrstop #if !defined(EMITTER_HPP) # include #endif #if !defined(TESTBT_HPP) # include #endif #include // HostManager::GetEntityPointer -- the update-record target resolve (task #51) // E8 (bring-up): the player fire input. The controls mapper that would normally write // the weapon's fireImpulse is bypassed (mech4.cpp), so EmitterSimulation reads this // global directly. Defined in btl4main.cpp; layout must match (see mech4.cpp). struct BTDriveInput { float throttle; float turn; int forced; int fire; int fireForced; float forcedThrottle; int keyFwd; int keyBack; int keyLeft; int keyRight; int allStop; }; extern BTDriveInput gBTDrive; //############################################################################# // Reconstruction support: artifact constants, globals and helper stand-ins. // static const Scalar kDamageScale = 1.0e-7f; // _DAT_004bafbc -- an x87 80-bit constant // (bytes bc427ae5d594bfd6e73f @0x4bafbc) // = 1e-7 EXACTLY: it cancels the ctor's // energyTotal x1e7, so at recommended // charge damagePortion == the AUTHORED // DamageAmount verbatim (task #8; the old // 1.0f "best-effort" was the whole // damage-economy mystery) static const Vector3D DAT_004e0fa4(0.0f, 0.0f, 0.0f); // zero vector static char DAT_00522524[4] = { 0 }; // default colour tag static int PTR_LAB_00511e6c, DAT_00511e70, DAT_00511e74; // beam-keepalive message template static int PTR_LAB_00511e78, DAT_00511e7c, DAT_00511e80; // beam-keepalive message template static Scalar Fabs(Scalar x) { return (x < 0.0f) ? -x : x; } static void Copy(Vector3D &o, const Vector3D &v) { o = v; } static void Set(Vector3D &o, const Vector3D *v) { o = *v; } static void Subtract(Vector3D &o, const Point3D &a, const Point3D &b) { o.x=a.x-b.x; o.y=a.y-b.y; o.z=a.z-b.z; } static void Negate(Point3D &o, const LinearMatrix &) { o = Point3D(0.0f,0.0f,0.0f); } // extract origin (best-effort) static void Normalize(EulerAngles &) {} static void MakeIdentity(LinearMatrix &m) { m.BuildIdentity(); } static Scalar VoltageCurve(Scalar) { return 1.0f; } // _DAT_004bb3c4 - _DAT_004bb3b8*v static void CopyColor(void *, const void *) {} // engine-internal artifacts the decomp called by address (stand-ins) static int FUN_004ad7d4(void *) { return 1; } // HeatableSubsystem active? // FUN_0049fb54 == Mech::IsDestroyed (movementMode 2||9; reconstructed as // Mech::IsDisabled). Was a mislabeled "cockpit/HUD query" no-op stub -- so a // live beam never CUT when its target mech died (@004ba8d0/@004baa20 check // this every discharge frame), and a destroyed mech's own weapons kept firing // (@004baa88). The binary's call sites first verify the object IS a Mech // (IsDerivedFrom vs 0x50bdb4 = Mech::ClassDerivations), reproduced here. static int BTMechDestroyed(Entity *e) { if (e == 0) return 0; if (!e->IsDerivedFrom(*Mech::GetClassDerivations())) return 0; return ((Mech *)e)->IsDisabled() ? 1 : 0; } static Scalar FUN_004b0d50(void *) { return 1.0f; }// PoweredSubsystem dt scale static int FUN_004ac9c8(void *) { return 0; } // heat-state query static void *FUN_00417ab4(void *) { return 0; } // follow SharedData link static int FUN_00421070(void *, void *) { return 0; } // damage source resolve static void *ResolveSource(void *) { return 0; } // named source registry lookup //############################################################################# // Shared Data Support // Derivation Emitter::ClassDerivations( &MechWeapon::ClassDerivations, "Emitter" ); Receiver::MessageHandlerSet Emitter::MessageHandlers; Simulation::AttributeIndexSet Emitter::AttributeIndex; Emitter::SharedData Emitter::DefaultData( &Emitter::ClassDerivations, Emitter::MessageHandlers, MechWeapon::GetAttributeIndex(), // gauge wave: inherit temps/InputVoltage/OutputVoltage/PercentDone (was empty) Emitter::StateCount ); //############################################################################# // State / flag accessors (recovered reads mapped onto the engine members). // int Emitter::GetWeaponState() { return weaponAlarm.GetLevel(); } // this+0x364 int Emitter::GetVoltageState() { return electricalStateAlarm.GetLevel(); } // this+0x278 LWord Emitter::GetFlags() { return simulationFlags; } int Emitter::GetFaultState() { return 0; } void Emitter::SetDirty() { simulationFlags |= 0x1; } void Emitter::ClearDirty() { simulationFlags &= ~0x2; } //############################################################################# // Firing -- the real discharge // // @004bace8 -- vtable slot 18. Only fires when the owning Mech actually has a // target (entity+0x388 != 0). It converts the stored charge into a damage // portion and a self-heat portion (E = 1/2 k V^2, split by damageFraction), // applies the heat to itself, drains the charge, then builds the beam geometry // (muzzle -> target), aims it, registers the hit/pip and marks the weapon dirty // for replication. PPC::FireWeapon (@004bb878) tail-calls straight into this. // void Emitter::FireWeapon() { if (!HasActiveTarget()) // entity+0x388 == 0 { return; } // re-arm the beam-on countdown dischargeTimer = dischargeTime; // 0x440 = 0x43c // E = 1/2 * currentLevel^2 * energyCoefficient Scalar energy = 0.5f * currentLevel * currentLevel * energyCoefficient; // _DAT_004bafb8 // THE AUTHENTIC PER-SHOT DAMAGE (task #8) -- the closed form of the // binary's energy algebra (@004bb120 ctor x @004bace8 fire): // damagePortion = dF x (0.5 V^2 EC) x 1e-7 // = authored DamageAmount x (charge / seekV[rec])^2 // (kDamageScale = _DAT_004bafbc = 1e-7 EXACTLY cancels the ctor's x1e7; // derived here from the STORED damageFraction/energyTotal so the authored // base never degrades, and the bring-up charge cycle -- calibrated to the // degenerate EC=1 electrical model -- stays untouched. At full charge: // the authored value verbatim; madcat ERLLaser=6, bhk1 PPC=12.) { Scalar vRec = seekVoltage[seekVoltageRecommendedIndex]; Scalar chargeRatio = (vRec > 0.0f) ? (currentLevel / vRec) : 1.0f; damagePortion = (damageFraction * energyTotal * kDamageScale) * chargeRatio * chargeRatio; // 0x44c } // Heat stays on the bring-up scale until the heat-calibration audit [T3]: // the authentic heatPortion is heatCostToFire x 1e7 energy units (the // missile path already feeds that scale raw); switching the emitters too // is the heat audit's call, not the damage economy's. heatPortion = energy - damageFraction * energy; // 0x450 // dump the heat portion into our INHERITED thermal accumulator if heatable/online. // E5: pendingHeat (HeatSink @0x1C8) is the field the heat sim absorbs each frame // (heatEnergy += pendingHeat -> temperature); the old Emitter-local heatAccumulator // was a SHADOW at the wrong offset, so firing never reached the heat model. if (FUN_004ad7d4(this)) // HeatableSubsystem active { pendingHeat += heatPortion; // inherited HeatSink::pendingHeat @0x1c8 } // BRING-UP verify (rate-limited): prove the real fire path executes + feeds the heat // sim (heatPortion -> pendingHeat -> flows to the central sink -> mech temp climbs). static int s_fireLog = 0; if ((s_fireLog++ % 20) == 0) DEBUG_STREAM << "[emitter] FIRED #" << s_fireLog << " damage=" << damagePortion << " heat=" << heatPortion << " pendingHeat=" << pendingHeat << "\n" << std::flush; // recompute output voltage now that the charge is about to be spent ComputeOutputVoltage(); // (*vtable+0x44)() currentLevel = 0.0f; // 0x414 -- discharge firingActive = 1; // 0x418 // --- build the beam: muzzle point -> target point --- Point3D muzzlePoint; GetMuzzlePoint(muzzlePoint); // FUN_004b9948 (cached muzzle) Point3D targetPoint; GetTargetPosition(targetPoint); // entity+0x37c (cached target) beamFlag = 1; // 0x46c Vector3D beamVector; beamVector.Subtract(targetPoint, muzzlePoint); // FUN_00408bf8 LinearMatrix aimTransform; // was the phantom beamHitData member ComputeAimOrientation(aimTransform, beamVector); // FUN_004b9864 -> aim matrix Vector3D delta; delta.Subtract(targetPoint, muzzlePoint); // FUN_00408644 Scalar dist = (Scalar)Sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z); // FUN_004dd138 beamScale.z = dist / graphicLength; // 0x434 (== beamScale[2]) = dist / 0x438 if (dist <= effectiveRange) // this+0x328 { // THE AUTHENTIC DAMAGE SUBMISSION (task #8; binary @004bace8:7758-7764): // fill the inherited Damage record -- amount = this shot's // damagePortion (== authored DamageAmount at full charge), // damageForce = target - muzzle (ALSO the gyro's directional // hit-bounce source, retiring its random fallback), impactPoint = // the target point -- then submit through SendDamageMessage // (@004b9728) into the owner's SubsystemMessageManager. damageData.damageAmount = damagePortion; // @0x3AC damageData.damageForce.Subtract(targetPoint, muzzlePoint); // @0x3B0 damageData.impactPoint = targetPoint; // @0x3C8 damageData.burstCount = 1; Entity *submitTarget = (owner != 0) ? *(Entity **)((char *)owner + 0x388) : 0; // mech target slot SendDamageMessage(submitTarget); // FUN_004b9728 } // stash the beam endpoint for replication (world hit point) + the beam's // TARGET (0x474 -- the discharge checks read it every frame: the beam cuts // the moment this mech is destroyed). The owner's target slot is the same // raw 0x388 the rest of the weapon path uses (HasActiveTarget/mech4). beamEndpoint = targetPoint; // 0x460 <- targetPoint targetEntity = (owner != 0) ? *(Entity **)((char *)owner + 0x388) : 0; // 0x474 <- mech target entity targetLocalFlag = 1; // 0x470 SetDirty(); // this+6 |= 1 (needs replication) } // // @004bb878 -- PPC::FireWeapon, reproduced from PPC.CPP for context: it simply // chains to Emitter::FireWeapon(). GaussRifle::FireWeapon overrides instead // (GAUSS.CPP: outputVoltage = 0.0f). // // void PPC::FireWeapon() { Emitter::FireWeapon(); } // //############################################################################# // Simulation -- the installed per-frame "performance" // // @004baa88 -- run the PoweredSubsystem step, sample the fire-edge and the // target range, then advance the Loading -> Loaded -> Firing state machine. // The "state" is carried in the weapon alarm level (this+0x364): // 0 = Firing 2 = Loaded(ready) 3 = Loading 4 = TriggerDuringLoad. // void Emitter::EmitterSimulation(Scalar time_slice) { // Real PoweredSubsystem step (@004b0bd0): runs HeatSink::HeatSinkSimulation // (absorb pendingHeat -> heatEnergy/temperature, then ConductHeat flows it to // the linked central sink) and advances the electrical state machine. This is // what makes weapon self-heat propagate to the mech's heat sinks. PoweredSubsystem::PoweredSubsystemSimulation(time_slice); // AUTHENTIC TRIGGER (task #5): fireImpulse IS the published TriggerState // attribute (id 0x13); the streamed per-mech controls map (type-6 resource, // installed at MakeViewpointEntity) binds each grouped fire BUTTON directly // onto it, and the keyboard/harness pushes press/release edges into the // LBE4 buttonGroup channels (mech4). The old gBT*Trigger bypass + the // per-type keyboard split are retired -- weapons sharing a button now fire // TOGETHER (the pod's authored groups: e.g. madcat Trigger = 4 weapons). Logical fireEdge = CheckFireEdge(); // @004b9608 // task #8 diag: the discharge-chain probe (button->TriggerState->edge->FSM) if (getenv("BT_FIRE_LOG")) { static int s_fp = 0; if ((++s_fp % 97) == 0 || fireEdge) // 97 prime: rotates through all weapons DEBUG_STREAM << "[fire-probe] " << GetName() << " owner=" << (void *)owner << " trigState=" << (float)fireImpulse << " edge=" << (int)fireEdge << " alarm=" << (int)weaponAlarm.GetState() << " level=" << (float)currentLevel << " tgt=" << (void *)((owner != 0) ? *(Entity **)((char *)owner + 0x388) : 0) << std::endl; } targetWithinRange = UpdateTargeting(); // @004b9bdc -> this+0x34c // hard failure: powered-off, faulted, or the OWNING MECH destroyed (@004baa88 // checks IsDestroyed on the owner -- a dead mech's weapons drop everything) if (GetFlags() == 1 || GetFaultState() == 2 || BTMechDestroyed((Entity *)owner)) { ResetFiringState(); // @004ba9a8 currentLevel = 0.0f; // 0x414 ComputeOutputVoltage(); // (*vtable+0x44)() return; } switch (GetWeaponState()) // this+0x364 { case 0: // Firing -- count the beam-on timer down dischargeTimer -= time_slice; // 0x440 if (dischargeTimer <= 0.0f) // _DAT_004bac08 { ResetFiringState(); } else { ContinueDischarge(); // @004baa20 } break; case 2: // Loaded -- ready; fire on the trigger's rising edge if (fireEdge) { if (useConfiguredPip && HasActiveTarget()) // this+0x3E0 (Loaded->Firing gate) && entity+0x388 { weaponAlarm.SetLevel(0); // -> Firing FireWeapon(); // (*vtable+0x48)() ContinueDischarge(); } else { weaponAlarm.SetLevel(4); // TriggerDuringLoad weaponAlarm.SetLevel(2); // back to Loaded } } break; case 3: // Loading -- keep charging if (fireEdge) { weaponAlarm.SetLevel(4); // TriggerDuringLoad blip weaponAlarm.SetLevel(3); } if (GetVoltageState() == 4) // this+0x278 { TrackSeekVoltage(time_slice); // @004ba838 (inert: unresolved src) } // E7 (bring-up): the real charge path is inert in the port -- the voltage-source // plug (this+0x1d0) doesn't resolve, so TrackSeekVoltage can't raise currentLevel // (and it runs whenever vState==4, so the old `else` force-charge never fired). // Force the charge to the seek level UNCONDITIONALLY so the weapon reaches Loaded // and the real fire path (E3/E4/E8 -> FireWeapon) executes. if (currentLevel < seekVoltage[seekVoltageIndex]) currentLevel = seekVoltage[seekVoltageIndex]; // -> ComputeOutputVoltage == 1.0 ComputeOutputVoltage(); // (*vtable+0x44)() if (rechargeLevel == 1.0f) // _DAT_004bac04 -- fully charged (== inherited @0x320) { weaponAlarm.SetLevel(2); // -> Loaded } break; default: break; } } //############################################################################# // Charge / discharge helpers // // // @004ba838 -- integrate the charge from the linked voltage source. Derive a // seek rate from (sourceVoltage - currentLevel)/dt, step currentLevel by // rate/energyCoefficient * dt, and (if heatable) feed rate^2 * dt back as heat. // void Emitter::TrackSeekVoltage(Scalar time_slice) { VoltageSource *src = (VoltageSource *)FUN_00417ab4(this + 0x1d0); Scalar dtScale = FUN_004b0d50(this); // PoweredSubsystem dt scale // No voltage source resolved (the source plug at this+0x1d0 binds via the registry; // not yet linked in the bring-up) or a zero dt scale -> can't charge this frame. // Guard the deref instead of crashing; charging resumes once the source links. if (src == 0 || dtScale == 0.0f) return; seekRate = (src->voltage - currentLevel) / dtScale; // 0x45c = (src+0x1dc - 0x414)/.. currentLevel = (seekRate / energyCoefficient) * time_slice + currentLevel; // 0x414 if (FUN_004ad7d4(this)) // heatable/online { // Raw @004ba838:7544 feeds rate^2*dt into the RESOLVED SOURCE's pendingHeat // (src+0x1c8). The VoltageSource overlay here is a compact shortcut reading // offsets 0/4/8/0xc, NOT the real 0x1dc/0x1c8 -- inert in bring-up (src never // resolves, guarded above). FOLLOW-UP (Emitter re-base tail): type src as the // real HeatSink-derived source and write src->pendingHeat@0x1c8. src->heatLoad += seekRate * seekRate * dtScale * time_slice; } } // // @004ba8d0 -- service an active discharge / beam. When the firing flag is // clear, just mark for replication. Otherwise decrement dischargeTimer; on // expiry (or when the HUD refuses the beam) tear the beam down and re-arm to // the Loaded state, restoring dischargeTimer from dischargeTime. // void Emitter::ServiceDischarge(Scalar time_slice) { if (firingActive == 0) // 0x418 { SetDirty(); // this+0x28 |= 2 return; } weaponAlarm.SetLevel(0); // Firing dischargeTimer -= time_slice; // 0x440 if (dischargeTimer <= 0.0f // _DAT_004ba9a4 || BTMechDestroyed(targetEntity)) // the beam CUTS when its target mech dies { firingActive = 0; weaponAlarm.SetLevel(2); // Loaded beamFlag = 0; // 0x46c dischargeTimer = dischargeTime; // 0x440 = 0x43c SetDirty(); // replicate the beam END: one final Emitter update record turns the // peer's copy off (the 1995 flush wrote idle-flagged subsystems too). ForceUpdate(); return; } // keep the beam alive for another frame -- BEAM REPLICATION (task #51): // the 1995 keepalive (FUN_0041c350, template @0x511e6c) marked the // subsystem dirty + queued the local deferred beam-effect callback; the // local draw is our per-weapon render walk, and the dirty-mark maps to the // engine's updateModel bit -- the next PerformAndWatch serializes THIS // emitter's record (@004ba65c) into the mech's update stream, and the // peer's replicant emitter applies it (@004ba568) so its own walk draws // the beam. ForceUpdate(); (void)PTR_LAB_00511e6c; (void)DAT_00511e70; (void)DAT_00511e74; } // // @004ba9a8 -- reset the transient firing/beam fields and re-arm to Loading. // void Emitter::ResetFiringState() { beamOrientation = EulerAngles(Radian(0.0f), Radian(0.0f), Radian(0.0f)); // 0x41c <- (0,0,0) beamScale = Vector3D(1.0f, 1.0f, 1.0f); // 0x42c <- (1,1,1) targetEntity = 0; // 0x474 firingActive = 0; // 0x418 beamFlag = 0; // 0x46c weaponAlarm.SetLevel(3); // Loading } // // @004baa20 -- keep an active beam alive for another frame. If the targeted // subsystem has gone away, reset; otherwise re-send the beam message. // void Emitter::ContinueDischarge() { if (beamFlag != 0) // 0x46c { if (BTMechDestroyed(targetEntity)) // the beam CUTS when its target mech dies { ResetFiringState(); // @004ba9a8 return; } // re-send the beam keepalive -- the engine's updateModel bit (task #51, // same mapping as ServiceDischarge above; 1995 template @0x511e78). ForceUpdate(); (void)PTR_LAB_00511e78; (void)DAT_00511e7c; (void)DAT_00511e80; } } // // @004ba478 -- advance the seek-voltage index, wrapping modulo (count+1). If // the index wrapped past the recommended one, re-arm via ResetFiringState. // Reached only when the heat-state query is clear and the message has payload. // (best-effort -- exact role of param +0xc unconfirmed.) // int Emitter::AdvanceSeekVoltage(UpdateRecord *message) { if (FUN_004ac9c8(this)) // heat-state set -> skip { return /*non-zero status*/ 1; } if (!FUN_004ad7d4(this) || message->payloadCount <= 0) // message+0xc { return 0; } int prev = seekVoltageIndex; // 0x3f0 int modulus = seekVoltageCount + 1; // 0x3fc + 1 int next = (prev + 1) % modulus; seekVoltageIndex = next; // 0x3f0 if (prev < next) { ResetFiringState(); // @004ba9a8 } return next; } //############################################################################# // Simulation virtual overrides (slots 16, 17) // // // @004ba6e0 -- True if the (supplied or linked) voltage source can satisfy the // current seekVoltage index and is itself in the "ready" state (source+0x210==2). // Logical Emitter::ReadyToDischarge(VoltageSource *source) { if (source == 0) { source = (VoltageSource *)FUN_00417ab4(this + 0x1d0); } if (source == 0) { return False; } if (seekVoltage[seekVoltageIndex] <= source->voltage // 0x400[idx] <= src+0x1dc && source->state == 2) // src+0x210 { return True; } return False; } // // @004ba738 -- recompute outputVoltage from the accumulated charge: // outputVoltage = currentLevel / seekVoltage[index] (0 if charge ~= 0) // snap to 1.0 within 0.01, then clamp to [0,1]. (outputVoltage aliases the // MechWeapon recharge-level slot @0x320.) // void Emitter::ComputeOutputVoltage() { // outputVoltage IS the inherited MechWeapon::rechargeLevel@0x320 (the binary keeps // no separate Emitter slot; the recon's own `outputVoltage` was a duplicate). if (Fabs(currentLevel - 0.0f) > 1.0e-4f) // _DAT_004ba818 / _DAT_004ba81c { rechargeLevel = currentLevel / seekVoltage[seekVoltageIndex]; // 0x320 } else { rechargeLevel = 0.0f; } if (Fabs(rechargeLevel - 1.0f) <= 0.01f) // _DAT_004ba820 / _DAT_004ba824 { rechargeLevel = 1.0f; } if (rechargeLevel < 0.0f) // _DAT_004ba818 { rechargeLevel = 0.0f; // _DAT_004ba828 } else if (rechargeLevel > 1.0f) // _DAT_004ba820 { rechargeLevel = 0.0f; // _DAT_004ba830 (NB: also 0.0f) } } //############################################################################# // Subsystem virtual overrides // // // @004ba568 -- slot 6 (TASK #51 RENAME: was mistranscribed as TakeDamage; the // body is unambiguous RECORD semantics -- it compares rec+0x30 against // EntityID::Null (DAT_00522524), resolves it through the entity index, and // applies alarm/beam fields). Apply an Emitter update record on the // REPLICANT: resolve the beam TARGET id (drop the record when a non-null id is // unknown on this host), chain the MechWeapon read (@004b964c, alarm apply), // then the beam-replication fields. The tail `this+0x10 = this+0x14` is the // 1995 Simulation lastUpdate=lastPerformance resync (the earlier transcription // misread it as rechargeLevel=effectiveRange) -- the engine base read already // maintains lastUpdate. // void Emitter::ReadUpdateRecord(Simulation__UpdateRecord *message) { Emitter__UpdateRecord *rec = (Emitter__UpdateRecord *)message; if (rec->targetID == EntityID::Null) // FUN_00421070 vs DAT_00522524 { targetEntity = 0; // 0x474 } else { targetEntity = (application != 0) ? application->GetHostManager()->GetEntityPointer(rec->targetID) : 0; if (targetEntity == 0) { return; // unknown target -> drop } } MechWeapon::ReadUpdateRecord(message); // FUN_004b964c (alarm apply) firingActive = rec->firingActive; // 0x418 <- rec+0x18 weaponAlarm.SetLevel(firingActive != 0 ? 0 : 2); // 0=Firing / 2=Loaded beamFlag = rec->beamFlag; // 0x46c <- rec+0x28 beamEndpoint = rec->beamEndpoint; // 0x460 <- rec+0x1c targetLocalFlag = rec->targetLocalFlag; // 0x470 <- rec+0x2c ClearDirty(); // this+0x28 &= ~2 (idle bit clear) if (getenv("BT_BEAM_LOG")) { static int s_rd = 0; if ((s_rd++ % 60) == 0) DEBUG_STREAM << "[emit-rd] " << (void*)this << " firing=" << firingActive << " beamFlag=" << beamFlag << "\n" << std::flush; } } // // @004ba65c -- slot 7. Append the Emitter beam-replication fields to the // update record (record type 0x38): firingActive, targetLocalFlag, beam // endpoint, beamFlag and the target's colour tag. // void Emitter::WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model) { MechWeapon::WriteUpdateRecord(message, update_model); // FUN_004b9690 Emitter__UpdateRecord *rec = (Emitter__UpdateRecord *)message; // DISASM CORRECTIONS (task #51): `*param_2 = 0x38` writes the record // LENGTH, not recordID; and rec+0x30 receives the TARGET's EntityID // (FUN_00420ef4 copies from targetEntity+0x184 = the 1995 entityID -- // the old `CopyColor(...targetEntity+0x184)` was both a misread AND a // databinding trap in our compiled Entity layout). rec->recordLength = sizeof(Emitter__UpdateRecord); // *message = 0x38 rec->firingActive = firingActive; // rec+0x18 <- 0x418 rec->beamEndpoint = beamEndpoint; // rec+0x1c <- 0x460 rec->beamFlag = beamFlag; // rec+0x28 <- 0x46c rec->targetLocalFlag = targetLocalFlag; // rec+0x2c <- 0x470 rec->targetID = (targetEntity != 0) ? ((Entity *)targetEntity)->GetEntityID() : EntityID::Null; // rec+0x30 if (getenv("BT_BEAM_LOG")) { static int s_wr = 0; if ((s_wr++ % 60) == 0) DEBUG_STREAM << "[emit-wr] " << (void*)this << " firing=" << firingActive << " beamFlag=" << beamFlag << " subsysID=" << (int)rec->subsystemID << " len=" << (int)rec->recordLength << "\n" << std::flush; } } // // @004ba4d0 -- slot 10. Reset to Loading: chain to MechWeapon, set the alarm // to 3 (Loading), clear the firing/beam transients and re-prime beamScale to // (1,1,1). Matches EMITTER.TCP ResetToInitialState (SetSimulationState(Loaded) // then base reset). // void Emitter::ResetToInitialState() { MechWeapon::ResetToInitialState(); // FUN_004b96ec weaponAlarm.SetLevel(3); // Loading currentLevel = 0.0f; // 0x414 beamFlag = 0; // 0x46c targetLocalFlag = 0; // 0x470 targetEntity = 0; // 0x474 beamScale = Vector3D(1.0f, 1.0f, 1.0f); // 0x42c beamOrientation = EulerAngles(Radian(0.0f), Radian(0.0f), Radian(0.0f)); // 0x41c firingActive = 0; // 0x418 SetDirty(); // this+6 |= 1 } // // @004bb014 -- slot 13. Print the base state, then the Emitter weapon state // from the alarm level (this+0x364): 0 "Firing", 2 "Loaded", 3 "Loading", // 4 "TriggerDuringLoad". // void Emitter::PrintState() { MechWeapon::PrintState(); // FUN_004b9d00 switch (GetWeaponState()) // this+0x364 { case 0: DebugStream << GetName() << " Firing"; break; case 2: DebugStream << GetName() << " Loaded"; break; case 3: DebugStream << GetName() << " Loading"; break; case 4: DebugStream << GetName() << " TriggerDuringLoad"; break; default: break; } } //############################################################################# // Construction / Destruction // // @004bb120 -- chain to the MechWeapon ctor (subsystem kind 5), install the // Emitter vtable PTR_FUN_00512078, choose the firing-message template (the // (flags & 0xC)==4 "external model" variant vs. the default), then seed the // firing state machine to Loading and derive the discharge-energy constants // from the resource's SeekVoltage curve. // Emitter::Emitter( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data ): MechWeapon(owner, subsystem_ID, subsystem_resource, shared_data) { // vtable installed by the compiler (PTR_FUN_00512078) // @004bb120 -- install the per-frame "performance". this[7..9] is the // Simulation::activePerformance member pointer (the same slot every sibling // subsystem fills via SetPerformance), and the ctor picks one of two variants: // default this[7..9] = {0x004baa88,0,0} == EmitterSimulation // external model this[7..9] = {0x004ba8d0,0,0} == ServiceDischarge // (member-pointer constants @00511e84 / @00511e90, resolved from section_dump: // 511e84: 88 aa 4b 00 -> 0x004baa88 ; 511e90: d0 a8 4b 00 -> 0x004ba8d0). // Installing it here is what makes the weapon tick: without it the chained // PoweredSubsystem ctor's PoweredSubsystemSimulation would run instead and the // Loading->Loaded->Firing charge/fire machine below would never execute. if ((GetFlags() & 0xC) == 4) // resource+0x28 -- external model { SetPerformance(&Emitter::ServiceDischarge); // this[7..9] = {0x004ba8d0,0,0} simulationFlags |= 0x2; // this[10] |= 2 (external model) } else { SetPerformance(&Emitter::EmitterSimulation); // this[7..9] = {0x004baa88,0,0} } // (E4 removed) the former `firingArmed = 1` wrote this+0x3e8 == MechWeapon::recoil, // NOT the Loaded->Firing gate: the binary gate reads useConfiguredPip@0x3E0 (set by // the MechWeapon ctor from usesExternalModel), so no separate arming is needed. weaponAlarm.SetLevel(3); // Loading beamEndpoint = Point3D(0.0f, 0.0f, 0.0f); // 0x460 <- (0,0,0) beamFlag = 0; // 0x46c targetEntity = 0; // 0x474 targetLocalFlag = 0; // 0x470 beamScale = Vector3D(1.0f, 1.0f, 1.0f); // 0x42c beamOrientation = EulerAngles(Radian(0.0f), Radian(0.0f), Radian(0.0f)); // 0x41c firingActive = 0; // 0x418 currentLevel = 0.0f; // 0x414 damagePortion = 0.0f; // 0x44c heatPortion = 0.0f; // 0x450 seekReserved = 0.0f; // 0x458 // E2 (bring-up): pre-init the seek-voltage table + energy coefficient so they are // valid even when the voltage-source plug below doesn't resolve (the electrical // block overwrites these with real values when src is live). Nominal seekVoltage=1 // lets the E7 force-charge reach Loaded (currentLevel=seek -> outputVoltage=1.0). energyCoefficient = 1.0f; // 0x454 seekVoltageIndex = 0; // 0x3f0 seekVoltageRecommendedIndex = 0; // 0x3f4 seekVoltageCount = 0; // 0x3fc for (int sv = 0; sv < 5; ++sv) seekVoltage[sv] = 1.0f; // 0x400[] graphicLength = subsystem_resource->graphicLength; // 0x438 <- +0x1bc dischargeTime = subsystem_resource->dischargeTime; // 0x43c <- +0x1c0 dischargeTimer = dischargeTime; // 0x440 // energyTotal = (damageAmount + heatCostToFire) * 1.0e7 energyTotal = (damageData.damageAmount + heatCostToFire) * 1.0e7f; // _DAT_004bb3ac // SeekVoltage curve is only meaningful for an electrically-driven weapon // ((flags & 0xC)==0 && (flags & 0x100)!=0): copy the per-index voltages, // scaled by the linked voltage source's rated voltage, then size the curve. if ((GetFlags() & 0xC) == 0 && (GetFlags() & 0x100) != 0) { VoltageSource *src = (VoltageSource *)FUN_00417ab4(this + 0x74); for (int i = 0; i < 5; ++i) { seekVoltage[i] = 0.0f; // 0x400[i] } for (int i = 0; i < 5; ++i) { if (subsystem_resource->seekVoltage[i] == -1.0f) // _DAT_004bb3b0 sentinel { seekVoltageCount = i - 1; // 0x3fc break; } seekVoltage[i] = subsystem_resource->seekVoltage[i] * src->ratedVoltage; // src+0x1d8 } seekVoltageRecommendedIndex = subsystem_resource->seekVoltageRecommendedIndex; // 0x3f4 <- +0x1d8 seekVoltageIndex = seekVoltageRecommendedIndex; // 0x3f0 seekStepCounter = 0; // 0x3f8 // energyCoefficient = energyTotal / (seekVoltage[rec]^2 * 0.5) Scalar v = seekVoltage[seekVoltageRecommendedIndex]; energyCoefficient = energyTotal / (v * v * /*_DAT_004bb3b4*/ 0.5f); // 0x454 // voltage-curve coefficient -> ramp time (expf of the seek curve). The binary // stores this in the base slot voltageScale@0x310 and never reads it back, so the // recon's own `energyRampTime` member was a phantom -> computed into a local. Scalar curve = /*_DAT_004bb3c4 - _DAT_004bb3b8 * v*/ VoltageCurve(v); // best-effort Scalar energyRampTime = (rechargeRate / -curve) / energyCoefficient; (void)energyRampTime; } // (task #8 note: energyCoefficient deliberately stays on the bring-up // electrical model -- the charge cycle is calibrated to it; the AUTHENTIC // damage algebra is applied as a closed form at fire time instead.) // damageFraction = damageAmount / (damageAmount + heatCostToFire) damageFraction = damageData.damageAmount / (damageData.damageAmount + heatCostToFire); // 0x444 seekStepCounter = 0; // 0x3f0/0x45c bookkeeping Check_Fpu(); } // // @004bb3c8 -- reinstall the Emitter vtable, chain to ~MechWeapon, then (when // the deleting-dtor bit is set) free the object. // Emitter::~Emitter() { Check(this); // vtable reset to PTR_FUN_00512078; FUN_004b9b9c handles the base chain. Check_Fpu(); } //############################################################################# // Test Class Support (EMITTER.TCP) // Logical Emitter::TestClass(Mech &mech) { return True; } Logical Emitter::TestInstance() const { return IsDerivedFrom(ClassDerivations); } //############################################################################# // CreateStreamedSubsystem // // @004bb478 -- parse the Emitter-specific resource fields after the MechWeapon // base parser. Stamps subsystemModelSize = 0x1DC and classID = EmitterClassID // (0xBC8). On the first pass GraphicLength/DischargeTime are primed to the // -1.0 sentinel and the SeekVoltage curve to -1.0; then DischargeTime, // GraphicLength, the SeekVoltage list and its RecommendedIndex are read. // int Emitter::CreateStreamedSubsystem( 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 ) { if ( !MechWeapon::CreateStreamedSubsystem( // FUN_004b9d10 resource_file, model_file, model_name, subsystem_name, subsystem_resource, subsystem_file, directories, passes ) ) { return False; } subsystem_resource->subsystemModelSize = sizeof(*subsystem_resource); // 0x1DC subsystem_resource->classID = (RegisteredClass::ClassID)Mech::WeaponEmitterClassID; // 0xBC8 if (passes == 1) { subsystem_resource->graphicLength = -1.0f; // +0x1bc _DAT_004bb798 subsystem_resource->dischargeTime = -1.0f; // +0x1c0 for (int i = 0; i < 5; ++i) { subsystem_resource->seekVoltage[i] = -1.0f; // +0x1c4.. } subsystem_resource->seekVoltageRecommendedIndex = -1; // +0x1d8 } if ( !model_file->GetEntry(subsystem_name, "DischargeTime", &subsystem_resource->dischargeTime) && subsystem_resource->dischargeTime == -1.0f ) { DebugStream << subsystem_name << " missing DischargeTime!"; return False; } if ( !model_file->GetEntry(subsystem_name, "GraphicLength", &subsystem_resource->graphicLength) && subsystem_resource->graphicLength == -1.0f ) { DebugStream << subsystem_name << " missing GraphicLength!"; return False; } // // SeekVoltage: in the shipped build this is a NotationList of named scalar // entries -- curve samples plus a "SeekVoltageRecommendedIndex" tag. The list // iterator (FUN_00404720) is an engine artifact; here we read the recommended // index and the first curve sample directly via the keyed notation API. // if ( !model_file->GetEntry(subsystem_name, "SeekVoltageRecommendedIndex", &subsystem_resource->seekVoltageRecommendedIndex) && subsystem_resource->seekVoltageRecommendedIndex == -1 ) { DebugStream << subsystem_name << " missing SeekVoltage!"; return False; } model_file->GetEntry(subsystem_name, "SeekVoltage", &subsystem_resource->seekVoltage[0]); return True; } //===========================================================================// // WAVE 3b factory bridge -- the Emitter beam weapon. Used for BOTH 0xBC8 // (Emitter) and 0xBD4 (PPC): PPC's FireWeapon tail-calls Emitter::FireWeapon // and the renderer keys off the streamed classID, so behaviour is identical. //===========================================================================// Subsystem *CreateEmitterSubsystem(Mech *owner, int id, void *seg) { Check(sizeof(Emitter) <= 0x478); return (Subsystem *) new (Memory::Allocate(0x478)) Emitter(owner, id, (Emitter::SubsystemResource *)seg, Emitter::DefaultData); }