Supersedes the mislanch change inefc3e9f, which multiplied the lead round by missileCount. That produced roughly the right average total but as ONE lump on ONE zone, with no scatter and no variance -- not what the arcade does. WHAT THE BINARY DOES (all byte-verified): * MissileLauncher ctor @004bcff0: burstCount = missileCount; damageAmount /= missileCount * FireWeapon @004bcc60 spawns exactly ONE Missile -- no loop, missileCount is never read there. The salvo IS one cluster round. * Missile::Perform rolls how many of the cluster connect right before it dispatches (part_013.c:10082): b = Random(n) + n/4, clamped to n -- i.e. between a quarter of the salvo and all of it. * It then dispatches DIRECTLY at the struck entity (FUN_004be078: param_2->Dispatch(&msg)) -- NOT through the shooter's message manager. * Mech::TakeDamageMessageHandler @0x4a0439-0x4a04d8 applies the hit burstCount times, RE-ROLLING the struck zone per burst. Our handler already implements that loop faithfully (mech.cpp, task #80) -- I wrongly believed it was missing, because the KB asserts as [T1] that "burstCount is cosmetic for zone damage". That is half right: DamageZone::TakeDamage really does ignore burstCount (verified @0041e4e0), but the HANDLER honours it by calling that function repeatedly. KB corrected separately. The actual defect was that the projectile impact path hardcoded burstCount = 1, and -- worse -- routed damage through the SubsystemMessageManager, whose consolidation rebuilds the record from a stream carrying only {damageType, damageAmount, subsystemID}. DamageInformation has no burstCount field, so the cluster count was DROPPED in transit no matter what the round carried. So: dispatch projectile damage directly, as the binary does, carrying the rolled burst. This also retires the #84 double explosion architecturally -- the second blast came from the manager's bundled explosion, and projectiles no longer go through the manager at all. The MarkRoundDetonated suppression added inefc3e9fis therefore dead and is removed. ALL-WEAPON-CLASS AUDIT (scratchpad/night8/allweap.{sh,py}), one run, all classes firing at once: EXPLOSIVE (missile) 38 zone applications, bursts spread 1x2,2x6,3x9,4x4,5x5, 6x12 across ELEVEN zones -- the [n/4..n] roll plus the per-burst zone re-roll, i.e. the cluster scattering ENERGY (beam) 8 applications, burst 1 -- unchanged, no regression type4 8 applications, burst 1 -- unchanged COLLISION 0 applications reached armour -- divert intact Explosions: 10 projectile impacts produce no bundled blast; direct-fire still queues its own (11 made). NOT yet verified: cross-pod delivery. Dispatch reroutes to a replicant on its own (and the binary relies on exactly that), but the manager routing is gone, so MP damage should get a two-node run before this ships. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
480 lines
21 KiB
C++
480 lines
21 KiB
C++
//============================================================================//
|
|
// 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 <bt.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(MISLANCH_HPP)
|
|
# include <mislanch.hpp>
|
|
#endif
|
|
#if !defined(MISSILE_HPP)
|
|
# include <missile.hpp>
|
|
#endif
|
|
#if !defined(AMMOBIN_HPP)
|
|
# include <ammobin.hpp>
|
|
#endif
|
|
#if !defined(TESTBT_HPP)
|
|
# include <testbt.hpp>
|
|
#endif
|
|
#include <math.h>
|
|
|
|
// 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*/);
|
|
// #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
|
|
};
|
|
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);
|
|
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: the round ACCELERATES like the binary Missile*/);
|
|
|
|
// 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;
|
|
}
|
|
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;
|
|
}
|
|
|
|
void
|
|
MissileLauncher::ReadUpdateRecord(Simulation__UpdateRecord *message)
|
|
{
|
|
MechWeapon::ReadUpdateRecord(message); // @004b964c (alarm apply)
|
|
|
|
MissileLauncher__UpdateRecord *rec = (MissileLauncher__UpdateRecord *)message;
|
|
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);
|
|
}
|
|
for (int i = 0; i < n; ++i)
|
|
BTPushProjectile(mz, owner, 0 /*no entity: aim point only*/,
|
|
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);
|
|
if (getenv("BT_PROJ_LOG"))
|
|
DEBUG_STREAM << "[projectile] REPLICANT salvo x" << n
|
|
<< " at(" << rec->salvoTarget.x << "," << rec->salvoTarget.y
|
|
<< "," << rec->salvoTarget.z << ")\n" << std::flush;
|
|
}
|
|
}
|
|
|
|
//#############################################################################
|
|
// 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);
|
|
}
|