Script settings in MoonLoader: inicfg and CP1251

Saving settings to INI with inicfg and handling Russian text: where CP1251 applies, where UTF-8 does, and how to convert between them.

Author: Published: Updated: 3 min read

Almost every script needs to remember something between launches: the player's rank, the delay between lines, which features are on. MoonLoader ships the inicfg library for that; it reads and writes INI files. The other recurring topic in SA:MP scripts is encoding: chat works in CP1251, ImGui in UTF-8, and Russian text breaks where they meet. Both show up in the same script, so this article covers them together.

Police Tools kept its settings in INI, and Police Tools Lite combines a mimgui window with CP1251 encoding, which is exactly the case the second half of this article deals with.

inicfg: loading and defaults

inicfg is part of MoonLoader's standard library set and is loaded with require. Settings are described as a table of sections, each holding key-value pairs:

local inicfg = require("inicfg")
local CONFIG_FILE = "my_script.ini"
local cfg = inicfg.load({
  main = {
    rank = "Sergeant",
    delay = 1000,
    greet = true,
  },
}, CONFIG_FILE)

The first argument of inicfg.load is the defaults, the second is the file name. If the file doesn't exist yet, the function returns the default table. If the file exists but lacks some keys, the missing ones are filled from the same table. That helps with updates: a new setting reaches every user at once, and nobody has to delete their old config.

Numbers and true/false come back from the file as numbers and booleans, so cfg.main.delay can go straight into wait.

Where the file lives and how to save

If the name has no full path, inicfg reads and writes the file in moonloader/config/. The file from the example ends up at moonloader/config/my_script.ini. The folder layout after installation is described in the article on installing MoonLoader.

inicfg.save(cfg, CONFIG_FILE) writes changes. It's handy to call it right after loading: on the first launch a file with defaults appears on disk, and the user can edit it in a text editor. After that, save when a setting changes, for example in a command handler. Saving every frame in the main loop just hammers the disk.

Name the file after the script. Every installed script puts its settings in config/, and a generic name like settings.ini will sooner or later collide with someone else's.

Why CP1251

The SA:MP client predates widespread UTF-8 and handles text in a single-byte Windows code page. Russian servers use CP1251: the server sends chat and dialogs in it and expects player text in it too. A UTF-8 string sent through sampSendChat or sampAddChatMessage turns into random characters.

Hence the first rule: a Lua script file with Russian text is saved in CP1251 (in Notepad++ that's "Cyrillic (Windows-1251)"). Then every string literal in the code is already in the right encoding, and chat and the INI file get it without conversion.

encoding and u8 for ImGui

The problem comes back once the script gets a window in ImGui. Dear ImGui only works with UTF-8, and a CP1251 string turns into question marks in the window. The encoding library converts it:

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

encoding.default sets the encoding the script's strings are in. After that, u8("text") converts a string from CP1251 to UTF-8 for ImGui, and u8:decode(text) does the reverse, turning UTF-8 from an ImGui input field into CP1251 for chat or INI. Inside a window it looks like this: imgui.Text(u8("Rank: " .. cfg.main.rank)).

A rule that keeps things simple: inside the script and in the config everything stays in CP1251, and text is converted to UTF-8 only on the way into ImGui and back on the way out.

A complete example

The script stores the player's rank and a delay, changes and saves the rank with /rank, and greets the player in chat on joining. /rankinfo opens a small mimgui window showing the same rank, already passed through 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 = "Sergeant",
    delay = 1000,
    greet = true,
  },
}, CONFIG_FILE)
inicfg.save(cfg, CONFIG_FILE)
local function set_rank(args)
  if args == "" then
    sampAddChatMessage("Usage: /rank [rank]", 0xAAAAAA)
    return
  end
  cfg.main.rank = args
  if inicfg.save(cfg, CONFIG_FILE) then
    sampAddChatMessage("Rank saved: " .. args, 0x33AA33)
  else
    sampAddChatMessage("Could not save " .. CONFIG_FILE, 0xFF5555)
  end
end
imgui.OnFrame(function() return show[0] end, function()
  imgui.Begin("config-demo", show)
  imgui.Text(u8("Rank: " .. 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("Your rank: " .. cfg.main.rank, 0xF5A524)
  end
  wait(-1)
end

Text typed after /rank arrives in CP1251 and is saved to INI unchanged, so the file opens in a text editor in the same encoding. In the window the same value goes through u8. If you later add a rank input field to the window, its value has to go through u8:decode before being written to cfg.main.rank.

Frequently asked questions

What happens if I delete a script's INI file?

On the next launch inicfg.load returns the defaults from the table in the code, and inicfg.save creates the file again. Any user settings are lost.

Can inicfg store nested tables and lists?

No, INI is flat: sections and key-value pairs. For lists such as binds, a separate file in your own format or JSON works better. Police Tools Lite, for example, keeps binds in config/PToolsLite.list, apart from its settings.

Why does text in ImGui show up as question marks?

ImGui expects UTF-8, while strings in a script saved as CP1251 are in CP1251. Run them through u8 before passing them to ImGui. The other possible cause is a font without Cyrillic glyphs.