Config Core

Конфиги в INI, YAML или JSON: читаются в типизированные объекты и пишутся обратно
v0.1.2kukson777 kukson777

Прочитайте файл конфига — INI, YAML или JSON, какой написал админ, — в объект той же формы, что его значения по умолчанию, типизированный в редакторе, измените его и запишите файл обратно, с комментариями на своих местах.

Возможности

  • Типизированный объект. configs.load("settings", { chat: { prefix: "[Server]" } }) даёт settings.chat.prefix того же типа, что значение по умолчанию; опечатка — ошибка редактора, а не молча пропавшая настройка.
  • Три формата, один API. settings.yaml, settings.json или settings.ini: один и тот же код читает любой из них.
  • Ошибки — с местом. Значение не того вида, имя не из своего юниона, незнакомый ключ — каждое называется в консоли сервера с файлом и строкой (в YAML и JSON — и со столбцом), с «did you mean», и остаётся значение по умолчанию.
  • Записывает то, что прочитал. configs.save(settings) кладёт значения на их места; комментарии на отдельных строках переживают сохранение, отсутствующий файл пишется в YAML.
  • И файлы любой формы. Дерево значений с путями — shop.items[0].name — для файла, ключи которого заранее неизвестны.
  • INI с блоками и строками. Секции, блоки и строки значений читаются в те же объекты — а 28 нативов cfg_* отдают их Pawn-плагинам.
  • Один экземпляр на сервер. Все плагины, на TypeScript и на Pawn, видят одни и те же загруженные файлы и одну базовую папку.

Установка

npx amxts module add config-core

Команда ставит пакет и добавляет его в modules в amxts.config.ts проекта. Настройки модуля пишутся рядом, в configs:

export default 
defineConfig
({
modules
: ["@amxts/config-core"],
configs
: {
baseDir
: "myserver", // имена файлов читаются из configs/myserver/
}, });
ОпцияПо умолчаниюЧто делает
baseDir""Папка внутри configs/, из которой читаются файлы; "" — сама configs/.

Модуль, который читает свои файлы через Config Core, например Menu Core, приносит его с собой — добавлять для него ничего не нужно.

Использование

Плагин пользуется модулем как configs, без строки импорта: сборка добавляет импорт в те плагины, которые им пользуются, и собирает модуль, только когда им пользуется хоть один.

const 
settings
=
configs
.
load
("settings", { // configs/myserver/settings.yaml, .json или .ini
chat
: {
prefix
: "[Server]" },
round
: {
time
: 2.5 },
maps
: ["de_dust2"],
});
console
.
log
(`${
settings
.
chat
.
prefix
} ${
settings
.
maps
.
length
} maps`); // из файла или по умолчанию
settings
.
round
.
time
= 3;
configs
.
save
(
settings
); // в формате файла, с комментариями

Значения по умолчанию задают форму объекта. Что есть в файле, заменяет значение по умолчанию, значение за значением; чего в файле нет, остаётся по умолчанию. Интерфейс даёт полям описания, необязательные поля и юнионы имён:

type Mode = "normal" | "dm" | "knife";

interface Settings {
    /** Текст перед каждым сообщением в чате. */
    
chat
: {
prefix
: string };
round
: {
time
: number;
mode
: Mode };
maps
: string[];
/** Показывается при входе; если не задан, ничего не показывается. */
motd
?: string;
} const
settings
=
configs
.
load
<Settings>("settings", {
chat
: {
prefix
: "[Server]" },
round
: {
time
: 2.5,
mode
: "normal" },
maps
: [],
});

Ошибка в файле оставляет значение по умолчанию, а консоль говорит, где она:

