//===========================================================================// // File: projweap.cpp // // Project: BattleTech Brick: Entity Manager // // Contents: ProjectileWeapon -- MechWeapon that spawns physical projectiles // //---------------------------------------------------------------------------// // Date Who Modification // // -------- --- ---------------------------------------------------------- // // 04/13/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 @004bbae0-@004bcff0 (file=bt/projweap.cpp). Method and // member names follow the surviving MISLANCH.HPP / PROJWEAP.TCP, the binary's own // attribute strings (DAT_00512290) and resource tag strings, and the base // mechweap.cpp. Each non-trivial method cites the originating @ADDR. // // CONFIDENCE // confident (full body recovered & translated): // ctor @004bc3fc, dtor @004bc688, deleting-dtor @004bcbcf, // ammoBinLink ctor @004bcbb0, ReadUpdateRecord @004bbae0, // ResetToInitialState @004bbaf8, HandleMessage @004bcabc, // PrintState @004bc6c8, CreateStreamedSubsystem @004bc7cc, // UpdateEject @004bbb50, DrawFiringCharge @004bbc78, // CheckForJam @004bbfcc, ComputeTimeOfFlight @004bc06c. // best-effort (vtable slot proven, function prologue present, BODY NOT // recovered by the decompiler -- bodies below are reconstructed from context // and clearly marked): // FireWeapon @004bc104 (slot 18), ProjectileWeaponSimulation @004bbd04 // (Performance), GetStatusFlags @004bbf88 (slot 12), // UpdateWeaponState @004bbc20 (slot 16). // excluded (belong to sibling/derived classes, NOT ProjectileWeapon): // Emitter family @004bb120/@004bb888/@004ba4d0-@004bb478 (energy weapons), // MissileLauncher @004bcff0/@004bd060/@004bd08c & FireWeapon @004bcc60, // AmmoBin (HeatWatcher-derived) @004bd5c4/@004bd6b0/@004bd6f0 and its // helpers @004bd2c0-@004bd4f4. // // Resolved read-only constants (4 LE bytes from section_dump CODE): // _DAT_004bc100 = 0000803f = 1.0f // _DAT_004bc064 = 85ebd13e = 0.41f (heat->jam-chance coefficient) // _DAT_004bc068 = 0000803f = 1.0f (jam-chance clamp ceiling) // _DAT_004bb3b0 = 000080bf = -1.0f (resource "unset" sentinel) // _DAT_004bb3b4 = 0000003f = 0.5f // DAT_004e0f74 = {0,0,0} (zero vector) // DAT_004e0fd4 = {0,0,0} (zero point) // 0x40400000 = 3.0f (TotalTimeToEject default) // // Helper-function name mapping (engine internals referenced by the decomp): // FUN_004b99a8 MechWeapon base constructor // FUN_004b9b9c ~MechWeapon // FUN_004b964c MechWeapon::TakeDamage (vtable slot 6, inherited) // FUN_004b9690 MechWeapon::WriteUpdateRecord (slot 7, inherited) // FUN_004add6c Subsystem::HandleMessage (slot 8 base) // FUN_004b96d4 MechWeapon::ReadUpdateRecord (slot 9 base) // FUN_004b96ec MechWeapon::ResetToInitialState (slot 10 base) // FUN_004b9d00 MechWeapon::PrintState (slot 13 base) // FUN_004b9948 MechWeapon::GetMuzzlePoint FUN_004b9bdc MechWeapon::UpdateTargeting // FUN_004b9cbc MechWeapon::GetTargetPosition FUN_004b0d50 voltage helper // FUN_004ad7d4 HeatableSubsystem present? FUN_00417ab4 Connection::Resolve // FUN_0041bbd8 AlarmIndicator::SetLevel(n) FUN_0041db7c Damage init // FUN_004179d4 / FUN_004179f8 SharedData::Connection ctor/dtor // FUN_004022d0 operator delete FUN_004215b0 resolve subsystem name->index // FUN_00408440 Vector copy FUN_0040a7f4 Point copy FUN_00408050 uniform random // FUN_004040d8 Read(int/bool) FUN_00404088 Read(string) FUN_00404118 Read(scalar) // FUN_004084fc vectors-close? FUN_00408944 parse vector // FUN_004dbb24 DebugStream << FUN_004d9c38 DebugStream flush // FUN_004b9d10 MechWeapon::CreateStreamedSubsystem // FUN_004bd588 AmmoBin::request-eject/dry FUN_004bf8bc spawn projectile entity // #include #pragma hdrstop #if !defined(PROJWEAP_HPP) # include #endif #if !defined(AMMOBIN_HPP) # include #endif #if !defined(PROJTILE_HPP) # include #endif #if !defined(TESTBT_HPP) # include #endif #include // WAVE 7 Phase B -- the port-side flying-projectile service (defined in mech4.cpp). // SIGNATURE MUST MATCH mech4.cpp EXACTLY (the missile-arc wave added // launch_velocity): a stale extern here mangles to a nonexistent symbol, /FORCE // fills the call with garbage, and the first autocannon shot AVs (user-hit // crash, 2026-07-12 -- gotcha #3). extern void BTPushProjectile(const Point3D &muzzle, void *shooter, void *target, const Point3D &targetPos, Scalar speed, Scalar damage, const Vector3D *launch_velocity = 0, int guided = 1, int weapon_subsys = -1); //############################################################################# // FIRE-VISUAL REPLICATION STATE (task #61) -- the AC twin of MissileLauncher's // BTSalvoState (mislanch.cpp). Port-side table keyed by the weapon pointer // (the 0x1D0 object is byte-locked): the master counts its shots + remembers // the aim; the replicant edge-detects the counter and mirrors ONE visual round // + DAFC muzzle flash so the enemy's cannon is visible on the peer. //############################################################################# namespace { struct BTAcFireState { const void *owner; int fired; // master: shots fired int seen; // replicant: last counter applied (-1 = unsynced) Point3D target; // master: the last shot's aim point }; BTAcFireState gAcFireTable[64]; BTAcFireState &BTAcFireOf(const void *weapon) { int freeSlot = -1; for (int i = 0; i < 64; ++i) { if (gAcFireTable[i].owner == weapon) return gAcFireTable[i]; if (gAcFireTable[i].owner == 0 && freeSlot < 0) freeSlot = i; } BTAcFireState &s = gAcFireTable[(freeSlot >= 0) ? freeSlot : 0]; s.owner = weapon; s.fired = 0; s.seen = -1; s.target = Point3D(0.0f, 0.0f, 0.0f); return s; } } //############################################################################# // Resolved read-only constants + unrecovered-helper stand-ins (see banner). // static const Scalar _DAT_004bc100 = 1.0f; // time-of-flight bias static const Scalar _DAT_004bc064 = 0.41f; // heat -> jam-chance coefficient static const Scalar _DAT_004bc068 = 1.0f; // jam-chance clamp ceiling static const Scalar _DAT_004bc678 = 1.0f; // muzzle-speed term (unresolved) static const Scalar _DAT_004bc67c = 1.0f; // muzzle-speed term (unresolved) // FUN_00408050 -- uniform [0,1) random (jam roll). The old `return 0.0f` stub ALWAYS // jammed (0 < any positive jam chance), so a projectile weapon could never fire. A real // pseudo-random (LCG) restores authentic behavior: it fires ~(1-jamChance) of the time. static Scalar UniformRandom() { static unsigned s = 0x12345678u; s = s * 1103515245u + 12345u; return (Scalar)((s >> 16) & 0x7fff) / 32768.0f; } static int ResolveTracerModel(void*) { return 0; } // explosion/tracer model handle static int ResolveSubsystemName(const ResourceDirectories*, const char*) { return -1; } // FUN_004215b0 //############################################################################# // Shared Data Support // Derivation ProjectileWeapon::ClassDerivations( &MechWeapon::ClassDerivations, "ProjectileWeapon" // DAT_00512280 ); // task #6: inherit MechWeapon's handler set (see emitter.cpp note). Receiver::MessageHandlerSet& ProjectileWeapon::GetMessageHandlers() { static Receiver::MessageHandlerSet messageHandlers( MechWeapon::GetMessageHandlers()); // copy-inherit return messageHandlers; } Simulation::AttributeIndexSet ProjectileWeapon::AttributeIndex; ProjectileWeapon::SharedData ProjectileWeapon::DefaultData( &ProjectileWeapon::ClassDerivations, ProjectileWeapon::GetMessageHandlers(), MechWeapon::GetAttributeIndex(), // gauge wave: inherit temps/InputVoltage/OutputVoltage/PercentDone (was empty) ProjectileWeapon::StateCount ); //############################################################################# // Test Class Support (PROJWEAP.TCP) // Logical ProjectileWeapon::TestClass(Mech &) { return True; } Logical ProjectileWeapon::TestInstance() const { return IsDerivedFrom(ClassDerivations); } //############################################################################# // Cross-family isolation helpers (mech subsystem roster + cockpit controls). // The owning Mech's subsystem roster and the cockpit "live fire" control flag // are owned by the mech / controls families; these stubs let the recovered // bodies compile and are wired up there. // int ProjectileWeapon::OwnerSubsystemCount(Mech *owner) const { // Real roster (binary reads owner->subsystemCount @0x124). The AmmoBin (0xBCB) // constructs before the projectile weapons (0xBCD/0xBD0) in the factory loop, so // its roster slot is populated by the time this weapon's ctor resolves it. return (owner != 0) ? owner->GetSubsystemCount() : 0; } Subsystem * ProjectileWeapon::OwnerSubsystem(Mech *owner, int index) const { // Real roster lookup (binary: subsystemArray[index] @0x128). return (owner != 0) ? owner->GetSubsystem(index) : 0; } Logical ProjectileWeapon::LiveFireEnabled() const { return True; } // // ConsumeRound -- pull one round from the linked AmmoBin (FUN_004bd4f4 // AmmoBin::FeedAmmo). Returns True when a round was dispensed; otherwise raises // the NoAmmo alarm and returns False. Shared by both FireWeapon spawn paths. // // Panel bridge (AFC100-counter fix, 2026-07-12): resolve the weapon's ammo // bin through the TYPED connection -- the gauge cluster's hand-rolled raw walk // (*(plug+8) twice at subsystem+0x43c) assumed the 1995 connection-object // internals and returned NULL/garbage on our layout, so the panel's ammo // digits never bound (fired shells consumed rounds invisibly). void * BTWeaponAmmoBin(void *weapon) { return (weapon != 0) ? (void *)((ProjectileWeapon *)weapon)->ammoBinLink.Resolve() : 0; } Logical ProjectileWeapon::ConsumeRound() { AmmoBin *bin = (AmmoBin*)ammoBinLink.Resolve(); // FUN_00417ab4(this+0x43c) if (bin != 0 && bin->FeedAmmo() != 0) // FUN_004bd4f4 -- dispensed? { return True; } weaponAlarm.SetLevel(NoAmmoState); // FUN_0041bbd8(this+0x350, 7) return False; } //############################################################################# // Construction / Destruction // // @004bc3fc -- chain to the MechWeapon ctor (@004b99a8, StateCount 8), install // vtable PTR_FUN_0051242c, construct the embedded AmmoBin connection, copy the // projectile parameters out of the resource record, build the launch-velocity // seed from effectiveRange, prime the eject cycle, install the Performance // simulation (unless the model is flagged non-simulated), and link to the // AmmoBin subsystem named in the resource. // ProjectileWeapon::ProjectileWeapon( 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); ammoBinLink.Construct(); // FUN_004bcbb0(this+0x43c, 0) tracerInterval = subsystem_resource->tracerInterval; // +0x1BC -> 0x438 minTimeOfFlight = subsystem_resource->minTimeOfFlight; // +0x1C4 -> 0x40C minJamChance = subsystem_resource->minJamChance; // +0x1CC -> 0x3FC leadPosition = Point3D(0,0,0); // FUN_0040a7f4(this+0x420, DAT_004e0fd4) minVoltagePercentToFire = subsystem_resource->minVoltagePercentToFire; // +0x1C8 -> 0x404 tracerCounter = 0; // 0x408 // launchVelocity seed = (0,0,0) then z = -(effectiveRange / muzzleSpeed) launchVelocity = Vector3D(0,0,0); // FUN_00408440(this+0x410, DAT_004e0f74) Scalar muzzleSpeed = (Scalar)Sqrt(_DAT_004bc67c * _DAT_004bc678); // FUN_004dd138 tracerOrigin = Vector3D(0,0,0); // FUN_00408440(this+0x42c, DAT_004e0f74) launchVelocity.z = -(effectiveRange / muzzleSpeed); // this[0x106] = -(this[0xca]/speed) totalTimeToEject = 3.0f; // 0x3F0 (0x40400000) timeToEject = totalTimeToEject; // 0x3F4 percentOfEject = 0.0f; // 0x3F8 ejectState = -1; // 0x400 (idle) // Install Performance unless the owning Mech is a non-authoritative ghost. // @004bc3fc reads the OWNER's flags (param_2+0x28 == owner->simulationFlags), // NOT the resource flags -- the same authoritative-simulation gate the AmmoBin // uses (owner&0xC==0 && owner&0x100). This is what arms the per-frame tick on // the bring-up's authoritative mech. Logical installPerf = (((owner->simulationFlags & 0xC) == 0) && ((owner->simulationFlags & 0x100) != 0)); if (installPerf) { SetPerformance(&ProjectileWeapon::ProjectileWeaponSimulation); // member-ptr {0x4bbd04,..} } else { simulationFlags |= 0x2; // this[10] |= 2 (mark "not simulated") } if (getenv("BT_PROJ_LOG")) DEBUG_STREAM << "[projweap] CTOR name=" << GetName() << " ownerFlags=" << owner->simulationFlags << " installPerf=" << (int)installPerf << "\n" << std::flush; // Link to the AmmoBin subsystem referenced by the resource and copy the // initial display fields out of it. (The Mech subsystem-roster lookup lives // in the mech family; isolated behind OwnerSubsystemCount/OwnerSubsystem.) if (subsystem_resource->ammoBinIndex < OwnerSubsystemCount(owner)) // +0x124 { Subsystem *roster_bin = OwnerSubsystem(owner, subsystem_resource->ammoBinIndex); // +0x128 table ammoBinLink.Connect(roster_bin); // (**bin.vtbl[1])(...) // CROSS-FAMILY (mech/ammobin): the shipped ctor also primed the bin's HUD // display block (iVar1+0x1f0/0x1f4/0x1f8/0x210) from this weapon; wired in // the AmmoBin family. } AmmoBin *bin = (AmmoBin*)ammoBinLink.Resolve(); // FUN_00417ab4(this+0x43c) if (bin == 0) { DebugStream << GetName() << " Could not get ammo bin!" << endl; // DAT_005122c0 } // Look up / ref-count the explosion (tracer) model for the round. tracerModelHandle = ResolveTracerModel(bin); // FUN_00407064(...,bin+0x1e8,0xf) -> this[0x107] Check_Fpu(); } // // @004bc688 -- destroy the embedded AmmoBin connection, chain to ~MechWeapon, // and (if the deleting-dtor bit is set) free the object. // ProjectileWeapon::~ProjectileWeapon() { Check(this); ammoBinLink.Destruct(); // FUN_004bcbcf(this+0x43c, 2) // base chain (FUN_004b9b9c) handles the rest. Check_Fpu(); } //############################################################################# // Subsystem virtual overrides // // // @004bcabc -- slot 8. A "jam" event (message 2) latches the weapon alarm to // the Jammed level; everything else defers to the Subsystem base handler. // Logical ProjectileWeapon::HandleMessage(int message) { if (message == 2) { weaponAlarm.SetLevel(JammedState); // FUN_0041bbd8(this+0x350, 5) return True; } return PoweredSubsystem::HandleMessage(message); // FUN_004add6c } // // @004bbae0 -- slot 9 (TASK #51 RENAME: slot 9 is TakeDamage, not // ReadUpdateRecord -- see the corrected slot map in mechweap.hpp). // ProjectileWeapon adds nothing of its own; defers to the MechWeapon // implementation (@004b96d4 chains @004b0efc = PoweredSubsystem::TakeDamage). // void ProjectileWeapon::TakeDamage(Damage &damage) { MechWeapon::TakeDamage(damage); // FUN_004b96d4 } // // @004bbaf8 -- slot 10. Reset the firing/eject state, re-arm the alarm to the // "Loading" level, restore the eject countdown, and clear the jam flag, then // chain to MechWeapon. (PROJWEAP.TCP showed an empty early stub; this is the // shipped body.) // void ProjectileWeapon::ResetToInitialState() { MechWeapon::ResetToInitialState(); // FUN_004b96ec weaponAlarm.SetLevel(3); // FUN_0041bbd8(this+0x350, 3) -- "Loading" recoil = rechargeRate; // this[0xfa] = this[0xf7] timeToEject = totalTimeToEject; // this[0xfd] = this[0xfc] percentOfEject = 0.0f; // this[0xfe] = 0 ejectState = -1; // this[0x100] = -1 simulationFlags |= 0x1; // this[6] |= 1 (state dirty) } // // @004bbf88 -- slot 12 (GetStatusFlags). Body NOT recovered by the decompiler // (function prologue present at 0x4bbf88). Overrides MechWeapon's // PoweredSubsystem::GetStatusFlags (@004b0f48); best-effort: ORs the weapon's // ammo/jam status bits onto the base flags. // LWord ProjectileWeapon::GetStatusFlags() { // BEST-EFFORT -- body unrecovered. Defer to base until a human disassembles // 0x4bbf88. return MechWeapon::GetStatusFlags(); } // // @004bc6c8 -- slot 13. Print the MechWeapon state, then append the // ammo-specific weapon-alarm state. // void ProjectileWeapon::PrintState() { MechWeapon::PrintState(); // FUN_004b9d00 switch (weaponAlarm.GetState()) // *(this+0x364) { case JammedState: DebugStream << GetName() << " = Jammed"; break; // 5 case TriggerDuringJamState: DebugStream << GetName() << " = TriggerDuringJam"; break; // 6 case NoAmmoState: DebugStream << GetName() << " = NoAmmo"; break; // 7 default: DebugStream << GetName() << "Unknown weapon state!"; break; } DebugStream.Flush(); } // // @004bbc20 -- slot 16, MechWeapon-family per-frame hook (overrides MechWeapon // @004b0b5c). Body NOT recovered (prologue present at 0x4bbc20). // void ProjectileWeapon::UpdateWeaponState() { // BEST-EFFORT -- body unrecovered. TODO: disassemble 0x4bbc20. } //############################################################################# // Internal model helpers // // // @004bbb50 -- advance the eject/reload cycle. While ejecting (ejectState==0) // run the TimeToEject countdown (clamped >= 0) and recompute PercentOfEject; // when the countdown reaches ~0 ask the AmmoBin for the next round, raising the // NoAmmo alarm and parking the weapon if the bin is dry. // void ProjectileWeapon::UpdateEject(Scalar time_slice) { if (ejectState == 0) { timeToEject -= time_slice; // this+0x3f4 if (timeToEject < 0.0f) // _DAT_004bbc18 == 0.0f timeToEject = 0.0f; percentOfEject = (totalTimeToEject - timeToEject) / totalTimeToEject; // this+0x3f8 } if (Close_Enough(timeToEject, 0.0f)) // FUN_004dcd00 .. _DAT_004bbc1c { AmmoBin *bin = (AmmoBin*)ammoBinLink.Resolve(); // FUN_00417ab4(this+0x43c) if (bin != 0) { bin->DumpAmmo(); // FUN_004bd588 weaponAlarm.SetLevel(NoAmmoState); // FUN_0041bbd8(this+0x350, 7) } ejectState = -1; // this+0x400 = -1 (done) } } // // @004bbc78 -- bleed the firing charge (MechWeapon recoil @0x3E8) at a rate set // by the available bus voltage and the AmmoBin's remaining supply ratio. // void ProjectileWeapon::DrawFiringCharge(Scalar time_slice) { // bus voltage scale (FUN_004b0d50) -- clamp to >= 1.0 Scalar voltage = 1.0f; if (voltage < 1.0f) // _DAT_004bbd00 == 1.0f voltage = 1.0f; // available supply ratio from the linked voltage source. The shipped code // read source->outputVoltage / source->ratedVoltage off the resolved Generator // segment; here we use the inherited PoweredSubsystem voltageScale, which is // that same normalised available-voltage figure. Scalar supply = voltageScale; // (+0x1dc)/(+0x1d8) if (supply < minVoltagePercentToFire) // this+0x404 supply = 0.0f; recoil -= (time_slice * supply) / voltage; // *(this+0x3e8) } // // @004bbfcc -- roll a heat-scaled jam chance. Returns True (jam!) when a // uniform random sample falls under the current jam probability: // p = clamp(0.41 * currentTemperature / failureTemperature, // minJamChance, 1.0) // Gated on the weapon being heat-active (this+0x184) and the owning Mech's // "live fire" flag (mech.controls+0x25c). // Logical ProjectileWeapon::CheckForJam() { if (!LiveFireEnabled()) // *(*(this+0xd0)+400)+0x25c return False; // BRING-UP: the reconstructed heat model holds currentTemperature at a constant ~77 // (thermalMass is huge, so temp doesn't yet rise/fall with fire), which makes this // heat-scaled jam roll fire spuriously EVERY shot -- and since JammedState clears only // on ResetToInitialState, a weapon that jams STAYS jammed for the whole mission (you'd // fire one salvo, then only lasers). The authentic rule is "jam risk scales with heat", // so gate the roll on the weapon actually being in a degraded HEAT STATE (heatAlarm // level >= 1) instead of the raw heatLoad. In bring-up the alarm stays 0 (temp << // degradation threshold) -> weapons fire reliably; real jams return once the heat model // drives true temps. (FOLLOW-UP: make JammedState recoverable via an unjam delay so // even a genuine overheat jam clears.) if (heatAlarm.GetLevel() < HeatSink::DegradationHeat) // was: if (heatLoad <= 0.0f) return False; Scalar p = (_DAT_004bc064 * currentTemperature) / failureTemperature; // 0.41 * temp/failTemp if (minJamChance <= p) // this+0x3fc { if (_DAT_004bc068 < p) // clamp to 1.0 p = _DAT_004bc068; } else { p = minJamChance; } return (p > UniformRandom()) ? True : False; // FUN_00408050 } // // @004bc06c -- lead-solution time-of-flight. Sample the current target // position, advance it by launchVelocity, and re-run targeting. With no target // in range, return minTimeOfFlight plus a small bias; otherwise return // rangeToTarget / projectileSpeed. // Scalar ProjectileWeapon::ComputeTimeOfFlight() { GetTargetPosition(leadPosition); // FUN_004b9cbc(this, this+0x420) leadPosition.x += launchVelocity.x; // this+0x420 += this+0x410 leadPosition.y += launchVelocity.y; leadPosition.z = launchVelocity.z; // (z copied, not accumulated) if (!UpdateTargeting()) // FUN_004b9bdc -- target in range? { return minTimeOfFlight + _DAT_004bc100; // +0x40c + 1.0 } Scalar speed = Sqrt( // FUN_004dd138 leadPosition.z * leadPosition.z + leadPosition.y * leadPosition.y + leadPosition.x * leadPosition.x); return rangeToTarget / speed; // this+0x324 / speed } //############################################################################# // Simulation / Firing // // // @004bbd04 -- Performance: per-frame ProjectileWeapon update. Body NOT // recovered by the decompiler (prologue confirmed at 0x4bbd04). From context it // sequences DrawFiringCharge / UpdateEject / CheckForJam / ComputeTimeOfFlight // and dispatches FireWeapon (vtable slot 18) when the trigger + charge + ammo // conditions are met. // void ProjectileWeapon::ProjectileWeaponSimulation(Scalar time_slice) { Check(this); // 0. The PoweredSubsystem step FIRST -- disasm @004bbd12 (task #10): // `call 0x4b0bd0` opens the body, exactly like the emitter sibling. It // runs HeatSink::HeatSinkSimulation (absorb pendingHeat -> temperature -> // conduct to the authored condenser) and the electrical state machine. // Without it the launcher's firing heat (task #9) accumulated in // pendingHeat forever and never reached the mech's thermal network. PoweredSubsystem::PoweredSubsystemSimulation(time_slice); // @004b0bd0 // 0b. The FAULT gate -- disasm @004bbd36-004bbd6e (task #10): flags==1, the // weapon's own heatAlarm at FailureHeat (this+0x184 == 2 -- THE consumer // of the authored FailureTemperature for the ballistic family), or the // owning mech destroyed -> pin recoil to the full rechargeRate and latch // alarm state 7 (unavailable). No return: the frame continues. if (simulationFlags == 1 || heatAlarm.GetLevel() == HeatSink::FailureHeat // this+0x184 == 2 || (owner != 0 && owner->IsDerivedFrom(*Mech::GetClassDerivations()) && ((Mech *)owner)->IsDisabled())) // FUN_0049fb54 { recoil = rechargeRate; // this+0x3E8 = this+0x3DC weaponAlarm.SetLevel(7); // FUN_0041bbd8(this+0x350, 7) } // 1. Recover the firing charge (recoil counts down toward 0 in // DrawFiringCharge) and advance any in-progress magazine eject. DrawFiringCharge(time_slice); // @004bbc78 -- bleed recoil (+0x3E8) UpdateEject(time_slice); // @004bbb50 -- idle unless ejectState==0 // 2. AUTHENTIC TRIGGER (task #5): fireImpulse = the TriggerState attribute // the streamed controls map binds to the pod fire buttons (see emitter.cpp // note). The gBTMissileTrigger bypass is retired. Logical trigger = CheckFireEdge(); // MechWeapon @004b9608 static int s_pwtick = 0; if (getenv("BT_PROJ_LOG") && (((++s_pwtick) % 240) == 0 || trigger)) DEBUG_STREAM << "[projweap] tick " << GetName() << " trig=" << (int)trigger << " trigState=" << (float)fireImpulse << " ready=" << (int)ReadyToFire() << " state=" << weaponAlarm.GetState() << " recoil=" << recoil << " x\n" << std::flush; // 3. Firing state machine (weaponAlarm @0x350). Modelled on the RP analog // RivetGun::RivetGunSimulation (WEAPSYS.cpp): the Jammed / NoAmmo latches // persist and only escalate on a trigger pull; the default (ready) branch // rolls the heat-scaled jam chance and -- on a clean shot with charge and // ammo -- dispatches FireWeapon through the vtable (slot 18) so the // MissileLauncher override spawns Missiles. switch (weaponAlarm.GetState()) // *(this+0x364) { case JammedState: // 5 -- cleared only by ResetToInitialState if (trigger) weaponAlarm.SetLevel(TriggerDuringJamState); // 6 break; case NoAmmoState: // 7 -- re-assert on a dry trigger pull if (trigger) weaponAlarm.SetLevel(NoAmmoState); break; default: // Loaded / Loading / Firing / idle if (trigger && ReadyToFire()) { if (CheckForJam()) // @004bbfcc -- heat-scaled jam roll { weaponAlarm.SetLevel(JammedState); // 5 break; } // refresh the lead solution (updates leadPosition / targeting), then // fire. FireWeapon() consumes a round + spawns the projectile/missile // and (on a dry bin) latches NoAmmo itself. ComputeTimeOfFlight(); // @004bc06c FireWeapon(); // vtable slot 18 -> spawn path if (weaponAlarm.GetState() != NoAmmoState && weaponAlarm.GetState() != JammedState) weaponAlarm.SetLevel(0); // Firing } break; } Check_Fpu(); } // // @004bc104 -- slot 18 FireWeapon (overrides MechWeapon's pure-virtual trap // @004ba45c). Body NOT recovered by the decompiler (prologue confirmed at // 0x4bc104, immediately following the 1.0f literal @0x4bc100). Spawns the // generic projectile/tracer and starts the eject cycle. MissileLauncher // overrides this again at @004bcc60 to launch Missile entities. // void ProjectileWeapon::FireWeapon() { Check(this); // THE FIRING HEAT (task #9; was "a separate pre-existing gap"): the // binary adds heatCostToFire RAW to the weapon's own pendingHeat -- // projectile resources author PRE-scaled 1e7-unit heat (AFC100 = 6.5e7) // -- gated on the heatable check (part_013.c:8744-8747 idiom). if (HeatModelActive()) // FUN_004ad7d4 (heat.hpp; [T3] permissive) { pendingHeat += heatCostToFire; // HeatSink @0x1C8, raw } // Binary @4bc136-4bc19c (task #56): the firing RECOIL kick, BEFORE the ammo // pull (no early-out precedes it in the binary). Gate: per-shot damage > // 3.0f (@0x4bc3f4) && the owner mech has a gyro. Direction (0, 0.6, -1.5) // (0x3f19999a/0xbfc00000), magnitude = damageAmount / 16 (@0x4bc3f8). if (damageData.damageAmount > 3.0f && owner != 0) { extern void GyroApplyDamageImpulse(Subsystem *, Scalar, Scalar, Scalar, Scalar); Subsystem *g = ((Mech *)owner)->GetGyroSubsystem(); // mech+0x528 if (g != 0) GyroApplyDamageImpulse(g, 0.0f, 0.6f, -1.5f, damageData.damageAmount * 0.0625f); } // Pull a round; a dry / not-ready bin latches NoAmmo and aborts the shot. if (!ConsumeRound()) // FUN_004bd4f4 (AmmoBin::FeedAmmo) { return; } // Begin the per-shot recharge cooldown (recoil bleeds back down to 0 in // DrawFiringCharge before the weapon is ReadyToFire again). recoil = rechargeRate; // this[0xfa] = this[0xf7] // Resolve the live muzzle. Point3D muzzle; GetMuzzlePoint(muzzle); // MechWeapon @004b9948 // GENUINE MUZZLE FLASH (task #61): the shipped DAFC fire-smoke blast at the // gun-port -- BTDPL.INI psfx 6/14 "the effect used on all projectile guns". // Fires on every AC shot (all ACs author TracerInterval=1); one ~0.2s // auto-expiring burst. Default ON (BT_MUZZLE=0 disables for diagnostics). { const char *mz = getenv("BT_MUZZLE"); // default ON; =0 disables if (mz == 0 || mz[0] != '0') { extern void BTFlashMuzzle(void *ownerMech, int seg_index, float mx, float my, float mz); BTFlashMuzzle(owner, GetSegmentIndex(), (float)muzzle.x, (float)muzzle.y, (float)muzzle.z); } } // WAVE 7 Phase B: launch one flying ballistic round toward the owner's target (port // reconstruction; the byte-exact world-entity Projectile is blocked by the 2007 engine // Entity base mismatch -- see BTPushProjectile / mech4.cpp). Speed = |launchVelocity|. char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech) // The owner's target slots (raw 0x388/0x37c -- the same raw offsets mech4's // targeting block writes; a consistent producer/consumer pair on our // compiled object). Task #41: the slots hold the WORLD PICK -- the enemy // mech under the boresight, or the terrain downrange (a missile fired at // the scenery flies to the ground point and detonates without damage), or // null at the sky (BTPushProjectile refuses; the weapon's own 0x388 gate // wouldn't have fired either). void *target = (o != 0) ? *(void **)(o + 0x388) : 0; Point3D targetPos = (o != 0) ? *(Point3D *)(o + 0x37c) : muzzle; Scalar speed = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x + launchVelocity.y*launchVelocity.y + launchVelocity.z*launchVelocity.z); BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount, 0 /*aim straight at the pick*/, 0 /*BALLISTIC: no seeker on a shell*/, subsystemID /*messmgr explosion bundling at impact (task #7)*/); // task #61: mark this shot for REPLICATION so the peer sees the enemy's // cannon. ++fireCounter + ForceUpdate() -- IDENTICAL to the missile path // (mislanch.cpp:316/326) and the emitter beam: the master's per-frame // subsystem serialize writes this weapon's record (WriteUpdateRecord), // Entity::UpdateMessageHandler routes it to the peer's replicant, and the // replicant's ReadUpdateRecord mirrors the shot. (The old code set only // `simulationFlags |= 0x1` -- the +0x28 instance flag, NOT the updateModel // bit WriteSimulationUpdate walks -- so the AC record never serialized: the // root cause of the invisible enemy autocannon.) { BTAcFireState &fs = BTAcFireOf(this); ++fs.fired; fs.target = targetPos; } simulationFlags |= 0x1; // replication-dirty (state flag) ForceUpdate(); // set the updateModel bit -> serialize the record Check_Fpu(); } //############################################################################# // FIRE-VISUAL REPLICATION (task #61) -- the AC twin of MissileLauncher's salvo // mirror. Master serializes the fire counter + aim after the MechWeapon base // fields; the replicant edge-detects the counter and mirrors ONE visual round // + DAFC muzzle flash from its OWN resolved muzzle -- so the enemy's autocannon // is visible on the peer (the port's local flying round otherwise never left // the firing node). //############################################################################# void ProjectileWeapon::WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model) { MechWeapon::WriteUpdateRecord(message, update_model); // @004b9690 (alarm fields + subsystemID) ProjectileWeapon__UpdateRecord *rec = (ProjectileWeapon__UpdateRecord *)message; BTAcFireState &s = BTAcFireOf(this); rec->recordLength = sizeof(ProjectileWeapon__UpdateRecord); rec->fireCounter = s.fired; rec->fireTarget = s.target; } void ProjectileWeapon::ReadUpdateRecord(Simulation__UpdateRecord *message) { MechWeapon::ReadUpdateRecord(message); // @004b964c (alarm apply) ProjectileWeapon__UpdateRecord *rec = (ProjectileWeapon__UpdateRecord *)message; BTAcFireState &s = BTAcFireOf(this); if (s.seen < 0) { s.seen = rec->fireCounter; // first sync: adopt silently return; } if (rec->fireCounter == s.seen) return; s.seen = rec->fireCounter; // Mirror ONE shot (a missed record collapses to one -- no phantom replays): // a VISUAL-ONLY round (damage 0; the master's round delivers cross-pod // damage) from this replicant's own muzzle toward the master's aim point, // plus the DAFC muzzle flash. Point3D mz; GetMuzzlePoint(mz); // @004b9948 Scalar spd = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x + launchVelocity.y*launchVelocity.y + launchVelocity.z*launchVelocity.z); // Fly toward the master's aim if it replicated a real pick; else straight // out the barrel (the shooter-frame launchVelocity, as the missile mirror // does) so an untargeted shot still streaks forward, not toward the origin. const int haveAim = (rec->fireTarget.x != 0.0f || rec->fireTarget.y != 0.0f || rec->fireTarget.z != 0.0f); BTPushProjectile(mz, owner, 0 /*aim point only, no entity*/, rec->fireTarget, spd, 0.0f /*VISUAL*/, haveAim ? 0 : &launchVelocity, 0 /*ballistic*/, subsystemID); const char *mzf = getenv("BT_MUZZLE"); // default ON; =0 disables if (mzf == 0 || mzf[0] != '0') { extern void BTFlashMuzzle(void *ownerMech, int seg_index, float, float, float); BTFlashMuzzle(owner, GetSegmentIndex(), (float)mz.x, (float)mz.y, (float)mz.z); } if (getenv("BT_PROJ_LOG")) DEBUG_STREAM << "[projectile] REPLICANT AC shot at(" << rec->fireTarget.x << "," << rec->fireTarget.y << "," << rec->fireTarget.z << ")\n" << std::flush; } //############################################################################# // CreateStreamedSubsystem // // @004bc7cc -- parse the ProjectileWeapon-specific resource fields after the // MechWeapon base parser. Stamps subsystemModelSize = 0x1D0. (The classID // stamp here equals the MechWeapon class id 0xBCD -- ProjectileWeapon is an // abstract base with no separately-registered ClassID; the concrete // MissileLauncher::CreateStreamedSubsystem @004bd08c overwrites the stamp with // 0x1E0 / classID 0xBD0.) // int ProjectileWeapon::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); // 0x1D0 subsystem_resource->classID = (RegisteredClass::ClassID)Mech::MechWeaponClassID; // 0xBCD (abstract) if (passes == 1) { // prime all projectile fields to "unset" subsystem_resource->tracerInterval = -1; // +0x1BC subsystem_resource->ammoBinIndex = -1; // +0x1C0 subsystem_resource->minTimeOfFlight = -1.0f; // +0x1C4 subsystem_resource->minVoltagePercentToFire = -1.0f; // +0x1C8 subsystem_resource->minJamChance = -1.0f; // +0x1CC } if ( !model_file->GetEntry(subsystem_name, "TracerInterval", &subsystem_resource->tracerInterval) && subsystem_resource->tracerInterval == -1 ) { DebugStream << subsystem_name << " missing TracerInterval!" << endl; return False; } // // AmmoBin: a subsystem name; "Unspecified" leaves the resource value, // otherwise it is resolved to a subsystem index (+2 bias applied when not // "Unspecified"). // const char *ammoBinName = "Unspecified"; if ( !model_file->GetEntry(subsystem_name, "AmmoBin", &ammoBinName) && subsystem_resource->ammoBinIndex == -1 ) { DebugStream << subsystem_name << " missing AmmoBin!" << endl; return False; } if (strcmp(ammoBinName, "Unspecified") != 0) { subsystem_resource->ammoBinIndex = ResolveSubsystemName(directories, ammoBinName); // FUN_004215b0 } if (subsystem_resource->ammoBinIndex < 0) { DebugStream << subsystem_name << " has an invalid Ammo Bin!" << endl; return False; } if (strcmp(ammoBinName, "Unspecified") != 0) { subsystem_resource->ammoBinIndex += 2; // segment-table +2 bias } if ( !model_file->GetEntry(subsystem_name, "MinTimeOfFlight", &subsystem_resource->minTimeOfFlight) && subsystem_resource->minTimeOfFlight == -1.0f // _DAT_004bca9c ) { DebugStream << subsystem_name << " missing MinTimeOfFlight !" << endl; return False; } if ( !model_file->GetEntry(subsystem_name, "MinJamChance", &subsystem_resource->minJamChance) && subsystem_resource->minJamChance == -1.0f ) { DebugStream << subsystem_name << " missing MinJamChance !" << endl; return False; } if ( !model_file->GetEntry(subsystem_name, "MinVoltagePercentToFire", &subsystem_resource->minVoltagePercentToFire) && subsystem_resource->minVoltagePercentToFire == -1.0f ) { DebugStream << subsystem_name << " missing MinVoltagePercentToFire !" << endl; return False; } return True; } //===========================================================================// // WAVE 7 factory bridge -- ProjectileWeapon (factory case 0xBCD, "MechWeapon" // mislabel). The real class at 0xBCD (ctor @004bc3fc) is ProjectileWeapon // (the autocannon/ballistic ammo weapon); the factory built the base MechWeapon // stub (whose FireWeapon is the pure-virtual trap @004ba45c). alloc 0x448 (== // the byte-exact ProjectileWeapon sizeof). Constructs the AmmoBin connection + // ticks its real ProjectileWeaponSimulation (charge/ammo/jam/eject); FireWeapon // consumes a round + recoils but DEFERS the projectile spawn (Phase B). //===========================================================================// Subsystem *CreateProjectileWeaponSubsystem(Mech *owner, int id, void *seg) { Check(sizeof(ProjectileWeapon) <= 0x448); return (Subsystem *) new (Memory::Allocate(0x448)) ProjectileWeapon(owner, id, (ProjectileWeapon::SubsystemResource *)seg, ProjectileWeapon::DefaultData); }