Files
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

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{};
}