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>
230 lines
9.8 KiB
C++
230 lines
9.8 KiB
C++
//============================================================================//
|
|
// File: projtile.cpp //
|
|
// Project: BattleTech //
|
|
// Contents: In-flight projectile entity base (Projectile : Entity) //
|
|
//----------------------------------------------------------------------------//
|
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
|
|
// PROPRIETARY AND CONFIDENTIAL //
|
|
//============================================================================//
|
|
//
|
|
// RECONSTRUCTED from the shipped binary. There is NO surviving projtile.cpp --
|
|
// only PROJTILE.TCP (Projectile::TestClass). Behaviour follows the Ghidra
|
|
// pseudo-C in part_013.c (@004be1bc ctor, @004be358 dtor, @004be384 allocator,
|
|
// @004be3b4 TestInstance). Each non-trivial method cites the originating @ADDR.
|
|
//
|
|
// Coverage:
|
|
// confident : ctor @004be1bc (Entity base chain, world/segment registration,
|
|
// launch-kinematics snapshot, Performance install), dtor thunk
|
|
// @004be358, allocator @004be384, TestInstance @004be3b4
|
|
// best-effort: the names of the descriptor-sourced members (+0x300..+0x33C)
|
|
// and the modelUpdate binding (+0x250/+0x254); MoveAndCollide
|
|
// (the base Performance variants PTR_LAB_005129e8/f4 -- bodies sit
|
|
// past the captured window; the live integrator that ships is the
|
|
// Missile override @004bef78)
|
|
// excluded : the 0x41xxxx/0x42xxxx Entity engine vtable slots and the base
|
|
// Entity ctor @004234f0
|
|
//
|
|
// Helper-function name mapping (engine internals referenced by the decomp):
|
|
// FUN_004234f0 Entity base constructor (ctx, default record)
|
|
// FUN_0041db7c init embedded connection/descriptor sub-object
|
|
// FUN_0041bbd8 AlarmIndicator::SetLevel FUN_0041a1a4 IsDerivedFrom
|
|
// FUN_00408440 Point3D / Vector3D copy FUN_00433ed4 motion-source lookup
|
|
// FUN_00402298 operator new FUN_004022d0 operator delete
|
|
// DAT_004efc94 global game/world context block
|
|
//
|
|
|
|
#include <bt.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(PROJTILE_HPP)
|
|
# include <projtile.hpp>
|
|
#endif
|
|
#if !defined(TESTBT_HPP)
|
|
# include <testbt.hpp>
|
|
#endif
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// Projectile
|
|
//###########################################################################
|
|
//###########################################################################
|
|
|
|
//#############################################################################
|
|
// Decompiler-artifact stand-ins (label/record operands).
|
|
//
|
|
static char DAT_005129b0[0x340] = { 0 }; // default streamed-entity record
|
|
|
|
//#############################################################################
|
|
// Shared Data Support
|
|
//
|
|
Derivation
|
|
Projectile::ClassDerivations(
|
|
Entity::GetClassDerivations(),
|
|
"Projectile"
|
|
);
|
|
|
|
Receiver::MessageHandlerSet Projectile::MessageHandlers;
|
|
Simulation::AttributeIndexSet Projectile::AttributeIndex;
|
|
|
|
Projectile::SharedData
|
|
Projectile::DefaultData(
|
|
&Projectile::ClassDerivations,
|
|
Projectile::MessageHandlers,
|
|
Projectile::AttributeIndex,
|
|
Projectile::StateCount,
|
|
&Projectile::Make
|
|
);
|
|
|
|
//
|
|
// Entity factory hook (Entity::SharedData requires a MakeHandler). Projectiles
|
|
// are spawned through New(), not the generic Make() path, so this is a stub.
|
|
//
|
|
Entity *Projectile::Make(MakeMessage * /*creation_message*/)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
//#############################################################################
|
|
// Allocation
|
|
//
|
|
// @004be384 Projectile::New(spawn_context) -- allocate 0x340 bytes and
|
|
// construct with the default streamed-entity record &DAT_005129b0.
|
|
//
|
|
Projectile *Projectile::New(int spawn_context)
|
|
{
|
|
Projectile *p = (Projectile *)::operator new(0x340); // FUN_00402298
|
|
if (p != 0)
|
|
p = new(p) Projectile(spawn_context, DAT_005129b0); // @004be1bc
|
|
return p;
|
|
}
|
|
|
|
//#############################################################################
|
|
// Construction
|
|
//
|
|
// @004be1bc Projectile::Projectile(int owner, const char *spawn_descriptor)
|
|
//
|
|
// 1. Chains the Entity base ctor and stamps the Projectile vtable.
|
|
// 2. Initialises the embedded segment/connection sub-object (this+0x30C),
|
|
// flags itself as a transient/guided entity (flags |= 0x10) and raises its
|
|
// status alarm to level 2 (in-flight).
|
|
// 3. Registers itself into the owning world's segment table (the segment is
|
|
// resolved from descriptor+0x84 indexing world+0x128) and snapshots the
|
|
// launch kinematics out of the spawn descriptor:
|
|
// segmentIndex <- descriptor +0x84
|
|
// sourceEntity <- descriptor +0x88
|
|
// sourceWeapon <- descriptor +0x8C
|
|
// launchPosition <- descriptor +0x90 (Point3D)
|
|
// launchVelocity <- descriptor +0x9C (Vector3D)
|
|
// aimDirection <- descriptor +0xA8 (Vector3D)
|
|
// modelIndex <- descriptor +0xB4
|
|
// damageRecord <- descriptor +0xB8
|
|
// 4. Binds the per-frame model-update callback (this[0x94..0x97],
|
|
// PTR_FUN_005129dc) and installs the active Performance: the authoritative
|
|
// integrator (PTR_LAB_005129e8) when flags&0xC==4, else the ghost variant
|
|
// (PTR_LAB_005129f4). Finally fires vtable slot 14 (initial placement).
|
|
//
|
|
Projectile::Projectile(int owner, const char *spawn_descriptor)
|
|
:
|
|
Entity((Entity::MakeMessage *)0, DefaultData) // @004234f0
|
|
{
|
|
(void)owner;
|
|
// vtable installed by the compiler (PTR_FUN_00512a5c)
|
|
|
|
InitSegmentLink(&this->sourceEntity); // @0041db7c (this+0x30C)
|
|
simulationFlags |= 0x10; // transient / projectile
|
|
SetStatusLevel(2); // FUN_0041bbd8(this+0xb, 2) -- in flight
|
|
|
|
// register into the world segment table
|
|
world = ResolveWorld(DAT_004efc94); // this[0xC0] @0x300
|
|
segmentIndex = *(int *)(spawn_descriptor + 0x84); // this[0xC2] @0x308
|
|
currentSegment = *(Segment **)(*(int *)((int)world + 0x128) + segmentIndex * 4); // this[0xC1] @0x304
|
|
|
|
sourceEntity = *(int *)(spawn_descriptor + 0x88); // this[0xC3] @0x30C
|
|
sourceWeapon = *(int *)(spawn_descriptor + 0x8C); // this[0xC4] @0x310
|
|
modelIndex = *(int *)(spawn_descriptor + 0xB4); // this[0xCE] @0x338
|
|
launchPosition = *(Point3D *)(spawn_descriptor + 0x90); // this[0xC5] @0x314 FUN_00408440
|
|
launchVelocity = *(Vector3D *)(spawn_descriptor + 0x9C); // this[0xC8] @0x320 FUN_00408440
|
|
aimDirection = *(Vector3D *)(spawn_descriptor + 0xA8); // this[0xCB] @0x32C FUN_00408440
|
|
damageRecord = *(int *)(spawn_descriptor + 0xB8); // this[0xCF] @0x33C
|
|
|
|
// model-update callback context + bound member function pointer triple
|
|
modelUpdateContext = ResolveMotionSource(DAT_004efc94); // this[0x94] @0x250 (FUN_00433ed4)
|
|
BindModelUpdate(0); // this[0x95..0x97] @0x254 (PTR_FUN_005129dc)
|
|
|
|
// select the per-frame integrator
|
|
if ((simulationFlags & 0xC) == 4)
|
|
SetPerformance(/*authoritative*/ &Projectile::MoveAndCollide); // PTR_LAB_005129e8
|
|
else
|
|
SetPerformance(/*ghost*/ &Projectile::MoveAndCollide); // PTR_LAB_005129f4
|
|
|
|
// slot 14 (initial placement): a virtual call in the shipped ctor; the
|
|
// concrete placement runs through the installed vtable at run time.
|
|
}
|
|
|
|
//#############################################################################
|
|
// Destruction
|
|
//
|
|
// @004be358 ~Projectile() -- restores the Projectile vtable then chains the
|
|
// Entity base destructor (which un-registers from the segment table).
|
|
//
|
|
Projectile::~Projectile()
|
|
{
|
|
// vtable reset + base ~Entity chaining handled by the compiler.
|
|
}
|
|
|
|
//#############################################################################
|
|
// MoveAndCollide -- default per-frame integrator (best-effort)
|
|
//
|
|
// The base Projectile integrate-and-collide bodies (PTR_LAB_005129e8 /
|
|
// 005129f4) lie past the captured decomp window. The concrete behaviour that
|
|
// ships for the only instantiated subclass is the Missile override
|
|
// (Missile::MoveAndCollide @004bef78): age the projectile, integrate motion
|
|
// (FUN_00421b2c) and advance the model (FUN_00421bac), run the world collision
|
|
// query (FUN_0042291c) and -- on a hit -- spawn the explosion/Damage entity and
|
|
// retire (FUN_0042061c). Declared here to anchor the vtable slot; the body is
|
|
// reconstructed structurally.
|
|
//
|
|
void Projectile::MoveAndCollide(Scalar time_slice)
|
|
{
|
|
IntegrateMotion(); // FUN_00421b2c
|
|
AdvanceModel(time_slice); // FUN_00421bac
|
|
Entity *hit = CollisionQuery(time_slice); // FUN_0042291c
|
|
if (hit != 0)
|
|
Retire(); // FUN_0042061c
|
|
}
|
|
|
|
//#############################################################################
|
|
// TestInstance
|
|
//
|
|
// @004be3b4 IsDerivedFrom(ClassDerivations tag 0x512980).
|
|
//
|
|
Logical Projectile::TestInstance() const
|
|
{
|
|
return IsDerivedFrom(Projectile::ClassDerivations);
|
|
}
|
|
|
|
//#############################################################################
|
|
// TestClass (PROJTILE.TCP)
|
|
//
|
|
// Surviving fragment: takes no argument and returns True.
|
|
//
|
|
Logical Projectile::TestClass()
|
|
{
|
|
return True;
|
|
}
|
|
|
|
//#############################################################################
|
|
// Recovered internal helpers (engine-internal world/segment/motion plumbing).
|
|
// These wrap Entity-engine operations the decompiler rendered as raw FUN_*
|
|
// calls; reconstructed as structural stubs.
|
|
//
|
|
void Projectile::InitSegmentLink(void * /*link*/) {}
|
|
void Projectile::SetStatusLevel(int /*level*/) {}
|
|
void *Projectile::ResolveWorld(void * /*app_context*/) { return 0; }
|
|
void *Projectile::ResolveMotionSource(void * /*app_context*/) { return 0; }
|
|
void Projectile::BindModelUpdate(void * /*callback*/) {}
|
|
void Projectile::IntegrateMotion() {}
|
|
void Projectile::AdvanceModel(Scalar /*time_slice*/) {}
|
|
Entity *Projectile::CollisionQuery(Scalar /*time_slice*/) { return 0; }
|
|
void Projectile::Retire() {}
|