diff --git a/restoration/source410/BT/MECHWEAP.CPP b/restoration/source410/BT/MECHWEAP.CPP index 7f44896c..1e0acd2c 100644 --- a/restoration/source410/BT/MECHWEAP.CPP +++ b/restoration/source410/BT/MECHWEAP.CPP @@ -135,6 +135,7 @@ MechWeapon::MechWeapon( damageAmount = subsystem_resource->damageAmount; damageType = subsystem_resource->damageType; heatCostToFire = subsystem_resource->heatCostToFire; + explosionResourceID = subsystem_resource->explosionModelFile; targetWithinRange = False; damageData.damageType = damageType; diff --git a/restoration/source410/BT/MECHWEAP.HPP b/restoration/source410/BT/MECHWEAP.HPP index c97110c7..e18b1adb 100644 --- a/restoration/source410/BT/MECHWEAP.HPP +++ b/restoration/source410/BT/MECHWEAP.HPP @@ -213,6 +213,9 @@ Scalar damageAmount; int damageType; Scalar heatCostToFire; + ResourceDescription::ResourceID + explosionResourceID; // binary @0x3e4 -- the projectile / + // explosion GameModel this weapon spawns int targetWithinRange; Damage damageData; Logical rearFiring; diff --git a/restoration/source410/BT/MISLANCH.CPP b/restoration/source410/BT/MISLANCH.CPP index dbb89ecf..daa11cf8 100644 --- a/restoration/source410/BT/MISLANCH.CPP +++ b/restoration/source410/BT/MISLANCH.CPP @@ -19,6 +19,24 @@ # include #endif +#if !defined(MISSILE_HPP) +# include +#endif + +#if !defined(APP_HPP) +# include +#endif + +#if !defined(HOSTMGR_HPP) +# include +#endif + +// +// STAGED launch speed (the binary derives it from the authored +// MuzzleVelocity vector -- the muzzle wave). +// +static const Scalar kMissileLaunchSpeed = 150.0f; // u/s + Derivation MissileLauncher::ClassDerivations( ProjectileWeapon::ClassDerivations, @@ -113,20 +131,73 @@ void } // - // The salvo delivery (CORRECTED to the arcade economy, BT411 task #62): - // the binary fires ONE cluster Missile per trigger and its zone damage - // lands EXACTLY ONCE -- damageData carries the ctor's salvo-split - // per-missile amount with burstCount = missileCount, and the zone armor - // formula ignores burstCount (it feeds only the gyro bounce + the splash - // falloff). A per-round delivery loop was ~missileCount-times too - // lethal (the port's "2-shot kill" regression). INTERIM instant-hit -- - // the Missile entity flight / seeker / proximity fuse / cluster SPLASH - // is the missile wave. + // The salvo launch (binary @004bcc60): ONE cluster Missile entity per + // trigger, spawned through the Mover make machinery -- the MakeMessage's + // resourceID is this weapon's projectile GameModel (explosionResourceID + // @0x3e4), so the Mover base streams the authored mass/drag. The round + // carries the salvo-split cluster damage record (burstCount = + // missileCount rides into the gyro bounce + splash) and homes via its + // Seeker; the proximity fuse delivers the damage ONCE (the arcade + // economy, BT411 task #62). Muzzle = the mech origin STAGED (the + // authored MuzzleVelocity arc through the MOUNT segment frame is the + // muzzle wave). If the projectile model resource is absent the launch + // falls back to the 5.3.16 instant-hit delivery. // if (targetWithinRange && owner != NULL + && owner->GetTargetEntity() != NULL + && getenv("BT_MISSILE_FLIGHT") == NULL) + { + // + // DEFAULT delivery (the 5.3.18a arcade economy): the instant-hit + // cluster message. The flying-Missile path below is complete and + // live-verified through DETONATE, but its DEATH-ROW TEARDOWN still + // page-faults (the 5.3.18 frontier, MISSILE.NOTES.md) -- opt in + // with BT_MISSILE_FLIGHT=1 until the dtor chain is closed. + // + SendDamage(owner->GetTargetEntity(), damageData); + } + else if (targetWithinRange && owner != NULL && owner->GetTargetEntity() != NULL) { - SendDamage(owner->GetTargetEntity(), damageData); + // + // The MakeMessage's resourceID must be a resource FAMILY id -- the + // Mover base ctor runs its own SearchList(resourceID, GameModel), + // and SearchList CRASHES (engine @0x414938) when the id isn't in + // the top-level directory (member resources resolve only through + // their family head). The streamed explosionModelFile is a small + // INDEX (SRM6 reads 17) into a model list -- decoding it into the + // real projectile family is the projectile-model brick. Until + // then the round spawns on the OWNER's family (its GameModel + // member provably streams every boot -- the mech model params); + // our staged flight integration reads none of the Mover mass/drag. + // + { + HostManager *host_manager = application->GetHostManager(); + Check(host_manager); + + Origin muzzle = owner->localOrigin; + Mover::MakeMessage + spawn_round( + Entity::MakeMessageID, + sizeof(Mover::MakeMessage), + EntityID(host_manager->GetLocalHostID()), + (Entity::ClassID)MissileClassID, + EntityID::Null, + owner->GetResourceID(), + Entity::DefaultFlags, + muzzle, + Motion::Identity, + Motion::Identity + ); + Missile *round = new Missile(&spawn_round); + Register_Object(round); + round->Launch( + owner, + owner->GetTargetEntity(), + damageData, + kMissileLaunchSpeed + ); + } } if (getenv("BT_MECH_LOG")) diff --git a/restoration/source410/BT/MISSILE.CPP b/restoration/source410/BT/MISSILE.CPP index 609a1559..11e8a6d1 100644 --- a/restoration/source410/BT/MISSILE.CPP +++ b/restoration/source410/BT/MISSILE.CPP @@ -1,10 +1,19 @@ //===========================================================================// -// Project: BattleTech // +// File: missile.cpp // +// Project: BattleTech Brick: Mech weapons // +// Contents: Missile -- the guided self-propelled projectile entity // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. // // All Rights reserved worldwide // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // //===========================================================================// +// +// RECONSTRUCTED against the binary via the BT411 decomp (ctor @004bf5b4, +// MoveAndCollide @004bef78, allocator @004bf8bc) and the surviving +// SEEKER.HPP / MISTHRST.HPP. See MISSILE.NOTES.md for the staged pieces +// (flight constants pending the missile GameModel record decode; world +// collision + explosion entity are the render/effects wave). +// #include #pragma hdrstop @@ -13,6 +22,34 @@ # include #endif +#if !defined(SEEKER_HPP) +# include +#endif + +#if !defined(MISTHRST_HPP) +# include +#endif + +#if !defined(MECH_HPP) +# include +#endif + +#if !defined(RANDOM_HPP) +# include +#endif + +// +// Guidance tuning (binary .rdata @004bf594..@004bf5b0, BT411-decoded). +// STAGED-DEFAULT flight envelope: lifetime / thrust come from the missile +// GameModel record (model +0x44 / +0x48) once that layout is decoded; until +// then the defaults below fly a believable SRM (fast, short-burn). +// +static const Scalar MissileSteerEps = 1.0e-4f; // _DAT_004bf598 steering deadband +static const Scalar MissileTurnGain = 4.0f; // _DAT_004bf5a4 turn rate gain (1/s) +static const Scalar MissileDriftGain = 0.1f; // _DAT_004bf5a8 +static const Scalar DefaultLifetime = 10.0f; // model +0x44 (staged default) +static const Scalar DefaultFuseRadius = 20.0f; // proximity fuse (staged) + Derivation Missile::ClassDerivations( Projectile::ClassDerivations, @@ -29,8 +66,259 @@ Missile::SharedData ); Missile* - Missile::Make(MakeMessage *) + Missile::Make(MakeMessage *creation_message) { - Fail("Missile::Make -- missile.cpp not yet reconstructed"); - return NULL; + return new Missile(creation_message); +} + +// +//############################################################################# +// Construction (binary @004bf5b4). Chains the Projectile base (Entity +// transform + Mover motion/model stream -- the MakeMessage's resourceID is +// the launcher's explosion/projectile GameModel). The subsystem roster is +// built in Launch(), which carries the target the binary's spawn descriptor +// delivered to the ctor. +//############################################################################# +// +Missile::Missile( + MakeMessage *creation_message, + SharedData &shared_data +): + Projectile(creation_message, shared_data) +{ + Check(creation_message); + + lifetime = DefaultLifetime; + ageFraction = 0.0f; + age = 0.0f; + detonationFlag = 0; + flightVelocity = Vector3D(0.0f, 0.0f, 0.0f); + fuseRadius = DefaultFuseRadius; + launcherEntityID = EntityID::Null; + telemetryCountdown = 0; + + Check_Fpu(); +} + +Missile::~Missile() +{ + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[msl] dtor (death row fry)" << endl << flush; + } +} + +Logical + Missile::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +// Launch -- seed the flight (the binary ctor tail + spawn-descriptor +// unpack). Builds the two-subsystem roster (Seeker @004bec34 with the +// homing target, MissileThruster @004be7c4), aims the initial velocity at +// the target and installs the guided integrator as the per-frame +// Performance. Tail = SetValidFlag(), like every 1995 entity. +//############################################################################# +// +void + Missile::Launch( + Mech *launcher, + Entity *target, + Damage &damage, + Scalar launch_speed + ) +{ + Check(this); + Check(launcher); + + launcherEntityID = launcher->GetEntityID(); + damageData = damage; + + // + // The 2-entry subsystem roster (binary this[0x49] = 2). + // + subsystemCount = SubsystemCount; + subsystemArray = new Subsystem *[SubsystemCount]; + Register_Pointer(subsystemArray); + subsystemArray[SeekerSubsystem] = + new Seeker(this, SeekerSubsystem, NULL, target, + Point3D(0.0f, 0.0f, 0.0f)); + Register_Object(subsystemArray[SeekerSubsystem]); + subsystemArray[MissileThrusterSubsystem] = + new MissileThruster(this, MissileThrusterSubsystem, NULL); + Register_Object(subsystemArray[MissileThrusterSubsystem]); + + // + // Initial velocity: straight at the target's current position (the + // authored MuzzleVelocity up-tilt arc through the MOUNT frame is the + // muzzle wave -- the seeker converges either way). + // + if (target != NULL) + { + Vector3D aim; + aim.Subtract( + target->localOrigin.linearPosition, + localOrigin.linearPosition + ); + Scalar range = aim.Length(); + if (range > 0.01f) + { + flightVelocity.x = aim.x / range * launch_speed; + flightVelocity.y = aim.y / range * launch_speed; + flightVelocity.z = aim.z / range * launch_speed; + } + } + + SetPerformance(&Missile::MoveAndCollide); + AlwaysExecute(); + SetValidFlag(); + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[msl] LAUNCH from " << launcherEntityID + << " at (" << localOrigin.linearPosition.x + << "," << localOrigin.linearPosition.y + << "," << localOrigin.linearPosition.z + << ") spd=" << launch_speed + << " dmg=" << damageData.damageAmount + << "x" << damageData.burstCount << endl << flush; + } +} + +// +//############################################################################# +// MoveAndCollide (binary @004bef78) -- the guided per-frame integrator. +// +// 1. Age; past the lifetime the round fizzles (death row, no damage). +// 2. Re-lead the target (Seeker::FindTarget) + bleed the thruster burn. +// 3. Steer the velocity toward the Seeker's lead point (turn gain 4.0, +// deadband 1e-4) and integrate the position. +// 4. Proximity fuse: inside fuseRadius of the aim point the round +// DETONATES -- the cluster damage record lands on the target (random +// hull zone interim, same as the direct path) and the round retires. +// World-geometry collision (@0042291c) + the explosion entity +// (@004be078, ClassID 0x5C) are the render/effects wave. +//############################################################################# +// +void + Missile::MoveAndCollide(Scalar time_slice) +{ + Check(this); + + age += time_slice; + ageFraction = (age <= lifetime) ? age / lifetime : 1.0f; + if (age > lifetime) + { + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[msl] fizzle (lifetime) at (" + << localOrigin.linearPosition.x << "," + << localOrigin.linearPosition.y << "," + << localOrigin.linearPosition.z << ")" << endl << flush; + } + CondemnToDeathRow(); + Check_Fpu(); + return; + } + + Seeker *seeker = + (Seeker *)subsystemArray[SeekerSubsystem]; + MissileThruster *thruster = + (MissileThruster *)subsystemArray[MissileThrusterSubsystem]; + Check(seeker); + Check(thruster); + + // + // The hosted subsystems are advanced FROM the integrator (the binary + // folds their work into this pass -- no separate roster tick). + // + seeker->FindTarget(time_slice); + thruster->MissileThrusterSimulation(time_slice); + + // + // Steering: bend the velocity toward the seeker's lead point at + // MissileTurnGain per second, holding speed; the thruster adds pace + // while burning. + // + Vector3D toLead; + toLead.Subtract(seeker->targetPosition, localOrigin.linearPosition); + Scalar range = toLead.Length(); + + Scalar speed = flightVelocity.Length(); + if (thruster->burnTimeRemaining > 0.0f) + { + speed += thruster->thrusterAcceleration * time_slice; + } + + if (range > MissileSteerEps) + { + Scalar blend = MissileTurnGain * time_slice; + if (blend > 1.0f) + { + blend = 1.0f; + } + Vector3D desired; + desired.x = toLead.x / range * speed; + desired.y = toLead.y / range * speed; + desired.z = toLead.z / range * speed; + flightVelocity.x += (desired.x - flightVelocity.x) * blend; + flightVelocity.y += (desired.y - flightVelocity.y) * blend; + flightVelocity.z += (desired.z - flightVelocity.z) * blend; + } + + localOrigin.linearPosition.x += flightVelocity.x * time_slice; + localOrigin.linearPosition.y += flightVelocity.y * time_slice; + localOrigin.linearPosition.z += flightVelocity.z * time_slice; + localToWorld = localOrigin; + + // + // The proximity fuse (the BANKED seeker notes: fuse on the seeker's + // rangeToTarget). + // + seeker->rangeToTarget = range; + if (range <= fuseRadius && seeker->targetEntity != NULL) + { + Entity *victim = seeker->targetEntity; + int zone = -1; + if (victim->damageZoneCount > 0) + { + zone = Random(victim->damageZoneCount); + } + Entity::TakeDamageMessage + hit( + Entity::TakeDamageMessageID, + sizeof(Entity::TakeDamageMessage), + launcherEntityID, + zone, + damageData + ); + victim->Dispatch(&hit); + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[msl] DETONATE on zone " << zone + << " amount=" << damageData.damageAmount + << " burst=" << damageData.burstCount + << " after " << age << "s" << endl << flush; + } + CondemnToDeathRow(); + Check_Fpu(); + return; + } + + if (getenv("BT_MECH_LOG") && --telemetryCountdown <= 0) + { + telemetryCountdown = 30; + DEBUG_STREAM << "[msl] t=" << age + << " pos=(" << localOrigin.linearPosition.x + << "," << localOrigin.linearPosition.y + << "," << localOrigin.linearPosition.z + << ") range=" << range + << " spd=" << speed << endl << flush; + } + + Check_Fpu(); } diff --git a/restoration/source410/BT/MISSILE.HPP b/restoration/source410/BT/MISSILE.HPP index 34e6c441..fab68f74 100644 --- a/restoration/source410/BT/MISSILE.HPP +++ b/restoration/source410/BT/MISSILE.HPP @@ -11,6 +11,12 @@ // All Rights reserved worldwide // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // //===========================================================================// +// +// STAGED header -- no missile.hpp survives (only the MISSILE.TCP fragment). +// Interface reconstructed from the binary (ctor @004bf5b4, integrator +// @004bef78, vtable @00512f2c, 0x368 bytes) and the surviving SEEKER.HPP / +// MISTHRST.HPP that name the two hosted subsystems. See MISSILE.NOTES.md. +// #if !defined(MISSILE_HPP) # define MISSILE_HPP @@ -19,6 +25,15 @@ # include # endif +# if !defined(DAMAGE_HPP) +# include +# endif + +//##################### Forward Class Declarations ####################### + class Mech; + class Seeker; + class MissileThruster; + //########################################################################### //############################## Missile ################################ //########################################################################### @@ -33,6 +48,40 @@ static Derivation ClassDerivations; static SharedData DefaultData; + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Subsystem roster (ctor build order @004bf5b4: Seeker first, + // MissileThruster second -- MISSILE.TCP's GetSubsystem indices). + // + public: + enum { + SeekerSubsystem = 0, + MissileThrusterSubsystem = 1, + SubsystemCount = 2 // binary this[0x49] == 2 + }; + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Model Support + // + public: + typedef void + (Missile::*Performance)(Scalar time_slice); + + // + // @004bef78 -- the per-frame integrate-guidance-and-collide pass: + // age the round, re-lead the target (Seeker), bleed the thruster + // burn, steer + integrate, then the proximity fuse / lifetime + // retire. + // + void + MoveAndCollide(Scalar time_slice); + + void + SetPerformance(Performance performance) + { + Check(this); + activePerformance = (Simulation::Performance)performance; + } + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Construction and Destruction // @@ -48,6 +97,22 @@ ); ~Missile(); + // + // Flight seed, called by the launcher right after construction: the + // launcher link (the inflictor carried into the hit), the homing + // target, the salvo-split cluster damage record, and the launch + // speed. Builds the Seeker/MissileThruster roster (the binary's + // spawn descriptor carried these into the ctor; our MakeMessage + // doesn't, so the roster waits for the target). + // + void + Launch( + Mech *launcher, + Entity *target, + Damage &damage, + Scalar launch_speed + ); + Logical TestInstance() const; @@ -64,10 +129,23 @@ ); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Local Data + // Local Data (binary offsets past the Projectile base: 0x340..0x364) // protected: - int reserved[16]; + Scalar lifetime; // @0x340 max time-of-flight + Scalar ageFraction; // @0x344 age/lifetime, clamped 1.0 + Scalar age; // @0x35c accumulated flight time + int detonationFlag; // @0x364 proximity/contact mode + + // + // Staged flight state (the binary keeps these in the Entity motion + // block; named members until the raw-offset motion wave). + // + Vector3D flightVelocity; + Scalar fuseRadius; + EntityID launcherEntityID; + Damage damageData; + int telemetryCountdown; }; #endif diff --git a/restoration/source410/BT/MISSILE.NOTES.md b/restoration/source410/BT/MISSILE.NOTES.md new file mode 100644 index 00000000..291d7c9d --- /dev/null +++ b/restoration/source410/BT/MISSILE.NOTES.md @@ -0,0 +1,73 @@ +# MISSILE.CPP / SEEKER.CPP / MISTHRST.CPP / PROJTILE.CPP — the flying rounds + +**Status: PARTIAL (5.3.18) — the guided flight loop is LIVE-VERIFIED through +DETONATE (spawn → fly → home → proximity fuse → cluster damage lands once), +but the death-row TEARDOWN page-faults, so the spawn path is opt-in +(`BT_MISSILE_FLIGHT=1`); the default delivery remains the 5.3.18a instant-hit +cluster message.** + +## Interfaces + +- `SEEKER.HPP` + `MISTHRST.HPP` are AUTHENTIC survivors (CODE/BT/BT/). The + Subsystem base chain is the NAME ctor (binary FUN_0041c52c) with the + shipped strings "Seeker" @00512e1e and **"MissleThruster"** @00512e25 (the + 1995 misspelling is authentic — keep it). +- `MISSILE.HPP` / `PROJTILE.HPP` are STAGED (only MISSILE.TCP / PROJTILE.TCP + survive). Binary chain: Entity → Projectile (@004be1bc, 0x340) → Missile + (@004bf5b4, vtable @00512f2c, 0x368). Our staged chain matches: + Projectile : Mover (make-message path), Missile : Projectile. + +## What is verified live (BT_MISSILE_FLIGHT=1 fight run) + +ONE cluster Missile entity per SRM salvo, spawned through the real Mover make +machinery mid-weapon-sim (the Entity ctor self-registers — the engine +tolerates mid-roster-walk creation, as the binary did); flight telemetry +shows thruster acceleration (150→153 u/s) and closing range; the proximity +fuse (rangeToTarget <= 20u staged) delivers `damageData` (salvo-split amount, +burstCount=missileCount) exactly once via TakeDamageMessage with the LAUNCHER +as inflictor; lifetime fizzle retires unexploded rounds. Guidance constants +are binary-decoded (steer deadband 1e-4, turn gain 4.0, seeker loft +200/50/300/0.1 — .rdata @004bec18/@004bf594). + +## THE OPEN CRASH (the 5.3.18 frontier) + +After the first detonated round's `~Missile` completes (dtor log proves it), +the frame page-faults with EIP IN THE HEAP (0x9FEAEF-style) — something +calls through freed missile memory or a base-dtor releases a structure +wrongly. The Seeker/MissileThruster are the FIRST SUBSYSTEMS and the Missile +the FIRST ENTITY ever DESTROYED in the reconstructed sim — the whole +~Entity/~Mover/~Subsystem teardown path is virgin. Checked already: NOT +AlwaysExecute (flag-only), NOT interestZoneID (message base NULLs it), NOT +the subsystemArray idiom (matches ~Entity's expectations exactly), Sensor +holds no pointers. Latent bug FIXED on the way: the Subsystem NAME ctor left +`damageZone` UNINITIALIZED (the resource ctor NULLs it) — garbage for every +name-built subsystem. Next suspects: ~Mover's collision/interest release +(collisionLists array delete), the executive's per-frame entity chain vs +mid-frame delete (FryDeathRow one-per-frame), the host-manager update-record +writer touching a condemned entity. + +## Engine truths learned (load-bearing) + +- `ResourceFile::SearchList(id, type)` CRASHES (@0x414938) when `id` is not + in the top-level directory — it derefs its inner FindResourceDescription + unconditionally. `FindResourceDescription(int)` is the NULL-safe probe. +- Resource IDs in MakeMessages must be FAMILY ids: the Mover base ctor runs + its own `SearchList(resourceID, GameModelResourceType)`, and member + resources resolve only through their family head. +- The streamed `explosionModelFile` is a small INDEX (SRM6 reads 17), a + model-list indirection like voltageSourceIndex/ammoBinIndex — decoding it + into the real projectile family is the projectile-model brick. Until then + the round spawns on the OWNER's family (GameModel member provably present; + our staged flight reads none of the Mover mass/drag). + +## Staged / deferred + +- Muzzle = mech origin; the authored MuzzleVelocity arc through the MOUNT + segment world frame (fire builder @0x4bcc60:8758-64, z NEGATED + mech + velocity) is the muzzle wave. +- Flight envelope defaults (lifetime 10s, burn 2s @120 u/s², launch 150 u/s, + fuse 20u) pending the missile GameModel record decode (model +0x44 + lifetime, +0x48 thrust, +0x50 detonation mode). +- Cluster SPLASH + explosion entity (ClassID 0x5C @004be078), world-geometry + collision (@0042291c), target-velocity intercept lead, Missile + WriteUpdateRecord (tag 0x78) for MP replication. diff --git a/restoration/source410/BT/MISTHRST.CPP b/restoration/source410/BT/MISTHRST.CPP new file mode 100644 index 00000000..a87dfdd2 --- /dev/null +++ b/restoration/source410/BT/MISTHRST.CPP @@ -0,0 +1,135 @@ +//===========================================================================// +// File: misthrst.cpp // +// Project: BattleTech Brick: Mech weapons // +// Contents: MissileThruster -- the missile's propulsion subsystem // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// +// +// The class interface is the AUTHENTIC surviving CODE/BT/BT/MISTHRST.HPP. +// Bodies reconstructed via the BT411 decomp (ctor @004be7c4). The Subsystem +// base chain is the NAME ctor with the shipped string "MissleThruster" +// @00512e25 (the 1995 misspelling is AUTHENTIC -- keep it). No streamed +// resource in the pod content: the flight envelope defaults below are +// STAGED until the missile GameModel record decode (see MISSILE.NOTES.md). +// + +#include +#pragma hdrstop + +#if !defined(MISTHRST_HPP) +# include +#endif + +#if !defined(MISSILE_HPP) +# include +#endif + +// +// STAGED-DEFAULT burn envelope (a believable SRM: short hard burn). +// +static const Scalar DefaultBurnTime = 2.0f; +static const Scalar DefaultThrusterAcceleration = 120.0f; // u/s^2 +static const Scalar DefaultThrusterClimb = 0.0f; + +Derivation + MissileThruster::ClassDerivations( + Subsystem::ClassDerivations, + "MissileThruster" + ); + +const MissileThruster::IndexEntry + MissileThruster::AttributePointers[] = +{ + ATTRIBUTE_ENTRY(MissileThruster, BurnTimeRemaining, burnTimeRemaining), + ATTRIBUTE_ENTRY(MissileThruster, MaxThrusterRotationRate, maxThrusterRotationRate), + ATTRIBUTE_ENTRY(MissileThruster, ThrusterAcceleration, thrusterAcceleration) +}; + +MissileThruster::AttributeIndexSet + MissileThruster::AttributeIndex( + ELEMENTS(MissileThruster::AttributePointers), + MissileThruster::AttributePointers, + Subsystem::AttributeIndex + ); + +MissileThruster::SharedData + MissileThruster::DefaultData( + MissileThruster::ClassDerivations, + Subsystem::MessageHandlers, + MissileThruster::AttributeIndex, + MissileThruster::StateCount + ); + +// +//############################################################################# +// Construction (binary @004be7c4). The const burn/thrust envelope streams +// from the missile resource in the binary; staged defaults here. +//############################################################################# +// +MissileThruster::MissileThruster( + Missile *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource +): + Subsystem( + owner, + subsystem_ID, + "MissleThruster", + (RegisteredClass::ClassID)ThrusterClassID, + DefaultData + ), + thrusterAcceleration( + (subsystem_resource != NULL) + ? subsystem_resource->thrusterAcceleration + : DefaultThrusterAcceleration + ), + thrusterClimb( + (subsystem_resource != NULL) + ? subsystem_resource->thrusterClimb + : DefaultThrusterClimb + ) +{ + Check(owner); + + burnTimeRemaining = (subsystem_resource != NULL) + ? subsystem_resource->burnTime + : DefaultBurnTime; + maxThrusterRotationRate = 0.0f; + + Check_Fpu(); +} + +MissileThruster::~MissileThruster() +{ +} + +Logical + MissileThruster::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +// MissileThrusterSimulation -- bleed the burn. (Driven from the Missile's +// MoveAndCollide integrator, not a roster Performance -- the binary folds +// the thruster work into the integrate pass.) +//############################################################################# +// +void + MissileThruster::MissileThrusterSimulation(Scalar time_slice) +{ + Check(this); + + if (burnTimeRemaining > 0.0f) + { + burnTimeRemaining -= time_slice; + if (burnTimeRemaining < 0.0f) + { + burnTimeRemaining = 0.0f; + } + } +} diff --git a/restoration/source410/BT/PROJTILE.CPP b/restoration/source410/BT/PROJTILE.CPP index 26a577ac..92e09898 100644 --- a/restoration/source410/BT/PROJTILE.CPP +++ b/restoration/source410/BT/PROJTILE.CPP @@ -1,10 +1,20 @@ //===========================================================================// -// Project: BattleTech // +// File: projtile.cpp // +// Project: BattleTech Brick: Mech weapons // +// Contents: Projectile -- the flying-round entity base (Missile derives) // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. // // All Rights reserved worldwide // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // //===========================================================================// +// +// PARTIAL (binary ctor @004be1bc, vtable @00512a5c, 0x340 bytes): the base +// chains the Mover make path -- the MakeMessage's resourceID is the +// projectile GameModel, so the Mover ctor streams the authored mass / drag / +// inertia. The tracer/ballistic body (the unguided MoveAndCollide, smoke +// trail, PROJTILE.TCP surface) joins the autocannon wave; the Missile +// subclass carries the only flying rounds in the 4.10 pod content. +// #include #pragma hdrstop @@ -29,8 +39,27 @@ Projectile::SharedData ); Projectile* - Projectile::Make(MakeMessage *) + Projectile::Make(MakeMessage *creation_message) { - Fail("Projectile::Make -- projtile.cpp not yet reconstructed"); - return NULL; + return new Projectile(creation_message); +} + +Projectile::Projectile( + MakeMessage *creation_message, + SharedData &shared_data +): + Mover(creation_message, shared_data) +{ + Check(creation_message); + Check_Fpu(); +} + +Projectile::~Projectile() +{ +} + +Logical + Projectile::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); } diff --git a/restoration/source410/BT/SEEKER.CPP b/restoration/source410/BT/SEEKER.CPP new file mode 100644 index 00000000..663a6b91 --- /dev/null +++ b/restoration/source410/BT/SEEKER.CPP @@ -0,0 +1,195 @@ +//===========================================================================// +// File: seeker.cpp // +// Project: BattleTech Brick: Mech weapons // +// Contents: Seeker -- the missile's homing-guidance subsystem // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// +// +// The class interface is the AUTHENTIC surviving CODE/BT/BT/SEEKER.HPP +// (07/12/95 GDU). Bodies reconstructed from the binary via the BT411 +// decomp: ctor @004bec34, LeadTarget @004beae4 (constants .rdata +// @004bec18..28). The Subsystem base chain is the NAME ctor (binary +// FUN_0041c52c) with the shipped string "Seeker" @00512e1e. +// + +#include +#pragma hdrstop + +#if !defined(SEEKER_HPP) +# include +#endif + +#if !defined(MISSILE_HPP) +# include +#endif + +// +// Lead/guidance tuning (binary .rdata, BT411-decoded). +// +static const Scalar SeekerLeadMinRange = 200.0f; // _DAT_004bec18 +static const Scalar SeekerLeadFloor = 50.0f; // _DAT_004bec20 +static const Scalar SeekerLeadMaxClamp = 300.0f; // _DAT_004bec24 +static const Scalar SeekerLeadCoef = 0.1f; // _DAT_004bec28 (loft gain) + +Derivation + Seeker::ClassDerivations( + Subsystem::ClassDerivations, + "Seeker" + ); + +const Seeker::IndexEntry + Seeker::AttributePointers[] = +{ + ATTRIBUTE_ENTRY(Seeker, TargetPosition, targetPosition), + ATTRIBUTE_ENTRY(Seeker, RangeToTarget, rangeToTarget), + ATTRIBUTE_ENTRY(Seeker, RangeFromCreation, rangeFromCreation) +}; + +Seeker::AttributeIndexSet + Seeker::AttributeIndex( + ELEMENTS(Seeker::AttributePointers), + Seeker::AttributePointers, + Subsystem::AttributeIndex + ); + +Seeker::SharedData + Seeker::DefaultData( + Seeker::ClassDerivations, + Subsystem::MessageHandlers, + Seeker::AttributeIndex, + Seeker::StateCount + ); + +// +//############################################################################# +// Construction (binary @004bec34): snapshot the firing geometry -- the +// creation point, the homing target + aim offset, and the initial ranges. +//############################################################################# +// +Seeker::Seeker( + Missile *owner, + int subsystem_ID, + SubsystemResource * /*subsystem_resource -- none streamed; the binary + chains the NAME ctor*/, + Entity *target, + const Point3D offset +): + Subsystem( + owner, + subsystem_ID, + "Seeker", + (RegisteredClass::ClassID)SeekerClassID, + DefaultData + ), + creationPoint(owner->localOrigin.linearPosition), + targetOffset(offset) +{ + Check(owner); + + targetEntity = target; + + if (targetEntity == NULL) + { + targetPosition = targetOffset; + } + else + { + targetPosition.x = + targetOffset.x + targetEntity->localOrigin.linearPosition.x; + targetPosition.y = + targetOffset.y + targetEntity->localOrigin.linearPosition.y; + targetPosition.z = + targetOffset.z + targetEntity->localOrigin.linearPosition.z; + } + + Vector3D delta; + delta.Subtract(targetPosition, creationPoint); + rangeToTarget = delta.Length(); + startingRangeToTarget = rangeToTarget; + rangeFromCreation = 0.0f; + + Check_Fpu(); +} + +Seeker::~Seeker() +{ +} + +Logical + Seeker::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +// LeadTarget (binary @004beae4): re-aim at the (moving) target each slice -- +// refresh the aim point from the target's live position + offset, then past +// SeekerLeadMinRange loft the aim upward by SeekerLeadCoef x the clamped +// excess (the authored missile arc). The target-velocity intercept term is +// STAGED (the binary projects along the target's Mover velocity; our +// per-frame refresh converges on live targets headlessly). +//############################################################################# +// +void + Seeker::LeadTarget() +{ + Check(this); + + if (targetEntity == NULL) + { + return; + } + + targetPosition.x = + targetOffset.x + targetEntity->localOrigin.linearPosition.x; + targetPosition.y = + targetOffset.y + targetEntity->localOrigin.linearPosition.y; + targetPosition.z = + targetOffset.z + targetEntity->localOrigin.linearPosition.z; + + Vector3D toMissile; + toMissile.Subtract( + targetPosition, + GetEntity()->localOrigin.linearPosition + ); + Scalar range = toMissile.Length(); + + if (range > SeekerLeadMinRange) + { + Scalar lead = range - SeekerLeadMinRange; + if (toMissile.y >= SeekerLeadFloor && lead > SeekerLeadMaxClamp) + { + lead = SeekerLeadMaxClamp; + } + targetPosition.y += SeekerLeadCoef * lead; + } + + rangeToTarget = range; + + Vector3D fromCreation; + fromCreation.Subtract( + GetEntity()->localOrigin.linearPosition, + creationPoint + ); + rangeFromCreation = fromCreation.Length(); +} + +void + Seeker::FindTarget(Scalar /*time_slice*/) +{ + Check(this); + LeadTarget(); +} + +void + Seeker::ResetToInitialState() +{ + Check(this); + rangeToTarget = 0.0f; + rangeFromCreation = 0.0f; + startingRangeToTarget = 0.0f; +} diff --git a/restoration/source410/MUNGA/SUBSYSTM.CPP b/restoration/source410/MUNGA/SUBSYSTM.CPP index 21a49b6a..6d88799b 100644 --- a/restoration/source410/MUNGA/SUBSYSTM.CPP +++ b/restoration/source410/MUNGA/SUBSYSTM.CPP @@ -79,6 +79,12 @@ Subsystem::Subsystem( Register_Pointer(subsystemName); Str_Copy(subsystemName, subsystem_name, name_size); segmentIndex = -1; + // + // (Was UNINITIALIZED in this ctor variant only -- the resource ctor + // NULLs it. Garbage here is a latent crash for any name-built + // subsystem whose dtor path or damage query walks the zone pointer.) + // + damageZone = NULL; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~