Root cause of "no sound, ever": the two audio backend DLLs shipped in the repo are fakes. libsndfile-1.dll exports 15 funcs BY ORDINAL ONLY (no names) and sf_open() always returns NULL; OpenAL32.dll (72KB) imports only KERNEL32 -- no dsound/wasapi/winmm -- so it is a pure no-op that returns fake handles (ctx=0x00000001, alGenSources->0) and never touches the hardware. The whole render->device->buffer->source->play chain ran clean and silent. Fixes: - OpenAL32.dll: replace the stub with the real OpenAL Soft 1.25.2 Win32 build (imports AVRT/ole32/WINMM, real WASAPI backend). The exe imports the 25 AL funcs by NAME so it is a drop-in; alGenSources now yields a live source and alSourcePlay reaches AL_PLAYING. - libsndfile: DROPPED entirely. It is replaced by LoadWavPCM() in L4AUDRES -- a tiny RIFF/WAVE fmt+data reader that loads our soundbank WAVs (16-bit PCM) straight into the AL buffer. Removed the .lib/.dll from the link + copy and git-rm'd the stub. (This also kills the "ordinal 50 could not be located in libsndfile-1.dll" load-failure popup: adding an sf_strerror import bound to an ordinal the 2..16-only stub could not satisfy.) - Soundbank: 241 samples cracked from AUDIO1/2.RES (SF2 v1.0) by tools/sf2extract.py into content/AUDIO/*.wav + the allPresets[2][128] table (audiopresets.cpp), replacing the zero-init btstubs stub. All 241 now load (alErr=0). BT_AUDIO_TEST plays buffer0 as proof-of-life; BT_AUDIO_LOG traces the chain. Remaining: in-game triggering (AudioEntities on fire/step/engine/explosion) so PlayNote fires during play -- next audio wave. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
401 lines
18 KiB
C++
401 lines
18 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;
|
|
|
|
// Audio preset bank table -- NOW DEFINED in audiopresets.cpp (241 presets generated
|
|
// from the AWE32 SoundFonts AUDIO1/2.RES by scratchpad/sf2extract.py, task #50
|
|
// 2026-07-15). The old zero-init stub here (PRESET_isImplemented -> false -> silent)
|
|
// is replaced by the real bank/preset->sample map.
|
|
|
|
//===========================================================================//
|
|
// 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;
|
|
}
|
|
|
|
// mech+0x404 = the radar display scale. Both the radar map's currentScale and
|
|
// the overlay range readout bind Mech/RadarRange -> radarRange. The controls
|
|
// mapper slews targetRangeExponent and calls SetTargetRange(250*2^exp) every
|
|
// frame (btl4mppr.cpp:407) off the zoom buttons, so writing the member makes the
|
|
// radar map zoom + the range numeric track the zoom (default exp 2.0 -> 1000,
|
|
// matching the historic frozen value). Faithful body (the binary writes [this+0x404]).
|
|
void Mech::SetTargetRange(Scalar range)
|
|
{
|
|
radarRange = 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;
|
|
// (task #7: no [0x10d] mirror -- the binary FUN_0049fe40 touches ONLY
|
|
// slot 0; the 0x434 cache belongs to the SubsystemMessageManager.)
|
|
}
|
|
|
|
// TODO(bring-up): raise a mech-level status alarm of the given id.
|
|
void Mech::RaiseStatusAlarm(int /*alarm_id*/)
|
|
{
|
|
}
|
|
|
|
// Mech::Reset (@0049fb74) is now the real faithful reconstruction in mech4.cpp
|
|
// (reposition + kill dead-reckon + clear death/damage + ResetToInitialState every
|
|
// subsystem + heal every damage zone + ForceUpdate) -- no longer the reposition-
|
|
// only stub that lived here.
|
|
|
|
// (Mech__DamageZone::LoadCriticalSubsystems was a no-op stub here; it is now the
|
|
// real type-0x1e damage-state descriptor-table loader in mechdmg.cpp.)
|
|
|
|
//===========================================================================//
|
|
// 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 (the "blue warp" translocation sphere) is now a real
|
|
// reconstruction in btl4vid.cpp (task #52) -- no longer a stub here.
|
|
|
|
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/Draw now live in btl4vid.cpp -- the real
|
|
// @004cc40c reconstruction replaced the old bring-up stubs here.)
|
|
|
|
//===========================================================================//
|
|
// 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()
|
|
{
|
|
}
|