Example: MCM Options¶
A reference implementation showing every common MCM option type, correct defaults handling, and the on_option_change pattern. Use this as a starting template for any mod that exposes settings.
What this covers¶
- All standard MCM control types: checkbox, slider, dropdown, keybind, text input
- Organising options into collapsible sections
- Providing defaults that work when MCM is not installed
- Reacting to setting changes at runtime
Files¶
gamedata/
scripts/
my_full_mod.script
my_full_mod_mcm.script
configs/
text/
eng/
ui_st_my_full_mod.xml
my_full_mod_mcm.script¶
function on_mcm_load()
return {
id = "my_full_mod",
sh = true,
gr = {
-- ─── Header banner ────────────────────────────────────
{
id = "banner",
type = "slide",
text = "ui_mcm_my_full_mod",
link = "ui_options_slider_player",
size = {512, 50},
spacing = 20,
},
-- ─── General settings section ─────────────────────────
{
id = "general",
sh = true,
gr = {
{id = "title_general", type = "title", text = "ui_mcm_my_full_mod_general"},
-- Checkbox: simple boolean toggle
{
id = "enable",
type = "check",
val = 1,
def = true,
text = "ui_mcm_my_full_mod_enable",
},
-- Slider: numeric value with min/max/step
{
id = "intensity",
type = "track",
val = 2,
min = 0.0,
max = 2.0,
step = 0.1,
def = 1.0,
text = "ui_mcm_my_full_mod_intensity",
},
-- Dropdown list
{
id = "mode",
type = "list",
val = 2,
def = 0,
text = "ui_mcm_my_full_mod_mode",
content = {
{0, "ui_mcm_my_full_mod_mode_off"},
{1, "ui_mcm_my_full_mod_mode_low"},
{2, "ui_mcm_my_full_mod_mode_medium"},
{3, "ui_mcm_my_full_mod_mode_high"},
},
},
},
},
-- ─── Advanced settings section ────────────────────────
{
id = "advanced",
sh = true,
gr = {
{id = "title_advanced", type = "title", text = "ui_mcm_my_full_mod_advanced"},
-- Another checkbox
{
id = "debug_log",
type = "check",
val = 1,
def = false,
text = "ui_mcm_my_full_mod_debug",
},
-- Text input
{
id = "custom_label",
type = "input",
val = 0,
def = "Stalker",
text = "ui_mcm_my_full_mod_label",
},
{id = "divider", type = "line"},
-- Description block (non-interactive)
{
id = "help_text",
type = "desc",
text = "ui_mcm_my_full_mod_help",
clr = {255, 150, 150, 150},
},
},
},
-- ─── Keybinds section ─────────────────────────────────
{
id = "keybinds",
sh = true,
gr = {
{id = "title_keys", type = "title", text = "ui_mcm_my_full_mod_keybinds"},
{
id = "hotkey_main",
type = "key_bind",
val = 2,
def = DIK_keys.DIK_F7,
text = "ui_mcm_my_full_mod_hotkey_main",
},
{
id = "hotkey_toggle",
type = "key_bind",
val = 2,
def = DIK_keys.DIK_F8,
text = "ui_mcm_my_full_mod_hotkey_toggle",
},
},
},
}
}
end
my_full_mod.script¶
-- ─────────────────────────────────────────────────────────────
-- Defaults (used when MCM isn't installed)
-- ─────────────────────────────────────────────────────────────
local defaults = {
enable = true,
intensity = 1.0,
mode = 0,
debug_log = false,
custom_label = "Stalker",
hotkey_main = DIK_keys.DIK_F7,
hotkey_toggle = DIK_keys.DIK_F8,
}
-- ─────────────────────────────────────────────────────────────
-- Settings accessor
-- ─────────────────────────────────────────────────────────────
local function cfg(key)
if ui_mcm then
local v = ui_mcm.get("my_full_mod/" .. key)
return v ~= nil and v or defaults[key]
end
return defaults[key]
end
-- For nested paths (general/enable, keybinds/hotkey_main, etc.)
local function cfg_path(path)
if ui_mcm then
local v = ui_mcm.get("my_full_mod/" .. path)
-- Extract the key after the last /
local key = path:match("[^/]+$")
return v ~= nil and v or defaults[key]
end
local key = path:match("[^/]+$")
return defaults[key]
end
-- ─────────────────────────────────────────────────────────────
-- Runtime state updated from settings
-- ─────────────────────────────────────────────────────────────
local is_enabled = true
local active_mode = 0
local debug_mode = false
local function apply_settings()
is_enabled = cfg_path("general/enable")
active_mode = cfg_path("general/mode")
debug_mode = cfg_path("advanced/debug_log")
if debug_mode then
printf("[my_full_mod] settings applied: enable=%s mode=%s intensity=%.2f",
tostring(is_enabled),
tostring(active_mode),
cfg_path("general/intensity")
)
end
end
-- ─────────────────────────────────────────────────────────────
-- Key handlers
-- ─────────────────────────────────────────────────────────────
local function on_key_press(key)
if not is_enabled then return end
if not actor_menu.is_hud_free() then return end
if key == cfg_path("keybinds/hotkey_main") then
do_main_action()
elseif key == cfg_path("keybinds/hotkey_toggle") then
toggle_feature()
end
end
function do_main_action()
local intensity = cfg_path("general/intensity")
local label = cfg_path("advanced/custom_label")
local msg = string.format("Hello, %s! Intensity: %.1f", label, intensity)
actor_menu.set_msg(1, msg, 4)
end
function toggle_feature()
-- override local enable independently of MCM
is_enabled = not is_enabled
local key = is_enabled and "my_full_mod_on" or "my_full_mod_off"
actor_menu.set_msg(1, game.translate_string(key), 3)
end
-- ─────────────────────────────────────────────────────────────
-- Option change callback
-- ─────────────────────────────────────────────────────────────
local function on_option_change()
apply_settings()
end
-- ─────────────────────────────────────────────────────────────
-- Lifecycle
-- ─────────────────────────────────────────────────────────────
function on_game_start()
RegisterScriptCallback("on_key_press", on_key_press)
RegisterScriptCallback("on_option_change", on_option_change)
-- Apply settings immediately so defaults take effect without MCM
apply_settings()
end
function on_game_end()
UnregisterScriptCallback("on_key_press", on_key_press)
UnregisterScriptCallback("on_option_change", on_option_change)
end
ui_st_my_full_mod.xml¶
<?xml version="1.0" encoding="windows-1251"?>
<string_table>
<string id="ui_mcm_my_full_mod"><text>My Full Mod</text></string>
<!-- General section -->
<string id="ui_mcm_my_full_mod_general"><text>General</text></string>
<string id="ui_mcm_my_full_mod_enable"><text>Enable mod</text></string>
<string id="ui_mcm_my_full_mod_intensity"><text>Effect intensity</text></string>
<string id="ui_mcm_my_full_mod_mode"><text>Operating mode</text></string>
<string id="ui_mcm_my_full_mod_mode_off"><text>Off</text></string>
<string id="ui_mcm_my_full_mod_mode_low"><text>Low</text></string>
<string id="ui_mcm_my_full_mod_mode_medium"><text>Medium</text></string>
<string id="ui_mcm_my_full_mod_mode_high"><text>High</text></string>
<!-- Advanced section -->
<string id="ui_mcm_my_full_mod_advanced"><text>Advanced</text></string>
<string id="ui_mcm_my_full_mod_debug"><text>Enable debug logging</text></string>
<string id="ui_mcm_my_full_mod_label"><text>Display name</text></string>
<string id="ui_mcm_my_full_mod_help">
<text>Changes take effect immediately after saving MCM settings.</text>
</string>
<!-- Keybinds section -->
<string id="ui_mcm_my_full_mod_keybinds"><text>Keybinds</text></string>
<string id="ui_mcm_my_full_mod_hotkey_main"><text>Main action key</text></string>
<string id="ui_mcm_my_full_mod_hotkey_toggle"><text>Toggle key</text></string>
<!-- Runtime strings -->
<string id="my_full_mod_on"><text>Feature enabled.</text></string>
<string id="my_full_mod_off"><text>Feature disabled.</text></string>
</string_table>
Key decisions explained¶
Nested MCM paths. When you use nested gr sections with their own id, the MCM key path includes each level: "my_full_mod/general/enable". The cfg_path helper in the script above handles this, but you can also use cfg with top-level keys if you keep all options flat.
apply_settings() on startup. Calling apply_settings() inside on_game_start means the mod reads its settings immediately — before the player ever opens the MCM menu. Without this, settings would only take effect after the first on_option_change fires.
defaults table. Every setting has a matching entry in defaults. The cfg / cfg_path function falls back to defaults[key] when MCM is nil or when the MCM value is nil (which happens for new keys on first load after an update).
Boolean nil check in cfg. The pattern v ~= nil and v or defaults[key] handles the case where v is false — a plain v or default would return the default for false, which is wrong for checkboxes.