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>
238 lines
9.6 KiB
C++
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();
|
|
}
|