--[[ RavenAcsendant 25MAY2021 Anommaly Mod Configuration Menu (Anomaly MCM) +Modified version of the Anomaly Options menu created by Tronex. It retains all the same features. +Dedicated space for mod options (As of 1_4 this includes easy save game specific options storage) +Dynamicly loads options from mod scripts at main menu load simplifying the process and alowing it to be done before a save is loaded without needing to edit a game script. There by reducing conflicts. +Change History: 1_1 added options table passed by referance 1_2 Fixed a crash that is actualy still part of ui_options 1_3 Added overide controls for option tree/branch text see the tutorial section "Parameters of option Tree" at the end of this sctipt 1_4 Integrated dph-hcl's script for save game specific MCM options. See "Tutorial: Using : dph-hcl's script for save game specific MCM options" 1_5 Pass value to functors 1.6.0 Adopted https://semver.org/spec/v1.0.0.html versioning spec for reasons. Far left list sorted alphabeticaly, except MCM will always be at the top to serve as a landing page. Added logging utility see instruction in mcm_log.script Now pass true to on_option_change callback to make it posible to identify when it is coming from mcm MCM tracks a session ID that only increments when game exe is launched. ui_mcm.get_session_id() Added assert to identify missing val fields as that tends to result in hard to diagnose errors. Added assert to block calling get() while mcm is building the options tree. Added keybind widget includes a simple conflict list and all keybinds list under mcm's tree. Added utility functions to support key binds. Tracking of held status of shift controls and alt keys with single functon to read all three and a no modifier state Functions to help identify single, double and long presses of a key. Provided templated mcm option tables for selecting modifier keys and single/double and long presses that include translation strings. Updated documentation at end of this sctipt for all keybind related features. 1.6.1 Fixed xrdebug 1040 crash. 1.6.2 Added a message for when io.open fails to create a file. Made ui_mcm.script not fail if mcm_log.script is missing. Made debug logging default to false as orginaly intended. 1.6.3 Fixed interferance between doubletap and single press when bonund to same key. 1.6.4 Missed one place in above fix. 1.6.5 Added support for unbound MCM keybinds pressing ESC while binding a key will unbind the key, clicking will still cancel the bind process -1 is the value for unbound unbound keys will not be reported as conflicting added mouse4-8 as valid keybinds. updated mcm_log to 1.0.2 fixing a crash on quit. chnaged the doubletap window to be 200ms. down from 500. this will make mcm keybinds feel more responsive. made it adjustable by increments of 5 special handeling for key wrappers in the keybinds list fixed a bug in dph-hcl's script for save game specific MCM options (HarukaSai) Tronex 2019/10/12 Anomlous Options Menu Features: - %100 customizable, support for all kind of xml elements - Highlight pending changes - Option description support - Option presets support - Reset button to clear pending changes - Script callback on option changes - Functors capability to excute on apply or init - Precoditions capability to hide/show/execute functors To get an option value inside other scripts, use: ui_mcm.get(parameter) Check the options table here to see the values used See the tutorial at the bottom for adding or modifying options table --]] VERSION = "1.6.5-b1" MCM_DEBUG = axr_main.config:r_value("mcm", "mcm/mcm_log/debug_logging2", 1, false) local enable_debug_prints = false ------------------------------------------------------------ -- Strings and LTX management ------------------------------------------------------------ local ini_pres = ini_file("presets\\includes.ltx") local ini_loc = ini_file_ex("localization.ltx") local ini_mcm_key = ini_file_ex("mcm_key_localization.ltx") local _opt_ = "/" -- For axr_options.ltx, don't touch! local opt_section = "mcm" -- For axr_options.ltx, don't touch! local opt_str = "ui_mcm_" -- Option name: "ui_mcm_(option ID)" -- Option description: "ui_mcm_(option ID)_desc" local opt_str_menu = "ui_mcm_menu_" -- Option menu: "ui_mcm_menu_(option menu ID)" local opt_str_prst = "ui_mcm_prst_" -- Option preset: "ui_mcm_prst_(option preset ID)" local opt_str_lst = "ui_mcm_lst_" -- List/Radio keys: "ui_mcm_lst_(key)" function cc(path,opt) return (path .. _opt_ .. opt) end ------------------------------------------------------------ -- Utilities ------------------------------------------------------------ local session_id = nil local m_floor, m_ceil, s_find, s_gsub = math.floor, math.ceil, string.find, string.gsub local clr, clamp, round_idp, str_explode = utils_xml.get_color, clamp, round_idp, str_explode local precision = 6 -- allowed number of zeros local width_factor = utils_xml.is_widescreen() and 0.8 or 1 local clr_o = GetARGB(255, 250, 150, 75) local clr_g1 = GetARGB(255, 170, 170, 170) local clr_g2 = GetARGB(255, 200, 200, 200) local clr_w = GetARGB(255, 255, 255, 255) local clr_r = GetARGB(255, 225, 0, 0) local clr_tree = { [1] = GetARGB(255, 180, 180, 180), [2] = GetARGB(255, 180, 180, 180), [3] = GetARGB(255, 180, 180, 180), } local dbg = nil function print_dbg(...) if (not dbg) and MCM_DEBUG and mcm_log then dbg = mcm_log.new("DBG") dbg.enabled = MCM_DEBUG dbg.continuous = true end if dbg then dbg:log(...) end if enable_debug_prints then printf(...) end end ------------------------------------------------------------ -- Options ------------------------------------------------------------ options = {} local opt_temp = {} -- stores pending changes local opt_backup = {} -- do a backup of original changes for comparison with pendings local opt_index = {} -- option index by path, so we can locate an option fast without iterating throw the whole options table local opt_val = {} -- option value type by path, to execute proper functions on different type without iterating throw the whole options table local allowed_type = { -- ignore tables from these types in temp tables ["check"] = true, ["list"] = true, ["input"] = true, ["radio_h"] = true, ["radio_v"] = true, ["track"] = true, ["key_bind"] = true, } local key_by_path = {} local paths_by_key = {} local gathering = false if AddScriptCallback then AddScriptCallback("mcm_option_change") end function init_opt_base() print_dbg("MCM options reset.") get_session_id() options = { { id= "mcm" , gr={ {id = "mcm_about", sh=true , gr={ { id= "slide_mcm" ,type= "slide" ,link= "AMCM_Banner.dds" ,text= "ui_mcm_menu_mcm_about" ,size= {512,50} }, { id= "desc_mcm" ,type= "desc" ,text= "ui_mcm_desc_mcm" ,clr= {255,125,175,200} }, { id= "desc_mcm2" ,type= "desc" ,text= "ui_mcm_desc_mcm2" ,clr= {255,125,175,200} }, --{ id="reset" ,type= "check" ,val= 1 ,def= false , functor = {reload } }, },}, {id = "mcm_kb", gr={ {id = "mcm_kb_main", sh=true ,gr={ { id= "slide_kb" ,type= "slide" ,link= "AMCM_Banner.dds" ,text= "ui_mcm_menu_mcm_kb" ,size= {512,50} }, { id= "desc_kb" ,type= "desc" ,text= "ui_mcm_mcm_desc_kb" ,clr= {255,125,175,200} }, { id= "desc_kb2" ,type= "desc" ,text= "ui_mcm_mcm_desc_kb2" ,clr= {255,125,175,200} }, {id = "dtaptime2", type = "track", val = 2, def = 200, min = 100, max = 750, step = 5}, {id = "presstime", type = "track", val = 2, def = 1.5, min = 1.2, max = 2, step = 0.01, prec = 2}, },}, {id = "mcm_all_kb", sh=true , gr={ { id= "slide_kb" ,type= "slide" ,link= "AMCM_Banner.dds" ,text= "ui_mcm_menu_mcm_all_kb" ,size= {512,50} }, { id= "desc_kb" ,type= "desc" ,text= "ui_mcm_mcm_desc_kb_all" ,clr= {255,125,175,200} }, { id= "desc_kb2" ,type= "desc" ,text= "ui_mcm_mcm_desc_kb_all2" ,clr= {255,125,175,200} }, },}, {id = "mcm_kb_conflicts", sh=true , gr={ { id= "slide_kb" ,type= "slide" ,link= "AMCM_Banner.dds" ,text= "ui_mcm_menu_mcm_kb_conflicts" ,size= {512,50} }, { id= "desc_kb" ,type= "desc" ,text= "ui_mcm_mcm_desc_kb_conflicts" ,clr= {255,125,175,200} }, { id= "desc_kb2" ,type= "desc" ,text= "ui_mcm_mcm_desc_kb_all2" ,clr= {255,125,175,200} }, },}, },}, {id = "mcm_log", sh=true , gr={ { id= "slide_log" ,type= "slide" ,link= "AMCM_Banner.dds" ,text= "ui_mcm_menu_mcm_log" ,size= {512,50} }, { id= "desc_log" ,type= "desc" ,text= "ui_mcm_mcm_desc_log" ,clr= {255,125,175,200} }, {id = "enable", type = "check", val = 1, def = true}, {id = "numlogs", type = "track", val = 2, def = 2, min = 1, max = 10, step = 1}, {id = "continuous", type = "check", val = 1, def = false}, {id = "savefreq", type = "input", val = 2, def = 1000, min = 100, max = 1000000000000000}, {id = "timestamp", type = "input", val = 2, def = 1000, min = 1, max = 1000000000000000}, {id = "debug_logging", type = "line"}, {id = "debug_logging2", type = "check", val = 1, def = false}, },}, }, } } local kb_meta = {all = {}, conflict = {}} gathering = true gather_options() gathering = false table.sort(options, function(a,b) if a.id == "mcm" then return true end if b.id == "mcm" then return false end return game.translate_string(a.text or opt_str_menu .. a.id) < game.translate_string(b.text or opt_str_menu .. b.id) end) init_opt_coder(kb_meta) table.sort(kb_meta.conflict, function(a,b) return dispaly_key(get(a.kb_path)) < dispaly_key(get(b.kb_path)) end) for i = 1, #kb_meta.all do options[1].gr[2].gr[2].gr[3+i] = kb_meta.all[i] options[1].gr[2].gr[3].gr[3+i] = kb_meta.conflict[i] end init_opt_coder() MCM_DEBUG = get("mcm/mcm_log/debug_logging2") end function init_opt_coder(kb_meta) local keybind_count = 0 ------------------------------------------------------------------------ -- Coding options local function code_option(gr, id, num) local path for i=1,#gr do if allowed_type[gr[i].type] then path = cc(id , gr[i].id) opt_index[path] = cc(num , i) opt_val[path] = assert(gr[i].val, "val is manditory! option path:"..path) --printf("-[%s] | index: %s - type: %s", path, opt_index[path], opt_val[path]) if gr[i].type == "key_bind" and kb_meta then keybind_count = keybind_count + 1 update_conflict(path, get(path)) local temp = dup_table(gr[i]) temp.id = temp.id .. "_"..keybind_count temp.kb_path = path temp.curr = {get,path} temp.hint = temp.hint or id.."_"..gr[i].id temp.functor = {function(p,v) set(p,v) update_conflict(p,v) end, path } if path:find("mcm/key_wrapper/") then temp.precondition = {function() return false end} --temp.precondition = {get, path:sub(1, -).."enable"} end kb_meta.all[#kb_meta.all+1] = dup_table(temp) temp.precondition = {get_conflict, path} kb_meta.conflict[#kb_meta.conflict+1] = dup_table(temp) end end end end local id_1, id_2, id_3 -- Level 1 for i=1,#options do id_1 = options[i].id if options[i].sh then code_option(options[i].gr, id_1, i) else -- Level 2 for ii=1,#options[i].gr do id_2 = options[i].gr[ii].id if options[i].gr[ii].sh then code_option( (options[i].gr[ii].gr), (id_1 .._opt_.. id_2), (i .._opt_.. ii) ) else -- Level 3 for iii=1,#options[i].gr[ii].gr do id_3 = options[i].gr[ii].gr[iii].id if options[i].gr[ii].gr[iii].sh then code_option( (options[i].gr[ii].gr[iii].gr), (id_1 .._opt_.. id_2 .._opt_.. id_3), (i .._opt_.. ii .._opt_.. iii) ) else ---- end end end end end end end function gather_options() -- this is modified from how axr_main loads all the other scripts thanks to Igi for the idea. local ignore = { ["ui_mcm.script"] = true, } local t = {} local size_t = 0 local f = getFS() local flist = f:file_list_open_ex("$game_scripts$",bit_or(FS.FS_ListFiles,FS.FS_RootOnly),"*mcm.script") local f_cnt = flist:Size() for it=0, f_cnt-1 do local file = flist:GetAt(it) local file_name = file:NameShort() --printf("%s size=%s",file_name,file:Size()) if (file:Size() > 0 and ignore[file_name] ~= true) then file_name = file_name:sub(0,file_name:len()-7) if (_G[file_name] and _G[file_name].on_mcm_load) then size_t = size_t + 1 t[size_t] = file_name -- load all scripts first end end end for i=1,#t do --printf("%s.on_game_start()",t[i]) local temp = nil temp, name = _G[ t[i] ].on_mcm_load(options) -- passing options by referance added in AMCM 1.1 if you use this, nil check, emphasise updating MCM in your mod desc or something if temp and name then local collect = false for j=1, #options do if options[j].id == name then collect = j end end if not collect then collection = { id = name , gr = {}} table.insert(collection.gr, temp) table.insert(options, collection) else table.insert(options[collect].gr, temp) end elseif temp then table.insert(options, temp) end end end ------------------------------------------------------------ -- Functors ------------------------------------------------------------ -- Special local function empty_functor() print_dbg("Empty functor called!") end function reload() set("mcm/reset", false) init_opt_base() end -- Preconditions function level_present() return level.present() end function debug_only() return DEV_DEBUG end function for_renderer(...) local rend = {...} local curr_rend = get_console_cmd(0, "renderer") local result = false for i=1,#rend do result = result or curr_rend == rend[i] end return result end --Key bind stuff local function set_conflict(ck) local conflict = nil for k, v in pairs(paths_by_key[ck]) do if k ~= "conflict" then conflict = conflict or conflict == false or false --if it is true it stays true, if false it becomes true if nil it becomes false. 0 entries = nil, 1 entry = false, more than 1 entry = true end end paths_by_key[ck].conflict = conflict print_dbg("set conflict ck:%s con:%s",ck,conflict) end function update_conflict(path, key) key = tonumber(key) local old_key = key_by_path[path] print_dbg("path:%s, key:%s, old:%s", path, key, old_key) if key == old_key then return end key_by_path[path] = key if old_key then paths_by_key[old_key][path] = nil if paths_by_key[old_key].conflict then -- if this key used to have a conflict see if it can be clered set_conflict(old_key) end end if not paths_by_key[key] then paths_by_key[key] = {} end paths_by_key[key][path] = true if not paths_by_key[key].conflict then --if this key didn't have a conflict see if it does now set_conflict(key) end end function get_conflict(path, key) local k = key or key_by_path[path] print_dbg("path:%s, key:%s, k:%s", path, type(key), type(k)) assert(k, "No key found for path") k = tonumber(k) if k == -1 then return false end if dik_to_bind(k) ~= dik_to_bind(1000) then --conflicts with engine keybind print_dbg("bind:%s", dik_to_bind(k)) return true end if key_by_path[path] == k then print_dbg("==") return paths_by_key[k] and paths_by_key[k].conflict end print_dbg("=/= kbp:%s %s, k:%s %s",type(key_by_path[path]), key_by_path[path], k, type(k)) return paths_by_key[k] and (paths_by_key[k].conflict or paths_by_key[k].conflict == false) --limited support for pending changes conflicts. end function dispaly_key(key) local loc = ini_loc:r_value("string_table","language") loc = ini_mcm_key:section_exist(loc) and loc or "eng" return ini_mcm_key:r_value(loc, key,0,"") end kb_mod_radio = "radio_h" kb_mod_list = "list" local tap = {} function double_tap(id, key, multi_tap) local tg = time_continual() tap[key] = tap[key] or {} if not tap[key][id] then --first press, set timer return false tap[key][id] = tg return false end local dtaptime = get("mcm/mcm_kb/mcm_kb_main/dtaptime2") if (tg - tap[key][id] <= dtaptime) then --if inside the dtap window tap[key][id] = multi_tap and tg -- if multi_tap set timer for next tap, else clear it. return true end tap[key][id] = tg -- if we get here we are past the window for a double tap so this is a first press return false end local hold = {} function key_hold(id,key, cycle) local dtaptime = get("mcm/mcm_kb/mcm_kb_main/dtaptime2") local hold_time = get("mcm/mcm_kb/mcm_kb_main/presstime") * dtaptime cycle = cycle or 10000 local tg = time_continual() hold[key] = hold[key] or {} if (not hold[key][id]) or (tg - hold[key][id] > hold_time*1.5) then hold[key][id] = tg elseif (tg - hold[key][id] > hold_time ) then hold[key][id] = tg + cycle * 1000 return true end return false end local function execute_if_not_held(id,key, functor,...) if not (hold[key] and hold[key]["mcm_single_"..id]) then exec(functor,...) end return true end function simple_press(id, key, functor,...) dtap = double_tap("mcm_single_"..id, key, true) local tg = time_continual() hold[key] = hold[key] or {} hold[key]["mcm_single_"..id] = tg local dtaptime = get("mcm/mcm_kb/mcm_kb_main/dtaptime2") if dtap then RemoveTimeEvent("mcm_single_press",id.."_mcm_single_"..key) else CreateTimeEvent("mcm_single_press",id.."_mcm_single_"..key,(dtaptime*1.1)/1000,execute_if_not_held,id, key, functor,...) end end MOD_NONE = true MOD_CTRL = false MOD_SHIFT = false MOD_ALT = false function get_mod_key(val) if val == 1 then return MOD_SHIFT elseif val == 2 then return MOD_CTRL elseif val == 3 then return MOD_ALT end return MOD_NONE end function on_key_release(key) hold[key] = nil if key == DIK_keys.DIK_RCONTROL or key == DIK_keys.DIK_LCONTROL then MOD_CTRL = false elseif key == DIK_keys.DIK_RSHIFT or key == DIK_keys.DIK_LSHIFT then MOD_SHIFT = false elseif key == DIK_keys.DIK_RMENU or key == DIK_keys.DIK_LMENU then MOD_ALT = false elseif key == DIK_keys.DIK_ESCAPE then MOD_CTRL = false MOD_SHIFT = false MOD_ALT = false end MOD_NONE = not(MOD_CTRL or MOD_SHIFT or MOD_ALT) end function on_key_press(key) if key == DIK_keys.DIK_RCONTROL or key == DIK_keys.DIK_LCONTROL then MOD_CTRL = true elseif key == DIK_keys.DIK_RSHIFT or key == DIK_keys.DIK_LSHIFT then MOD_SHIFT = true elseif key == DIK_keys.DIK_RMENU or key == DIK_keys.DIK_LMENU then MOD_ALT = true end MOD_NONE = not(MOD_CTRL or MOD_SHIFT or MOD_ALT) end -- Utilities function is_int(num) return (m_floor(num) == num) end function exec(func,...) if (not func) then return false end return func(...) end function str_opt_explode(id, by_num) local nums = by_num and opt_index[id] or id local t = nums and str_explode(nums, _opt_) or {} if by_num then for i=1,#t do t[i] = tonumber(t[i]) end end return t end function get_opt_table(id) local t = str_opt_explode(id, true) if #t == 0 then return {} end if #t == 1 then return options[t[1]] elseif #t == 2 then return options[t[1]].gr[t[2]] elseif #t == 3 then return options[t[1]].gr[t[2]].gr[t[3]] elseif #t == 4 then return options[t[1]].gr[t[2]].gr[t[3]].gr[t[4]] end end function check_opt_table(id) local t = str_opt_explode(id, true) return #t > 0 end function CTimeTo_mSec(ct) local Y, M, D, h, m, s, ms = 0, 0, 0, 0, 0, 0, 0 Y, M, D, h, m, s, ms = ct:get(Y, M, D, h, m, s, ms) return ((D*24*60*60 + h*60*60 + m*60 + s)*1000 + ms) end local function create_session_id() if not session_id then session_id = axr_main.config:r_value(opt_section, "session_id", 2, 0) if not level.present() then -- if there is a level present then this is an inprogress session, use ID in file. if not compare the stored session start to the curent time and time continual to identify new session local session_start = axr_main.config:r_value(opt_section, "session_start", 2, 0) local now = os.time() * 1000 if (now - session_start) > (time_continual() + 10000) then --ten seconds of slop session_id = session_id + 1 axr_main.config:w_value(opt_section, "session_id", session_id) axr_main.config:w_value(opt_section, "session_start", now) axr_main.config:save() printf("MCM Session ID:%s", session_id) end end end end function get_session_id() create_session_id() return session_id end function is_gathering() return gathering end -------------------- function get(id) assert(not gathering, "ui_mcm.get() cannot be called during script load or on_mcm_load()!") if (#options == 0) then init_opt_base() end local value = axr_main.config:r_value(opt_section, id, opt_val[id]) if (value ~= nil) then --print_dbg("/Got axr_option [%s] = %s", id, value) return value end -- Write in axr_main if it doesn't exist local v = get_opt_table(id) if v.cmd then if v.val == 0 then value = get_console_cmd(0, v.cmd) elseif v.val == 1 then value = get_console_cmd(1, v.cmd) elseif v.val == 2 then value = get_console_cmd(0, v.cmd) --get_console_cmd(2, v.cmd) value = tonumber(value) if v.min and v.max then value = clamp(value, v.min, v.max) end value = round_idp(value, v.prec or precision) end elseif (type(v.def) == "table") then value = exec(unpack(v.def)) axr_main.config:w_value(opt_section, id, value) axr_main.config:save() else value = v.def axr_main.config:w_value(opt_section, id, value) axr_main.config:save() end --print_dbg("/Got option [%s] = %s", id, value) if (value == nil) then printe("!Found nil option value [%s]", id) end return value end function set(id, value) axr_main.config:w_value(opt_section, id, value) axr_main.config:save() end -------------------- --=========================================================== --//////////////////////// OPTIONS ////////////////////////// --=========================================================== class "UIMCM" (CUIScriptWnd) function UIMCM:__init() super() self.last_tree = {} self.last_path = nil self.last_curr_tree = nil self._Cap = {} self._Check = {} self._List = {} self._Input = {} self._Track = {} self._Radio = {} self._Hint = {} -- Prepare the options table if (#options == 0) then init_opt_base() end printf("MCM init") self:InitControls() self:InitCallBacks() self:Reset() end function UIMCM:__finalize() end function UIMCM:InitControls() self:SetWndRect (Frect():set(0,0,1024,768)) self:Enable (true) self.xml = CScriptXmlInit() local xml = self.xml xml:ParseFile ("ui_mcm.xml") self.background = xml:InitStatic("background", self) self.dialog = xml:InitStatic("main", self) xml:InitStatic("main:frame", self.dialog) -- Buttons self.btn_accept = xml:Init3tButton("main:btn_accept", self.dialog) self:Register(self.btn_accept, "btn_accept") self.btn_reset = xml:Init3tButton("main:btn_reset", self.dialog) self:Register(self.btn_reset, "btn_reset") self.btn_default = xml:Init3tButton("main:btn_default", self.dialog) self:Register(self.btn_default, "btn_default") self.btn_cancel = xml:Init3tButton("main:btn_cancel", self.dialog) self:Register(self.btn_cancel, "btn_cancel") -- Pending text --xml:InitFrame("main:notify_frame", self.dialog) self.pending = xml:InitTextWnd("main:notify", self.dialog) -- Options lists self.tree = {} self.bl = {} -- Options showcase self.scroll_opt = xml:InitScrollView("main:scroll", self.dialog) -- Presets self.preset_cap = xml:InitStatic("main:cap_preset", self.dialog) self.preset = xml:InitComboBox("main:preset",self.dialog) self:Register(self.preset, "preset") self.preset:Show(false) self.preset_cap:Show(false) -- Message box self.message_box = CUIMessageBoxEx() self:Register (self.message_box, "mb") -- Hint Window self.hint_wnd = utils_ui.UIHint(self) --MCM Key Bind self.k_binder = nil self.k_timer = 0 self.key_input = nil end function UIMCM:InitCallBacks() self:AddCallback("btn_accept", ui_events.BUTTON_CLICKED, self.OnButton_Accept, self) self:AddCallback("btn_reset", ui_events.BUTTON_CLICKED, self.OnButton_Reset, self) self:AddCallback("btn_default", ui_events.BUTTON_CLICKED, self.OnButton_Default, self) self:AddCallback("btn_cancel", ui_events.BUTTON_CLICKED, self.OnButton_Cancel, self) self:AddCallback("preset", ui_events.LIST_ITEM_SELECT, self.Callback_Preset, self) self:AddCallback("mb", ui_events.MESSAGE_BOX_YES_CLICKED, self.On_Discard, self) --self:AddCallback("mb", ui_events.MESSAGE_BOX_NO_CLICKED, self.On_Discard,self) end function UIMCM:Update() CUIScriptWnd.Update(self) -- Show hint on hover for id,ctrl in pairs(self._Cap) do if ctrl:IsCursorOverWindow() then local str = opt_str .. (self._Hint[id] or id) .. "_desc" local str_t = game.translate_string(str) if (str ~= str_t) then self.hint_wnd:Update(str_t) end return end end self.hint_wnd:Update() -- Hack to simulate tracing method for TrackBar value changes. TODO: add callback support for CUITrackBar in engine, this is just silly for id,e in pairs(self._Track) do if e.ctrl:IsCursorOverWindow() then local v = self:GetOption(id) local value = round_idp(e.ctrl:GetFValue(), v.prec or precision) if (value ~= e.value) then e.value = value self:Callback_Track(e.txt, e.path, e.opt, v, value) return end end end end function UIMCM:Reset() -- Clear all trees for i=1,3 do if self.tree[i] then if type(self.tree[i]) == table then for j=1,#self.tree[i] do self.tree[i][j]:Clear() end else self.tree[i]:Clear() end end end self.k_binder = nil self.k_timer = 0 self.key_input = nil self:Register_Tree(1, "", options, 1) end function UIMCM:Reset_opt(curr_tree, path, flags) flags = flags or {} local xml = self.xml self.scroll_opt:Clear() -- If options tree has a precondition that must be met, don't show it if it returns false if curr_tree.precondition and (not exec(unpack(curr_tree.precondition))) then if curr_tree.output then local _txt = xml:InitTextWnd("elements:block", nil) _txt:SetText( game.translate_string(curr_tree.output) ) self.scroll_opt:AddWindow(_txt, true) _txt:SetAutoDelete(false) end else self.k_binder = nil self.k_timer = 0 self.key_input = nil -- Presets self:Register_Preset(curr_tree) if curr_tree.apply_to_all and curr_tree.id_gr then flags.apply_to_all = true flags.group = curr_tree.id_gr else flags.apply_to_all = nil end empty_table(self._Cap) empty_table(self._Check) empty_table(self._List) empty_table(self._Input) empty_table(self._Track) empty_table(self._Radio) empty_table(self._Hint) for i=1,#curr_tree.gr do -- Check preconditions local to_hide = curr_tree.gr[i].precondition and (not exec(unpack(curr_tree.gr[i].precondition))) for j=1,10 do -- support for 10 preconditions if (not curr_tree.gr[i]["precondition_" .. j]) then break elseif (not exec(unpack(curr_tree.gr[i]["precondition_" .. j]))) then to_hide = true break end end if (not to_hide) then local opt = curr_tree.gr[i].id local v = curr_tree.gr[i] local _st = xml:InitStatic("main:st", nil) local _h = 0 ----------- Support if (v.type == "line") then _h = self:Register_Line(xml, _st) elseif (v.type == "image") then _h = self:Register_Image(xml, _st, v) elseif (v.type == "slide") then _h = self:Register_Slide(xml, _st, v) elseif (v.type == "title") then _h = self:Register_Title(xml, _st, v) elseif (v.type == "desc") then _h = self:Register_Desc(xml, _st, v) ----------- Option elseif (v.type == "check") then _h = self:Register_Check(xml, _st, path, opt, v, flags) elseif (v.type == "button") then _h = self:Register_Button(xml, _st, path, opt, v, flags) elseif (v.type == "list") then _h = self:Register_List(xml, _st, path, opt, v, flags) elseif (v.type == "input") then _h = self:Register_Input(xml, _st, path, opt, v, flags) elseif (v.type == "track") then _h = self:Register_Track(xml, _st, path, opt, v, flags) elseif (v.type == "radio_h") then _h = self:Register_Radio(xml, _st, path, opt, v, true, flags) elseif (v.type == "radio_v") then _h = self:Register_Radio(xml, _st, path, opt, v, false, flags) elseif (v.type == "key_bind") then _h = self:Register_Key_Bind(xml, _st, path, opt, v, flags) end _st:SetWndSize(vector2():set(_st:GetWidth(), _h + 10)) self.scroll_opt:AddWindow(_st, true) _st:SetAutoDelete(true) end end if self.Save_AXR then self.Save_AXR = false axr_main.config:save() end end end function UIMCM:Reset_last_opt() Register_UI("UIMCM") if self.last_curr_tree and self.last_path then if self.last_path == "mcm/mcm_kb/mcm_conflicts" then self.last_curr_tree = options[1].gr[2].gr[3] end self:Reset_opt(self.last_curr_tree, self.last_path) self:UpdatePending() end end ------------------------------------------------------------ -- Elements ------------------------------------------------------------ function UIMCM:Register_Cap(xml, handler, id, hint) id = s_gsub(id, _opt_, "_") self._Cap[id] = xml:InitStatic("elements:cap",handler) self._Cap[id]:TextControl():SetText( game.translate_string(opt_str .. (hint or id)) ) self._Hint[id] = hint return self._Cap[id]:GetHeight() end function UIMCM:Register_Line(xml, handler) local line = xml:InitStatic("elements:line",handler) return (line:GetHeight() + 10) end function UIMCM:Register_Image(xml, handler, v) local pic = xml:InitStatic("elements:image",handler) if v.link then if (v.pos) then local pos = pic:GetWndPos() pic:SetWndPos(vector2():set( pos.x + v.pos[1] , pos.y + v.pos[2] )) end if (v.size) then pic:SetWndSize(vector2():set( v.size[1] , v.size[2] )) end pic:InitTexture(v.link) pic:SetStretchTexture(v.stretch and true or false) end return pic:GetHeight() end function UIMCM:Register_Slide(xml, handler, v) local frame = xml:InitStatic("elements:slide", handler) local _pos = frame:GetWndPos() frame:SetWndPos(vector2():set( _pos.x , _pos.y + (v.spacing or 20) )) local pic = xml:InitStatic("elements:slide:pic", frame) if v.link then pic:InitTexture(v.link) pic:SetStretchTexture(true) if (v.pos) then local pos = pic:GetWndPos() pic:SetWndPos(vector2():set( pos.x + v.pos[1] , pos.y + v.pos[2] )) end if (v.size) then pic:SetWndSize(vector2():set( v.size[1] * width_factor , v.size[2] )) end pic:InitTexture(v.link) end local txt = xml:InitTextWnd("elements:slide:txt", frame) if v.text then txt:SetText( game.translate_string(v.text) ) end xml:InitStatic("elements:slide:line_1", frame) xml:InitStatic("elements:slide:line_2", frame) return (pic:GetHeight() + 20) end function UIMCM:Register_Title(xml, handler, v) local title = xml:InitTextWnd("elements:title_" .. (v.align or "l"), handler) title:SetText( game.translate_string(v.text) ) title:AdjustHeightToText() title:SetWndSize(vector2():set(title:GetWidth(), title:GetHeight() + 20)) if v.clr and v.clr[4] then title:SetTextColor( GetARGB(v.clr[1], v.clr[2], v.clr[3], v.clr[4]) ) end return title:GetHeight() end function UIMCM:Register_Desc(xml, handler, v) local desc = xml:InitTextWnd("elements:desc", handler) desc:SetText( game.translate_string(v.text) ) desc:AdjustHeightToText() desc:SetWndSize(vector2():set(desc:GetWidth(), desc:GetHeight() + 20)) if v.clr and v.clr[4] then desc:SetTextColor( GetARGB(v.clr[1], v.clr[2], v.clr[3], v.clr[4]) ) end return desc:GetHeight() end function UIMCM:Register_Check(xml, handler, path, opt, v, flags) local id = cc(path , opt) -- Caption local h = self:Register_Cap(xml, handler, id, v.hint) -- Apply to all button if flags.apply_to_all and flags.group then self:Register_BtnAll(xml, handler, path, opt, v, flags) end -- Create control local ctrl = xml:InitCheck("elements:check",handler) if (ctrl:GetHeight() > h) then h = ctrl:GetHeight() end -- Get values local value = self:GetValue(path, opt, v, flags) ctrl:SetCheck(value) -- Register local id_ctrl = self:Stacker(path, opt, v) self:Register(ctrl, id_ctrl) local _wrapper = function(handler) -- we need wrapper in order to pass ctrl to method self:Callback_Check(ctrl, path, opt, v) end self:AddCallback(id_ctrl, ui_events.BUTTON_CLICKED, _wrapper, self) return h end function UIMCM:Callback_Check(ctrl, path, opt, v) local value = ctrl:GetCheck() self:CacheValue(path, opt, value, v) end function UIMCM:Register_Key_Bind(xml, handler, path, opt, v, flags) local id = cc(path , opt) self.k_binder = nil self.k_timer = 0 self.key_input = nil --[[ Apply to all button if flags.apply_to_all and flags.group then self:Register_BtnAll(xml, handler, path, opt, v, flags) end --]] xml:InitFrame("elements:frame_key_button", handler) local txt = xml:InitStatic("elements:txt_key_button", handler) -- Create control local ctrl = xml:Init3tButton("elements:btn_key_button", handler) local h = ctrl:GetHeight() -- Caption local h = self:Register_Cap(xml, handler, id, v.hint) -- Get values local value = self:GetValue(path, opt, v, flags) txt:TextControl():SetText(dispaly_key(value)) if get_conflict(v.kb_path or id, value) then txt:TextControl():SetTextColor(clr_r) else txt:TextControl():SetTextColor(clr_g1) end -- Register local id_ctrl = self:Stacker(path, opt, v) self:Register(ctrl, id_ctrl) local _wrapper = function(handler) -- we need wrapper in order to pass ctrl to method self:Callback_Key_Bind(ctrl, path, opt, v, txt) end self:AddCallback(id_ctrl, ui_events.BUTTON_CLICKED, _wrapper, self) return h end function UIMCM:Callback_Key_Bind(ctrl, path, opt, v, txt) self.key_input = txt:TextControl() self.key_input:SetText( "") txt:TextControl():SetTextColor(clr_w) if self.k_binder then self.k_binder() -- clear other bind inputs end self.k_timer = time_continual() self.k_binder = function(key) self:Key_Binder(ctrl, path, opt, v, key, txt) end end function UIMCM:Key_Binder(ctrl, path, opt, v, key, txt) self.key_input = nil self.k_binder = nil local value if key then self:CacheValue(path, opt, key, v) value = key else value = self:GetValue(path, opt, v, flags) end txt:TextControl():SetText(dispaly_key(value)) if get_conflict(v.kb_path or cc(path , opt), value) then txt:TextControl():SetTextColor(clr_r) else txt:TextControl():SetTextColor(clr_g1) end end function UIMCM:Register_Button(xml, handler, path, opt, v, flags) local id = cc(path , opt) --[[ Apply to all button if flags.apply_to_all and flags.group then self:Register_BtnAll(xml, handler, path, opt, v, flags) end --]] xml:InitFrame("elements:frame_button", handler) -- Create control local ctrl = xml:Init3tButton("elements:btn_button", handler) local h = ctrl:GetHeight() -- Caption local id_cap = s_gsub(id, _opt_, "_") self._Cap[id_cap] = xml:InitStatic("elements:cap_button",handler) self._Cap[id_cap]:TextControl():SetText( game.translate_string(opt_str .. (v.hint or id_cap)) ) if (self._Cap[id_cap]:GetHeight() > h) then h = self._Cap[id_cap]:GetHeight() end -- Register local id_ctrl = self:Stacker(path, opt, v) self:Register(ctrl, id_ctrl) local _wrapper = function(handler) -- we need wrapper in order to pass ctrl to method self:Callback_Button(ctrl, path, opt, v) end self:AddCallback(id_ctrl, ui_events.BUTTON_CLICKED, _wrapper, self) return h end function UIMCM:Callback_Button(ctrl, path, opt, v) if v.functor_ui then local id = cc(path , opt) print_dbg("- Executing functor_ui of [%s]",id) exec(unpack(v.functor_ui),self) end if v.functor then local id = cc(path , opt) print_dbg("- Executing functor of [%s]",id) exec(unpack(v.functor)) end end function UIMCM:Register_List(xml, handler, path, opt, v, flags) local id = cc(path , opt) -- Caption local h = self:Register_Cap(xml, handler, id, v.hint) -- Apply to all button if flags.apply_to_all and flags.group then self:Register_BtnAll(xml, handler, path, opt, v, flags) end -- Create control local ctrl = xml:InitComboBox("elements:list",handler) if (ctrl:GetHeight() > h) then --h = ctrl:GetHeight() end -- Get values local idx local value = self:GetValue(path, opt, v, flags) local content = self:GetContent(path, opt, v) -- Setup for i=1,#content do local str_2 = content[i][2] or tostring(content[i][1]) local str = v.no_str and str_2 or game.translate_string(opt_str_lst .. str_2) ctrl:AddItem( game.translate_string(str), i) if content[i][1] == value then idx = i end end idx = idx or 1 --printf(path.." ".. idx) local str_2 = content[idx][2] or tostring(content[idx][1]) local str = v.no_str and str_2 or game.translate_string(opt_str_lst .. str_2) ctrl:enable_id( idx ) ctrl:SetText( game.translate_string(str) ) -- Register local id_ctrl = self:Stacker(path, opt, v) self:Register(ctrl, id_ctrl) local _wrapper = function(handler) -- we need wrapper in order to pass ctrl to method self:Callback_List(ctrl, path, opt, v) end self:AddCallback(id_ctrl, ui_events.LIST_ITEM_SELECT, _wrapper, self) return h end function UIMCM:Callback_List(ctrl, path, opt, v) local i = ctrl:CurrentID() local content = self:GetContent(path, opt, v) self:CacheValue(path, opt, content[i][1], v) end function UIMCM:Register_Input(xml, handler, path, opt, v, flags) local id = cc(path , opt) -- Caption local h = self:Register_Cap(xml, handler, id, v.hint) -- Apply to all button if flags.apply_to_all and flags.group then self:Register_BtnAll(xml, handler, path, opt, v, flags) end -- Create control local ctrl = xml:InitEditBox("elements:input",handler) if (ctrl:GetHeight() > h) then h = ctrl:GetHeight() end -- Get values local value = self:GetValue(path, opt, v, flags) ctrl:SetText(value) -- Register local id_ctrl = self:Stacker(path, opt, v) self:Register(ctrl, id_ctrl) local _wrapper = function(handler) -- we need wrapper in order to pass ctrl to method self:Callback_Input(ctrl, path, opt, v) end self:AddCallback(id_ctrl, ui_events.EDIT_TEXT_COMMIT, _wrapper, self) return h end function UIMCM:Callback_Input(ctrl, path, opt, v) local value = ctrl:GetText() if not (value and value ~= "") then ctrl:SetText( self:GetCurrentValue(path, opt, v) or self:GetDefaultValue(path, opt, v) ) return end if (v.val == 2) then value = tonumber(value) if (not value) then ctrl:SetText( self:GetCurrentValue(path, opt, v) or self:GetDefaultValue(path, opt, v) ) return end value = clamp(value, v.min, v.max) end self:CacheValue(path, opt, value, v) ctrl:SetText(value) end function UIMCM:Register_Track(xml, handler, path, opt, v, flags) local id = cc(path , opt) -- Caption local h = self:Register_Cap(xml, handler, id, v.hint) -- Apply to all button if flags.apply_to_all and flags.group then self:Register_BtnAll(xml, handler, path, opt, v, flags) end -- Create control self._Track[id] = {} self._Track[id].ctrl = xml:InitTrackBar("elements:track",handler) self._Track[id].path = path self._Track[id].opt = opt if (self._Track[id].ctrl:GetHeight() > h) then h = self._Track[id].ctrl:GetHeight() end self._Track[id].txt = xml:InitTextWnd("elements:track_value",handler) -- Get values local value = self:GetValue(path, opt, v, flags) value = clamp(value, v.min, v.max) value = round_idp(value, v.prec or precision) local int = false --is_int(value) and is_int(v.step) and is_int(v.min) and is_int(v.max) self._Track[id].value = value -- temp self._Track[id].ctrl:SetInvert(v.invert and true or false) self._Track[id].ctrl:SetStep(v.step) if int then self._Track[id].ctrl:SetOptIBounds(v.min, v.max) self._Track[id].ctrl:SetIValue(value) else self._Track[id].ctrl:SetOptFBounds(v.min, v.max) self._Track[id].ctrl:SetFValue(value) end if (not v.no_str) then self._Track[id].txt:SetText(value) end return h end function UIMCM:Callback_Track(ctrl, path, opt, v, value) if (not v.no_str) then ctrl:SetText(value) end self:CacheValue(path, opt, value, v) end function UIMCM:Register_Radio(xml, handler, path, opt, v, typ, flags) local id = cc(path , opt) -- Caption local h = self:Register_Cap(xml, handler, id, v.hint) -- Apply to all button if flags.apply_to_all and flags.group then self:Register_BtnAll(xml, handler, path, opt, v, flags) end -- Determine type local str = typ and "horz" or "vert" local content = self:GetContent(path, opt, v) local num = #content if num > 8 and (not v.force_horz) then typ = false str = "vert" end -- Create control local frame = xml:InitStatic("elements:radio_" .. str, handler) local ctrl = {} local txt local offset = typ and m_floor(frame:GetWidth()/num) or 30 local h_factor = typ and 1 or 0 local v_factor = typ and 0 or 1 local h1, h2 = 0, 0 --printf("offset: %s - h_factor: %s - v_factor: %s - num: %s", offset, h_factor, v_factor, num) for i=1,num do -- Buttons ctrl[i] = xml:InitCheck("elements:radio_" .. str .. ":btn", frame) local pos = ctrl[i]:GetWndPos() h1 = (h1 * v_factor) + ctrl[i]:GetHeight() ctrl[i]:SetWndPos(vector2():set( pos.x + ((i-1) * offset * h_factor) , pos.y + ((i-1) * offset * v_factor) )) -- Text txt = xml:InitTextWnd("elements:radio_" .. str .. ":txt", frame) local pos2 = txt:GetWndPos() h2 = h_factor * txt:GetHeight() txt:SetWndPos(vector2():set( pos2.x + ((i-1) * offset * h_factor) , pos2.y - (v_factor * 30) + ((i-1) * offset * v_factor) )) local str_2 = content[i][2] or tostring(content[i][1]) local str = v.no_str and game.translate_string(str_2) or game.translate_string(opt_str_lst .. str_2) txt:SetText( str ) if (h1 + h2 > h) then h = h1 + h2 end end -- Get values local value = self:GetValue(path, opt, v, flags) local id_ctrl = self:Stacker(path, opt, v) for i=1,num do if (content[i][1] == value) then ctrl[i]:SetCheck(true) else ctrl[i]:SetCheck(false) end -- Register self:Register(ctrl[i], id_ctrl .. i) local _wrapper = function(handler) -- we need wrapper in order to pass ctrl to method self:Callback_Radio(ctrl, path, opt, v, i) end self:AddCallback(id_ctrl .. i, ui_events.BUTTON_CLICKED, _wrapper, self) end return h end function UIMCM:Callback_Radio(ctrl, path, opt, v, n) local value = ctrl[n]:GetCheck() --printf("n = %s", n) if value then for i=1,#ctrl do if i ~= n then ctrl[i]:SetCheck(false) end end local content = self:GetContent(path, opt, v) self:CacheValue(path, opt, content[n][1], v) else ctrl[n]:SetCheck(true) end end function UIMCM:Register_BtnAll(xml, handler, path, opt, v, flags) local ctrl = xml:Init3tButton("elements:btn_all",handler) xml:InitStatic("elements:cap_all",handler) local id_ctrl = self:Stacker(path, opt, v) self:Register(ctrl, id_ctrl) local _wrapper = function(handler) -- we need wrapper in order to pass ctrl to method self:Callback_BtnAll(ctrl, path, opt, v, flags) end self:AddCallback(id_ctrl, ui_events.BUTTON_CLICKED, _wrapper, self) end function UIMCM:Callback_BtnAll(ctrl, path, opt, v, flags) local id = cc(path , opt) local group = flags.group local value = self:GetValue(path, opt, v, flags) -- Set same value for identical options of same group local function set_in_group(p, group, path, opt, value) print_dbg("~set_in_group | current path: %s - target opt: %s", path, opt) for i=1,#p do local path_ext = path and (path ~= "") and cc(path , p[i].id) or p[i].id if p[i].sh then print_dbg("~set_in_group | current path: %s - target opt: %s", path_ext, opt) if (p[i].id_gr == group) then local gr = p[i].gr for j=1,#gr do if gr[j].id == opt then local id_ext = cc(path_ext , opt) if check_opt_table(id_ext) then print_dbg("-set_in_group | Found match: %s", id_ext) self:CacheValue(path_ext, opt, value, gr[j]) end end end end else set_in_group(p[i].gr, group, path_ext, opt, value) end end end set_in_group(options, group, "", opt, value) end function UIMCM:Register_Preset(ct) if ct.presets then self.preset:ClearList() -- for i=1,#ct.presets do self.preset:AddItem( game.translate_string(opt_str_prst .. ct.presets[i]), i) end if (ct.curr_preset) then self.preset:SetText( game.translate_string(opt_str_prst .. ct.curr_preset) ) end self.preset:Show(true) self.preset_cap:Show(true) else self.preset:ClearList() self.preset:Show(false) self.preset_cap:Show(false) end end function UIMCM:Callback_Preset() if not (self.last_curr_tree and self.last_path) then return end local txt = self.preset:GetText() if not (txt and txt ~= "") then return end -- Retrieve the preset section local pres local presets = self.last_curr_tree.presets for i=1,#presets do if game.translate_string(opt_str_prst .. presets[i]) == txt then pres = presets[i] break end end if pres and ini_pres:section_exist(pres) then self.last_curr_tree.curr_preset = pres --self:Reset_opt(self.last_curr_tree, self.last_path, { preset = pres }) local n = ini_pres:line_count(pres) local result, id, value for i=0,n-1 do result, id, value = ini_pres:r_line_ex(pres,i,"","") -- Validate option local v = get_opt_table(id) if v and v.type then -- No need to modify options that can't be seen local to_hide = v.precondition and (not exec(unpack(v.precondition))) if (not to_hide) then -- Get proper value if v.val == 0 then elseif v.val == 1 then value = (value == "true") and true or false elseif v.val == 2 then value = tonumber(value) end -- Extract path and opt local t = str_opt_explode(id) local opt = t[#t] local path = t[1] for i=2,#t-1 do path = cc(path , t[i]) end -- Cache changes self:CacheValue(path, opt, value, v) end end end -- Update XML elements self:Reset_opt(self.last_curr_tree, self.last_path) -- Update state self:UpdatePending() end end function UIMCM:Register_Tree(tr, path, group, idx) print_dbg("-Register_Tree | tr: %s - path: %s", tr, path) local xml = self.xml self.key_input = nil self.k_binder = nil if (not self.tree[tr]) then self.tree[tr] = {} end if (not self.tree[tr][path]) then self.tree[tr][path] = xml:InitScrollView("main:tree_" .. tr, self.dialog) --[[ local pos = self.tree[tr][path]:GetWndPos() if tr == 3 then idx = 1 end self.tree[tr][path]:SetWndPos(vector2():set( pos.x , pos.y + (25*(idx-1)) )) --]] if (not self.bl[tr]) then self.bl[tr] = {} end self.bl[tr][path] = {} -- Fill tree for i=1,#group do local _st = xml:InitStatic("main:st_tree", nil) self.bl[tr][path][i] = xml:InitCheck("elements:btn_list", _st) self.bl[tr][path][i]:SetCheck(false) local txt = xml:InitTextWnd("elements:txt_list", _st) txt:SetText( game.translate_string(group[i].text or opt_str_menu .. group[i].id) ) txt:SetTextColor( clr_tree[tr] ) self.tree[tr][path]:AddWindow(_st, true) _st:SetAutoDelete(false) end -- Set Callback for tree buttons for i=1,#self.bl[tr][path] do local path_i = (path ~= "") and cc(path , group[i].id) or group[i].id self:Register(self.bl[tr][path][i], ("tree_"..path_i)) local _wrapper = function(handler) -- we need wrapper in order to pass ctrl to method self:Callback_Tree(tr, path_i, group[i], self.bl[tr][path], i) end self:AddCallback(("tree_"..path_i), ui_events.BUTTON_CLICKED, _wrapper, self) end end self.tree[tr][path]:Show(true) self.bl[tr][path][1]:SetCheck(true) local path_1 = (path ~= "") and cc(path , group[1].id) or group[1].id self:Callback_Tree(tr, path_1, group[1], self.bl[tr][path], 1) end function UIMCM:Callback_Tree(tr, path, group, ctrl, i) print_dbg("-Callback_Tree | tr: %s - path: %s - index: %s", tr, path, i) self.key_input = nil self.k_binder = nil -- Radio buttons behavior if (ctrl[i]:GetCheck() == false) then ctrl[i]:SetCheck(true) return end for k=1,#ctrl do if k ~= i then ctrl[k]:SetCheck(false) end end -- Hide all sub trees for k=tr+1,#self.tree do for _,v in pairs(self.tree[k]) do v:Show(false) end end -- If its an option list, show it if group.sh then self:Reset_opt(group, path) -- Caching current options self.last_path = path if (not self.last_curr_tree) then self.last_curr_tree = {} end empty_table(self.last_curr_tree) copy_table(self.last_curr_tree, group) else self:Register_Tree(tr+1, path, group.gr, i) end end ------------------------------------------------------------ -- Utilities ------------------------------------------------------------ function UIMCM:GetValue(path, opt, v, flags) -- NOTE: make sure to check for nil values only, since false exists as legit value for commands and check boxes local value if flags and flags.def then value = self:GetDefaultValue(path, opt, v) elseif flags and flags.preset then local pres = flags.preset local id = cc(path , opt) if v.val == 0 then value = ini_pres:r_string_ex(pres, id) elseif v.val == 1 then value = ini_pres:r_bool_ex(pres, id) elseif v.val == 2 then value = ini_pres:r_float_ex(pres, id) end end if (value ~= nil) or (flags and flags.def) then if (value ~= nil) and (v.type == "track") then value = clamp(value, v.min, v.max) end self:CacheValue(path, opt, value, v) end if (value == nil) then value = self:GetCurrentValue(path, opt, v) end if (value == nil) then value = self:GetDefaultValue(path, opt, v) end if (value ~= nil) and (v.type == "track") then value = clamp(value, v.min, v.max) end return value end function UIMCM:GetDefaultValue(path, opt, v) local id = cc(path , opt) local value if (type(v.def) == "table") then value = exec(unpack(v.def)) else value = v.def end -- We cache default values for the first time, so current values rely on them up later -- because some default values are randomized and player might not touch them, thus causing randomized effects where they are used in-game if (axr_main.config:r_value(opt_section, id, v.val) == nil) and (value ~= nil) then axr_main.config:w_value(opt_section, id, value) self.Save_AXR = true end return value end function UIMCM:GetCurrentValue(path, opt, v) local id = cc(path , opt) if (opt_temp[id] ~= nil) then local _id = s_gsub(id, _opt_, "_") if self._Cap[_id] and self._Cap[_id]:IsShown() then self._Cap[_id]:TextControl():SetTextColor( clr_o ) end return opt_temp[id] end local value if v.curr then value = exec(unpack(v.curr)) elseif v.cmd then if v.val == 0 then value = get_console_cmd(0, v.cmd) elseif v.val == 1 then value = get_console_cmd(1, v.cmd) elseif v.val == 2 then value = get_console_cmd(0, v.cmd) --get_console_cmd(2, v.cmd) -- some commands are integers, using get_float will return 0. This is a walkaround value = tonumber(value) if v.min and v.max then value = clamp(value, v.min, v.max) end value = round_idp(value, v.prec or precision) end else value = axr_main.config:r_value(opt_section, id, v.val) end return value end function UIMCM:GetContent(path, opt, v) if v.cmd and (not v.content) then local value if v.val == 0 then value = get_console_cmd(0, v.cmd) elseif v.val == 1 then value = get_console_cmd(1, v.cmd) elseif v.val == 2 then value = get_console_cmd(0, v.cmd) --get_console_cmd(2, v.cmd) value = tonumber(value) if v.min and v.max then value = clamp(value, v.min, v.max) end value = round_idp(value, v.prec or precision) end return {{value,tostring(value)}} elseif (type(v.content[1]) == "function") then return exec(unpack(v.content)) else return v.content end end function UIMCM:GetOption(id) local t = str_explode(id,_opt_ ) local v = options for i=1,#t do for j=1,#v do if v[j].id == t[i] then if i == #t then v = v[j] else v = v[j].gr end break end end end return v end function UIMCM:CacheValue(path, opt, value, v) local id = cc(path , opt) -- Do a backup of current values first if (opt_backup[id] == nil) then opt_backup[id] = self:GetValue(path, opt, v) print_dbg("# Backup [%s] = %s", id, opt_backup[id]) end -- Cache changed values if (value ~= nil) and (value ~= opt_backup[id]) then opt_temp[id] = value print_dbg("/ Cached [%s] = %s", id, value) else opt_temp[id] = nil -- no need to cache current values print_dbg("~ Cleared cache [%s]", id) end -- Change text color local _id = s_gsub(id, _opt_, "_") if self._Cap[_id] and self._Cap[_id]:IsShown() then if (opt_temp[id] ~= nil) and (opt_temp[id] ~= opt_backup[id]) then self._Cap[_id]:TextControl():SetTextColor( clr_o ) else self._Cap[_id]:TextControl():SetTextColor( clr_g1 ) end end -- Update state self:UpdatePending() end function UIMCM:Stacker(path, opt, v) -- This assure that each time a control is created, an unique callback id is given to it -- Why? because in normal case, jumping between options removes the previous ones constantly, getting back to them will create new controls and assign them to the old ids -- This is bad because callbacks are still attached to the old controls, any fresh controls that get assigned to those ids will be inactive as a result -- My solution is this function to generate unique id each time a control is created if (not v.stack) then v.stack = 0 end v.stack = v.stack + 1 return cc( cc(path , opt) , v.stack) end function UIMCM:UpdatePending() local size = size_table(opt_temp) if size > 0 then self.pending:SetText( strformat( game.translate_string("ui_mm_warning_pending"), size) ) else self.pending:SetText("") end end ------------------------------------------------------------ -- Callbacks ------------------------------------------------------------ function UIMCM:OnButton_Accept() --if self.Need_VidRestart then -- self.message_box:InitMessageBox("message_box_yes_no") -- self.message_box:SetText(string.format("%s %d% s", game.translate_string("ui_mm_confirm_changes"), 15, game.translate_string("mp_si_sec"))) -- self.message_box:ShowDialog(true) --else self:On_Accept() --end end function UIMCM:OnButton_Reset() if self.last_path and self.last_curr_tree and is_not_empty(opt_temp) then local to_reset for id, val in pairs(opt_temp) do if s_find(id,self.last_path) then to_reset = true opt_temp[id] = nil local _id = s_gsub(id, _opt_, "_") if self._Cap[_id] and self._Cap[_id]:IsShown() then self._Cap[_id]:TextControl():SetTextColor( clr_g1 ) end end end if (to_reset) then self:UpdatePending() self:Reset_opt(self.last_curr_tree, self.last_path) end end end function UIMCM:OnButton_Default() if self.last_path and self.last_curr_tree then self:Reset_opt(self.last_curr_tree, self.last_path, { def = true }) end end function UIMCM:OnButton_Cancel() if is_not_empty(opt_temp) then self.message_box:InitMessageBox("message_box_yes_no") self.message_box:SetText(game.translate_string("ui_mm_discard_changes")) self.message_box:ShowDialog(true) else self:On_Cancel() end end function UIMCM:On_Accept() self.Changes ={} for id, val in pairs(opt_temp) do local v = self:GetOption(id) -- Cache the changes if (not v.curr) then print_dbg("- Saved [%s] := %s", id, val) axr_main.config:w_value(opt_section, id, val) if key_by_path[id] then update_conflict(id, val) end self.Save_AXR = true end self.Change_Done = true self.Changes[id] = true -- Execute functors if found if v.functor then if v.postcondition then if exec(unpack(v.postcondition))then print_dbg("- Executing postcondition functor of [%s]",id) v.functor[#v.functor+1] = val exec(unpack(v.functor)) v.functor[#v.functor] = nil end else print_dbg("- Executing functor of [%s]",id) v.functor[#v.functor+1] = val exec(unpack(v.functor)) v.functor[#v.functor] = nil end end if v.type == "key_bind" then self.key_changed = true end -- See if it needs restart if v.restart then self.Need_Restart = true end if v.vid then self.Need_VidRestart = true end -- Send callback and apply changes if v.cmd then local cmd_value = val if type(cmd_value) == "boolean" then if v.bool_to_num then cmd_value = cmd_value and "1" or "0" else cmd_value = cmd_value and "on" or "off" end end print_dbg("- Saved CMD [%s] := %s", id, cmd_value) exec_console_cmd(v.cmd .. " " .. cmd_value) self.Save_CFG = true end end -- Save axr_options if self.Save_AXR then axr_main.config:save() self.Save_AXR = false end -- appdata if self.Save_CFG then print_dbg("- Saved CFG") exec_console_cmd("cfg_save") --exec_console_cmd("cfg_save tmp") end if level.present() and self.Change_Done then print_dbg("% Sent callback (on_option_change)") SendScriptCallback("on_option_change", true) end if AddScriptCallback and self.Change_Done then print_dbg("% Sent callback (mcm_option_change)") SendScriptCallback("mcm_option_change", self.Changes ) end -- Clear cache empty_table(opt_temp) empty_table(opt_backup) if self.key_changed then --resort the conflict key list. self.key_changed = false table.sort(options[1].gr[2].gr[3].gr, function(a,b) local t = {slide_kb = 1, desc_kb = 2, desc_kb2 = 3} if t[a.id] and t[b.id] then return t[a.id] < t[b.id] end if t[a.id] and not t[b.id] then return true end if (not t[a.id]) and t[b.id] then return false end return dispaly_key(get(a.kb_path)) < dispaly_key(get(b.kb_path)) end) init_opt_coder() end -- Exit self:On_Cancel() end function UIMCM:On_Cancel() self.owner:ShowDialog(true) self:HideDialog() self.owner:Show(true) -- Restart vid if self.Need_VidRestart then exec_console_cmd("vid_restart") end if self.Need_Restart then self.owner:SetMsg( game.translate_string("ui_mm_change_done_restart") , 7 ) self.message_box:InitMessageBox("message_box_restart_game") self.message_box:ShowDialog(true) elseif self.Change_Done then self.owner:SetMsg( game.translate_string("ui_mm_change_done") , 5 ) end self.Change_Done = false self.Need_VidRestart = false self.Need_Restart = false self.Save_CFG = false Unregister_UI("UIMCM") end function UIMCM:On_Discard() empty_table(opt_temp) if (self.last_path and self.last_curr_tree) then self:UpdatePending() self:Reset_opt(self.last_curr_tree, self.last_path) end self:On_Cancel() end function UIMCM:OnKeyboard(dik, keyboard_action) local res = CUIScriptWnd.OnKeyboard(self,dik,keyboard_action) if (res == false) then local bind = dik_to_bind(dik) if keyboard_action == ui_events.WINDOW_KEY_PRESSED then if dik == DIK_keys.DIK_ESCAPE then if self.k_binder then self.k_binder(-1) else self:OnButton_Cancel() end elseif self.k_binder and dispaly_key(dik) then self.k_binder(dik) end end end if self.k_binder and ((time_continual() - self.k_timer) > 500) then --clear k_binder on any other input after half a second. self.k_binder() end return res end function trader_cond(x) if x == 'get' then -- printf('@@@ returning %s', alife_storage_manager.get_state().trader_buy_condition_override or "0 (DEFAULT)") return alife_storage_manager.get_state().trader_buy_condition_override or 0 else -- printf('@@@ setting %s', opt_temp["gameplay/economy_diff/condition_buy_override"] or '0 (DEFAULT)') alife_storage_manager.get_state().trader_buy_condition_override = opt_temp["gameplay/economy_diff/condition_buy_override"] or 0 end end function store_in_save() printf("MCM Error: dph_mcm_save_storage.script not found") end if dph_mcm_save_storage and dph_mcm_save_storage.register_module then store_in_save = dph_mcm_save_storage.register_module end function on_game_start() RegisterScriptCallback("on_key_release",on_key_release) RegisterScriptCallback("on_key_press",on_key_press) end ------------------------------------------------------------ -- Tutorial:Table of Contents: ------------------------------------------------------------ --[[ Use the [-] in the margin in notpad++ to colapse sections for easier navigation 1. How to read these options in your script (RavenAscendant) 2. How to add new options (Tronex orginal turotial from ui_options) 3. How to make your script talk to MCM 4. Examples ]]-- ------------------------------------------------------------ -- Tutorial: How to read these options in your script: ------------------------------------------------------------ --[[ Feilds in [backets] are described in "How to add new options" First a bit about setting up your options. Nine times out of ten you don't need any functors. MCM will read the curent value of the setting from axr_options with out a [curr] functor and will write values to axr_options if no [functor] is provided. For simple global settings this will be more than adequate. The easiest way to read your setting is call ui_mcm.get(path) where path is the id fields of the nested tables down to the option in the table you returned in on_mcm_load() . Mostlikely this will take the form of "modname/settingname" but you can break your settings into multiple panels if you want resulting in a loinger path. see the options section of axr_configs for how anomaly options menu translates into paths, same system is used here. ui_mcm.get(path) is cached and fails back to the value you set in [def=] Just like ui_options when MCM applies a settings change it sends an on_option_change callback you can use this to do a one time read of your options into variables in your script. you can either get the values with ui_mcm.get(path) or read them directly from axr_configs like so: axr_main.config:r_value("mcm", path, type,default) --see _g for how r_value functions. Examples of when you might want to use functors: Saving mod settings to the save game file instead of globaly to axr_configs You are building your settings dynaicaly can't rely on the path being consistant. You otherwise like to over complicate things. ]]-- ------------------------------------------------------------ -- Tutorial: How to add new options: ------------------------------------------------------------ --[[ The only thing changed from this as compared to the version in ui_options is changing all ocurances of ui_mm_ to ui_mcm_ ------------------------------------------------------------------------------------------------ Option name: script will read option name (id) and show it automatically, naming should be like this: "ui_mcm_[tree_1]_[tree_2]_[tree_...]_[option]" [tree_n] and [option] are detemined from option path inside the table Example: options["video"]["general"]["renderer"] name will come from "ui_mcm_video_general_renderer" string ------------------------------------------------------------------------------------------------ Option description: option description can show up in the hint window if its defined by its name followed by "_desc" Example: option with a name of "ui_mcm_video_general_renderer" will show its hint if "ui_mcm_video_general_renderer_desc" exists ------------------------------------------------------------------------------------------------ Parameters of option Tree: ------------------------------------------------------------------------------------------------ - [id] - Define: (string) To give a tree its own identity - [sh] - Define: (boolean) used to detemine that the sub-tree tables are actual list of options to set and show - [text] - Define: (string) To over ride the display text for the tree in the tree select - [precondition] - Define: ( table {function, parameters} ) don't show tree options if its precondition return false - [output] - Define: (string) Text to show when precondition fails - [gr] - Define: ( table { ... } ) Table of a sub-tree or options list - [apply_to_all] - Define: (boolean) when you have options trees with similar options and group, you can use this to add "Apply to All" button to each option clicking it will apply option changes to this option in all other trees from same group you must give these a tree a group id - [id_gr] - Define: (string) allows you to give options tree a group id, to connect them when you want to use "Apply to all" button for options ------------------------------------------------------------------------------------------------ Parameters of options: ------------------------------------------------------------------------------------------------ ---------------------- Critical parameters: -------------------- These parameters must be declared for elements [id] - Define: (string) Option identity/name. Option get stored in axr_main or called in other sripts by its path (IDs of sub trees and option): Example: ( tree_1_id/tree_2_id/.../option_id ) The top id in the table you return to MCM (tree_1_id in the above example) should be as unique as posible to prevent it from conflicting with another mod. [type] - Define: (string) - Possible values: - Option elements: "check" : Option, check box, either ON or OFF "list" : Option, list of strings, useful for options with too many selections "input" : Option, input box, you can type a value of your choice "radio_h" : Option, radio box, select one out of many choices. Can fit up to 8 selections (Horizental layout) "radio_v" : Option, radio box, select one out of many choices. Can fit up any number of selections (Vertical layout) "track" : Option, track bar, easy way to control numric options with min/max values (can be used only if [val] = 2) "key_bind" : Option, button that registers a keypress after being clicked. (See suplimental instructions below) - Support elements: "line" : Support element, a simple line to separate things around "image" : Support element, 563x50 px image box, full-area coverage "slide" : Support element, image box on left, text on right "title" : Support element, title (left/right/center alignment) "desc" : Support element, description (left alignment) ---------------------- Dependable parameters: ---------------------- These parameters must be declared when other specific parameters are declared already. They work along with them [val] - Define: (number) - Used by: option elements: ALL Option's value type: 0. string | 1. boolean | 2. float It tells the script what kind of value the option is storing / dealing with [cmd]: - Define: (string) - Used by: option elements: ALL (needed if you want to control a console command) Tie an option to a console command, so when the option value get changed, it get applied directly to the command The option will show command's current value NOTE: cmd options don't get cached in axr_options, instead they get stored in appdata/user.ltx [def] parameter is not needed here since we engine applies default values to commands if they don't exist in user.ltx automatically [def] - Define: (boolean) / (number) / (string) / ( table {function, parameters} ) - Used by: option elements: ALL (not needed if [cmd] is used) Default value of an option when no cached values are found in axr_options, the default value will be used [min] - Define: (number) - Used by: option elements: "input" / "track": (only if [val] = 2) Minimum viable value for an option, to make sure a value stays in range [max] - Define: (number) - Used by: option elements: "input" / "track": (only if [val] = 2) Maximum viable value for an option, to make sure a value stays in range [step] - Define: (number) - Used by: option elements: "track": (only if [val] = 2) How much a value can be increased/decreased in one step [content] - Define: ( table {double pairs} ) / ( table {function, parameters} ) - Used by: option elements: "list" / "radio_h" / "radio_v": Delcares option's selection list Pairs: { value of the selection, string to show on UI } Example: content= { {0,"off"} , {1,"half"} , {2,"full"}} So the list or radio option will show 3 selections (translated strings): (ui_mcm_lst_off) and (ui_mcm_lst_half) and (ui_mcm_lst_full) When you select one and it get applied, the assosiated value will get applied So picking the first one will pass ( 0 ) Because all lists and radio button elements share the same prefix, "ui_mcm_lst_" it is important that you not use common words like the ones in the example above. Make your element names unique. [link] - Define: (string) - Used by: support elements: "image" / "slide" Link to texture you want to show [text] - Define: (string) - Used by: support elements: "slide" / "title" / "desc" String to show near the image, it will be translated ---------------------- Optional parameters: ---------------------- These parameters are completely optionals, and can be used for custom stuff [force_horz] - Define: (boolean) - Used by: option elements: "radio_h" Force the radio buttons into horizental layout, despite their number [no_str] - Define: (boolean) - Used by: option elements: "list" / "radio_h" / "radio_v" / "track" Usually, the 2nd key of pairs in content table are strings to show on the UI, by translating "opt_str_lst_(string)" when we set [no_str] to true, it will show the string fromm table as it is without translations or "opt_str_lst_" For TrackBars: no_str won't show value next to the slider [prec] - Define: (number) - Used by: option elements: "track" allowed number of zeros in a number [precondition] - Define: ( table {function, parameters} ) - Used by: option elements: ALL Show the option on UI if the precondition function returns true [functor] - Define: ( table {function, parameters} ) - Used by: option elements: ALL Execute a function when option's changes get applied The value of the option is added to the end of the parameters list. [postcondition] - Define: ( table {function, parameters} ) - Used by: option elements: ALL, with defined [functor] Option won't execute its functor when changes are applied, unless if the postcondition function returns true [curr] - Define: ( table {function, parameters} ) - Used by: option elements: ALL get current value of an option by executing the declared function, instead of reading it from axr_options.ltx [hint] (as of MCM 1.6.0 this will actualy show _desc strings) - Define: (string) - Used by: option elements: ALL Override default name / desc rule to replace the translation of an option with a custom one, should be set without "ui_mcm_" and "_desc" Example: { hint = "alife_warfare_capture"} will force the script to use "ui_mcm_alife_warfare_capture" and "ui_mcm_alife_warfare_capture_desc" for name and desc of the option [clr] - Define: ( table {a,r,b,g} ) - Used by: support elements: "title" / "desc" determines the color of the text [stretch] - Define: (boolean) - Used by: support elements: "slide" force the texture to stretch or not [pos] - Define: ( table {x,y} ) - Used by: support elements: "slide" custom pos for the texture [size] - Define: ( table {w,z} ) - Used by: support elements: "slide" custom size for the texture [align] - Define: (string) "l" "r" "c" - Used by: support elements: "title" determines the alignment of the title [spacing] - Define: (number) - Used by: support elements: "slide" hight offset to add extra space --]] ------------------------------------------------------------ -- Tutorial: How to make your script talk to MCM: ------------------------------------------------------------ --[[ MCM looks for scripts with names ending in mcm: *mcm.script you can use an _ to sperate it from the rest of the name of your script but it isn't necessary. In those scripts MCM will execute the function on_mcm_load() In order for options to be added to MCM, on_mcm_load() must return a valid options tree as described in the tutorial here, used in the ui_options script and shown in the examples below An aditioanl retun value of a string naming a collection is optional. The string will be used to create a catagory to which the the options menues of mods returning the same collection name will be added to. This is to allow for modular mods to have the settings for each module be grooped under a common heading. Note the collection name becomes the root name in your settings path and translation strings. As a root name care should be taken to ensure it will not conflict with another mod. ]]-- --------------------------------------------------------------------------------------- -- Tutorial: Using dph-hcl's script for save game specific MCM options --------------------------------------------------------------------------------------- --[[ dph-hcl's orginal script from https://www.moddb.com/mods/stalker-anomaly/addons/151-mcm-13-mcm-savefile-storage is included un altered and can be used as described and documented in thier mod and script Aditionaly for convinence the function has been aliased here as ui_mcm.store_in_save(path) this function can be called safely as MCM will simply print an error if dph-hcl's script is missing To make an option be stored in a save game instead of globaly call ui_mcm.store_in_save(path) path can be a full option path like is used by ui_mcm.get(path) or a partial path If a partial path is used all options that caontain that path will be stored in the savegame partial paths must start with a valid root and cannot end with a / In the second example below the second checkbox in the second options menu would be stored buy ui_mcm.store_in_save("example_example/example_two/2check2") In the same example storing all options (both checks) in the first option menu would be: ui_mcm.store_in_save("example_example/example_one") Lastly storing all of the options for your mod would look like: ui_mcm.store_in_save("example_example") ui_mcm.store_in_save(path) can be called at any time. The easyiest is probably in on_mcm_load() however it could be done as late as on_game_start() if one wanted to have an MCM option for global vs save specific options storing (calling ui_mcm.get(path) in on_mcm_load() is a bad idea don't do that ) ]]-- --------------------------------------------------------------------------------------- -- Tutorial: Additional information on key_bind --------------------------------------------------------------------------------------- --[[ Key binds are gathered into two meta lists for the users convienance. This means it is very important that your translation strings clearly identify what the key does and ideally it should be clear what addon the keybiind is from. The value stored by the key bind is the DIK_keys value of the key. Same number that will be given to the key related callbacks. val must be set to 2 and is still manditory. curr and functor are not curently supported. Post an issue on github describing the usecase you had for them, if it's cool enough they might get fixed. Old (pre 1.6.0) versions of MCM will not display key_bind and calling ui_mcm.get for it will return nil, take that into acount if you want reverse compatablity. ]]-- --------------------------------------------------------------------------------------- -- Tutorial: Additional Key Bind utilities --------------------------------------------------------------------------------------- --[[ MCM tracks the held status of the control and shift keys as well as a flag that is true when neither is pressed ui_mcm.MOD_NONE ui_mcm.MOD_SHIFT and ui_mcm.MOD_CTRL ui_mcm.get_mod_key(val) will return the above flags based on val: 0:MOD_NONE 1:MOD_SHIFT and 2:MOD_CTRL If these somehow get latched they reset when Escape is pressed. Please report cases of latching. MCM provides functions for detecting key double taps and keys that are held down, and single key presses that do not go on to be double or long presses. ui_mcm.double_tap(id, key, [multi_tap]) should be called from on_key_press callback after you have filtered for your key id: should be a unique identifier for your event, scriptname and a number work well:"ui_mcm01" key: is of course they key passed into the on_key_press callback. multi_tap: if true timer is updated instead of cleared allowing for the detection of triple/quad/ect taps returns: true for a given id and key if less than X ms has elapsed since the last time it was called with that id and key (X is a user configurable value between 100ms and 1000 ms returns false otherwise. If multi_tap is false timer is reset when true is returned preventing the function from returning true twice in a row If multi_tap is true the function will return true any time the gap between a call and the one before is within the window. ui_mcm.key_hold(id, key, [repeat]) should be called from on_key_hold callback after you have filtered for your key id: should be a unique identifer for your event, scriptname and a number work well:"ui_mcm01" key: is the key passed into the on_key_hold callback. repeat: Optional. time in seconds. If the key continues to be held down will return true again after this many seconds on a cycle. when called from the on_key_hold callback it will return true after the key has been held down for Y ms (determined by applying a user defined multiplier to X above) and then again every repeat seconds if repeat is provided. sequence resets when key is released. ui_mcm.simple_press(id, key, functor) should be called from on_key_press callback after you have filtered for your key id: should be a unique identifier for your event, scrip name and a number work well:"ui_mcm01" key: is the key passed into the on_key_press callback. function: table {function, parameters}, to be executed when it is determined that the press is not long or double (or multi press in general) Unlike the other two this does not return any thing but instead you give it a function to execute. Using this function you gain exclusivity, your event won't fire when the key is double(multi) taped or held (long press), at the cost of a small bit of input delay. This delay is dependent on the double tap window the used defines in the MCM Key Bind settings. The following option entries have translation stings provided by MCM and are setup to be ignored by pre 1.6.0 versions of MCM Note the keybind conflict identification in MCM does NOT look for these and reports conflict on the keybind value alone. With shift and control, radio buton style {id = "modifier", type = ui_mcm.kb_mod_radio, val = 2, def = 0, hint = "mcm_kb_modifier" , content= { {0,"mcm_kb_mod_none"} , {1,"mcm_kb_mod_shift"} , {2,"mcm_kb_mod_ctrl"},{3,"mcm_kb_mod_alt"}}}, With shift and control, list style {id = "modifier", type = ui_mcm.kb_mod_list, val = 2, def = 0, hint = "mcm_kb_modifier" , content= { {0,"mcm_kb_mod_none"} , {1,"mcm_kb_mod_shift"} , {2,"mcm_kb_mod_ctrl"},{3,"mcm_kb_mod_alt"}}}, Single double or long press, , radio buton style {id = "mode", type = ui_mcm.kb_mod_radio, val = 2, def = 0, hint = "mcm_kb_mode" , content= { {0,"mcm_kb_mode_press"} , {1,"mcm_kb_mode_dtap"} , {2,"mcm_kb_mode_hold"}}}, Single double or long press, , radio buton style {id = "mode", type = ui_mcm.kb_mod_list, val = 2, def = 0, hint = "mcm_kb_mode" , content= { {0,"mcm_kb_mode_press"} , {1,"mcm_kb_mode_dtap"} , {2,"mcm_kb_mode_hold"}}}, An example script making use of all of these can be found at: https://github.com/RAX-Anomaly/MiniMapToggle/blob/main/gamedata/scripts/mini_map_toggle_mcm.script ]]-- ------------------------------------------------------------ -- Tutorial: Examples: ------------------------------------------------------------ -- these examples can all be copied to a blank script example_mcm.script and ran. -- A simple menu with a title slide and check boxes. --[[ function on_mcm_load() op = { id= "example_example" ,sh=true ,gr={ { id= "slide_example_example" ,type= "slide" ,link= "AMCM_Banner.dds" ,text= "ui_mcm_title_example_example" ,size= {512,50} ,spacing= 20 }, {id = "check1", type = "check", val = 1, def = false}, {id = "check2", type = "check", val = 1, def = false}, } } return op end ]]-- -- A a tree with a root containing three menues with a title slide and check boxes. --[[ function on_mcm_load() op = { id= "example_example" , ,gr={ { id= "example_one" ,sh=true ,gr={ { id= "slide_example_example" ,type= "slide" ,link= "AMCM_Banner.dds" ,text= "ui_mcm_title_example_example" ,size= {512,50} ,spacing= 20 }, {id = "1check1", type = "check", val = 1, def = false}, {id = "1check2", type = "check", val = 1, def = false}, } }, { id= "example_two" ,sh=true ,gr={ { id= "slide_example_example" ,type= "slide" ,link= "AMCM_Banner.dds" ,text= "ui_mcm_title_example_example" ,size= {512,50} ,spacing= 20 }, {id = "2check1", type = "check", val = 1, def = false}, {id = "2check2", type = "check", val = 1, def = false}, } }, { id= "example_three" ,sh=true ,gr={ { id= "slide_example_example" ,type= "slide" ,link= "AMCM_Banner.dds" ,text= "ui_mcm_title_example_example" ,size= {512,50} ,spacing= 20 }, {id = "3check1", type = "check", val = 1, def = false}, {id = "3check2", type = "check", val = 1, def = false}, } }, } } return op end ]]-- -- Two scripts with a simple menu with a title slide and check boxes, that will be added to a collection named "collection_example" --[[ -- example1_mcm.script function on_mcm_load() op = { id= "first_example" ,sh=true ,gr={ { id= "slide_first_example" ,type= "slide" ,link= "AMCM_Banner.dds" ,text= "ui_mcm_title_first_example" ,size= {512,50} ,spacing= 20 }, {id = "check1", type = "check", val = 1, def = false}, {id = "check2", type = "check", val = 1, def = false}, } } return op, "collection_example" end -- example2_mcm.script. function on_mcm_load() op = { id= "second_example" ,sh=true ,gr={ { id= "slide_second_example" ,type= "slide" ,link= "AMCM_Banner.dds" ,text= "ui_mcm_title_second_example" ,size= {512,50} ,spacing= 20 }, {id = "check1", type = "check", val = 1, def = false}, {id = "check2", type = "check", val = 1, def = false}, } } return op, "collection_example" end ]]--