Настройки скрипта в MoonLoader: inicfg и CP1251

Сохранение настроек в INI через inicfg и работа с русским текстом: где CP1251, где UTF-8 и как между ними переключаться.

Автор: Опубликовано: Обновлено: 3 мин чтения

Почти любому скрипту нужно что-то запоминать между запусками: звание игрока, задержку между строками, включённые функции. В MoonLoader для этого есть библиотека inicfg, которая читает и пишет INI-файлы. Вторая постоянная тема в скриптах для SA:MP – кодировка: чат работает в CP1251, ImGui в UTF-8, и русский текст ломается на стыке. Обе темы встречаются в одном скрипте, поэтому разберём их вместе.

Police Tools хранила настройки в INI, а Police Tools Lite сочетает окно на mimgui с кодировкой CP1251: ровно тот случай, который разбирает вторая половина статьи.

inicfg: загрузка и значения по умолчанию

inicfg входит в стандартный набор библиотек MoonLoader и подключается через require. Настройки описываются таблицей секций, внутри каждой – пары ключ-значение:

local inicfg = require("inicfg")
local CONFIG_FILE = "my_script.ini"
local cfg = inicfg.load({
  main = {
    rank = "Сержант",
    delay = 1000,
    greet = true,
  },
}, CONFIG_FILE)

Первый аргумент inicfg.load – значения по умолчанию, второй – имя файла. Если файла ещё нет, функция вернёт таблицу по умолчанию. Если файл есть, но в нём не хватает каких-то ключей, недостающие возьмутся из той же таблицы. Это удобно при обновлении скрипта: новая настройка появляется у всех пользователей сразу, старый конфиг удалять не нужно.

Числа и true/false при загрузке превращаются обратно в числа и логические значения, поэтому cfg.main.delay можно сразу передать в wait.

Где лежит файл и как сохранять

Если в имени нет полного пути, inicfg ищет и сохраняет файл в moonloader/config/. Файл из примера окажется в moonloader/config/my_script.ini. Структура папок после установки описана в статье про установку MoonLoader.

Сохраняет изменения inicfg.save(cfg, CONFIG_FILE). Удобно вызвать его сразу после загрузки: при первом запуске на диске появится файл со значениями по умолчанию, и пользователь сможет поправить его в блокноте. Дальше сохраняйте в тот момент, когда настройка изменилась, например в обработчике команды. Сохранение на каждом кадре в главном цикле бессмысленно нагружает диск.

Имя файла лучше давать по имени скрипта. В config/ складывают настройки все установленные скрипты, и общее имя вроде settings.ini рано или поздно совпадёт с чужим.

Почему CP1251

Клиент SA:MP появился задолго до повсеместного UTF-8 и работает с текстом в однобайтовой кодировке Windows. Русские серверы используют CP1251: в ней сервер отправляет чат и диалоги, в ней же ждёт текст от игрока. Строка в UTF-8, отправленная через sampSendChat или sampAddChatMessage, превращается в набор случайных символов.

Отсюда первое правило: Lua-файл скрипта с русским текстом сохраняется в CP1251 (в Notepad++ это «Кириллица (Windows-1251)»). Тогда все строковые литералы в коде уже в нужной кодировке, а чат и INI-файл получают их без преобразований.

encoding и u8 для ImGui

Проблема возвращается, когда в скрипте появляется окно на ImGui. Dear ImGui работает только с UTF-8, и строка в CP1251 в окне превратится в знаки вопроса. Перевести её помогает библиотека encoding:

local encoding = require("encoding")
encoding.default = "CP1251"
local u8 = encoding.UTF8

encoding.default задаёт кодировку, в которой лежат строки скрипта. После этого u8("текст") переводит строку из CP1251 в UTF-8 для ImGui, а u8:decode(text) делает обратное: превращает UTF-8 из поля ввода ImGui в CP1251 для чата или INI. Внутри окна это выглядит так: imgui.Text(u8("Звание: " .. cfg.main.rank)).

Удобное правило: внутри скрипта и в конфиге всё хранится в CP1251, в UTF-8 текст переводится только на входе в ImGui и обратно на выходе из него.

Пример целиком

Скрипт хранит звание игрока и задержку, по команде /rank меняет звание и сохраняет его в INI, при входе в игру выводит приветствие в чат. Команда /rankinfo открывает маленькое окно на mimgui с тем же званием, уже через u8:

script_name("config-demo")
script_author("Pakulichev")
local inicfg = require("inicfg")
local imgui = require("mimgui")
local encoding = require("encoding")
encoding.default = "CP1251"
local u8 = encoding.UTF8
local CONFIG_FILE = "config-demo.ini"
local show = imgui.new.bool(false)
local cfg = inicfg.load({
  main = {
    rank = "Сержант",
    delay = 1000,
    greet = true,
  },
}, CONFIG_FILE)
inicfg.save(cfg, CONFIG_FILE)
local function set_rank(args)
  if args == "" then
    sampAddChatMessage("Использование: /rank [звание]", 0xAAAAAA)
    return
  end
  cfg.main.rank = args
  if inicfg.save(cfg, CONFIG_FILE) then
    sampAddChatMessage("Звание сохранено: " .. args, 0x33AA33)
  else
    sampAddChatMessage("Не удалось сохранить " .. CONFIG_FILE, 0xFF5555)
  end
end
imgui.OnFrame(function() return show[0] end, function()
  imgui.Begin("config-demo", show)
  imgui.Text(u8("Звание: " .. cfg.main.rank))
  imgui.End()
end)
function main()
  while not isSampAvailable() do wait(100) end
  sampRegisterChatCommand("rank", set_rank)
  sampRegisterChatCommand("rankinfo", function()
    show[0] = not show[0]
  end)
  if cfg.main.greet then
    wait(cfg.main.delay)
    sampAddChatMessage("Ваше звание: " .. cfg.main.rank, 0xF5A524)
  end
  wait(-1)
end

Текст, введённый в чат после /rank, приходит в CP1251 и сохраняется в INI без преобразований, поэтому файл читается в блокноте в той же кодировке. В окно то же значение попадает через u8. Если позже добавить в окно поле ввода звания, значение из поля нужно будет пропустить через u8:decode перед записью в cfg.main.rank.

Частые вопросы

Что будет, если удалить INI-файл скрипта?

При следующем запуске inicfg.load вернёт значения по умолчанию из таблицы в коде, а inicfg.save создаст файл заново. Пользовательские настройки при этом пропадут.

Можно ли хранить в inicfg вложенные таблицы и списки?

Нет, INI плоский: секции и пары ключ-значение. Для списков вроде биндов удобнее отдельный файл в своём формате или JSON. Police Tools Lite, например, держит бинды в config/PToolsLite.list отдельно от настроек.

Почему текст в ImGui отображается знаками вопроса?

ImGui ждёт UTF-8, а строки в скрипте, сохранённом в CP1251, лежат в CP1251. Перед передачей в ImGui их нужно пропустить через u8. Вторая возможная причина – в шрифте нет кириллических глифов.