s2cfgtojson 2.2.14 → 3.0.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, 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
  }
package/Struct.test.mts CHANGED
@@ -1,34 +1,46 @@
1
1
  import { describe, test, expect } from "vitest";
2
- import { ERank, Struct } from "./Struct.mjs";
2
+ import {
3
+ createDynamicClassInstance,
4
+ ERank,
5
+ pad,
6
+ IStruct,
7
+ Struct,
8
+ } from "./Struct.mjs";
9
+ import fs from "node:fs";
3
10
 
4
11
  class ChimeraHPFix extends Struct {
5
- _id = "ChimeraHPFix";
6
- bskipref = true;
7
- entries = { MaxHP: 750 };
8
- isRoot = true;
12
+ __internal__ = {
13
+ rawName: "ChimeraHPFix",
14
+ bskipref: true,
15
+ };
16
+
17
+ MaxHP = 750;
9
18
  }
10
19
  class TradePrototype extends Struct {
11
- _id = "TradersDontBuyWeaponsArmor";
12
- refurl = "../TradePrototypes.cfg";
13
- refkey = 0;
14
- isRoot = true;
15
- entries = { TradeGenerators: new TradeGenerators() };
20
+ __internal__ = {
21
+ rawName: "TradersDontBuyWeaponsArmor",
22
+ refurl: "../TradePrototypes.cfg",
23
+ refkey: 0,
24
+ };
25
+
26
+ TradeGenerators = new TradeGenerators();
16
27
  }
17
28
  class TradeGenerators extends Struct {
18
- _id = "TradeGenerators";
19
- entries = { "*": new TradeGenerator() };
29
+ "*" = new TradeGenerator();
20
30
  }
21
31
  class TradeGenerator extends Struct {
22
- _id = "TradeGenerator";
23
- entries = { BuyLimitations: new BuyLimitations() };
32
+ BuyLimitations = new BuyLimitations();
24
33
  }
34
+
25
35
  class BuyLimitations extends Struct {
26
- _id = "BuyLimitations";
27
- entries = { [0]: "EItemType::Weapon", [1]: "EItemType::Armor" };
36
+ [0] = "EItemType::Weapon";
37
+ [1] = "EItemType::Armor";
28
38
  }
29
39
 
