UNPKG

s2cfgtojson

Version:

Converts Stalker 2 Cfg file into POJOs

179 lines (178 loc) 6.25 kB
import { Struct, createDynamicClassInstance, parseKey, parseValue, } from "./Struct.mjs"; class BinaryCursor { bytes; view; position = 0; get length() { return this.bytes.length; } constructor(input) { this.bytes = input instanceof Uint8Array ? input : new Uint8Array(input); this.view = new DataView(this.bytes.buffer, this.bytes.byteOffset, this.bytes.byteLength); } readByte() { this.ensureAvailable(1); return this.view.getUint8(this.position++); } readInt32() { this.ensureAvailable(4); const value = this.view.getInt32(this.position, true); this.position += 4; return value; } readUint32() { this.ensureAvailable(4); const value = this.view.getUint32(this.position, true); this.position += 4; return value; } readBytes(length) { this.ensureAvailable(length); const start = this.position; this.position += length; return this.bytes.subarray(start, this.position); } ensureAvailable(length) { if (this.position + length > this.length) { throw new Error("Unexpected end of binary cfg data."); } } } export function readBinaryCfg(bytes) { const reader = new BinaryCursor(bytes); if (reader.length < 12) { return []; } const stringPool = readBinaryHeader(reader); skipPostPoolPadding(reader); const roots = []; while (reader.position < reader.length) { if (reader.position + 16 > reader.length) { break; } const childsCount = reader.readInt32(); for (let currentChild = 0; currentChild < childsCount; currentChild++) { const root = readBinaryStruct(reader, stringPool); root.__internal__.isRoot = true; roots.push(root); } } return roots; } function readBinaryHeader(reader) { reader.readUint32(); // version const stringCount = reader.readInt32(); const stringPool = []; reader.readUint32(); // reserved for (let i = 0; i < stringCount - 1; i++) { if (reader.position + 4 > reader.length) { break; } let length = reader.readInt32(); if (length === 0) { reader.position -= 4; break; } if (length < 0) { length = Math.abs(length); const stringBytes = reader.readBytes(length * 2); stringPool.push(Buffer.from(stringBytes.subarray(0, -2)).toString("utf16le")); continue; } const stringBytes = reader.readBytes(length); stringPool.push(stringBytes.length > 1 ? Buffer.from(stringBytes.subarray(0, -1)).toString("utf-8") : ""); } return stringPool; } function skipPostPoolPadding(reader) { if (reader.position + 9 <= reader.length) { reader.readUint32(); reader.readUint32(); reader.readByte(); } } function readBinaryCfgConfig(reader, poolSize) { const position = reader.position; const values = []; let value = 0; while (reader.position + 4 <= reader.length) { value = reader.readInt32(); // A zero/negative int32 is the block terminator. if (value <= 0) { break; } // Every value is a 1-based string-pool index. A value beyond the pool // means this block has no int32 terminator (it ends right at the // following lastByte); rewind so the lastByte is read from here. if (value > poolSize) { reader.position -= 4; break; } values.push(value); } const lastByte = reader.readByte(); return { values, lastByte, position }; } function readBinaryStruct(reader, stringPool) { const block = readBinaryCfgConfig(reader, stringPool.length); const node = createDynamicClassInstance(getBinaryString(block.values[1], stringPool)); if (block.lastByte > 0 && [1, 5, 7].includes(block.lastByte)) { const link = readBinaryLinkPair(reader); // lastByte 1/7 = inherit ref shown in text cfg; 5 = resolved field link, skip if (block.lastByte === 1 || block.lastByte === 7) { node.__internal__.refkey = link.parentName; if (link.refPath) { node.__internal__.refurl = link.refPath; } } } const fieldsCount = reader.readInt32(); for (let currentField = 0; currentField < fieldsCount; currentField++) { const fieldBlock = readBinaryCfgConfig(reader, stringPool.length); if (fieldBlock.values.length <= 1) { continue; } const fieldName = getBinaryString(fieldBlock.values[0], stringPool); switch (fieldBlock.values.length) { case 2: { reader.position = fieldBlock.position; const nested = readBinaryStruct(reader, stringPool); assignField(node, fieldName, isEmptyNestedStruct(nested) ? "" : nested, currentField); break; } case 3: case 4: assignField(node, fieldName, parseValue(getBinaryString(fieldBlock.values[2], stringPool).trim()), currentField); break; } } return node; } function assignField(parent, rawKey, value, index) { const key = parseKey(rawKey, parent, index); parent[key] = value; } function isEmptyNestedStruct(node) { return (node.entries().length === 0 && !node.__internal__.refkey && !node.__internal__.refurl && !node.__internal__.bpatch && !node.__internal__.bskipref); } function getBinaryString(index, stringPool) { return stringPool[index - 1] ?? ""; } function readBinaryLinkPair(reader) { const parentLength = reader.readInt32(); const parentBytes = reader.readBytes(parentLength); const parentName = Buffer.from(parentBytes.subarray(0, -1)).toString("utf-8"); const refLength = reader.readInt32(); let refPath; if (refLength > 0) { const refBytes = reader.readBytes(refLength); refPath = Buffer.from(refBytes.subarray(0, -1)).toString("utf-8"); } return { parentName, refPath }; }