Files
BT411/game/reconstructed/seeker.cpp
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

238 lines
9.6 KiB
C++

//============================================================================//
// 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 <bt.hpp>
#pragma hdrstop
#if !defined(SEEKER_HPP)
# include <seeker.hpp>
#endif
#if !defined(MISSILE_HPP)
# include <missile.hpp>
#endif
#if !defined(TESTBT_HPP)
# include <testbt.hpp>
#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();
}