Files
BT411/game/reconstructed/missile.cpp
T
arcattackandClaude Opus 4.8 7b7d465e5e Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.

Layout:
  engine/   MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
            work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
            models) + image codec; the minimal rp/ headers the audio HAL needs
  game/     reconstructed BT logic + surviving-original BT source + fwd shims
            + WinMain launcher
  content/  full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
  docs/     format specs + reconstruction ledgers
  reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
  tools/    MP console emulator + map/resource scanners

One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:03:40 -05:00

318 lines
13 KiB
C++

//============================================================================//
// File: missile.cpp //
// Project: BattleTech //
// Contents: Guided self-propelled projectile entity (Missile : Projectile) //
//----------------------------------------------------------------------------//
// 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; there is NO surviving missile.cpp -- only MISSILE.TCP
// (Missile::TestClass) and PROJTILE.TCP guide the member/method naming. Each
// non-trivial method cites the originating @ADDR. Coverage is partial by
// design (Missile is the lowest-priority of the three classes; it has no HPP).
//
// confident : ctor @004bf5b4 (subsystem roster build), MoveAndCollide skeleton
// @004bef78, WriteUpdateRecord @004bef4c
// best-effort: member names/offsets past the Projectile base, the collision
// + detonation branch (FUN_0042291c / FUN_004be078)
// excluded : the Projectile base ctor @004be1bc and the 0x41xxxx/0x42xxxx
// Entity engine vtable slots
//
// Decoded MoveAndCollide tuning constants (.rdata @004bf594..@004bf5b0):
// 0x40000000 = 2.0f 0x38d1b717 = 1.0e-4f 0x40800000 = 4.0f
// 0x3dcccccd = 0.1f 0x41200000 = 10.0f 0xbf800000 = -1.0f
//
// Helper-function name mapping:
// FUN_004be1bc Projectile base constructor (vtable @00512a5c)
// FUN_004bec34 Seeker::Seeker FUN_004be7c4 MissileThruster::MissileThruster
// FUN_0042291c world collision query FUN_004be078 spawn explosion/Damage entity
// FUN_00421b2c Entity::IntegrateMotion FUN_00421bac Entity::AdvanceModel
// FUN_0042061c Entity::Retire FUN_00402298 operator new
// FUN_00408644 Vector3D = a - b FUN_004086ac scaled add
// FUN_004dd138 sqrt() FUN_0041a1a4 IsDerivedFrom
//
#include <bt.hpp>
#pragma hdrstop
#if !defined(MISSILE_HPP)
# include <missile.hpp>
#endif
#if !defined(SEEKER_HPP)
# include <seeker.hpp>
#endif
#if !defined(MISTHRST_HPP)
# include <misthrst.hpp>
#endif
#if !defined(TESTBT_HPP)
# include <testbt.hpp>
#endif
//
// MoveAndCollide tuning constants (read-only globals in the decomp).
//
static const Scalar MissileLifeWarn = 2.0f; // _DAT_004bf594 age-ratio dead threshold
static const Scalar MissileSteerEps = 1.0e-4f;// _DAT_004bf598/9c/a0 steering deadband
static const Scalar MissileTurnGain = 4.0f; // _DAT_004bf5a4
static const Scalar MissileDriftGain = 0.1f; // _DAT_004bf5a8
static const Scalar MissileMinSpeed2 = 10.0f; // _DAT_004bf5ac/a8 min squared-speed before fizzle
static const Scalar MissileDead = -1.0f; // _DAT_004bf5b0
//###########################################################################
//###########################################################################
// Missile
//###########################################################################
//###########################################################################
//#############################################################################
// Decompiler-artifact stand-ins.
//
static char DAT_00512d48[0x368] = { 0 }; // default streamed-entity record
//#############################################################################
// Shared Data Support
//
Derivation
Missile::ClassDerivations(
&Projectile::ClassDerivations,
"Missile"
);
Receiver::MessageHandlerSet Missile::MessageHandlers;
Simulation::AttributeIndexSet Missile::AttributeIndex;
Missile::SharedData
Missile::DefaultData(
&Missile::ClassDerivations,
Missile::MessageHandlers,
Missile::AttributeIndex,
Missile::StateCount,
&Missile::Make
);
Entity *Missile::Make(MakeMessage * /*creation_message*/)
{
return 0;
}
//#############################################################################
// Allocation
//
// @004bf8bc Missile::New(spawn_context) -- allocate 0x368 bytes and construct
// with the default streamed-entity record &DAT_00512d48.
//
Missile *Missile::New(int spawn_context)
{
Missile *missile = (Missile *)::operator new(0x368); // FUN_00402298
if (missile != 0)
missile = new(missile) Missile(spawn_context, DAT_00512d48); // @004bf5b4
return missile;
}
//#############################################################################
// Construction
//
// @004bf5b4 Missile::Missile(...)
//
// Chains the Projectile base ctor (which copies the launch transform,
// velocity and segment data out of the spawn descriptor), stamps the Missile
// vtable, resolves the detonation model + lifetime out of the entity model
// record, then constructs the two hosted subsystems into a freshly allocated
// 2-entry roster:
// roster[SeekerSubsystem] = new Seeker("Seeker", target, offset)
// roster[MissileThrusterSubsystem] = new MissileThruster("MissleThruster")
//
Missile::Missile(int owner, const char *spawn_descriptor)
:
// Projectile base ctor (Entity transform, velocity, model, segment).
Projectile(owner, spawn_descriptor) // @004be1bc
{
// vtable installed by the compiler (PTR_FUN_00512f2c)
mass = 1.0f; // model mass (avoid /0)
ageFraction = 0.0f; // @0x344 this[0xd1]
targetConnection = ResolveTargetConnection(owner); // @0x34c this[0xd3]
targetOffset = LaunchAimOffset(owner); // @0x350 this[0xd4..0xd6]
age = 0.0f; // @0x35c this[0xd7]
simulationFlags |= 0x10; // this[0x0a] |= 0x10 (guided)
// pull lifetime / thrust / detonation-mode out of the entity model record
const ModelRecord *model = ResolveModel(); // FUN_00407064 chain
lifetime = model->maxTimeOfFlight; // @0x340 this[0xd0] (model +0x44)
thrustScale = (Scalar)Sqrt(model->thrust / mass); // @0x348 this[0xd2] (model +0x48)
detonationFlag = model->detonationMode; // @0x364 this[0xd9] (model +0x50)
// allocate the 2-entry subsystem roster
subsystemCount = SubsystemCount; // @0x124 this[0x49] = 2
subsystems = (Subsystem **)::operator new(SubsystemCount * sizeof(Subsystem *)); // @0x128 this[0x4a]
// slot 0: Seeker (homing guidance) -- "Seeker" @00512e1e
subsystems[SeekerSubsystem] =
(Subsystem *)new Seeker((Missile *)this, 0, 0, (Entity *)targetConnection,
Point3D(targetOffset.x, targetOffset.y, targetOffset.z)); // @004bec34
// slot 1: MissileThruster (propulsion) -- "MissleThruster" @00512e25
subsystems[MissileThrusterSubsystem] =
(Subsystem *)new MissileThruster((Missile *)this, 1, /*resource*/0); // @004be7c4
// Install the guided per-frame integrator as this entity's active Performance.
// The Projectile base ctor installed the plain Projectile::MoveAndCollide;
// the Missile overrides it so the flight entity homes + collides each frame.
// (Without this re-install the missile would coast on the unguided base pass.)
SetPerformance(&Missile::MoveAndCollide); // @004bef78
// hand ourselves to the spawning launcher's projectile slot
// (FUN_0049fb54 chain: launcher->currentProjectile = this) @004bf5b4 tail
}
//#############################################################################
// Destruction
//
// @004bf890 ~Missile() -- restores the Missile vtable, destroys the hosted
// subsystems via the Projectile base destructor (@004be358).
//
Missile::~Missile()
{
// vtable reset + base ~Projectile chaining handled by the compiler.
if (subsystems != 0)
::operator delete(subsystems);
}
//#############################################################################
// Per-frame integration
//
// @004bef78 Missile::MoveAndCollide(time_slice)
//
// 1. Age the missile (age += time_slice) and update ageFraction. Past the
// lifetime / below MissileMinSpeed2 the missile flags itself dead
// (flags |= 1) and is retired by the entity manager.
// 2. Sample the Seeker lead point and the target's relative geometry; apply
// a per-axis steering impulse (turn toward the lead, gain MissileTurnGain,
// deadband MissileSteerEps) into the velocity (this+0x1dc) plus the
// MissileThruster acceleration (this+0x234 -> this+0x1e8).
// 3. Integrate the motion (FUN_00421b2c / FUN_00421bac) and run the world
// collision query FUN_0042291c.
// 4. On a hit, spawn the explosion + Damage entity (FUN_004be078, streamed
// ClassID 0x5C) at the impact point and retire; otherwise advance the
// smoke-trail model and bounds-check.
//
void Missile::MoveAndCollide(Scalar time_slice)
{
age += time_slice; // @0x35c this[0xd7]
// dead if it has out-lived its flight envelope
ageFraction = (age <= lifetime) ? age / lifetime : 1.0f; // @0x344
if (age > lifetime)
simulationFlags |= 1;
// --- drive the hosted subsystems for this slice -----------------------
// In the shipped binary the Seeker / MissileThruster Performance work is
// folded into this integrator (it samples their state out of the roster),
// so rather than ticking them through a separate subsystem loop we advance
// them explicitly here: re-lead the (moving) target and bleed burn time.
Seeker *seeker = (Seeker *)subsystems[SeekerSubsystem];
MissileThruster *thruster = (MissileThruster *)subsystems[MissileThrusterSubsystem];
if (seeker != 0)
seeker->FindTarget(time_slice); // @004beae4 LeadTarget
if (thruster != 0)
thruster->MissileThrusterSimulation(time_slice); // bleed burnTimeRemaining
// --- guidance: build steering toward the Seeker's lead point ----------
Vector3D forward;
GetForward(forward); // FUN_0040d150(.., +0x100)
const ThrusterState *thr =
(const ThrusterState *)this->subsystems[MissileThrusterSubsystem]; // this+0x250
// vector from the missile's facing to the Seeker's lead/aim point (@0x1c4)
Vector3D toLead;
toLead.Subtract(seeker->targetPosition, forward);
// per-axis: turn-rate vs drift gain depending on alignment, squared error
Vector3D steer;
for (int axis = 0; axis < 3; ++axis)
steer[axis] = (fabs(toLead[axis]) <= MissileSteerEps)
? toLead[axis] * MissileDriftGain
: toLead[axis] * MissileTurnGain;
Scalar thrustAccel = (thr != 0) ? thr->acceleration : 0.0f; // FUN_004086ac
velocity.x += steer.x * thrustAccel; // this+0x1dc
velocity.y += steer.y * thrustAccel;
velocity.z += steer.z * thrustAccel;
// thruster acceleration (this+0x234) folded into velocity (this+0x1e8)
thrustVelocity.x += thrusterAxis.x * thrustImpulse; // this+0x1e8 += this+0x234 * this+0x1d0
thrustVelocity.y += thrusterAxis.y * thrustImpulse;
thrustVelocity.z += thrusterAxis.z * thrustImpulse;
// --- integrate + collide ---------------------------------------------
IntegrateMotion(); // FUN_00421b2c
AdvanceModel(time_slice); // FUN_00421bac
Entity *hit = CollisionQuery(time_slice); // FUN_0042291c
if (hit == 0)
{
// no impact this frame -- bounds check vs MissileMinSpeed2 elsewhere
}
else
{
Retire(); // FUN_0042061c
// spawn explosion + Damage entity at impact (FUN_004be078, ClassID 0x5C)
SpawnDetonation(hit);
simulationFlags |= 1;
}
}
//#############################################################################
// WriteUpdateRecord
//
// @004bef4c vtable slot 7 -- chains the Entity base writer (FUN_004225a4),
// stamps record tag 0x78 and appends the detonation/age word (this+0x35c).
//
void Missile::WriteUpdateRecord(UpdateRecord *message, int update_model)
{
Entity::WriteUpdateRecord(message, update_model); // @004225a4
message->recordID = 0x78; // record tag
// the shipped record also appends the age word (this+0x35c) for replication.
}
//#############################################################################
// TestInstance
//
Logical Missile::TestInstance() const
{
return Projectile::TestInstance();
}
//#############################################################################
// Recovered helpers (engine/model plumbing rendered as decomp artifacts).
//
void Missile::GetForward(Vector3D &out)
{
out = Vector3D(0.0f, 0.0f, -1.0f); // FUN_0040d150 (facing -Z)
}
void *Missile::ResolveTargetConnection(int /*owner*/)
{
return 0; // resolved target/launcher link
}
Vector3D Missile::LaunchAimOffset(int /*owner*/)
{
return Vector3D(0.0f, 0.0f, 0.0f);
}
const Missile::ModelRecord *Missile::ResolveModel()
{
// FUN_00407064 chain -- the entity model record (lifetime/thrust/detonation).
static const ModelRecord defaultModel = { 10.0f, 1.0f, 0 };
return &defaultModel;
}
void Missile::SpawnDetonation(Entity * /*hit*/)
{
// FUN_004be078 -- spawn the explosion + Damage entity (streamed ClassID 0x5C)
// at the impact point. Cross-family (entity factory / damage) integration.
}