[ConfigCore] configs/myserver/settings.yaml:6:9: "mode" is "dmm", not one of "normal", "dm", "knife" - did you mean "dm"? - the default stays
[ConfigCore] configs/myserver/settings.yaml:2:3: unknown key "prefx" in "chat" - did you mean "prefix"?
В объектеВ файле
string, number, booleanзначение; число можно текстом, логическое — true/false, yes/no, on/off в любом регистре или число (0 — ложь)
юнион имён, "normal" | "dm"одно из них
string[], number[], boolean[], список имёнсписок — элемент другого вида пропускается
string[][] (текста, чисел, логических значений или имён)список списков: строки значений; в INI — блок строк
объект любой вложенностиобъект
список объектовсписок объектов — поле, которого нет у элемента, пустое, и об этом сказано
Map<string, number> (текста, чисел, логических значений или имён)объект
field?: Tможно не писать: undefined
  • configs.load() отдаёт копию значений по умолчанию со значениями из файла; второй вызов читает файл заново. configs.save(settings) пишет каждое поле в файл, из которого объект прочитан.
  • У объекта, который load() прочитал без типа, нет имени типа, чтобы написать его в параметре функции: чтобы передавать настройки дальше, объявите интерфейс и пишите configs.load<Settings>(...).
  • Сборка читает форму из исходника: значения по умолчанию — объектным литералом (в списке — элемент), или указан тип — configs.load<Settings>(...). Чего так не прочитать — пустой список без типа, список списков списков, — останавливает сборку с местом и тем, как исправить.

Какой файл читается

load("settings") и read("settings") читают первый из settings.ini, settings.yaml, settings.yml, settings.json и settings.jsonc, который есть. Если есть два, читается первый, а консоль сервера об этом говорит: оставьте один. Имя с расширением — load("settings.json", ...) — читает этот файл. Файла нет — читаются значения по умолчанию, а сохраняется он как settings.yaml.

Файлы неизвестной формы

Файл, ключи которого заранее неизвестны, — список карт со своими настройками, файл, который пишет другой плагин, — читается деревом значений:

const 
maps
=
configs
.
read
("maps");
for (const
map
of
maps
.
values
()) {
console
.
log
(`${
map
.
key
}: ${
map
.
getNumber
("rounds", 30)} rounds`);
}
maps
.
setNumber
("de_dust2.rounds", 20);
maps
.
save
();

Значение — это ConfigNode: объект, массив, текст, число, логическое значение или null (kind), с файлом, строкой и столбцом, где оно прочитано. Путь ведёт внутрь — chat.prefix, items[0].name, — а геттер принимает запасное значение на случай, если значения нет или оно другого вида.

API

ФункцияЧто делает
load(name, defaults)Читает файл конфига в объект той же формы, что defaults.
save(settings)Записывает объект, прочитанный load(), обратно в его файл.
read(name)Читает файл конфига деревом значений; верхнее значение файла.
resolve(name)Файл, который читают load(name) и read(name), например "settings.yaml".
parse(text, format)Читает текст — "yaml", "json" или "ini" — деревом.
ConfigNodeЧто делает
kind · key · file · format · line · columnЧто это за значение и где оно прочитано.
get(path) · has(path)Значение, к которому ведёт путь, или null; есть ли оно.
keys(path?) · values(path?)Ключи объекта; элементы массива или значения объекта.
getString(path?, fallback?) · getNumber · getBoolean · getStringsЗначение как текст, число, логическое значение, список текста.
set(path, text) · setNumber · setBoolean · setStringsЗадаёт значение, создавая объекты на пути; false, если путь идёт через текст или число.
remove(path) · save()Удаляет значение; записывает весь файл обратно в его формате.

Форматы файлов

Одни и те же настройки в каждом из них:

# settings.yaml
chat:
  prefix: "[Server]"
  rules:
    - Be nice
    - No cheats
round:
  time: 2.5
hud:
  enabled: true
// settings.json или settings.jsonc
{
  "chat": {
    "prefix": "[Server]",
    "rules": ["Be nice", "No cheats"],
  },
  "round": { "time": 2.5 },
  "hud": { "enabled": true },
}
; settings.ini
[chat]
prefix = [Server]
rules = "Be nice" "No cheats"

[round]
time = 2.5

