local storage = core.get_mod_storage() playersettings = {} playersettings.registered = {} playersettings.settingslist = {} -- per-player values for formspec creation playersettings.highlighted = {} playersettings.show_technical_names = {} playersettings.form = "formspec_version[4]".. "size[15,12.5]" .. "button[11, 9.5;3,1;bset;Set]".. "button[12,11;2,1;breset;Reset]".. "textlist[0.5,0.5;7,11;settinglist;%s;%d;false]".. "checkbox[0.5,12;ctechnical;Show technical names;%s]".. " box[8,1;6.5,4;#222]".. "textarea[8,1;6.5,4;;%s;%s]".. "%s" local text_no_settings = [[ No settings currently exist. To use the settings menu, one or more mods that provide some settings must be installed. ]] playersettings.form_fallback = ( "formspec_version[4]".. "size[10,4.5]".. "textarea[0.5,0.5;9,2.5;;;" .. text_no_settings .."]".. "button_exit[4,3;2,1;quit;Close]" ) --- Returns: setting, def local function get_setting_at(index) local settingname = playersettings.settingslist[index] if not settingname then return nil, nil end return settingname, playersettings.registered[settingname] end local function table_concat_escaped(list, delim) local esc = {} for i, v in ipairs(list) do esc[i] = core.formspec_escape(v) end return table.concat(esc, delim) end local setting_get_no_fallback -- (name, setting, require_existence) function playersettings.openform(player) if type(player) == "string" then player = core.get_player_by_name(player) end local name = player:get_player_name() if #playersettings.settingslist < 1 then core.show_formspec(name,"playersettings:nosettings", playersettings.form_fallback) return end local settingslist do local list = {} local technical = playersettings.show_technical_names[name] for _,setting in ipairs(playersettings.settingslist) do local def = playersettings.registered[setting] local row if technical then row = ("%s (%s)"):format(setting, def.type) else row = def.shortdesc end if (setting_get_no_fallback(name, setting, false)) ~= nil then -- 'Value is set' indicator row = row .. " *" end table.insert(list, row) end settingslist = table_concat_escaped(list, ",") end local settingname, def = get_setting_at(playersettings.highlighted[name]) if not def then playersettings.highlighted[name] = 1 settingname, def = get_setting_at(1) end local settingvalue = playersettings.get(name, settingname) -- Engine bug? Even though this code uses the correct values, when pressing -- "Set" and then "Reset", the shown value does not update. --print(def.type, settingvalue) local extras = "" if def.type == "boolean" then extras = ( "checkbox[8,6.5;checkbox;Enabled;%s]" .. "field_close_on_enter[field;false]" ):format( settingvalue and "true" or "false" ) elseif def.type == "string" then extras = ( "field[8,6;6,1;field;Enter value:;%s]" .. "textarea[8,7.5;6,2;;;Allowed values: %s\nCharacters limit: %d]" .. "field_close_on_enter[field;false]" ):format( core.formspec_escape(settingvalue), core.formspec_escape(def.values or "any string"), def.max ) elseif def.type == "number" then local allowed = "any number" if def.min and def.max then allowed = "numbers "..def.min.." to "..def.max elseif def.max then allowed = "numbers up to "..def.max elseif def.min then allowed = "numbers "..def.min.." and up" end if def.integer then allowed = allowed.." (whole numbers only)" end extras = ( "field[8,6;6,1;field;Enter value:;%s]" .. "textarea[8,7.5;6,2;;;Allowed values: %s]" .. "field_close_on_enter[field;false]" ):format( core.formspec_escape(settingvalue), allowed ) elseif def.type == "enum" then local selected = table.indexof(def.values, settingvalue) if selected == -1 then selected = 1 end extras = ("dropdown[8,6;6,1;dropdown;%s;%d;true]"):format( table_concat_escaped(table.copy(def.values), ","), selected ) end local form = playersettings.form:format( settingslist, playersettings.highlighted[name], playersettings.show_technical_names[name] and "true" or "false", core.formspec_escape(def.shortdesc), core.formspec_escape(def.longdesc), extras ) core.show_formspec(name,"playersettings:settings",form) end function playersettings.handleform(player,form,fields) if form ~= "playersettings:settings" then return end local name = player:get_player_name() if fields.ctechnical then playersettings.show_technical_names[name] = fields.ctechnical == "true" playersettings.openform(player) return end local settingname, def = get_setting_at(playersettings.highlighted[name]) if def then if fields.breset then playersettings.reset(name, settingname) playersettings.openform(player) return end -- May also be triggered by `fields.bset` if def.type == "boolean" and fields.checkbox then playersettings.set(name,settingname,fields.checkbox == "true") elseif def.type == "enum" and fields.dropdown then local value = def.values[tonumber(fields.dropdown)] if value then playersettings.set(name, settingname, value) end elseif def.type == "number" and fields.field then local value = tonumber(fields.field) if value and ((not def.max) or (value <= def.max)) and ((not def.min) or (value >= def.min)) then if def.integer then value = math.floor(value) end playersettings.set(name,settingname,value) end elseif def.type == "string" and fields.field then local value = fields.field:sub(1, def.max) playersettings.set(name,settingname,value) end end if fields.settinglist then local exp = core.explode_textlist_event(fields.settinglist) if exp.type == "CHG" then playersettings.highlighted[name] = exp.index playersettings.openform(player) end end end local function CHECK_TYPE(typename, value, readable) local got = type(value) assert(got == typename, ("Invalid %s (expected %s, got %s)"):format(readable, typename, got)) end function playersettings.getdefault(setting) local def = playersettings.registered[setting] if def.default ~= nil then return def.default end if def.type == "boolean" then return false elseif def.type == "number" then return def.min or 0 elseif def.type == "string" then return "" end end --- Return #1: Value or `nil` if no value was found --- Return #2: Database key setting_get_no_fallback = function(name, setting, require_existence) CHECK_TYPE("string", name, "player name") CHECK_TYPE("string", setting, "setting name") local key = ("%s|%s"):format(name, setting) if require_existence then assert(playersettings.registered[setting],"No such setting: "..setting) end return core.deserialize(storage:get_string(key)), key end function playersettings.get(name,setting) local value = setting_get_no_fallback(name, setting, true) if value ~= nil then return value else return playersettings.getdefault(setting) end end function playersettings.set(name,setting,value) local old, key = setting_get_no_fallback(name, setting, true) -- Compare value without the .get(...) default fallback local is_different = old ~= value if old == nil then local default = playersettings.getdefault(setting) -- Allow using the default value but do not fire callbacks if they are equal is_different = value ~= default end local def = playersettings.registered[setting] if def.type == "enum" then -- Prohibit unknown values if table.indexof(def.values, value) == -1 then return end end if is_different and def.onchange then if not def.onchange(name,old,value) then return end end storage:set_string(key,core.serialize(value)) if is_different and def.afterchange then def.afterchange(name,old,value) end end function playersettings.reset(name, setting) local old, key = setting_get_no_fallback(name, setting, false) -- ^ 'false': Do not require 'setting' to be defined to allow removing unused settings. local def = playersettings.registered[setting] local default = def and playersettings.getdefault(setting) local is_different = def and old ~= default if is_different and def.onchange then if not def.onchange(name,old,default) then return end end storage:set_string(key, "") if is_different and def.afterchange then def.afterchange(name,old,default) end end function playersettings.register(setting,def) CHECK_TYPE("string", setting, "setting name") CHECK_TYPE("table", def, "setting definition") assert(not playersettings.registered[setting],string.format("Setting %s is already defined",setting)) assert(type(def.type) == "string") assert(type(def.shortdesc) == "string") assert(type(def.longdesc) == "string") assert(not def.onjoin or type(def.onjoin) == "function") assert(not def.onchange or type(def.onchange) == "function") assert(not def.afterchange or type(def.afterchange) == "function") -- Type-specific checks if def.type == "string" then assert(type(def.values) == "string") def.max = def.max or 200 elseif def.type == "enum" then assert(type(def.values) == "table") end local default_type = (def.type == "enum") and "string" or def.type assert(def.default == nil or type(def.default) == default_type) table.insert(playersettings.settingslist,setting) table.sort(playersettings.settingslist) playersettings.registered[setting] = def end function playersettings.onjoin(player) local name = player:get_player_name() for setting,def in pairs(playersettings.registered) do if def.onjoin then def.onjoin(name,playersettings.get(name,setting)) end end end local function on_leaveplayer(player) -- Clean up memory local name = player:get_player_name() playersettings.highlighted[name] = nil playersettings.show_technical_names[name] = nil end if core.global_exists("unified_inventory") then unified_inventory.register_button("playersettings", { action = playersettings.openform, tooltip = "Settings", type = "image", image = "playersettings_settings_button.png" }) end core.register_chatcommand("settings",{ description = "Open player settings menu", func = playersettings.openform, }) core.register_on_joinplayer(playersettings.onjoin) core.register_on_leaveplayer(on_leaveplayer) core.register_on_player_receive_fields(playersettings.handleform) -- For testing purposes only! --dofile(core.get_modpath(core.get_current_modname()) .. "/examples.lua")