@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kukson777
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/PAWN.md ADDED
@@ -0,0 +1,13 @@
1
+ # Config Core for Pawn plugins
2
+
3
+ [English](PAWN.md) | [Русский](PAWN.ru.md)
4
+
5
+ Config Core gives Pawn plugins the 28 natives of `universal_config.inc` — `cfg_load_file`, `cfg_get_value`, `cfg_set_int` and the rest — with their signatures, so compiled `.amxx` plugins (Menu Core's Pawn users among them) work against it unchanged.
6
+
7
+ It takes the place of `universal_config.amxx`: comment that one out in `plugins.ini`, since two plugins cannot give the same natives. Pawn plugins write `#include <universal_config>`; the package ships `include/universal_config.inc`.
8
+
9
+ ## How the natives behave
10
+
11
+ - No Pawn limits: a key or a value is as long as it is written, a section keeps every entry, blocks nest as deep as they are written, any number of files loads.
12
+ - A number reads the way `parseFloat` reads it (`1e5` is 100000) and is written as the number it is (`2.5`, not `2.500000`).
13
+ - `CFG_CONTENT_SIMPLE` and `CFG_CONTENT_STRINGS` are one content: a block of either holds a line of values, and `cfg_set_value` writes into it.
package/PAWN.ru.md ADDED
@@ -0,0 +1,13 @@
1
+ # Config Core для Pawn-плагинов
2
+
3
+ [English](PAWN.md) | [Русский](PAWN.ru.md)
4
+
5
+ Config Core отдаёт Pawn-плагинам 28 нативов из `universal_config.inc` — `cfg_load_file`, `cfg_get_value`, `cfg_set_int` и остальные — с их сигнатурами, так что собранные `.amxx`-плагины (в том числе Pawn-плагины, работающие с Menu Core) работают без изменений.
6
+
7
+ Он встаёт на место `universal_config.amxx`: тот закомментируйте в `plugins.ini` — два плагина не могут отдавать одни и те же нативы. Pawn-плагины пишут `#include <universal_config>`; в пакете лежит `include/universal_config.inc`.
8
+
9
+ ## Как работают нативы
10
+
11
+ - Нет ограничений Pawn: ключ и значение любой длины, секция хранит все записи, блоки вложены на любую глубину, загружается сколько угодно файлов.
12
+ - Число читается так, как его читает `parseFloat` (`1e5` — это 100000), и записывается как есть (`2.5`, а не `2.500000`).
13
+ - `CFG_CONTENT_SIMPLE` и `CFG_CONTENT_STRINGS` — одно и то же содержимое: блок любого из них хранит строку значений, и `cfg_set_value` пишет в него.
package/README.md ADDED
@@ -0,0 +1,249 @@
1
+ <div align="center">
2
+
3
+ <img src="assets/logo.svg" width="96" alt="Config Core">
4
+
5
+ # Config Core
6
+
7
+ *Configs in INI, YAML or JSON, read into typed objects and written back*
8
+
9
+ [![amxts module](https://img.shields.io/badge/amxts-module-3178c6?style=flat-square)](https://amxts.github.io/)
10
+ [![License](https://img.shields.io/badge/License-MIT-blue?style=flat-square)](LICENSE)
11
+
12
+ [Features](#features) • [Installation](#installation) • [Usage](#usage) • [File formats](#file-formats) • [INI files](#ini-files) • [Pawn plugins](#pawn-plugins)
13
+
14
+ **English** | [Русский](README.ru.md)
15
+
16
+ </div>
17
+
18
+ Read a config file — INI, YAML or JSON, whichever the admin wrote — into an object shaped like its defaults, typed in the editor, change it and write the file back, with its comments where they were.
19
+
20
+ ## Features
21
+
22
+ - **A typed object.** `configs.load("settings", { chat: { prefix: "[Server]" } })` gives `settings.chat.prefix`, typed as its default is; a typo is the editor's error, not a setting silently missing.
23
+ - **Three formats, one API.** `settings.yaml`, `settings.json` or `settings.ini`: the same code reads each of them.
24
+ - **Mistakes said where they are.** A value of the wrong kind, a name not in its union, an unknown key — each is named in the server console with the file and the line (and the column in YAML and JSON), with "did you mean", and the default is kept.
25
+ - **Writes back what it read.** `configs.save(settings)` puts the values in their places; comments on lines of their own survive, a missing file is written as YAML.
26
+ - **Files of any shape too.** A tree of values with paths — `shop.items[0].name` — for a file whose keys are not known beforehand.
27
+ - **INI with blocks and rows.** Sections, blocks and rows of values read into the same objects — and the 28 `cfg_*` natives serve them to Pawn plugins.
28
+ - **One instance per server.** Every plugin, TypeScript or Pawn, sees the same loaded files and the same base folder.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ npx amxts module add config-core
34
+ ```
35
+
36
+ It installs the package and adds it to `modules` in your project's `amxts.config.ts`. The module's options go beside it, under `configs`:
37
+
38
+ ```ts
39
+ export default defineConfig({
40
+ modules: ["@amxts/config-core"],
41
+ configs: {
42
+ baseDir: "myserver", // names are read from configs/myserver/
43
+ },
44
+ });
45
+ ```
46
+
47
+ | Option | Default | What it does |
48
+ | --- | --- | --- |
49
+ | `baseDir` | `""` | The folder under `configs/` that file names are read from; `""` is `configs/` itself. |
50
+
51
+ A module that reads its files through Config Core, such as [Menu Core](https://github.com/amxts/menu-core), brings it along — there is nothing to add for it.
52
+
53
+ ## Usage
54
+
55
+ A plugin uses the module as `configs`, without an import line: the build adds the import to the plugins that use it, and builds the module only when some plugin does.
56
+
57
+ ```ts
58
+ const settings = configs.load("settings", { // configs/myserver/settings.yaml, .json or .ini
59
+ chat: { prefix: "[Server]" },
60
+ round: { time: 2.5 },
61
+ maps: ["de_dust2"],
62
+ });
63
+
64
+ console.log(`${settings.chat.prefix} ${settings.maps.length} maps`); // from the file, or the default
65
+
66
+ settings.round.time = 3;
67
+ configs.save(settings); // in the file's format, comments kept
68
+ ```
69
+
70
+ The defaults say the object's shape. What the file has replaces the default, value by value; what it leaves out stays the default. An interface gives the fields descriptions, optional fields and unions of names:
71
+
72
+ ```ts
73
+ type Mode = "normal" | "dm" | "knife";
74
+
75
+ interface Settings {
76
+ /** The text before every chat message. */
77
+ chat: { prefix: string };
78
+ round: { time: number; mode: Mode };
79
+ maps: string[];
80
+ /** Shown on join; left out, nothing is shown. */
81
+ motd?: string;
82
+ }
83
+
84
+ const settings = configs.load<Settings>("settings", {
85
+ chat: { prefix: "[Server]" },
86
+ round: { time: 2.5, mode: "normal" },
87
+ maps: [],
88
+ });
89
+ ```
90
+
91
+ A mistake in the file keeps the default, and the console says where it is:
92
+
93
+ ```
94
+ [ConfigCore] configs/myserver/settings.yaml:6:9: "mode" is "dmm", not one of "normal", "dm", "knife" - did you mean "dm"? - the default stays
95
+ [ConfigCore] configs/myserver/settings.yaml:2:3: unknown key "prefx" in "chat" - did you mean "prefix"?
96
+ ```
97
+
98
+ | In the object | In the file |
99
+ | --- | --- |
100
+ | `string`, `number`, `boolean` | a value; a number may be text, a boolean `true`/`false`, `yes`/`no`, `on`/`off` in any case or a number (0 is false) |
101
+ | a union of names, `"normal" \| "dm"` | one of them |
102
+ | `string[]`, `number[]`, `boolean[]`, a list of names | a list — an item of another kind is left out |
103
+ | `string[][]` (of text, numbers, booleans or names) | a list of lists: rows of values; an INI block of rows |
104
+ | an object, nested as deep as needed | an object |
105
+ | a list of objects | a list of objects — a field an item leaves out is empty, and said |
106
+ | `Map<string, number>` (of text, numbers, booleans or names) | an object |
107
+ | `field?: T` | may be left out: `undefined` |
108
+
109
+ - `configs.load()` gives a copy of the defaults with the file's values in it; a second call reads the file again. `configs.save(settings)` writes every field into the file it came from.
110
+ - An object `load()` read without a type has no type name to write in a function's parameter: to pass the settings around, declare an interface and write `configs.load<Settings>(...)`.
111
+ - The build reads the shape from the source: the defaults as an object literal (a list with an item in it), or the type given — `configs.load<Settings>(...)`. What it cannot read — an empty list without a type, a list of lists of lists — stops the build with the place and the fix.
112
+
113
+ ### Which file is read
114
+
115
+ `load("settings")` and `read("settings")` read the first of `settings.ini`, `settings.yaml`, `settings.yml`, `settings.json` and `settings.jsonc` that is there. When two of them are, the first is read and the server console says so: keep one. A name with its extension — `load("settings.json", ...)` — reads that file. A file that is not there reads as the defaults, and is saved as `settings.yaml`.
116
+
117
+ ### Files of unknown shape
118
+
119
+ A file whose keys are not known beforehand — a list of maps with settings of their own, a file another plugin writes — reads as a tree of values:
120
+
121
+ ```ts
122
+ const maps = configs.read("maps");
123
+
124
+ for (const map of maps.values()) {
125
+ console.log(`${map.key}: ${map.getNumber("rounds", 30)} rounds`);
126
+ }
127
+
128
+ maps.setNumber("de_dust2.rounds", 20);
129
+ maps.save();
130
+ ```
131
+
132
+ A value is a `ConfigNode`: an object, an array, text, a number, a boolean or null (`kind`), with the file, line and column it was read from. A path leads into it — `chat.prefix`, `items[0].name` — and a getter takes the value's fallback for when it is not there or not of that kind.
133
+
134
+ ### API
135
+
136
+ | Function | What it does |
137
+ | --- | --- |
138
+ | `load(name, defaults)` | Reads a config file into an object shaped like `defaults`. |
139
+ | `save(settings)` | Writes an object `load()` read back into its file. |
140
+ | `read(name)` | Reads a config file as a tree of values; the file's top value. |
141
+ | `resolve(name)` | The file `load(name)` and `read(name)` read, e.g. `"settings.yaml"`. |
142
+ | `parse(text, format)` | Reads a text — `"yaml"`, `"json"` or `"ini"` — as a tree. |
143
+
144
+ | `ConfigNode` | What it does |
145
+ | --- | --- |
146
+ | `kind` · `key` · `file` · `format` · `line` · `column` | What the value is, and where it was read. |
147
+ | `get(path)` · `has(path)` | The value a path leads to, or `null`; whether there is one. |
148
+ | `keys(path?)` · `values(path?)` | An object's keys; an array's items or an object's values. |
149
+ | `getString(path?, fallback?)` · `getNumber` · `getBoolean` · `getStrings` | A value as text, a number, a boolean, a list of text. |
150
+ | `set(path, text)` · `setNumber` · `setBoolean` · `setStrings` | Sets a value, making the objects on its path; `false` where the path goes through text or a number. |
151
+ | `remove(path)` · `save()` | Removes a value; writes the whole file back in its format. |
152
+
153
+ ## File formats
154
+
155
+ The same settings in each of them:
156
+
157
+ ```yaml
158
+ # settings.yaml
159
+ chat:
160
+ prefix: "[Server]"
161
+ rules:
162
+ - Be nice
163
+ - No cheats
164
+ round:
165
+ time: 2.5
166
+ hud:
167
+ enabled: true
168
+ ```
169
+
170
+ ```jsonc
171
+ // settings.json or settings.jsonc
172
+ {
173
+ "chat": {
174
+ "prefix": "[Server]",
175
+ "rules": ["Be nice", "No cheats"],
176
+ },
177
+ "round": { "time": 2.5 },
178
+ "hud": { "enabled": true },
179
+ }
180
+ ```
181
+
182
+ ```ini
183
+ ; settings.ini
184
+ [chat]
185
+ prefix = [Server]
186
+ rules = "Be nice" "No cheats"
187
+
188
+ [round]
189
+ time = 2.5
190
+
191
+ [hud]
192
+ enabled = 1
193
+ ```
194
+
195
+ - **YAML:** mappings and lists, indented or in `{ }` and `[ ]`; plain, `'single'` and `"double"`-quoted text with its escapes; `|` and `>` blocks for text of several lines; numbers, `true`/`false`, `null` and `~` as YAML 1.2 reads them — `yes` is text; `#` comments; `---` before the document and `...` after it.
196
+ - **Not read in YAML:** anchors and aliases (`&`, `*`), tags (`!`), complex keys (`?`), directives (`%YAML`), several documents in one file, and a plain value going on over several lines. The file then reads empty, and the console names the line and the column: `configs/settings.yaml:4:9: anchors and aliases (& and *) are not supported - write the value out`.
197
+ - **JSON:** as JSON is written, plus JSONC's `//` and `/* */` comments and a comma after the last member or item — in `.json` files too.
198
+ - **INI:** each `[section]` is an object of the top — `[chat]` with `prefix = [Server]` is `settings.chat.prefix`; a line of several values is a list; a `key = { ... }` block is an object, or a list of rows. The rules INI adds are under [INI files](#ini-files).
199
+ - **Saving** writes the file in its own format: comments on lines of their own and blank lines stay; a comment after a value on its line, and comments inside `{ }` and `[ ]` in YAML, do not. JSON keeps its indentation.
200
+
201
+ ## INI files
202
+
203
+ An INI config reads into the same kind of object: each `[section]` is an object, a `key = { ... }` block of keys an object in it, a line of several values a list, and a block of rows in quotes a list of lists:
204
+
205
+ ```ini
206
+ ; configs/myserver/settings.ini
207
+ [MAIN]
208
+ CHAT_PREFIX = [MYPLUGIN]
209
+ MAPS = de_dust2 de_inferno de_nuke
210
+
211
+ HUD = {
212
+ HIDE_TIME = 255 50 50
213
+ }
214
+
215
+ CVARS = {
216
+ "mp_timelimit" "30"
217
+ "mp_freezetime" "3"
218
+ }
219
+ ```
220
+
221
+ ```ts
222
+ const settings = configs.load("settings", {
223
+ MAIN: {
224
+ CHAT_PREFIX: "[Server]",
225
+ MAPS: ["de_dust2"],
226
+ HUD: { HIDE_TIME: [255, 255, 255] },
227
+ CVARS: [["mp_timelimit", "20"]],
228
+ },
229
+ });
230
+
231
+ for (const row of settings.MAIN.CVARS) {
232
+ const cvar = new Cvar(row[0]);
233
+ cvar.value = row[1];
234
+ }
235
+ ```
236
+
237
+ - A block of rows stays a block when the object is saved — rows of one value too; the comments above its rows stay.
238
+ - A path into the tree of such a file (`configs.read()`) is written the same way: `MAIN.HUD.HIDE_TIME[0]`, `MAIN.CVARS[1][0]`. An item of a list is `[0]`: `MAIN.MAPS.0` looks for a key `0`.
239
+
240
+ > [!WARNING]
241
+ > **In an INI file:**
242
+ >
243
+ > - **Keys are found in any case, as the Pawn natives find them; section names are not:** `[main]` is not `MAIN`. A name the file has twice is its last section.
244
+ > - **A value with spaces is quoted:** `TITLE = "Main menu"`. Unquoted, it is a list of words: a text field keeps its default and says why, and Pawn's `cfg_*` natives and Menu Core read the first word.
245
+ > - **No place for a value at the top of the object that is not an object, nor for a list of objects:** in an INI file such a field stays its default, and the console says so once — write that config in YAML or JSON.
246
+
247
+ ## Pawn plugins
248
+
249
+ Pawn plugins use Config Core through the 28 `cfg_*` natives of `universal_config.inc` — `#include <universal_config>`, which the package ships in `include/`; compiled `.amxx` plugins work unchanged. The natives read INI files. In `plugins.ini` it takes the place of `universal_config.amxx`. When no TypeScript plugin of the project uses the module, `pawn: ["@amxts/config-core"]` in `amxts.config.ts` keeps it in the build for them. The details: [PAWN.md](PAWN.md).
package/README.ru.md ADDED
@@ -0,0 +1,249 @@
1
+ <div align="center">
2
+
3
+ <img src="assets/logo.svg" width="96" alt="Config Core">
4
+
5
+ # Config Core
6
+
7
+ *Конфиги в INI, YAML или JSON: читаются в типизированные объекты и пишутся обратно*
8
+
9
+ [![amxts module](https://img.shields.io/badge/amxts-module-3178c6?style=flat-square)](https://amxts.github.io/)
10
+ [![License](https://img.shields.io/badge/License-MIT-blue?style=flat-square)](LICENSE)
11
+
12
+ [Возможности](#возможности) • [Установка](#установка) • [Использование](#использование) • [Форматы файлов](#форматы-файлов) • [INI-файлы](#ini-файлы) • [Pawn-плагины](#pawn-плагины)
13
+
14
+ [English](README.md) | **Русский**
15
+
16
+ </div>
17
+
18
+ Прочитайте файл конфига — INI, YAML или JSON, какой написал админ, — в объект той же формы, что его значения по умолчанию, типизированный в редакторе, измените его и запишите файл обратно, с комментариями на своих местах.
19
+
20
+ ## Возможности
21
+
22
+ - **Типизированный объект.** `configs.load("settings", { chat: { prefix: "[Server]" } })` даёт `settings.chat.prefix` того же типа, что значение по умолчанию; опечатка — ошибка редактора, а не молча пропавшая настройка.
23
+ - **Три формата, один API.** `settings.yaml`, `settings.json` или `settings.ini`: один и тот же код читает любой из них.
24
+ - **Ошибки — с местом.** Значение не того вида, имя не из своего юниона, незнакомый ключ — каждое называется в консоли сервера с файлом и строкой (в YAML и JSON — и со столбцом), с «did you mean», и остаётся значение по умолчанию.
25
+ - **Записывает то, что прочитал.** `configs.save(settings)` кладёт значения на их места; комментарии на отдельных строках переживают сохранение, отсутствующий файл пишется в YAML.
26
+ - **И файлы любой формы.** Дерево значений с путями — `shop.items[0].name` — для файла, ключи которого заранее неизвестны.
27
+ - **INI с блоками и строками.** Секции, блоки и строки значений читаются в те же объекты — а 28 нативов `cfg_*` отдают их Pawn-плагинам.
28
+ - **Один экземпляр на сервер.** Все плагины, на TypeScript и на Pawn, видят одни и те же загруженные файлы и одну базовую папку.
29
+
30
+ ## Установка
31
+
32
+ ```bash
33
+ npx amxts module add config-core
34
+ ```
35
+
36
+ Команда ставит пакет и добавляет его в `modules` в `amxts.config.ts` проекта. Настройки модуля пишутся рядом, в `configs`:
37
+
38
+ ```ts
39
+ export default defineConfig({
40
+ modules: ["@amxts/config-core"],
41
+ configs: {
42
+ baseDir: "myserver", // имена файлов читаются из configs/myserver/
43
+ },
44
+ });
45
+ ```
46
+
47
+ | Опция | По умолчанию | Что делает |
48
+ | --- | --- | --- |
49
+ | `baseDir` | `""` | Папка внутри `configs/`, из которой читаются файлы; `""` — сама `configs/`. |
50
+
51
+ Модуль, который читает свои файлы через Config Core, например [Menu Core](https://github.com/amxts/menu-core), приносит его с собой — добавлять для него ничего не нужно.
52
+
53
+ ## Использование
54
+
55
+ Плагин пользуется модулем как `configs`, без строки импорта: сборка добавляет импорт в те плагины, которые им пользуются, и собирает модуль, только когда им пользуется хоть один.
56
+
57
+ ```ts
58
+ const settings = configs.load("settings", { // configs/myserver/settings.yaml, .json или .ini
59
+ chat: { prefix: "[Server]" },
60
+ round: { time: 2.5 },
61
+ maps: ["de_dust2"],
62
+ });
63
+
64
+ console.log(`${settings.chat.prefix} ${settings.maps.length} maps`); // из файла или по умолчанию
65
+
66
+ settings.round.time = 3;
67
+ configs.save(settings); // в формате файла, с комментариями
68
+ ```
69
+
70
+ Значения по умолчанию задают форму объекта. Что есть в файле, заменяет значение по умолчанию, значение за значением; чего в файле нет, остаётся по умолчанию. Интерфейс даёт полям описания, необязательные поля и юнионы имён:
71
+
72
+ ```ts
73
+ type Mode = "normal" | "dm" | "knife";
74
+
75
+ interface Settings {
76
+ /** Текст перед каждым сообщением в чате. */
77
+ chat: { prefix: string };
78
+ round: { time: number; mode: Mode };
79
+ maps: string[];
80
+ /** Показывается при входе; если не задан, ничего не показывается. */
81
+ motd?: string;
82
+ }
83
+
84
+ const settings = configs.load<Settings>("settings", {
85
+ chat: { prefix: "[Server]" },
86
+ round: { time: 2.5, mode: "normal" },
87
+ maps: [],
88
+ });
89
+ ```
90
+
91
+ Ошибка в файле оставляет значение по умолчанию, а консоль говорит, где она:
92
+
93
+ ```
94
+ [ConfigCore] configs/myserver/settings.yaml:6:9: "mode" is "dmm", not one of "normal", "dm", "knife" - did you mean "dm"? - the default stays
95
+ [ConfigCore] configs/myserver/settings.yaml:2:3: unknown key "prefx" in "chat" - did you mean "prefix"?
96
+ ```
97
+
98
+ | В объекте | В файле |
99
+ | --- | --- |
100
+ | `string`, `number`, `boolean` | значение; число можно текстом, логическое — `true`/`false`, `yes`/`no`, `on`/`off` в любом регистре или число (0 — ложь) |
101
+ | юнион имён, `"normal" \| "dm"` | одно из них |
102
+ | `string[]`, `number[]`, `boolean[]`, список имён | список — элемент другого вида пропускается |
103
+ | `string[][]` (текста, чисел, логических значений или имён) | список списков: строки значений; в INI — блок строк |
104
+ | объект любой вложенности | объект |
105
+ | список объектов | список объектов — поле, которого нет у элемента, пустое, и об этом сказано |
106
+ | `Map<string, number>` (текста, чисел, логических значений или имён) | объект |
107
+ | `field?: T` | можно не писать: `undefined` |
108
+
109
+ - `configs.load()` отдаёт копию значений по умолчанию со значениями из файла; второй вызов читает файл заново. `configs.save(settings)` пишет каждое поле в файл, из которого объект прочитан.
110
+ - У объекта, который `load()` прочитал без типа, нет имени типа, чтобы написать его в параметре функции: чтобы передавать настройки дальше, объявите интерфейс и пишите `configs.load<Settings>(...)`.
111
+ - Сборка читает форму из исходника: значения по умолчанию — объектным литералом (в списке — элемент), или указан тип — `configs.load<Settings>(...)`. Чего так не прочитать — пустой список без типа, список списков списков, — останавливает сборку с местом и тем, как исправить.
112
+
113
+ ### Какой файл читается
114
+
115
+ `load("settings")` и `read("settings")` читают первый из `settings.ini`, `settings.yaml`, `settings.yml`, `settings.json` и `settings.jsonc`, который есть. Если есть два, читается первый, а консоль сервера об этом говорит: оставьте один. Имя с расширением — `load("settings.json", ...)` — читает этот файл. Файла нет — читаются значения по умолчанию, а сохраняется он как `settings.yaml`.
116
+
117
+ ### Файлы неизвестной формы
118
+
119
+ Файл, ключи которого заранее неизвестны, — список карт со своими настройками, файл, который пишет другой плагин, — читается деревом значений:
120
+
121
+ ```ts
122
+ const maps = configs.read("maps");
123
+
124
+ for (const map of maps.values()) {
125
+ console.log(`${map.key}: ${map.getNumber("rounds", 30)} rounds`);
126
+ }
127
+
128
+ maps.setNumber("de_dust2.rounds", 20);
129
+ maps.save();
130
+ ```
131
+
132
+ Значение — это `ConfigNode`: объект, массив, текст, число, логическое значение или null (`kind`), с файлом, строкой и столбцом, где оно прочитано. Путь ведёт внутрь — `chat.prefix`, `items[0].name`, — а геттер принимает запасное значение на случай, если значения нет или оно другого вида.
133
+
134
+ ### API
135
+
136
+ | Функция | Что делает |
137
+ | --- | --- |
138
+ | `load(name, defaults)` | Читает файл конфига в объект той же формы, что `defaults`. |
139
+ | `save(settings)` | Записывает объект, прочитанный `load()`, обратно в его файл. |
140
+ | `read(name)` | Читает файл конфига деревом значений; верхнее значение файла. |
141
+ | `resolve(name)` | Файл, который читают `load(name)` и `read(name)`, например `"settings.yaml"`. |
142
+ | `parse(text, format)` | Читает текст — `"yaml"`, `"json"` или `"ini"` — деревом. |
143
+
144
+ | `ConfigNode` | Что делает |
145
+ | --- | --- |
146
+ | `kind` · `key` · `file` · `format` · `line` · `column` | Что это за значение и где оно прочитано. |
147
+ | `get(path)` · `has(path)` | Значение, к которому ведёт путь, или `null`; есть ли оно. |
148
+ | `keys(path?)` · `values(path?)` | Ключи объекта; элементы массива или значения объекта. |
149
+ | `getString(path?, fallback?)` · `getNumber` · `getBoolean` · `getStrings` | Значение как текст, число, логическое значение, список текста. |
150
+ | `set(path, text)` · `setNumber` · `setBoolean` · `setStrings` | Задаёт значение, создавая объекты на пути; `false`, если путь идёт через текст или число. |
151
+ | `remove(path)` · `save()` | Удаляет значение; записывает весь файл обратно в его формате. |
152
+
153
+ ## Форматы файлов
154
+
155
+ Одни и те же настройки в каждом из них:
156
+
157
+ ```yaml
158
+ # settings.yaml
159
+ chat:
160
+ prefix: "[Server]"
161
+ rules:
162
+ - Be nice
163
+ - No cheats
164
+ round:
165
+ time: 2.5
166
+ hud:
167
+ enabled: true
168
+ ```
169
+
170
+ ```jsonc
171
+ // settings.json или settings.jsonc
172
+ {
173
+ "chat": {
174
+ "prefix": "[Server]",
175
+ "rules": ["Be nice", "No cheats"],
176
+ },
177
+ "round": { "time": 2.5 },
178
+ "hud": { "enabled": true },
179
+ }
180
+ ```
181
+
182
+ ```ini
183
+ ; settings.ini
184
+ [chat]
185
+ prefix = [Server]
186
+ rules = "Be nice" "No cheats"
187
+
188
+ [round]
189
+ time = 2.5
190
+
191
+ [hud]
192
+ enabled = 1
193
+ ```
194
+
195
+ - **YAML:** отображения и списки — с отступами или в `{ }` и `[ ]`; простой текст, текст в `'одинарных'` и `"двойных"` кавычках со своими экранированиями; блоки `|` и `>` для текста в несколько строк; числа, `true`/`false`, `null` и `~`, как их читает YAML 1.2, — `yes` это текст; комментарии `#`; `---` перед документом и `...` после.
196
+ - **Чего в YAML нет:** якорей и ссылок (`&`, `*`), тегов (`!`), сложных ключей (`?`), директив (`%YAML`), нескольких документов в одном файле и простого значения, которое продолжается на следующих строках. Тогда файл читается пустым, а консоль называет строку и столбец: `configs/settings.yaml:4:9: anchors and aliases (& and *) are not supported - write the value out`.
197
+ - **JSON:** как пишется JSON, плюс комментарии `//` и `/* */` из JSONC и запятая после последнего поля или элемента — и в файлах `.json` тоже.
198
+ - **INI:** каждая `[секция]` — объект верхнего уровня: `[chat]` с `prefix = [Server]` — это `settings.chat.prefix`; строка из нескольких значений — список; блок `key = { ... }` — объект или список строк. Правила, которые добавляет INI, — в [INI-файлах](#ini-файлы).
199
+ - **Сохранение** пишет файл в его формате: комментарии на отдельных строках и пустые строки остаются; комментарий после значения на той же строке и комментарии внутри `{ }` и `[ ]` в YAML — нет. JSON сохраняет свои отступы.
200
+
201
+ ## INI-файлы
202
+
203
+ INI-конфиг читается в такой же объект: каждая `[секция]` — объект, блок ключей `key = { ... }` — объект в нём, строка из нескольких значений — список, а блок строк в кавычках — список списков:
204
+
205
+ ```ini
206
+ ; configs/myserver/settings.ini
207
+ [MAIN]
208
+ CHAT_PREFIX = [MYPLUGIN]
209
+ MAPS = de_dust2 de_inferno de_nuke
210
+
211
+ HUD = {
212
+ HIDE_TIME = 255 50 50
213
+ }
214
+
215
+ CVARS = {
216
+ "mp_timelimit" "30"
217
+ "mp_freezetime" "3"
218
+ }
219
+ ```
220
+
221
+ ```ts
222
+ const settings = configs.load("settings", {
223
+ MAIN: {
224
+ CHAT_PREFIX: "[Server]",
225
+ MAPS: ["de_dust2"],
226
+ HUD: { HIDE_TIME: [255, 255, 255] },
227
+ CVARS: [["mp_timelimit", "20"]],
228
+ },
229
+ });
230
+
231
+ for (const row of settings.MAIN.CVARS) {
232
+ const cvar = new Cvar(row[0]);
233
+ cvar.value = row[1];
234
+ }
235
+ ```
236
+
237
+ - Блок строк остаётся блоком, когда объект сохраняется, — и строки из одного значения тоже; комментарии над его строками остаются.
238
+ - Путь в дереве такого файла (`configs.read()`) пишется так же: `MAIN.HUD.HIDE_TIME[0]`, `MAIN.CVARS[1][0]`. Элемент списка — это `[0]`: `MAIN.MAPS.0` ищет ключ `0`.
239
+
240
+ > [!WARNING]
241
+ > **В INI-файле:**
242
+ >
243
+ > - **Ключи ищутся без учёта регистра, как их ищут Pawn-нативы, имена секций — нет:** `[main]` — не `MAIN`. Из двух секций с одним именем берётся последняя.
244
+ > - **Значение с пробелами берётся в кавычки:** `TITLE = "Main menu"`. Без кавычек это список слов: текстовое поле остаётся со значением по умолчанию, а консоль говорит почему, и нативы `cfg_*` Pawn и Menu Core читают первое слово.
245
+ > - **Нет места значению на верхнем уровне объекта, которое не объект, и списку объектов:** в INI-файле такое поле остаётся по умолчанию, и консоль один раз об этом говорит — такой конфиг пишите в YAML или JSON.
246
+
247
+ ## Pawn-плагины
248
+
249
+ Pawn-плагины работают с Config Core через 28 нативов `cfg_*` из `universal_config.inc` — `#include <universal_config>`, который лежит в пакете в `include/`; собранные `.amxx`-плагины работают без изменений. Нативы читают INI-файлы. В `plugins.ini` он встаёт на место `universal_config.amxx`. Когда модулем не пользуется ни один плагин проекта на TypeScript, `pawn: ["@amxts/config-core"]` в `amxts.config.ts` оставляет его в сборке для них. Подробности — в [PAWN.ru.md](PAWN.ru.md).