//===========================================================================// // File: emitter.cpp // // Project: BattleTech Brick: Mech weapons // // Contents: Emitter -- the energy-weapon (beam) base // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. // // All Rights reserved worldwide // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // //===========================================================================// #include #pragma hdrstop #if !defined(EMITTER_HPP) # include #endif #if !defined(MECH_HPP) # include #endif #include Derivation Emitter::ClassDerivations( MechWeapon::ClassDerivations, "Emitter" ); // //############################################################################# // Attribute Support. Emitter publishes the beam charge level (ChargeLevel, at // the authentic 0x1D past the MechWeapon table end): the HUD weapon-charge // gauge binds it, and -- load-bearing -- GaussRifle's AttributeIndex chains // THIS index (GAUSS.CPP), and PPC::DefaultData binds it via the inherited // PPC::AttributeIndex. It MUST be a real defined index (a declared-but- // undefined Emitter::AttributeIndex leaves activeAttributeIndex NULL and // faults the first GetAttributePointer on any PPC/Gauss). Chained to the // MechWeapon index so every energy weapon exposes the full weapon table // (TriggerState / PercentDone / ...). //############################################################################# // const Emitter::IndexEntry Emitter::AttributePointers[]= { ATTRIBUTE_ENTRY(Emitter, ChargeLevel, chargeLevel) }; Emitter::AttributeIndexSet Emitter::AttributeIndex( ELEMENTS(Emitter::AttributePointers), Emitter::AttributePointers, MechWeapon::AttributeIndex ); Emitter::SharedData Emitter::DefaultData( Emitter::ClassDerivations, MechWeapon::MessageHandlers, Emitter::AttributeIndex, Subsystem::StateCount ); Emitter::Emitter( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data ): MechWeapon(owner, subsystem_ID, subsystem_resource, shared_data) { Check(owner); Check_Pointer(subsystem_resource); chargeLevel = 0.0f; seekRate = 0.0f; damagePortion = 0.0f; heatPortion = 0.0f; firingActive = 0; graphicLength = subsystem_resource->graphicLength; dischargeTime = subsystem_resource->dischargeTime; dischargeTimer = dischargeTime; // // Bring-up-safe pre-init: the electrical block below overwrites these with // the calibrated values when the voltage source is live. // energyCoefficient = 1.0f; seekVoltageIndex = 0; seekVoltageRecommendedIndex = 0; minSeekVoltageIndex = 0; maxSeekVoltageIndex = 0; { for (int sv = 0; sv < 5; ++sv) { seekVoltage[sv] = 1.0f; } } // // energyTotal = (damage + heat) x 1e7 -- the discharge energy in the // 1e7-native heat units. // energyTotal = (damageData.damageAmount + heatCostToFire) * 1.0e7f; // // The SeekVoltage calibration (binary ctor @004bb120): the authored curve // values are FRACTIONS of the powering generator's rated voltage (-1 // sentinel ends the list); the energy coefficient pins E = 0.5*V^2*EC to // energyTotal exactly at the recommended gear; and voltageScale calibrates // the exponential charge level(t) = Vrated*(1 - exp(-t/(EC*voltageScale))) // to reach the recommended seek voltage in the authored RechargeRate // seconds on a COLD generator (0.0001 == 1/Vrated). Generator heat then // stretches the scale through ChargeTimeScale -- the heat/firepower // feedback. // { Generator *src = (Generator *)ResolveVoltageSource(); if (src != NULL) { int i; for (i = 0; i < 5; ++i) { seekVoltage[i] = 0.0f; } for (i = 0; i < 5; ++i) { if (subsystem_resource->seekVoltage[i] == -1.0f) { maxSeekVoltageIndex = i - 1; break; } seekVoltage[i] = subsystem_resource->seekVoltage[i] * src->RatedVoltageOf(); } seekVoltageRecommendedIndex = subsystem_resource->seekVoltageRecommendedIndex; seekVoltageIndex = seekVoltageRecommendedIndex; minSeekVoltageIndex = 0; Scalar v = seekVoltage[seekVoltageRecommendedIndex]; energyCoefficient = energyTotal / (v * v * 0.5f); voltageScale = (rechargeRate / -(Scalar)log((double)(1.0f - 0.0001f * v))) / energyCoefficient; if (getenv("BT_POWER_LOG")) { DEBUG_STREAM << "[charge] '" << GetName() << "' seekV={" << seekVoltage[0] << "," << seekVoltage[1] << "," << seekVoltage[2] << "," << seekVoltage[3] << "," << seekVoltage[4] << "} rec=" << seekVoltageRecommendedIndex << " max=" << maxSeekVoltageIndex << " EC=" << energyCoefficient << " vScale=" << voltageScale << " discharge=" << dischargeTime << " E=" << energyTotal << endl << flush; } } } // // damageFraction = the damage share of the discharge energy. // damageFraction = damageData.damageAmount / (damageData.damageAmount + heatCostToFire); // // Install the beam-weapon fire state machine; spawn LOADING (the charge // integrates up from zero). (A replicant copy is driven by console // updates / ServiceDischarge instead -- that path joins the network wave.) // weaponAlarm.SetLevel(LoadingState); if (owner->GetInstance() != Entity::ReplicantInstance) { SetPerformance(&Emitter::EmitterSimulation); } Check_Fpu(); } // //############################################################################# // TrackSeekVoltage -- integrate the charge from the powering generator // (binary @004ba838): derive the seek rate from the voltage gap over the // (heat-stretched) charge time-scale, step the level, and feed the charging // I^2R loss back into the GENERATOR's heat -- recharging weapons is what // heats generators, which conduct to their condensers and slow further // charging through ChargeTimeScale. The heat/firepower feedback loop. //############################################################################# // void Emitter::TrackSeekVoltage(Scalar time_slice) { Check(this); Generator *src = (Generator *)ResolveVoltageSource(); if (src == NULL) { return; } Scalar dtScale = ChargeTimeScale(); seekRate = (src->MeasuredVoltage() - chargeLevel) / dtScale; chargeLevel = (seekRate / energyCoefficient) * time_slice + chargeLevel; if (HeatModelActive()) { src->AddPendingHeat(seekRate * seekRate * dtScale * time_slice); } } // //############################################################################# // ComputeOutputVoltage -- the Emitter override (binary @004ba738): the // recharge dial = the charge level as a fraction of the selected seek // voltage, snapped to 1.0 within 0.01 and clamped to [0,1] (the byte-verified // over-1 clamp zeroes the dial). //############################################################################# // void Emitter::ComputeOutputVoltage() { Check(this); Scalar magnitude = (chargeLevel < 0.0f) ? -chargeLevel : chargeLevel; if (magnitude > 1.0e-4f) { rechargeLevel = chargeLevel / seekVoltage[seekVoltageIndex]; } else { rechargeLevel = 0.0f; } Scalar snap = rechargeLevel - 1.0f; if (snap < 0.0f) { snap = -snap; } if (snap <= 0.01f) { rechargeLevel = 1.0f; } if (rechargeLevel < 0.0f) { rechargeLevel = 0.0f; } else if (rechargeLevel > 1.0f) { rechargeLevel = 0.0f; } } Emitter::~Emitter() { } Logical Emitter::TestClass(Mech &) { return True; } Logical Emitter::TestInstance() const { return IsDerivedFrom(ClassDerivations); } // //############################################################################# // FireWeapon The energy-beam discharge (PARTIAL -- binary @004bace8). The // authentic body: re-arm the beam-on countdown, compute the per-shot damage / // heat from the charge energy (0.5*V^2*EC closed forms), dump the heat into // the inherited thermal accumulator, spend the charge, build the beam // (muzzle -> target) and submit the Damage record at the owner's target. // The energy/heat algebra needs the electrical charge model (TrackSeekVoltage // / seekVoltage / generator -- the powersub wave), and the beam/damage need // the targeting slot + renderer. This partial performs the DISCHARGE // bookkeeping -- countdown re-arm, charge spent, recoil loaded so the recharge // dial animates -- so the fire state machine runs end-to-end. //############################################################################# // void Emitter::FireWeapon() { Check(this); // // Re-arm the beam-on countdown, then THE AUTHENTIC ENERGY ALGEBRA (binary // @004bace8): the shot's damage and heat are the two shares of the stored // charge energy, scaled by the SQUARE of the charge ratio (the level as a // fraction of the recommended seek voltage) -- an under-charged or // down-geared shot delivers proportionally less of both. At full charge on // the recommended gear: damage = the authored DamageAmount verbatim, heat = // heatCostToFire x 1e7 (the 1e7-native chain). // dischargeTimer = dischargeTime; Scalar vRec = seekVoltage[seekVoltageRecommendedIndex]; Scalar chargeRatio = (vRec > 0.0f) ? (chargeLevel / vRec) : 1.0f; damagePortion = (damageFraction * energyTotal * 1.0e-7f) * chargeRatio * chargeRatio; heatPortion = (1.0f - damageFraction) * energyTotal * chargeRatio * chargeRatio; // // The damage submission (binary @004bace8 tail): the shot's portion rides // the inherited Damage record to the owner's target when it is inside the // weapon's effective range. (The beam build / impact point join the // render wave.) // damageData.damageAmount = damagePortion; damageData.burstCount = 1; if (targetWithinRange && owner != NULL && owner->GetTargetEntity() != NULL) { SendDamage(owner->GetTargetEntity(), damageData); } if (HeatModelActive()) { AddPendingHeat(heatPortion); } // // Spend the charge. // ComputeOutputVoltage(); chargeLevel = 0.0f; firingActive = 1; if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[fire] '" << GetName() << "' FIRED (dmg=" << damagePortion << " heat=" << heatPortion << " ratio=" << chargeRatio << " T=" << CurrentTemperatureOf() << ")" << endl << flush; } } // //############################################################################# // ResetFiringState (binary @004ba9a8) -- the beam has finished: drop back to // Loading so the recharge cycle begins. //############################################################################# // void Emitter::ResetFiringState() { Check(this); firingActive = 0; weaponAlarm.SetLevel(LoadingState); } // //############################################################################# // EmitterSimulation The beam-weapon per-frame fire state machine (binary // @004baa88). The weapon state is carried in the weapon alarm level: // 0 = Firing, 2 = Loaded (ready), 3 = Loading, 4 = the trigger-during-load // blip. PARTIAL: the leading PoweredSubsystem electrical step and the // destroyed / heat-failure / dead-mech hard gates are deferred with the // power / heat / damage waves; the Loading charge uses the authored // RechargeRate seconds directly (recoil decay -> ComputeOutputVoltage dial) // instead of the TrackSeekVoltage generator integration; the Loaded->Firing // gate honors viewFireEnable (the look-view arm) -- the HasActiveTarget gate // joins it with the targeting wave. // // DEV hook BT_FORCE_FIRE=1: pulses the trigger whenever the weapon is Loaded // (press while Loaded, release otherwise), so every armed weapon auto-fires at // its authored recharge cadence -- the headless fire-cycle verification. //############################################################################# // void Emitter::EmitterSimulation(Scalar time_slice) { Check(this); // // The PoweredSubsystem step first (binary @004baa88 head): the HeatSink // thermal absorb/conduct + the electrical state machine. // PoweredSubsystem::PoweredSubsystemSimulation(time_slice); // // Hard failure (@4baab9): weapon DESTROYED or its own sink at FailureHeat // -> drop the beam state and the charge, and hold there. Unlike the // ballistic roach-motel this recovers by itself: once conduction cools the // sink below the failure threshold the gate stops firing and the weapon // resumes from Loading. (The owning-mech-disabled half joins with the // damage wave.) // if (GetSimulationState() == 1 || GetHeatState() == FailureHeat) { if (getenv("BT_MECH_LOG") && GetWeaponState() != LoadingState) { DEBUG_STREAM << "[fire] '" << GetName() << "' THERMAL SHUTDOWN (T=" << CurrentTemperatureOf() << ")" << endl << flush; } ResetFiringState(); chargeLevel = 0.0f; ComputeOutputVoltage(); Check_Fpu(); return; } { static int forceFire = -1; if (forceFire < 0) { forceFire = (getenv("BT_FORCE_FIRE") != NULL) ? 1 : 0; } if (forceFire) { fireImpulse = (GetWeaponState() == LoadedState) ? 1.0f : 0.0f; } } Logical fireEdge = CheckFireEdge(); targetWithinRange = UpdateTargeting(); switch (GetWeaponState()) { case FiringState: // // Count the beam-on timer down; when it expires, drop to Loading. // dischargeTimer -= time_slice; if (dischargeTimer <= 0.0f) { ResetFiringState(); } break; case LoadedState: if (fireEdge) { // // The authentic Loaded->Firing gate: armed in the current look // view AND a target under the reticle -- a denied shot is the // one-frame blip, no discharge. // if (viewFireEnable && HasActiveTarget()) { weaponAlarm.SetLevel(FiringState); FireWeapon(); } else { weaponAlarm.SetLevel(TriggerDuringLoadState); weaponAlarm.SetLevel(LoadedState); } } break; case LoadingState: if (fireEdge) { weaponAlarm.SetLevel(TriggerDuringLoadState); weaponAlarm.SetLevel(LoadingState); } // // THE CHARGE INTEGRATION, sub-stepped at the pod's locked 60 fps: the // binary's Loading tick assumes fixed frames -- the Loaded transition // fires while the dial crosses the +-0.01 snap window around the // selected seek voltage, a window a variable-dt spike can jump clean // over (the level then overshoots, the over-1 clamp zeroes the dial, // and the weapon bricks in Loading -- the BT411 weapon-brick fix). // Charging gates on the electrical Ready state. // { const Scalar kPodFrame = 1.0f / 60.0f; Scalar remaining = time_slice; int guard = 600; while (remaining > 0.0f && guard-- > 0 && GetWeaponState() == LoadingState) { Scalar slice = (remaining < kPodFrame) ? remaining : kPodFrame; remaining -= slice; if (GetVoltageState() == Ready) { TrackSeekVoltage(slice); } ComputeOutputVoltage(); if (rechargeLevel == 1.0f) { weaponAlarm.SetLevel(LoadedState); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[fire] '" << GetName() << "' LOADED (level=" << chargeLevel << " T=" << CurrentTemperatureOf() << ")" << endl << flush; } break; } // // Overcharge rescue (a DOCUMENTED DIVERGENCE, BT411 issue #21: // the binary deadlocks here -- a mid-charge gear change can // land the level above the new gear's snap window, the over-1 // clamp zeroes the dial forever, and the weapon bricks. The // arcade's own math says full == the gear's seek voltage, so // an overcharged weapon IS loaded). // if (chargeLevel > seekVoltage[seekVoltageIndex] && seekVoltage[seekVoltageIndex] > 0.0f) { rechargeLevel = 1.0f; weaponAlarm.SetLevel(LoadedState); break; } } } break; default: break; } Check_Fpu(); } // //############################################################################# // CreateStreamedSubsystem Model-load-time construction. Not yet reconstructed. //############################################################################# // int Emitter::CreateStreamedSubsystem( ResourceFile *, NotationFile *, const char *, const char *, SubsystemResource *, NotationFile *, const ResourceDirectories *, int ) { Fail("Emitter::CreateStreamedSubsystem -- emitter.cpp not yet reconstructed"); return 0; }