//===========================================================================// // File: ammobin.cpp // // Project: BattleTech Brick: Mech weapons // // Contents: AmmoBin -- ammunition store with cook-off heat // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. // // All Rights reserved worldwide // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // //===========================================================================// #include #pragma hdrstop #if !defined(AMMOBIN_HPP) # include #endif #if !defined(MECH_HPP) # include #endif #if !defined(MECHWEAP_HPP) # include #endif #if !defined(RANDOM_HPP) # include #endif Derivation AmmoBin::ClassDerivations( HeatWatcher::ClassDerivations, "AmmoBin" ); const AmmoBin::IndexEntry AmmoBin::AttributePointers[]= { ATTRIBUTE_ENTRY(AmmoBin, AmmoCount, ammoCount), ATTRIBUTE_ENTRY(AmmoBin, TimeToReady, feedTimer), ATTRIBUTE_ENTRY(AmmoBin, AmmoClassID, ammoClassID), ATTRIBUTE_ENTRY(AmmoBin, AmmoState, ammoAlarm), ATTRIBUTE_ENTRY(AmmoBin, FireCountdownStarted, fireCountdownStarted) }; AmmoBin::AttributeIndexSet AmmoBin::AttributeIndex( ELEMENTS(AmmoBin::AttributePointers), AmmoBin::AttributePointers, MechSubsystem::AttributeIndex ); AmmoBin::SharedData AmmoBin::DefaultData( AmmoBin::ClassDerivations, Subsystem::MessageHandlers, AmmoBin::AttributeIndex, Subsystem::StateCount ); AmmoBin::AmmoBin( Mech *owner, int subsystem_ID, SubsystemResource *resource, SharedData &shared_data ): HeatWatcher(owner, subsystem_ID, resource, shared_data), ammoAlarm(6) { Check(owner); Check_Pointer(resource); ammoCount = resource->ammoCount; initialAmmoCount = resource->ammoCount; feedRate = resource->feedRate; feedTimer = 0.0f; ammoClassID = resource->ammoClassID; ammoModelFile = resource->ammoModelFile; explosionModelFile = resource->explosionModelFile; heatPerRound = resource->heatPerRound; cookOffArmed = 0; cookOffTime = 0; fireCountdownStarted = 0; // was: reserved for (int i = 0; i < 12; ++i) { cookOffState[i] = 0; } // // The per-frame cook-off watch, on the master instance only (the // binary ctor @004bd5c4 installs its performance triple under the // same MasterInstance + DynamicFlag gate every sibling uses). // if ( (owner->simulationFlags & Entity::InstanceMask) == Entity::MasterInstance && (owner->simulationFlags & Entity::DynamicFlag) != 0 ) { SetPerformance(&AmmoBin::AmmoBinSimulation); } Check_Fpu(); } AmmoBin::~AmmoBin() { } Logical AmmoBin::TestClass(Mech &) { return True; } Logical AmmoBin::TestInstance() const { return IsDerivedFrom(ClassDerivations); } // //############################################################################# // DeathReset -- the respawn restock: base heal + refill from the authored // resource count (the launchers' NoAmmo state releases in their own reset). //############################################################################# // void AmmoBin::DeathReset(Logical full_reset) { Check(this); HeatWatcher::DeathReset(full_reset); // // Restock from the CTOR-CACHED authored count -- NEVER through the // `resource` member: it points into the Mech ctor's padded stream copy, // which is delete[]d before the ctor returns (a respawn-time deref // reads freed heap -- caught by the 5.3.24 adversarial review). // ammoCount = initialAmmoCount; } // //############################################################################# // AmmoBinSimulation -- binary @004bd394. The bin watches the GUN IT FEEDS // (the watch link bound in 5.3.132): once that weapon reaches heat FAILURE // the bin latches a randomised countdown, and when the countdown expires // the rounds go off inside the mech. // // A SPENT bin neither arms nor keeps a countdown running -- the binary's // gate is a cell adjacent to the ammo alarm, read as the alarm having // SETTLED on spent (level 2); ours reads the alarm directly, which is the // same condition in our simpler alarm ([T1] noted in AMMOBIN.NOTES.md). //############################################################################# // void AmmoBin::AmmoBinSimulation(Scalar time_slice) { Check(this); // // The mirrored heat watch first -- this is what reads the gun. // WatchSimulation(time_slice); if (GetSimulationState() == 1) { ammoAlarm.SetLevel(2); } Logical spent = (ammoAlarm.GetLevel() == 2); // // ARM: the fed weapon is cooking and this bin still has rounds. // // // BT_FORCE_COOKOFF=1: treat the fed weapon as cooking, so the whole // mechanic can be exercised headlessly without first driving a gun // into real heat failure. // Logical gun_cooking = (heatAlarm.GetLevel() == HeatSink::FailureHeat); if (getenv("BT_FORCE_COOKOFF") != NULL) { gun_cooking = True; } if ( gun_cooking && !spent && fireCountdownStarted == 0 ) { fireCountdownStarted = 1; cookOffArmed = 1; // // The binary stores an absolute deadline (now + a randomised // delay); we hold the same delay as a COUNTDOWN in milliseconds, // which needs no clock source and is frame-rate independent. The // spread is what stops a whole rack going off on one frame. // cookOffTime = 1000 + (int)((Scalar)Random * 4000.0f); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[cookoff] '" << GetName() << "' ARMED -- fed weapon in heat failure, " << ammoCount << " rounds aboard" << endl << flush; } } // // RUN or CANCEL. // if (fireCountdownStarted != 0) { if (spent) { fireCountdownStarted = 0; cookOffArmed = 0; } else if (cookOffTime > 0) { cookOffTime -= (int)(time_slice * 1000.0f); } else { fireCountdownStarted = 0; cookOffArmed = 0; SetSimulationState(1); // the bin is gone CookOff(); } } if (ammoCount < 1) { ammoAlarm.SetLevel(2); } Check_Fpu(); } // //############################################################################# // CookOff -- binary @004bd300 -> @004ac274. The stored rounds go off: // // * the damage is authored PER ROUND and scaled by the rounds remaining, // so a full bin guts the mech and a nearly empty one barely coughs; // * the bin's own private damage zone is pinned destroyed; // * the blast is SHARED -- divided by the number of damage zones, not // applied whole to each (DamageZoneClassID == 78 == the binary's 0x4e // collector filter, computed from our own VDATA enum); // * every victim is announced with the authentic // "ammo explosion damaging " (string @0x50df61). //############################################################################# // void AmmoBin::CookOff() { Check(this); Mech *mech = (Mech *)GetEntity(); if (mech == NULL || mech->damageZoneCount < 1) { ammoCount = 0; ammoAlarm.SetLevel(2); return; } // // The charge: rounds x the per-round damage. // // [T1] THE PER-ROUND FIGURE IS NOT SOURCED YET. The binary reads it // from the bin's streamed EXPLOSION RECORD (+0x1f4, the second field // of the block our `cookOffState[12]` reserves and nothing parses), // then writes the product back into a copy of that record. Until // that record is parsed we stand in the FED WEAPON's authored // per-shot damage -- the right order of magnitude and the physically // sensible reading, but a stand-in; the first pass over the ammo // resource block should replace it. // // (The first attempt used `heatPerRound`, a HEAT figure of about // 3e-9, which made the whole detonation a silent no-op -- caught only // because the soak printed the arithmetic.) // Scalar perRound = 0.0f; { MechWeapon *fed = (MechWeapon *)watchedLink.Resolve(); if (fed != NULL && fed->IsDerivedFrom(MechWeapon::ClassDerivations)) { perRound = fed->DamageAmountOf(); } } Scalar charge = (Scalar)ammoCount * perRound; // // Our own zone goes first, before the blast is shared out. // if (damageZone != NULL) { ((::DamageZone *)damageZone)->damageLevel = 1.0f; } Scalar share = charge / (Scalar)mech->damageZoneCount; if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[cookoff] '" << GetName() << "' DETONATES: " << ammoCount << " rounds x " << perRound << " = " << charge << " shared over " << mech->damageZoneCount << " zones (" << share << " each)" << endl << flush; } for (int z = 0; z < mech->damageZoneCount; ++z) { if (mech->damageZones[z] == NULL) { continue; } Damage blast; blast.damageType = Damage::ExplosiveDamageType; blast.damageAmount = share; blast.damageForce = Vector3D(0.0f, 0.0f, 0.0f); blast.impactPoint = mech->localOrigin.linearPosition; Entity::TakeDamageMessage hit( Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage), mech->GetEntityID(), z, blast ); mech->Dispatch(&hit); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "ammo explosion damaging zone " << z << endl << flush; } } ammoCount = 0; ammoAlarm.SetLevel(2); Check_Fpu(); }