2620 lines
82 KiB
Plaintext
2620 lines
82 KiB
Plaintext
|
|
||
|
--[[
|
||
|
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
|
||
|
]]--
|
||
|
|