// TXD (RenderWare Texture Dictionary) reader. // // TextureDictionary -> TextureNative*, each holding a D3D8 or D3D9 raster. // Everything is converted to RGBA8 at mip level 0. import { RW, Chunk } from './stream.js'; import { decodeDXT } from './dxt.js'; const FORMAT_MASK = 0x0f00; const FMT_1555 = 0x0100; const FMT_565 = 0x0200; const FMT_4444 = 0x0300; const FMT_LUM8 = 0x0400; const FMT_8888 = 0x0500; const FMT_888 = 0x0600; const FMT_555 = 0x0700; const EXT_PAL8 = 0x2000; const EXT_PAL4 = 0x4000; function fourCCToDXT(fourCC) { switch (fourCC) { case 0x31545844: return 1; // 'DXT1' case 0x33545844: return 3; // 'DXT3' case 0x35545844: return 5; // 'DXT5' default: return 0; } } // D3D rasters are stored BGRA in memory, so red and blue are swapped. function decodeUncompressed(data, width, height, format, palette, depth) { const out = new Uint8Array(width * height * 4); const pixels = width * height; if (palette) { for (let i = 0; i < pixels; i++) { const p = data[i] * 4; out[i * 4] = palette[p + 2]; out[i * 4 + 1] = palette[p + 1]; out[i * 4 + 2] = palette[p]; out[i * 4 + 3] = palette[p + 3]; } return out; } for (let i = 0; i < pixels; i++) { const dst = i * 4; switch (format) { case FMT_8888: { const s = i * 4; out[dst] = data[s + 2]; out[dst + 1] = data[s + 1]; out[dst + 2] = data[s]; out[dst + 3] = data[s + 3]; break; } case FMT_888: { const s = i * (depth === 24 ? 3 : 4); out[dst] = data[s + 2]; out[dst + 1] = data[s + 1]; out[dst + 2] = data[s]; out[dst + 3] = 255; break; } case FMT_565: { const c = data[i * 2] | (data[i * 2 + 1] << 8); out[dst] = ((c >> 11) & 0x1f) * 255 / 31; out[dst + 1] = ((c >> 5) & 0x3f) * 255 / 63; out[dst + 2] = (c & 0x1f) * 255 / 31; out[dst + 3] = 255; break; } case FMT_1555: case FMT_555: { const c = data[i * 2] | (data[i * 2 + 1] << 8); out[dst] = ((c >> 10) & 0x1f) * 255 / 31; out[dst + 1] = ((c >> 5) & 0x1f) * 255 / 31; out[dst + 2] = (c & 0x1f) * 255 / 31; out[dst + 3] = format === FMT_555 || c & 0x8000 ? 255 : 0; break; } case FMT_4444: { const c = data[i * 2] | (data[i * 2 + 1] << 8); out[dst] = ((c >> 8) & 0x0f) * 17; out[dst + 1] = ((c >> 4) & 0x0f) * 17; out[dst + 2] = (c & 0x0f) * 17; out[dst + 3] = ((c >> 12) & 0x0f) * 17; break; } case FMT_LUM8: { out[dst] = out[dst + 1] = out[dst + 2] = data[i]; out[dst + 3] = 255; break; } default: out[dst] = out[dst + 1] = out[dst + 2] = 128; out[dst + 3] = 255; } } return out; } function parseTextureNative(chunk) { const r = chunk.structReader(); const platform = r.u32(); r.u32(); // filter and uv addressing modes const name = r.fixedString(32); const mask = r.fixedString(32); const rasterFormat = r.u32(); let dxt = 0; let hasAlpha = false; if (platform === 9) { dxt = fourCCToDXT(r.u32()); } else { hasAlpha = !!r.u32(); } const width = r.u16(); const height = r.u16(); const depth = r.u8(); const numLevels = r.u8(); r.u8(); // raster type const flags = r.u8(); if (platform === 9) { hasAlpha = !!(flags & 1); if (!(flags & 8)) dxt = 0; } else { dxt = flags; // D3D8 stores the compression mode here (0, 1 or 3) } const format = rasterFormat & FORMAT_MASK; let palette = null; if (rasterFormat & EXT_PAL8) palette = r.bytes(256 * 4); else if (rasterFormat & EXT_PAL4) palette = r.bytes(32 * 4); const size = r.u32(); const data = r.bytes(Math.min(size, r.remaining)); let rgba; if (dxt) rgba = decodeDXT(data, width, height, dxt); else if (rasterFormat & EXT_PAL4) rgba = null; // 4 bit palettes are not used by SA else rgba = decodeUncompressed(data, width, height, format, palette, depth); return { name, mask, width, height, depth, numLevels, platform, hasAlpha, compression: dxt ? `DXT${dxt}` : palette ? 'PAL8' : `0x${format.toString(16)}`, rgba, }; } export function parseTXD(buffer) { const view = new DataView(buffer); const dict = new Chunk(view, 0); if (dict.type !== RW.TEXDICT) { throw new Error(`not a txd (root chunk is 0x${dict.type.toString(16)})`); } const textures = new Map(); const errors = []; dict.findAll(RW.TEXNATIVE).forEach((chunk, i) => { try { const tex = parseTextureNative(chunk); if (tex.rgba) textures.set(tex.name.toLowerCase(), tex); else errors.push(`${tex.name}: unsupported format ${tex.compression}`); } catch (err) { errors.push(`texture ${i}: ${err.message}`); } }); return { textures, errors }; }