require 'utils.list_utils' local insert = table.insert local size = table.size local format = string.format local next = next local serialize = serpent.line local Command = {} local option_names = { ['description'] = 'A description of the command', ['arguments'] = 'A table of arguments, example: {"foo", "bar"} would map the first 2 arguments to foo and bar', ['default_values'] = 'A default value for a given argument when omitted, example: {bar = false}', ['admin_only'] = 'Set this to true if only admins may execute this command', ['debug_only'] = 'Set this to true if it should only be registered when _DEBUG is true', ['allowed_by_server'] = 'Set to true if the server (host) may execute this command', ['allowed_by_player'] = 'Set to false to disable players from executing this command', ['log_command'] = 'Set to true to log commands. Always true when admin_only is enabled', ['capture_excess_arguments'] = 'Allows the last argument to be the remaining text in the command', } ---Validates if there aren't any wrong fields in the options. ---@param command_name string ---@param options table local function assert_existing_options(command_name, options) local invalid = {} for name, _ in pairs(options) do if not option_names[name] then insert(invalid, name) end end if next(invalid) then error(format("The following options were given to the command '%s' but are invalid: %s", command_name, serialize(invalid))) end end ---Adds a command to be executed. --- ---Options table accepts the following structure: { --- description = 'A description of the command', --- arguments = {'foo', 'bar'}, -- maps arguments to these names in the given sequence --- default_values = {bar = false}, -- gives a default value to 'bar' when omitted --- admin_only = true, -- defaults to false --- debug_only = false, -- only registers it if _DEBUG is set to true when false --- allowed_by_server = false -- lets the server execute this, defaults to false --- allowed_by_player = true -- lets players execute this, defaults to true --- log_command = true, -- defaults to false unless admin only, then always true --- capture_excess_arguments = true, defaults to false, captures excess arguments in the last argument, useful for sentences ---} --- ---The callback receives the following arguments: --- - arguments (indexed by name, value is extracted from the parameters) --- - the LuaPlayer or nil if it doesn't exist (such as the server player) --- - the game tick in which the command was executed --- ---@param command_name string ---@param options table ---@param callback function function Command.add(command_name, options, callback) local description = options.description or '[Undocumented command]' local arguments = options.arguments or {} local default_values = options.default_values or {} local admin_only = options.admin_only or false local debug_only = options.debug_only or false local capture_excess_arguments = options.capture_excess_arguments or false local allowed_by_server = options.allowed_by_server or false local allowed_by_player = options.allowed_by_player local log_command = options.log_command or options.admin_only or false local argument_list_size = size(arguments) local argument_list = '' assert_existing_options(command_name, options) if nil == options.allowed_by_player then allowed_by_player = true end if not _DEBUG and debug_only then return end if not allowed_by_player and not allowed_by_server then error(format("The command '%s' is not allowed by the server nor player, please enable at least one of them.", command_name)) end for index, argument_name in pairs(arguments) do local argument_display = argument_name for default_value_name, _ in pairs(default_values) do if default_value_name == argument_name then argument_display = argument_display .. ':optional' break end end if argument_list_size == index and capture_excess_arguments then argument_display = argument_display .. ':sentence' end argument_list = format('%s<%s> ', argument_list, argument_display) end local extra = '' if allowed_by_server and not allowed_by_player then extra = ' (Server Only)' elseif allowed_by_player and admin_only then extra = ' (Admin Only)' end commands.add_command(command_name, argument_list .. description .. extra, function (command) local print -- custom print reference in case no player is present local player = game.player local player_name = player and player.valid and player.name or '' if not player or not player.valid then print = _G.print if not allowed_by_server then print(format("The command '%s' is not allowed to be executed by the server.", command_name)) return end else print = player.print if not allowed_by_player then print(format("The command '%s' is not allowed to be executed by players.", command_name)) return end if admin_only and not player.admin then print(format("The command '%s' requires an admin to be be executed", command_name)) return end end local named_arguments = {} local from_command = {} local raw_parameter_index = 1 for param in string.gmatch(command.parameter or '', '%S+') do if capture_excess_arguments and raw_parameter_index == argument_list_size then if not from_command[raw_parameter_index] then from_command[raw_parameter_index] = param else from_command[raw_parameter_index] = from_command[raw_parameter_index] .. ' ' .. param end else from_command[raw_parameter_index] = param raw_parameter_index = raw_parameter_index + 1 end end local errors = {} for index, argument in pairs(arguments) do local parameter = from_command[index] if not parameter then for default_value_name, default_value in pairs(default_values) do if default_value_name == argument then parameter = default_value break end end end if not parameter then insert(errors, format('Argument %s from command %s is missing.', argument, command_name)) else named_arguments[argument] = parameter end end local return_early = false for _, error in pairs(errors) do return_early = true print(error) end if return_early then return end if log_command then log(format('[%s Command] %s, used: %s %s', admin_only and 'Admin' or 'Player', player_name, command_name, serialize(named_arguments))) end local success, error = pcall(function () callback(named_arguments, player, command.tick) end) if not success then local serialized_arguments = serialize(named_arguments) if _DEBUG then print(format("%s triggered an error running a command and has been logged: '%s' with arguments %s", player_name, command_name, serialized_arguments)) print(error) return end print(format('There was an error running %s, it has been logged.', command_name)) log(format("Error while running '%s' with arguments %s: %s", command_name, serialized_arguments, error)) end end) end return Command