//============================================================================// // 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 #pragma hdrstop #if !defined(MISSILE_HPP) # include #endif #if !defined(SEEKER_HPP) # include #endif #if !defined(MISTHRST_HPP) # include #endif #if !defined(TESTBT_HPP) # include #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. }