30
40
  describe("Struct", () => {
31
41
  test("toString()", () => {
42
+ const c = new ChimeraHPFix();
43
+ expect(c.MaxHP).toBe(750);
32
44
  expect(new ChimeraHPFix().toString()).toBe(
33
45
  `ChimeraHPFix : struct.begin {bskipref}
34
46
  MaxHP = 750
@@ -50,23 +62,16 @@ struct.end`,
50
62
  });
51
63
 
52
64
  test("pad()", () => {
53
- expect(Struct.pad("test")).toBe(" test");
54
- expect(Struct.pad(Struct.pad("test"))).toBe(" test");
65
+ expect(pad("test")).toBe(" test");
66
+ expect(pad(pad("test"))).toBe(" test");
55
67
  });
56
68
 
57
- describe("toTs()", () => {
69
+ describe("createDynamicClassInstance", () => {
58
70
  test("1", () => {
59
- expect(new TradePrototype().toTs()).toBe(
60
- JSON.stringify({
61
- TradeGenerators: {
62
- "*": {
63
- BuyLimitations: {
64
- "0": "EItemType::Weapon",
65
- "1": "EItemType::Armor",
66
- },
67
- },
68
- },
69
- }),
71
+ const instance = createDynamicClassInstance("DynamicClass");
72
+ expect(instance).toBeInstanceOf(Struct);
73
+ expect(instance.toString()).toBe(
74
+ `DynamicClass : struct.begin\n\nstruct.end`,
70
75
  );
71
76
  });
72
77
  });
@@ -197,15 +202,57 @@ struct.end`;
197
202
  N5 = .1f
198
203
  N6 = -2.22f
199
204
  struct.end`;
200
- const str = Struct.fromString<Struct<{ [key: `N${number}`]: number }>>(
205
+ const str = Struct.fromString<IStruct & { [key: `N${number}`]: number }>(
201
206
  dynamicItemGeneratorText,
202
207
  );
203
- expect(str[0].entries.N1).toBe(0.1);
204
- expect(str[0].entries.N2).toBe(1);
205
- expect(str[0].entries.N3).toBe(0);
206
- expect(str[0].entries.N4).toBe(0.1);
207
- expect(str[0].entries.N5).toBe(0.1);
208
- expect(str[0].entries.N6).toBe(-2.22);
208
+ expect(str[0].N1).toBe(0.1);
209
+ expect(str[0].N2).toBe(1);
210
+ expect(str[0].N3).toBe(0);
211
+ expect(str[0].N4).toBe(0.1);
212
+ expect(str[0].N5).toBe(0.1);
213
+ expect(str[0].N6).toBe(-2.22);
214
+ });
215
+
216
+ test("5", () => {
217
+ const testCfg = fs.readFileSync("./test.cfg", "utf-8").trim();
218
+ const structs = Struct.fromString(testCfg);
219
+ expect(structs.map((s) => s.toString()).join("\n")).toBe(testCfg);
220
+ });
221
+ });
222
+
223
+ describe("fork", () => {
224
+ test("1", () => {
225
+ const a = new TradePrototype();
226
+ const b = a.fork() as TradePrototype;
227
+
228
+ expect(a === b).toBe(false);
229
+ b.TradeGenerators = new TradeGenerators().fork();
230
+ expect(b.toString()).toBe(
231
+ "TradersDontBuyWeaponsArmor : struct.begin {bpatch}\n" +
232
+ " TradeGenerators : struct.begin {bpatch}\n" +
233
+ " struct.end\n" +
234
+ "struct.end",
235
+ );
236
+ });
237
+ });
238
+
239
+ describe("clone", () => {
240
+ test("1", () => {
241
+ const a = new TradePrototype();
242
+ const b = a.clone() as TradePrototype;
243
+
244
+ expect(a === b).toBe(false);
245
+ expect(a.toString()).toBe(b.toString());
246
+ });
247
+ });
248
+
249
+ describe("removeNode", () => {
250
+ test("1", () => {
251
+ const a = new TradePrototype().fork(true);
252
+ expect(a.TradeGenerators[0].BuyLimitations[0]).toBe("EItemType::Weapon");
253
+ expect(a.TradeGenerators[0].BuyLimitations[1]).toBe("EItemType::Armor");
254
+ a.TradeGenerators[0].BuyLimitations.removeNode(0);
255
+ expect(a.TradeGenerators[0].BuyLimitations[0]).toBe("removenode");
209
256
  });
210
257
  });
211
258
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "s2cfgtojson",
3
- "version": "2.2.14",
4
- "description": "Converts Stalker 2 Cfg file into a POJO",
3
+ "version": "3.0.0",
4
+ "description": "Converts Stalker 2 Cfg file into POJOs",
5
5
  "keywords": [
6
6
  "stalker",
7
7
  "unrealengine",
@@ -27,9 +27,10 @@
27
27
  "test": "vitest"
28
28
  },
29
29
  "devDependencies": {
30
- "vitest": "^3.2.4",
30
+ "@types/node": "^24.5.2",
31
+ "prettier": "^3.6.2",
31
32
  "typescript": "^5.8.3",
32
- "prettier": "^3.6.2"
33
+ "vitest": "^3.2.4"
33
34
  },
34
35
  "engines": {
35
36
  "node": ">=24"
package/test.cfg ADDED
@@ -0,0 +1,598 @@
1
+ ANCQ27_Start : struct.begin
2
+ SID = ANCQ27_Start
3
+ NodePrototypeVersion = 1
4
+ QuestSID = ANCQ27
5
+ NodeType = EQuestNodeType::Technical
6
+ StartDelay = 0
7
+ LaunchOnQuestStart = true
8
+ struct.end
9
+ ANCQ27_End : struct.begin
10
+ SID = ANCQ27_End
11
+ NodePrototypeVersion = 2
12
+ QuestSID = ANCQ27
13
+ NodeType = EQuestNodeType::End
14
+ Launchers : struct.begin
15
+ [0] : struct.begin
16
+ Excluding = false
17
+ Connections : struct.begin
18
+ [0] : struct.begin
19
+ SID = ANCQ27_End_Pin_0
20
+ struct.end
21
+ [1] : struct.begin
22
+ SID = ANCQ27_Start
23
+ Name =
24
+ struct.end
25
+ struct.end
26
+ struct.end
27
+ struct.end
28
+ ExcludeAllNodesInContainer = false
29
+ struct.end
30
+ ANCQ27_End_Pin_0 : struct.begin
31
+ SID = ANCQ27_End_Pin_0
32
+ NodePrototypeVersion = 5
33
+ NodeType = EQuestNodeType::Condition
34
+ QuestSID = ANCQ27
35
+ Launchers : struct.begin
36
+ [0] : struct.begin
37
+ Excluding = false
38
+ Connections : struct.begin
39
+ [0] : struct.begin
40
+ SID = ANCQ27_Start
41
+ Name =
42
+ struct.end
43
+ struct.end
44
+ struct.end
45
+ struct.end
46
+ Conditions : struct.begin
47
+ [0] : struct.begin
48
+ [0] : struct.begin
49
+ ConditionType = EQuestConditionType::DistanceToPoint
50
+ ConditionComparance = EConditionComparance::GreaterOrEqual
51
+ TargetCharacter = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
52
+ NumericValue = 8000.1
53
+ TargetPoint : struct.begin
54
+ X = 196807.1
55
+ Y = 362287.1
56
+ Z = 1851.040101
57
+ struct.end
58
+ RequiredSquadMembers = ERequiredSquadMembers::AllMembers
59
+ struct.end
60
+ struct.end
61
+ [1] : struct.begin
62
+ [0] : struct.begin
63
+ ConditionType = EQuestConditionType::Note
64
+ ConditionComparance = EConditionComparance::Equal
65
+ NotePrototypeSID = nvgpack_ACNQ27_BanditNote
66
+ struct.end
67
+ struct.end
68
+ [2] : struct.begin
69
+ [0] : struct.begin
70
+ ConditionType = EQuestConditionType::Note
71
+ ConditionComparance = EConditionComparance::Equal
72
+ NotePrototypeSID = ANCQ27_AL_Kluc
73
+ struct.end
74
+ struct.end
75
+ ConditionCheckType = EConditionCheckType::And
76
+ struct.end
77
+ struct.end
78
+ ANCQ27_ItemAdd_nvgpack_supack_KlucPDA : struct.begin
79
+ SID = ANCQ27_ItemAdd_nvgpack_supack_KlucPDA
80
+ NodePrototypeVersion = 2
81
+ QuestSID = ANCQ27
82
+ NodeType = EQuestNodeType::ItemAdd
83
+ Launchers : struct.begin
84
+ [0] : struct.begin
85
+ Excluding = false
86
+ Connections : struct.begin
87
+ [0] : struct.begin
88
+ SID = ANCQ27_Start
89
+ Name =
90
+ struct.end
91
+ [1] : struct.begin
92
+ SID = ANCQ27_ItemAdd_nvgpack_supack_KlucPDA_Pin_0
93
+ struct.end
94
+ struct.end
95
+ struct.end
96
+ struct.end
97
+ TargetQuestGuid = BDAF159A416BD7D0978E26B54962CC0B
98
+ ItemSID = nvgpack_supack_KlucPDA
99
+ ItemsCount = 1
100
+ AddToPlayerStash = false
101
+ struct.end
102
+ ANCQ27_ItemAdd_nvgpack_supack_KlucPDA_Pin_0 : struct.begin
103
+ SID = ANCQ27_ItemAdd_nvgpack_supack_KlucPDA_Pin_0
104
+ NodePrototypeVersion = 2
105
+ NodeType = EQuestNodeType::Condition
106
+ QuestSID = ANCQ27
107
+ Launchers : struct.begin
108
+ [0] : struct.begin
109
+ Excluding = false
110
+ Connections : struct.begin
111
+ [0] : struct.begin
112
+ SID = ANCQ27_Start
113
+ Name =
114
+ struct.end
115
+ struct.end
116
+ struct.end
117
+ struct.end
118
+ Conditions : struct.begin
119
+ [0] : struct.begin
120
+ [0] : struct.begin
121
+ ConditionType = EQuestConditionType::IsCreated
122
+ ConditionComparance = EConditionComparance::Equal
123
+ TargetPlaceholder = BDAF159A416BD7D0978E26B54962CC0B
124
+ struct.end
125
+ struct.end
126
+ ConditionCheckType = EConditionCheckType::And
127
+ struct.end
128
+ struct.end
129
+ ANCQ27_OnPlayerGetItemEvent_nvgpack_supack_KlucPDA : struct.begin
130
+ SID = ANCQ27_OnPlayerGetItemEvent_nvgpack_supack_KlucPDA
131
+ NodePrototypeVersion = 1
132
+ QuestSID = ANCQ27
133
+ NodeType = EQuestNodeType::OnPlayerGetItemEvent
134
+ LaunchOnQuestStart = true
135
+ EventType = EQuestEventType::OnPlayerGetItem
136
+ TrackBeforeActive = false
137
+ ItemPrototypeSID = nvgpack_supack_KlucPDA
138
+ ExpectedItemsCount = 1
139
+ WithEquipped = true
140
+ struct.end
141
+ ANCQ27_ItemAdd_nvgpack_ANCQ27_BanditPDA : struct.begin
142
+ SID = ANCQ27_ItemAdd_nvgpack_ANCQ27_BanditPDA
143
+ NodePrototypeVersion = 2
144
+ QuestSID = ANCQ27
145
+ NodeType = EQuestNodeType::ItemAdd
146
+ Launchers : struct.begin
147
+ [0] : struct.begin
148
+ Excluding = false
149
+ Connections : struct.begin
150
+ [0] : struct.begin
151
+ SID = ANCQ27_Start
152
+ Name =
153
+ struct.end
154
+ [1] : struct.begin
155
+ SID = ANCQ27_ItemAdd_nvgpack_ANCQ27_BanditPDA_Pin_0
156
+ struct.end
157
+ struct.end
158
+ struct.end
159
+ struct.end
160
+ TargetQuestGuid = DF8F23524BC037EB138CCEA75DC885F8
161
+ ItemSID = nvgpack_ANCQ27_BanditPDA
162
+ ItemsCount = 1
163
+ AddToPlayerStash = false
164
+ struct.end
165
+ ANCQ27_ItemAdd_nvgpack_ANCQ27_BanditPDA_Pin_0 : struct.begin
166
+ SID = ANCQ27_ItemAdd_nvgpack_ANCQ27_BanditPDA_Pin_0
167
+ NodePrototypeVersion = 2
168
+ NodeType = EQuestNodeType::Condition
169
+ QuestSID = ANCQ27
170
+ Launchers : struct.begin
171
+ [0] : struct.begin
172
+ Excluding = false
173
+ Connections : struct.begin
174
+ [0] : struct.begin
175
+ SID = ANCQ27_Start
176
+ Name =
177
+ struct.end
178
+ struct.end
179
+ struct.end
180
+ struct.end
181
+ Conditions : struct.begin
182
+ [0] : struct.begin
183
+ [0] : struct.begin
184
+ ConditionType = EQuestConditionType::IsCreated
185
+ ConditionComparance = EConditionComparance::Equal
186
+ TargetPlaceholder = DF8F23524BC037EB138CCEA75DC885F8
187
+ struct.end
188
+ struct.end
189
+ ConditionCheckType = EConditionCheckType::And
190
+ struct.end
191
+ struct.end
192
+ ANCQ27_OnPlayerGetItemEvent_nvgpack_ANCQ27_BanditPDA : struct.begin
193
+ SID = ANCQ27_OnPlayerGetItemEvent_nvgpack_ANCQ27_BanditPDA
194
+ NodePrototypeVersion = 1
195
+ QuestSID = ANCQ27
196
+ NodeType = EQuestNodeType::OnPlayerGetItemEvent
197
+ LaunchOnQuestStart = true
198
+ EventType = EQuestEventType::OnPlayerGetItem
199
+ TrackBeforeActive = false
200
+ ItemPrototypeSID = nvgpack_ANCQ27_BanditPDA
201
+ ExpectedItemsCount = 1
202
+ WithEquipped = true
203
+ struct.end
204
+ ANCQ27_AddNote_nvgpack_ACNQ27_BanditNote : struct.begin
205
+ SID = ANCQ27_AddNote_nvgpack_ACNQ27_BanditNote
206
+ NodePrototypeVersion = 1
207
+ QuestSID = ANCQ27
208
+ NodeType = EQuestNodeType::AddNote
209
+ Launchers : struct.begin
210
+ [0] : struct.begin
211
+ Excluding = false
212
+ Connections : struct.begin
213
+ [0] : struct.begin
214
+ SID = ANCQ27_OnPlayerGetItemEvent_nvgpack_ANCQ27_BanditPDA
215
+ Name =
216
+ struct.end
217
+ struct.end
218
+ struct.end
219
+ struct.end
220
+ NotePrototypeSID = nvgpack_ACNQ27_BanditNote
221
+ PlayWhenReceived = false
222
+ struct.end
223
+ ANCQ27_SwitchQuestItemState_nvgpack_supack_KlucPDA : struct.begin
224
+ SID = ANCQ27_SwitchQuestItemState_nvgpack_supack_KlucPDA
225
+ NodePrototypeVersion = 1
226
+ QuestSID = ANCQ27
227
+ NodeType = EQuestNodeType::SwitchQuestItemState
228
+ Launchers : struct.begin
229
+ [0] : struct.begin
230
+ Excluding = false
231
+ Connections : struct.begin
232
+ [0] : struct.begin
233
+ SID = ANCQ27_OnPlayerGetItemEvent_nvgpack_supack_KlucPDA
234
+ Name =
235
+ struct.end
236
+ struct.end
237
+ struct.end
238
+ struct.end
239
+ ItemPrototypeSID = nvgpack_supack_KlucPDA
240
+ QuestItem = false
241
+ struct.end
242
+ ANCQ27_SpawnObjectNPCMonster_BP_NPCPlaceholder_Snork2 : struct.begin
243
+ SID = ANCQ27_SpawnObjectNPCMonster_BP_NPCPlaceholder_Snork2
244
+ NodePrototypeVersion = 1
245
+ QuestSID = ANCQ27
246
+ NodeType = EQuestNodeType::Spawn
247
+ Launchers : struct.begin
248
+ [0] : struct.begin
249
+ Excluding = false
250
+ Connections : struct.begin
251
+ [0] : struct.begin
252
+ SID = ANCQ27_Start
253
+ Name =
254
+ struct.end
255
+ struct.end
256
+ struct.end
257
+ struct.end
258
+ TargetQuestGuid = AB8A78AE4F8498F19F267885035C7808
259
+ IgnoreDamageType = EIgnoreDamageType::None
260
+ SpawnHidden = false
261
+ SpawnNodeExcludeType = ESpawnNodeExcludeType::SeamlessDespawn
262
+ struct.end
263
+ ANCQ27_SpawnObjectNPCMonster_BP_NPCPlaceholder_Snork1 : struct.begin
264
+ SID = ANCQ27_SpawnObjectNPCMonster_BP_NPCPlaceholder_Snork1
265
+ NodePrototypeVersion = 1
266
+ QuestSID = ANCQ27
267
+ NodeType = EQuestNodeType::Spawn
268
+ Launchers : struct.begin
269
+ [0] : struct.begin
270
+ Excluding = false
271
+ Connections : struct.begin
272
+ [0] : struct.begin
273
+ SID = ANCQ27_Start
274
+ Name =
275
+ struct.end
276
+ struct.end
277
+ struct.end
278
+ struct.end
279
+ TargetQuestGuid = 9D8504AD45988116FBC98CA48D8816B5
280
+ IgnoreDamageType = EIgnoreDamageType::None
281
+ SpawnHidden = false
282
+ SpawnNodeExcludeType = ESpawnNodeExcludeType::SeamlessDespawn
283
+ struct.end
284
+ ANCQ27_OnNPCCreateEvent_BP_NPCPlaceholder_Snork1 : struct.begin
285
+ SID = ANCQ27_OnNPCCreateEvent_BP_NPCPlaceholder_Snork1
286
+ NodePrototypeVersion = 1
287
+ QuestSID = ANCQ27
288
+ NodeType = EQuestNodeType::OnNPCCreateEvent
289
+ LaunchOnQuestStart = true
290
+ EventType = EQuestEventType::OnModelCreate
291
+ TrackBeforeActive = false
292
+ TargetQuestGuid = 9D8504AD45988116FBC98CA48D8816B5
293
+ struct.end
294
+ ANCQ27_OnNPCCreateEvent_BP_NPCPlaceholder_Snork2 : struct.begin
295
+ SID = ANCQ27_OnNPCCreateEvent_BP_NPCPlaceholder_Snork2
296
+ NodePrototypeVersion = 1
297
+ QuestSID = ANCQ27
298
+ NodeType = EQuestNodeType::OnNPCCreateEvent
299
+ LaunchOnQuestStart = true
300
+ EventType = EQuestEventType::OnModelCreate
301
+ TrackBeforeActive = false
302
+ TargetQuestGuid = AB8A78AE4F8498F19F267885035C7808
303
+ struct.end
304
+ ANCQ27_SetAIBehavior_BP_NPCPlaceholder_Snork2 : struct.begin
305
+ SID = ANCQ27_SetAIBehavior_BP_NPCPlaceholder_Snork2
306
+ NodePrototypeVersion = 3
307
+ QuestSID = ANCQ27
308
+ NodeType = EQuestNodeType::SetAIBehavior
309
+ Launchers : struct.begin
310
+ [0] : struct.begin
311
+ Excluding = false
312
+ Connections : struct.begin
313
+ [0] : struct.begin
314
+ SID = ANCQ27_OnNPCCreateEvent_BP_NPCPlaceholder_Snork2
315
+ Name =
316
+ struct.end
317
+ struct.end
318
+ struct.end
319
+ struct.end
320
+ TargetQuestGuid = AB8A78AE4F8498F19F267885035C7808
321
+ GoalPriority = EGoalPriority::HigherThanCombat
322
+ IgnoreRadiationFeilds = false
323
+ IgnoreAnomalyFields = false
324
+ IgnoreEmission = false
325
+ IgnoreCombat = false
326
+ IgnoreThreat = false
327
+ FailureByEmission = false
328
+ FailureByCombat = false
329
+ FailureByThreat = false
330
+ FailureByTargetLost = false
331
+ FailureByPlayerKill = false
332
+ FailureByHumanKil = false
333
+ FailureByMutantKill = false
334
+ CanBeInterrupted = true
335
+ WeaponState = EWeaponState::NoWeapon
336
+ UseSecondaryWeapon = false
337
+ BehaviorType = EBehaviorType::Stay
338
+ MovementType = EMovementBehaviour::Walk
339
+ StayContextualAction = B8CA574548B5A716F53ADE81133D4AEC
340
+ CanBeTeleported = true
341
+ struct.end
342
+ ANCQ27_SetAIBehavior_BP_NPCPlaceholder_Snork1 : struct.begin
343
+ SID = ANCQ27_SetAIBehavior_BP_NPCPlaceholder_Snork1
344
+ NodePrototypeVersion = 3
345
+ QuestSID = ANCQ27
346
+ NodeType = EQuestNodeType::SetAIBehavior
347
+ Launchers : struct.begin
348
+ [0] : struct.begin
349
+ Excluding = false
350
+ Connections : struct.begin
351
+ [0] : struct.begin
352
+ SID = ANCQ27_OnNPCCreateEvent_BP_NPCPlaceholder_Snork1
353
+ Name =
354
+ struct.end
355
+ struct.end
356
+ struct.end
357
+ struct.end
358
+ TargetQuestGuid = 9D8504AD45988116FBC98CA48D8816B5
359
+ GoalPriority = EGoalPriority::HigherThanCombat
360
+ IgnoreRadiationFeilds = false
361
+ IgnoreAnomalyFields = false
362
+ IgnoreEmission = false
363
+ IgnoreCombat = false
364
+ IgnoreThreat = false
365
+ FailureByEmission = false
366
+ FailureByCombat = false
367
+ FailureByThreat = false
368
+ FailureByTargetLost = false
369
+ FailureByPlayerKill = false
370
+ FailureByHumanKil = false
371
+ FailureByMutantKill = false
372
+ CanBeInterrupted = true
373
+ WeaponState = EWeaponState::NoWeapon
374
+ UseSecondaryWeapon = false
375
+ BehaviorType = EBehaviorType::Stay
376
+ MovementType = EMovementBehaviour::Walk
377
+ StayContextualAction = 1431FDEC4F0960A887959A84FDEBB843
378
+ CanBeTeleported = true
379
+ struct.end
380
+ ANCQ27_SetAIBehavior_BP_NPCPlaceholder_Snork2_1 : struct.begin
381
+ SID = ANCQ27_SetAIBehavior_BP_NPCPlaceholder_Snork2_1
382
+ NodePrototypeVersion = 2
383
+ QuestSID = ANCQ27
384
+ NodeType = EQuestNodeType::SetAIBehavior
385
+ Launchers : struct.begin
386
+ [0] : struct.begin
387
+ Excluding = false
388
+ Connections : struct.begin
389
+ [0] : struct.begin
390
+ SID = ANCQ27_OnTriggerEnterEvent
391
+ Name =
392
+ struct.end
393
+ struct.end
394
+ struct.end
395
+ struct.end
396
+ TargetQuestGuid = AB8A78AE4F8498F19F267885035C7808
397
+ GoalPriority = EGoalPriority::HigherThanCombat
398
+ IgnoreRadiationFeilds = false
399
+ IgnoreAnomalyFields = false
400
+ IgnoreEmission = false
401
+ IgnoreCombat = false
402
+ IgnoreThreat = false
403
+ FailureByEmission = false
404
+ FailureByCombat = false
405
+ FailureByThreat = false
406
+ FailureByTargetLost = false
407
+ FailureByPlayerKill = false
408
+ FailureByHumanKil = false
409
+ FailureByMutantKill = false
410
+ CanBeInterrupted = false
411
+ WeaponState = EWeaponState::NoWeapon
412
+ UseSecondaryWeapon = false
413
+ BehaviorType = EBehaviorType::Kill
414
+ SimulateBattle = false
415
+ ImmediatelyIdentifyEnemy = false
416
+ TargetQuestGuids : struct.begin
417
+ [0] = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
418
+ struct.end
419
+ struct.end
420
+ ANCQ27_SetAIBehavior_BP_NPCPlaceholder_Snork1_1 : struct.begin
421
+ SID = ANCQ27_SetAIBehavior_BP_NPCPlaceholder_Snork1_1
422
+ NodePrototypeVersion = 2
423
+ QuestSID = ANCQ27
424
+ NodeType = EQuestNodeType::SetAIBehavior
425
+ Launchers : struct.begin
426
+ [0] : struct.begin
427
+ Excluding = false
428
+ Connections : struct.begin
429
+ [0] : struct.begin
430
+ SID = ANCQ27_OnTriggerEnterEvent
431
+ Name =
432
+ struct.end
433
+ struct.end
434
+ struct.end
435
+ struct.end
436
+ TargetQuestGuid = 9D8504AD45988116FBC98CA48D8816B5
437
+ GoalPriority = EGoalPriority::HigherThanCombat
438
+ IgnoreRadiationFeilds = false
439
+ IgnoreAnomalyFields = false
440
+ IgnoreEmission = false
441
+ IgnoreCombat = false
442
+ IgnoreThreat = false
443
+ FailureByEmission = false
444
+ FailureByCombat = false
445
+ FailureByThreat = false
446
+ FailureByTargetLost = false
447
+ FailureByPlayerKill = false
448
+ FailureByHumanKil = false
449
+ FailureByMutantKill = false
450
+ CanBeInterrupted = false
451
+ WeaponState = EWeaponState::NoWeapon
452
+ UseSecondaryWeapon = false
453
+ BehaviorType = EBehaviorType::Kill
454
+ SimulateBattle = false
455
+ ImmediatelyIdentifyEnemy = false
456
+ TargetQuestGuids : struct.begin
457
+ [0] = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
458
+ struct.end
459
+ struct.end
460
+ ANCQ27_OnTriggerEnterEvent : struct.begin
461
+ SID = ANCQ27_OnTriggerEnterEvent
462
+ NodePrototypeVersion = 1
463
+ QuestSID = ANCQ27
464
+ NodeType = EQuestNodeType::Trigger
465
+ LaunchOnQuestStart = true
466
+ EventType = EQuestEventType::OnTriggerEnter
467
+ TrackBeforeActive = false
468
+ TargetQuestGuid = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
469
+ TriggerQuestGuid = A16B65FA4C04AC227BFAB6AF2F62AD01
470
+ bTriggersByAnybody = false
471
+ TriggerAction = 0
472
+ RequiredSquadMembers = ERequiredSquadMembers::AnyMember
473
+ ReactType = ETriggerReact::ReactOnAlive
474
+ struct.end
475
+ ANCQ27_SpawnObjectNPCMonster_BP_ItemPlaceholder22_ANCQ27_A12Rounds : struct.begin
476
+ SID = ANCQ27_SpawnObjectNPCMonster_BP_ItemPlaceholder22_ANCQ27_A12Rounds
477
+ NodePrototypeVersion = 1
478
+ QuestSID = ANCQ27
479
+ NodeType = EQuestNodeType::Spawn
480
+ Launchers : struct.begin
481
+ [0] : struct.begin
482
+ Excluding = false
483
+ Connections : struct.begin
484
+ [0] : struct.begin
485
+ SID = ANCQ27_Start
486
+ Name =
487
+ struct.end
488
+ struct.end
489
+ struct.end
490
+ struct.end
491
+ TargetQuestGuid = 408CE3214AC62133DC4F25BBEA5B0CC6
492
+ IgnoreDamageType = EIgnoreDamageType::None
493
+ SpawnHidden = false
494
+ SpawnNodeExcludeType = ESpawnNodeExcludeType::SeamlessDespawn
495
+ struct.end
496
+ ANCQ27_SpawnObjectNPCMonster_BP_ItemPlaceholder22_ANCQ27_A12Rounds2 : struct.begin
497
+ SID = ANCQ27_SpawnObjectNPCMonster_BP_ItemPlaceholder22_ANCQ27_A12Rounds2
498
+ NodePrototypeVersion = 1
499
+ QuestSID = ANCQ27
500
+ NodeType = EQuestNodeType::Spawn
501
+ Launchers : struct.begin
502
+ [0] : struct.begin
503
+ Excluding = false
504
+ Connections : struct.begin
505
+ [0] : struct.begin
506
+ SID = ANCQ27_Start
507
+ Name =
508
+ struct.end
509
+ struct.end
510
+ struct.end
511
+ struct.end
512
+ TargetQuestGuid = BB9867AB47049519BEEF5AB7211FB832
513
+ IgnoreDamageType = EIgnoreDamageType::None
514
+ SpawnHidden = false
515
+ SpawnNodeExcludeType = ESpawnNodeExcludeType::SeamlessDespawn
516
+ struct.end
517
+ ANCQ27_SpawnObjectNPCMonster_BP_ItemPlaceholder22_ANCQ27_A12Rounds3 : struct.begin
518
+ SID = ANCQ27_SpawnObjectNPCMonster_BP_ItemPlaceholder22_ANCQ27_A12Rounds3
519
+ NodePrototypeVersion = 1
520
+ QuestSID = ANCQ27
521
+ NodeType = EQuestNodeType::Spawn
522
+ Launchers : struct.begin
523
+ [0] : struct.begin
524
+ Excluding = false
525
+ Connections : struct.begin
526
+ [0] : struct.begin
527
+ SID = ANCQ27_Start
528
+ Name =
529
+ struct.end
530
+ struct.end
531
+ struct.end
532
+ struct.end
533
+ TargetQuestGuid = 113B9E1A483EA96FCE5BD1B07ED239F9
534
+ IgnoreDamageType = EIgnoreDamageType::None
535
+ SpawnHidden = false
536
+ SpawnNodeExcludeType = ESpawnNodeExcludeType::SeamlessDespawn
537
+ struct.end
538
+ ANCQ27_SpawnObjectNPCMonster_BP_ItemPlaceholder22_ANCQ27_Obrez : struct.begin
539
+ SID = ANCQ27_SpawnObjectNPCMonster_BP_ItemPlaceholder22_ANCQ27_Obrez
540
+ NodePrototypeVersion = 1
541
+ QuestSID = ANCQ27
542
+ NodeType = EQuestNodeType::Spawn
543
+ Launchers : struct.begin
544
+ [0] : struct.begin
545
+ Excluding = false
546
+ Connections : struct.begin
547
+ [0] : struct.begin
548
+ SID = ANCQ27_Start
549
+ Name =
550
+ struct.end
551
+ struct.end
552
+ struct.end
553
+ struct.end
554
+ TargetQuestGuid = CF3D1AEC41BF22B5DB4B4989748AA7E5
555
+ IgnoreDamageType = EIgnoreDamageType::None
556
+ SpawnHidden = false
557
+ SpawnNodeExcludeType = ESpawnNodeExcludeType::SeamlessDespawn
558
+ struct.end
559
+ ANCQ27_SpawnObjectNPCMonster_BP_ItemPlaceholder22_ANCQ27_Obrez2 : struct.begin
560
+ SID = ANCQ27_SpawnObjectNPCMonster_BP_ItemPlaceholder22_ANCQ27_Obrez2
561
+ NodePrototypeVersion = 1
562
+ QuestSID = ANCQ27
563
+ NodeType = EQuestNodeType::Spawn
564
+ Launchers : struct.begin
565
+ [0] : struct.begin
566
+ Excluding = false
567
+ Connections : struct.begin
568
+ [0] : struct.begin
569
+ SID = ANCQ27_Start
570
+ Name =
571
+ struct.end
572
+ struct.end
573
+ struct.end
574
+ struct.end
575
+ TargetQuestGuid = B2300EE942E38665B13563A2D2C80B52
576
+ IgnoreDamageType = EIgnoreDamageType::None
577
+ SpawnHidden = false
578
+ SpawnNodeExcludeType = ESpawnNodeExcludeType::SeamlessDespawn
579
+ struct.end
580
+ ANCQ27_AddNote_nvgpack_supack_ANCQ27_AudiologKluc : struct.begin
581
+ SID = ANCQ27_AddNote_nvgpack_supack_ANCQ27_AudiologKluc
582
+ NodePrototypeVersion = 1
583
+ QuestSID = ANCQ27
584
+ NodeType = EQuestNodeType::AddNote
585
+ Launchers : struct.begin
586
+ [0] : struct.begin
587
+ Excluding = false
588
+ Connections : struct.begin
589
+ [0] : struct.begin
590
+ SID = ANCQ27_OnPlayerGetItemEvent_nvgpack_supack_KlucPDA
591
+ Name =
592
+ struct.end
593
+ struct.end
594
+ struct.end
595
+ struct.end
596
+ NotePrototypeSID = nvgpack_supack_ANCQ27_AudiologKluc
597
+ PlayWhenReceived = true
598
+ struct.end
package/types.mts CHANGED
@@ -67,23 +67,28 @@ import {
67
67
  EMeshSubType,
68
68
  } from "./enums.mjs";
69
69
 
70
- export interface Struct<T extends Entries = {}> {
71
- isRoot?: boolean;
70
+ export type Internal = "__internal__";
71
+ export type IStruct = {
72
+ [k in Internal]: DefaultEntries;
73
+ };
74
+
75
+ export interface DefaultEntries {
76
+ rawName?: string;
72
77
  refurl?: string;
73
78
  refkey?: string | number;
74
79
  bskipref?: boolean;
75
- _id: string;
76
- entries: T;
77
- toTs(): string;
80
+ bpatch?: boolean;
81
+
82
+ isArray?: boolean;
83
+ useAsterisk?: boolean;
78
84
  }
79
- export type DefaultEntries = { _isArray?: boolean; _useAsterisk?: boolean };
80
85
 
81
- export type Value = Omit<Struct, "toTs"> | string | boolean | number;
86
+ export type Value = string | boolean | number;
82
87
 
83
- export type Entries = Record<string | number, Value> & DefaultEntries;
88
+ export type Entries = Record<string | number, Value>;
84
89
 
85
- export type GetTsType<In extends Struct, E = In["entries"]> = {
86
- [key in Exclude<keyof E, keyof DefaultEntries>]: E[key] extends Struct
90
+ export type GetTsType<E extends IStruct> = {
91
+ [key in Exclude<keyof E, Internal>]: E[key] extends IStruct
87
92
  ? GetTsType<E[key]>
88
93
  : E[key];
89
94
  };
@@ -91,9 +96,9 @@ type RKey<In> = Exclude<keyof In, keyof DefaultEntries>;
91
96
 
92
97
  export type GetStructType<In> =
93
98
  In extends Array<any>
94
- ? Struct<{ [key: number]: GetStructType<In[number]> }>
99
+ ? IStruct & { [key in number]: GetStructType<In[key]> }
95
100
  : In extends Record<any, any>
96
- ? Struct<{ [key in RKey<In>]: GetStructType<In[key]> }>
101
+ ? IStruct & { [key in RKey<In>]: GetStructType<In[key]> }
97
102
  : In extends string
98
103
  ? In
99
104
  : In extends number