s2cfgtojson
Version:
Converts Stalker 2 Cfg file into POJOs
179 lines (178 loc) • 6.25 kB
JavaScript
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 };
}