2019-03-04 13:54:55 +02:00
-- luacheck: globals commands
2019-01-02 17:34:17 +02:00
local Event = require ' utils.event '
local Game = require ' utils.game '
2019-02-03 00:08:44 +02:00
local Utils = require ' utils.core '
local Timestamp = require ' utils.timestamp '
2019-02-25 03:04:19 +02:00
local ErrorLogging = require ' utils.error_logging '
2019-02-03 00:08:44 +02:00
local Rank = require ' features.rank_system '
2019-01-30 05:52:43 +02:00
local Donator = require ' features.donator '
2019-02-03 00:08:44 +02:00
local Server = require ' features.server '
2019-01-30 05:52:43 +02:00
local Ranks = require ' resources.ranks '
2018-11-24 22:41:05 +02:00
2018-11-24 01:34:02 +02:00
local insert = table.insert
2018-11-24 21:44:02 +02:00
local format = string.format
2018-11-25 00:33:10 +02:00
local next = next
local serialize = serpent.line
2019-01-30 05:52:43 +02:00
local gmatch = string.gmatch
local get_rank_name = Rank.get_rank_name
2019-05-28 21:04:30 +02:00
local pairs = pairs
local pcall = pcall
2018-11-24 01:34:02 +02:00
local Command = { }
2019-01-02 17:34:17 +02:00
local deprecated_command_alternatives = {
2020-12-29 00:37:20 +02:00
[ ' dc ' ] = ' sc ' ,
2019-01-27 23:44:45 +02:00
[ ' tpplayer ' ] = ' tp <player> ' ,
2019-01-02 17:34:17 +02:00
[ ' tppos ' ] = ' tp ' ,
2019-01-27 23:44:45 +02:00
[ ' tpmode ' ] = ' tp mode ' ,
2019-01-02 17:34:17 +02:00
}
2019-02-02 03:46:12 +02:00
local notify_on_commands = {
[ ' version ' ] = ' RedMew has a version as well, accessible via /redmew-version ' ,
[ ' ban ' ] = ' In case your forgot: please remember to include a message on how to appeal a ban '
}
2018-11-25 00:33:10 +02:00
local option_names = {
2018-11-30 20:15:11 +02:00
[ ' 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} ' ,
2019-01-30 05:52:43 +02:00
[ ' required_rank ' ] = ' Set this to determins what rank is required to execute a command ' ,
[ ' donator_only ' ] = ' Set this to true if only donators may execute this command ' ,
2019-01-02 17:34:17 +02:00
[ ' debug_only ' ] = ' Set this to true if it should be registered when _DEBUG is true ' ,
[ ' cheat_only ' ] = ' Set this to true if it should be registered when _CHEATS is true ' ,
2018-11-30 20:15:11 +02:00
[ ' 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 ' ,
2019-01-30 05:52:43 +02:00
[ ' log_command ' ] = ' Set to true to log commands. Always true when admin is required ' ,
2018-11-30 20:15:11 +02:00
[ ' capture_excess_arguments ' ] = ' Allows the last argument to be the remaining text in the command ' ,
2019-03-02 19:20:56 +02:00
[ ' custom_help_text ' ] = ' Sets a custom help text to override the auto-generated help '
2018-11-25 00:33:10 +02:00
}
---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
2019-03-02 19:20:56 +02:00
error ( format ( " The following options were given to the command '%s' but are invalid: %s " , command_name , serialize ( invalid ) ) ) -- command.error_bad_option when bug fixed
2018-11-25 00:33:10 +02:00
end
end
2018-11-24 01:34:02 +02:00
---Adds a command to be executed.
---
---Options table accepts the following structure: {
2018-11-24 21:44:02 +02:00
--- description = 'A description of the command',
--- arguments = {'foo', 'bar'}, -- maps arguments to these names in the given sequence
2018-11-30 20:15:11 +02:00
--- default_values = {bar = false}, -- gives a default value to 'bar' when omitted
2019-01-30 05:52:43 +02:00
--- required_rank = Ranks.regular, -- defaults to Ranks.guest
--- donator_only = true, -- defaults to false
2019-01-02 17:34:17 +02:00
--- debug_only = true, -- registers the command if _DEBUG is set to true, defaults to false
--- cheat_only = true, -- registers the command if _CHEATS is set to true, defaults to false
2019-02-20 03:02:50 +02:00
--- allowed_by_server = true, -- lets the server execute this, defaults to false
--- allowed_by_player = false, -- lets players execute this, defaults to true
2018-11-24 01:34:02 +02:00
--- log_command = true, -- defaults to false unless admin only, then always true
2019-02-20 02:58:56 +02:00
--- capture_excess_arguments = true, -- defaults to false, captures excess arguments in the last argument, useful for sentences
2018-11-24 01:34:02 +02:00
---}
---
---The callback receives the following arguments:
--- - arguments (indexed by name, value is extracted from the parameters)
2018-11-24 15:47:33 +02:00
--- - the LuaPlayer or nil if it doesn't exist (such as the server player)
2018-11-24 01:34:02 +02:00
--- - 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 )
2019-03-02 19:20:56 +02:00
local description = options.description or { ' command.undocumented_command ' }
2018-11-24 01:34:02 +02:00
local arguments = options.arguments or { }
2018-11-24 21:44:02 +02:00
local default_values = options.default_values or { }
2019-01-30 05:52:43 +02:00
local required_rank = options.required_rank or Ranks.guest
local donator_only = options.donator_only or false
2018-11-24 21:44:02 +02:00
local debug_only = options.debug_only or false
2019-01-02 17:34:17 +02:00
local cheat_only = options.cheat_only or false
2018-11-24 22:41:05 +02:00
local capture_excess_arguments = options.capture_excess_arguments or false
2019-01-02 17:34:17 +02:00
local custom_help_text = options.custom_help_text or false
2018-11-24 21:44:02 +02:00
local allowed_by_server = options.allowed_by_server or false
local allowed_by_player = options.allowed_by_player
2019-01-30 05:52:43 +02:00
local log_command = options.log_command or ( required_rank >= Ranks.admin ) or false
2018-11-30 20:29:06 +02:00
local argument_list_size = table_size ( arguments )
2018-11-24 01:34:02 +02:00
local argument_list = ' '
2018-11-25 00:33:10 +02:00
assert_existing_options ( command_name , options )
2018-11-24 21:44:02 +02:00
if nil == options.allowed_by_player then
allowed_by_player = true
2018-11-24 01:34:02 +02:00
end
2019-01-02 17:34:17 +02:00
if ( not _DEBUG and debug_only ) and ( not _CHEATS and cheat_only ) then
2018-11-24 21:44:02 +02:00
return
end
if not allowed_by_player and not allowed_by_server then
2019-03-02 19:20:56 +02:00
error ( format ( " The command %s is not allowed by the server nor player, please enable at least one of them. " , command_name ) ) -- command.error_no_player_no_server when bug fixed
2018-11-24 21:44:02 +02:00
end
2018-11-30 20:21:46 +02:00
for index , argument_name in pairs ( arguments ) do
2018-11-24 21:44:02 +02:00
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
2018-11-24 22:41:05 +02:00
if argument_list_size == index and capture_excess_arguments then
argument_display = argument_display .. ' :sentence '
end
2018-11-24 21:44:02 +02:00
argument_list = format ( ' %s<%s> ' , argument_list , argument_display )
end
2019-03-02 19:20:56 +02:00
local extra = { ' ' }
2018-11-24 21:44:02 +02:00
if allowed_by_server and not allowed_by_player then
2019-03-02 19:20:56 +02:00
extra = { ' command.server_only ' }
2019-01-30 05:52:43 +02:00
elseif allowed_by_player and ( required_rank > Ranks.guest ) then
2019-02-14 21:28:50 +02:00
extra = { ' command.required_rank ' , get_rank_name ( required_rank ) }
2019-01-30 05:52:43 +02:00
elseif allowed_by_player and donator_only then
2019-03-02 19:20:56 +02:00
extra = { ' command.donator_only ' }
2018-11-24 21:44:02 +02:00
end
2019-03-02 19:20:56 +02:00
local help_text = { ' command.help_text_format ' , ( custom_help_text or argument_list ) , description , extra }
commands.add_command (
command_name ,
help_text ,
function ( command )
local print -- custom print reference in case no player is present
2021-05-20 21:51:26 +02:00
local player_index = command.player_index
local player = nil
if player_index ~= nil and player_index ~= 0 then
player = game.get_player ( player_index )
end
2019-03-02 19:20:56 +02:00
local player_name = player and player.valid and player.name or ' <server> '
if not player or not player.valid then
print = log
if not allowed_by_server then
print ( { ' command.not_allowed_by_server ' , command_name } )
return
end
else
print = player.print
2018-11-24 15:59:49 +02:00
2019-03-02 19:20:56 +02:00
if not allowed_by_player then
print ( { ' command.not_allowed_by_players ' , command_name } )
return
end
2018-11-24 21:44:02 +02:00
2019-03-02 19:20:56 +02:00
if Rank.less_than ( player_name , required_rank ) then
print ( { ' command.higher_rank_needed ' , command_name , get_rank_name ( required_rank ) } )
return
end
2019-01-02 15:42:18 +02:00
2019-03-02 19:20:56 +02:00
if donator_only and not Donator.is_donator ( player_name ) then
print ( { ' command.not_allowed_by_non_donators ' , command_name } )
return
end
2018-11-24 15:59:49 +02:00
end
2018-11-24 01:34:02 +02:00
2019-03-02 19:20:56 +02:00
local named_arguments = { }
local from_command = { }
local raw_parameter_index = 1
for param in 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
2018-11-24 22:41:05 +02:00
else
2019-03-02 19:20:56 +02:00
from_command [ raw_parameter_index ] = param
raw_parameter_index = raw_parameter_index + 1
2018-11-24 22:41:05 +02:00
end
end
2018-11-24 01:34:02 +02:00
2019-03-02 19:20:56 +02:00
local errors = { }
2018-11-24 15:59:49 +02:00
2019-03-02 19:20:56 +02:00
for index , argument in pairs ( arguments ) do
local parameter = from_command [ index ]
2018-11-24 21:44:02 +02:00
2019-03-02 19:20:56 +02:00
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
2018-11-24 21:44:02 +02:00
end
end
2019-03-02 19:20:56 +02:00
if parameter == nil then
insert ( errors , { ' command.fail_missing_argument ' , argument , command_name } )
else
named_arguments [ argument ] = parameter
end
2018-11-24 01:34:02 +02:00
end
2018-11-24 15:59:49 +02:00
2019-03-02 19:20:56 +02:00
local return_early = false
2018-11-24 01:34:02 +02:00
2019-03-02 19:20:56 +02:00
for _ , error in pairs ( errors ) do
return_early = true
print ( error )
2019-02-03 00:08:44 +02:00
end
2019-03-02 19:20:56 +02:00
if return_early then
return
end
2018-11-24 01:34:02 +02:00
2019-03-02 19:20:56 +02:00
if log_command then
local tick = ' pre-game '
if game then
tick = Utils.format_time ( game.tick )
end
local server_time = Server.get_current_time ( )
if server_time then
server_time = format ( ' (Server time: %s) ' , Timestamp.to_string ( server_time ) )
else
server_time = ' '
end
2019-03-04 00:13:56 +02:00
log ( { ' command.log_entry ' , server_time , tick , ( required_rank >= Ranks.admin ) and ' Admin ' or ' Player ' , player_name , command_name , serialize ( named_arguments ) } )
2018-11-25 00:33:10 +02:00
end
2018-11-30 19:50:57 +02:00
2019-03-02 19:20:56 +02:00
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 ( { ' command.error_while_running_debug ' , player_name , command_name , serialized_arguments } )
print ( error )
ErrorLogging.generate_error_report ( error )
return
end
print ( { ' command.warn_player_of_error ' , command_name } )
local err = { ' command.error_log ' , command_name , serialized_arguments , error }
log ( err )
ErrorLogging.generate_error_report ( err )
end
2018-11-24 01:34:02 +02:00
end
2019-03-02 19:20:56 +02:00
)
2018-11-24 01:34:02 +02:00
end
2019-02-02 03:46:12 +02:00
--- Trigger messages on deprecated or defined commands, ignores the server
local function on_command ( event )
2019-02-03 08:01:32 +02:00
if not event.player_index then
return
end
2019-01-02 17:34:17 +02:00
local alternative = deprecated_command_alternatives [ event.command ]
if alternative then
2019-05-16 12:10:56 +02:00
local player = game.get_player ( event.player_index )
2019-02-03 08:01:32 +02:00
if player then
2019-03-02 19:20:56 +02:00
player.print ( { ' command.warn_deprecated_command ' , event.command , alternative } )
2019-01-02 17:34:17 +02:00
end
end
2019-02-02 03:46:12 +02:00
local notification = notify_on_commands [ event.command ]
2019-02-02 19:11:31 +02:00
if notification and event.player_index then
2019-05-16 12:10:56 +02:00
local player = game.get_player ( event.player_index )
2019-02-03 08:01:32 +02:00
if player then
player.print ( notification )
end
2019-02-02 03:46:12 +02:00
end
2019-01-02 17:34:17 +02:00
end
--- Traps command errors if not in DEBUG.
if not _DEBUG then
local old_add_command = commands.add_command
2019-03-04 13:54:55 +02:00
commands.add_command =
function ( name , desc , func )
2019-01-02 17:34:17 +02:00
old_add_command (
name ,
desc ,
function ( cmd )
local success , error = pcall ( func , cmd )
if not success then
log ( error )
2019-03-02 19:20:56 +02:00
Game.player_print ( { ' command.failed_command ' , cmd.name } )
2019-01-02 17:34:17 +02:00
end
end
)
end
end
2019-02-02 03:46:12 +02:00
Event.add ( defines.events . on_console_command , on_command )
2019-01-02 17:34:17 +02:00
2021-04-19 22:28:58 +02:00
-- Backdoor for testing
if _DEBUG then
local EventCore = require ' utils.event_core '
local commands_store = { }
_G.commands_store = commands_store
local old_add_command = commands.add_command
commands.add_command = function ( name , desc , func )
old_add_command ( name , desc , func )
commands_store [ name ] = func
end
function Command . _raise_command ( name , player_index , parameter )
local func = commands_store [ name ] or error ( ' command \' ' .. name .. ' \' not found. ' , 2 )
func ( { name = name , tick = game.tick , player_index = player_index , parameter = parameter } )
EventCore.on_event ( {
name = defines.events . on_console_command ,
tick = game.tick ,
player_index = player_index ,
command = name ,
parameters = parameter } )
end
end
2018-11-24 01:34:02 +02:00
return Command