diff --git a/Readme.md b/Readme.md index 7f985b4..ebd1fc5 100644 --- a/Readme.md +++ b/Readme.md @@ -8,4 +8,11 @@ No dependencies. Licensed under the MIT License. Written by Lars Mueller aka LMD ## API -Mostly self-documenting code. Mod namespace is `modlib` or `_ml`, containing all variables & functions. \ No newline at end of file +Mostly self-documenting code. Mod namespace is `modlib` or `_ml`, containing all variables & functions. + +## Configuration + +1. Configuration is loaded from `/config/.`, the following extensions are supported and will be searched for (in that order): + 1. [`json`](https://json.org) + 2. [`conf`](https://github.com/minetest/minetest/blob/master/doc/lua_api.txt) +2. Settings are loaded and override configuration values \ No newline at end of file diff --git a/conf.lua b/conf.lua index bb4175b..6de8552 100644 --- a/conf.lua +++ b/conf.lua @@ -2,13 +2,84 @@ minetest.mkdir(minetest.get_worldpath().."/config") function get_path(confname) return minetest.get_worldpath().."/config/"..confname end -function load (filename, constraints) +function read_conf(text) + local lines = modlib.text.split_lines(text, nil, true) + local dict = {} + for i, line in ipairs(lines) do + local error_base = "Line " .. (i+1) .. ": " + line = modlib.text.trim_left(lines[i]) + if line ~= "" and line:sub(1,1) ~= "#" then + line = modlib.text.split(line, "=", 2) + if #line ~= 2 then + error(error_base .. "No value given") + end + local prop = modlib.text.trim_right(line[1]) + if prop == "" then + error(error_base .. "No key given") + end + local val = modlib.text.trim_left(line[2]) + if val == "" then + error(error_base .. "No value given") + end + if modlib.text.starts_with(val, '"""') then + val = val:sub(3) + local total_val = {} + local function readMultiline() + while i < #lines do + if modlib.text.ends_with(val, '"""') then + val = val:sub(1, val:len() - 3) + return + end + table.insert(total_val, val) + i = i + 1 + val = lines[i] + end + i = i - 1 + error(error_base .. "Unclosed multiline block") + end + readMultiline() + table.insert(total_val, val) + val = table.concat(total_val, "\n") + else + val = modlib.text.trim_right(val) + end + if dict[prop] then + error(error_base .. "Duplicate key") + end + dict[prop] = val + end + end + return dict +end +function build_tree(dict) + local tree = {} + for key, value in pairs(dict) do + local path = modlib.text.split_unlimited(key, ".") + local subtree = tree + for i = 1, #path - 1 do + local index = tonumber(path[i]) or path[i] + subtree[index] = subtree[index] or {} + subtree = subtree[index] + end + subtree[path[#path]] = value + end + return tree +end +function build_setting_tree() + modlib.conf.settings = build_tree(minetest.settings:to_table()) +end +function check_config_constraints(config, constraints, handler) + local no_error, error_or_retval = pcall(function() check_constraints(config, constraints) end) + if not no_error then + handler(error_or_retval) + end +end +function load(filename, constraints) local config = minetest.parse_json(modlib.file.read(filename)) if constraints then - local error_message = check_constraints(config, constraints) - if error_message then - error("Configuration - "..filename.." doesn't satisfy constraints: "..error_message) - end + check_config_constraints(config, constraints, function(message) + error('Configuration of file "'..filename.."\" doesn't satisfy constraints: "..message) + end) end return config end @@ -16,86 +87,191 @@ function load_or_create(filename, replacement_file, constraints) modlib.file.create_if_not_exists_from_file(filename, replacement_file) return load(filename, constraints) end -function import(modname, constraints) - return load_or_create(get_path(modname)..".json", modlib.mod.get_resource(modname, "default_config.json"), constraints) +function import(modname, constraints, no_settingtypes) + local default_config = modlib.mod.get_resource(modname, "default_config.json") + local config = load_or_create(get_path(modname)..".json", default_config, constraints) + if not no_settingtypes then + constraints.name = modname + local settingtypes = generate_settingtypes(minetest.parse_json(modlib.file.read(default_config)), constraints) + modlib.file.write(modlib.mod.get_resource(modname, "settingtypes.txt"), settingtypes) + end + local additional_settings = modlib.conf.settings[modname] or {} + additional_settings = fix_types(additional_settings, constraints) + config = merge_config(config, additional_settings) + if constraints then + check_config_constraints(config, constraints, function(message) + error('Configuration of mod "'..modname.."\" doesn't satisfy constraints: "..message) + end) + end + return config end -function check_constraints(value, constraints) - local t=type(value) - if constraints.func then - local possible_errors=constraints.func(value) - if possible_errors then - return possible_errors +function merge_config(config, additional_settings) + if not config or type(additional_settings) ~= "table" then + return additional_settings + end + for setting, value in pairs(additional_settings) do + if config[setting] then + config[setting] = merge_config(config[setting], value) end end - if constraints.type and constraints.type~=t then - return "Wrong type: Expected "..constraints.type..", found "..t + return config +end +-- format: # comment +-- name (Readable name) type type_args +function generate_settingtypes(default_conf, constraints) + local constraint_type = constraints.type + if constraints.children or constraints.possible_children or constraints.required_children or constraints.keys or constraints.values then + constraint_type = "table" + end + local settingtype, type_args + local title = constraints.title + if not title then + title = modlib.text.split(constraints.name, "_") + title[1] = modlib.text.upper_first(title[1]) + title = table.concat(title, " ") + end + if constraint_type == "boolean" then + settingtype = "bool" + default_conf = default_conf and "true" or "false" + elseif constraint_type == "string" then + settingtype = "string" + elseif constraint_type == "number" then + settingtype = constraints.int and "int" or "float" + local range = constraints.range + if range then + -- TODO consider better max + type_args = (constraints.int and "%d %d" or "%f %f"):format(range[1], range[2] or math.pow(2, 30)) + end + elseif constraint_type == "table" then + local handled = {} + local settings = {} + local function setting(key, value_constraints) + if handled[key] then + return + end + handled[key] = true + value_constraints.name = constraints.name .. "." .. key + value_constraints.title = title .. " " .. key + table.insert(settings, generate_settingtypes(default_conf and default_conf[key], value_constraints)) + end + for _, table in ipairs{"children", "required_children", "possible_children"} do + for key, constraints in pairs(constraints[table] or {}) do + setting(key, constraints) + end + end + return table.concat(settings, "\n") + end + if not constraint_type then + return "" + end + local comment = constraints.comment + if comment then + comment = "# " .. comment .. "\n" + else + comment = "" + end + assert(type(default_conf) == "string" or type(default_conf) == "number" or type(default_conf) == "nil", dump(default_conf)) + return comment .. constraints.name .. " (" .. title .. ") " .. settingtype .. " " .. (default_conf or "") ..(type_args and (" "..type_args) or "") +end +function fix_types(value, constraints) + local type = type(value) + local expected_type = constraints.type + if expected_type and expected_type ~= type then + assert(type == "string", "Can't fix non-string value") + if expected_type == "boolean" then + assert(value == "true" or value == "false", "Not a boolean (true or false): " .. value) + value = value == "true" + elseif expected_type == "number" then + assert(tonumber(value), "Not a number: " .. value) + value = tonumber(value) + end + end + if type == "table" then + for key, val in pairs(value) do + for _, child_constraints in ipairs{"required_children", "children", "possible_children"} do + child_constraints = (constraints[child_constraints] or {})[key] + if child_constraints then + val = fix_types(val, child_constraints) + end + end + if constraints.values then + val = fix_types(val, constraints.values) + end + if constraints.keys then + value[key] = nil + value[fix_types(key, constraints.keys)] = val + else + value[key] = val + end + end + end + return value +end +function check_constraints(value, constraints) + local t = type(value) + if constraints.type and constraints.type ~= t then + error("Wrong type: Expected "..constraints.type..", found "..t) end if (t == "number" or t == "string") and constraints.range then if value < constraints.range[1] or (constraints.range[2] and value > constraints.range[2]) then - return "Not inside range: Expected value >= "..constraints.range[1].." and <= "..constraints.range[1]..", found "..minetest.write_json(value) + error("Not inside range: Expected value >= "..constraints.range[1].." and <= "..(constraints.range[2] or "inf")..", found "..minetest.write_json(value)) end end + if t == "number" and constraints.int and value % 1 ~= 0 then + error("Not an integer number: " .. minetest.write_json(value)) + end if constraints.possible_values and not constraints.possible_values[value] then - return "None of the possible values: Expected one of "..minetest.write_json(modlib.table.keys(constraints.possible_values))..", found "..minetest.write_json(value) + error("None of the possible values: Expected one of "..minetest.write_json(modlib.table.keys(constraints.possible_values))..", found "..minetest.write_json(value)) end if t == "table" then if constraints.children then - for k, v in pairs(value) do - local child=constraints.children[k] - if not child then - return "Unexpected table entry: Expected one of "..minetest.write_json(modlib.table.keys(constraints.children))..", found "..minetest.write_json(k) + for key, val in pairs(value) do + local child_constraints = constraints.children[key] + if not child_constraints then + error("Unexpected table entry: Expected one of "..minetest.write_json(modlib.table.keys(constraints.children))..", found "..minetest.write_json(key)) else - local possible_errors=check_constraints(v, child) - if possible_errors then - return possible_errors - end + check_constraints(val, child_constraints) end end - for k, _ in pairs(constraints.children) do - if value[k] == nil then - return "Table entry missing: Expected key "..minetest.write_json(k).." to be present in table "..minetest.write_json(value) + for key, _ in pairs(constraints.children) do + if value[key] == nil then + error("Table entry missing: Expected key "..minetest.write_json(key).." to be present in table "..minetest.write_json(value)) end end end if constraints.required_children then - for k,v_constraints in pairs(constraints.required_children) do - local v=value[k] - if v then - local possible_errors=check_constraints(v, v_constraints) - if possible_errors then - return possible_errors - end + for key, value_constraints in pairs(constraints.required_children) do + local val = value[key] + if val then + check_constraints(val, value_constraints) else - return "Table entry missing: Expected key "..minetest.write_json(k).." to be present in table "..minetest.write_json(value) + error("Table entry missing: Expected key "..minetest.write_json(key).." to be present in table "..minetest.write_json(value)) end end end if constraints.possible_children then - for k,v_constraints in pairs(constraints.possible_children) do - local v=value[k] - if v then - local possible_errors=check_constraints(v, v_constraints) - if possible_errors then - return possible_errors - end + for key, value_constraints in pairs(constraints.possible_children) do + local val = value[key] + if val then + check_constraints(val, value_constraints) end end end if constraints.keys then - for k,_ in pairs(value) do - local possible_errors=check_constraints(k, constraints.keys) - if possible_errors then - return possible_errors - end + for key,_ in pairs(value) do + check_constraints(key, constraints.keys) end end if constraints.values then - for _,v in pairs(value) do - local possible_errors=check_constraints(v, constraints.values) - if possible_errors then - return possible_errors - end + for _, val in pairs(value) do + check_constraints(val, constraints.values) end end end + if constraints.func then + local possible_errors = constraints.func(value) + if possible_errors then + error(possible_errors) + end + end end \ No newline at end of file diff --git a/init.lua b/init.lua index 73eb3dc..484f6f8 100644 --- a/init.lua +++ b/init.lua @@ -77,6 +77,8 @@ for component, additional in pairs(components) do end end +modlib.conf.build_setting_tree() + modlib.mod.loadfile_exports = loadfile_exports -- complete the string library (=metatable) with text helpers diff --git a/mod.conf b/mod.conf index 9878aae..93b4b95 100644 --- a/mod.conf +++ b/mod.conf @@ -1,2 +1,3 @@ name=modlib description=Multipurpose Minetest Modding Library +author=LMD aka appguru(eu) \ No newline at end of file diff --git a/screenshot.png b/screenshot.png index 38c62a2..2738af6 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/text.lua b/text.lua index 1c91d0f..efa6909 100644 --- a/text.lua +++ b/text.lua @@ -41,35 +41,58 @@ function trim_begin(str, to_remove) return str:sub(j) end -function split(str, delim, limit) - if not limit then return split_without_limit(str, delim) end +trim_left = trim_begin + +function trim_end(str, to_remove) + local k = 1 + for i = string.len(str), 1, -1 do + if str:sub(i, i) ~= to_remove then + k = i + break + end + end + return str:sub(1, k) +end + +trim_right = trim_end + +function split(str, delim, limit, regex) + if not limit then return split_without_limit(str, delim, regex) end + local no_regex = not regex local parts = {} local occurences = 1 local last_index = 1 - local index = string.find(str, delim, 1, true) + local index = string.find(str, delim, 1, no_regex) while index and occurences < limit do table.insert(parts, string.sub(str, last_index, index - 1)) last_index = index + string.len(delim) - index = string.find(str, delim, index + string.len(delim), true) + index = string.find(str, delim, index + string.len(delim), no_regex) occurences = occurences + 1 end table.insert(parts, string.sub(str, last_index)) return parts end -function split_without_limit(str, delim) +function split_without_limit(str, delim, regex) + local no_regex = not regex local parts = {} local last_index = 1 - local index = string.find(str, delim, 1, true) + local index = string.find(str, delim, 1, no_regex) while index do table.insert(parts, string.sub(str, last_index, index - 1)) last_index = index + string.len(delim) - index = string.find(str, delim, index + string.len(delim), true) + index = string.find(str, delim, index + string.len(delim), no_regex) end table.insert(parts, string.sub(str, last_index)) return parts end +split_unlimited = split_without_limit + +function split_lines(str, limit) + modlib.text.split(str, "\r?\n", limit, true) +end + hashtag = string.byte("#") zero = string.byte("0") nine = string.byte("9")