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

446 lines
20 KiB
C++

//===========================================================================//
// File: btstubs.cpp //
// Project: BattleTech port (WinTesla / btl4) //
//---------------------------------------------------------------------------//
// BRING-UP STUBS + engine data-global definitions needed to LINK btl4.exe. //
// //
// Every definition in this file is a flagged first-link placeholder. None //
// of these carry the real shipped behaviour yet -- they exist only so the //
// executable links and can be booted. Each must be replaced with the real //
// body recovered from BTL4OPT.EXE (the binary oracle) / the RP analogue. //
// //
// == RUNTIME BRING-UP WORKLIST (replace these) == //
// Mech::GetMissionReviewMode / IsAirborne / SetTargetRange / //
// SetMappingSubsystem / RaiseStatusAlarm //
// Mech__DamageZone::LoadCriticalSubsystems //
// MechSubsystem::TakeDamage / OnAlarmChanged //
// Generator::ForceShortRecovery //
// Is_Destroyed / ToggleVoiceAssist / Set_Alarm_Level / //
// Notify_Objective_Reached //
// data: FrameTimeScale, StatusMessagePool, BTPlayerVTable, //
// BTStatusMessageVTable, allPresets //
//===========================================================================//
#include <bt.hpp> // Mech, MechSubsystem, BTPlayer, Damage, Scalar, ...
#pragma hdrstop
#if !defined(POWERSUB_HPP)
# include <powersub.hpp> // Generator
#endif
#if !defined(MECHDMG_HPP)
# include <mechdmg.hpp> // Mech__DamageZone (full definition)
#endif
#include <L4AUDLVL.hpp> // PRESETINFO + extern allPresets[2][100]
#include <MEMBLOCK.hpp> // MemoryBlock
#if !defined(BTL4VID_HPP)
# include <btl4vid.hpp> // BT*Renderable hierarchy + dpl2d_* prototypes
#endif
//===========================================================================//
// Engine data globals (declared extern by the engine/game but defined in a
// .cpp that is not part of munga_engine.lib).
//===========================================================================//
// TODO(bring-up): per-frame time scale; real value is set by the sim clock.
// Default 1.0 so the mech-age divide in mech.cpp does not divide by zero.
Scalar FrameTimeScale = 1.0f;
// TODO(bring-up): audio preset bank table (real data lives in WTPresets/L4AUDLVL).
// Zero-initialised -> PRESET_isImplemented() returns false (no presets) at boot.
PRESETINFO allPresets[2][100] = {};
//===========================================================================//
// BattleTech globals (status-message pool + hand-rolled vtable pointers the
// recovered ctors splice in). Real homes were btplayer.cpp .data.
//===========================================================================//
// TODO(bring-up): BT StatusMessage MemoryBlock pool (@00512f6c). Must be a real
// MemoryBlock sized for StatusMessage before status messages are allocated.
MemoryBlock *StatusMessagePool = 0;
// TODO(bring-up): hand-built vtables the recovered BTPlayer/StatusMessage ctors
// patch into the object's first word (PTR_FUN_00513300 / PTR_LAB_00513344).
// Null for now -- any virtual call through these will fault until reconstructed.
void *BTPlayerVTable = 0;
void *BTStatusMessageVTable = 0;
//===========================================================================//
// Free-function stubs (declared extern at the call sites).
//===========================================================================//
// TODO(bring-up): true when the entity handle refers to a destroyed entity.
Logical Is_Destroyed(int /*entity_handle*/)
{
return False;
}
// TODO(bring-up): toggle the pilot voice-assist subsystem.
void ToggleVoiceAssist(int /*voice_assist_subsystem*/)
{
}
// TODO(bring-up): raise/clear a cockpit alarm indicator to the given level.
void Set_Alarm_Level(void * /*alarm_indicator*/, int /*level*/)
{
}
// TODO(bring-up): notify mission scoring that an objective subsystem was reached.
void Notify_Objective_Reached(int * /*objective_subsystem*/, Mech * /*mech*/)
{
}
//===========================================================================//
// Mech method stubs.
//===========================================================================//
// TODO(bring-up): reads mech+0x414 (mission-review playback flag).
int Mech::GetMissionReviewMode()
{
return 0;
}
// TODO(bring-up): true while the mech is off the ground (jump-jet / fall state).
int Mech::IsAirborne()
{
return 0;
}
// FUN_0049fb54 -- true when the mech is destroyed/shut down (masks control input
// and freezes gait). For the drivable bring-up the mech is always live.
// TODO(bring-up): derive from the status/master alarm once damage is wired.
// Mech::IsDisabled -- @0049fb54. Reconstructed: the mech is "disabled" (its action
// requests are masked, gait frozen) when its movementMode (mech+0x40, the gait/death
// selector) is in a death state -- 2 or 9. movementMode is driven to those values by
// the gait/death-transition code (mech2 SetLegAnimation/SetBodyAnimation family) when a
// vital zone is destroyed; that transition path is part of the locomotion/Simulate
// reconstruction (bypassed in the bring-up drive override), so this reads false until
// that lands. Faithful to the binary regardless.
Logical Mech::IsDisabled()
{
return (movementMode == 2 || movementMode == 9) ? True : False;
}
// TODO(bring-up): writes mech+0x404 (HUD/weapon target range).
void Mech::SetTargetRange(Scalar /*range*/)
{
}
// Install the cockpit-mapping subsystem into the Mech's reserved ControlsMapper
// roster slot (index 0), mirroring VTV::SetMappingSubsystem (RP/VTV.cpp:394) and
// VTV::ControlsMapperSubsystem==0. The streamed control-mapping resource binds
// its DirectMappings to subsystemID 0, so the mapper MUST live there for
// Entity::GetSimulation(0) to resolve it. (Previously a no-op -> slot 0 stayed
// NULL -> LBE4ControlsManager::CreateStreamedMappings dereferenced a NULL
// subsystem.) controlsMapper mirrors the same pointer for the gameplay path.
void Mech::SetMappingSubsystem(Subsystem *subsystem)
{
Check(this);
Check_Pointer(subsystemArray);
if (subsystemArray[0] != 0)
{
// re-spawn: drop the previous mapper (NULL on first spawn).
Unregister_Object(subsystemArray[0]);
delete subsystemArray[0];
}
subsystemArray[0] = subsystem;
controlsMapper = (MechControlsMapper *)subsystem;
}
// TODO(bring-up): raise a mech-level status alarm of the given id.
void Mech::RaiseStatusAlarm(int /*alarm_id*/)
{
}
// Mech::Reset (@0049fb74) -- place the mech at a (re)spawn origin.
// The shipped Reset additionally zeroes ~50 heat/damage/motion state fields and
// resets every subsystem in the roster (slot vtable+0x28 with the reset mode);
// that full sweep is deferred (TODO(bring-up)). For first spawn a freshly
// constructed mech is already clean, so positioning it + refreshing the world
// transform + forcing an update is enough to get it placed and rendering.
void Mech::Reset(const Origin &origin, int /*mode*/)
{
Check(this);
localOrigin = origin; // this+0x100 (FUN_0040a938 copy)
localToWorld = localOrigin; // this+0xd0 (FUN_0040ab44 rebuild)
ForceUpdate(); // FUN_004a4c54 family -- push to renderer/replicants
// BRING-UP (Tier-2 locomotion): get the player mech onto the per-frame
// simulation path so it can walk. Two engine gates block a freshly spawned
// master entity from being executed:
// (1) Entity::Execute only calls PerformAndWatch when the app is in
// RunningMission/EndingMission OR the entity IsPreRunnable()
// (ENTITY.cpp:~549) -> set the pre-run flag.
// (2) UpdateManager::Execute skips a master unless IsInteresting()
// (interestCount!=0) && IsNonReplicantExecutable() (UPDATE.cpp:148)
// -> force it interesting so the update manager ticks it.
// Without these the mech renders but is never simulated (static
// localToWorld). See Mech::PerformAndWatch (mech4.cpp).
SetPreRunFlag();
if (interestCount == 0) interestCount = 1; // Entity::interestCount (public)
DEBUG_STREAM << "[drive] Mech::Reset spawn at ("
<< localOrigin.linearPosition.x << ", "
<< localOrigin.linearPosition.y << ", "
<< localOrigin.linearPosition.z << ") -- prerun+interest forced\n" << std::flush;
Check_Fpu();
}
//===========================================================================//
// Mech__DamageZone method stub.
//===========================================================================//
// TODO(bring-up): stream in the per-zone critical-subsystem plug list.
void Mech__DamageZone::LoadCriticalSubsystems(MemoryStream * /*stream*/)
{
}
//===========================================================================//
// MechSubsystem method stubs.
//===========================================================================//
// TODO(bring-up): apply damage to a generic mech subsystem (virtual override).
void MechSubsystem::TakeDamage(Damage & /*damage*/)
{
}
// TODO(bring-up): react to a change in this subsystem's alarm level.
void MechSubsystem::OnAlarmChanged()
{
}
//===========================================================================//
// Generator method stub.
//===========================================================================//
// TODO(bring-up): force the generator into the short-event fast-recovery path.
void Generator::ForceShortRecovery()
{
}
//===========================================================================//
// BT subsystem<->Mech bridge accessors (declared extern in powersub.hpp).
//---------------------------------------------------------------------------//
// INTEGRATION NOTE (Task 2): the shipped binary reads these off fixed Mech byte
// offsets (msg-mgr @+0x190, bus-live @*(+0x190)+0x274, myomers @+0x374). The
// reconstructed Mech is NOT byte-exact (named members + a Wword scratch bank),
// so a raw `*(owner+0x190)` would read an unrelated member and, when chased one
// level further for the bus flag, dereference garbage. These are therefore
// implemented against intent rather than raw offsets, and kept deref-safe:
// * BT_IsBusLive -> True: the running player mech is a powered master; this
// un-gates the power-dependent subsystem paths (PoweredSubsystem/Generator)
// so they compute when those real classes are wired (today their consumers
// are RECON_SUBSYS stubs -- see RECONCILE.md -- so this is latent but safe).
// * BT_GetMessageManager -> 0: the SubsystemMessageManager IS in the roster
// (slot "MessageManager") but cannot be located here without the byte-exact
// layout; returning a bogus non-null would be WORSE (callers deref it). Left
// 0 (callers null-guard) until the roster lookup / real layout is restored.
// * BT_ClearMyomers -> no-op: no myomer back-pointer member exists at a known
// offset in the reconstructed Mech to clear.
//===========================================================================//
// Real impl reads *(owner+0x190). No byte-exact +0x190 in the reconstructed
// Mech; callers null-guard, so 0 is the safe value (see note above).
SubsystemMessageManager *BT_GetMessageManager(Mech * /*owner*/)
{
return 0;
}
// The running player mech is a live, powered master -> its electrical bus is up.
// (Real impl read *(*(owner+0x190)+0x274); see layout note above.)
Logical BT_IsBusLive(Mech * /*owner*/)
{
return True;
}
// No known-offset myomer back-pointer in the reconstructed Mech to clear.
void BT_ClearMyomers(Mech * /*owner*/)
{
}
// BT_GetSegmentFlags -- the per-subsystem segment-flag accessor the HUD (hud.cpp)
// and the segment-flag-gated subsystem ctors call. It was DECLARED extern but
// never DEFINED, so it only linked under /FORCE (an unresolved external whose
// return was undefined if ever called). The oracle-verified authoritative source
// for the master/copy + master-bit gate is the OWNER's simulationFlags
// (owner+0x28): (flags & 0xC)==0 (master, not a replicant copy) && (flags & 0x100)
// (master bit). Defining it here resolves the dangling symbol and feeds the gate
// the same authoritative value the reconciled subsystem gates now read directly.
LWord BT_GetSegmentFlags(Mech *owner)
{
return owner ? owner->simulationFlags : 0;
}
//===========================================================================//
// Legacy libDPL material-name callback.
//---------------------------------------------------------------------------//
// The dpl2d_ 2D display-list API (NewDisplayList/Begin/SetColor/Circle/
// PushMatrix/PopMatrix/MoveTo/End/Compile) was MOVED OUT of this file and
// reimplemented over Direct3D 9 in decomp/reconstructed/dpl2d.cpp (the HUD/
// reticle 2D layer). Only the 3D material-name-substitution hook -- which is
// part of the world renderer, not the 2D HUD -- remains stubbed here.
// TODO(bring-up): wire the material substitution table (FUN_00459eb8).
//===========================================================================//
void dpl_SetMaterialNameCallback(char *(*)(char *)) {}
//===========================================================================//
// BTL4VideoRenderer helper methods whose bodies were not reconstructed.
//---------------------------------------------------------------------------//
// TODO(bring-up): real bodies @FUN_0045a724 / 00498448 / 0045a994 / 00489cec
// and LoadMissionImplementation drive the D3D scene/object pipeline.
//===========================================================================//
Scene *BTL4VideoRenderer::GetScene() { return 0; }
d3d_OBJECT *BTL4VideoRenderer::LoadObject(const char * /*name*/) { return 0; }
void BTL4VideoRenderer::AttachToEyeDCS(dpl_DCS *, LinearMatrix &) {}
void BTL4VideoRenderer::AddDynamicRenderable(VideoComponent *, Entity *) {}
void BTL4VideoRenderer::LoadMissionImplementation(Mission *mission)
{
// Load the real BattleTech renderer environment for this mission: paths,
// clip range, projection, fog, ambient + directional lights, cached shapes,
// effects -- driven by BTDPL.INI (L4DPLCFG, set in btl4main.cpp). This is
// what RP does (RPL4VideoRenderer::LoadMissionImplementation -> base), and is
// what gives the cavern world its environment. DPLReadINIPage builds a valid
// RH projection from the INI viewangle/clip, so the mech view is preserved.
DPLRenderer::LoadMissionImplementation(mission);
// Safety net (mProjectionMatrix is private to DPLRenderer, so we cannot
// inspect it here): unconditionally re-assert a known-good RH projection and
// disable fog so the freshly loaded mech body + cavern geometry are not
// clipped or fogged to the background. DPLReadINIPage already set the
// background colour / paths / lights from BTDPL.INI; this only guarantees the
// view stays valid. TODO(bring-up): once the env-driven projection/fog are
// confirmed good, drop this and honour the INI fog/clip directly.
EnsureValidProjection();
}
//===========================================================================//
// BattleTech renderable hierarchy (the unreconstructed "BT renderables" module).
//---------------------------------------------------------------------------//
// TODO(bring-up): these ctors (FUN_0045xxxx) built the D3D DCS/instance graph
// for each mech part/effect. Stubbed to chain to BTRenderableBase so the object
// is a valid (empty) VideoComponent; nothing is drawn until reconstructed.
//===========================================================================//
BTWorldContainerRenderable::BTWorldContainerRenderable(
Entity *entity, int, int, Scene *)
: BTRenderableBase(entity) {}
BTRootRenderable::BTRootRenderable(
Entity *entity, int, d3d_OBJECT *, Scene *, int, uint32)
: BTRenderableBase(entity) {}
BTDCSObjectRenderable::BTDCSObjectRenderable(
Entity *entity, Scene *, d3d_OBJECT *, int, uint32, LinearMatrix *, dpl_DCS *)
: BTRenderableBase(entity) {}
BTHingeRenderable::BTHingeRenderable(
Entity *entity, int, d3d_OBJECT *, Scene *, int, uint32, dpl_DCS *,
LinearMatrix *, const Hinge *)
: BTRenderableBase(entity) {}
BTBallJointRenderable::BTBallJointRenderable(
Entity *entity, int, d3d_OBJECT *, Scene *, int, uint32, dpl_DCS *,
LinearMatrix *, const EulerAngles *)
: BTRenderableBase(entity) {}
BTBallTranslateJointRenderable::BTBallTranslateJointRenderable(
Entity *entity, int, d3d_OBJECT *, Scene *, int, uint32, dpl_DCS *,
LinearMatrix *, const EulerAngles *, const Point3D *)
: BTRenderableBase(entity) {}
BTEyeRenderable::BTEyeRenderable(
Entity *entity, Scene *, LinearMatrix *, dpl_DCS *, dpl_VIEW *, EulerAngles *)
: BTRenderableBase(entity) {}
BTDeathEffectRenderable::BTDeathEffectRenderable(
Entity *entity, int, dpl_VIEW *, Scene *, dpl_DCS *, StateIndicator *, int)
: BTRenderableBase(entity) {}
BTMarkerWatcherRenderable::BTMarkerWatcherRenderable(
Entity *entity, int, dpl_VIEW *, dpl_DCS *)
: BTRenderableBase(entity) {}
BTTranslocationRenderable::BTTranslocationRenderable(
Entity *entity, int, dpl_VIEW *, StateIndicator *, Point3D *, int)
: BTRenderableBase(entity) {}
BTPOVStartEndRenderable::BTPOVStartEndRenderable(
Entity *entity, int, dpl_VIEW *, dpl_ZONE *, dpl_ZONE *, StateIndicator *,
float, float, float, float, float, int, int)
: BTRenderableBase(entity) {}
BTTracerEffectRenderable::BTTracerEffectRenderable(
Entity *entity, int, void *, int, dpl_ZONE *, dpl_DCS *, LinearMatrix *)
: BTRenderableBase(entity) {}
BTEmitterBeamRenderable::BTEmitterBeamRenderable(
Entity *entity, int, d3d_OBJECT *, Scene *, int, uint32, dpl_DCS *,
LinearMatrix *, void *, int, void *, void *)
: BTRenderableBase(entity) {}
BTPPCBeamRenderable::BTPPCBeamRenderable(
Entity *entity, int, d3d_OBJECT *, Scene *, int, uint32, dpl_DCS *,
LinearMatrix *, void *, int, void *, void *, float)
: BTRenderableBase(entity) {}
BTLightConnection::BTLightConnection(
Entity *entity, int, dpl_INSTANCE *, int, int *)
: BTRenderableBase(entity) {}
//===========================================================================//
// BTReticleRenderable ctor/dtor/Execute (AddWeapon lives in btl4vid.cpp).
//---------------------------------------------------------------------------//
// TODO(bring-up): real ctor @004cc40c builds the reticle 2D display lists.
//===========================================================================//
BTReticleRenderable::BTReticleRenderable(
Entity *entity, int, Reticle **, dpl_VIEW *,
void *, void *, void *, void *, void *, Scalar, int,
void *, void *, void *)
: VideoRenderable(entity, VideoRenderable::Dynamic), weaponCount(0)
{
}
BTReticleRenderable::~BTReticleRenderable()
{
}
void BTReticleRenderable::Execute()
{
}
//===========================================================================//
// VideoComponent (legacy renderable base) -- declared in L4VIDRND.h but NOT
// implemented anywhere in the WinTesla engine (superseded by VideoRenderable).
// BTRenderableBase still derives from it, so provide first-link stub bodies.
//---------------------------------------------------------------------------//
// TODO(bring-up): either reimplement VideoComponent over the new D3D renderable
// hierarchy or reparent BTRenderableBase onto VideoRenderable.
//===========================================================================//
VideoComponent::VideoComponent(Entity * /*entity*/, VideoExecutionType /*type*/)
: HierarchicalDrawComponent((RegisteredClass::ClassID)0)
, videoComponentSocket((Node *)0) // TODO(bring-up): owner node when reimplemented
{
}
VideoComponent::~VideoComponent()
{
}
void VideoComponent::Add(VideoComponent * /*component_to_add*/)
{
}
void VideoComponent::Connect(VideoComponent * /*component_to_connect*/)
{
}
void VideoComponent::ReceiveControl(VideoControlID /*control_ID*/, Scalar /*control_value*/)
{
}
void VideoComponent::Execute()
{
}