@amxts/config-core 0.1.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.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Config Core's own bookkeeping. Not part of the API.
3
+ */
4
+ import { ConfigFormat, ConfigKind } from "./types";
5
+
6
+ /** A value of a config read as a tree: what a ConfigNode shows. */
7
+ export interface TreeNode {
8
+ kind: ConfigKind;
9
+ /** The key it has in its object; "" in an array and at the top. */
10
+ key: string;
11
+ /** A scalar as it was written: a string's text, a number as written, "true", "false", "null" or "" for an empty value. */
12
+ text: string;
13
+ /** A number's value; 0 for anything else. */
14
+ value: number;
15
+ /** An object's members or an array's items, in order. */
16
+ items: TreeNode[];
17
+ /** The comment and blank lines read before it; null for none. */
18
+ comments: string[] | null;
19
+ /** Where it was read, from 1; 0 for one made at run time or a column not known. */
20
+ line: number;
21
+ column: number;
22
+ /** An object whose keys are found case-insensitively: an INI section's. */
23
+ foldCase: boolean;
24
+ /** A list that is an INI block of rows: written back as a block, even when each row is one value. */
25
+ block: boolean;
26
+ }
27
+
28
+ /** A config file read as a tree. */
29
+ export interface TreeDocument {
30
+ /** The file's path; "" for a text that came from no file. */
31
+ file: string;
32
+ format: ConfigFormat;
33
+ root: TreeNode;
34
+ /** One level of indentation, as the file has it. */
35
+ indent: string;
36
+ /** The comment and blank lines after the last value. */
37
+ tail: string[] | null;
38
+ }
39
+
40
+ /** A place in a text: its line and column, from 1. */
41
+ export interface Place {
42
+ line: number;
43
+ column: number;
44
+ }
package/src/json.ts ADDED
@@ -0,0 +1,386 @@
1
+ /**
2
+ * JSON for Config Core, with what JSONC adds: // and /* *\/ comments and a
3
+ * comma after the last member or item. Read into the tree and written back.
4
+ * Not part of the API.
5
+ */
6
+ import { TreeDocument, TreeNode } from "./internal";
7
+ import { BACKSLASH, isControl, isDigits, isHexDigits, isScalar, lineStarts, makeNode, numberNode, placeOf, unicodeEscape, writeComments } from "./tree";
8
+
9
+ /** Escapes of JSON text: the letter after the backslash, and what it stands for, at the same place. */
10
+ const ESCAPE_LETTERS = "\"\\/bfnrt";
11
+ const ESCAPED = "\"\\/\u0008\u000C\u000A\u000D\u0009";
12
+
13
+ function isWhitespace(character: string) {
14
+ return character == " " || character == "\t" || character == "\n";
15
+ }
16
+
17
+ /** A number as JSON writes one: -12, 3.5, 1e-3 - no +, no leading zeros, no bare dot. */
18
+ function isJsonNumber(text: string) {
19
+ const body = text.startsWith("-") ? text.slice(1) : text;
20
+ let exponent = body.indexOf("e");
21
+ if (exponent < 0) exponent = body.indexOf("E");
22
+ let mantissa = body;
23
+
24
+ if (exponent >= 0) {
25
+ mantissa = body.slice(0, exponent);
26
+ const power = body.slice(exponent + 1);
27
+ if (!isDigits(power.startsWith("-") || power.startsWith("+") ? power.slice(1) : power)) return false;
28
+ }
29
+
30
+ const dot = mantissa.indexOf(".");
31
+ const whole = dot < 0 ? mantissa : mantissa.slice(0, dot);
32
+ if (!isDigits(whole) || (whole.length > 1 && whole.startsWith("0"))) return false;
33
+ return dot < 0 || isDigits(mantissa.slice(dot + 1));
34
+ }
35
+
36
+ /** Reads a JSON or JSONC text into the tree; the first thing it cannot read stops it, with the place. */
37
+ export class JsonReader {
38
+ pos = 0;
39
+ /** What stopped the reading; "" while nothing has. */
40
+ error = "";
41
+ errorAt = 0;
42
+ /** One level of indentation as the text has it; "" until an indented line is met. */
43
+ indent = "";
44
+ /** Comment and blank lines waiting for the value they are written before. */
45
+ pending: string[] | null = null;
46
+ starts: number[];
47
+
48
+ constructor(public text: string) {
49
+ this.starts = lineStarts(text);
50
+ }
51
+
52
+ get failed() {
53
+ return this.error.length > 0;
54
+ }
55
+
56
+ fail(at: number, message: string) {
57
+ if (this.failed) return;
58
+ this.error = message;
59
+ this.errorAt = at;
60
+ }
61
+
62
+ placeAt(at: number) {
63
+ return placeOf(this.starts, at);
64
+ }
65
+
66
+ takeComments() {
67
+ const taken = this.pending;
68
+ this.pending = null;
69
+ return taken;
70
+ }
71
+
72
+ keep(line: string) {
73
+ let list = this.pending;
74
+
75
+ if (list == null) {
76
+ list = [];
77
+ this.pending = list;
78
+ }
79
+
80
+ list.push(line);
81
+ }
82
+
83
+ /**
84
+ * Skips spaces and comments. A comment on a line of its own is kept for the
85
+ * value after it, and so is a blank line; one after a value on its line is
86
+ * dropped.
87
+ */
88
+ skip() {
89
+ let lineEnds = 0;
90
+ let blankKept = false;
91
+
92
+ for (;;) {
93
+ const character = this.text.charAt(this.pos);
94
+
95
+ if (isWhitespace(character)) {
96
+ if (character == "\n") lineEnds++;
97
+ if (character == "\n" && lineEnds >= 2 && !blankKept) this.keep("");
98
+ if (character == "\n" && lineEnds >= 2) blankKept = true;
99
+ if (character == "\n" && this.indent.length == 0) this.noteIndent(this.pos + 1);
100
+ this.pos++;
101
+ continue;
102
+ }
103
+
104
+ if (character != "/") return;
105
+ const next = this.text.charAt(this.pos + 1);
106
+ const own = lineEnds > 0 || this.pos == 0;
107
+
108
+ if (next == "/") {
109
+ const end = this.text.indexOf("\n", this.pos);
110
+ const stop = end < 0 ? this.text.length : end;
111
+ if (own) this.keep(this.text.slice(this.pos, stop).trim());
112
+ this.pos = stop;
113
+ } else if (next == "*") {
114
+ const end = this.text.indexOf("*/", this.pos + 2);
115
+
116
+ if (end < 0) {
117
+ this.fail(this.pos, "a /* comment that is never closed with */");
118
+ return;
119
+ }
120
+
121
+ if (own) {
122
+ for (const line of this.text.slice(this.pos, end + 2).split("\n")) this.keep(line.trim());
123
+ }
124
+
125
+ this.pos = end + 2;
126
+ } else {
127
+ return;
128
+ }
129
+
130
+ lineEnds = 0;
131
+ blankKept = false;
132
+ }
133
+ }
134
+
135
+ noteIndent(start: number) {
136
+ let end = start;
137
+ while (this.text.charAt(end) == " " || this.text.charAt(end) == "\t") end++;
138
+ const next = this.text.charAt(end);
139
+ if (end > start && next != "\n" && next != "") this.indent = this.text.slice(start, end);
140
+ }
141
+
142
+ /** The document: one value, spaces and comments around it. */
143
+ read() {
144
+ this.skip();
145
+
146
+ if (this.pos >= this.text.length) {
147
+ const empty = makeNode("object", "", "", { line: 1, column: 1 });
148
+ empty.comments = this.takeComments();
149
+ return empty;
150
+ }
151
+
152
+ const comments = this.takeComments();
153
+ const root = this.value();
154
+ root.comments = comments;
155
+ this.skip();
156
+ if (this.pos < this.text.length) this.fail(this.pos, "unexpected text after the end of the value");
157
+ return root;
158
+ }
159
+
160
+ value() {
161
+ const at = this.pos;
162
+ const first = this.text.charAt(at);
163
+ if (first == "{") return this.object();
164
+ if (first == "[") return this.array();
165
+ if (first == "\"") return this.string();
166
+ let end = at;
167
+ while (end < this.text.length && !",]}/ \t\n".includes(this.text.charAt(end))) end++;
168
+ const word = this.text.slice(at, end);
169
+ const place = this.placeAt(at);
170
+ this.pos = end;
171
+ if (word == "true" || word == "false") return makeNode("boolean", "", word, place);
172
+ if (word == "null") return makeNode("null", "", word, place);
173
+ if (isJsonNumber(word)) return numberNode(word, parseFloat(word), place);
174
+ if (word.length == 0) this.fail(at, first.length > 0 ? `unexpected ${first}` : "the text ends where a value should be");
175
+ else if (first == "'") this.fail(at, "text is written in double quotes");
176
+ else this.fail(at, `"${word}" is not a value - text is written in double quotes`);
177
+ return makeNode("null", "", "", place);
178
+ }
179
+
180
+ object() {
181
+ const node = makeNode("object", "", "", this.placeAt(this.pos));
182
+ this.pos++;
183
+
184
+ while (!this.failed) {
185
+ this.skip();
186
+ if (this.failed) return node;
187
+
188
+ if (this.text.charAt(this.pos) == "}") {
189
+ this.pos++;
190
+ this.takeComments();
191
+ return node;
192
+ }
193
+
194
+ if (this.text.charAt(this.pos) != "\"") {
195
+ this.fail(this.pos, this.pos >= this.text.length ? "the object is never closed with }" : "a key is written in double quotes");
196
+ return node;
197
+ }
198
+
199
+ const comments = this.takeComments();
200
+ const keyAt = this.pos;
201
+ const key = this.string().text;
202
+ this.skip();
203
+
204
+ if (this.text.charAt(this.pos) != ":") {
205
+ this.fail(this.pos, "expected : after the key");
206
+ return node;
207
+ }
208
+
209
+ this.pos++;
210
+ this.skip();
211
+ this.takeComments();
212
+ const value = this.value();
213
+ if (this.failed) return node;
214
+ const twice = node.items.find(each => each.key == key);
215
+
216
+ if (twice != null) {
217
+ this.fail(keyAt, `"${key}" is here twice - first on line ${twice.line}`);
218
+ return node;
219
+ }
220
+
221
+ const place = this.placeAt(keyAt);
222
+ value.key = key;
223
+ value.line = place.line;
224
+ value.column = place.column;
225
+ value.comments = comments;
226
+ node.items.push(value);
227
+ if (!this.comma("}")) return node;
228
+ }
229
+ return node;
230
+ }
231
+
232
+ array() {
233
+ const node = makeNode("array", "", "", this.placeAt(this.pos));
234
+ this.pos++;
235
+
236
+ while (!this.failed) {
237
+ this.skip();
238
+ if (this.failed) return node;
239
+
240
+ if (this.text.charAt(this.pos) == "]") {
241
+ this.pos++;
242
+ this.takeComments();
243
+ return node;
244
+ }
245
+
246
+ if (this.pos >= this.text.length) {
247
+ this.fail(this.pos, "the array is never closed with ]");
248
+ return node;
249
+ }
250
+
251
+ const comments = this.takeComments();
252
+ const item = this.value();
253
+ if (this.failed) return node;
254
+ item.comments = comments;
255
+ node.items.push(item);
256
+ if (!this.comma("]")) return node;
257
+ }
258
+ return node;
259
+ }
260
+
261
+ /** After a member or an item: a comma, or the bracket that closes; false when it is neither. */
262
+ comma(close: string) {
263
+ this.skip();
264
+ const next = this.text.charAt(this.pos);
265
+
266
+ if (next == ",") {
267
+ this.pos++;
268
+ return true;
269
+ }
270
+
271
+ if (next != close) this.fail(this.pos, `expected , or ${close}`);
272
+ return next == close;
273
+ }
274
+
275
+ string() {
276
+ const at = this.pos;
277
+ let value = "";
278
+ let i = at + 1;
279
+
280
+ for (;;) {
281
+ const character = this.text.charAt(i);
282
+
283
+ if (character == "\"") {
284
+ i++;
285
+ break;
286
+ }
287
+
288
+ if (character == "" || character == "\n") {
289
+ this.fail(at, "text that is never closed with \"");
290
+ break;
291
+ }
292
+
293
+ if (character == BACKSLASH) {
294
+ const letter = this.text.charAt(i + 1);
295
+ const known = ESCAPE_LETTERS.indexOf(letter);
296
+ const hex = this.text.slice(i + 2, i + 6);
297
+
298
+ if (known >= 0 && letter.length == 1) {
299
+ value += ESCAPED.charAt(known);
300
+ i += 2;
301
+ } else if (letter == "u" && hex.length == 4 && isHexDigits(hex)) {
302
+ value += String.fromCharCode(parseInt(hex, 16));
303
+ i += 6;
304
+ } else {
305
+ this.fail(i, `an unknown escape ${BACKSLASH}${letter}`);
306
+ break;
307
+ }
308
+
309
+ continue;
310
+ }
311
+
312
+ value += character;
313
+ i++;
314
+ }
315
+
316
+ this.pos = i;
317
+ return makeNode("string", "", value, this.placeAt(at));
318
+ }
319
+ }
320
+
321
+ /** Text in double quotes, escaped as JSON escapes it. */
322
+ export function jsonText(text: string) {
323
+ let quoted = "";
324
+ for (const character of text.split("")) {
325
+ if (character == "\"" || character == BACKSLASH) quoted += `${BACKSLASH}${character}`;
326
+ else if (character == "\n") quoted += `${BACKSLASH}n`;
327
+ else if (character == "\t") quoted += `${BACKSLASH}t`;
328
+ else if (isControl(character)) quoted += unicodeEscape(character);
329
+ else quoted += character;
330
+ }
331
+ return `"${quoted}"`;
332
+ }
333
+
334
+ function scalar(node: TreeNode) {
335
+ if (node.kind == "string") return jsonText(node.text);
336
+ if (node.kind == "number" && node.text.length > 0) return node.text;
337
+ // JSON has no NaN and no Infinity.
338
+ if (node.kind == "number") return isFinite(node.value) ? `${node.value}` : "null";
339
+ if (node.kind == "boolean") return node.text;
340
+ return "null";
341
+ }
342
+
343
+ /** An array of a few short scalars fits on one line: ["a", "b"]. */
344
+ function oneLine(array: TreeNode) {
345
+ if (!array.items.every(item => (isScalar(item) || item.kind == "null") && item.comments == null)) return "";
346
+ const line = `[${array.items.map(scalar).join(", ")}]`;
347
+ return line.length <= 60 ? line : "";
348
+ }
349
+
350
+ /** A value's lines at `pad`: its first line goes after `head` (a key, or nothing), its last gets `end` (a comma, or nothing). */
351
+ function writeValue(lines: string[], node: TreeNode, head: string, end: string, pad: string, unit: string) {
352
+ const container = node.kind == "object" || node.kind == "array";
353
+
354
+ if (!container || node.items.length == 0) {
355
+ const empty = node.kind == "object" ? "{}" : "[]";
356
+ lines.push(`${pad}${head}${container ? empty : scalar(node)}${end}`);
357
+ return;
358
+ }
359
+
360
+ const short = node.kind == "array" ? oneLine(node) : "";
361
+
362
+ if (short.length > 0) {
363
+ lines.push(`${pad}${head}${short}${end}`);
364
+ return;
365
+ }
366
+
367
+ lines.push(`${pad}${head}${node.kind == "object" ? "{" : "["}`);
368
+ const inner = pad + unit;
369
+ for (let i = 0; i < node.items.length; i++) {
370
+ const each = node.items[i];
371
+ writeComments(lines, each.comments, inner);
372
+ const key = node.kind == "object" ? `${jsonText(each.key)}: ` : "";
373
+ writeValue(lines, each, key, i < node.items.length - 1 ? "," : "", inner, unit);
374
+ }
375
+ lines.push(`${pad}${node.kind == "object" ? "}" : "]"}${end}`);
376
+ }
377
+
378
+ /** The document as JSON lines, its comments kept. */
379
+ export function writeJson(document: TreeDocument) {
380
+ const lines: string[] = [];
381
+ const unit = document.indent.length > 0 ? document.indent : " ";
382
+ writeComments(lines, document.root.comments, "");
383
+ writeValue(lines, document.root, "", "", "", unit);
384
+ writeComments(lines, document.tail, "");
385
+ return lines;
386
+ }
package/src/natives.ts ADDED
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Config Core for Pawn plugins: the 28 cfg_* natives of the original
3
+ * universal_config.amxx, as include/universal_config.inc declares them.
4
+ */
5
+ import { Player, plugin, print, server } from "~/facade";
6
+ import { console_print } from "~/natives";
7
+ import { setBaseDir } from "./index";
8
+ import * as ini from "./ini";
9
+
10
+ plugin({ name: "Config Core", version: "0.1.0", author: "kukson777", description: "INI configs for plugins: the cfg_* natives", include: "universal_config.inc" });
11
+
12
+ /** A loaded config file. */
13
+ export enum ConfigFile {
14
+ CFG_FILE_INVALID = -1,
15
+ }
16
+
17
+ /** A section of a loaded file. */
18
+ export enum ConfigSection {
19
+ CFG_SECTION_INVALID = -1,
20
+ }
21
+
22
+ /** What a key is: a `key = value` line or a `key = { ... }` block. */
23
+ export enum EntryType {
24
+ CFG_ENTRY_SIMPLE,
25
+ CFG_ENTRY_BRACKET,
26
+ }
27
+
28
+ /** What a key holds: one value, a line of strings, or rows - one value is a line of one string. */
29
+ export enum ContentType {
30
+ CFG_CONTENT_SIMPLE,
31
+ CFG_CONTENT_STRINGS,
32
+ CFG_CONTENT_ENTRIES,
33
+ }
34
+
35
+ server.addEventListener("init", () => {
36
+ console.log("[ConfigCore] Plugin initialized (v0.1.0)");
37
+ });
38
+
39
+ server.addCommand("dump_config", dumpConfig, { access: "Cvar", description: "Dumps all configurations" });
40
+
41
+ /** Every loaded section and its entries: headings to the admin's console, entries to the server's, as the original does. */
42
+ function dumpConfig(player: Player) {
43
+ print(player, "Current Configuration Dump:", "console");
44
+ for (const section of ini.dump()) {
45
+ print(player, section.heading, "console");
46
+ for (const line of section.lines) console_print(0, line);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Loads a config file from `configs/<base dir>/`; ".ini" is added when the
52
+ * name has none. A file that is not there still gets a handle, empty.
53
+ */
54
+ export function cfg_load_file(fileName: string) {
55
+ return ini.configHandle(ini.loadFile(fileName));
56
+ }
57
+
58
+ /** A section of a loaded file by its name, or CFG_SECTION_INVALID. */
59
+ export function cfg_get_section(cfg: ConfigFile, sectionName: string) {
60
+ const config = ini.configByHandle(cfg);
61
+ const found = config != null ? ini.section(config, sectionName) : null;
62
+ return found != null ? ini.sectionHandle(found) : -1;
63
+ }
64
+
65
+ /**
66
+ * The value of a key, or of a path ("block/subkey"), into value[]; index is
67
+ * the value's place in its line. false when there is none.
68
+ */
69
+ export function cfg_get_value(section: ConfigSection, key: string, index = 0) {
70
+ const found = ini.sectionByHandle(section);
71
+ return found != null ? ini.getValue(found, key, index) : null;
72
+ }
73
+
74
+ /** A value by a path: index is its place in the line, lineIndex the row of the block. */
75
+ export function cfg_get_value_by_path(section: ConfigSection, path: string, index = 0, lineIndex = 0) {
76
+ const found = ini.sectionByHandle(section);
77
+ return found != null ? ini.getValueByPath(found, path, index, lineIndex) : null;
78
+ }
79
+
80
+ /** The whole number a key or path holds; 0 when there is none. */
81
+ export function cfg_get_int(section: ConfigSection, key: string, index = 0) {
82
+ const found = ini.sectionByHandle(section);
83
+ return found != null ? ini.getInt(found, key, index) : 0;
84
+ }
85
+
86
+ /** The number a key or path holds; 0.0 when there is none. */
87
+ export function cfg_get_float(section: ConfigSection, key: string, index = 0) {
88
+ const found = ini.sectionByHandle(section);
89
+ return found != null ? ini.getNumber(found, key, index) : 0;
90
+ }
91
+
92
+ /** true when the value is a whole number other than 0. */
93
+ export function cfg_get_bool(section: ConfigSection, key: string, index = 0) {
94
+ const found = ini.sectionByHandle(section);
95
+ return found != null ? ini.getBoolean(found, key, index) : false;
96
+ }
97
+
98
+ /**
99
+ * The words of a key's line (index: which line, when the key is there more
100
+ * than once); for a block, every value of every row. The caller destroys the
101
+ * array; Invalid_Array when there are no words.
102
+ */
103
+ export function cfg_get_value_array(section: ConfigSection, key: string, index = 0) {
104
+ const found = ini.sectionByHandle(section);
105
+ if (found == null) return null;
106
+ return ini.getWords(found, key, index);
107
+ }
108
+
109
+ /** The numbers of value index as an array of Floats; the caller destroys it. Invalid_Array when there are none. */
110
+ export function cfg_get_float_array(section: ConfigSection, key: string, index = 0) {
111
+ const found = ini.sectionByHandle(section);
112
+ if (found == null) return null;
113
+ const numbers = ini.getNumbers(found, key, index);
114
+ return numbers != null && numbers.length > 0 ? numbers : null;
115
+ }
116
+
117
+ /**
118
+ * Every value of the line a path leads to (lineIndex: the row of a block), as
119
+ * written. The caller destroys the array.
120
+ */
121
+ export function cfg_get_value_array_by_path(section: ConfigSection, path: string, index = 0, lineIndex = 0) {
122
+ const found = ini.sectionByHandle(section);
123
+ if (found == null) return null;
124
+ return ini.getValues(found, path, index, lineIndex);
125
+ }
126
+
127
+ /** The section's keys in file order; Invalid_Array when it has none. */
128
+ export function cfg_get_top_level_keys(section: ConfigSection) {
129
+ const found = ini.sectionByHandle(section);
130
+ if (found == null) return null;
131
+ const keys = ini.keys(found);
132
+ return keys.length > 0 ? keys : null;
133
+ }
134
+
135
+ /**
136
+ * The whole section, as an array of three: the keys, the values (an array of
137
+ * strings per key, "{block}" for a block) and [key, value] index pairs.
138
+ */
139
+ export function cfg_get_section_data(section: ConfigSection) {
140
+ const found = ini.sectionByHandle(section);
141
+ if (found == null) return null;
142
+ const entries = ini.entries(found);
143
+ if (entries.length == 0) return null;
144
+
145
+ const pairs: number[][] = [];
146
+ for (let i = 0; i < entries.length; i++) pairs.push([i, i]);
147
+ const data: SectionData = {
148
+ keys: entries.map(entry => entry.key),
149
+ values: entries.map(entry => (entry.block ? ["{block}"] : entry.values)),
150
+ pairs,
151
+ };
152
+ return data;
153
+ }
154
+
155
+ /** What cfg_get_section_data hands Pawn: an Array of these three, in this order. */
156
+ interface SectionData {
157
+ keys: string[];
158
+ values: string[][];
159
+ /** [key, value] index pairs. */
160
+ pairs: number[][];
161
+ }
162
+
163
+ /** How many values a line has, rows a block has, or - for a key that is there more than once - how many times it is. */
164
+ export function cfg_get_array_size(section: ConfigSection, key: string) {
165
+ const found = ini.sectionByHandle(section);
166
+ return found != null ? ini.size(found, key) : 0;
167
+ }
168
+
169
+ /** Sets a value; a key that is not there is made. For a block, lineIndex is the row. */
170
+ export function cfg_set_value(section: ConfigSection, key: string, value: string, index = 0, lineIndex = 0) {
171
+ const found = ini.sectionByHandle(section);
172
+ return found != null ? ini.set(found, key, value, index, lineIndex) : false;
173
+ }
174
+
175
+ /** Sets a whole number. */
176
+ export function cfg_set_int(section: ConfigSection, key: string, value: number, index = 0) {
177
+ const found = ini.sectionByHandle(section);
178
+ return found != null ? ini.setInt(found, key, value, index) : false;
179
+ }
180
+
181
+ /** Sets a number; it is written as it was given, 2.5 as "2.5". */
182
+ export function cfg_set_float(section: ConfigSection, key: string, value: number, index = 0) {
183
+ const found = ini.sectionByHandle(section);
184
+ return found != null ? ini.setNumber(found, key, value, index) : false;
185
+ }
186
+
187
+ /** Sets 1 or 0. */
188
+ export function cfg_set_bool(section: ConfigSection, key: string, value: boolean, index = 0) {
189
+ const found = ini.sectionByHandle(section);
190
+ return found != null ? ini.setBoolean(found, key, value, index) : false;
191
+ }
192
+
193
+ /** Removes every entry of the key; false when there was none. */
194
+ export function cfg_delete_key(section: ConfigSection, key: string) {
195
+ const found = ini.sectionByHandle(section);
196
+ return found != null ? ini.remove(found, key) : false;
197
+ }
198
+
199
+ /** Whether the section has the key. */
200
+ export function cfg_has_key(section: ConfigSection, key: string) {
201
+ const found = ini.sectionByHandle(section);
202
+ return found != null ? ini.has(found, key) : false;
203
+ }
204
+
205
+ /** A section of the file; one that is not there is made, in memory. */
206
+ export function cfg_create_section(cfg: ConfigFile, sectionName: string) {
207
+ const config = ini.configByHandle(cfg);
208
+ return config != null ? ini.sectionHandle(ini.createSection(config, sectionName)) : -1;
209
+ }
210
+
211
+ /** Makes the key a line of values or a block; a key that is not there is made. */
212
+ export function cfg_set_entry_type(section: ConfigSection, key: string, entryType: EntryType) {
213
+ const kind = entryType == EntryType.CFG_ENTRY_BRACKET ? "block" : "value";
214
+ const found = ini.sectionByHandle(section);
215
+ if (found == null) return false;
216
+ ini.setKind(found, key, kind);
217
+ return true;
218
+ }
219
+
220
+ /** Says what the key holds; a key that is not there is made. */
221
+ export function cfg_set_entry_content_type(section: ConfigSection, key: string, contentType: ContentType) {
222
+ const content = contentType == ContentType.CFG_CONTENT_ENTRIES ? "entries" : "strings";
223
+ const found = ini.sectionByHandle(section);
224
+ if (found == null) return false;
225
+ ini.setContent(found, key, content);
226
+ return true;
227
+ }
228
+
229
+ /**
230
+ * Writes one section to a file; the file's other sections are not in it.
231
+ * With cfg = CFG_FILE_INVALID the section is looked for in every loaded file.
232
+ */
233
+ export function cfg_write_file(cfg: ConfigFile, fileName: string, sectionName: string) {
234
+ const config = ini.configByHandle(cfg);
235
+ // No file named: the section of that name in any file, the last one loaded.
236
+ const found = config != null ? ini.section(config, sectionName) : ini.findSection(sectionName);
237
+ return found != null && ini.writeSection(found, fileName);
238
+ }
239
+
240
+ /**
241
+ * Saves every section of the file with the comments it was read with. The
242
+ * file name has to be given: without one it returns false.
243
+ */
244
+ export function cfg_save_config(cfg: ConfigFile, fileName?: string) {
245
+ const config = ini.configByHandle(cfg);
246
+ return config != null && ini.saveFile(config, fileName ?? "");
247
+ }
248
+
249
+ /** The folder under configs/ that file names are relative to. */
250
+ export function cfg_set_base_dir(dir: string) {
251
+ setBaseDir(dir);
252
+ }
253
+
254
+ /** How many sections are loaded, across every file. */
255
+ export function cfg_get_sections_count() {
256
+ return ini.sectionCount();
257
+ }
258
+
259
+ /** A section's name by its number, 0 .. cfg_get_sections_count() - 1. */
260
+ export function cfg_get_section_name(index: number) {
261
+ return ini.sectionByHandle(index)?.name ?? null;
262
+ }
263
+
264
+ /**
265
+ * The comment line written above row `row` of the block `key` - "; ID | VALUE",
266
+ * say; "" removes it. false when there is no such block or row.
267
+ */
268
+ export function cfg_set_row_comment(section: ConfigSection, key: string, row: number, comment: string) {
269
+ const found = ini.sectionByHandle(section);
270
+ return found != null ? ini.setRowComment(found, key, row, comment) : false;
271
+ }