mirror of
https://github.com/minetest/minetest.git
synced 2024-09-19 20:08:33 +02:00
484 lines
12 KiB
Lua
484 lines
12 KiB
Lua
--Minetest
|
|
--Copyright (C) 2015 PilzAdam
|
|
--
|
|
--This program is free software; you can redistribute it and/or modify
|
|
--it under the terms of the GNU Lesser General Public License as published by
|
|
--the Free Software Foundation; either version 2.1 of the License, or
|
|
--(at your option) any later version.
|
|
--
|
|
--This program is distributed in the hope that it will be useful,
|
|
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
--GNU Lesser General Public License for more details.
|
|
--
|
|
--You should have received a copy of the GNU Lesser General Public License along
|
|
--with this program; if not, write to the Free Software Foundation, Inc.,
|
|
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
settingtypes = {}
|
|
|
|
-- A Setting type is a table with the following keys:
|
|
--
|
|
-- name: Identifier
|
|
-- readable_name: Readable title
|
|
-- type: Category
|
|
--
|
|
-- name = mod.name,
|
|
-- readable_name = mod.title,
|
|
-- level = 1,
|
|
-- type = "category", "int", "string", ""
|
|
-- }
|
|
|
|
|
|
local FILENAME = "settingtypes.txt"
|
|
|
|
local CHAR_CLASSES = {
|
|
SPACE = "[%s]",
|
|
VARIABLE = "[%w_%-%.]",
|
|
INTEGER = "[+-]?[%d]",
|
|
FLOAT = "[+-]?[%d%.]",
|
|
FLAGS = "[%w_%-%.,]",
|
|
}
|
|
|
|
local function flags_to_table(flags)
|
|
return flags:gsub("%s+", ""):split(",", true) -- Remove all spaces and split
|
|
end
|
|
|
|
-- returns error message, or nil
|
|
local function parse_setting_line(settings, line, read_all, base_level, allow_secure)
|
|
|
|
-- strip carriage returns (CR, /r)
|
|
line = line:gsub("\r", "")
|
|
|
|
-- comment
|
|
local comment_match = line:match("^#" .. CHAR_CLASSES.SPACE .. "*(.*)$")
|
|
if comment_match then
|
|
settings.current_comment[#settings.current_comment + 1] = comment_match
|
|
return
|
|
end
|
|
|
|
-- clear current_comment so only comments directly above a setting are bound to it
|
|
-- but keep a local reference to it for variables in the current line
|
|
local current_comment = settings.current_comment
|
|
settings.current_comment = {}
|
|
|
|
-- empty lines
|
|
if line:match("^" .. CHAR_CLASSES.SPACE .. "*$") then
|
|
return
|
|
end
|
|
|
|
-- category
|
|
local stars, category = line:match("^%[([%*]*)([^%]]+)%]$")
|
|
if category then
|
|
table.insert(settings, {
|
|
name = category,
|
|
level = stars:len() + base_level,
|
|
type = "category",
|
|
})
|
|
return
|
|
end
|
|
|
|
-- settings
|
|
local first_part, name, readable_name, setting_type = line:match("^"
|
|
-- this first capture group matches the whole first part,
|
|
-- so we can later strip it from the rest of the line
|
|
.. "("
|
|
.. "([" .. CHAR_CLASSES.VARIABLE .. "+)" -- variable name
|
|
.. CHAR_CLASSES.SPACE .. "*"
|
|
.. "%(([^%)]*)%)" -- readable name
|
|
.. CHAR_CLASSES.SPACE .. "*"
|
|
.. "(" .. CHAR_CLASSES.VARIABLE .. "+)" -- type
|
|
.. CHAR_CLASSES.SPACE .. "*"
|
|
.. ")")
|
|
|
|
if not first_part then
|
|
return "Invalid line"
|
|
end
|
|
|
|
if name:match("secure%.[.]*") and not allow_secure then
|
|
return "Tried to add \"secure.\" setting"
|
|
end
|
|
|
|
local requires = {}
|
|
local last_line = #current_comment > 0 and current_comment[#current_comment]:trim()
|
|
if last_line and last_line:lower():sub(1, 9) == "requires:" then
|
|
local parts = last_line:sub(10):split(",")
|
|
current_comment[#current_comment] = nil
|
|
|
|
for _, part in ipairs(parts) do
|
|
part = part:trim()
|
|
|
|
local value = true
|
|
if part:sub(1, 1) == "!" then
|
|
value = false
|
|
part = part:sub(2):trim()
|
|
end
|
|
|
|
requires[part] = value
|
|
end
|
|
end
|
|
|
|
if readable_name == "" then
|
|
readable_name = nil
|
|
end
|
|
local remaining_line = line:sub(first_part:len() + 1)
|
|
|
|
local comment = table.concat(current_comment, "\n"):trim()
|
|
|
|
if setting_type == "int" then
|
|
local default, min, max = remaining_line:match("^"
|
|
-- first int is required, the last 2 are optional
|
|
.. "(" .. CHAR_CLASSES.INTEGER .. "+)" .. CHAR_CLASSES.SPACE .. "*"
|
|
.. "(" .. CHAR_CLASSES.INTEGER .. "*)" .. CHAR_CLASSES.SPACE .. "*"
|
|
.. "(" .. CHAR_CLASSES.INTEGER .. "*)"
|
|
.. "$")
|
|
|
|
if not default or not tonumber(default) then
|
|
return "Invalid integer setting"
|
|
end
|
|
|
|
min = tonumber(min)
|
|
max = tonumber(max)
|
|
table.insert(settings, {
|
|
name = name,
|
|
readable_name = readable_name,
|
|
type = "int",
|
|
default = default,
|
|
min = min,
|
|
max = max,
|
|
requires = requires,
|
|
comment = comment,
|
|
})
|
|
return
|
|
end
|
|
|
|
if setting_type == "string"
|
|
or setting_type == "key" or setting_type == "v3f" then
|
|
local default = remaining_line:match("^(.*)$")
|
|
|
|
if not default then
|
|
return "Invalid string setting"
|
|
end
|
|
if setting_type == "key" and not read_all then
|
|
-- ignore key type if read_all is false
|
|
return
|
|
end
|
|
|
|
table.insert(settings, {
|
|
name = name,
|
|
readable_name = readable_name,
|
|
type = setting_type,
|
|
default = default,
|
|
requires = requires,
|
|
comment = comment,
|
|
})
|
|
return
|
|
end
|
|
|
|
if setting_type == "noise_params_2d"
|
|
or setting_type == "noise_params_3d" then
|
|
local default = remaining_line:match("^(.*)$")
|
|
|
|
if not default then
|
|
return "Invalid string setting"
|
|
end
|
|
|
|
local values = {}
|
|
local ti = 1
|
|
local index = 1
|
|
for match in default:gmatch("[+-]?[%d.-e]+") do -- All numeric characters
|
|
index = default:find("[+-]?[%d.-e]+", index) + match:len()
|
|
table.insert(values, match)
|
|
ti = ti + 1
|
|
if ti > 9 then
|
|
break
|
|
end
|
|
end
|
|
index = default:find("[^, ]", index)
|
|
local flags = ""
|
|
if index then
|
|
flags = default:sub(index)
|
|
default = default:sub(1, index - 3) -- Make sure no flags in single-line format
|
|
end
|
|
table.insert(values, flags)
|
|
|
|
table.insert(settings, {
|
|
name = name,
|
|
readable_name = readable_name,
|
|
type = setting_type,
|
|
default = default,
|
|
default_table = {
|
|
offset = values[1],
|
|
scale = values[2],
|
|
spread = {
|
|
x = values[3],
|
|
y = values[4],
|
|
z = values[5]
|
|
},
|
|
seed = values[6],
|
|
octaves = values[7],
|
|
persistence = values[8],
|
|
lacunarity = values[9],
|
|
flags = values[10]
|
|
},
|
|
values = values,
|
|
requires = requires,
|
|
comment = comment,
|
|
noise_params = true,
|
|
flags = flags_to_table("defaults,eased,absvalue")
|
|
})
|
|
return
|
|
end
|
|
|
|
if setting_type == "bool" then
|
|
if remaining_line ~= "false" and remaining_line ~= "true" then
|
|
return "Invalid boolean setting"
|
|
end
|
|
|
|
table.insert(settings, {
|
|
name = name,
|
|
readable_name = readable_name,
|
|
type = "bool",
|
|
default = remaining_line,
|
|
requires = requires,
|
|
comment = comment,
|
|
})
|
|
return
|
|
end
|
|
|
|
if setting_type == "float" then
|
|
local default, min, max = remaining_line:match("^"
|
|
-- first float is required, the last 2 are optional
|
|
.. "(" .. CHAR_CLASSES.FLOAT .. "+)" .. CHAR_CLASSES.SPACE .. "*"
|
|
.. "(" .. CHAR_CLASSES.FLOAT .. "*)" .. CHAR_CLASSES.SPACE .. "*"
|
|
.. "(" .. CHAR_CLASSES.FLOAT .. "*)"
|
|
.."$")
|
|
|
|
if not default or not tonumber(default) then
|
|
return "Invalid float setting"
|
|
end
|
|
|
|
min = tonumber(min)
|
|
max = tonumber(max)
|
|
table.insert(settings, {
|
|
name = name,
|
|
readable_name = readable_name,
|
|
type = "float",
|
|
default = default,
|
|
min = min,
|
|
max = max,
|
|
requires = requires,
|
|
comment = comment,
|
|
})
|
|
return
|
|
end
|
|
|
|
if setting_type == "enum" then
|
|
local default, values = remaining_line:match("^"
|
|
-- first value (default) may be empty (i.e. is optional)
|
|
.. "(" .. CHAR_CLASSES.VARIABLE .. "*)" .. CHAR_CLASSES.SPACE .. "*"
|
|
.. "(" .. CHAR_CLASSES.FLAGS .. "+)"
|
|
.. "$")
|
|
|
|
if not default or values == "" then
|
|
return "Invalid enum setting"
|
|
end
|
|
|
|
table.insert(settings, {
|
|
name = name,
|
|
readable_name = readable_name,
|
|
type = "enum",
|
|
default = default,
|
|
values = values:split(",", true),
|
|
requires = requires,
|
|
comment = comment,
|
|
})
|
|
return
|
|
end
|
|
|
|
if setting_type == "path" or setting_type == "filepath" then
|
|
local default = remaining_line:match("^(.*)$")
|
|
|
|
if not default then
|
|
return "Invalid path setting"
|
|
end
|
|
|
|
table.insert(settings, {
|
|
name = name,
|
|
readable_name = readable_name,
|
|
type = setting_type,
|
|
default = default,
|
|
requires = requires,
|
|
comment = comment,
|
|
})
|
|
return
|
|
end
|
|
|
|
if setting_type == "flags" then
|
|
local default, possible = remaining_line:match("^"
|
|
-- first value (default) may be empty (i.e. is optional)
|
|
-- this is implemented by making the last value optional, and
|
|
-- swapping them around if it turns out empty.
|
|
.. "(" .. CHAR_CLASSES.FLAGS .. "+)" .. CHAR_CLASSES.SPACE .. "*"
|
|
.. "(" .. CHAR_CLASSES.FLAGS .. "*)"
|
|
.. "$")
|
|
|
|
if not default or not possible then
|
|
return "Invalid flags setting"
|
|
end
|
|
|
|
if possible == "" then
|
|
possible = default
|
|
default = ""
|
|
end
|
|
|
|
table.insert(settings, {
|
|
name = name,
|
|
readable_name = readable_name,
|
|
type = "flags",
|
|
default = default,
|
|
possible = flags_to_table(possible),
|
|
requires = requires,
|
|
comment = comment,
|
|
})
|
|
return
|
|
end
|
|
|
|
return "Invalid setting type \"" .. setting_type .. "\""
|
|
end
|
|
|
|
local function parse_single_file(file, filepath, read_all, result, base_level, allow_secure)
|
|
-- store this helper variable in the table so it's easier to pass to parse_setting_line()
|
|
result.current_comment = {}
|
|
|
|
local line = file:read("*line")
|
|
while line do
|
|
local error_msg = parse_setting_line(result, line, read_all, base_level, allow_secure)
|
|
if error_msg then
|
|
core.log("error", error_msg .. " in " .. filepath .. " \"" .. line .. "\"")
|
|
end
|
|
line = file:read("*line")
|
|
end
|
|
|
|
result.current_comment = nil
|
|
end
|
|
|
|
|
|
--- Returns table of setting types
|
|
--
|
|
-- @param read_all Whether to ignore certain setting types for GUI or not
|
|
-- @parse_mods Whether to parse settingtypes.txt in mods and games
|
|
function settingtypes.parse_config_file(read_all, parse_mods)
|
|
local settings = {}
|
|
|
|
do
|
|
local builtin_path = core.get_builtin_path() .. FILENAME
|
|
local file = io.open(builtin_path, "r")
|
|
if not file then
|
|
core.log("error", "Can't load " .. FILENAME)
|
|
return settings
|
|
end
|
|
|
|
parse_single_file(file, builtin_path, read_all, settings, 0, true)
|
|
|
|
file:close()
|
|
end
|
|
|
|
if parse_mods then
|
|
-- Parse games
|
|
local games_category_initialized = false
|
|
for _, game in ipairs(pkgmgr.games) do
|
|
local path = game.path .. DIR_DELIM .. FILENAME
|
|
local file = io.open(path, "r")
|
|
if file then
|
|
if not games_category_initialized then
|
|
fgettext_ne("Content: Games") -- not used, but needed for xgettext
|
|
table.insert(settings, {
|
|
name = "Content: Games",
|
|
level = 0,
|
|
type = "category",
|
|
})
|
|
games_category_initialized = true
|
|
end
|
|
|
|
table.insert(settings, {
|
|
name = game.path,
|
|
readable_name = game.title,
|
|
level = 1,
|
|
type = "category",
|
|
})
|
|
|
|
parse_single_file(file, path, read_all, settings, 2, false)
|
|
|
|
file:close()
|
|
end
|
|
end
|
|
|
|
-- Parse mods
|
|
local mods_category_initialized = false
|
|
local mods = {}
|
|
pkgmgr.get_mods(core.get_modpath(), "mods", mods)
|
|
table.sort(mods, function(a, b) return a.name < b.name end)
|
|
|
|
for _, mod in ipairs(mods) do
|
|
local path = mod.path .. DIR_DELIM .. FILENAME
|
|
local file = io.open(path, "r")
|
|
if file then
|
|
if not mods_category_initialized then
|
|
fgettext_ne("Content: Mods") -- not used, but needed for xgettext
|
|
table.insert(settings, {
|
|
name = "Content: Mods",
|
|
level = 0,
|
|
type = "category",
|
|
})
|
|
mods_category_initialized = true
|
|
end
|
|
|
|
table.insert(settings, {
|
|
name = mod.path,
|
|
readable_name = mod.title or mod.name,
|
|
level = 1,
|
|
type = "category",
|
|
})
|
|
|
|
parse_single_file(file, path, read_all, settings, 2, false)
|
|
|
|
file:close()
|
|
end
|
|
end
|
|
|
|
-- Parse client mods
|
|
local clientmods_category_initialized = false
|
|
local clientmods = {}
|
|
pkgmgr.get_mods(core.get_clientmodpath(), "clientmods", clientmods)
|
|
for _, mod in ipairs(clientmods) do
|
|
local path = mod.path .. DIR_DELIM .. FILENAME
|
|
local file = io.open(path, "r")
|
|
if file then
|
|
if not clientmods_category_initialized then
|
|
fgettext_ne("Client Mods") -- not used, but needed for xgettext
|
|
table.insert(settings, {
|
|
name = "Client Mods",
|
|
level = 0,
|
|
type = "category",
|
|
})
|
|
clientmods_category_initialized = true
|
|
end
|
|
|
|
table.insert(settings, {
|
|
name = mod.path,
|
|
readable_name = mod.title or mod.name,
|
|
level = 1,
|
|
type = "category",
|
|
})
|
|
|
|
parse_single_file(file, path, read_all, settings, 2, false)
|
|
|
|
file:close()
|
|
end
|
|
end
|
|
end
|
|
|
|
return settings
|
|
end
|