diff --git a/game/reconstructed/mechweap.cpp b/game/reconstructed/mechweap.cpp index cb74ea8..65ef4df 100644 --- a/game/reconstructed/mechweap.cpp +++ b/game/reconstructed/mechweap.cpp @@ -327,6 +327,20 @@ MechWeapon::MechWeapon( rangeToTarget = 0.0f; // 0x324 targetWithinRange = False; // 0x34C + + // #95-follow-up WEAPON SPEC AUDIT (BT_SPEC_LOG): one line per weapon with + // the AUTHORED numbers, so "missile damage is too low" can be argued + // against the shipped data instead of memory. damageAmount here is the + // authored TOTAL (the MissileLauncher ctor divides by missileCount after + // this, mirroring @004bcff0). + if (getenv("BT_SPEC_LOG")) + DEBUG_STREAM << "[wspec] " << (GetName() ? GetName() : "?") + << " dmg=" << damageData.damageAmount + << " type=" << (int)damageData.damageType + << " recycle=" << rechargeRate + << " range=" << weaponRange + << " heat=" << heatCostToFire + << "\n" << std::flush; // segmentReference/pipSegment were phantom tail members (past the binary 0x3F0 end); // removed. The muzzle segment index is the inherited base slot this+0xdc (used by // GetMuzzlePoint); the pip mount is gated by useConfiguredPip in DrawWeaponPip. diff --git a/game/reconstructed/mislanch.cpp b/game/reconstructed/mislanch.cpp index c621c11..f3ae0e6 100644 --- a/game/reconstructed/mislanch.cpp +++ b/game/reconstructed/mislanch.cpp @@ -1,525 +1,531 @@ -//============================================================================// -// File: mislanch.cpp // -// Project: BattleTech // -// Contents: Weapon Which Launches a missile (MissileLauncher : ProjectileWeapon)// -//----------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved // -// PROPRIETARY AND CONFIDENTIAL // -//============================================================================// -// -// RECONSTRUCTED from the shipped binary. Behaviour follows the Ghidra -// pseudo-C in part_013.c; member / method names follow the surviving -// MISLANCH.HPP, the resource-parser tag strings, and the reconstructed -// mechweap.hpp / heat.hpp siblings. Each non-trivial method cites the -// originating @ADDR. -// -// Class map for the projectile-weapon branch (resolved from vtable + ctor -// chaining + CreateStreamedSubsystem resource strings): -// ProjectileWeapon ctor @004bc3fc dtor @004bc688 vtable @0051242c -// CSS @004bc7cc (TracerInterval, AmmoBin, -// MinTimeOfFlight, MinJamChance, -// MinVoltagePercentToFire) -// MissileLauncher ctor @004bcff0 dtor @004bd060 vtable @00512570 -// CSS @004bd08c (MissileCount, MuzzleVelocity) -// -// ProjectileWeapon (base) member offsets used below: -// @0x3ac this[0xeb] damageAmount (divided by missileCount in ctor) -// @0x3d4 this[0xf5] per-salvo count copy -// @0x3f0 this[0xfc] MinTimeOfFlight timer base -// @0x3fc this[0xff] MinJamChance -// @0x404 this[0x101] MinVoltagePercentToFire -// @0x40c this[0x103] MinTimeOfFlight -// @0x410 this[0x104] (MissileLauncher) muzzleVelocity (Vector3D) -// @0x418 this[0x106] jam-chance-derived scalar -// @0x438 this[0x10e] TracerInterval -// @0x43c this[0x10f] AmmoBin SharedData connection (FUN_004bcbb0) -// @0x448 this[0x112] (MissileLauncher) missileCount -// -// Helper-function name mapping: -// FUN_004bc3fc ProjectileWeapon base constructor -// FUN_004bc7cc ProjectileWeapon::CreateStreamedSubsystem -// FUN_004bcbb0 AmmoBin-connection ctor (SharedData, vtable @00512424) -// FUN_004b964c MechWeapon::TakeDamage FUN_004b9690 WriteUpdateRecord -// FUN_004bcabc ProjectileWeapon::HandleMessage (msg 2 -> jam alarm) -// FUN_004bc6c8 ProjectileWeapon::PrintState (Jammed/NoAmmo/Trigger...) -// FUN_004bcc60 spawn-a-Missile (FireWeapon path) FUN_004bf8bc Missile::New -// FUN_00408440 Point3D/Vector3D copy FUN_00404088/004040d8 resource get str/int -// FUN_00404088 ResourceFile::GetString FUN_00408944 parse Vector3D from text -// FUN_004084fc Vector3D approx-equal FUN_004dbb24 error-string append -// +//============================================================================// +// File: mislanch.cpp // +// Project: BattleTech // +// Contents: Weapon Which Launches a missile (MissileLauncher : ProjectileWeapon)// +//----------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved // +// PROPRIETARY AND CONFIDENTIAL // +//============================================================================// +// +// RECONSTRUCTED from the shipped binary. Behaviour follows the Ghidra +// pseudo-C in part_013.c; member / method names follow the surviving +// MISLANCH.HPP, the resource-parser tag strings, and the reconstructed +// mechweap.hpp / heat.hpp siblings. Each non-trivial method cites the +// originating @ADDR. +// +// Class map for the projectile-weapon branch (resolved from vtable + ctor +// chaining + CreateStreamedSubsystem resource strings): +// ProjectileWeapon ctor @004bc3fc dtor @004bc688 vtable @0051242c +// CSS @004bc7cc (TracerInterval, AmmoBin, +// MinTimeOfFlight, MinJamChance, +// MinVoltagePercentToFire) +// MissileLauncher ctor @004bcff0 dtor @004bd060 vtable @00512570 +// CSS @004bd08c (MissileCount, MuzzleVelocity) +// +// ProjectileWeapon (base) member offsets used below: +// @0x3ac this[0xeb] damageAmount (divided by missileCount in ctor) +// @0x3d4 this[0xf5] per-salvo count copy +// @0x3f0 this[0xfc] MinTimeOfFlight timer base +// @0x3fc this[0xff] MinJamChance +// @0x404 this[0x101] MinVoltagePercentToFire +// @0x40c this[0x103] MinTimeOfFlight +// @0x410 this[0x104] (MissileLauncher) muzzleVelocity (Vector3D) +// @0x418 this[0x106] jam-chance-derived scalar +// @0x438 this[0x10e] TracerInterval +// @0x43c this[0x10f] AmmoBin SharedData connection (FUN_004bcbb0) +// @0x448 this[0x112] (MissileLauncher) missileCount +// +// Helper-function name mapping: +// FUN_004bc3fc ProjectileWeapon base constructor +// FUN_004bc7cc ProjectileWeapon::CreateStreamedSubsystem +// FUN_004bcbb0 AmmoBin-connection ctor (SharedData, vtable @00512424) +// FUN_004b964c MechWeapon::TakeDamage FUN_004b9690 WriteUpdateRecord +// FUN_004bcabc ProjectileWeapon::HandleMessage (msg 2 -> jam alarm) +// FUN_004bc6c8 ProjectileWeapon::PrintState (Jammed/NoAmmo/Trigger...) +// FUN_004bcc60 spawn-a-Missile (FireWeapon path) FUN_004bf8bc Missile::New +// FUN_00408440 Point3D/Vector3D copy FUN_00404088/004040d8 resource get str/int +// FUN_00404088 ResourceFile::GetString FUN_00408944 parse Vector3D from text +// FUN_004084fc Vector3D approx-equal FUN_004dbb24 error-string append +// + +#include +#pragma hdrstop + +#if !defined(MISLANCH_HPP) +# include +#endif +#if !defined(MISSILE_HPP) +# include +#endif +#if !defined(AMMOBIN_HPP) +# include +#endif +#if !defined(TESTBT_HPP) +# include +#endif +#if !defined(APP_HPP) +# include // #84: application -> host manager +#endif +#include // #84: HostManager::GetEntityPointer (salvo target resolve) +#include + +// WAVE 7 Phase B -- the port-side flying-projectile service (defined in mech4.cpp, a +// complete-Mech TU with the render + damage path). A fired missile becomes a tracked +// projectile that flies to the target + delivers this weapon's damage on impact. +// Missile-arc wave: launch_velocity = the authored MuzzleVelocity VECTOR in the +// shooter's frame (up-tilted rack launch); damage <= 0 = a VISUAL-ONLY round. +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, int splash_burst = 0, int muzzle_seg = -1, + int damage_type = 2 /*ExplosiveDamageType default*/, + Scalar thrust_accel = 0.0f, Scalar thrust_burn = 0.0f /*#84 thruster*/, + int salvo_lead = 1 /*ONE Explosion per salvo*/); +// #84: resolve this launcher's authored thruster (burn s, accel u/s^2) through +// its connected bin's round-model resource. 0/0 when unresolvable (flies at +// the old constant eject speed -- never worse than before). +extern void *BTWeaponAmmoBin(void *weapon); // projweap.cpp +extern int BTAmmoBinModelFile(void *bin); // ammobin.cpp +extern int BTMissileThrustOf(int model_id, float *out_burn, float *out_accel); // mech4.cpp + +//########################################################################### +// Port-side salvo replication state (missile-visibility wave). +// +// The launcher OBJECT is byte-locked at 0x44C (factory alloc) -- salvo state +// cannot live on it. Keyed by launcher identity: on a MASTER, `fired` counts +// FireWeapon salvos (serialized in the extended update record); on a +// REPLICANT, `seen` tracks the last counter applied (-1 = unsynced, so a +// freshly-joined peer does not replay the master's historical count). +//########################################################################### +namespace { + struct BTSalvoState + { + const void *owner; + int fired; // master: salvos launched + int seen; // replicant: last counter applied (-1 = unsynced) + Point3D target; // master: the last salvo's aim point + EntityID targetID; // master: the locked target (#84; Null = point fire) + }; + BTSalvoState gSalvoTable[64]; + + BTSalvoState &BTSalvoOf(const void *launcher) + { + int freeSlot = -1; + for (int i = 0; i < 64; ++i) + { + if (gSalvoTable[i].owner == launcher) + return gSalvoTable[i]; + if (gSalvoTable[i].owner == 0 && freeSlot < 0) + freeSlot = i; + } + BTSalvoState &s = gSalvoTable[(freeSlot >= 0) ? freeSlot : 0]; + s.owner = launcher; + s.fired = 0; + s.seen = -1; + s.target = Point3D(0.0f, 0.0f, 0.0f); + s.targetID = EntityID::Null; + return s; + } +} + +//########################################################################### +//########################################################################### +// MissileLauncher +//########################################################################### +//########################################################################### + +//############################################################################# +// Shared Data Support +// +Derivation + MissileLauncher::ClassDerivations( + &ProjectileWeapon::ClassDerivations, + "MissileLauncher" + ); + +// task #6: inherit ProjectileWeapon's handler set (see emitter.cpp note). +Receiver::MessageHandlerSet& + MissileLauncher::GetMessageHandlers() +{ + static Receiver::MessageHandlerSet messageHandlers( + ProjectileWeapon::GetMessageHandlers()); // copy-inherit + return messageHandlers; +} +Simulation::AttributeIndexSet MissileLauncher::AttributeIndex; + +MissileLauncher::SharedData + MissileLauncher::DefaultData( + &MissileLauncher::ClassDerivations, + MissileLauncher::GetMessageHandlers(), + MechWeapon::GetAttributeIndex(), // gauge wave: inherit temps/InputVoltage/OutputVoltage/PercentDone (was empty) + MissileLauncher::StateCount + ); + +//############################################################################# +// Construction +// +// @004bcff0 MissileLauncher::MissileLauncher(...) +// +// Chains the ProjectileWeapon base ctor, stamps the MissileLauncher vtable, +// then reads the two launcher resource fields and splits the damage budget +// evenly across the salvo so each spawned Missile carries +// damageAmount/missileCount. +// +MissileLauncher::MissileLauncher( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &default_data) +: + // ProjectileWeapon base ctor (ammo bin, jam, tracer, targeting). + ProjectileWeapon(owner, subsystem_ID, subsystem_resource, default_data) // @004bc3fc +{ + // vtable installed by the compiler (PTR_FUN_00512570) + + missileCount = subsystem_resource->missileCount; // @0x448 resource +0x1d0 -#include -#pragma hdrstop - -#if !defined(MISLANCH_HPP) -# include -#endif -#if !defined(MISSILE_HPP) -# include -#endif -#if !defined(AMMOBIN_HPP) -# include -#endif -#if !defined(TESTBT_HPP) -# include -#endif -#if !defined(APP_HPP) -# include // #84: application -> host manager -#endif -#include // #84: HostManager::GetEntityPointer (salvo target resolve) -#include - -// WAVE 7 Phase B -- the port-side flying-projectile service (defined in mech4.cpp, a -// complete-Mech TU with the render + damage path). A fired missile becomes a tracked -// projectile that flies to the target + delivers this weapon's damage on impact. -// Missile-arc wave: launch_velocity = the authored MuzzleVelocity VECTOR in the -// shooter's frame (up-tilted rack launch); damage <= 0 = a VISUAL-ONLY round. -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, int splash_burst = 0, int muzzle_seg = -1, - int damage_type = 2 /*ExplosiveDamageType default*/, - Scalar thrust_accel = 0.0f, Scalar thrust_burn = 0.0f /*#84 thruster*/, - int salvo_lead = 1 /*ONE Explosion per salvo*/); -// #84: resolve this launcher's authored thruster (burn s, accel u/s^2) through -// its connected bin's round-model resource. 0/0 when unresolvable (flies at -// the old constant eject speed -- never worse than before). -extern void *BTWeaponAmmoBin(void *weapon); // projweap.cpp -extern int BTAmmoBinModelFile(void *bin); // ammobin.cpp -extern int BTMissileThrustOf(int model_id, float *out_burn, float *out_accel); // mech4.cpp - -//########################################################################### -// Port-side salvo replication state (missile-visibility wave). -// -// The launcher OBJECT is byte-locked at 0x44C (factory alloc) -- salvo state -// cannot live on it. Keyed by launcher identity: on a MASTER, `fired` counts -// FireWeapon salvos (serialized in the extended update record); on a -// REPLICANT, `seen` tracks the last counter applied (-1 = unsynced, so a -// freshly-joined peer does not replay the master's historical count). -//########################################################################### -namespace { - struct BTSalvoState - { - const void *owner; - int fired; // master: salvos launched - int seen; // replicant: last counter applied (-1 = unsynced) - Point3D target; // master: the last salvo's aim point - EntityID targetID; // master: the locked target (#84; Null = point fire) - }; - BTSalvoState gSalvoTable[64]; - - BTSalvoState &BTSalvoOf(const void *launcher) - { - int freeSlot = -1; - for (int i = 0; i < 64; ++i) - { - if (gSalvoTable[i].owner == launcher) - return gSalvoTable[i]; - if (gSalvoTable[i].owner == 0 && freeSlot < 0) - freeSlot = i; - } - BTSalvoState &s = gSalvoTable[(freeSlot >= 0) ? freeSlot : 0]; - s.owner = launcher; - s.fired = 0; - s.seen = -1; - s.target = Point3D(0.0f, 0.0f, 0.0f); - s.targetID = EntityID::Null; - return s; - } -} - -//########################################################################### -//########################################################################### -// MissileLauncher -//########################################################################### -//########################################################################### - -//############################################################################# -// Shared Data Support -// -Derivation - MissileLauncher::ClassDerivations( - &ProjectileWeapon::ClassDerivations, - "MissileLauncher" - ); - -// task #6: inherit ProjectileWeapon's handler set (see emitter.cpp note). -Receiver::MessageHandlerSet& - MissileLauncher::GetMessageHandlers() -{ - static Receiver::MessageHandlerSet messageHandlers( - ProjectileWeapon::GetMessageHandlers()); // copy-inherit - return messageHandlers; -} -Simulation::AttributeIndexSet MissileLauncher::AttributeIndex; - -MissileLauncher::SharedData - MissileLauncher::DefaultData( - &MissileLauncher::ClassDerivations, - MissileLauncher::GetMessageHandlers(), - MechWeapon::GetAttributeIndex(), // gauge wave: inherit temps/InputVoltage/OutputVoltage/PercentDone (was empty) - MissileLauncher::StateCount - ); - -//############################################################################# -// Construction -// -// @004bcff0 MissileLauncher::MissileLauncher(...) -// -// Chains the ProjectileWeapon base ctor, stamps the MissileLauncher vtable, -// then reads the two launcher resource fields and splits the damage budget -// evenly across the salvo so each spawned Missile carries -// damageAmount/missileCount. -// -MissileLauncher::MissileLauncher( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &default_data) -: - // ProjectileWeapon base ctor (ammo bin, jam, tracer, targeting). - ProjectileWeapon(owner, subsystem_ID, subsystem_resource, default_data) // @004bc3fc -{ - // vtable installed by the compiler (PTR_FUN_00512570) - - missileCount = subsystem_resource->missileCount; // @0x448 resource +0x1d0 - launchVelocity = subsystem_resource->muzzleVelocity; // @0x410 (inherited ProjectileWeapon slot; was aliased "muzzleVelocity") - - // per-salvo count copy + per-missile damage (write the inherited Damage fields) - this->damageData.burstCount = missileCount; // @0x3d4 this[0xf5] (was shadow "salvoCount") - this->damageData.damageAmount /= (Scalar)missileCount; // @0x3ac this[0xeb] /= missileCount -} - -//############################################################################# -// Destruction -// -// @004bd060 ~MissileLauncher() -- restores the launcher vtable, chains the -// ProjectileWeapon destructor (which releases the AmmoBin connection @00512424). -// -MissileLauncher::~MissileLauncher() -{ - // vtable reset + base ~ProjectileWeapon chaining handled by the compiler. -} - -//############################################################################# -// CreateStreamedSubsystem -// -// @004bd08c Reads MissileCount (+0x1d0) and MuzzleVelocity (+0x1d4) after -// chaining ProjectileWeapon::CreateStreamedSubsystem (@004bc7cc). Stamps the -// streamed-record class/size header bytes, then validates both fields, -// emitting "missing MissileCount!" / "missing MuzzleVelocity!" on failure. -// -Logical MissileLauncher::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) -{ - // chain the ProjectileWeapon record parser (ammo/jam/tracer fields) - if (!ProjectileWeapon::CreateStreamedSubsystem( - resource_file, model_file, model_name, subsystem_name, - subsystem_resource, subsystem_file, directories, passes)) // @004bc7cc - return False; - - // stamp streamed-record header (classID / size bytes @+0x20/+0x24) - // ... (record housekeeping, see @004bd08c) ... - - if (passes == 1) - subsystem_resource->missileCount = -1; // sentinel - - // "MissileCount" (+0x1d0) - if (!model_file->GetEntry(subsystem_name, "MissileCount", - &subsystem_resource->missileCount) - && subsystem_resource->missileCount == -1) - { - DebugStream << subsystem_name << " missing MissileCount!" << endl; // @004bd08c - return False; - } - - // "MuzzleVelocity" (+0x1d4, Vector3D parsed from text) - const char *text = "Unspecified"; - Vector3D &mv = subsystem_resource->muzzleVelocity; - if (!model_file->GetEntry(subsystem_name, "MuzzleVelocity", &text) - && (mv.x == 0.0f && mv.y == 0.0f && mv.z == 0.0f)) - { - DebugStream << subsystem_name << " missing MuzzleVelocity!" << endl; // @004bd08c - return False; - } - if (strcmp(text, "Unspecified") != 0) - { - // FUN_00408944 -- parse a "x y z" Vector3D from the resource text. - float x = 0.0f, y = 0.0f, z = 0.0f; - if (sscanf(text, "%f %f %f", &x, &y, &z) == 3) - { - mv.x = x; mv.y = y; mv.z = z; - } - } - - return True; -} - -//############################################################################# -// FireWeapon (ProjectileWeapon spawn path, @004bcc60) -// -// Builds a streamed-entity spawn descriptor for a Missile (entity ClassID -// 0xBD0), seeded from this weapon's muzzle transform, the explosion model -// resource (this->explosionResourceID @0x3e4), the per-missile Damage block -// (this->damageData @0x3a8) and the owning Mech's target. The descriptor is -// handed to Missile::New (FUN_004bf8bc), which allocates the 0x368-byte -// entity and runs the Missile ctor (@004bf5b4). Heat from firing is added -// to the owner here (this->heatCostToFire -> owner heatLoad) when the weapon -// is heat-bearing (FUN_004ad7d4). -// -// NOTE: this body lives in the ProjectileWeapon address range and is shared -// by MissileLauncher; it is documented here because MISLANCH.HPP declares -// FireWeapon() as the launcher's protected entry point. -// -void MissileLauncher::FireWeapon() -{ - Check(this); - - // Gitea #12: this body is heat + spawn ONLY, per the recovered @004bcc60 - // (part_013.c:8741+): NO viewFireEnable gate, NO ammo pull, NO recoil set. - // All of that lives in the CALLER -- the recovered ProjectileWeaponSimulation - // Loaded case (@4bbec2 view/target denial blip, @4bbee6 bin->FeedAmmo, - // @4bbf51 recoil). The old early-returns here, under a caller that cycled - // the alarm unconditionally, faked a full firing pip cycle on every denied - // shot -- the "missiles cycle but never launch" incident mechanic. - - // THE FIRING HEAT (task #9): heatCostToFire RAW into the launcher's own - // pendingHeat (pre-scaled 1e7-unit resources: SRM6 = 5e7, LRM15 = 6.5e7) - // -- the binary's add @part_013.c:8744-8747, gated on the heatable check. - if (HeatModelActive()) // FUN_004ad7d4 (heat.hpp -> player+0x260 experience gate; issue #2) - { - pendingHeat += heatCostToFire; // HeatSink @0x1C8, raw - } - - // Resolve the muzzle / lead geometry (seeds each flying missile). - Point3D muzzle; - GetMuzzlePoint(muzzle); // @004b9948 - damageData.burstCount = missileCount; // @0x3d4 (was shadow "salvoCount") - - // WAVE 7 Phase B: launch `missileCount` flying projectiles toward the owner's target. - // (Port reconstruction -- the byte-exact world-entity Missile is blocked by the 2007 - // engine Entity base mismatch; see BTPushProjectile / mech4.cpp.) Speed = |launchVelocity| - // (resource MuzzleVelocity); per-missile damage = damageData.damageAmount (already - // divided by missileCount in the ctor). - char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech) - void *target = (o != 0) ? *(void **)(o + 0x388) : 0; // owner's locked target entity (null in bring-up) - Point3D targetPos = (o != 0) ? *(Point3D *)(o + 0x37c) : muzzle; // owner target point - Scalar speed = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x - + launchVelocity.y*launchVelocity.y - + launchVelocity.z*launchVelocity.z); - // Always launch -- BTPushProjectile falls back to gEnemyMech when the owner's target - // slot isn't populated (bring-up); missileCount missiles per salvo. The authored - // MuzzleVelocity VECTOR rides along (missile-arc wave): the round leaves the rack - // on the authored up-tilt, then the seeker loft + steering arc it onto the target. - int nmiss = (missileCount > 0 && missileCount < 40) ? missileCount : 1; - float tBurn = 0.0f, tAccel = 0.0f; // #84 authored thruster - { - void *bin = BTWeaponAmmoBin(this); - int mid = (bin != 0) ? BTAmmoBinModelFile(bin) : -1; - int hit = 0; - if (mid >= 0) - hit = BTMissileThrustOf(mid, &tBurn, &tAccel); - if (getenv("BT_PROJ_LOG")) - DEBUG_STREAM << "[projectile] thrust resolve: bin=" << bin - << " modelID=" << mid << " hit=" << hit - << " burn=" << tBurn << " accel=" << tAccel << "\n" << std::flush; - } - for (int i = 0; i < nmiss; ++i) - // DAMAGE + SPLASH ONCE PER SALVO (task #62 fix): the arcade fires ONE - // cluster Missile per trigger, and its zone damage is applied EXACTLY ONCE - // -- DamageZone::TakeDamage (@0041e4e0) is `damageLevel += amount*scale` - // and IGNORES burstCount. The port draws that cluster as N visual rounds; - // damaging on EVERY round applied the hit N times = ~missileCount-x too - // lethal (the "2-shot kill" regression). Only the LEAD round (i==0) - // carries the missile's damageAmount + the cluster splash; the rest are - // VISUAL (damage 0) -- the ripple of tracers, no extra damage. - // - // The lead round carries the PER-MISSILE amount, exactly as the binary's - // ctor prepared it (damageAmount /= missileCount, burstCount = missileCount). - // The salvo total is reconstituted at IMPACT, where the round hands the - // handler a burstCount and the handler applies the hit that many times - // (Mech::TakeDamageMessageHandler @0x4a0423-0x4a04d8, mech.cpp) -- see the - // cluster-burst note at the impact site in mech4.cpp. Do NOT pre-multiply - // here: that would deliver the salvo as one lump on one zone, losing both - // the per-burst zone re-roll and the arcade's hit-count variance. - BTPushProjectile(muzzle, o, target, targetPos, speed, - (i == 0) ? damageData.damageAmount : 0.0f, - &launchVelocity, 1, - subsystemID /*messmgr explosion bundling at impact (task #7)*/, - (i == 0) ? nmiss : 0 /*salvo-lead: cluster splash baseBurst*/, - GetSegmentIndex() /*task #67: launch through the MOUNT frame (torso twist)*/, - (int)damageData.damageType /*missile = Explosive (authentic)*/, - tAccel, tBurn /*#84 thruster*/, - (i == 0) /*ONE Explosion per salvo: the lead only*/); - - // Salvo replication (missile-visibility wave): bump the fire counter + stamp - // the aim point; the extended update record carries both to peer nodes. - // The CALLER's Loaded case makes the two `updateModel |= 1` marks - // (@4bbf05/@4bbf4c == ForceUpdate) that serialize the record. - // Gitea #12 / gotcha #20: the old `simulationFlags |= 0x1` here set the - // engine DelayWatchersFlag -- permanently muting this launcher's audio - // watchers after the first salvo; removed. - { - BTSalvoState &s = BTSalvoOf(this); - ++s.fired; - s.target = targetPos; - s.targetID = (target != 0) - ? ((Entity *)target)->GetEntityID() : EntityID::Null; - } - Check_Fpu(); -} - -//############################################################################# -// WriteUpdateRecord / ReadUpdateRecord (PORT record extension -- missile- -// visibility wave; see the mislanch.hpp banner). The master serializes the -// salvo counter + aim point after the MechWeapon base fields; the replicant -// edge-detects the counter and mirrors the salvo as VISUAL-ONLY rounds -// (damage 0 -- the master's own rounds deliver the damage cross-pod). -// -void - MissileLauncher::WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model) -{ - MechWeapon::WriteUpdateRecord(message, update_model); // @004b9690 (alarm fields) - - MissileLauncher__UpdateRecord *rec = (MissileLauncher__UpdateRecord *)message; - BTSalvoState &s = BTSalvoOf(this); - rec->recordLength = sizeof(MissileLauncher__UpdateRecord); - rec->salvoCounter = s.fired; - rec->salvoRounds = missileCount; - rec->salvoTarget = s.target; - rec->salvoTargetID = s.targetID; -} - -void - MissileLauncher::ReadUpdateRecord(Simulation__UpdateRecord *message) -{ - MechWeapon::ReadUpdateRecord(message); // @004b964c (alarm apply) - - MissileLauncher__UpdateRecord *rec = (MissileLauncher__UpdateRecord *)message; - if (getenv("BT_PROJ_LOG")) - { - static int s_rr = 0; - if (s_rr++ < 200) - DEBUG_STREAM << "[mlrec] len=" << (int)rec->recordLength - << "/" << (int)sizeof(MissileLauncher__UpdateRecord) - << " recID=" << (int)rec->recordID - << " subsys=" << (int)rec->subsystemID - << " ctr=" << rec->salvoCounter - << " n=" << rec->salvoRounds - << " aim=(" << rec->salvoTarget.x << "," << rec->salvoTarget.y - << "," << rec->salvoTarget.z << ")" << std::endl; - } - BTSalvoState &s = BTSalvoOf(this); - if (s.seen < 0) - { - // first sync after spawn/join: adopt the master's count silently -- - // no replay of salvos fired before we could see them. - s.seen = rec->salvoCounter; - } - else if (rec->salvoCounter != s.seen) - { - s.seen = rec->salvoCounter; - - // Mirror ONE salvo (a missed record collapses to one -- catch-up - // replays would draw stale phantom volleys). Rounds fly the same - // authored launch vector + seeker-loft arc as the master's, from - // this replicant's own resolved rack ports, damage 0. - Point3D mz; - GetMuzzlePoint(mz); // @004b9948 - Scalar spd = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x - + launchVelocity.y*launchVelocity.y - + launchVelocity.z*launchVelocity.z); - int n = rec->salvoRounds; - if (n < 1 || n > 40) - n = (missileCount > 0 && missileCount < 40) ? missileCount : 1; - float tBurn = 0.0f, tAccel = 0.0f; // #84: mirror the master's - { // thruster so peer missiles - void *bin = BTWeaponAmmoBin(this); // fly at the same speed - int mid = (bin != 0) ? BTAmmoBinModelFile(bin) : -1; - if (mid >= 0) - BTMissileThrustOf(mid, &tBurn, &tAccel); - } - // #84 -- home the mirror at the LIVE target, as the binary's GHOST - // missiles did. The record carries the master's locked-target entity - // ID; resolve it against THIS node's registry (the victim's local - // master or replicant -- both track the true position here). The - // frozen salvoTarget remains the launch aim + the fallback for point - // fire, unresolved IDs (target died / not yet replicated here), and - // short records from older builds (length-gated: a stale-build record - // has no ID field and its tail would be garbage). - Entity *mirror_tgt = 0; - if (message->recordLength >= (int)sizeof(MissileLauncher__UpdateRecord) - && !(rec->salvoTargetID == EntityID::Null) - && application != 0 && application->GetHostManager() != 0) - { - mirror_tgt = application->GetHostManager()-> - GetEntityPointer(rec->salvoTargetID); - extern int BTIsRegisteredMech(Entity *e); - if (mirror_tgt != 0 && !BTIsRegisteredMech(mirror_tgt)) - mirror_tgt = 0; // resolved to a non-mech: refuse - } - for (int i = 0; i < n; ++i) - BTPushProjectile(mz, owner, mirror_tgt /*#84 live target; 0 -> point*/, - rec->salvoTarget, spd, 0.0f /*VISUAL*/, &launchVelocity, 1, - subsystemID /*per-round detonation resolve on this node*/, - 0, GetSegmentIndex() /*task #67 mount frame*/, - 2 /*Explosive*/, tAccel, tBurn, - (i == 0) /*mirror lead: observers see ONE explosion per salvo*/); - if (getenv("BT_PROJ_LOG")) - DEBUG_STREAM << "[projectile] REPLICANT salvo x" << n - << " at(" << rec->salvoTarget.x << "," << rec->salvoTarget.y - << "," << rec->salvoTarget.z << ")" - << " tgtID=" << rec->salvoTargetID - << " resolved=" << (void *)mirror_tgt << std::endl; - } -} - -//############################################################################# -// TakeDamage / DeathReset -// -// TakeDamage inherits MechWeapon::TakeDamage (vtable slot 6, @004b964c). -// DeathReset chains the ProjectileWeapon ammo/feed reset (@004bbaf8). -// -void MissileLauncher::TakeDamage(Damage &damage) -{ - MechWeapon::TakeDamage(damage); // @004b964c -} - -void MissileLauncher::DeathReset(Logical /*full_reset*/) -{ - // issue #22: the respawn sweep's dispatch vehicle -- chains the authentic - // slot-10 RTIS [T1 vtable +0x28]; ammo itself refills in AmmoBin's own - // DeathReset (the bins are separate roster subsystems). - ProjectileWeapon::ResetToInitialState(); // @004bbaf8 -} - -//############################################################################# -// TestInstance -- inherits the ProjectileWeapon/MechWeapon base check. -// -Logical MissileLauncher::TestInstance() const -{ - return ProjectileWeapon::TestInstance(); -} - - -//===========================================================================// -// WAVE 7 factory bridge -- MissileLauncher (factory case 0xBD0, "BallisticWeapon" -// mislabel). VDATA.h enum: 0xBD0 == MissileLauncherClassID. The real class -// (ctor @004bcff0) is MissileLauncher : ProjectileWeapon (sizeof 0x44C); the -// factory built a BallisticWeapon RECON_SUBSYS stub. alloc 0x44C. Ticks its -// real per-frame sim (charge/ammo/heat, damage split across missileCount); -// FireWeapon consumes a round + recoils but DEFERS the missile spawn (Phase B). -//===========================================================================// -Subsystem *CreateMissileLauncherSubsystem(Mech *owner, int id, void *seg) -{ - Check(sizeof(MissileLauncher) <= 0x44C); - return (Subsystem *) new (Memory::Allocate(0x44C)) - MissileLauncher(owner, id, (MissileLauncher::SubsystemResource *)seg, MissileLauncher::DefaultData); -} + launchVelocity = subsystem_resource->muzzleVelocity; // @0x410 (inherited ProjectileWeapon slot; was aliased "muzzleVelocity") + + // per-salvo count copy + per-missile damage (write the inherited Damage fields) + this->damageData.burstCount = missileCount; // @0x3d4 this[0xf5] (was shadow "salvoCount") + this->damageData.damageAmount /= (Scalar)missileCount; + if (getenv("BT_SPEC_LOG")) + DEBUG_STREAM << "[wspec-msl] " << (GetName() ? GetName() : "?") + << " count=" << missileCount + << " perMissile=" << damageData.damageAmount + << " burstModel=[n/4..n]" << "\n" << std::flush; // @0x3ac this[0xeb] /= missileCount +} + +//############################################################################# +// Destruction +// +// @004bd060 ~MissileLauncher() -- restores the launcher vtable, chains the +// ProjectileWeapon destructor (which releases the AmmoBin connection @00512424). +// +MissileLauncher::~MissileLauncher() +{ + // vtable reset + base ~ProjectileWeapon chaining handled by the compiler. +} + +//############################################################################# +// CreateStreamedSubsystem +// +// @004bd08c Reads MissileCount (+0x1d0) and MuzzleVelocity (+0x1d4) after +// chaining ProjectileWeapon::CreateStreamedSubsystem (@004bc7cc). Stamps the +// streamed-record class/size header bytes, then validates both fields, +// emitting "missing MissileCount!" / "missing MuzzleVelocity!" on failure. +// +Logical MissileLauncher::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) +{ + // chain the ProjectileWeapon record parser (ammo/jam/tracer fields) + if (!ProjectileWeapon::CreateStreamedSubsystem( + resource_file, model_file, model_name, subsystem_name, + subsystem_resource, subsystem_file, directories, passes)) // @004bc7cc + return False; + + // stamp streamed-record header (classID / size bytes @+0x20/+0x24) + // ... (record housekeeping, see @004bd08c) ... + + if (passes == 1) + subsystem_resource->missileCount = -1; // sentinel + + // "MissileCount" (+0x1d0) + if (!model_file->GetEntry(subsystem_name, "MissileCount", + &subsystem_resource->missileCount) + && subsystem_resource->missileCount == -1) + { + DebugStream << subsystem_name << " missing MissileCount!" << endl; // @004bd08c + return False; + } + + // "MuzzleVelocity" (+0x1d4, Vector3D parsed from text) + const char *text = "Unspecified"; + Vector3D &mv = subsystem_resource->muzzleVelocity; + if (!model_file->GetEntry(subsystem_name, "MuzzleVelocity", &text) + && (mv.x == 0.0f && mv.y == 0.0f && mv.z == 0.0f)) + { + DebugStream << subsystem_name << " missing MuzzleVelocity!" << endl; // @004bd08c + return False; + } + if (strcmp(text, "Unspecified") != 0) + { + // FUN_00408944 -- parse a "x y z" Vector3D from the resource text. + float x = 0.0f, y = 0.0f, z = 0.0f; + if (sscanf(text, "%f %f %f", &x, &y, &z) == 3) + { + mv.x = x; mv.y = y; mv.z = z; + } + } + + return True; +} + +//############################################################################# +// FireWeapon (ProjectileWeapon spawn path, @004bcc60) +// +// Builds a streamed-entity spawn descriptor for a Missile (entity ClassID +// 0xBD0), seeded from this weapon's muzzle transform, the explosion model +// resource (this->explosionResourceID @0x3e4), the per-missile Damage block +// (this->damageData @0x3a8) and the owning Mech's target. The descriptor is +// handed to Missile::New (FUN_004bf8bc), which allocates the 0x368-byte +// entity and runs the Missile ctor (@004bf5b4). Heat from firing is added +// to the owner here (this->heatCostToFire -> owner heatLoad) when the weapon +// is heat-bearing (FUN_004ad7d4). +// +// NOTE: this body lives in the ProjectileWeapon address range and is shared +// by MissileLauncher; it is documented here because MISLANCH.HPP declares +// FireWeapon() as the launcher's protected entry point. +// +void MissileLauncher::FireWeapon() +{ + Check(this); + + // Gitea #12: this body is heat + spawn ONLY, per the recovered @004bcc60 + // (part_013.c:8741+): NO viewFireEnable gate, NO ammo pull, NO recoil set. + // All of that lives in the CALLER -- the recovered ProjectileWeaponSimulation + // Loaded case (@4bbec2 view/target denial blip, @4bbee6 bin->FeedAmmo, + // @4bbf51 recoil). The old early-returns here, under a caller that cycled + // the alarm unconditionally, faked a full firing pip cycle on every denied + // shot -- the "missiles cycle but never launch" incident mechanic. + + // THE FIRING HEAT (task #9): heatCostToFire RAW into the launcher's own + // pendingHeat (pre-scaled 1e7-unit resources: SRM6 = 5e7, LRM15 = 6.5e7) + // -- the binary's add @part_013.c:8744-8747, gated on the heatable check. + if (HeatModelActive()) // FUN_004ad7d4 (heat.hpp -> player+0x260 experience gate; issue #2) + { + pendingHeat += heatCostToFire; // HeatSink @0x1C8, raw + } + + // Resolve the muzzle / lead geometry (seeds each flying missile). + Point3D muzzle; + GetMuzzlePoint(muzzle); // @004b9948 + damageData.burstCount = missileCount; // @0x3d4 (was shadow "salvoCount") + + // WAVE 7 Phase B: launch `missileCount` flying projectiles toward the owner's target. + // (Port reconstruction -- the byte-exact world-entity Missile is blocked by the 2007 + // engine Entity base mismatch; see BTPushProjectile / mech4.cpp.) Speed = |launchVelocity| + // (resource MuzzleVelocity); per-missile damage = damageData.damageAmount (already + // divided by missileCount in the ctor). + char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech) + void *target = (o != 0) ? *(void **)(o + 0x388) : 0; // owner's locked target entity (null in bring-up) + Point3D targetPos = (o != 0) ? *(Point3D *)(o + 0x37c) : muzzle; // owner target point + Scalar speed = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x + + launchVelocity.y*launchVelocity.y + + launchVelocity.z*launchVelocity.z); + // Always launch -- BTPushProjectile falls back to gEnemyMech when the owner's target + // slot isn't populated (bring-up); missileCount missiles per salvo. The authored + // MuzzleVelocity VECTOR rides along (missile-arc wave): the round leaves the rack + // on the authored up-tilt, then the seeker loft + steering arc it onto the target. + int nmiss = (missileCount > 0 && missileCount < 40) ? missileCount : 1; + float tBurn = 0.0f, tAccel = 0.0f; // #84 authored thruster + { + void *bin = BTWeaponAmmoBin(this); + int mid = (bin != 0) ? BTAmmoBinModelFile(bin) : -1; + int hit = 0; + if (mid >= 0) + hit = BTMissileThrustOf(mid, &tBurn, &tAccel); + if (getenv("BT_PROJ_LOG")) + DEBUG_STREAM << "[projectile] thrust resolve: bin=" << bin + << " modelID=" << mid << " hit=" << hit + << " burn=" << tBurn << " accel=" << tAccel << "\n" << std::flush; + } + for (int i = 0; i < nmiss; ++i) + // DAMAGE + SPLASH ONCE PER SALVO (task #62 fix): the arcade fires ONE + // cluster Missile per trigger, and its zone damage is applied EXACTLY ONCE + // -- DamageZone::TakeDamage (@0041e4e0) is `damageLevel += amount*scale` + // and IGNORES burstCount. The port draws that cluster as N visual rounds; + // damaging on EVERY round applied the hit N times = ~missileCount-x too + // lethal (the "2-shot kill" regression). Only the LEAD round (i==0) + // carries the missile's damageAmount + the cluster splash; the rest are + // VISUAL (damage 0) -- the ripple of tracers, no extra damage. + // + // The lead round carries the PER-MISSILE amount, exactly as the binary's + // ctor prepared it (damageAmount /= missileCount, burstCount = missileCount). + // The salvo total is reconstituted at IMPACT, where the round hands the + // handler a burstCount and the handler applies the hit that many times + // (Mech::TakeDamageMessageHandler @0x4a0423-0x4a04d8, mech.cpp) -- see the + // cluster-burst note at the impact site in mech4.cpp. Do NOT pre-multiply + // here: that would deliver the salvo as one lump on one zone, losing both + // the per-burst zone re-roll and the arcade's hit-count variance. + BTPushProjectile(muzzle, o, target, targetPos, speed, + (i == 0) ? damageData.damageAmount : 0.0f, + &launchVelocity, 1, + subsystemID /*messmgr explosion bundling at impact (task #7)*/, + (i == 0) ? nmiss : 0 /*salvo-lead: cluster splash baseBurst*/, + GetSegmentIndex() /*task #67: launch through the MOUNT frame (torso twist)*/, + (int)damageData.damageType /*missile = Explosive (authentic)*/, + tAccel, tBurn /*#84 thruster*/, + (i == 0) /*ONE Explosion per salvo: the lead only*/); + + // Salvo replication (missile-visibility wave): bump the fire counter + stamp + // the aim point; the extended update record carries both to peer nodes. + // The CALLER's Loaded case makes the two `updateModel |= 1` marks + // (@4bbf05/@4bbf4c == ForceUpdate) that serialize the record. + // Gitea #12 / gotcha #20: the old `simulationFlags |= 0x1` here set the + // engine DelayWatchersFlag -- permanently muting this launcher's audio + // watchers after the first salvo; removed. + { + BTSalvoState &s = BTSalvoOf(this); + ++s.fired; + s.target = targetPos; + s.targetID = (target != 0) + ? ((Entity *)target)->GetEntityID() : EntityID::Null; + } + Check_Fpu(); +} + +//############################################################################# +// WriteUpdateRecord / ReadUpdateRecord (PORT record extension -- missile- +// visibility wave; see the mislanch.hpp banner). The master serializes the +// salvo counter + aim point after the MechWeapon base fields; the replicant +// edge-detects the counter and mirrors the salvo as VISUAL-ONLY rounds +// (damage 0 -- the master's own rounds deliver the damage cross-pod). +// +void + MissileLauncher::WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model) +{ + MechWeapon::WriteUpdateRecord(message, update_model); // @004b9690 (alarm fields) + + MissileLauncher__UpdateRecord *rec = (MissileLauncher__UpdateRecord *)message; + BTSalvoState &s = BTSalvoOf(this); + rec->recordLength = sizeof(MissileLauncher__UpdateRecord); + rec->salvoCounter = s.fired; + rec->salvoRounds = missileCount; + rec->salvoTarget = s.target; + rec->salvoTargetID = s.targetID; +} + +void + MissileLauncher::ReadUpdateRecord(Simulation__UpdateRecord *message) +{ + MechWeapon::ReadUpdateRecord(message); // @004b964c (alarm apply) + + MissileLauncher__UpdateRecord *rec = (MissileLauncher__UpdateRecord *)message; + if (getenv("BT_PROJ_LOG")) + { + static int s_rr = 0; + if (s_rr++ < 200) + DEBUG_STREAM << "[mlrec] len=" << (int)rec->recordLength + << "/" << (int)sizeof(MissileLauncher__UpdateRecord) + << " recID=" << (int)rec->recordID + << " subsys=" << (int)rec->subsystemID + << " ctr=" << rec->salvoCounter + << " n=" << rec->salvoRounds + << " aim=(" << rec->salvoTarget.x << "," << rec->salvoTarget.y + << "," << rec->salvoTarget.z << ")" << std::endl; + } + BTSalvoState &s = BTSalvoOf(this); + if (s.seen < 0) + { + // first sync after spawn/join: adopt the master's count silently -- + // no replay of salvos fired before we could see them. + s.seen = rec->salvoCounter; + } + else if (rec->salvoCounter != s.seen) + { + s.seen = rec->salvoCounter; + + // Mirror ONE salvo (a missed record collapses to one -- catch-up + // replays would draw stale phantom volleys). Rounds fly the same + // authored launch vector + seeker-loft arc as the master's, from + // this replicant's own resolved rack ports, damage 0. + Point3D mz; + GetMuzzlePoint(mz); // @004b9948 + Scalar spd = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x + + launchVelocity.y*launchVelocity.y + + launchVelocity.z*launchVelocity.z); + int n = rec->salvoRounds; + if (n < 1 || n > 40) + n = (missileCount > 0 && missileCount < 40) ? missileCount : 1; + float tBurn = 0.0f, tAccel = 0.0f; // #84: mirror the master's + { // thruster so peer missiles + void *bin = BTWeaponAmmoBin(this); // fly at the same speed + int mid = (bin != 0) ? BTAmmoBinModelFile(bin) : -1; + if (mid >= 0) + BTMissileThrustOf(mid, &tBurn, &tAccel); + } + // #84 -- home the mirror at the LIVE target, as the binary's GHOST + // missiles did. The record carries the master's locked-target entity + // ID; resolve it against THIS node's registry (the victim's local + // master or replicant -- both track the true position here). The + // frozen salvoTarget remains the launch aim + the fallback for point + // fire, unresolved IDs (target died / not yet replicated here), and + // short records from older builds (length-gated: a stale-build record + // has no ID field and its tail would be garbage). + Entity *mirror_tgt = 0; + if (message->recordLength >= (int)sizeof(MissileLauncher__UpdateRecord) + && !(rec->salvoTargetID == EntityID::Null) + && application != 0 && application->GetHostManager() != 0) + { + mirror_tgt = application->GetHostManager()-> + GetEntityPointer(rec->salvoTargetID); + extern int BTIsRegisteredMech(Entity *e); + if (mirror_tgt != 0 && !BTIsRegisteredMech(mirror_tgt)) + mirror_tgt = 0; // resolved to a non-mech: refuse + } + for (int i = 0; i < n; ++i) + BTPushProjectile(mz, owner, mirror_tgt /*#84 live target; 0 -> point*/, + rec->salvoTarget, spd, 0.0f /*VISUAL*/, &launchVelocity, 1, + subsystemID /*per-round detonation resolve on this node*/, + 0, GetSegmentIndex() /*task #67 mount frame*/, + 2 /*Explosive*/, tAccel, tBurn, + (i == 0) /*mirror lead: observers see ONE explosion per salvo*/); + if (getenv("BT_PROJ_LOG")) + DEBUG_STREAM << "[projectile] REPLICANT salvo x" << n + << " at(" << rec->salvoTarget.x << "," << rec->salvoTarget.y + << "," << rec->salvoTarget.z << ")" + << " tgtID=" << rec->salvoTargetID + << " resolved=" << (void *)mirror_tgt << std::endl; + } +} + +//############################################################################# +// TakeDamage / DeathReset +// +// TakeDamage inherits MechWeapon::TakeDamage (vtable slot 6, @004b964c). +// DeathReset chains the ProjectileWeapon ammo/feed reset (@004bbaf8). +// +void MissileLauncher::TakeDamage(Damage &damage) +{ + MechWeapon::TakeDamage(damage); // @004b964c +} + +void MissileLauncher::DeathReset(Logical /*full_reset*/) +{ + // issue #22: the respawn sweep's dispatch vehicle -- chains the authentic + // slot-10 RTIS [T1 vtable +0x28]; ammo itself refills in AmmoBin's own + // DeathReset (the bins are separate roster subsystems). + ProjectileWeapon::ResetToInitialState(); // @004bbaf8 +} + +//############################################################################# +// TestInstance -- inherits the ProjectileWeapon/MechWeapon base check. +// +Logical MissileLauncher::TestInstance() const +{ + return ProjectileWeapon::TestInstance(); +} + + +//===========================================================================// +// WAVE 7 factory bridge -- MissileLauncher (factory case 0xBD0, "BallisticWeapon" +// mislabel). VDATA.h enum: 0xBD0 == MissileLauncherClassID. The real class +// (ctor @004bcff0) is MissileLauncher : ProjectileWeapon (sizeof 0x44C); the +// factory built a BallisticWeapon RECON_SUBSYS stub. alloc 0x44C. Ticks its +// real per-frame sim (charge/ammo/heat, damage split across missileCount); +// FireWeapon consumes a round + recoils but DEFERS the missile spawn (Phase B). +//===========================================================================// +Subsystem *CreateMissileLauncherSubsystem(Mech *owner, int id, void *seg) +{ + Check(sizeof(MissileLauncher) <= 0x44C); + return (Subsystem *) new (Memory::Allocate(0x44C)) + MissileLauncher(owner, id, (MissileLauncher::SubsystemResource *)seg, MissileLauncher::DefaultData); +}