diff --git a/expcore/commands.lua b/expcore/commands.lua index 42b4bcbe..7b7b0c77 100644 --- a/expcore/commands.lua +++ b/expcore/commands.lua @@ -3,19 +3,17 @@ @core Commands @alias Commands -@usage--- Full code example, see below for explaination +@usage--- Full code example, see below for explanation Commands.new_command('repeat-name', 'Will repeat you name a number of times in chat.') -:add_param('repeat-count', false, 'number-range-int', 1, 5) -- required int in range 1 to 5 inclusive +:add_param('repeat-count', 'number-range-int', 1, 5) -- required int in range 1 to 5 inclusive :add_param('smiley', true, function(input, player, reject) -- optional boolean default false if not input then return end - if input:lower() == 'true' or input:lower() == 'yes' then - return true - else - return false - end + input = input:lower() + if input == 'true' or input == 'yes' then return true end + return false end) -:set_defaults{ smiley=false } -:set_flag('admin_only', true) -- command is admin only +:set_defaults{ smiley = false } +:set_flag('admin_only') -- command is admin only :add_alias('name', 'rname') -- allow alias: name and rname :register(function(player, repeat_count, smiley, raw) game.print(player.name..' used a command with input: '..raw) @@ -30,89 +28,85 @@ end) end end) -@usage--- Example Command: --- How for the fun part making the commands, the commands can be set up with any number of params and flags that you want, --- you can add aliases for the commands and set default values for optional params and of course register your command callback --- in our example we will just have a command that will repeat the users name in chat X amount of times and only allow admins to use it. +@usage--- Example Command Explanation: +-- Making commands basics, the commands can be set up with any number of params and flags that you want, +-- you can add aliases for the commands and set default values for optional params and of course register your command callback. +-- In our example we will have a command that will repeat the users name in chat X amount of times and only allow admins to use it. --- First we create the new command, nb this will not register the command to the game this is done at the end, we will call --- the command "repeat-name" and set the help message as follows: +-- First we create the new command, note this will not register the command to the game this is done at the end. +-- We will call the command "repeat-name" and set the help message as follows: Commands.new_command('repeat-name', 'Will repeat you name a number of times in chat.') --- Now for our first param we will call "repeat-count" and it will be a required value between 1 and 5 inclusive: -:add_param('repeat-count', false, 'number-range-int', 1, 5) +-- Now for our first param, we have named it "repeat-count" and it will be a required value, between 1 and 5 inclusive: +-- By using "number-range-int" we are saying to use this parser to convert our input text, common ones exist in config.expcore.command_general_parse +:add_param('repeat-count', 'number-range-int', 1, 5) --- Our second param we need a custom parse for but we have not defined it, this is an option for when it is unlikely for --- any other command to use the same input type; however in our case it will just be a boolean which should be noted as being --- included in the general command parse config. As for the param its self it will be called "smiley" and will be optional with --- a default value of false: +-- Our second param needs a custom parser, meaning it isnt defined with add_parser, this is an option for when it is unlikely for +-- any other command to use the same input type. In the example it is a boolean type and we are just showing it here as part of the example. +-- As for the param its self it will be called "smiley" and will be optional with a default value of false: :add_param('smiley', true, function(input, player, reject) - -- since it is optional the input can be nil, in which case we just return + -- Since it is optional the input can be nil, in which case we just return if not input then return end - -- if it is not nil then we check for a truthy value - if input:lower() == 'true' or input:lower() == 'yes' then - return true - else - -- note that because we did not return nil or reject then false will be passed to command callback, see example parse - return false - end + -- If it is not nil then we check for a truthy value + if input == 'true' or input == 'yes' then return true end + -- Note that because we did not return nil or reject then false will be passed to command callback, see example parse + return false end) --- Once all params are defined you can now define some default values if you have optional params, the default value will be used only --- when no value is given as input, if an invalid value is given then the command will still fail and this value will not be used, the --- default can also be a function which is passed the player using the command and returns a value. Here we set the default for "smiley" to false: +-- Once all params are defined you can add some default values for your optional params, the default value will be used only +-- when no value is given as input, if an invalid value is given then the command will fail and the default will not be used, the +-- default can also be a function which is passed the player as an argument and should return a value to be the default. +-- Here we set the default for "smiley" to false: :set_defaults{smiley=false} -- Another example of defaults if we have: item, amount[opt], player[opt] :set_defaults{ - amount = 50, -- more than one value can be set at a time - player = function(player) - return player -- default is the player using the command - end + amount = 50, -- More than one value can be set at a time + player = function(player) return player end -- Default is the player using the command } --- Now the params are set up we can alter how the command works, we can set auth flags, add aliases to this command or enable "auto concat" --- which is when you want all extra words to be concatenated onto the end of the last param, useful for reason or messages: -:set_flag('admin_only', true) -- in our case we want "admin_only" to be set to true so only admins can use the command -:add_alias('name', 'rname') -- we also add two aliases here: "name" and "rname" which point to this command --- :enable_auto_concat() we do not use this in our case but this can also be used to enable the "auto concat" feature +-- Now the params are set up we can alter how the command works, we can set auth flags, add aliases, or enable "auto concat": +:set_flag('admin_only') -- In our case we want "admin_only" to be set to true so only admins can use the command +:add_alias('name', 'rname') -- We also add two aliases here: "name" and "rname" which point to this command +-- :enable_auto_concat() -- We do not use this in our case but this can also be used to enable the "auto concat" feature -- And finally we want to register a callback to this command, the callback is what defines what the command does, can be as complex as you --- want it to be to as simple as our example; the command receives two params plus all that you have defines: +-- want it to be, or as simple as our example; the command receives two params plus all param you have defined: -- 1) the player who used the command -- 2) in our case repeat_count which will be a number -- 3) in our case smiley which will be a boolean -- 4) the raw input; this param is always last as is always present as a catch all :register(function(player, repeat_count, smiley, raw) - -- this is to show the value for raw as this is an example command, the log file will also show this + -- This is to show the value for raw as this is an example command, the log file will also show this game.print(player.name..' used a command with input: '..raw) local msg = ') '..player.name + if smiley then - -- this is where that smiley param is used msg = ':'..msg end + for 1 = 1, repeat_count do -- this print function will return ANY value to the user in a desync safe manor, this includes if the command was used through rcon Command.print(1..msg) end - -- see below for what else can be used here + -- See below for what can be used here end) --- Other values that can be returned from register -Commands.print(any, colour[opt]) -- this will return any value value to the user including if it is ran through rcon console -Commands.error(message[opt]) -- this returns a warning to the user, aka an error that does not prevent execution of the command -return Commands.error(message[opt]) -- this returns an error to the user, and will halt the command execution, ie no success message is returned -Commands.success(message[opt]) -- used to return a success message however don't use this method see below -return Commands.success(message[opt]) -- will return the success message to the user and your given message, halts execution -return -- if any value is returned then it will be returned to the player via a Commands.success call +-- Values that can be returned from register callback +Commands.print(any, colour[opt]) -- This will return any value value to the user including if it is ran through rcon console +Commands.error(message[opt]) -- This returns a warning to the user, aka an error that does not prevent execution of the command +return Commands.error(message[opt]) -- This returns an error to the user, and will halt the command execution, ie no success message is returned +Commands.success(message[opt]) -- Used to return a success message however don't use this method, see below +return Commands.success(message[opt]) -- Will return the success message to the user and your given message, halts execution +return -- If any value is returned then it will be returned to the player via a Commands.success call @usage--- Example Authenticator: -- The command system is best used when you can control who uses commands; --- to do this would would need to define an authenticator which is ran every time a command is run; +-- to do this you need to define an authenticator which is ran every time a command is run; -- in this example I will show a simple one that requires certain commands to require the user to be a game admin. -- For our admin only example we will set a flag to true when we want it to be admin only; --- when we define the command will will use :set_flag('admin_only', true); +-- when we define the command will will use :set_flag('admin_only'); -- then inside the authenticator we will test if the flag is present using: if flags.admin_only then -- When the authenticator is called by the command handler it will be passed 4 arguments: @@ -126,11 +120,11 @@ return -- if any value is returned then it will be returned to the player -- 1) when the "admin_only" flag is not set, which we take assume that any one can use it -- 2) when the "admin_only" flag is set, and the player is admin --- When want to prevent exicution of the command we must reject it, listed is how that can be done: +-- When want to prevent execution of the command we must reject it, listed is how that can be done: -- 1) return false -- this is the most basic rejection and should only be used while testing -- 2) return reject -- returning the reject function is as a fail safe in case you forget to call it, same as returning false --- 3) reject() -- this will block execution without to allowing further code to be ran in your authenticator --- 4) reject('This command is for admins only!') -- Using reject as a function allows a error message to be returned +-- 3) reject() -- this will block execution while allowing further code to be ran in your authenticator +-- 4) reject('This command is for admins only!') -- using reject as a function allows a error message to be returned -- 5) return reject() -- using return on either case above is best practice as you should execute all your code before rejecting -- Example Code: @@ -145,15 +139,15 @@ Commands.add_authenticator(function(player, command, flags, reject) end end) -@usage--- Example Parse: +@usage--- Example Parser: -- Before you make a command it is important to understand the most powerful feature of this command handler; -- when you define a command you are able to type the params and have then be parsed and validated before your command is executed; --- This module should is paired with a general command parse but you may want to create your own. +-- This module should be paired with a general command parse but you may want to create your own. -- For our example we will create a parse to accept only integer numbers in a given range: -- 1) we will give it the name "number-range-int" this is the "type" that the input is expected to be -- 2) when we define the type we will also define the min and max of the range so we can use the function more than once -:add_param('repeat_count', false, 'number-range-int', 5, 10) -- "repeat_count" is required "number-range-int" in a range 5 to 10 inclusive +:add_param('repeat_count', 'number-range-int', 5, 10) -- "repeat_count" is a required "number-range-int" in a range 5 to 10 inclusive -- The command parse will be passed 3 arguments plus any other which you define, in our case: -- 1) input - the input that has been given by the user for this param, the role of this function is to transform this value @@ -193,133 +187,193 @@ end) local Game = require 'utils.game' --- @dep utils.game local player_return, write_json = _C.player_return, _C.write_json --- @dep expcore.common +local trace = debug.traceback local Commands = { - --- Values returned by the signal functions to cause the command system to react + --- Constant values used by the command system defines = { - error='CommandError', - unauthorized='CommandErrorUnauthorized', - success='CommandSuccess' + error = 'CommandError', + unauthorized = 'CommandErrorUnauthorized', + success = 'CommandSuccess' }, - --- Custom command data will be stored here - commands={}, - --- Set true to have authorize fail if a callback fails to run, more secure - authorization_fail_on_error=false, - --- Custom function are stored here which control who can use what commands - authorization={}, + --- An array of all custom commands that are registered + commands = {}, + --- When true any authenticator error will result in authorization failure, more secure + authorization_failure_on_error = false, + --- An array of all custom authenticators that are registered + authenticators = {}, --- Used to store default functions which are common parse function such as player or number in range - parse_functions={}, - -- Sends a value to the player, different to success as this does not signal the end of your command - print=player_return, - --- Used to store functions which gets added to new custom commands - _prototype={}, + parsers = {}, + --- Returns a value to the player, different to success as this does not signal the end of your command + print = player_return, + --- The command prototype which stores all command defining functions + _prototype = {}, } ---- Authenication. +--- Authentication. -- Functions that control who can use commands -- @section auth ---[[-- Adds an authorization callback, function used to check if a player if allowed to use a command -@tparam function callback the callback you want to register as an authenticator -@treturn number the index it was inserted at use to remove the callback, if anon function used +--[[-- Adds an authorization function, function used to check if a player if allowed to use a command +@tparam function authenticator The function you want to register as an authenticator +@treturn number The index it was inserted at, used to remove the authenticator -@usage-- Test if a command is admin only and if the player is admin +@usage-- If the admin_only flag is set, then make sure the player is an admin local admin_authenticator = Commands.add_authenticator(function(player, command, flags, reject) - if flags.admin_only then - return player.admin or reject('This command is for admins only!') + if flags.admin_only and not player.admin then + return reject('This command is for admins only!') else return true end end) ]] -function Commands.add_authenticator(callback) - table.insert(Commands.authorization, callback) - return #Commands.authorization +function Commands.add_authenticator(authenticator) + local next_index = #Commands.authenticators + 1 + Commands.authenticators[next_index] = authenticator + return next_index end ---[[-- Removes an authorization callback -@tparam function|number callback the callback to remove, an index returned by add_authenticator can be passed -@treturn boolean if the callback found and removed successfuly +--[[-- Removes an authorization function, can use the index or the function value +@tparam function|number authenticator The authenticator to remove, either the index return from add_authenticator or the function used +@treturn boolean If the authenticator was found and removed successfully -@usage-- Removing the admin authenticator, can not be done dueing runtime +@usage-- Removing the admin authenticator, can not be done during runtime Commands.remove_authenticator(admin_authenticator) ]] -function Commands.remove_authenticator(callback) - if type(callback) == 'number' then - -- if a number is passed then it is assumed to be the index - if Commands.authorization[callback] then - table.remove(Commands.authorization, callback) +function Commands.remove_authenticator(authenticator) + if type(authenticator) == 'number' then + -- If a number is passed then it is assumed to be the index + if Commands.authenticators[authenticator] then + Commands.authenticators[authenticator] = nil return true end else -- will search the array and remove the key - local index - for key, value in pairs(Commands.authorization) do - if value == callback then - index = key - break + for index, value in pairs(Commands.authenticators) do + if value == authenticator then + Commands.authenticators[index] = nil + return true end end - -- if the function was found it is removed - if index then - table.remove(Commands.authorization, index) - return true - end end return false end ---[[-- Mostly used internally, calls all authorization callbacks, returns if the player is authorized -@tparam LuaPlayer player the player that is using the command, passed to callbacks -@tparam string command_name the command that is being used, passed to callbacks -@treturn[1] boolean true player is authorized -@treturn[1] string commands const for success -@treturn[2] boolean false player is unauthorized -@treturn[2] string|locale_string the reason given by the authenticator +--[[-- Mostly used internally, calls all authenticators, returns if the player is authorized +@tparam LuaPlayer player The player who is using the command, passed to authenticators +@tparam string command_name The name of the command being used, passed to authenticators +@treturn[1] boolean true Player is authorized +@treturn[1] string commands Define value for success +@treturn[2] boolean false Player is unauthorized +@treturn[2] string|locale_string The reason given by the failed authenticator @usage-- Test if a player can use "repeat-name" local authorized, status = Commands.authorize(game.player, 'repeat-name') ]] function Commands.authorize(player, command_name) - local failed - if not player then return true end local command_data = Commands.commands[command_name] if not command_data then return false end + if not player then return true end - -- function passed to authorization callback to make it simpler to use - local auth_fail = function(error_message) - failed = error_message or {'expcore-commands.unauthorized'} + -- This is the reject function given to authenticators + local failure_message + local function reject(message) + failure_message = message or {'expcore-commands.unauthorized'} return Commands.defines.unauthorized end - -- loops over each authorization callback if any return false or unauthorized command will fail - for _, callback in pairs(Commands.authorization) do - -- callback(player: LuaPlayer, command: string, flags: table, reject: function(error_message?: string)) - local success, rtn = pcall(callback, player, command_name, command_data.flags, auth_fail) - -- error handler - if not success then - -- the callback failed to run - log('[ERROR] Authorization failed: '..rtn) - if Commands.authorization_fail_on_error then - failed = 'Internal Error' - end - elseif rtn == false or rtn == Commands.defines.unauthorized or rtn == auth_fail or failed then - -- the callback returned unauthorized, failed be now be set if no value returned - failed = failed or {'expcore-commands.unauthorized'} - break + -- This is the internal error function used when an authenticator errors + local function authenticator_error(err) + log('[ERROR] Authorization failed: '..trace(err)) + if Commands.authorization_failure_on_error then + return reject('Internal Error') end end - -- checks if the authorization failed - if failed then - return false, failed - else - return true, Commands.defines.success + -- Loops over each authenticator, if any return false then then command will not be ran + for _, authenticator in pairs(Commands.authenticators) do + -- player: LuaPlayer, command: string, flags: table, reject: function(error_message: string) + local _, rtn = xpcall(authenticator, authenticator_error, player, command_name, command_data.flags, reject) + if rtn == false or rtn == Commands.defines.unauthorized or rtn == reject or failure_message ~= nil then + if failure_message == nil then failure_message = {'expcore-commands.unauthorized'} end + return false, failure_message + end end + + return true, Commands.defines.success +end + +--- Parse. +-- Functions that help with parsing +-- @section parse + +--[[-- Adds a parse function which can be called by name (used in add_param) +nb: this is not required as you can use the callback directly this just allows it to be called by name +@tparam string name The name of the parse, should describe a type of input such as number or player, must be unique +@tparam function parser The function that is ran to parse the input string +@treturn boolean Was the parse added, will be false if the name is already used + +@usage-- Adding a parse to validate integers in a given range +Commands.add_parse('number-range-int', function(input, player, reject, range_min, range_max) + local rtn = tonumber(input) and math.floor(tonumber(input)) or nil -- converts input to number + if not rtn or rtn < range_min or rtn > range_max then + -- The input is either not a number or is outside the range + return reject('Number entered is not in range: '..range_min..', '..range_max) + else + -- Returns the input as a number rather than a string, thus the param is now the correct type + return rtn + end +end) + +]] +function Commands.add_parse(name, parser) + if Commands.parsers[name] then return false end + Commands.parsers[name] = parser + return true +end + +--[[-- Removes a parse function, see add_parse for adding them, cant be done during runtime +@tparam string name The name of the parse to remove + +@usage-- Removing a parse +Commands.remove_parse('number-range-int') + +]] +function Commands.remove_parse(name) + Commands.parsers[name] = nil +end + +--[[-- Intended to be used within other parse functions, runs a parse and returns success and new value +@tparam string name The name of the parse to call, must be a registered parser +@tparam string input The input to pass to the parse, must be a string but not necessarily the original input +@tparam LuaPlayer player The player that is using the command, pass directly from your arguments +@tparam function reject The reject function, pass directly from your arguments +@treturn any The new value for the input, if nil is return then either there was an error or the input was nil + +@usage-- Parsing an int after first checking it is a number +Commands.add_parse('number', function(input, player, reject) + local number = tonumber(input) + if number then return number end + return reject('Input must be a number value') +end) + +Commands.add_parse('number-int', function(input, player, reject) + local number = Commands.parse('number', input, player, reject) + if not number then return end + return math.floor(number) +end) + +]] +function Commands.parse(name, input, player, reject, ...) + if not Commands.parsers[name] then return end + local success, rtn = pcall(Commands.parsers[name], input, player, reject, ...) + if not success then error(rtn, 2) return end + if not rtn or rtn == Commands.defines.error then return end + return rtn end --- Getters. @@ -327,10 +381,10 @@ end -- @section getters --[[-- Gets all commands that a player is allowed to use, game commands are not included -@tparam[opt] LuaPlayer player the player that you want to get commands of, nil will return all commands -@treturn table all commands that that player is allowed to use, or all commands +@tparam[opt] LuaPlayer player The player that you want to get commands of, nil will return all commands +@treturn table All commands that that player is allowed to use, or all commands -@usage-- Get the command you are allowed to use +@usage-- Get the commands you are allowed to use local commands = Commands.get(game.player) @usage-- Get all commands that are registered @@ -343,16 +397,16 @@ function Commands.get(player) local allowed = {} for name, command_data in pairs(Commands.commands) do if Commands.authorize(player, name) then - allowed[name]=command_data + allowed[name] = command_data end end return allowed end --[[-- Searches command names and help messages to find possible commands, game commands are included -@tparam string keyword the word which you are trying to find in your search -@tparam[opt] LuaPlayer player the player to get allowed commands of, if nil all commands are searched -@treturn table all commands that contain the key word, and allowed by player if player given +@tparam string keyword The word which you are trying to find in your search +@tparam[opt] LuaPlayer player The player to get allowed commands of, if nil all commands are searched +@treturn table All commands that contain the key word, and allowed by the player if a player was given @usage-- Get all commands which "repeat" local commands = Commands.search('repeat') @@ -365,7 +419,7 @@ function Commands.search(keyword, player) local custom_commands = Commands.get(player) local matches = {} keyword = keyword:lower() - -- loops over custom commands + -- Loops over custom commands for name, command_data in pairs(custom_commands) do -- combines name help and aliases into one message to be searched local search = string.format('%s %s %s', name, command_data.help, table.concat(command_data.aliases, ' ')) @@ -373,127 +427,63 @@ function Commands.search(keyword, player) matches[name] = command_data end end - -- loops over the names of game commands + -- Loops over the names of game commands for name, description in pairs(commands.game_commands) do if name:lower():match(keyword) then - -- because game commands lack some stuff that the custom ones have they are formated + -- because game commands lack some stuff that the custom ones have they are formatted matches[name] = { - name=name, - help=description, - description='', - aliases={} + name = name, + help = description, + description = '', + aliases = {} } end end return matches end ---- Parse. --- Functions that help with parsing --- @section parse - ---[[-- Adds a parse function which can be called by name (used in add_param) -nb: this is not required as you can use the callback directly this just allows it to be called by name -@tparam string name the name of the parse, should be the type like player or player_alive, must be unique -@tparam function callback the callback that is ran to parse the input -@treturn boolean was the parse added will be false if the name is already used - -@usage-- Adding a parse to validate ints in a given range -Commands.add_parse('number-range-int', function(input, player, reject, range_min, range_max) - local rtn = tonumber(input) and math.floor(tonumber(input)) or nil -- converts input to number - if not rtn or rtn < range_min or rtn > range_max then - -- the input is either not a number or is outside the range - return reject('Number entered is not in range: '..range_min..', '..range_max) - else - -- returns the input as a number rather than a string, thus the param is now the correct type - return rtn - end -end) - -]] -function Commands.add_parse(name, callback) - if Commands.parse_functions[name] then - return false - else - Commands.parse_functions[name] = callback - return true - end -end - ---[[-- Removes a parse function, see add_parse for adding them -@tparam string name the name of the parse to remove - -@usage-- Removing a parse -Commands.remove_parse('number-range-int') - -]] -function Commands.remove_parse(name) - Commands.parse_functions[name] = nil -end - ---[[-- Intended to be used within other parse functions, runs a parse and returns success and new value -@tparam string name the name of the parse to call, must be registered parse -@tparam string input string the input to pass to the parse, must be a string but not necessarily the original input -@tparam LuaPlayer player the player that is using the command -@tparam function reject the reject function that was passed by the command hander -@treturn any the new value for the input, may be nil, if nil then either there was an error or input was nil - -@usage-- Parsing a int in a given range -local parsed_input = Commands.parse('number-range-int', '7', player, reject, 1, 10) -- valid range 1 to 10 - -]] -function Commands.parse(name, input, player, reject, ...) - if not Commands.parse_functions[name] then return end - local success, rtn = pcall(Commands.parse_functions[name], input, player, reject, ...) - if not success then error(rtn, 2) return end - if not rtn then return end - if rtn == Commands.defines.error then return end - return rtn -end - --- Creation. -- Functions that create a new command -- @section creation ---[[-- Creates a new command object to added details to, note this does not register the command to the game api -@tparam string name the name of the command to be created -@tparam string help the help message for the command -@treturn Commands._prototype this will be used with other functions to generate the command functions +--[[-- Creates a new command object to added details to, this does not register the command to the game api +@tparam string name The name of the command to be created +@tparam string help The help message for the command +@treturn table This will be used with other functions to define the new command @usage-- Define a new command -local command = Commands.new_command('repeat-name', 'Will repeat you name a number of times in chat.') ]] function Commands.new_command(name, help) local command = setmetatable({ - name=name, - help=help, - callback=function() Commands.internal_error(false, name, 'No callback registered') end, - auto_concat=false, - min_param_count=0, - max_param_count=0, - flags={}, -- stores flags that can be used by auth - aliases={}, -- n = name: string - params={}, -- [param_name] = {optional: boolean, default: any, parse: function, parse_args: table} + name = name, + help = help, + callback = function() Commands.internal_error(false, name, 'No callback registered') end, + auto_concat = false, + min_param_count = 0, + max_param_count = 0, + flags = {}, -- stores flags that can be used by auth + aliases = {}, -- stores aliases to this command + params = {}, -- [param_name] = {optional: boolean, default: any, parse: function, parse_args: table} }, { - __index= Commands._prototype + __index = Commands._prototype }) Commands.commands[name] = command return command end --[[-- Adds a new param to the command this will be displayed in the help and used to parse the input -@tparam string name the name of the new param that is being added to the command -@tparam[opt=false] boolean optional is this param required for this command, these must be after all required params -@tparam[opt=pass function through] ?string|function parse this function will take the input and return a new (or same) value -@param[opt] ... extra args you want to pass to the parse function; for example if the parse is general use -@treturn Commands._prototype pass through to allow more functions to be called +@tparam string name The name of the new param that is being added to the command +@tparam[opt=false] boolean optional Is this param optional, these must be added after all required params +@tparam[opt] ?string|function parse This function will take the input and return a new value, if not given no parse is done +@tparam[opt] any ... Extra args you want to pass to the parse function; for example if the parse is general use +@treturn table Pass through to allow more functions to be called -@usage-- Adding a param which has an parse defined -command:add_param('repeat-count', false, 'number-range-int', 1, 5) +@usage-- Adding a required param which has a parser pre-defined +command:add_param('repeat-count', 'number-range-int', 1, 5) -@usage-- Adding a param which has a custom parse, see Commands.add_parse for details +@usage-- Adding an optional param which has a custom parse, see Commands.add_parse for details command:add_param('smiley', true, function(input, player, reject) if not input then return end return input:lower() == 'true' or input:lower() == 'yes' or false @@ -507,22 +497,24 @@ function Commands._prototype:add_param(name, optional, parse, ...) parse = optional optional = false end - parse = parse or function(string) return string end + self.params[name] = { - optional=optional, - parse=parse, - parse_args=parse_args + optional = optional, + parse = parse or function(string) return string end, + parse_args = parse_args } - self.max_param_count = self.max_param_count+1 + + self.max_param_count = self.max_param_count + 1 if not optional then - self.min_param_count = self.min_param_count+1 + self.min_param_count = self.min_param_count + 1 end + return self end ---[[-- Add default values to params, only as an effect if the param is optional, if default value is a function it is called with acting player -@tparam table defaults table which is keyed by the name of the param and the value is the default value -@treturn Commands._prototype pass through to allow more functions to be called +--[[-- Add default values to params, only as an effect if the param is optional, if default value is a function it is called with the acting player +@tparam table defaults A table which is keyed by the name of the param and the value is the default value for that param +@treturn table Pass through to allow more functions to be called @usage-- Adding default values command:set_defaults{ @@ -543,10 +535,10 @@ function Commands._prototype:set_defaults(defaults) return self end ---[[-- Adds a tag to the command which is passed via the flags param to the authenticators, can be used to assign command roles or type -@tparam string name the name of the tag to be added, set to true if no value is given -@tparam[opt=true] any value the tag that you want can be anything that the authenticators are expecting -@treturn Commands._prototype pass through to allow more functions to be called +--[[-- Adds a flag to the command which is passed via the flags param to the authenticators, can be used to assign command roles or usage type +@tparam string name The name of the flag to be added, set to true if no value is given +@tparam[opt=true] any value The value for the flag, can be anything that the authenticators are expecting +@treturn table Pass through to allow more functions to be called @usage-- Setting a custom flag command:set_flag('admin_only', true) @@ -556,31 +548,29 @@ command:set_flag('admin_only') ]] function Commands._prototype:set_flag(name, value) - value = value or true - self.flags[name] = value + self.flags[name] = value or true return self end ---[[-- Adds an alias, or multiple, that will also be registered with the same callback, eg /teleport can be used as /tp -@tparam string any ... amount of aliases that you want this command to be callable with -@treturn Commands._prototype pass through to allow more functions to be called +--[[-- Adds an alias, or multiple, that will be registered to this command, eg /teleport can be used as /tp +@tparam string ... Any amount of aliases that you want this command to be callable with +@treturn table Pass through to allow more functions to be called @usage-- Added multiple aliases to a command command:add_alias('name', 'rname') ]] function Commands._prototype:add_alias(...) - for _, alias in pairs({...}) do - table.insert(self.aliases, alias) - --Commands.alias_map[alias] = self.name + local start_index = #self.aliases + for index, alias in ipairs{...} do + self.aliases[start_index+index] = alias end return self end ---[[-- Enables auto concatenation of any params on the end so quotes are not needed for last param +--[[-- Enables auto concatenation for this command, all params after the last are added to the last param, useful for reasons or other long text input nb: this will disable max param checking as they will be concatenated onto the end of that last param -this can be useful for reasons or longs text, can only have one per command -@treturn Commands._prototype pass through to allow more functions to be called +@treturn table Pass through to allow more functions to be called @usage-- Enable auto concat for a command command:enable_auto_concat() @@ -591,12 +581,12 @@ function Commands._prototype:enable_auto_concat() return self end ---[[-- Adds the callback to the command and registers all aliases, params and help message with the game api +--[[-- Adds the callback to the command and registers: aliases, params and help message with the base game api nb: this must be the last function ran on the command and must be done for the command to work -@tparam function callback the callback for the command, will receive the player running command, and params added with add_param +@tparam function callback The callback for the command, will receive the player running command, and any params added with add_param -@usage-- Registering your command to the game api -command:register(function(player, repeat_count, smiley, _) +@usage-- Registering your command to the base game api +command:register(function(player, repeat_count, smiley, raw) local msg = ') '..player.name if smiley then msg = ':'..msg end @@ -607,8 +597,9 @@ end) ]] function Commands._prototype:register(callback) - -- generates a description to be used self.callback = callback + + -- Generates a description to be used local description = '' for param_name, param_details in pairs(self.params) do if param_details.optional then @@ -618,19 +609,26 @@ function Commands._prototype:register(callback) end end self.description = description - -- registers the command under its own name - commands.add_command(self.name, {'expcore-commands.command-help', description, self.help}, function(command_event) - local success, err = pcall(Commands.run_command, command_event) - if not success then log('[ERROR] command/'..self.name..' :: '..err) end - end) - -- adds any aliases that it has + + -- Last resort error handler for commands + local function command_error(err) + Commands.internal_error(false, self.name, trace(err)) + end + + -- Callback that the game will call + local function command_callback(event) + event.name = self.name + xpcall(Commands.run_command, command_error, event) + end + + -- Registers the command under its own name + local help = {'expcore-commands.command-help', description, self.help} + commands.add_command(self.name, help, command_callback) + + -- Adds any aliases that it has for _, alias in pairs(self.aliases) do if not commands.commands[alias] and not commands.game_commands[alias] then - commands.add_command(alias, {'expcore-commands.command-help', description, self.help}, function(command_event) - command_event.name = self.name - local success, err = pcall(Commands.run_command, command_event) - Commands.internal_error(success, self.name, err) - end) + commands.add_command(alias, help, command_callback) end end end @@ -639,52 +637,9 @@ end -- Functions that indicate status -- @section status ---[[-- Sends an error message to the player and when returned will stop exicution of the command -nb: this is for non fatal errors meaning there is no log of this event, use during register callback -@tparam[opt=''] string error_message an optional error message that can be sent to the user -@tparam[opt=utility/wire_pickup] string play_sound the sound to play for the error -@treturn Commands.defines.error return this to command handler to exit execution - -@usage-- Send an error message to the player, and stops further code running -return Commands.error('The player you selected is offline') - -]] -function Commands.error(error_message, play_sound) - error_message = error_message or '' - player_return({'expcore-commands.command-fail', error_message}, 'orange_red') - if play_sound ~= false then - play_sound = play_sound or 'utility/wire_pickup' - if game.player then game.player.play_sound{path=play_sound} end - end - return Commands.defines.error -end - ---[[-- Sends an error to the player and logs the error, used with pcall within command handler please avoid direct use -nb: use error(error_message) within your callback to trigger do not trigger directly as code exictuion may still continue -@tparam boolean success the success value returned from pcall, or just false to trigger error -@tparam string command_name the name of the command this is used within the log -@tparam string error_message the error returned by pcall or some other error, this is logged and not returned to player -@treturn boolean the opposite of success so true means to cancel execution, used internally - -@usage-- Used in the command system to log handler errors -local success, err = pcall(command_data.callback, player, unpack(params)) -if Commands.internal_error(success, command_data.name, err) then - return command_log(player, command_data, 'Internal Error: Command Callback Fail', raw_params, command_event.parameter, err) -end - -]] -function Commands.internal_error(success, command_name, error_message) - if not success then - Commands.error('Internal Error, Please contact an admin', 'utility/cannot_build') - log{'expcore-commands.command-error-log-format', command_name, error_message} - end - return not success -end - ---[[-- Sends a value to the player, followed by a command complete message -nb: returning any value from your callback will trigger this function, return this function to prevent duplicate messages -@tparam[opt] any value the value to return to the player, if nil then only success message returned -@treturn Commands.defines.success return this to the command handler to prevent two success messages +--[[-- Sends a value to the player, followed by a command complete message, returning a value will trigger this automatically +@tparam[opt] any value The value to return to the player, if nil then only the success message is returned +@treturn Commands.defines.success Return this to the command handler to prevent two success messages @usage-- Print a custom success message return Commands.success('Your message has been printed') @@ -701,39 +656,81 @@ end --[[-- Sends a value to the player, different to success as this does not signal the end of your command @function print -@tparam any value the value that you want to return to the player -@tparam table colour the colour of the message that the player sees +@tparam any value The value that you want to return to the player +@tparam table colour The colour of the message that the player sees @usage-- Output a message to the player Commands.print('Your command is in progress') ]] --- logs command usage to file +--[[-- Sends an error message to the player and when returned will stop execution of the command +nb: this is for non fatal errors meaning there is no log of this event, use during register callback +@tparam[opt=''] string error_message An optional error message that can be sent to the user +@tparam[opt=utility/wire_pickup] string play_sound The sound to play for the error +@treturn Commands.defines.error Return this to command handler to terminate execution + +@usage-- Send an error message to the player, and stops further code running +return Commands.error('The player you selected is offline') + +]] +function Commands.error(error_message, play_sound) + error_message = error_message or '' + player_return({'expcore-commands.command-fail', error_message}, 'orange_red') + if play_sound ~= false then + play_sound = play_sound or 'utility/wire_pickup' + if game.player then game.player.play_sound{path=play_sound} end + end + return Commands.defines.error +end + +--[[-- Sends an error to the player and logs the error, used internally please avoid direct use +nb: use error(error_message) within your callback to trigger do not trigger directly as code execution may still continue +@tparam boolean success The success value returned from pcall, or just false to trigger error +@tparam string command_name The name of the command this is used within the log +@tparam string error_message The error returned by pcall or some other error, this is logged and not returned to player +@treturn boolean The opposite of success so true means to cancel execution, used internally + +@usage-- Used in the command system to log handler errors +local success, err = pcall(command_data.callback, player, unpack(params)) +if Commands.internal_error(success, command_data.name, err) then + return command_log(player, command_data, 'Internal Error: Command Callback Fail', raw_params, command_event.parameter, err) +end + +]] +function Commands.internal_error(success, command_name, error_message) + if not success then + Commands.error('Internal Error, Please contact an admin', 'utility/cannot_build') + log{'expcore-commands.command-error-log-format', command_name, error_message} + end + return not success +end + +--- Logs command usage to file local function command_log(player, command, comment, params, raw, details) local player_name = player and player.name or '' write_json('log/commands.log', { - player_name=player_name, - command_name=command.name, - comment=comment, - details=details, - params=params, - raw=raw + player_name = player_name, + command_name = command.name, + comment = comment, + details = details, + params = params, + raw = raw }) end --- Main event function that is ran for all commands, used internally please avoid direct use --- @tparam table command_event passed directly from command event from the add_command function +-- @tparam table command_event Passed directly from the add_command function -- @usage Commands.run_command(event) function Commands.run_command(command_event) local command_data = Commands.commands[command_event.name] - -- player can be nil when it is the server + -- Player can be nil when it is the server local player if command_event.player_index and command_event.player_index > 0 then - player = Game.get_player_by_index(command_event.player_index) + player = game.players[command_event.player_index] end - -- checks if player is allowed to use the command + -- Check if the player is allowed to use the command local authorized, auth_fail = Commands.authorize(player, command_data.name) if not authorized then command_log(player, command_data, 'Failed Auth', {}, command_event.parameter) @@ -741,134 +738,134 @@ function Commands.run_command(command_event) return end - -- null param check + -- Check for parameter being nil if command_data.min_param_count > 0 and not command_event.parameter then command_log(player, command_data, 'No Params Given', {}, command_event.parameter) - Commands.error({'expcore-commands.invalid-inputs', command_data.name, command_data.description}) + Commands.error{'expcore-commands.invalid-inputs', command_data.name, command_data.description} return end - -- splits the arguments - local input_string = command_event.parameter or '' - local quote_params = {} -- stores any " " params - input_string = input_string:gsub(' "[^"]-"', function(w) - -- finds all " " params are removes spaces for the next part - local no_spaces = w:gsub('%s', '_') - local no_quotes = w:sub(2, -2) - quote_params[no_spaces]=no_quotes - if command_data.auto_concat then - -- if auto concat then don't remove quotes as it should be included later - quote_params[no_spaces]=w - end + -- Extract quoted arguments + local raw_input = command_event.parameter or '' + local quote_params = {} + local input_string = raw_input:gsub(' "[^"]-"', function(word) + word = word:sub(2) + local no_spaces = word:gsub('%s', '%%s') + quote_params[no_spaces] = word:sub(2, -2) return no_spaces end) - local raw_params = {} -- stores all params - local param_number = 0 + -- Extract unquoted arguments + local raw_params = {} local last_index = 0 + local param_number = 0 for word in input_string:gmatch('%S+') do param_number = param_number + 1 if param_number > command_data.max_param_count then -- there are too many params given to the command if not command_data.auto_concat then -- error as they should not be more - command_log(player, command_data, 'Invalid Input: Too Many Params', raw_params, input_string) - Commands.error({'expcore-commands.invalid-inputs', command_data.name, command_data.description}) + command_log(player, command_data, 'Invalid Input: Too Many Params', raw_params, raw_input) + Commands.error{'expcore-commands.invalid-inputs', command_data.name, command_data.description} return else -- concat to the last param if quote_params[word] then - -- if it was a " " param then the spaces are re added now - raw_params[last_index]=raw_params[last_index]..' '..quote_params[word] + raw_params[last_index] = raw_params[last_index]..' "'..quote_params[word]..'"' else - raw_params[last_index]=raw_params[last_index]..' '..word + raw_params[last_index] = raw_params[last_index]..' '..word end end else -- new param that needs to be added - -- all words are added to an array if quote_params[word] then - -- if it was a " " param then the spaces are re added now - table.insert(raw_params, quote_params[word]) last_index = last_index + 1 + raw_params[last_index] = quote_params[word] else - table.insert(raw_params, word) last_index = last_index + 1 + raw_params[last_index] = word end end end - -- checks param count + -- Check the param count local param_count = #raw_params if param_count < command_data.min_param_count then - command_log(player, command_data, 'Invalid Input: Not Enough Params', raw_params, input_string) - Commands.error({'expcore-commands.invalid-inputs', command_data.name, command_data.description}) + command_log(player, command_data, 'Invalid Input: Not Enough Params', raw_params, raw_input) + Commands.error{'expcore-commands.invalid-inputs', command_data.name, command_data.description} return end - -- parses the arguments + -- Parse the arguments local index = 1 local params = {} for param_name, param_data in pairs(command_data.params) do local parse_callback = param_data.parse + -- If its a string this get it from the parser table if type(parse_callback) == 'string' then - -- if its a string this allows it to be pulled from the common store - parse_callback = Commands.parse_functions[parse_callback] + parse_callback = Commands.parsers[parse_callback] end + + -- If its not a function throw and error if not type(parse_callback) == 'function' then - -- if its not a function throw and error Commands.internal_error(false, command_data.name, 'Invalid param parse '..tostring(param_data.parse)) - command_log(player, command_data, 'Internal Error: Invalid Param Parse', params, command_event.parameter, tostring(param_data.parse)) + command_log(player, command_data, 'Internal Error: Invalid Param Parse', params, raw_input, tostring(param_data.parse)) return end - -- used below as the reject function - local parse_fail = function(error_message) + + -- This is the reject function given to parse callbacks + local function reject(error_message) error_message = error_message or '' command_log(player, command_data, 'Invalid Param Given', raw_params, input_string) return Commands.error{'expcore-commands.invalid-param', param_name, error_message} end + -- input: string, player: LuaPlayer, reject: function, ... extra args - local success, param_parsed = pcall(parse_callback, raw_params[index], player, parse_fail, unpack(param_data.parse_args)) + local success, param_parsed = pcall(parse_callback, raw_params[index], player, reject, unpack(param_data.parse_args)) if Commands.internal_error(success, command_data.name, param_parsed) then - return command_log(player, command_data, 'Internal Error: Param Parse Fail', params, command_event.parameter, param_parsed) + return command_log(player, command_data, 'Internal Error: Param Parse Fail', params, raw_input, param_parsed) end + if param_data.optional == true and raw_params[index] == nil then - -- if it is optional and param is nil then it is set to default + -- If the param is optional and nil then it is set to default param_parsed = param_data.default if type(param_parsed) == 'function' then - -- player: LuaPlayer success, param_parsed = pcall(param_parsed, player) if Commands.internal_error(success, command_data.name, param_parsed) then - return command_log(player, command_data, 'Internal Error: Default Value Fail', params, command_event.parameter, param_parsed) + return command_log(player, command_data, 'Internal Error: Default Value Fail', params, raw_input, param_parsed) end end - elseif param_parsed == nil or param_parsed == Commands.defines.error or param_parsed == parse_fail then - -- no value was returned or error was returned, if nil then give generic error + + elseif param_parsed == nil or param_parsed == Commands.defines.error or param_parsed == reject then + -- No value was returned or error was returned, if nil then give generic error if not param_parsed == Commands.defines.error then - command_log(player, command_data, 'Invalid Param Given', raw_params, input_string, param_name) + command_log(player, command_data, 'Invalid Param Given', raw_params, raw_input, param_name) Commands.error{'expcore-commands.command-error-param-format', param_name, 'please make sure it is the correct type'} end return + end - -- adds the param to the table to be passed to the command callback - table.insert(params, param_parsed) - index=index+1 + + -- Add the param to the table to be passed to the command callback + params[index] = param_parsed + index = index + 1 end - -- runs the command + -- Run the command -- player: LuaPlayer, ... command params, raw: string - table.insert(params, command_data.max_param_count+1, input_string) - local success, err = pcall(command_data.callback, player, unpack(params)) - if Commands.internal_error(success, command_data.name, err) then - return command_log(player, command_data, 'Internal Error: Command Callback Fail', raw_params, command_event.parameter, err) + params[command_data.max_param_count+1] = raw_input + local success, rtn = pcall(command_data.callback, player, unpack(params)) + if Commands.internal_error(success, command_data.name, rtn) then + return command_log(player, command_data, 'Internal Error: Command Callback Fail', raw_params, command_event.parameter, rtn) end - if err == Commands.defines.error or err == Commands.error then - return command_log(player, command_data, 'Custom Error', raw_params, input_string) - elseif err ~= Commands.defines.success and err ~= Commands.success then - -- in this case the user has not received any output - Commands.success(err) + + -- Give output to the player + if rtn == Commands.defines.error or rtn == Commands.error then + return command_log(player, command_data, 'Custom Error', raw_params, raw_input) + elseif rtn ~= Commands.defines.success and rtn ~= Commands.success then + Commands.success(rtn) end - command_log(player, command_data, 'Success', raw_params, input_string) + command_log(player, command_data, 'Success', raw_params, raw_input) end return Commands \ No newline at end of file