s2cfgtojson 2.2.13 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Struct.mts CHANGED
@@ -1,262 +1,328 @@
1
- const defaultEntries = new Set(["_isArray", "_useAsterisk"]);
2
1
  export * from "./types.mts";
3
2
  export * from "./enums.mts";
4
- import { DefaultEntries, Value, Entries, Struct as IStruct } from "./types.mts";
3
+ import { DefaultEntries, Struct as IStruct, Value } from "./types.mts";
4
+
5
+ const TAB = " ";
6
+ const WILDCARD = "_wildcard";
7
+ const KEYWORDS = [
8
+ "refurl", // a file path to override
9
+ "refkey", // SID to override
10
+ "bskipref", // ??? not sure
11
+ "bpatch", // allows patching only specific keys
12
+ ];
13
+ const REMOVE_NODE = "removenode";
5
14
 
6
15
  /**
7
16
  * This file is part of the Stalker 2 Modding Tools project.
8
17
  * This is a base class for all structs.
9
18
  */
10
- export abstract class Struct<T extends Entries = {}> implements IStruct<T> {
11
- isRoot?: boolean;
12
- refurl?: string;
13
- refkey?: string | number;
14
- bskipref?: boolean;
15
- abstract _id: string;
16
- entries: T;
19
+ export abstract class Struct implements IStruct {
20
+ __internal__: Refs = new Refs();
17
21
 
18
- static TAB = " ";
19
- static pad(text: string): string {
20
- return `${Struct.TAB}${text.replace(/\n+/g, `\n${Struct.TAB}`)}`;
21
- }
22
- static WILDCARD = "_wildcard";
23
- static isNumber(ref: string): boolean {
24
- return Number.isInteger(parseInt(ref)) || typeof ref === "number";
25
- }
26
- static isArrayKey(key: string) {
27
- return key.includes("[") && key.includes("]");
28
- }
29
- static renderKeyName(ref: string, useAsterisk?: boolean): string {
30
- if (`${ref}`.startsWith("_")) {
31
- return Struct.renderKeyName(ref.slice(1), useAsterisk); // Special case for indexed structs
32
- }
33
- if (`${ref}`.includes("*") || useAsterisk) {
34
- return "[*]"; // Special case for wildcard structs
22
+ /**
23
+ * Creates a new struct instance.
24
+ */
25
+ constructor(parentOrRaw?: string | Struct) {
26
+ if (parentOrRaw instanceof Struct) {
27
+ Object.assign(this, parentOrRaw.clone());
35
28
  }
36
- if (`${ref}`.includes("_dupe_")) {
37
- return Struct.renderKeyName(ref.slice(0, ref.indexOf("_dupe_")));
29
+ if (typeof parentOrRaw === "string") {
30
+ Object.assign(this, Struct.fromString(parentOrRaw)[0]);
38
31
  }
39
- if (Struct.isNumber(ref)) {
40
- return `[${parseInt(ref)}]`;
41
- }
42
- return ref;
43
32
  }
44
33
 
45
- static renderStructName(name: string): string {
46
- if (name === Struct.WILDCARD) {
47
- return "[*]"; // Special case for wildcard structs
48
- }
49
- if (`${name}`.startsWith("_")) {
50
- return Struct.renderStructName(name.slice(1)); // Special case for indexed structs
51
- }
52
- if (Struct.isNumber(name)) {
53
- return `[${parseInt(name)}]`;
54
- }
55
- return name;
56
- }
34
+ fork(clone = false) {
35
+ const patch = clone
36
+ ? this.clone()
37
+ : createDynamicClassInstance(
38
+ this.__internal__.rawName || this.constructor.name,
39
+ );
57
40
 
58
- static extractKeyFromBrackets(key: string) {
59
- if (/\[(.+)]/.test(key)) {
60
- return key.match(/\[(.+)]/)[1];
41
+ function markAsbPatch(s: Struct) {
42
+ s.__internal__.bpatch = true;
43
+ Object.values(s)
44
+ .filter((v) => v instanceof Struct)
45
+ .forEach(markAsbPatch);
61
46
  }
62
- return "";
47
+
48
+ markAsbPatch(patch);
49
+ return patch as this;
63
50
  }
64
51
 
65
- static parseStructName(name: string): string {
66
- if (Struct.extractKeyFromBrackets(name) === "*") {
67
- return Struct.WILDCARD; // Special case for wildcard structs
68
- }
69
- if (Struct.isNumber(Struct.extractKeyFromBrackets(name))) {
70
- return `_${name.match(/\[(\d+)]/)[1]}`; // Special case for indexed structs
52
+ removeNode(key: keyof this) {
53
+ if (this.__internal__.bpatch !== true) {
54
+ throw new Error(
55
+ "Cannot remove node from non-patch struct. Use fork() first.",
56
+ );
71
57
  }
72
- return name
73
- .replace(/\W/g, "_")
74
- .replace(/^\d+/, "_")
75
- .replace(/_+/g, "_")
76
- .replace(/^_+/, "");
58
+ this[key] = REMOVE_NODE as any;
59
+ return this;
77
60
  }
78
61
 
79
- static createDynamicClass = (name: string): new () => Struct =>
80
- new Function("parent", `return class ${name} extends parent {}`)(Struct);
62
+ clone() {
63
+ return Struct.fromString(this.toString())[0] as this;
64
+ }
81
65
 
82
66
  toString(): string {
83
- let text: string;
84
- text = this.isRoot ? `${this._id} : ` : "";
67
+ if (!(this.__internal__ instanceof Refs)) {
68
+ this.__internal__ = new Refs(this.__internal__);
69
+ }
70
+ const { __internal__: internal, ...entries } = this;
71
+
72
+ let text: string = internal.rawName ? `${internal.rawName} : ` : "";
85
73
  text += "struct.begin";
86
- const refs = ["refurl", "refkey", "bskipref"]
87
- .map((k) => [k, this[k]])
88
- .filter(([_, v]) => v !== "" && v !== undefined && v !== false)
89
- .map(([k, v]) => {
90
- if (v === true) return k;
91
- return `${k}=${Struct.renderKeyName(v)}`;
92
- })
93
- .join(";");
74
+
75
+ const refs = internal.toString();
94
76
  if (refs) {
95
77
  text += ` {${refs}}`;
96
78
  }
79
+
97
80
  text += "\n";
98
81
  // Add all keys
99
- text += Object.entries(this.entries || {})
100
- .filter(([key]) => key !== "_useAsterisk" && key !== "_isArray")
82
+ text += Object.entries(entries || {})
101
83
  .map(([key, value]) => {
102
- const keyOrIndex = Struct.renderKeyName(key, this.entries._useAsterisk);
103
- const equalsOrColon = value instanceof Struct ? ":" : "=";
104
- const spaceOrNoSpace = value === "" ? "" : " ";
105
- return Struct.pad(
106
- `${keyOrIndex} ${equalsOrColon}${spaceOrNoSpace}${value}`,
107
- );
84
+ const nameAlreadyRendered =
85
+ value instanceof Struct && value.__internal__.rawName;
86
+ const useAsterisk = internal.isArray && internal.useAsterisk;
87
+ let keyOrIndex = "";
88
+ let equalsOrColon = "";
89
+ let spaceOrNoSpace = "";
90
+ if (!nameAlreadyRendered) {
91
+ keyOrIndex = renderKeyName(key, useAsterisk) + " ";
92
+ equalsOrColon = value instanceof Struct ? ":" : "=";
93
+ spaceOrNoSpace = value === "" ? "" : " ";
94
+ }
95
+
96
+ return pad(`${keyOrIndex}${equalsOrColon}${spaceOrNoSpace}${value}`);
108
97
  })
109
98
  .join("\n");
110
99
  text += "\nstruct.end";
111
100
  return text;
112
101
  }
113
102
 
114
- toTs(pretty = false): string {
115
- const collect = (struct: Struct) => {
116
- const obj = {};
117
- if (struct.entries) {
118
- Object.entries(struct.entries)
119
- .filter(([key]) => !defaultEntries.has(key))
120
- .forEach(([key, value]) => {
121
- if (value instanceof Struct) {
122
- obj[key] = collect(value);
123
- } else {
124
- obj[key] = value;
125
- }
126
- });
127
- }
128
- return obj;
129
- };
130
- return JSON.stringify(collect(this), null, pretty ? 2 : 0);
103
+ static fromString<IntendedType extends Partial<Struct> = Struct>(
104
+ text: string,
105
+ ): IntendedType[] {
106
+ return walk(text.trim().split("\n")) as IntendedType[];
131
107
  }
108
+ }
132
109
 
133
- static addEntry(
134
- parent: Struct<DefaultEntries>,
135
- key: string,
136
- value: Value,
137
- index: number,
138
- ) {
139
- parent.entries ||= {};
140
-
141
- const getKey = () => {
142
- let normKey: string | number = key;
143
- if (Struct.isArrayKey(key)) {
144
- parent.entries._isArray = true;
145
- normKey = Struct.extractKeyFromBrackets(key);
146
-
147
- if (normKey === "*") {
148
- parent.entries._useAsterisk = true;
149
- return Object.keys(parent.entries).length;
150
- }
110
+ export class Refs implements DefaultEntries {
111
+ rawName?: string;
112
+ refurl?: string;
113
+ refkey?: string | number;
114
+ bskipref?: boolean;
115
+ bpatch?: boolean;
116
+ isArray?: boolean;
117
+ useAsterisk?: boolean;
118
+
119
+ constructor(ref?: string | object) {
120
+ if (typeof ref === "string") {
121
+ ref
122
+ .split(";")
123
+ .map((ref) => ref.trim())
124
+ .filter(Boolean)
125
+ .reduce((acc, ref) => {
126
+ const [key, value] = ref.split("=");
127
+ if (KEYWORDS.includes(key.trim())) {
128
+ acc[key.trim()] = value ? value.trim() : true;
129
+ }
130
+ return acc;
131
+ }, this);
132
+ }
133
+ if (typeof ref === "object") {
134
+ Object.assign(this, ref);
135
+ }
136
+ }
151
137
 
152
- if (parent.entries[normKey] !== undefined) {
153
- return `${normKey}_dupe_${index}`;
138
+ toString() {
139
+ return KEYWORDS.map((k) => [k, this[k]])
140
+ .filter(([_, v]) => v !== "" && v !== undefined && v !== false)
141
+ .map(([k, v]) => {
142
+ if (v === true) return k;
143
+ if (k === "refkey") {
144
+ return `${k}=${renderKeyName(`${v}`, this.useAsterisk)}`;
154
145
  }
155
- return normKey;
156
- }
157
- if (parent.entries[normKey] !== undefined) {
158
- return `${normKey}_dupe_${index}`;
159
- }
160
- return normKey;
161
- };
146
+ return `${k}=${v}`;
147
+ })
148
+ .join(";");
149
+ }
150
+ }
162
151
 
163
- parent.entries[getKey()] = value;
152
+ const structHeadRegex = new RegExp(
153
+ `^(.*)\\s*:\\s*struct\\.begin\\s*({\\s*((${KEYWORDS.join("|")})\\s*(=.+)?)\\s*})?`,
154
+ );
155
+
156
+ function parseHead(line: string, index: number): Struct {
157
+ const match = line.match(structHeadRegex);
158
+ if (!match) {
159
+ throw new Error(`Invalid struct head: ${line}`);
164
160
  }
165
161
 
166
- static fromString<IntendedType extends Partial<Struct> = Struct>(
167
- text: string,
168
- ): IntendedType[] {
169
- const lines = text.trim().split("\n");
162
+ const dummy = createDynamicClassInstance(match[1].trim(), index);
163
+ if (match[3]) {
164
+ Object.assign(dummy.__internal__, new Refs(match[3]));
165
+ }
170
166
 
171
- const parseHead = (line: string, index: number): Struct => {
172
- const match = line.match(
173
- /^(.*)\s*:\s*struct\.begin\s*({\s*((refurl|refkey|bskipref)\s*(=.+)?)\s*})?/,
174
- );
175
- if (!match) {
176
- throw new Error(`Invalid struct head: ${line}`);
177
- }
178
- let name =
179
- Struct.parseStructName(match[1].trim()) || `UnnamedStruct${index}`;
180
-
181
- const dummy = new (Struct.createDynamicClass(name))();
182
- dummy._id = match[1].trim();
183
- if (match[3]) {
184
- const refs = match[3]
185
- .split(";")
186
- .map((ref) => ref.trim())
187
- .filter(Boolean)
188
- .reduce(
189
- (acc, ref) => {
190
- const [key, value] = ref.split("=");
191
- acc[key.trim()] = value ? value.trim() : true;
192
- return acc;
193
- },
194
- {} as { refurl?: string; refkey?: string; bskipref?: boolean },
195
- );
196
- if (refs.refurl) dummy.refurl = refs.refurl;
197
- if (refs.refkey) dummy.refkey = refs.refkey;
198
- if (refs.bskipref) dummy.bskipref = refs.bskipref;
199
- }
200
- return dummy as Struct;
201
- };
167
+ return dummy as Struct;
168
+ }
202
169
 
203
- const parseKeyValue = (line: string, parent: Struct): void => {
204
- const match = line.match(/^(.*?)(\s*:\s*|\s*=\s*)(.*)$/);
205
- if (!match) {
206
- throw new Error(`Invalid key-value pair: ${line}`);
207
- }
208
- const key = match[1].trim();
209
- let value: string | number | boolean = match[3].trim();
210
- if (value === "true" || value === "false") {
211
- value = value === "true";
170
+ export function pad(text: string): string {
171
+ return `${TAB}${text.replace(/\n+/g, `\n${TAB}`)}`;
172
+ }
173
+
174
+ function isNumber(ref: string): boolean {
175
+ return Number.isInteger(parseInt(ref)) || typeof ref === "number";
176
+ }
177
+
178
+ export function createDynamicClassInstance<T extends Struct = Struct>(
179
+ rawName: string,
180
+ index?: number,
181
+ ): T {
182
+ const name = parseStructName(rawName) || `UnnamedStruct${index}`;
183
+ return new (new Function(
184
+ "parent",
185
+ "Refs",
186
+ `return class ${name} extends parent {
187
+ __internal__ = new Refs({ rawName: "${rawName.trim()}" });
188
+ }`,
189
+ )(Struct, Refs))() as T;
190
+ }
191
+
192
+ function parseKeyValue(line: string, parent: Struct, index: number): void {
193
+ const match = line.match(/^(.*?)(\s*:\s*|\s*=\s*)(.*)$/);
194
+ if (!match) {
195
+ throw new Error(`Invalid key-value pair: ${line}`);
196
+ }
197
+
198
+ const key = parseKey(match[1].trim(), parent, index);
199
+
200
+ parent[key] = parseValue(match[3].trim());
201
+ }
202
+
203
+ function walk(lines: string[]) {
204
+ const roots: Struct[] = [];
205
+ const stack = [];
206
+ let index = 0;
207
+ while (index < lines.length) {
208
+ const line = lines[index++].trim();
209
+ if (line.startsWith("#") || line.startsWith("//")) {
210
+ continue; // Skip comments
211
+ }
212
+ const current = stack[stack.length - 1];
213
+
214
+ if (line.includes("struct.begin")) {
215
+ const newStruct = parseHead(line, index);
216
+ if (current) {
217
+ const key = parseKey(
218
+ renderStructName(newStruct.constructor.name),
219
+ current,
220
+ index,
221
+ );
222
+ current[key] = newStruct;
212
223
  } else {
213
- try {
214
- // understand +- 0.1f / 1. / 0.f / .1 / .1f -> ((\d*)\.?(\d+)|(\d+)\.?(\d*))f?
215
- const matches = value.match(/^(-?)(\d*)\.?(\d*)f?$/);
216
- const minus = matches[1];
217
- const first = matches[2];
218
- const second = matches[3];
219
- if (first || second) {
220
- value = parseFloat(
221
- `${minus ? "-" : ""}${first || 0}${second ? `.${second}` : ""}`,
222
- );
223
- } else {
224
- value = JSON.parse(value);
225
- }
226
- } catch (e) {}
227
- }
228
- Struct.addEntry(parent, key, value, index);
229
- };
230
- let index = 0;
231
-
232
- const walk = () => {
233
- const roots: Struct[] = [];
234
- const stack = [];
235
- while (index < lines.length) {
236
- const line = lines[index++].trim();
237
- if (line.startsWith("#") || line.startsWith("//")) {
238
- continue; // Skip comments
239
- }
240
- const current = stack[stack.length - 1];
241
- if (line.includes("struct.begin")) {
242
- const newStruct = parseHead(line, index);
243
- if (current) {
244
- const key = Struct.renderStructName(newStruct.constructor.name);
245
- Struct.addEntry(current, key, newStruct, index);
246
- } else {
247
- newStruct.isRoot = true;
248
- roots.push(newStruct);
249
- }
250
- stack.push(newStruct);
251
- } else if (line.includes("struct.end")) {
252
- stack.pop();
253
- } else if (line.includes("=") && current) {
254
- parseKeyValue(line, current);
255
- }
224
+ roots.push(newStruct);
256
225
  }
257
- return roots;
258
- };
226
+ stack.push(newStruct);
227
+ } else if (line.includes("struct.end")) {
228
+ stack.pop();
229
+ } else if (line.includes("=") && current) {
230
+ parseKeyValue(line, current, index);
231
+ }
232
+ }
233
+ return roots;
234
+ }
235
+
236
+ function parseKey(key: string, parent: Struct, index: number) {
237
+ let normKey: string | number = key;
238
+
239
+ if (key.startsWith("[") && key.endsWith("]")) {
240
+ parent.__internal__.isArray = true;
241
+ normKey = extractKeyFromBrackets(key);
259
242
 
260
- return walk() as IntendedType[];
243
+ if (normKey === "*") {
244
+ parent.__internal__.useAsterisk = true;
245
+ return Object.keys(parent).length - 1;
246
+ }
247
+
248
+ if (parent[normKey] !== undefined) {
249
+ return `${normKey}_dupe_${index}`;
250
+ }
251
+ return normKey;
252
+ }
253
+ if (parent[normKey] !== undefined) {
254
+ return `${normKey}_dupe_${index}`;
255
+ }
256
+ return normKey;
257
+ }
258
+
259
+ function parseValue(value: string): Value {
260
+ if (value === "true" || value === "false") {
261
+ return value === "true";
262
+ }
263
+ try {
264
+ // understand +- 0.1f / 1. / 0.f / .1 / .1f -> ((\d*)\.?(\d+)|(\d+)\.?(\d*))f?
265
+ const matches = value.match(/^(-?)(\d*)\.?(\d*)f?$/);
266
+ const minus = matches[1];
267
+ const first = matches[2];
268
+ const second = matches[3];
269
+ if (first || second) {
270
+ return parseFloat(
271
+ `${minus ? "-" : ""}${first || 0}${second ? `.${second}` : ""}`,
272
+ );
273
+ }
274
+ return JSON.parse(value);
275
+ } catch (e) {
276
+ return value;
277
+ }
278
+ }
279
+
280
+ function renderStructName(name: string): string {
281
+ if (name === WILDCARD) {
282
+ return "[*]"; // Special case for wildcard structs
283
+ }
284
+ if (`${name}`.startsWith("_")) {
285
+ return renderStructName(name.slice(1)); // Special case for indexed structs
286
+ }
287
+ if (isNumber(name)) {
288
+ return `[${parseInt(name)}]`;
289
+ }
290
+ return name;
291
+ }
292
+
293
+ function renderKeyName(key: string, useAsterisk?: boolean): string {
294
+ if (`${key}`.startsWith("_")) {
295
+ return renderKeyName(key.slice(1), useAsterisk); // Special case for indexed structs
296
+ }
297
+ if (`${key}`.includes("*") || useAsterisk) {
298
+ return "[*]"; // Special case for wildcard structs
299
+ }
300
+ if (`${key}`.includes("_dupe_")) {
301
+ return renderKeyName(key.slice(0, key.indexOf("_dupe_")));
302
+ }
303
+ if (isNumber(key)) {
304
+ return `[${parseInt(key)}]`;
305
+ }
306
+ return key;
307
+ }
308
+
309
+ function extractKeyFromBrackets(key: string) {
310
+ if (/\[(.+)]/.test(key)) {
311
+ return key.match(/\[(.+)]/)[1];
312
+ }
313
+ return "";
314
+ }
315
+
316
+ function parseStructName(name: string): string {
317
+ if (extractKeyFromBrackets(name) === "*") {
318
+ return WILDCARD; // Special case for wildcard structs
319
+ }
320
+ if (isNumber(extractKeyFromBrackets(name))) {
321
+ return `_${name.match(/\[(\d+)]/)[1]}`; // Special case for indexed structs
261
322
  }
323
+ return name
324
+ .replace(/\W/g, "_")
325
+ .replace(/^\d+/, "_")
326
+ .replace(/_+/g, "_")
327
+ .replace(/^_+/, "");
262
328
  }