LosaDev Tools: Lost Saga modding tools in the browser

This commit is contained in:
LosaDev committed 2026-11-07 23:08:43 +07:00
commit c5e04472fb
105 files changed
+38541

No files matched your search

+182
View File
@@ -0,0 +1,182 @@
"use strict";
// Texture decoding for the UI Image Set editor: DDS (DXT1/DXT3/DXT5, 32/24/16-bit RGB masks) and
// TGA (true-colour, RLE) → ImageData. BMP/PNG/JPG go through the browser (createImageBitmap).
const TexDecode = (() => {
const rgb565 = (c) => [((c >> 11) & 31) * 255 / 31, ((c >> 5) & 63) * 255 / 63, (c & 31) * 255 / 31];
/** One 4×4 DXT colour block → 16 RGB(A) values into out (at pixel x,y). */
function colorBlock(src, o, out, w, h, x, y, dxt1, alphaSrc) {
const c0 = src[o] | (src[o + 1] << 8);
const c1 = src[o + 2] | (src[o + 3] << 8);
const p0 = rgb565(c0);
const p1 = rgb565(c1);
const pal = [p0, p1];
if (!dxt1 || c0 > c1) {
pal.push([(2 * p0[0] + p1[0]) / 3, (2 * p0[1] + p1[1]) / 3, (2 * p0[2] + p1[2]) / 3]);
pal.push([(p0[0] + 2 * p1[0]) / 3, (p0[1] + 2 * p1[1]) / 3, (p0[2] + 2 * p1[2]) / 3]);
} else {
pal.push([(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2, (p0[2] + p1[2]) / 2]);
pal.push(null); // transparent black (DXT1 1-bit alpha)
}
const bits = src[o + 4] | (src[o + 5] << 8) | (src[o + 6] << 16) | (src[o + 7] << 24);
for (let i = 0; i < 16; i++) {
const px = x + (i & 3);
const py = y + (i >> 2);
if (px >= w || py >= h) continue;
const c = pal[(bits >>> (2 * i)) & 3];
const d = (py * w + px) * 4;
if (!c) {
out[d] = out[d + 1] = out[d + 2] = out[d + 3] = 0;
continue;
}
out[d] = c[0];
out[d + 1] = c[1];
out[d + 2] = c[2];
out[d + 3] = alphaSrc ? alphaSrc[i] : 255;
}
}
function dxt(src, offset, w, h, kind) {
const out = new Uint8ClampedArray(w * h * 4);
const blockBytes = kind === "DXT1" ? 8 : 16;
let o = offset;
const alpha = new Uint8Array(16);
for (let y = 0; y < h; y += 4) {
for (let x = 0; x < w; x += 4) {
let a = null;
if (kind === "DXT3") {
for (let i = 0; i < 8; i++) {
const b = src[o + i];
alpha[i * 2] = (b & 15) * 17;
alpha[i * 2 + 1] = (b >> 4) * 17;
}
a = alpha;
} else if (kind === "DXT5") {
const a0 = src[o];
const a1 = src[o + 1];
const pal = [a0, a1];
if (a0 > a1) for (let i = 1; i < 7; i++) pal.push(((7 - i) * a0 + i * a1) / 7);
else {
for (let i = 1; i < 5; i++) pal.push(((5 - i) * a0 + i * a1) / 5);
pal.push(0, 255);
}
// 48 bits of 3-bit indices
let lo = src[o + 2] | (src[o + 3] << 8) | (src[o + 4] << 16);
let hi = src[o + 5] | (src[o + 6] << 8) | (src[o + 7] << 16);
for (let i = 0; i < 8; i++) alpha[i] = pal[(lo >> (3 * i)) & 7];
for (let i = 0; i < 8; i++) alpha[8 + i] = pal[(hi >> (3 * i)) & 7];
a = alpha;
}
colorBlock(src, o + (kind === "DXT1" ? 0 : 8), out, w, h, x, y, kind === "DXT1", a);
o += blockBytes;
}
}
return new ImageData(out, w, h);
}
const shiftOf = (mask) => {
if (!mask) return [0, 0];
let s = 0;
while (!((mask >>> s) & 1)) s++;
let n = 0;
while ((mask >>> (s + n)) & 1) n++;
return [s, n];
};
function masked(src, offset, w, h, bpp, masks, hasAlpha) {
const out = new Uint8ClampedArray(w * h * 4);
const bytes = bpp / 8;
const pitch = w * bytes;
const chans = masks.map(shiftOf);
const dv = new DataView(src.buffer, src.byteOffset, src.byteLength);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const o = offset + y * pitch + x * bytes;
const v = bytes === 4 ? dv.getUint32(o, true) : bytes === 3 ? src[o] | (src[o + 1] << 8) | (src[o + 2] << 16) : dv.getUint16(o, true);
const d = (y * w + x) * 4;
for (let c = 0; c < 3; c++) {
const [s, n] = chans[c];
out[d + c] = n ? (((v >>> s) & ((1 << n) - 1)) * 255) / ((1 << n) - 1) : 0;
}
const [as, an] = chans[3];
out[d + 3] = hasAlpha && an ? (((v >>> as) & ((1 << an) - 1)) * 255) / ((1 << an) - 1) : 255;
}
}
return new ImageData(out, w, h);
}
function dds(bytes) {
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
if (dv.getUint32(0, true) !== 0x20534444) throw new Error("Not a DDS file");
const h = dv.getUint32(12, true);
const w = dv.getUint32(16, true);
const pf = dv.getUint32(80, true);
const fourCC = String.fromCharCode(bytes[84], bytes[85], bytes[86], bytes[87]);
const offset = 128;
if (pf & 4) {
if (fourCC === "DXT1" || fourCC === "DXT3" || fourCC === "DXT5") return dxt(bytes, offset, w, h, fourCC);
if (fourCC === "DXT2" || fourCC === "DXT4") return dxt(bytes, offset, w, h, fourCC === "DXT2" ? "DXT3" : "DXT5");
throw new Error(`DDS format ${fourCC} isn't supported`);
}
const bpp = dv.getUint32(88, true);
const masks = [dv.getUint32(92, true), dv.getUint32(96, true), dv.getUint32(100, true), dv.getUint32(104, true)];
return masked(bytes, offset, w, h, bpp, masks, !!(pf & 1));
}
function tga(bytes) {
const idLen = bytes[0];
const type = bytes[2];
const w = bytes[12] | (bytes[13] << 8);
const h = bytes[14] | (bytes[15] << 8);
const bpp = bytes[16];
const topDown = (bytes[17] & 0x20) !== 0;
if (![2, 10].includes(type) || ![24, 32].includes(bpp)) throw new Error("Only true-colour TGA (24/32-bit) is supported");
const px = bpp / 8;
const out = new Uint8ClampedArray(w * h * 4);
let o = 18 + idLen;
let i = 0;
const put = (b, g, r, a) => {
const x = i % w;
const y = Math.floor(i / w);
const d = ((topDown ? y : h - 1 - y) * w + x) * 4;
out[d] = r;
out[d + 1] = g;
out[d + 2] = b;
out[d + 3] = a;
i++;
};
while (i < w * h) {
if (type === 2) {
put(bytes[o], bytes[o + 1], bytes[o + 2], px === 4 ? bytes[o + 3] : 255);
o += px;
continue;
}
const hdr = bytes[o++];
const n = (hdr & 127) + 1;
if (hdr & 128) {
const c = [bytes[o], bytes[o + 1], bytes[o + 2], px === 4 ? bytes[o + 3] : 255];
o += px;
for (let k = 0; k < n; k++) put(...c);
} else {
for (let k = 0; k < n; k++) {
put(bytes[o], bytes[o + 1], bytes[o + 2], px === 4 ? bytes[o + 3] : 255);
o += px;
}
}
}
return new ImageData(out, w, h);
}
/** Any supported texture → ImageBitmap. */
async function decode(bytes, ext) {
const e = (ext || "").toLowerCase();
if (e === "dds") return createImageBitmap(dds(bytes));
if (e === "tga") return createImageBitmap(tga(bytes));
return createImageBitmap(new Blob([bytes]));
}
return { decode, dds, tga };
})();
window.TexDecode = TexDecode;