mirror of
https://github.com/appgurueu/modlib.git
synced 2024-11-26 01:03:46 +01:00
Large configuration improvements, text helpers
This commit is contained in:
parent
03de70c774
commit
e5266d9862
@ -9,3 +9,10 @@ No dependencies. Licensed under the MIT License. Written by Lars Mueller aka LMD
|
|||||||
## API
|
## API
|
||||||
|
|
||||||
Mostly self-documenting code. Mod namespace is `modlib` or `_ml`, containing all variables & functions.
|
Mostly self-documenting code. Mod namespace is `modlib` or `_ml`, containing all variables & functions.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
1. Configuration is loaded from `<worldpath>/config/<modname>.<extension>`, 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
|
284
conf.lua
284
conf.lua
@ -2,13 +2,84 @@ minetest.mkdir(minetest.get_worldpath().."/config")
|
|||||||
function get_path(confname)
|
function get_path(confname)
|
||||||
return minetest.get_worldpath().."/config/"..confname
|
return minetest.get_worldpath().."/config/"..confname
|
||||||
end
|
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))
|
local config = minetest.parse_json(modlib.file.read(filename))
|
||||||
if constraints then
|
if constraints then
|
||||||
local error_message = check_constraints(config, constraints)
|
check_config_constraints(config, constraints, function(message)
|
||||||
if error_message then
|
error('Configuration of file "'..filename.."\" doesn't satisfy constraints: "..message)
|
||||||
error("Configuration - "..filename.." doesn't satisfy constraints: "..error_message)
|
end)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
return config
|
return config
|
||||||
end
|
end
|
||||||
@ -16,86 +87,191 @@ function load_or_create(filename, replacement_file, constraints)
|
|||||||
modlib.file.create_if_not_exists_from_file(filename, replacement_file)
|
modlib.file.create_if_not_exists_from_file(filename, replacement_file)
|
||||||
return load(filename, constraints)
|
return load(filename, constraints)
|
||||||
end
|
end
|
||||||
function import(modname, constraints)
|
function import(modname, constraints, no_settingtypes)
|
||||||
return load_or_create(get_path(modname)..".json", modlib.mod.get_resource(modname, "default_config.json"), constraints)
|
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 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
|
||||||
|
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
|
end
|
||||||
function check_constraints(value, constraints)
|
function check_constraints(value, constraints)
|
||||||
local t=type(value)
|
local t = type(value)
|
||||||
if constraints.func then
|
if constraints.type and constraints.type ~= t then
|
||||||
local possible_errors=constraints.func(value)
|
error("Wrong type: Expected "..constraints.type..", found "..t)
|
||||||
if possible_errors then
|
|
||||||
return possible_errors
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if constraints.type and constraints.type~=t then
|
|
||||||
return "Wrong type: Expected "..constraints.type..", found "..t
|
|
||||||
end
|
end
|
||||||
if (t == "number" or t == "string") and constraints.range then
|
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
|
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
|
||||||
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
|
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
|
end
|
||||||
if t == "table" then
|
if t == "table" then
|
||||||
if constraints.children then
|
if constraints.children then
|
||||||
for k, v in pairs(value) do
|
for key, val in pairs(value) do
|
||||||
local child=constraints.children[k]
|
local child_constraints = constraints.children[key]
|
||||||
if not child then
|
if not child_constraints then
|
||||||
return "Unexpected table entry: Expected one of "..minetest.write_json(modlib.table.keys(constraints.children))..", found "..minetest.write_json(k)
|
error("Unexpected table entry: Expected one of "..minetest.write_json(modlib.table.keys(constraints.children))..", found "..minetest.write_json(key))
|
||||||
else
|
else
|
||||||
local possible_errors=check_constraints(v, child)
|
check_constraints(val, child_constraints)
|
||||||
if possible_errors then
|
|
||||||
return possible_errors
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
for key, _ in pairs(constraints.children) do
|
||||||
for k, _ in pairs(constraints.children) do
|
if value[key] == nil then
|
||||||
if value[k] == nil then
|
error("Table entry missing: Expected key "..minetest.write_json(key).." to be present in table "..minetest.write_json(value))
|
||||||
return "Table entry missing: Expected key "..minetest.write_json(k).." to be present in table "..minetest.write_json(value)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if constraints.required_children then
|
if constraints.required_children then
|
||||||
for k,v_constraints in pairs(constraints.required_children) do
|
for key, value_constraints in pairs(constraints.required_children) do
|
||||||
local v=value[k]
|
local val = value[key]
|
||||||
if v then
|
if val then
|
||||||
local possible_errors=check_constraints(v, v_constraints)
|
check_constraints(val, value_constraints)
|
||||||
if possible_errors then
|
|
||||||
return possible_errors
|
|
||||||
end
|
|
||||||
else
|
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
|
end
|
||||||
end
|
end
|
||||||
if constraints.possible_children then
|
if constraints.possible_children then
|
||||||
for k,v_constraints in pairs(constraints.possible_children) do
|
for key, value_constraints in pairs(constraints.possible_children) do
|
||||||
local v=value[k]
|
local val = value[key]
|
||||||
if v then
|
if val then
|
||||||
local possible_errors=check_constraints(v, v_constraints)
|
check_constraints(val, value_constraints)
|
||||||
if possible_errors then
|
|
||||||
return possible_errors
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if constraints.keys then
|
if constraints.keys then
|
||||||
for k,_ in pairs(value) do
|
for key,_ in pairs(value) do
|
||||||
local possible_errors=check_constraints(k, constraints.keys)
|
check_constraints(key, constraints.keys)
|
||||||
if possible_errors then
|
|
||||||
return possible_errors
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if constraints.values then
|
if constraints.values then
|
||||||
for _,v in pairs(value) do
|
for _, val in pairs(value) do
|
||||||
local possible_errors=check_constraints(v, constraints.values)
|
check_constraints(val, constraints.values)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if constraints.func then
|
||||||
|
local possible_errors = constraints.func(value)
|
||||||
if possible_errors then
|
if possible_errors then
|
||||||
return possible_errors
|
error(possible_errors)
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
2
init.lua
2
init.lua
@ -77,6 +77,8 @@ for component, additional in pairs(components) do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
modlib.conf.build_setting_tree()
|
||||||
|
|
||||||
modlib.mod.loadfile_exports = loadfile_exports
|
modlib.mod.loadfile_exports = loadfile_exports
|
||||||
|
|
||||||
-- complete the string library (=metatable) with text helpers
|
-- complete the string library (=metatable) with text helpers
|
||||||
|
1
mod.conf
1
mod.conf
@ -1,2 +1,3 @@
|
|||||||
name=modlib
|
name=modlib
|
||||||
description=Multipurpose Minetest Modding Library
|
description=Multipurpose Minetest Modding Library
|
||||||
|
author=LMD aka appguru(eu)
|
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 49 KiB |
37
text.lua
37
text.lua
@ -41,35 +41,58 @@ function trim_begin(str, to_remove)
|
|||||||
return str:sub(j)
|
return str:sub(j)
|
||||||
end
|
end
|
||||||
|
|
||||||
function split(str, delim, limit)
|
trim_left = trim_begin
|
||||||
if not limit then return split_without_limit(str, delim) end
|
|
||||||
|
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 parts = {}
|
||||||
local occurences = 1
|
local occurences = 1
|
||||||
local last_index = 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
|
while index and occurences < limit do
|
||||||
table.insert(parts, string.sub(str, last_index, index - 1))
|
table.insert(parts, string.sub(str, last_index, index - 1))
|
||||||
last_index = index + string.len(delim)
|
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
|
occurences = occurences + 1
|
||||||
end
|
end
|
||||||
table.insert(parts, string.sub(str, last_index))
|
table.insert(parts, string.sub(str, last_index))
|
||||||
return parts
|
return parts
|
||||||
end
|
end
|
||||||
|
|
||||||
function split_without_limit(str, delim)
|
function split_without_limit(str, delim, regex)
|
||||||
|
local no_regex = not regex
|
||||||
local parts = {}
|
local parts = {}
|
||||||
local last_index = 1
|
local last_index = 1
|
||||||
local index = string.find(str, delim, 1, true)
|
local index = string.find(str, delim, 1, no_regex)
|
||||||
while index do
|
while index do
|
||||||
table.insert(parts, string.sub(str, last_index, index - 1))
|
table.insert(parts, string.sub(str, last_index, index - 1))
|
||||||
last_index = index + string.len(delim)
|
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
|
end
|
||||||
table.insert(parts, string.sub(str, last_index))
|
table.insert(parts, string.sub(str, last_index))
|
||||||
return parts
|
return parts
|
||||||
end
|
end
|
||||||
|
|
||||||
|
split_unlimited = split_without_limit
|
||||||
|
|
||||||
|
function split_lines(str, limit)
|
||||||
|
modlib.text.split(str, "\r?\n", limit, true)
|
||||||
|
end
|
||||||
|
|
||||||
hashtag = string.byte("#")
|
hashtag = string.byte("#")
|
||||||
zero = string.byte("0")
|
zero = string.byte("0")
|
||||||
nine = string.byte("9")
|
nine = string.byte("9")
|
||||||
|
Loading…
Reference in New Issue
Block a user