mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-07-19 00:37:07 +00:00
306 lines
9.9 KiB
TypeScript
306 lines
9.9 KiB
TypeScript
import pako from "pako";
|
|
import {
|
|
GrfCompressionLevel,
|
|
GrfCompressionType,
|
|
GrfFile,
|
|
GrfFileEntry,
|
|
GrfHeader,
|
|
GrfNode
|
|
} from "../types/grf";
|
|
export class GrfService {
|
|
private static textDecoder = new TextDecoder("ascii");
|
|
private static textEncoder = new TextEncoder();
|
|
private static rleDecode(data: Uint8Array): Uint8Array {
|
|
const output: number[] = [];
|
|
const marker = 0xff;
|
|
const maxLength = 0xfe;
|
|
let i = 0;
|
|
while (i < data.length) {
|
|
const val = data[i++];
|
|
if (val !== marker) {
|
|
output.push(val);
|
|
} else {
|
|
if (i >= data.length) throw new Error("Invalid RLE");
|
|
const next = data[i++];
|
|
if (next === marker) {
|
|
output.push(marker);
|
|
} else {
|
|
const length = next;
|
|
if (length > 2 && length <= maxLength) {
|
|
if (i >= data.length) throw new Error("Invalid RLE");
|
|
const runVal = data[i++];
|
|
for (let j = 0; j < length + 1; j++) {
|
|
output.push(runVal);
|
|
}
|
|
} else {
|
|
output.push(length);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return new Uint8Array(output);
|
|
}
|
|
|
|
private static rleEncode(data: Uint8Array): Uint8Array {
|
|
const output: number[] = [];
|
|
if (data.length === 0) return new Uint8Array(0);
|
|
const marker = 0xff;
|
|
const maxLength = 0xfe;
|
|
let firstRunValue = data[0];
|
|
let runLength = 1;
|
|
const makeRun = (value: number, length: number) => {
|
|
if ((length <= 3 && value !== marker) || length <= 1) {
|
|
for (let i = 0; i < length; i++) {
|
|
if (value === marker) {
|
|
output.push(marker, marker);
|
|
} else {
|
|
output.push(value);
|
|
}
|
|
}
|
|
} else {
|
|
output.push(marker);
|
|
output.push(length - 1);
|
|
output.push(value);
|
|
}
|
|
};
|
|
|
|
for (let i = 1; i < data.length; i++) {
|
|
const currentValue = data[i];
|
|
if (currentValue === firstRunValue) {
|
|
runLength++;
|
|
} else {
|
|
makeRun(firstRunValue, runLength);
|
|
firstRunValue = currentValue;
|
|
runLength = 1;
|
|
}
|
|
|
|
if (runLength > maxLength) {
|
|
makeRun(firstRunValue, maxLength);
|
|
runLength -= maxLength;
|
|
}
|
|
}
|
|
makeRun(firstRunValue, runLength);
|
|
return new Uint8Array(output);
|
|
}
|
|
|
|
private static readStringList(view: DataView, off: { p: number }): string[] {
|
|
const count = view.getInt32(off.p, false);
|
|
off.p += 4;
|
|
const list: string[] = [];
|
|
for (let i = 0; i < count; i++) {
|
|
const len = view.getInt16(off.p, false);
|
|
off.p += 2;
|
|
const str = this.textDecoder.decode(new Uint8Array(view.buffer, view.byteOffset + off.p, len));
|
|
off.p += len;
|
|
list.push(str);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
public static readGRF(buffer: ArrayBuffer): GrfFile {
|
|
const view = new DataView(buffer);
|
|
let off = { p: 0 };
|
|
const firstWord = view.getUint16(off.p, false);
|
|
off.p += 2;
|
|
let header: GrfHeader;
|
|
if (firstWord === 0) {
|
|
off.p += 14;
|
|
header = {
|
|
compressionLevel: GrfCompressionLevel.None,
|
|
crc: 0xffffffff,
|
|
compressionType: GrfCompressionType.Unknown,
|
|
unknownData: new Uint8Array([0, 0, 0, 0])
|
|
};
|
|
} else {
|
|
const compressionLevel = view.getUint8(off.p++) as GrfCompressionLevel;
|
|
const crc = view.getUint32(off.p, false);
|
|
off.p += 4;
|
|
const unknownData = new Uint8Array(view.buffer, view.byteOffset + off.p, 4);
|
|
off.p += 4;
|
|
if (unknownData[3] > 0) {
|
|
throw new Error("World grf's are not currently supported.");
|
|
}
|
|
header = {
|
|
compressionLevel,
|
|
crc,
|
|
compressionType: GrfCompressionType.Zlib,
|
|
unknownData
|
|
};
|
|
}
|
|
|
|
let bodyData: Uint8Array;
|
|
let bodyView: DataView;
|
|
let bodyOff = { p: 0 };
|
|
if (header.compressionLevel !== GrfCompressionLevel.None) {
|
|
view.getInt32(off.p, false); //neo: this is decompressedSize
|
|
off.p += 4;
|
|
const compressedSize = view.getInt32(off.p, false);
|
|
off.p += 4;
|
|
const compressedData = new Uint8Array(view.buffer, view.byteOffset + off.p, compressedSize);
|
|
off.p += compressedSize;
|
|
let decompressed: Uint8Array;
|
|
if (header.compressionType === GrfCompressionType.Zlib || true) {
|
|
try {
|
|
decompressed = pako.inflate(compressedData);
|
|
} catch {
|
|
decompressed = pako.inflateRaw(compressedData); //neo: fallback to pako.inflateRaw if raw deflate
|
|
}
|
|
}
|
|
|
|
if (header.compressionLevel > GrfCompressionLevel.Compressed) {
|
|
bodyData = this.rleDecode(decompressed!);
|
|
} else {
|
|
bodyData = decompressed!;
|
|
}
|
|
|
|
bodyView = new DataView(bodyData.buffer, bodyData.byteOffset, bodyData.byteLength);
|
|
} else {
|
|
bodyData = new Uint8Array(view.buffer, view.byteOffset + off.p);
|
|
bodyView = new DataView(bodyData.buffer, bodyData.byteOffset, bodyData.byteLength);
|
|
}
|
|
|
|
const lut = this.readStringList(bodyView, bodyOff);
|
|
const fileCount = bodyView.getInt32(bodyOff.p, false);
|
|
bodyOff.p += 4;
|
|
const files: GrfFileEntry[] = [];
|
|
for (let i = 0; i < fileCount; i++) {
|
|
const nlen = bodyView.getInt16(bodyOff.p, false);
|
|
bodyOff.p += 2;
|
|
const filename = this.textDecoder.decode(new Uint8Array(bodyView.buffer, bodyView.byteOffset + bodyOff.p, nlen));
|
|
bodyOff.p += nlen;
|
|
const size = bodyView.getInt32(bodyOff.p, false);
|
|
bodyOff.p += 4;
|
|
const data = new Uint8Array(bodyView.buffer, bodyView.byteOffset + bodyOff.p, size).slice();
|
|
bodyOff.p += size;
|
|
files.push({ filename, data });
|
|
}
|
|
|
|
const root: GrfNode = { name: "__ROOT__", parameters: [], children: [] };
|
|
this.readGameRuleHierarchy(bodyView, bodyOff, root, lut);
|
|
return { header, files, root };
|
|
}
|
|
|
|
private static readGameRuleHierarchy(view: DataView, off: { p: number }, parent: GrfNode, lut: string[]) {
|
|
const count = view.getInt32(off.p, false);
|
|
off.p += 4;
|
|
for (let i = 0; i < count; i++) {
|
|
const nameIdx = view.getInt32(off.p, false);
|
|
off.p += 4;
|
|
const paramCount = view.getInt32(off.p, false);
|
|
off.p += 4;
|
|
|
|
const node: GrfNode = {
|
|
name: lut[nameIdx],
|
|
parameters: [],
|
|
children: []
|
|
};
|
|
|
|
for (let j = 0; j < paramCount; j++) {
|
|
const kIdx = view.getInt32(off.p, false);
|
|
off.p += 4;
|
|
const vLen = view.getInt16(off.p, false);
|
|
off.p += 2;
|
|
const value = this.textDecoder.decode(new Uint8Array(view.buffer, view.byteOffset + off.p, vLen));
|
|
off.p += vLen;
|
|
node.parameters.push({ name: lut[kIdx], value });
|
|
}
|
|
|
|
parent.children.push(node);
|
|
this.readGameRuleHierarchy(view, off, node, lut);
|
|
}
|
|
}
|
|
|
|
private static makeString(s: string): Uint8Array {
|
|
const enc = this.textEncoder.encode(s);
|
|
const out = new Uint8Array(2 + enc.length);
|
|
new DataView(out.buffer).setInt16(0, enc.length, false);
|
|
out.set(enc, 2);
|
|
return out;
|
|
}
|
|
|
|
public static serializeGRF(grf: GrfFile): ArrayBuffer {
|
|
const lut: string[] = [];
|
|
const buildLut = (node: GrfNode) => {
|
|
if (!lut.includes(node.name) && node.name !== "__ROOT__") lut.push(node.name);
|
|
for (const p of node.parameters) {
|
|
if (!lut.includes(p.name)) lut.push(p.name);
|
|
}
|
|
for (const c of node.children) buildLut(c);
|
|
};
|
|
buildLut(grf.root);
|
|
let parts: Uint8Array[] = [];
|
|
const lutCount = new Uint8Array(4);
|
|
new DataView(lutCount.buffer).setInt32(0, lut.length, false);
|
|
parts.push(lutCount);
|
|
for (const s of lut) parts.push(this.makeString(s));
|
|
const fCount = new Uint8Array(4);
|
|
new DataView(fCount.buffer).setInt32(0, grf.files.length, false);
|
|
parts.push(fCount);
|
|
for (const f of grf.files) {
|
|
parts.push(this.makeString(f.filename));
|
|
const sz = new Uint8Array(4);
|
|
new DataView(sz.buffer).setInt32(0, f.data.length, false);
|
|
parts.push(sz);
|
|
parts.push(f.data);
|
|
}
|
|
|
|
const writeNode = (node: GrfNode) => {
|
|
const cCount = new Uint8Array(4);
|
|
new DataView(cCount.buffer).setInt32(0, node.children.length, false);
|
|
parts.push(cCount);
|
|
for (const c of node.children) {
|
|
const nid = new Uint8Array(4);
|
|
new DataView(nid.buffer).setInt32(0, lut.indexOf(c.name), false);
|
|
parts.push(nid);
|
|
const pCount = new Uint8Array(4);
|
|
new DataView(pCount.buffer).setInt32(0, c.parameters.length, false);
|
|
parts.push(pCount);
|
|
for (const p of c.parameters) {
|
|
const pid = new Uint8Array(4);
|
|
new DataView(pid.buffer).setInt32(0, lut.indexOf(p.name), false);
|
|
parts.push(pid);
|
|
parts.push(this.makeString(p.value));
|
|
}
|
|
|
|
writeNode(c);
|
|
}
|
|
};
|
|
writeNode(grf.root);
|
|
let uncompressedSize = parts.reduce((acc, p) => acc + p.length, 0);
|
|
const uncompressedData = new Uint8Array(uncompressedSize);
|
|
let offset = 0;
|
|
for (const p of parts) {
|
|
uncompressedData.set(p, offset);
|
|
offset += p.length;
|
|
}
|
|
|
|
let bodyData = uncompressedData;
|
|
let header = grf.header;
|
|
if (header.compressionLevel >= GrfCompressionLevel.CompressedRle) {
|
|
bodyData = this.rleEncode(bodyData);
|
|
}
|
|
|
|
let compressedSize = 0;
|
|
if (header.compressionLevel >= GrfCompressionLevel.Compressed) {
|
|
bodyData = pako.deflate(bodyData, { level: 9 });
|
|
compressedSize = bodyData.length;
|
|
}
|
|
|
|
const finalBuffer = new Uint8Array(10 + (header.compressionLevel >= GrfCompressionLevel.Compressed ? 8 : 0) + bodyData.length);
|
|
const fw = new DataView(finalBuffer.buffer);
|
|
let poff = 0;
|
|
fw.setInt16(poff, 1, false); poff += 2;
|
|
fw.setUint8(poff, header.compressionLevel); poff += 1;
|
|
fw.setUint32(poff, header.crc, false); poff += 4;
|
|
finalBuffer.set(header.unknownData, poff); poff += 4;
|
|
if (header.compressionLevel >= GrfCompressionLevel.Compressed) {
|
|
fw.setInt32(poff, uncompressedSize, false); poff += 4;
|
|
fw.setInt32(poff, compressedSize, false); poff += 4;
|
|
}
|
|
|
|
finalBuffer.set(bodyData as any, poff);
|
|
|
|
return finalBuffer.buffer;
|
|
}
|
|
}
|