From c9bf85835f649c034c23ac06ee57aed713414752 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:59:12 +0000 Subject: [PATCH] Improvements for ExpCommands --- exp_commands/module/commands/authorities.lua | 9 +- exp_commands/module/commands/help.lua | 10 +- exp_commands/module/commands/ipc.lua | 3 +- exp_commands/module/commands/rcon.lua | 56 ++++--- exp_commands/module/commands/sudo.lua | 3 +- exp_commands/module/commands/types.lua | 22 +-- exp_commands/module/locale/en.cfg | 2 - exp_commands/module/module_exports.lua | 158 ++++++++++--------- exp_commands/module/search.lua | 16 +- 9 files changed, 142 insertions(+), 137 deletions(-) diff --git a/exp_commands/module/commands/authorities.lua b/exp_commands/module/commands/authorities.lua index 704151b9..b254fc75 100644 --- a/exp_commands/module/commands/authorities.lua +++ b/exp_commands/module/commands/authorities.lua @@ -9,13 +9,14 @@ The default permission authorities controlled by the flags: admin_only, system_o ]] local Storage = require("modules/exp_util/storage") -local Commands = require("modules/exp_commands") + +local Commands = require("modules/exp_commands") --- @class Commands local add, allow, deny = Commands.add_permission_authority, Commands.status.success, Commands.status.unauthorised local authorities = {} -local system_players = {} -local disabled_commands = {} +local system_players = {} --- @type table +local disabled_commands = {} --- @type table Storage.register({ system_players, disabled_commands, @@ -100,7 +101,7 @@ authorities.system_only = --- If Commands.disable was called then no one can use the command authorities.disabled = - add(function(_player, command) + add(function(player, command) if disabled_commands[command.name] then return deny{ "exp-commands-authorities.disabled" } else diff --git a/exp_commands/module/commands/help.lua b/exp_commands/module/commands/help.lua index af05f631..ed74665d 100644 --- a/exp_commands/module/commands/help.lua +++ b/exp_commands/module/commands/help.lua @@ -1,6 +1,5 @@ ---[[-- Command Module - Help +--[[-- Commands - Help Game command to list and search all registered commands in a nice format -@commands _system-ipc --- Get all messages related to banning a player /commands ban @@ -13,13 +12,16 @@ local Commands = require("modules/exp_commands") local PAGE_SIZE = 5 -local search_cache = {} +--- @alias ResultsPage LocalisedString[] +--- @class HelpCacheEntry: { keyword: string, pages: ResultsPage[], found: number } + +local search_cache = {} --- @type table Storage.register(search_cache, function(tbl) search_cache = tbl end) --- Format commands into a strings across multiple pages ---- @param commands { [string]: Commands.Command } The commands to split into pages +--- @param commands table The commands to split into pages --- @param page_size number The number of requests to show per page --- @return LocalisedString[][], number local function format_as_pages(commands, page_size) diff --git a/exp_commands/module/commands/ipc.lua b/exp_commands/module/commands/ipc.lua index e2649b7a..b4799405 100644 --- a/exp_commands/module/commands/ipc.lua +++ b/exp_commands/module/commands/ipc.lua @@ -1,6 +1,5 @@ ---[[-- Command Module - IPC +--[[-- Commands - IPC System command which sends an object to the clustorio api, should be used for debugging / echo commands -@commands _system-ipc --- Send a message on your custom channel, message is a json string /_ipc myChannel { "myProperty": "foo", "playerName": "Cooldude2606" } diff --git a/exp_commands/module/commands/rcon.lua b/exp_commands/module/commands/rcon.lua index 69d1cdea..2cdce0b4 100644 --- a/exp_commands/module/commands/rcon.lua +++ b/exp_commands/module/commands/rcon.lua @@ -1,6 +1,5 @@ ---[[-- Command Module - Rcon +--[[-- Commands - Rcon System command which runs arbitrary code within a custom (not sandboxed) environment -@commands _system-rcon --- Get the names of all online players, using rcon /_system-rcon local names = {}; for index, player in pairs(game.connected_player) do names[index] = player.name end; return names; @@ -12,53 +11,60 @@ System command which runs arbitrary code within a custom (not sandboxed) environ local ExpUtil = require("modules/exp_util") local Async = require("modules/exp_util/async") local Storage = require("modules/exp_util/storage") -local Commands = require("modules/exp_commands") local Clustorio = require("modules/clusterio/api") -local rcon_env = {} -local rcon_statics = {} -local rcon_callbacks = {} -setmetatable(rcon_statics, { __index = _G }) -setmetatable(rcon_env, { __index = rcon_statics }) +local Commands = require("modules/exp_commands") --- @class Commands + +local rcon_env = {} --- @type table +local rcon_static = {} --- @type table +local rcon_dynamic = {} --- @type table +setmetatable(rcon_static, { __index = _G }) +setmetatable(rcon_env, { __index = rcon_static }) --- Some common static values which can be added now --- @diagnostic disable: name-style-check -rcon_statics.Async = Async -rcon_statics.ExpUtil = ExpUtil -rcon_statics.Commands = Commands -rcon_statics.Clustorio = Clustorio -rcon_statics.print = Commands.print -rcon_statics.ipc = Clustorio.send_json +rcon_static.Async = Async +rcon_static.ExpUtil = ExpUtil +rcon_static.Commands = Commands +rcon_static.Clustorio = Clustorio +rcon_static.print = Commands.print +rcon_static.ipc = Clustorio.send_json --- @diagnostic enable: name-style-check --- Some common callback values which are useful when a player uses the command -function rcon_callbacks.player(player) return player end +--- @alias ExpCommand.RconDynamic fun(player: LuaPlayer?): any -function rcon_callbacks.surface(player) return player and player.surface end +function rcon_dynamic.player(player) return player end -function rcon_callbacks.force(player) return player and player.force end +function rcon_dynamic.surface(player) return player and player.surface end -function rcon_callbacks.position(player) return player and player.position end +function rcon_dynamic.force(player) return player and player.force end -function rcon_callbacks.entity(player) return player and player.selected end +function rcon_dynamic.position(player) return player and player.position end -function rcon_callbacks.tile(player) return player and player.surface.get_tile(player.position) end +function rcon_dynamic.entity(player) return player and player.selected end + +function rcon_dynamic.tile(player) return player and player.surface.get_tile(player.position.x, player.position.y) end --- The rcon env is saved between command runs to prevent desyncs Storage.register(rcon_env, function(tbl) - rcon_env = setmetatable(tbl, { __index = rcon_statics }) + rcon_env = setmetatable(tbl, { __index = rcon_static }) end) --- Static values can be added to the rcon env which are not stored in global such as modules +--- @param name string Name of the value as it will appear in the rcon environment +--- @param value any Value it is have function Commands.add_rcon_static(name, value) ExpUtil.assert_not_runtime() - rcon_statics[name] = value + rcon_static[name] = value end --- Callback values can be added to the rcon env, these are called on each invocation and should return one value -function Commands.add_rcon_callback(name, callback) +--- @param name string Name of the value as it will appear in the rcon environment +--- @param callback ExpCommand.RconDynamic Callback called to get the current value +function Commands.add_rcon_dynamic(name, callback) ExpUtil.assert_not_runtime() - rcon_callbacks[name] = callback + rcon_dynamic[name] = callback end Commands.new("_rcon", { "exp-commands_rcon.description" }) @@ -70,7 +76,7 @@ Commands.new("_rcon", { "exp-commands_rcon.description" }) -- Construct the environment the command will run within local env = setmetatable({}, { __index = rcon_env, __newindex = rcon_env }) - for name, callback in pairs(rcon_callbacks) do + for name, callback in pairs(rcon_dynamic) do local _, rtn = pcall(callback, player.index > 0 and player or nil) rawset(env, name, rtn) end diff --git a/exp_commands/module/commands/sudo.lua b/exp_commands/module/commands/sudo.lua index 077eb491..938fb34c 100644 --- a/exp_commands/module/commands/sudo.lua +++ b/exp_commands/module/commands/sudo.lua @@ -1,6 +1,5 @@ ---[[-- Command Module - Sudo +--[[-- Commands - Sudo System command to execute a command as another player using their permissions (except for permissions group actions) -@commands _system-sudo --- Run the example command as another player -- As Cooldude2606: /repeat 5 diff --git a/exp_commands/module/commands/types.lua b/exp_commands/module/commands/types.lua index 644ec409..451ef0a0 100644 --- a/exp_commands/module/commands/types.lua +++ b/exp_commands/module/commands/types.lua @@ -25,10 +25,9 @@ local Commands = require("modules/exp_commands") local add, parse = Commands.add_data_type, Commands.parse_input local valid, invalid = Commands.status.success, Commands.status.invalid_input -local types = {} +local types = {} --- @class Commands._types --- A boolean value where true is one of: yes, y, true, 1 ---- @type Commands.InputParser types.boolean = add("boolean", function(input) input = input:lower() @@ -43,14 +42,12 @@ types.boolean = end) --- A string, validation does nothing but it is a requirement ---- @type Commands.InputParser types.string = add("string", function(input) return valid(input) end) --- A string from a set of options, takes one argument which is an array of options ---- @type Commands.InputParserFactory types.enum = add("enum", function(options) --- @cast options string[] @@ -65,10 +62,9 @@ types.enum = end) --- A string which is the key of a table, takes one argument which is an map of string keys to values ---- @type Commands.InputParserFactory types.key_of = add("key_of", function(map) - --- @cast map { [string]: any } + --- @cast map table return function(input) local option = auto_complete(map, input, true) if option == nil then @@ -80,7 +76,6 @@ types.key_of = end) --- A string with a maximum length, takes one argument which is the maximum length of a string ---- @type Commands.InputParserFactory types.string_max_length = add("string_max_length", function(maximum) --- @cast maximum number @@ -94,7 +89,6 @@ types.string_max_length = end) --- A number ---- @type Commands.InputParser types.number = add("number", function(input) local number = tonumber(input) @@ -106,7 +100,6 @@ types.number = end) --- An integer, number which has been floored ---- @type Commands.InputParser types.integer = add("integer", function(input) local number = tonumber(input) @@ -118,7 +111,6 @@ types.integer = end) --- A number in a given inclusive range ---- @type Commands.InputParserFactory types.number_range = add("number_range", function(minimum, maximum) --- @cast minimum number @@ -137,7 +129,6 @@ types.number_range = end) --- An integer in a given inclusive range ---- @type Commands.InputParserFactory types.integer_range = add("integer_range", function(minimum, maximum) --- @cast minimum number @@ -156,7 +147,6 @@ types.integer_range = end) --- A player who has joined the game at least once ---- @type Commands.InputParser types.player = add("player", function(input) local player = game.get_player(input) @@ -168,7 +158,6 @@ types.player = end) --- A player who is online ---- @type Commands.InputParser types.player_online = add("player_online", function(input, player) local success, status, result = parse(input, player, Commands.types.player) @@ -183,8 +172,7 @@ types.player_online = end) --- A player who is online and alive ---- @type Commands.InputParser -types.player_online = +types.player_alive = add("player_alive", function(input, player) local success, status, result = parse(input, player, Commands.types.player_online) --- @cast result LuaPlayer @@ -198,7 +186,6 @@ types.player_online = end) --- A force within the game ---- @type Commands.InputParser types.force = add("force", function(input) local force = game.forces[input] @@ -210,7 +197,6 @@ types.force = end) --- A surface within the game ---- @type Commands.InputParser types.surface = add("surface", function(input) local surface = game.surfaces[input] @@ -222,7 +208,6 @@ types.surface = end) --- A planet within the game ---- @type Commands.InputParser types.planet = add("planet", function(input) local surface = game.planets[input] @@ -234,7 +219,6 @@ types.planet = end) --- A name of a color from the predefined list, too many colours to use string-key ---- @type Commands.InputParser types.color = add("color", function(input) local color = auto_complete(Commands.color, input, true) diff --git a/exp_commands/module/locale/en.cfg b/exp_commands/module/locale/en.cfg index 37578099..5691ca4c 100644 --- a/exp_commands/module/locale/en.cfg +++ b/exp_commands/module/locale/en.cfg @@ -1,5 +1,3 @@ -color-tag=[color=__1__]__2__[/color] - [exp-commands] help=__1__- __2____3__ aliases=\n Aliaies: __1__ diff --git a/exp_commands/module/module_exports.lua b/exp_commands/module/module_exports.lua index a84c2ea7..a7245f04 100644 --- a/exp_commands/module/module_exports.lua +++ b/exp_commands/module/module_exports.lua @@ -57,6 +57,7 @@ end) local ExpUtil = require("modules/exp_util") local Search = require("modules/exp_commands/search") +--- @class Commands local Commands = { color = ExpUtil.color, format_rich_text_color = ExpUtil.format_rich_text_color, @@ -64,11 +65,9 @@ local Commands = { format_player_name = ExpUtil.format_player_name, format_player_name_locale = ExpUtil.format_player_name_locale, - types = {}, --- @type { [string]: Commands.InputParser | Commands.InputParserFactory } Stores all input parsers and validators for different data types - registered_commands = {}, --- @type { [string]: Commands.ExpCommand } Stores a reference to all registered commands + registered_commands = {}, --- @type table Stores a reference to all registered commands permission_authorities = {}, --- @type Commands.PermissionAuthority[] Stores a reference to all active permission authorities - status = {}, -- Contains the different status values a command can return - + --- @package Stores the event handlers events = { [defines.events.on_player_locale_changed] = Search.on_player_locale_changed, @@ -77,6 +76,14 @@ local Commands = { }, } +--- @class Commands._status: table +--- Contains the different status values a command can return +Commands.status = {} + +--- @class Commands._types: table +--- Stores all input parsers and validators for different data types +Commands.types = {} + --- @package function Commands.on_init() Search.prepare(Commands.registered_commands) end @@ -91,7 +98,7 @@ end --- @class Commands.Argument --- @field name string The name of the argument ---- @field description LocalisedString The description of the argument +--- @field description LocalisedString? The description of the argument --- @field input_parser Commands.InputParser The input parser for the argument --- @field optional boolean True when the argument is optional --- @field default any? The default value of the argument @@ -135,15 +142,17 @@ Commands.server = setmetatable({ show_on_map = false, valid = true, object_name = "LuaPlayer", + print = rcon.print, }, { + -- To prevent unnecessary logging Commands.error is called here and error is filtered by command_callback __index = function(_, key) if key == "__self" or type(key) == "number" then return nil end - Commands.error("Command does not support rcon usage, requires reading player." .. key) - error("Command does not support rcon usage, requires reading player." .. key) + Commands.error("Command does not support rcon usage, requires LuaPlayer." .. key) + error("Command does not support rcon usage, requires LuaPlayer." .. key) end, __newindex = function(_, key) - Commands.error("Command does not support rcon usage, requires reading player." .. key) - error("Command does not support rcon usage, requires setting player." .. key) + Commands.error("Command does not support rcon usage, requires LuaPlayer." .. key) + error("Command does not support rcon usage, requires LuaPlayer." .. key) end, }) @@ -155,7 +164,6 @@ Commands.server = setmetatable({ --- Used to signal success from a command, data type parser, or permission authority --- @param msg LocalisedString? An optional message to be included when a command completes (only has an effect in command callbacks) --- @return Commands.Status, LocalisedString # Should be returned directly without modification ---- @type Commands.Status function Commands.status.success(msg) return Commands.status.success, msg or { "exp-commands.success" } end @@ -164,7 +172,6 @@ end --- For data type parsers and permission authority, an error return will prevent the command from being executed --- @param msg LocalisedString? An optional error message to be included in the output, a generic message is used if not provided --- @return Commands.Status, LocalisedString # Should be returned directly without modification ---- @type Commands.Status function Commands.status.error(msg) return Commands.status.error, { "exp-commands.error", msg or { "exp-commands.error-default" } } end @@ -173,7 +180,6 @@ end --- For permission authorities, an unauthorised return will prevent the command from being executed --- @param msg LocalisedString? An optional error message to be included in the output, a generic message is used if not provided --- @return Commands.Status, LocalisedString # Should be returned directly without modification ---- @type Commands.Status function Commands.status.unauthorised(msg) return Commands.status.unauthorised, msg or { "exp-commands.unauthorized", msg or { "exp-commands.unauthorized-default" } } end @@ -182,7 +188,6 @@ end --- For data type parsers, an invalid_input return will prevent the command from being executed --- @param msg LocalisedString? An optional error message to be included in the output, a generic message is used if not provided --- @return Commands.Status, LocalisedString # Should be returned directly without modification ---- @type Commands.Status function Commands.status.invalid_input(msg) return Commands.status.invalid_input, msg or { "exp-commands.invalid-input" } end @@ -191,12 +196,11 @@ end --- @param msg LocalisedString A message detailing the error which has occurred, will be logged and outputted --- @return Commands.Status, LocalisedString # Should be returned directly without modification --- @package ---- @type Commands.Status function Commands.status.internal_error(msg) return Commands.status.internal_error, { "exp-commands.internal-error", msg } end ---- @type { [Commands.Status]: string } +--- @type table local valid_command_status = {} -- Hashmap lookup for testing if a status is valid for name, status in pairs(Commands.status) do valid_command_status[status] = name @@ -205,7 +209,7 @@ end --- Permission Authority. -- Functions that control who can use commands ---- @alias Commands.PermissionAuthority fun(player: LuaPlayer, command: Commands.ExpCommand): boolean | Commands.Status, LocalisedString? +--- @alias Commands.PermissionAuthority fun(player: LuaPlayer, command: Commands.ExpCommand): boolean|Commands.Status, LocalisedString? --- Add a permission authority, a permission authority is a function which provides access control for commands, multiple can be active at once --- When multiple are active, all authorities must give permission for the command to execute, if any deny access then the command is not ran @@ -241,7 +245,7 @@ end --- @param player LuaPlayer? The player to test the permission of, nil represents the server and always returns true --- @param command Commands.ExpCommand The command the player is attempting to use --- @return boolean # True if the player has permission to use the command ---- @return LocalisedString # When permission is denied, this is the reason permission was denied +--- @return LocalisedString? # When permission is denied, this is the reason permission was denied function Commands.player_has_permission(player, command) if player == nil or player == Commands.server then return true end @@ -257,9 +261,7 @@ function Commands.player_has_permission(player, command) return false, msg end else - local class_name = ExpUtil.get_class_name(status) - local _, rtn_msg = Commands.status.internal_error("Permission authority returned unexpected value: " .. class_name) - return false, rtn_msg + error("Permission authority returned unexpected value: " .. ExpUtil.get_class_name(status)) end end @@ -269,16 +271,14 @@ end --- Data Type Parsing. -- Functions that parse and validate player input ---- @generic T ---- @alias Commands.InputParser (fun(input: string, player: LuaPlayer): T) | (fun(input: string, player: LuaPlayer): Commands.Status, LocalisedString | T) +--- @alias Commands.InputParser fun(input: string, player: LuaPlayer): Commands.Status, (T | LocalisedString) ---- @generic T ---- @alias Commands.InputParserFactory fun(...: any): Commands.InputParser +--- @alias Commands.InputParserFactory fun(...: any): Commands.InputParser --- Add a new input parser to the command library, this method validates that it does not already exist --- @generic T : Commands.InputParser | Commands.InputParserFactory --- @param data_type string The name of the data type the input parser reads in and validates, becomes a key of Commands.types ---- @param input_parser `T` The function used to parse and validate the data type +--- @param input_parser T The function used to parse and validate the data type --- @return T # The function which was provided as the second argument function Commands.add_data_type(data_type, input_parser) if Commands.types[data_type] then @@ -310,33 +310,25 @@ end --- @return Commands.Status status, T | LocalisedString result # If success is false then Remaining values should be returned directly without modification function Commands.parse_input(input, player, input_parser) local status, status_msg = input_parser(input, player) - if status == nil then + if status == nil or not valid_command_status[status] then local data_type = table.get_key(Commands.types, input_parser) or ExpUtil.get_function_name(input_parser, true) - local rtn_status, rtn_msg = Commands.status.internal_error("Parser for data type \"" .. data_type .. "\" returned a nil value") - return false, rtn_status, rtn_msg - elseif valid_command_status[status] then - if status ~= Commands.status.success then - return false, status, status_msg - else - return true, status, status_msg -- status_msg is the parsed data - end - else - return true, Commands.status.success, status -- status is the parsed data + error("Parser for data type \"" .. data_type .. "\" did not return a valid status got: " .. ExpUtil.get_class_name(status)) end + return status == Commands.status.success, status, status_msg end --- List and Search -- Functions used to list and search for commands --- Returns a list of all registered custom commands ---- @return { [string]: Commands.ExpCommand } # A dictionary of commands +--- @return table # A dictionary of commands function Commands.list_all() return Commands.registered_commands end --- Returns a list of all registered custom commands which the given player has permission to use --- @param player LuaPlayer? The player to get the command of, nil represents the server but list_all should be used ---- @return { [string]: Commands.ExpCommand } # A dictionary of commands +--- @return table # A dictionary of commands function Commands.list_for_player(player) local rtn = {} @@ -351,7 +343,7 @@ end --- Searches all custom commands and game commands for the given keyword --- @param keyword string The keyword to search for ---- @return { [string]: Commands.Command } # A dictionary of commands +--- @return table # A dictionary of commands function Commands.search_all(keyword) return Search.search_commands(keyword, Commands.list_all(), "en") end @@ -359,7 +351,7 @@ end --- Searches custom commands allowed for this player and all game commands for the given keyword --- @param keyword string The keyword to search for --- @param player LuaPlayer? The player to search the commands of, nil represents server but search_all should be used ---- @return { [string]: Commands.Command } # A dictionary of commands +--- @return table # A dictionary of commands function Commands.search_for_player(keyword, player) return Search.search_commands(keyword, Commands.list_for_player(player), player and player.locale) end @@ -368,7 +360,13 @@ end -- Prints output to the player or rcon connection local print_format_options = { max_line_count = 20 } -local print_default_settings = { sound_path = "utility/scenario_message" } +local print_settings_default = { sound_path = "utility/scenario_message", color = ExpUtil.color.white } +local print_settings_error = { sound_path = "utility/wire_pickup", color = ExpUtil.color.orange_red } + +Commands.print_settings = { + default = print_settings_default, + error = print_settings_error, +} --- Print a message to the user of a command, accepts any value and will print in a readable and safe format --- @param message any The message / value to be printed @@ -379,9 +377,9 @@ function Commands.print(message, settings) rcon.print(ExpUtil.format_any(message)) else if not settings then - settings = print_default_settings + settings = print_settings_default elseif not settings.sound_path then - settings.sound_path = print_default_settings.sound_path + settings.sound_path = print_settings_default.sound_path end player.print(ExpUtil.format_any(message, print_format_options), settings) end @@ -390,18 +388,21 @@ end --- Print an error message to the user of a command, accepts any value and will print in a readable and safe format --- @param message any The message / value to be printed function Commands.error(message) - Commands.print(message, { - color = ExpUtil.color.orange_red, - sound_path = "utility/wire_pickup", - }) + Commands.print(message, print_settings_error) end --- Command Prototype -- The prototype definition for command objects +local function assert_command_mutable(command) + if not Commands.registered_commands[command.name] then + error("Command cannot be modified after being registered.", 3) + end +end + --- Returns a new command object, this will not register the command but act as a way to start construction --- @param name string The name of the command as it will be registered later ---- @param description LocalisedString The description of the command displayed in the help message +--- @param description LocalisedString? The description of the command displayed in the help message --- @return Commands.ExpCommand function Commands.new(name, description) ExpUtil.assert_argument_type(name, "string", 1, "name") @@ -411,7 +412,7 @@ function Commands.new(name, description) return setmetatable({ name = name, - description = description, + description = description or "", help_text = description, -- Will be replaced in command:register callback = default_command_callback, -- Will be replaced in command:register defined_at = ExpUtil.safe_file_path(2), @@ -426,10 +427,11 @@ end --- Add a new required argument to the command of the given data type --- @param name string The name of the argument being added ---- @param description LocalisedString The description of the argument being added +--- @param description LocalisedString? The description of the argument being added --- @param input_parser Commands.InputParser The input parser to be used for the argument --- @return Commands.ExpCommand function Commands._prototype:argument(name, description, input_parser) + assert_command_mutable(self) if self.min_arg_count ~= self.max_arg_count then error("Can not have required arguments after optional arguments", 2) end @@ -446,10 +448,11 @@ end --- Add a new optional argument to the command of the given data type --- @param name string The name of the argument being added ---- @param description LocalisedString The description of the argument being added +--- @param description LocalisedString? The description of the argument being added --- @param input_parser Commands.InputParser The input parser to be used for the argument --- @return Commands.ExpCommand function Commands._prototype:optional(name, description, input_parser) + assert_command_mutable(self) self.max_arg_count = self.max_arg_count + 1 self.arguments[#self.arguments + 1] = { name = name, @@ -461,9 +464,10 @@ function Commands._prototype:optional(name, description, input_parser) end --- Set the defaults for optional arguments, any not provided will have their value as nil ---- @param defaults table The default values for the optional arguments, the key is the name of the argument +--- @param defaults table The default values for the optional arguments, the key is the name of the argument --- @return Commands.ExpCommand function Commands._prototype:defaults(defaults) + assert_command_mutable(self) local matched = {} for _, argument in ipairs(self.arguments) do if defaults[argument.name] then @@ -489,6 +493,7 @@ end --- @param flags table An array of strings or a dictionary of flag names and values, when an array is used the flags values are set to true --- @return Commands.ExpCommand function Commands._prototype:add_flags(flags) + assert_command_mutable(self) for name, value in pairs(flags) do if type(name) == "number" then self.flags[value] = true @@ -504,6 +509,7 @@ end --- @param aliases string[] An array of string names to use as aliases to this command --- @return Commands.ExpCommand function Commands._prototype:add_aliases(aliases) + assert_command_mutable(self) local start_index = #self.aliases for index, alias in ipairs(aliases) do self.aliases[start_index + index] = alias @@ -515,32 +521,41 @@ end --- Enable concatenation of all arguments after the last, this should be used for user provided reason text --- @return Commands.ExpCommand function Commands._prototype:enable_auto_concatenation() + assert_command_mutable(self) self.auto_concat = true return self end --- Register the command to the game with the given callback, this must be the final step as the object becomes immutable afterwards --- @param callback Commands.Callback The function which is called to perform the command action +--- @return Commands.ExpCommand function Commands._prototype:register(callback) + assert_command_mutable(self) Commands.registered_commands[self.name] = self self.callback = callback -- Generates a description to be used local argument_names = { "" } --- @type LocalisedString - local argument_help_text = { "", "" } --- @type LocalisedString - local help_text = { "exp-commands.help", argument_names, self.description, argument_help_text } --- @type LocalisedString - self.help_text = help_text + local argument_verbose = { "" } --- @type LocalisedString + self.help_text = { "exp-commands.help", argument_names, self.description, argument_verbose } --- @type LocalisedString if next(self.aliases) then - argument_help_text[2] = { "exp-commands.aliases", table.concat(self.aliases, ", ") } + argument_verbose[2] = { "exp-commands.aliases", table.concat(self.aliases, ", ") } end + local verbose_index = #argument_verbose for index, argument in pairs(self.arguments) do if argument.optional then - argument_names[index + 2] = { "exp-commands.optional", argument.name } - argument_help_text[index + 2] = { "exp-commands.optional-verbose", argument.name, argument.description } + argument_names[index + 1] = { "exp-commands.optional", argument.name } + if argument.description and argument.description ~= "" then + verbose_index = verbose_index + 1 + argument_verbose[verbose_index] = { "exp-commands.optional-verbose", argument.name, argument.description } + end else - argument_names[index + 2] = { "exp-commands.argument", argument.name } - argument_help_text[index + 2] = { "exp-commands.argument-verbose", argument.name, argument.description } + argument_names[index + 1] = { "exp-commands.argument", argument.name } + if argument.description and argument.description ~= "" then + verbose_index = verbose_index + 1 + argument_verbose[verbose_index] = { "exp-commands.argument-verbose", argument.name, argument.description } + end end end @@ -565,6 +580,8 @@ function Commands._prototype:register(callback) for _, alias in ipairs(self.aliases) do commands.add_command(alias, self.help_text, command_callback) end + + return self end --- Command Runner @@ -633,7 +650,6 @@ end --- Internal event handler for the command event --- @param event CustomCommandData --- @return nil ---- @package function Commands._event_handler(event) local command = Commands.registered_commands[event.name] if command == nil then @@ -695,20 +711,20 @@ function Commands._event_handler(event) end end - -- Run the command, dont need xpcall here because errors are caught in command_callback + -- Run the command, don't need xpcall here because errors are caught in command_callback local status, status_msg = command.callback(player, table.unpack(arguments)) - if status and valid_command_status[status] then - if status ~= Commands.status.success then - log_command("Custom Error", command, player, event.parameter, status_msg) - return Commands.error(status_msg) - else - log_command("Command Ran", command, player, event.parameter) - return Commands.print(status_msg) - end - else + if status == nil then log_command("Command Ran", command, player, event.parameter) local _, msg = Commands.status.success() return Commands.print(msg) + elseif not valid_command_status[status] then + error("Command \"" .. command.name .. "\" did not return a valid status got: " .. ExpUtil.get_class_name(status)) + elseif status ~= Commands.status.success then + log_command("Custom Error", command, player, event.parameter, status_msg) + return Commands.error(status_msg) + else + log_command("Command Ran", command, player, event.parameter) + return Commands.print(status_msg) end end diff --git a/exp_commands/module/search.lua b/exp_commands/module/search.lua index 2fdb07fa..0f1869f7 100644 --- a/exp_commands/module/search.lua +++ b/exp_commands/module/search.lua @@ -3,8 +3,8 @@ local Search = {} local Storage = require("modules/exp_util/storage") --- Setup the storage to contain the pending translations and the completed ones -local pending = {} --- @type { [uint]: { [1]: string, [2]: string }? } -local translations = {} --- @type { [string]: { [string]: string } } +local pending = {} --- @type { [1]: string, [2]: string }[] +local translations = {} --- @type table> Storage.register({ pending, translations, @@ -14,13 +14,13 @@ Storage.register({ end) local command_names = {} --- @type string[] -local command_objects = {} --- @type { [string]: Commands.Command } +local command_objects = {} --- @type table local required_translations = {} --- @type LocalisedString[] --- Gets the descriptions of all commands, not including their aliases ---- @param custom_commands { [string]: Commands.ExpCommand } The complete list of registered custom commands +--- @param custom_commands table The complete list of registered custom commands function Search.prepare(custom_commands) - local known_aliases = {} --- @type { [string]: string } + local known_aliases = {} --- @type table for name, command in pairs(custom_commands) do for _, alias in ipairs(command.aliases) do known_aliases[alias] = name @@ -85,11 +85,11 @@ end --- Searches all game commands and the provided custom commands for the given keyword --- @param keyword string The keyword to search for ---- @param custom_commands { [string]: Commands.ExpCommand } A dictionary of commands to search +--- @param custom_commands table A dictionary of commands to search --- @param locale string? The local to search, default is english ("en") ---- @return { [string]: Commands.Command } # A dictionary of commands +--- @return table # A dictionary of commands function Search.search_commands(keyword, custom_commands, locale) - local rtn = {} --- @type { [string]: Commands.Command } + local rtn = {} --- @type table keyword = keyword:lower() locale = locale or "en"