LCE-Emerald-Launcher/src/services/GrfService.ts

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