//============================================================================// // File: seeker.cpp // // Project: BattleTech // // Contents: A subsystem which guides, seeks out and finds a target // //----------------------------------------------------------------------------// // 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 (@004bec34-@004bee70); member / method names follow // the surviving SEEKER.HPP and MISSILE.TCP. Each non-trivial method cites the // originating @ADDR. Hex float constants converted to decimal: // 0x3c8efa35 = 0.0174533f (PI/180, deg->rad) @004be8b8 // 0x43480000 = 200.0f 0x42480000 = 50.0f 0x43960000 = 300.0f // // Coverage: // confident : ctor @004bec34, LeadTarget @004beae4, dtor thunk @004bf890-chain // best-effort: FindTarget (folded into Missile guidance; see header note), // ResetToInitialState / TestInstance (inherit Subsystem base) // excluded : the 0x41xxxx engine vtable slots (Subsystem base behaviour) // // Helper-function name mapping (engine internals referenced by the decomp): // FUN_0041c52c Subsystem base constructor (owner, id, name, sharedData) // FUN_00408644 Vector3D = a - b (subtract) // FUN_004085ec Vector3D += b (accumulate) // FUN_004086ac Vector3D = base + dir*scale (scaled add) // FUN_00408b98 build/normalise direction // FUN_00408440 Point3D copy // FUN_0040a7f4 Point3D copy (read seeker target into thruster buffer) // FUN_004dd138 sqrt() FUN_0041a1a4 IsDerivedFrom(classDerivations) // FUN_0041bbd8 AlarmIndicator::SetLevel // #include #pragma hdrstop #if !defined(SEEKER_HPP) # include #endif #if !defined(MISSILE_HPP) # include #endif #if !defined(TESTBT_HPP) # include #endif // // Lead/guidance tuning constants, read as read-only globals in the decomp. // (Decoded from .rdata @004bec18..@004bec28.) // static const Scalar SeekerLeadMinRange = 200.0f; // _DAT_004bec18 start leading past this range static const Scalar SeekerLeadZero = 0.0f; // _DAT_004bec1c static const Scalar SeekerLeadFloor = 50.0f; // _DAT_004bec20 static const Scalar SeekerLeadMaxClamp = 300.0f; // _DAT_004bec24 lead distance clamp static const Scalar SeekerLeadCoef = 0.1f; // _DAT_004bec28 (repeating-C double 0.1; loft/lead gain) //########################################################################### //########################################################################### // Seeker //########################################################################### //########################################################################### //############################################################################# // Shared Data Support // Derivation Seeker::ClassDerivations( Subsystem::GetClassDerivations(), "Seeker" ); Receiver::MessageHandlerSet Seeker::MessageHandlers; const Seeker::IndexEntry Seeker::AttributePointers[] = { { 0, 0, 0 } }; // IndexEntry table (see header @00512d50) Seeker::AttributeIndexSet Seeker::AttributeIndex; Seeker::SharedData Seeker::DefaultData( // resolved as &DAT_00512bec &Seeker::ClassDerivations, Seeker::MessageHandlers, Seeker::AttributeIndex, Seeker::StateCount ); //############################################################################# // Construction // // @004bec34 Seeker::Seeker(Missile *owner, int id, SubsystemResource*, // Entity *target, const Point3D offset) // // Chains the Subsystem base ctor, stamps the Seeker vtable, then snapshots // the firing geometry: creationPoint <- owner's current position, the homing // target + aim offset, and the initial range readings. // Seeker::Seeker( Missile *owner, int subsystem_ID, SubsystemResource *subsystem_resource, Entity *target, const Point3D offset) : // Subsystem base ctor: owner, id, resource, shared default data. Subsystem(owner, subsystem_ID, subsystem_resource, DefaultData) { // vtable installed by the compiler (PTR_LAB_00512ce8) // CROSS-FAMILY (Entity/Missile kinematics): the owning Missile's current world // position/facing and the target's world position are read from the Entity // transform. Those accessors live in the Entity/missile families; the spawn // snapshot below uses the launch geometry available here. Point3D ownerPos(0.0f, 0.0f, 0.0f); // owner + 0x100 (missile spawn position) creationPoint = ownerPos; // @0xF0 this[0x3c..0x3e] // homing parameters targetEntity = target; // @0xFC this[0x3f] targetOffset = offset; // @0x100 this[0x40..0x42] // Initial aim point: if dumb-fire (target == 0) aim straight ahead from the // owner's facing, otherwise aim at the target's mount + offset. if (targetEntity == 0) { // project the aim point a small distance ahead of the launch facing targetPosition = targetOffset; // @004bec34 (target==0 branch) } else { Point3D targetPos(0.0f, 0.0f, 0.0f); // targetEntity + 0x100 targetPosition.x = targetOffset.x + targetPos.x; targetPosition.y = targetOffset.y + targetPos.y; targetPosition.z = targetOffset.z + targetPos.z; } // rangeToTarget = |targetPosition - ownerPos| (sampled at spawn) // startingRangeToTarget = rangeToTarget // rangeFromCreation = |ownerPos - creationPoint| (== 0 at spawn) Vector3D delta; delta.Subtract(targetPosition, ownerPos); rangeToTarget = (Scalar)Sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z); // @0x10C startingRangeToTarget = rangeToTarget; // @0x114 this[0x45] delta.Subtract(ownerPos, creationPoint); rangeFromCreation = (Scalar)Sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z); // @0x110 } //############################################################################# // Destruction // // @004bee44 ~Seeker() -- restores the Seeker vtable then chains the Subsystem // base destructor. (Reached through the scalar-deleting thunk FUN_004bee70.) // Seeker::~Seeker() { // vtable reset + base ~Subsystem chaining handled by the compiler. } //############################################################################# // Guidance // // @004beae4 Seeker::LeadTarget() // // Per-frame homing update. When a live targetEntity exists, build the // vector from the current targetPosition back to the missile, measure the // range, and -- once outside SeekerLeadMinRange (200 m) -- nudge the aim // point upward/ahead by SeekerLeadCoef * clamp(range-200, .., 300). The // lead is also projected along the target's own velocity so a moving target // is intercepted rather than chased. // void Seeker::LeadTarget() { if (targetEntity == 0) // @004beae4 if (this[0xfc] != 0) return; // CROSS-FAMILY (Entity/Missile kinematics): the owning Missile's world // position + bounding radius and the target's velocity / "alive" state are // read from the Entity transform layer. Isolated as documented locals here. Point3D ownerPos(0.0f, 0.0f, 0.0f); // owner + 0x100 Scalar boundingRadius = 1.0f; // owner + 0x348 (avoid /0) Logical targetTeamValid = True; // owner team chain (best-effort) // vector from aim point to the missile, and its length Vector3D toMissile; toMissile.Subtract(targetPosition, ownerPos); // @004beae4 FUN_00408644(.., +0xe4, owner+0x100) Scalar range = (Scalar)Sqrt(toMissile.x*toMissile.x + toMissile.y*toMissile.y + toMissile.z*toMissile.z); Scalar approach = range / boundingRadius; // owner + 0x348 if (range > SeekerLeadMinRange && targetTeamValid) { Scalar lead = range - SeekerLeadMinRange; if (toMissile.y >= SeekerLeadFloor && lead > SeekerLeadMaxClamp) lead = SeekerLeadMaxClamp; // clamp the loft contribution targetPosition.y += SeekerLeadCoef * lead; // @0xE8 this[0x3a] } // if the target is itself a mover, advance the aim point along its velocity if (targetEntity->TestInstance()) // FUN_0041a1a4(.., 0x4e4518) Mover check { Vector3D targetVelocity(0.0f, 0.0f, 0.0f); // targetEntity velocity targetPosition.x += targetVelocity.x * approach; // @004beae4 FUN_004086ac / FUN_004085ec targetPosition.y += targetVelocity.y * approach; targetPosition.z += targetVelocity.z * approach; } } //############################################################################# // FindTarget -- per-frame Performance entry (best-effort) // // The shipped Missile reads Seeker::targetPosition directly during its // integration pass (Missile::MoveAndCollide @004bef78 samples this+0x1c4), // so the standalone FindTarget reduces to re-leading the target each slice. // No distinct @ADDR survives; this mirrors MISSILE.TCP's call site. // void Seeker::FindTarget(Scalar /*time_slice*/) { LeadTarget(); } //############################################################################# // ResetToInitialState / TestInstance -- inherit the Subsystem base // implementations (vtable @00512ce8 slots 10/.. are all 0x41xxxx engine code). // void Seeker::ResetToInitialState() { // The engine Subsystem base exposes no ResetToInitialState; the Seeker simply // re-zeros its transient guidance range readings. rangeToTarget = 0.0f; rangeFromCreation = 0.0f; startingRangeToTarget = 0.0f; } Logical Seeker::TestInstance() const { return Subsystem::TestInstance(); }