#include "image.h" #include #include #include namespace { std::vector readBytes(const std::string& path) { std::vector 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& 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 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& 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& 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 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{}; }