# Script settings in MoonLoader: inicfg and CP1251

> How to store Lua script settings for SA:MP: inicfg.load and inicfg.save, default values, the moonloader/config folder, CP1251 encoding and the encoding library for chat and ImGui.

- URL: https://pakulichev.com/en/wiki/moonloader-config
- Author: Pakulichev (https://pakulichev.com/)
- Language: en
- Updated: 2026-10-16
- Whole site for LLMs: https://pakulichev.com/llms.txt (index), https://pakulichev.com/llms-full.txt (full text)

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](https://pakulichev.com/en/projects/police-tools) kept its settings in INI, and [Police Tools Lite](https://pakulichev.com/en/projects/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:

```lua
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](https://pakulichev.com/en/wiki/moonloader-install).

`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](https://pakulichev.com/en/wiki/imgui-for-gta-sa). Dear ImGui only works with UTF-8, and a CP1251 string turns into question marks in the window. The `encoding` library converts it:

```lua
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`:

```lua
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`.

## FAQ

### 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.