[hud]
enabled = 1
  • YAML: отображения и списки — с отступами или в { } и [ ]; простой текст, текст в 'одинарных' и "двойных" кавычках со своими экранированиями; блоки | и > для текста в несколько строк; числа, true/false, null и ~, как их читает YAML 1.2, — yes это текст; комментарии #; --- перед документом и ... после.
  • Чего в YAML нет: якорей и ссылок (&, *), тегов (!), сложных ключей (?), директив (%YAML), нескольких документов в одном файле и простого значения, которое продолжается на следующих строках. Тогда файл читается пустым, а консоль называет строку и столбец: configs/settings.yaml:4:9: anchors and aliases (& and *) are not supported - write the value out.
  • JSON: как пишется JSON, плюс комментарии // и /* */ из JSONC и запятая после последнего поля или элемента — и в файлах .json тоже.
  • INI: каждая [секция] — объект верхнего уровня: [chat] с prefix = [Server] — это settings.chat.prefix; строка из нескольких значений — список; блок key = { ... } — объект или список строк. Правила, которые добавляет INI, — в INI-файлах.
  • Сохранение пишет файл в его формате: комментарии на отдельных строках и пустые строки остаются; комментарий после значения на той же строке и комментарии внутри { } и [ ] в YAML — нет. JSON сохраняет свои отступы.

INI-файлы

INI-конфиг читается в такой же объект: каждая [секция] — объект, блок ключей key = { ... } — объект в нём, строка из нескольких значений — список, а блок строк в кавычках — список списков:

; configs/myserver/settings.ini
[MAIN]
CHAT_PREFIX = [MYPLUGIN]
MAPS = de_dust2 de_inferno de_nuke

HUD = {
    HIDE_TIME = 255 50 50
}

CVARS = {
    "mp_timelimit" "30"
    "mp_freezetime" "3"
}
const 
settings
=
configs
.
load
("settings", {
MAIN
: {
CHAT_PREFIX
: "[Server]",
MAPS
: ["de_dust2"],
HUD
: {
HIDE_TIME
: [255, 255, 255] },
CVARS
: [["mp_timelimit", "20"]],
}, }); for (const
row
of
settings
.
MAIN
.
CVARS
) {
const
cvar
= new
Cvar
(
row
[0]);
cvar
.
value
=
row
[1];
}
  • Блок строк остаётся блоком, когда объект сохраняется, — и строки из одного значения тоже; комментарии над его строками остаются.
  • Путь в дереве такого файла (configs.read()) пишется так же: MAIN.HUD.HIDE_TIME[0], MAIN.CVARS[1][0]. Элемент списка — это [0]: MAIN.MAPS.0 ищет ключ 0.
В INI-файле:
  • Ключи ищутся без учёта регистра, как их ищут Pawn-нативы, имена секций — нет: [main] — не MAIN. Из двух секций с одним именем берётся последняя.
  • Значение с пробелами берётся в кавычки: TITLE = "Main menu". Без кавычек это список слов: текстовое поле остаётся со значением по умолчанию, а консоль говорит почему, и нативы cfg_* Pawn и Menu Core читают первое слово.
  • Нет места значению на верхнем уровне объекта, которое не объект, и списку объектов: в INI-файле такое поле остаётся по умолчанию, и консоль один раз об этом говорит — такой конфиг пишите в YAML или JSON.

Pawn-плагины

Pawn-плагин тоже читает и пишет конфиги через Config Core — 28 нативами, имена которых начинаются с cfg_: cfg_load_file, cfg_get_value, cfg_set_int и остальные. Они читают INI-файлы. Их include лежит в пакете в include/:

#include <universal_config>

Pawn-плагин, уже собранный с этим include, работает как есть — пересобирать ничего не нужно.

Config Core работает на сервере как один из плагинов проекта. Если им не пользуется ни один плагин проекта на TypeScript, оставьте его в сборке для Pawn-плагинов: pawn: ["@amxts/config-core"] в amxts.config.ts. Все нативы с сигнатурами — в PAWN.ru.md.