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>
296 lines
10 KiB
C++
296 lines
10 KiB
C++
//===========================================================================//
|
|
// File: dpl2d.cpp //
|
|
// Project: BattleTech port (WinTesla / btl4) //
|
|
//---------------------------------------------------------------------------//
|
|
// libDPL 2D vector display-list layer, re-hosted over Direct3D 9. //
|
|
// //
|
|
// Replaces the inert dpl2d_* stubs that used to live in btstubs.cpp. The //
|
|
// recorder calls (dpl2d_NewDisplayList / Begin / SetColor / Circle / //
|
|
// PushMatrix / PopMatrix / MoveTo / End / Compile -- prototypes in //
|
|
// btl4vid.hpp) build a small command list; dpl2d_ExecuteList (dpl2d.hpp) //
|
|
// draws a compiled list with screen-space 2D primitives. //
|
|
// //
|
|
// The recorded calls match what the reconstructed reticle/PIP builder in //
|
|
// btl4vid.cpp actually issues, e.g. //
|
|
// dpl2d_Begin(list, 1); //
|
|
// dpl2d_SetColor(list, r, g, b); //
|
|
// dpl2d_Circle(list, x, y, 0.012f, 1); // filled blip //
|
|
// dpl2d_SetColor(list, 0, 0, 0); //
|
|
// dpl2d_Circle(list, x, y, 0.014f, 0); // dark outline ring //
|
|
// dpl2d_PushMatrix(list); dpl2d_MoveTo(list, x, y); dpl2d_PopMatrix(); //
|
|
// dpl2d_End(list); dpl2d_Compile(list); //
|
|
//===========================================================================//
|
|
|
|
#include <bt.hpp> // Scalar + engine prelude
|
|
#pragma hdrstop
|
|
|
|
#if !defined(BTL4VID_HPP)
|
|
# include <btl4vid.hpp> // dpl2d_DISPLAY (opaque) + dpl2d_* prototypes
|
|
#endif
|
|
#if !defined(DPL2D_HPP)
|
|
# include <dpl2d.hpp>
|
|
#endif
|
|
|
|
#include <d3d9.h>
|
|
#include <vector>
|
|
#include <math.h>
|
|
|
|
//===========================================================================//
|
|
// Pre-transformed 2D vertex -- mirrors MUNGA_L4/L4D3D.h L4VERTEX_2D and its
|
|
// L4VERTEX_2D_FVF, redeclared locally so this TU does not need the MUNGA_L4
|
|
// source dir on the include path (the engine lib uses the identical layout).
|
|
//===========================================================================//
|
|
namespace
|
|
{
|
|
struct Vertex2D
|
|
{
|
|
float x, y, z, rhw;
|
|
DWORD color;
|
|
};
|
|
const DWORD kVertex2DFVF = (D3DFVF_XYZRHW | D3DFVF_DIFFUSE);
|
|
|
|
const int kCircleSegments = 32; // tessellation of dpl2d_Circle
|
|
const float kPi = 3.14159265358979323846f;
|
|
|
|
inline float ClampUnit(float v)
|
|
{
|
|
return (v < 0.0f) ? 0.0f : (v > 1.0f ? 1.0f : v);
|
|
}
|
|
|
|
//
|
|
// One recorded drawing command. The dpl2d_ API the port uses only ever
|
|
// emits coloured circles (filled disc or outline ring); the matrix stack
|
|
// and pen move are tracked as a translation so any future MoveTo-relative
|
|
// geometry lands in the right place.
|
|
//
|
|
struct Command
|
|
{
|
|
enum Kind { kCircleFill, kCircleOutline } kind;
|
|
DWORD color;
|
|
float x, y; // normalised view coords (already translated)
|
|
float radius; // normalised (fraction of viewport width)
|
|
};
|
|
|
|
struct Vec2 { float x, y; };
|
|
}
|
|
|
|
//
|
|
// The concrete display list. dpl2d_DISPLAY is an opaque empty class
|
|
// (DPLSTUB.h), so the recorder allocates one of these and hands the caller a
|
|
// reinterpret_cast handle; every dpl2d_ call casts it straight back.
|
|
//
|
|
struct Dpl2dList
|
|
{
|
|
bool recording;
|
|
bool compiled;
|
|
int mode;
|
|
DWORD currentColor;
|
|
float penX, penY; // current pen (MoveTo)
|
|
float transX, transY; // current translation
|
|
std::vector<Vec2> matrixStack; // saved translations (Push/Pop)
|
|
std::vector<Command> commands;
|
|
|
|
Dpl2dList()
|
|
: recording(false), compiled(false), mode(0),
|
|
currentColor(0xFFFFFFFF),
|
|
penX(0.0f), penY(0.0f), transX(0.0f), transY(0.0f)
|
|
{}
|
|
};
|
|
|
|
static inline Dpl2dList *AsList(dpl2d_DISPLAY *handle)
|
|
{
|
|
return reinterpret_cast<Dpl2dList *>(handle);
|
|
}
|
|
|
|
//===========================================================================//
|
|
// Recorder -- the dpl2d_* entry points the game calls (declared in btl4vid.hpp)
|
|
//===========================================================================//
|
|
|
|
dpl2d_DISPLAY *dpl2d_NewDisplayList()
|
|
{
|
|
return reinterpret_cast<dpl2d_DISPLAY *>(new Dpl2dList);
|
|
}
|
|
|
|
void dpl2d_Begin(dpl2d_DISPLAY *list, int mode)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
self->recording = true;
|
|
self->compiled = false;
|
|
self->mode = mode;
|
|
self->currentColor= 0xFFFFFFFF;
|
|
self->transX = self->transY = 0.0f;
|
|
self->penX = self->penY = 0.0f;
|
|
self->matrixStack.clear();
|
|
self->commands.clear();
|
|
}
|
|
|
|
void dpl2d_SetColor(dpl2d_DISPLAY *list, Scalar red, Scalar green, Scalar blue)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
self->currentColor = D3DCOLOR_COLORVALUE(
|
|
ClampUnit((float)red), ClampUnit((float)green), ClampUnit((float)blue), 1.0f);
|
|
}
|
|
|
|
void dpl2d_Circle(dpl2d_DISPLAY *list, Scalar x, Scalar y, Scalar radius, int fill)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
Command cmd;
|
|
cmd.kind = fill ? Command::kCircleFill : Command::kCircleOutline;
|
|
cmd.color = self->currentColor;
|
|
cmd.x = (float)x + self->transX;
|
|
cmd.y = (float)y + self->transY;
|
|
cmd.radius = (float)radius;
|
|
self->commands.push_back(cmd);
|
|
}
|
|
|
|
void dpl2d_PushMatrix(dpl2d_DISPLAY *list)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
Vec2 saved = { self->transX, self->transY };
|
|
self->matrixStack.push_back(saved);
|
|
}
|
|
|
|
void dpl2d_PopMatrix(dpl2d_DISPLAY *list)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0 || self->matrixStack.empty()) return;
|
|
Vec2 saved = self->matrixStack.back();
|
|
self->matrixStack.pop_back();
|
|
self->transX = saved.x;
|
|
self->transY = saved.y;
|
|
}
|
|
|
|
//
|
|
// MoveTo: position the pen. In the recovered usage it is bracketed by
|
|
// Push/Pop with no draw in between (a no-op marker stamp), so it only updates
|
|
// the pen + the current translation; nothing is emitted.
|
|
//
|
|
void dpl2d_MoveTo(dpl2d_DISPLAY *list, Scalar x, Scalar y)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
self->penX = (float)x;
|
|
self->penY = (float)y;
|
|
self->transX = (float)x;
|
|
self->transY = (float)y;
|
|
}
|
|
|
|
void dpl2d_End(dpl2d_DISPLAY *list)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
self->recording = false;
|
|
}
|
|
|
|
void dpl2d_Compile(dpl2d_DISPLAY *list)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
self->compiled = true; // geometry is tessellated lazily at execute time
|
|
}
|
|
|
|
//===========================================================================//
|
|
// Executor -- rasterise a compiled list on the supplied device.
|
|
//===========================================================================//
|
|
|
|
void dpl2d_ExecuteList(dpl2d_DISPLAY *list, IDirect3DDevice9 *device)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0 || device == 0 || self->commands.empty())
|
|
return;
|
|
|
|
D3DVIEWPORT9 vp;
|
|
if (FAILED(device->GetViewport(&vp)))
|
|
return;
|
|
const float ox = (float)vp.X;
|
|
const float oy = (float)vp.Y;
|
|
const float vw = (float)vp.Width;
|
|
const float vh = (float)vp.Height;
|
|
|
|
// --- save the render states we touch ---------------------------------
|
|
DWORD oldLighting, oldZ, oldCull, oldAlpha, oldSrc, oldDst, oldFog;
|
|
device->GetRenderState(D3DRS_LIGHTING, &oldLighting);
|
|
device->GetRenderState(D3DRS_ZENABLE, &oldZ);
|
|
device->GetRenderState(D3DRS_CULLMODE, &oldCull);
|
|
device->GetRenderState(D3DRS_ALPHABLENDENABLE, &oldAlpha);
|
|
device->GetRenderState(D3DRS_SRCBLEND, &oldSrc);
|
|
device->GetRenderState(D3DRS_DESTBLEND, &oldDst);
|
|
device->GetRenderState(D3DRS_FOGENABLE, &oldFog);
|
|
IDirect3DBaseTexture9 *oldTex = 0;
|
|
device->GetTexture(0, &oldTex);
|
|
|
|
device->SetTexture(0, 0);
|
|
device->SetRenderState(D3DRS_LIGHTING, FALSE);
|
|
device->SetRenderState(D3DRS_ZENABLE, FALSE);
|
|
device->SetRenderState(D3DRS_FOGENABLE, FALSE);
|
|
device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
|
|
device->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
|
|
device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
|
|
device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
|
|
device->SetFVF(kVertex2DFVF);
|
|
|
|
Vertex2D ring[kCircleSegments + 2];
|
|
|
|
for (size_t i = 0; i < self->commands.size(); ++i)
|
|
{
|
|
const Command &cmd = self->commands[i];
|
|
|
|
// normalised view coords -> pixels. radius scales with viewport width
|
|
// so it stays round on a non-square viewport.
|
|
const float cx = ox + cmd.x * vw;
|
|
const float cy = oy + cmd.y * vh;
|
|
const float rx = cmd.radius * vw;
|
|
const float ry = cmd.radius * vw;
|
|
|
|
if (cmd.kind == Command::kCircleFill)
|
|
{
|
|
// triangle fan: centre + rim, closed back to the first rim vertex.
|
|
ring[0].x = cx; ring[0].y = cy; ring[0].z = 0.0f; ring[0].rhw = 1.0f;
|
|
ring[0].color = cmd.color;
|
|
for (int s = 0; s <= kCircleSegments; ++s)
|
|
{
|
|
float a = (2.0f * kPi * s) / kCircleSegments;
|
|
ring[s + 1].x = cx + cosf(a) * rx;
|
|
ring[s + 1].y = cy + sinf(a) * ry;
|
|
ring[s + 1].z = 0.0f;
|
|
ring[s + 1].rhw = 1.0f;
|
|
ring[s + 1].color = cmd.color;
|
|
}
|
|
device->DrawPrimitiveUP(
|
|
D3DPT_TRIANGLEFAN, kCircleSegments,
|
|
ring, sizeof(Vertex2D));
|
|
}
|
|
else // kCircleOutline
|
|
{
|
|
// line strip around the rim, closed.
|
|
for (int s = 0; s <= kCircleSegments; ++s)
|
|
{
|
|
float a = (2.0f * kPi * s) / kCircleSegments;
|
|
ring[s].x = cx + cosf(a) * rx;
|
|
ring[s].y = cy + sinf(a) * ry;
|
|
ring[s].z = 0.0f;
|
|
ring[s].rhw = 1.0f;
|
|
ring[s].color = cmd.color;
|
|
}
|
|
device->DrawPrimitiveUP(
|
|
D3DPT_LINESTRIP, kCircleSegments,
|
|
ring, sizeof(Vertex2D));
|
|
}
|
|
}
|
|
|
|
// --- restore ----------------------------------------------------------
|
|
device->SetTexture(0, oldTex);
|
|
if (oldTex) oldTex->Release();
|
|
device->SetRenderState(D3DRS_LIGHTING, oldLighting);
|
|
device->SetRenderState(D3DRS_ZENABLE, oldZ);
|
|
device->SetRenderState(D3DRS_FOGENABLE, oldFog);
|
|
device->SetRenderState(D3DRS_CULLMODE, oldCull);
|
|
device->SetRenderState(D3DRS_ALPHABLENDENABLE, oldAlpha);
|
|
device->SetRenderState(D3DRS_SRCBLEND, oldSrc);
|
|
device->SetRenderState(D3DRS_DESTBLEND, oldDst);
|
|
}
|