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>
181 lines
7.5 KiB
C++
181 lines
7.5 KiB
C++
#include "image.h"
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
|
|
namespace {
|
|
|
|
std::vector<uint8_t> readBytes(const std::string& path) {
|
|
std::vector<uint8_t> b;
|
|
FILE* f = std::fopen(path.c_str(), "rb");
|
|
if (!f) return b;
|
|
std::fseek(f, 0, SEEK_END);
|
|
long n = std::ftell(f);
|
|
std::fseek(f, 0, SEEK_SET);
|
|
if (n > 0) { b.resize((size_t)n); if (std::fread(b.data(), 1, b.size(), f) != b.size()) b.clear(); }
|
|
std::fclose(f);
|
|
return b;
|
|
}
|
|
|
|
uint16_t u16(const uint8_t* p) { return (uint16_t)(p[0] | (p[1] << 8)); }
|
|
uint32_t u32(const uint8_t* p) { return (uint32_t)(p[0] | (p[1] << 8) | (p[2] << 16) | ((uint32_t)p[3] << 24)); }
|
|
uint32_t argb(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255) {
|
|
return ((uint32_t)a << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
|
|
}
|
|
|
|
// ---- TGA (uncompressed type 2 and RLE type 10; 24/32 bpp) ----
|
|
Image decodeTGA(const std::vector<uint8_t>& d) {
|
|
Image im;
|
|
if (d.size() < 18) return im;
|
|
uint8_t idlen = d[0], type = d[2], bpp = d[16], desc = d[17];
|
|
int w = u16(&d[12]), h = u16(&d[14]);
|
|
if ((type != 2 && type != 10) || (bpp != 24 && bpp != 32) || w <= 0 || h <= 0) return im;
|
|
int bytespp = bpp / 8;
|
|
size_t off = 18 + idlen;
|
|
std::vector<uint32_t> px((size_t)w * h);
|
|
size_t pi = 0;
|
|
auto put = [&](const uint8_t* p) {
|
|
px[pi++] = argb(p[2], p[1], p[0], bytespp == 4 ? p[3] : 255); // TGA is BGR(A)
|
|
};
|
|
if (type == 2) {
|
|
if (off + (size_t)w * h * bytespp > d.size()) return im;
|
|
for (int i = 0; i < w * h; ++i) put(&d[off + (size_t)i * bytespp]);
|
|
} else { // RLE
|
|
while (pi < px.size() && off < d.size()) {
|
|
uint8_t c = d[off++];
|
|
int cnt = (c & 0x7f) + 1;
|
|
if (c & 0x80) {
|
|
if (off + bytespp > d.size()) break;
|
|
for (int k = 0; k < cnt && pi < px.size(); ++k) put(&d[off]);
|
|
off += bytespp;
|
|
} else {
|
|
for (int k = 0; k < cnt && pi < px.size(); ++k) { if (off + bytespp > d.size()) break; put(&d[off]); off += bytespp; }
|
|
}
|
|
}
|
|
}
|
|
// TGA is bottom-up unless descriptor bit 5 is set: flip to top-down if needed.
|
|
im.w = w; im.h = h; im.argb.resize(px.size());
|
|
bool topDown = (desc & 0x20) != 0;
|
|
for (int y = 0; y < h; ++y) {
|
|
int src = topDown ? y : (h - 1 - y);
|
|
std::memcpy(&im.argb[(size_t)y * w], &px[(size_t)src * w], (size_t)w * 4);
|
|
}
|
|
im.ok = true;
|
|
return im;
|
|
}
|
|
|
|
// ---- DIV TLV walk (shared by VTX): find first leaf with tag id, recursing containers ----
|
|
const uint8_t* findTag(const uint8_t* p, const uint8_t* end, uint16_t want, size_t& outLen) {
|
|
while (p + 3 <= end) {
|
|
uint16_t tw = u16(p); p += 2;
|
|
uint16_t id = tw & 0x2fff;
|
|
int lw = (tw & 0x8000) ? 4 : (tw & 0x4000) ? 2 : 1;
|
|
if (p + lw > end) return nullptr;
|
|
size_t len = (lw == 1) ? *p : (lw == 2) ? u16(p) : u32(p);
|
|
p += lw;
|
|
if (p + len > end) return nullptr;
|
|
if (id == want) { outLen = len; return p; }
|
|
if (id == 0x0003 || id == 0x0060) { // HEADER / image container
|
|
size_t l2; const uint8_t* r = findTag(p, p + len, want, l2);
|
|
if (r) { outLen = l2; return r; }
|
|
}
|
|
p += len;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// ---- VTX (DIV-VTX2): size tag 0x2062 = (w,h) int32; pixels = trailing w*h*3 RGB ----
|
|
Image decodeVTX(const std::vector<uint8_t>& d) {
|
|
Image im;
|
|
if (d.size() < 16) return im;
|
|
size_t len = 0;
|
|
const uint8_t* sz = findTag(d.data() + 8, d.data() + d.size(), 0x2062, len);
|
|
if (!sz || len < 8) return im;
|
|
int w = (int)u32(sz), h = (int)u32(sz + 4);
|
|
if (w <= 0 || h <= 0 || (size_t)w * h * 3 > d.size()) return im;
|
|
const uint8_t* px = d.data() + d.size() - (size_t)w * h * 3; // trailing RGB block
|
|
im.w = w; im.h = h; im.argb.resize((size_t)w * h);
|
|
for (int i = 0; i < w * h; ++i) im.argb[i] = argb(px[i * 3], px[i * 3 + 1], px[i * 3 + 2]);
|
|
im.ok = true;
|
|
return im;
|
|
}
|
|
|
|
// ---- BSL (DIV-BSL2): a BIT-SLICED texture container ----
|
|
// Header (all int32 LE): w @0x08, h @0x0C, depth @0x10 (bits per slice, always
|
|
// 4), tableBytes @0x14 (directory length counted from the nEntries field),
|
|
// nEntries @0x18; then the entry directory {int32 recLen; int32 sliceType;
|
|
// char name[recLen-4]} x nEntries (names are authoring records only -- the
|
|
// runtime selects by the BMF BITSLICE tag, never by name); then w*h little-
|
|
// endian 32-bit texel WORDS. Byte 0 of every word is pad; the other 3 bytes
|
|
// hold six 4-bit slices, nibble PAIR-SWAPPED (even slice = HIGH nibble):
|
|
// slice 0 = bits 12-15 slice 1 = bits 8-11 slice 2 = bits 20-23
|
|
// slice 3 = bits 16-19 slice 4 = bits 28-31 slice 5 = bits 24-27
|
|
// sliceType 7 = RGB444 (r=s5,g=s4,b=s3), 8 = RGBA4444 (+a=s2).
|
|
// Byte-exact with the shipping loader: nibble is returned <<4 (0x00..0xF0).
|
|
// Reference: VGCDivLoader::LoadBSLFile/getBSLData (DivLoader/VGCDivLoader.cpp
|
|
// :323-410), Division dsys PIMAGE.H dpiBSLTYPE_MONO0..5/RGB/RGBA; corpus-
|
|
// verified on all 66 archive BSLs (predicted populated slices match measured
|
|
// in every file). BT_BSL=0 restores the old (WRONG) [pad,R,G,B] byte decode
|
|
// -- which overlaid 2-3 different gray slices as color channels = the rainbow
|
|
// "graffiti" mech skins.
|
|
uint8_t bslSlice(uint32_t word, int c) {
|
|
int shift = (c + ((c + 1) % 2) * 2) * 4; // pair swap
|
|
return (uint8_t)((word & (0xF0u << shift)) >> shift); // 0x00..0xF0
|
|
}
|
|
Image decodeBSL(const std::vector<uint8_t>& d, int channel) {
|
|
Image im;
|
|
if (d.size() < 28) return im;
|
|
int w = (int)u32(&d[8]), h = (int)u32(&d[12]);
|
|
uint32_t tableBytes = u32(&d[20]);
|
|
if (w <= 0 || h <= 0) return im;
|
|
size_t need = (size_t)w * h * 4;
|
|
size_t dataStart = 0x18 + (size_t)tableBytes;
|
|
if (dataStart + need > d.size()) {
|
|
if (need > d.size()) return im;
|
|
dataStart = d.size() - need; // damaged directory: fall back to trailing block
|
|
}
|
|
static const bool s_legacy = [] {
|
|
const char* v = getenv("BT_BSL");
|
|
return v != nullptr && v[0] == '0';
|
|
}();
|
|
if (channel < 0) channel = 0; // BMF BITSLICE tag absent = slice 0
|
|
const uint8_t* p = d.data() + dataStart;
|
|
im.w = w; im.h = h; im.argb.resize((size_t)w * h);
|
|
for (int i = 0; i < w * h; ++i) {
|
|
const uint8_t* q = p + (size_t)i * 4;
|
|
if (s_legacy) { im.argb[i] = argb(q[1], q[2], q[3]); continue; }
|
|
uint32_t word = u32(q);
|
|
if (channel <= 5) { // 4-bit grayscale slice
|
|
uint8_t g = bslSlice(word, channel);
|
|
im.argb[i] = argb(g, g, g);
|
|
} else { // 7 = RGB444, 8 = RGBA4444
|
|
uint8_t a = (channel >= 8) ? bslSlice(word, 2) : 255;
|
|
im.argb[i] = argb(bslSlice(word, 5), bslSlice(word, 4), bslSlice(word, 3), a);
|
|
}
|
|
}
|
|
im.ok = true;
|
|
return im;
|
|
}
|
|
|
|
std::string lowerExt(const std::string& path) {
|
|
size_t dot = path.find_last_of('.');
|
|
std::string e = (dot == std::string::npos) ? "" : path.substr(dot);
|
|
for (char& c : e) c = (char)tolower((unsigned char)c);
|
|
return e;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Image decodeImage(const std::string& path, int bslChannel) {
|
|
std::vector<uint8_t> d = readBytes(path);
|
|
if (d.size() < 16) return Image{};
|
|
std::string e = lowerExt(path);
|
|
if (e == ".tga") return decodeTGA(d);
|
|
if (std::memcmp(d.data(), "DIV-VTX2", 8) == 0) return decodeVTX(d);
|
|
if (std::memcmp(d.data(), "DIV-BSL2", 8) == 0) return decodeBSL(d, bslChannel);
|
|
if (e == ".vtx") return decodeVTX(d);
|
|
if (e == ".bsl") return decodeBSL(d, bslChannel);
|
|
return Image{};
|
|
}
|