s2cfgtojson 1.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 +222 -0
- package/Struct.test.mts +174 -0
- package/package.json +37 -0
- package/readme.md +78 -0
- package/tsconfig.json +8 -0
package/Struct.mts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
export type Value = Omit<Struct, "toTs"> | string | boolean | number;
|
|
2
|
+
type DefaultEntries = { _isArray?: boolean; _useAsterisk?: boolean };
|
|
3
|
+
export type Entries = Record<string | number, Value> & DefaultEntries;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This file is part of the Stalker 2 Modding Tools project.
|
|
7
|
+
* This is a base class for all structs.
|
|
8
|
+
*/
|
|
9
|
+
export abstract class Struct<T extends Entries = {}> {
|
|
10
|
+
isRoot?: boolean;
|
|
11
|
+
refurl?: string;
|
|
12
|
+
refkey?: string | number;
|
|
13
|
+
bskipref?: boolean;
|
|
14
|
+
abstract _id: string;
|
|
15
|
+
entries: T;
|
|
16
|
+
|
|
17
|
+
static TAB = " ";
|
|
18
|
+
static pad(text: string): string {
|
|
19
|
+
return `${Struct.TAB}${text.replace(/\n+/g, `\n${Struct.TAB}`)}`;
|
|
20
|
+
}
|
|
21
|
+
static WILDCARD = "_wildcard";
|
|
22
|
+
static isNumber(ref: string): boolean {
|
|
23
|
+
return Number.isInteger(parseInt(ref)) || typeof ref === "number";
|
|
24
|
+
}
|
|
25
|
+
static isArrayKey(key: string) {
|
|
26
|
+
return key.includes("[") && key.includes("]");
|
|
27
|
+
}
|
|
28
|
+
static renderKeyName(ref: string, useAsterisk?: boolean): string {
|
|
29
|
+
if (`${ref}`.startsWith("_")) {
|
|
30
|
+
return Struct.renderKeyName(ref.slice(1), useAsterisk); // Special case for indexed structs
|
|
31
|
+
}
|
|
32
|
+
if (`${ref}`.includes("*") || useAsterisk) {
|
|
33
|
+
return "[*]"; // Special case for wildcard structs
|
|
34
|
+
}
|
|
35
|
+
if (`${ref}`.includes("_dupe_")) {
|
|
36
|
+
return Struct.renderKeyName(ref.slice(0, ref.indexOf("_dupe_")));
|
|
37
|
+
}
|
|
38
|
+
if (Struct.isNumber(ref)) {
|
|
39
|
+
return `[${parseInt(ref)}]`;
|
|
40
|
+
}
|
|
41
|
+
return ref;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static renderStructName(name: string): string {
|
|
45
|
+
if (name === Struct.WILDCARD) {
|
|
46
|
+
return "[*]"; // Special case for wildcard structs
|
|
47
|
+
}
|
|
48
|
+
if (`${name}`.startsWith("_")) {
|
|
49
|
+
return Struct.renderStructName(name.slice(1)); // Special case for indexed structs
|
|
50
|
+
}
|
|
51
|
+
if (Struct.isNumber(name)) {
|
|
52
|
+
return `[${parseInt(name)}]`;
|
|
53
|
+
}
|
|
54
|
+
return name;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static extractKeyFromBrackets(key: string) {
|
|
58
|
+
if (/\[(.+)]/.test(key)) {
|
|
59
|
+
return key.match(/\[(.+)]/)[1];
|
|
60
|
+
}
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static parseStructName(name: string): string {
|
|
65
|
+
if (Struct.extractKeyFromBrackets(name) === "*") {
|
|
66
|
+
return Struct.WILDCARD; // Special case for wildcard structs
|
|
67
|
+
}
|
|
68
|
+
if (Struct.isNumber(Struct.extractKeyFromBrackets(name))) {
|
|
69
|
+
return `_${name.match(/\[(\d+)]/)[1]}`; // Special case for indexed structs
|
|
70
|
+
}
|
|
71
|
+
return name;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static createDynamicClass = (name: string): new () => Struct =>
|
|
75
|
+
new Function("parent", `return class ${name} extends parent {}`)(Struct);
|
|
76
|
+
|
|
77
|
+
toString(): string {
|
|
78
|
+
let text: string;
|
|
79
|
+
text = this.isRoot ? `${Struct.renderStructName(this._id)} : ` : "";
|
|
80
|
+
text += "struct.begin";
|
|
81
|
+
const refs = ["refurl", "refkey", "bskipref"]
|
|
82
|
+
.map((k) => [k, this[k]])
|
|
83
|
+
.filter(([_, v]) => v !== "" && v !== undefined && v !== false)
|
|
84
|
+
.map(([k, v]) => {
|
|
85
|
+
if (v === true) return k;
|
|
86
|
+
return `${k}=${Struct.renderKeyName(v)}`;
|
|
87
|
+
})
|
|
88
|
+
.join(";");
|
|
89
|
+
if (refs) {
|
|
90
|
+
text += ` {${refs}}`;
|
|
91
|
+
}
|
|
92
|
+
text += "\n";
|
|
93
|
+
// Add all keys
|
|
94
|
+
text += Object.entries(this.entries || {})
|
|
95
|
+
.filter(([key]) => key !== "_useAsterisk" && key !== "_isArray")
|
|
96
|
+
.map(([key, value]) => {
|
|
97
|
+
const keyOrIndex = Struct.renderKeyName(key, this.entries._useAsterisk);
|
|
98
|
+
const equalsOrColon = value instanceof Struct ? ":" : "=";
|
|
99
|
+
const spaceOrNoSpace = value === "" ? "" : " ";
|
|
100
|
+
return Struct.pad(
|
|
101
|
+
`${keyOrIndex} ${equalsOrColon}${spaceOrNoSpace}${value}`,
|
|
102
|
+
);
|
|
103
|
+
})
|
|
104
|
+
.join("\n");
|
|
105
|
+
text += "\nstruct.end";
|
|
106
|
+
return text;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
toTs(): string {
|
|
110
|
+
return JSON.stringify(this, null, 2);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static addEntry(
|
|
114
|
+
parent: Struct<DefaultEntries>,
|
|
115
|
+
key: string,
|
|
116
|
+
value: Value,
|
|
117
|
+
index: number,
|
|
118
|
+
) {
|
|
119
|
+
parent.entries ||= {};
|
|
120
|
+
|
|
121
|
+
const getKey = () => {
|
|
122
|
+
let normKey: string | number = key;
|
|
123
|
+
if (Struct.isArrayKey(key)) {
|
|
124
|
+
parent.entries._isArray = true;
|
|
125
|
+
normKey = Struct.extractKeyFromBrackets(key);
|
|
126
|
+
|
|
127
|
+
if (normKey === "*") {
|
|
128
|
+
parent.entries._useAsterisk = true;
|
|
129
|
+
return Object.keys(parent.entries).length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (parent.entries[normKey] !== undefined) {
|
|
133
|
+
return `${normKey}_dupe_${index}`;
|
|
134
|
+
}
|
|
135
|
+
return normKey;
|
|
136
|
+
}
|
|
137
|
+
if (parent.entries[normKey] !== undefined) {
|
|
138
|
+
return `${normKey}_dupe_${index}`;
|
|
139
|
+
}
|
|
140
|
+
return normKey;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
parent.entries[getKey()] = value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static fromString<IntendedType extends Partial<Struct> = Struct>(
|
|
147
|
+
text: string,
|
|
148
|
+
): IntendedType[] {
|
|
149
|
+
const lines = text.trim().split("\n");
|
|
150
|
+
|
|
151
|
+
const parseHead = (line: string): Struct => {
|
|
152
|
+
const match = line.match(
|
|
153
|
+
/^(.*)\s*:\s*struct\.begin\s*({\s*((refurl|refkey|bskipref)\s*(=.+)?)\s*})?/,
|
|
154
|
+
);
|
|
155
|
+
if (!match) {
|
|
156
|
+
throw new Error(`Invalid struct head: ${line}`);
|
|
157
|
+
}
|
|
158
|
+
let name = Struct.parseStructName(match[1].trim());
|
|
159
|
+
|
|
160
|
+
const dummy = new (Struct.createDynamicClass(name))();
|
|
161
|
+
if (name === match[1].trim()) {
|
|
162
|
+
dummy._id = name;
|
|
163
|
+
}
|
|
164
|
+
if (match[3]) {
|
|
165
|
+
const refs = match[3]
|
|
166
|
+
.split(";")
|
|
167
|
+
.map((ref) => ref.trim())
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
.reduce(
|
|
170
|
+
(acc, ref) => {
|
|
171
|
+
const [key, value] = ref.split("=");
|
|
172
|
+
acc[key.trim()] = value ? value.trim() : true;
|
|
173
|
+
return acc;
|
|
174
|
+
},
|
|
175
|
+
{} as { refurl?: string; refkey?: string; bskipref?: boolean },
|
|
176
|
+
);
|
|
177
|
+
if (refs.refurl) dummy.refurl = refs.refurl;
|
|
178
|
+
if (refs.refkey) dummy.refkey = refs.refkey;
|
|
179
|
+
if (refs.bskipref) dummy.bskipref = refs.bskipref;
|
|
180
|
+
}
|
|
181
|
+
return dummy as Struct;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const parseKeyValue = (line: string, parent: Struct): void => {
|
|
185
|
+
const match = line.match(/^(.*?)(\s*:\s*|\s*=\s*)(.*)$/);
|
|
186
|
+
if (!match) {
|
|
187
|
+
throw new Error(`Invalid key-value pair: ${line}`);
|
|
188
|
+
}
|
|
189
|
+
const key = match[1].trim();
|
|
190
|
+
const value = match[3].trim();
|
|
191
|
+
Struct.addEntry(parent, key, value, index);
|
|
192
|
+
};
|
|
193
|
+
let index = 0;
|
|
194
|
+
|
|
195
|
+
const walk = () => {
|
|
196
|
+
const roots: Struct[] = [];
|
|
197
|
+
const stack = [];
|
|
198
|
+
while (index < lines.length) {
|
|
199
|
+
const line = lines[index++].trim();
|
|
200
|
+
const current = stack[stack.length - 1];
|
|
201
|
+
if (line.includes("struct.begin")) {
|
|
202
|
+
const newStruct = parseHead(line);
|
|
203
|
+
if (current) {
|
|
204
|
+
const key = Struct.renderStructName(newStruct.constructor.name);
|
|
205
|
+
Struct.addEntry(current, key, newStruct, index);
|
|
206
|
+
} else {
|
|
207
|
+
newStruct.isRoot = true;
|
|
208
|
+
roots.push(newStruct);
|
|
209
|
+
}
|
|
210
|
+
stack.push(newStruct);
|
|
211
|
+
} else if (line.includes("struct.end")) {
|
|
212
|
+
stack.pop();
|
|
213
|
+
} else if (line.includes("=") && current) {
|
|
214
|
+
parseKeyValue(line, current);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return roots;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
return walk() as IntendedType[];
|
|
221
|
+
}
|
|
222
|
+
}
|
package/Struct.test.mts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { Struct } from "./Struct.mjs";
|
|
3
|
+
|
|
4
|
+
class ChimeraHPFix extends Struct {
|
|
5
|
+
_id = "ChimeraHPFix";
|
|
6
|
+
bskipref = true;
|
|
7
|
+
entries = { MaxHP: 750 };
|
|
8
|
+
isRoot = true;
|
|
9
|
+
}
|
|
10
|
+
class TradersDontBuyWeaponsArmor extends Struct {
|
|
11
|
+
_id = "TradersDontBuyWeaponsArmor";
|
|
12
|
+
refurl = "../TradePrototypes.cfg";
|
|
13
|
+
refkey = 0;
|
|
14
|
+
isRoot = true;
|
|
15
|
+
entries = { TradeGenerators: new TradeGenerators() };
|
|
16
|
+
}
|
|
17
|
+
class TradeGenerators extends Struct {
|
|
18
|
+
_id = "TradeGenerators";
|
|
19
|
+
entries = { "*": new TradeGenerator() };
|
|
20
|
+
}
|
|
21
|
+
class TradeGenerator extends Struct {
|
|
22
|
+
_id = "TradeGenerator";
|
|
23
|
+
entries = { BuyLimitations: new BuyLimitations() };
|
|
24
|
+
}
|
|
25
|
+
class BuyLimitations extends Struct {
|
|
26
|
+
_id = "BuyLimitations";
|
|
27
|
+
entries = { [0]: "EItemType::Weapon", [1]: "EItemType::Armor" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("Struct", () => {
|
|
31
|
+
test("toString()", () => {
|
|
32
|
+
expect(new ChimeraHPFix().toString()).toBe(
|
|
33
|
+
`ChimeraHPFix : struct.begin {bskipref}
|
|
34
|
+
MaxHP = 750
|
|
35
|
+
struct.end`,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
expect(new TradersDontBuyWeaponsArmor().toString()).toBe(
|
|
39
|
+
`TradersDontBuyWeaponsArmor : struct.begin {refurl=../TradePrototypes.cfg;refkey=[0]}
|
|
40
|
+
TradeGenerators : struct.begin
|
|
41
|
+
[*] : struct.begin
|
|
42
|
+
BuyLimitations : struct.begin
|
|
43
|
+
[0] = EItemType::Weapon
|
|
44
|
+
[1] = EItemType::Armor
|
|
45
|
+
struct.end
|
|
46
|
+
struct.end
|
|
47
|
+
struct.end
|
|
48
|
+
struct.end`,
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("pad()", () => {
|
|
53
|
+
expect(Struct.pad("test")).toBe(" test");
|
|
54
|
+
expect(Struct.pad(Struct.pad("test"))).toBe(" test");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("fromString()", () => {
|
|
58
|
+
test("1", () => {
|
|
59
|
+
const chimeraText = new ChimeraHPFix().toString();
|
|
60
|
+
|
|
61
|
+
expect(Struct.fromString(chimeraText)[0].toString()).toBe(chimeraText);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("2", () => {
|
|
65
|
+
const complexStructText = `BasePhantomAttack : struct.begin {refkey=BaseAttackAbility}
|
|
66
|
+
TriggeredCooldowns : struct.begin
|
|
67
|
+
[0] : struct.begin
|
|
68
|
+
CooldownTag = Ability.Cooldown.RunAttack
|
|
69
|
+
Duration = 50.f
|
|
70
|
+
struct.end
|
|
71
|
+
struct.end
|
|
72
|
+
Effects : struct.begin
|
|
73
|
+
[0] : struct.begin
|
|
74
|
+
EffectPrototypeSID = MutantMediumAttackCameraShake
|
|
75
|
+
Chance = 1.f
|
|
76
|
+
struct.end
|
|
77
|
+
struct.end
|
|
78
|
+
struct.end`;
|
|
79
|
+
expect(Struct.fromString(complexStructText)[0].toString()).toBe(
|
|
80
|
+
complexStructText,
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("3", () => {
|
|
85
|
+
const dynamicItemGeneratorText = `DynamicTraderItemGenerator : struct.begin {refurl=../ItemGeneratorPrototypes.cfg;refkey=[0]}
|
|
86
|
+
SID = DynamicTraderItemGenerator
|
|
87
|
+
ItemGenerator : struct.begin
|
|
88
|
+
[0] : struct.begin
|
|
89
|
+
Category = EItemGenerationCategory::WeaponPrimary
|
|
90
|
+
PlayerRank = ERank::Newbie
|
|
91
|
+
bAllowSameCategoryGeneration = true
|
|
92
|
+
ReputationThreshold = -500
|
|
93
|
+
RefreshTime = 1h
|
|
94
|
+
PossibleItems : struct.begin
|
|
95
|
+
[0] : struct.begin
|
|
96
|
+
ItemPrototypeSID = GunMark_SP
|
|
97
|
+
Weight = 1
|
|
98
|
+
MinDurability = 0.3
|
|
99
|
+
MaxDurability = 0.6
|
|
100
|
+
AmmoMinCount = 600
|
|
101
|
+
AmmoMaxCount = 900
|
|
102
|
+
struct.end
|
|
103
|
+
struct.end
|
|
104
|
+
struct.end
|
|
105
|
+
[1] : struct.begin
|
|
106
|
+
Category = EItemGenerationCategory::BodyArmor
|
|
107
|
+
PlayerRank = ERank::Newbie
|
|
108
|
+
bAllowSameCategoryGeneration = true
|
|
109
|
+
ReputationThreshold = -500
|
|
110
|
+
RefreshTime = 1h
|
|
111
|
+
PossibleItems : struct.begin
|
|
112
|
+
[0] : struct.begin
|
|
113
|
+
ItemPrototypeSID = DutyArmor_3_U1
|
|
114
|
+
Weight = 1
|
|
115
|
+
MinDurability = 1.
|
|
116
|
+
MaxDurability = 1.
|
|
117
|
+
struct.end
|
|
118
|
+
struct.end
|
|
119
|
+
struct.end
|
|
120
|
+
[2] : struct.begin
|
|
121
|
+
Category = EItemGenerationCategory::WeaponPistol
|
|
122
|
+
ReputationThreshold = -500
|
|
123
|
+
bAllowSameCategoryGeneration = true
|
|
124
|
+
RefreshTime = 1h
|
|
125
|
+
PossibleItems : struct.begin
|
|
126
|
+
[0] : struct.begin
|
|
127
|
+
ItemPrototypeSID = GunAPB_HG
|
|
128
|
+
Weight = 1
|
|
129
|
+
MinDurability = 0.25
|
|
130
|
+
MaxDurability = 0.5
|
|
131
|
+
AmmoMinCount = 60
|
|
132
|
+
AmmoMaxCount = 100
|
|
133
|
+
struct.end
|
|
134
|
+
struct.end
|
|
135
|
+
struct.end
|
|
136
|
+
[3] : struct.begin
|
|
137
|
+
Category = EItemGenerationCategory::WeaponSecondary
|
|
138
|
+
PlayerRank = ERank::Veteran
|
|
139
|
+
bAllowSameCategoryGeneration = true
|
|
140
|
+
RefreshTime = 1h
|
|
141
|
+
PossibleItems : struct.begin
|
|
142
|
+
[0] : struct.begin
|
|
143
|
+
ItemPrototypeSID = GunTOZ_SG
|
|
144
|
+
Weight = 1
|
|
145
|
+
MinDurability = 0.3
|
|
146
|
+
MaxDurability = 0.6
|
|
147
|
+
AmmoMinCount = 60
|
|
148
|
+
AmmoMaxCount = 90
|
|
149
|
+
struct.end
|
|
150
|
+
struct.end
|
|
151
|
+
struct.end
|
|
152
|
+
[4] : struct.begin
|
|
153
|
+
Category = EItemGenerationCategory::Head
|
|
154
|
+
PlayerRank = ERank::Veteran
|
|
155
|
+
bAllowSameCategoryGeneration = true
|
|
156
|
+
ReputationThreshold = -500
|
|
157
|
+
RefreshTime = 1h
|
|
158
|
+
PossibleItems : struct.begin
|
|
159
|
+
[0] : struct.begin
|
|
160
|
+
ItemPrototypeSID = DutyMask_1
|
|
161
|
+
Weight = 2
|
|
162
|
+
MinDurability = 1.
|
|
163
|
+
MaxDurability = 1.
|
|
164
|
+
struct.end
|
|
165
|
+
struct.end
|
|
166
|
+
struct.end
|
|
167
|
+
struct.end
|
|
168
|
+
struct.end`;
|
|
169
|
+
expect(Struct.fromString(dynamicItemGeneratorText)[0].toString()).toBe(
|
|
170
|
+
dynamicItemGeneratorText,
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "s2cfgtojson",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Converts Stalker 2 Cfg file into a POJO",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"stalker",
|
|
7
|
+
"unrealengine",
|
|
8
|
+
"typescript",
|
|
9
|
+
"javascript",
|
|
10
|
+
"modding",
|
|
11
|
+
"mod",
|
|
12
|
+
"utility"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/sdwvit/s2cfgtojson#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/sdwvit/s2cfgtojson/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/sdwvit/s2cfgtojson.git"
|
|
21
|
+
},
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"author": "sdwvit",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "Struct.mts",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "vitest"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"vitest": "^3.2.4",
|
|
31
|
+
"typescript": "^5.8.3",
|
|
32
|
+
"prettier": "^3.6.2"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=24"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Stalker 2 CFG to JSON Converter
|
|
2
|
+
|
|
3
|
+
A utility to convert Stalker 2 `.cfg` configuration files into structured JavaScript objects (POJOs), enabling easier mod development and analysis.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
9
|
+
- **Struct Class**: A flexible base class for parsing and generating Stalker 2 config structures, handling nested data, wildcards, and indexed entries.
|
|
10
|
+
- **TypeScript Support**: Built with TypeScript for type safety and clarity.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Getting Started
|
|
15
|
+
|
|
16
|
+
### Prerequisites
|
|
17
|
+
- Node.js ≥24
|
|
18
|
+
- npm (or pnpm/yarn)
|
|
19
|
+
|
|
20
|
+
### Installation
|
|
21
|
+
```bash
|
|
22
|
+
npm install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Usage
|
|
26
|
+
Import the `Struct` class from `Struct.mts` to parse or generate Stalker 2 config data. For example:
|
|
27
|
+
```ts
|
|
28
|
+
import { Struct } from './Struct.mts';
|
|
29
|
+
|
|
30
|
+
const config = new Struct();
|
|
31
|
+
// Use methods like `toString()` or `fromString()` to process configs
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Example
|
|
37
|
+
Convert a `.cfg` file to a structured object:
|
|
38
|
+
```ts
|
|
39
|
+
const configText = `
|
|
40
|
+
BasePhantomAttack : struct.begin {refkey=BaseAttackAbility}
|
|
41
|
+
TriggeredCooldowns : struct.begin
|
|
42
|
+
[0] : struct.begin
|
|
43
|
+
CooldownTag = Ability.Cooldown.RunAttack
|
|
44
|
+
Duration = 50.f
|
|
45
|
+
struct.end
|
|
46
|
+
struct.end
|
|
47
|
+
struct.end
|
|
48
|
+
`;
|
|
49
|
+
const parsed = Struct.fromString(configText)[0];
|
|
50
|
+
console.log(parsed.toString());
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Development
|
|
56
|
+
|
|
57
|
+
### Tests
|
|
58
|
+
Run tests using:
|
|
59
|
+
```bash
|
|
60
|
+
npm test
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Contributing
|
|
66
|
+
|
|
67
|
+
Feel free to contribute by submitting issues or pull requests. Ensure your code adheres to the project's coding standards and includes tests for new features.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
Free to use in non-commercial projects.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Notes
|
|
77
|
+
|
|
78
|
+
Thanks, GSC and modders for providing the community with tools to enhance the modding experience.
|