cmdlib/main.lua

369 lines
14 KiB
Lua

modlib.mod.extend("cmdlib", "trie")
error_format = minetest.get_color_escape_sequence("#FF0000") .. "%s"
success_format = minetest.get_color_escape_sequence("#00FF00") .. "%s"
function scope_func(scope)
return function()
return false,
"Not a chatcommand, but a category. For a list of subcommands do /help " ..
scope .. "."
end
end
chatcommands = trie.new()
chatcommand_info = {}
chatcommand_info_by_mod = {}
format_error = function(str) return string.format(error_format, str) end
format_success = function(str) return string.format(success_format, str) end
chatcommand_help_built = false
function validate_privs(required, actual)
local missing, to_lose = {}, {}
for priv, expected in pairs(required) do
if expected then
if not actual[priv] then table.insert(missing, priv) end
elseif actual[priv] then
table.insert(to_lose, priv)
end
end
return missing, to_lose
end
function validate_privs_ipairs(required, forbidden, actual)
local missing, to_lose = {}, {}
for _, priv in ipairs(required) do
if not actual[priv] then table.insert(missing, priv) end
end
for _, priv in ipairs(forbidden) do
if actual[priv] then table.insert(to_lose, priv) end
end
return missing, to_lose
end
function sufficient_privs(required, playername)
local missing, to_lose = validate_privs(required, minetest.get_player_privs(playername))
local str
if not modlib.table.is_empty(missing) then
str = string.format("Missing privilege%s: ",
("s" and #missing > 1) or "") ..
table.concat(missing)
end
if not modlib.table.is_empty(to_lose) then
str = (str or "") ..
string.format("%srivilege%s which need to be lost: ",
(str and ", p") or "P",
("s" and #to_lose > 1) or "") ..
table.concat(to_lose)
end
return str
end
function build_param_parser(syntax)
local params = modlib.text.split_without_limit(syntax, " ")
local required_params, optional_params, list_param = {}, {}
local i = 1
while i <= #params and params[i]:sub(1, 1) == "<" and
params[i]:sub(params[i]:len()) == ">" do
table.insert(required_params, params[i]:sub(2, params[i]:len() - 1))
i = i + 1
end
while i <= #params and params[i]:sub(1, 1) == "[" and
params[i]:sub(params[i]:len()) == "]" do
table.insert(optional_params, params[i]:sub(2, params[i]:len() - 1))
i = i + 1
end
if i <= #params then
-- check for list param
if i == #params and params[i]:sub(1, 1) == "{" and
params[i]:sub(params[i]:len()) == "}" then
list_param = params[i]:sub(2, params[i]:len() - 1)
else
return -- Failure
end
end
local limit = #required_params + #optional_params
if list_param then limit = nil end
local minimum = #required_params
local paramlist = required_params
modlib.table.append(paramlist, optional_params)
return function(param)
local params = modlib.text.split(param, " ", limit)
for index, param in modlib.table.rpairs(params) do
if param == "" then table.remove(params, index) end
end
if #params < minimum then
return "Too few parameters given! At least " .. minimum .. " " ..
((minimum == 1 and "is") or "are") ..
" required. The following parameters are missing: " ..
table.concat({unpack(required_params, #params + 1)}, ", ")
end
local paramtable = {}
for index, name in ipairs(paramlist) do
paramtable[name] = params[index]
end
if list_param and #params > #paramlist then
paramtable[list_param] = {unpack(params, #paramlist + 1)}
end
return nil, paramtable
end
end
function build_func(def)
if not def.param_parser then
return function(invokername, params)
if def.privs then
local error = sufficient_privs(def.privs, invokername)
if error then return false, error end
end
return def.fnc(invokername, params)
end
end
return function(invokername, params)
if def.privs then
local error = sufficient_privs(def.privs, invokername)
if error then return false, error end
end
local error, params = def.param_parser(params)
if error then return false, error end
return def.fnc(invokername, params)
end
end
function register_chatcommand(name, def, override)
local definition = {
description = def.description ~= "" and def.description,
privs = def.privs and next(def.privs) and def.privs,
params = def.params ~= "" and def.params,
custom_syntax = def.custom_syntax,
implicit_call = def.implicit_call,
fnc = def.func or error("/" .. name .. ": No function given"),
mod = def.mod or minetest.get_current_modname()
}
if definition.params then definition.implicit_call = true end
if not definition.custom_syntax then
definition.param_parser = build_param_parser(definition.params or "")
end
definition.func = build_func(definition)
local scopes = modlib.text.split_without_limit(name, " ")
local function insert_info_by_mod(name)
local mod = definition.mod
if mod then
local mod_commands = chatcommand_info_by_mod[mod] or {}
mod_commands[name] = chatcommand_info[name]
chatcommand_info_by_mod[mod] = mod_commands
end
end
if #scopes == 1 then
chatcommand_info[name] = modlib.table.tablecopy(definition)
insert_info_by_mod(name)
trie.insert(chatcommands, name, definition, override)
else
local supercommand = trie.get(chatcommands, scopes[1])
local super_info = chatcommand_info[scopes[1]]
insert_info_by_mod(scopes[1])
if not supercommand then
supercommand = {
subcommands = trie.new(),
func = scope_func(scopes[1])
}
trie.insert(chatcommands, scopes[1], supercommand)
super_info = {subcommands = {}}
chatcommand_info[scopes[1]] = super_info
end
local inherited_privs = modlib.table.copy(supercommand.privs or {})
for i = 2, #scopes - 1 do
if not supercommand.subcommands then
supercommand.subcommands = trie.new()
end
if not super_info.subcommands then
super_info.subcommands = {}
end
local subcommand = {
subcommands = trie.new(),
privs = modlib.table.copy(inherited_privs),
func = scope_func(scopes[1])
}
local prevval = trie.insert(supercommand.subcommands, scopes[i],
subcommand)
modlib.table.add_all(inherited_privs,
(prevval and prevval.privs) or {})
supercommand = prevval or subcommand
super_info.subcommands[scopes[i]] =
super_info.subcommands[scopes[i]] or {subcommands = {}, privs = modlib.table.copy(inherited_privs)}
super_info = super_info.subcommands[scopes[i]]
end
modlib.table.add_all(inherited_privs, def.privs or {})
if not supercommand.subcommands then
supercommand.subcommands = trie.new()
end
if not super_info.subcommands then super_info.subcommands = {} end
definition.privs = next(inherited_privs) and inherited_privs
super_info.subcommands[scopes[#scopes]] = modlib.table.copy(definition)
trie.insert(supercommand.subcommands, scopes[#scopes], definition,
override)
end
if chatcommand_help_built then
-- TODO insert into help instead of rebuilding & use modlib's logging
minetest.log("warning", "Chatcommand registered after mods are loaded, rebuilding chatcommand info")
build_chatcommand_info()
end
end
local function name_comparator(info, name)
return modlib.table.default_comparator(info.name, name)
end
local binary_search_name = modlib.table.binary_search_comparator(name_comparator)
function unregister_chatcommand(name)
local function get(info, name)
return info[(#chatcommand_info ~= 0 and binary_search(info, name)) or name]
end
local scopes = modlib.text.split_without_limit(name, " ")
local super_info = chatcommand_info
local super_trie = chatcommands
local head_trie = chatcommands
local head_info, head_name = chatcommand_info, scopes[1]
for i = 2, #scopes do
super_info = get(super_info, scopes[i-1]).subcommands
super_trie = trie.get(super_trie, scopes[i-1]).subcommands
local reset_head = next(super_info, next(super_info)) or super_info.implicit_call
if reset_head then
head_info, head_name = super_info, scopes[i]
head_trie = super_trie
end
end
trie.remove(head_trie, head_name)
if #chatcommand_info ~= 0 then
head_name = binary_search_name(head_info, head_name)
end
head_info[head_name] = nil
end
local function wrap_line(text, max)
local res = {text}
while res[#res]:len() > max do
for i = max, 1, -1 do
if res[#res]:sub(i, i) == " " then
table.insert(res, res[#res]:sub(i + 1))
res[#res - 1] = res[#res - 1]:sub(1, i - 1)
break
end
end
end
return res
end
function wrap_text(text, max)
local lines = {}
for line in text:gmatch"[^\n]+" do
local wraps = wrap_line(line, max)
if #wraps > 1 and #lines > 1 then
table.insert(lines, "") -- add blank line
end
modlib.table.append(lines, wraps)
end
return lines
end
function handle_chat_message(sendername, message)
if message:sub(1, 1) == "/" then
local last_space, next_space = 2, message:find(" ")
local command_trie, command_name = chatcommands
local cmd, suggestion
repeat
next_space = next_space or message:len() + 1
command_name = message:sub(last_space, next_space - 1)
if command_name == "" and cmd and not cmd.params then
break
end
cmd, suggestion, _ = trie.search(command_trie, command_name)
if not cmd then
minetest.chat_send_player(sendername,
string.format(error_format,
"No such chatcommand. " ..
((suggestion and
'Did you mean "' ..
message:sub(1,
last_space -
1) ..
suggestion ..
'" ?') or "")))
return true
elseif cmd.subcommands and not cmd.implicit_call then
command_trie = cmd.subcommands
last_space, next_space = next_space + 1,
message:find(" ", next_space + 1)
else
last_space = next_space + 1
break
end
until next_space == message:len()
local params = message:sub(last_space)
local success, response = cmd.func(sendername, params)
if response then
if success == true then
minetest.chat_send_player(sendername, string.format(
success_format, response))
elseif success == false then
minetest.chat_send_player(sendername,
string.format(error_format, response))
else
minetest.chat_send_player(sendername, response)
end
end
return true
end
end
table.insert(core.registered_on_chat_messages, 1, handle_chat_message)
function build_info(chatcommands)
local new_info = {}
for name, def in pairs(chatcommands) do
local newdef = {}
newdef.name = name
local newprivs, newforbiddenprivs = {}, {}
for priv, val in pairs(def.privs or {}) do
if val then
table.insert(newprivs, priv)
else
table.insert(newforbiddenprivs, priv)
end
end
newdef.is_mod = def.is_mod
newdef.implicit_call = def.implicit_call
newdef.description = def.description or ""
newdef.descriptions = wrap_text(def.description or "", 100)
if #newdef.descriptions > 1 then
table.insert(newdef.descriptions, "") -- add blank line
end
newdef.privs = next(newprivs) and newprivs
newdef.forbidden_privs = next(newforbiddenprivs) and newforbiddenprivs
newdef.params = def.params or ""
newdef.subcommands = def.subcommands
table.insert(new_info, newdef)
if newdef.subcommands then
newdef.subcommands = build_info(newdef.subcommands)
end
end
table.sort(new_info, function(d1, d2) return d1.name < d2.name end)
return new_info
end
function build_chatcommand_info()
local chatcommand_info_by_mods = modlib.table.copy(chatcommand_info)
for mod, commands in pairs(chatcommand_info_by_mod) do
local count = modlib.table.count(commands)
if count >= 3 then
local mod_info = {is_mod = true, subcommands = {}}
chatcommand_info_by_mods["Mod: "..mod] = mod_info
for command, info in pairs(commands) do
mod_info.subcommands[command] = info
chatcommand_info_by_mods[command] = nil
end
end
end
chatcommand_help = build_info(chatcommand_info_by_mods)
end
minetest.register_on_mods_loaded(function()
modlib.mod.extend("cmdlib", "help")
build_chatcommand_info()
chatcommand_help_built = true
end)