UNPKG

s2cfgtojson

Version:

Converts Stalker 2 Cfg file into a POJO

259 lines (239 loc) 8.04 kB
const defaultEntries = new Set(["_isArray", "_useAsterisk"]); export * from "./types.mts"; import { DefaultEntries, Value, Entries, Struct as IStruct } from "./types.mts"; /** * This file is part of the Stalker 2 Modding Tools project. * This is a base class for all structs. */ export abstract class Struct<T extends Entries = {}> implements IStruct<T> { isRoot?: boolean; refurl?: string; refkey?: string | number; bskipref?: boolean; abstract _id: string; entries: T; static TAB = " "; static pad(text: string): string { return `${Struct.TAB}${text.replace(/\n+/g, `\n${Struct.TAB}`)}`; } static WILDCARD = "_wildcard"; static isNumber(ref: string): boolean { return Number.isInteger(parseInt(ref)) || typeof ref === "number"; } static isArrayKey(key: string) { return key.includes("[") && key.includes("]"); } static renderKeyName(ref: string, useAsterisk?: boolean): string { if (`${ref}`.startsWith("_")) { return Struct.renderKeyName(ref.slice(1), useAsterisk); // Special case for indexed structs } if (`${ref}`.includes("*") || useAsterisk) { return "[*]"; // Special case for wildcard structs } if (`${ref}`.includes("_dupe_")) { return Struct.renderKeyName(ref.slice(0, ref.indexOf("_dupe_"))); } if (Struct.isNumber(ref)) { return `[${parseInt(ref)}]`; } return ref; } static renderStructName(name: string): string { if (name === Struct.WILDCARD) { return "[*]"; // Special case for wildcard structs } if (`${name}`.startsWith("_")) { return Struct.renderStructName(name.slice(1)); // Special case for indexed structs } if (Struct.isNumber(name)) { return `[${parseInt(name)}]`; } return name; } static extractKeyFromBrackets(key: string) { if (/\[(.+)]/.test(key)) { return key.match(/\[(.+)]/)[1]; } return ""; } static parseStructName(name: string): string { if (Struct.extractKeyFromBrackets(name) === "*") { return Struct.WILDCARD; // Special case for wildcard structs } if (Struct.isNumber(Struct.extractKeyFromBrackets(name))) { return `_${name.match(/\[(\d+)]/)[1]}`; // Special case for indexed structs } return name .replace(/\W/g, "_") .replace(/^\d+/, "_") .replace(/_+/g, "_") .replace(/^_+/, ""); } static createDynamicClass = (name: string): new () => Struct => new Function("parent", `return class ${name} extends parent {}`)(Struct); toString(): string { let text: string; text = this.isRoot ? `${this._id} : ` : ""; text += "struct.begin"; const refs = ["refurl", "refkey", "bskipref"] .map((k) => [k, this[k]]) .filter(([_, v]) => v !== "" && v !== undefined && v !== false) .map(([k, v]) => { if (v === true) return k; return `${k}=${Struct.renderKeyName(v)}`; }) .join(";"); if (refs) { text += ` {${refs}}`; } text += "\n"; // Add all keys text += Object.entries(this.entries || {}) .filter(([key]) => key !== "_useAsterisk" && key !== "_isArray") .map(([key, value]) => { const keyOrIndex = Struct.renderKeyName(key, this.entries._useAsterisk); const equalsOrColon = value instanceof Struct ? ":" : "="; const spaceOrNoSpace = value === "" ? "" : " "; return Struct.pad( `${keyOrIndex} ${equalsOrColon}${spaceOrNoSpace}${value}`, ); }) .join("\n"); text += "\nstruct.end"; return text; } toTs(pretty = false): string { const collect = (struct: Struct) => { const obj = {}; if (struct.entries) { Object.entries(struct.entries) .filter(([key]) => !defaultEntries.has(key)) .forEach(([key, value]) => { if (value instanceof Struct) { obj[key] = collect(value); } else { obj[key] = value; } }); } return obj; }; return JSON.stringify(collect(this), null, pretty ? 2 : 0); } static addEntry( parent: Struct<DefaultEntries>, key: string, value: Value, index: number, ) { parent.entries ||= {}; const getKey = () => { let normKey: string | number = key; if (Struct.isArrayKey(key)) { parent.entries._isArray = true; normKey = Struct.extractKeyFromBrackets(key); if (normKey === "*") { parent.entries._useAsterisk = true; return Object.keys(parent.entries).length; } if (parent.entries[normKey] !== undefined) { return `${normKey}_dupe_${index}`; } return normKey; } if (parent.entries[normKey] !== undefined) { return `${normKey}_dupe_${index}`; } return normKey; }; parent.entries[getKey()] = value; } static fromString<IntendedType extends Partial<Struct> = Struct>( text: string, ): IntendedType[] { const lines = text.trim().split("\n"); const parseHead = (line: string, index: number): Struct => { const match = line.match( /^(.*)\s*:\s*struct\.begin\s*({\s*((refurl|refkey|bskipref)\s*(=.+)?)\s*})?/, ); if (!match) { throw new Error(`Invalid struct head: ${line}`); } let name = Struct.parseStructName(match[1].trim()) || `UnnamedStruct${index}`; const dummy = new (Struct.createDynamicClass(name))(); dummy._id = match[1].trim(); if (match[3]) { const refs = match[3] .split(";") .map((ref) => ref.trim()) .filter(Boolean) .reduce( (acc, ref) => { const [key, value] = ref.split("="); acc[key.trim()] = value ? value.trim() : true; return acc; }, {} as { refurl?: string; refkey?: string; bskipref?: boolean }, ); if (refs.refurl) dummy.refurl = refs.refurl; if (refs.refkey) dummy.refkey = refs.refkey; if (refs.bskipref) dummy.bskipref = refs.bskipref; } return dummy as Struct; }; const parseKeyValue = (line: string, parent: Struct): void => { const match = line.match(/^(.*?)(\s*:\s*|\s*=\s*)(.*)$/); if (!match) { throw new Error(`Invalid key-value pair: ${line}`); } const key = match[1].trim(); let value: string | number | boolean = match[3].trim(); if (value === "true" || value === "false") { value = value === "true"; } else { try { // understand 0.1f / 1. / 0.f / .1 / .1f -> ((\d*)\.?(\d+)|(\d+)\.?(\d*))f? const matches = value.match(/^(\d*)\.?(\d*)f?$/); const first = matches[1]; const second = matches[2]; if (first || second) { value = parseFloat(`${first || 0}${second ? `.${second}` : ""}`); } else { value = JSON.parse(value); } } catch (e) {} } Struct.addEntry(parent, key, value, index); }; let index = 0; const walk = () => { const roots: Struct[] = []; const stack = []; while (index < lines.length) { const line = lines[index++].trim(); if (line.startsWith("#") || line.startsWith("//")) { continue; // Skip comments } const current = stack[stack.length - 1]; if (line.includes("struct.begin")) { const newStruct = parseHead(line, index); if (current) { const key = Struct.renderStructName(newStruct.constructor.name); Struct.addEntry(current, key, newStruct, index); } else { newStruct.isRoot = true; roots.push(newStruct); } stack.push(newStruct); } else if (line.includes("struct.end")) { stack.pop(); } else if (line.includes("=") && current) { parseKeyValue(line, current); } } return roots; }; return walk() as IntendedType[]; } }