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)