---@diagnostic disable: deprecated --luacheck: ignore 561 local Global = require 'utils.global' local Core = require 'utils.core' local Session = require 'utils.datastore.session_data' local Supporters = require 'utils.datastore.supporters' local Task = require 'utils.task_token' local Server = require 'utils.server' ---@class CommandData ---@field name string ---@field help string ---@field aliases table ---@field parameters table ---@field parameters_count number ---@field parameters_required number ---@field check_server boolean ---@field check_backend boolean ---@field check_admin boolean ---@field check_supporter boolean ---@field check_trusted boolean ---@field check_playtime number ---@field callback function ---@field validate_self boolean ---@field validated_command boolean ---@field validate_activated boolean ---@field command_activated boolean local this = { commands = {} } local trace = debug.traceback local output = { backend_is_required = 'No backend is currently available. Please try again later.', server_is_required = 'This command requires to be run from the server.', admin_is_required = 'This command requires admin permissions to run.', supporter_is_required = 'This command requires supporter permissions to run.', trusted_is_required = 'This command requires trusted permissions to run.', playtime_is_required = 'This command requires a minimum playtime to run.', param_is_required = 'This command requires a parameter to run.', command_failed = 'Command failed to run.', command_success = 'Command ran successfully.', command_needs_validation = 'This command requires validation to run. Please re-run the command if you wish to proceed.', command_needs_custom_validation = 'This command requires validation to run. %s - please re-run the command if you wish to proceed.', command_is_active = 'This command is already active.', command_is_inactive = 'This command is already inactive.' } local check_boolean = { ['true'] = true, ['false'] = true } ---@class MetaCommand local Public = {} Public.metatable = { __index = Public } Global.register( this, function (tbl) this = tbl for _, command in pairs(this.commands) do setmetatable(command, Public.metatable) end end ) local function conv(v) if tonumber(v) then return tonumber(v) end return v end --- Handles errors. ---@param message string ---@param notify_sound string local function handle_error(message, notify_sound) message = message or '' Core.output_message('Command failed: ' .. message, 'warning') if notify_sound then notify_sound = notify_sound or 'utility/wire_pickup' if game.player then game.player.play_sound { path = notify_sound } end end end --- Handles internal errors. ---@param has_run boolean ---@param name string ---@param message string ---@return boolean local function internal_error(has_run, name, message) if not has_run then handle_error('Action has been logged!', 'utility/cannot_build') if type(message) == 'string' then Server.output_data('[ERROR] Command failed to run: ' .. name .. ' - ' .. message) else Server.output_data('[ERROR] Command failed to run: ' .. name) end end return not has_run end ---@param event EventData.on_console_command local function execute(event) local command_data = this.commands[event.name] --[[@as CommandData]] local player if event.player_index and event.player_index > 0 then player = game.get_player(event.player_index) else player = { name = '', position = { x = 0, y = 0 }, surface = game.get_surface('nauvis'), force = game.forces.player, print = Server.output_data } end local is_server = event.player_index == nil local function reject(error_message) error_message = error_message or '' command_data.validated_command = false return handle_error(error_message, 'utility/cannot_build') end -- Check if player and return local check_server = command_data.check_server or false if (check_server and not is_server) and player and player.valid then reject(output.server_is_required) return end -- Check if player and return local check_backend = command_data.check_backend or false if (check_backend and not is_server) and event.player_index then if not Server.get_current_time() then reject(output.backend_is_required) return end end -- Check if the player is an admin and if the command requires it local check_admin = command_data.check_admin or false if (check_admin and not is_server) and player and not player.admin then reject(output.admin_is_required) return end -- Check if the player is trusted and if the command requires it local check_trusted = command_data.check_trusted or false if (check_trusted and not is_server) and Core.validate_player(player) then local is_trusted = Session.get_trusted_player(player) if not is_trusted then reject(output.trusted_is_required) return end end -- Check if the player is a supporter and if the command requires it local check_supporter = command_data.check_supporter or false if (check_supporter and not is_server) and Core.validate_player(player) then local is_supporter = Supporters.is_supporter(player.name) if not is_supporter then reject(output.supporter_is_required) return end end -- Check if the player has the required playtime and if the command requires it local check_playtime = command_data.check_playtime or false if (check_playtime and not is_server) and Core.validate_player(player) then local playtime = Session.get_session_player(player) if not playtime then reject(output.trusted_is_required) return end if playtime < check_playtime then reject(output.playtime_is_required) return end end -- Check for parameters if command_data.parameters_required > 0 and not event.parameter then reject(output.param_is_required) return end -- Check if the command requires the player to validate the command local validate_self = command_data.validate_self or false if validate_self and not command_data.validated_command then command_data.validated_command = true if command_data.custom_message then handle_error(string.format(output.command_needs_custom_validation, command_data.custom_message), 'utility/cannot_build') else handle_error(output.command_needs_validation, 'utility/cannot_build') end return end -- Extract quoted arguments local input_text = event.parameter or '' local quoted_segments = {} local processed_input = input_text:gsub( '"([^"]-)"', function (segment) local no_spaces_segment = segment:gsub('%s', '%%s') quoted_segments[no_spaces_segment] = segment return ' ' .. no_spaces_segment .. ' ' end ) -- Extract unquoted arguments local parameters = {} local current_index = 0 local parameter_count = 0 for word in processed_input:gmatch('%S+') do parameter_count = parameter_count + 1 local quoted_word = quoted_segments[word] local formatted_word = quoted_word and ('"' .. quoted_word .. '"') or word if parameter_count > command_data.parameters_count then parameters[current_index] = parameters[current_index] .. ' ' .. formatted_word else current_index = current_index + 1 parameters[current_index] = formatted_word end end -- Check the param count local parameters_count = #parameters if parameters_count < command_data.parameters_required then reject(output.param_is_required) return end -- Parse the arguments local index = 1 local handled_parameters = {} for _, param_data in pairs(command_data.parameters) do if param_data.as_type then local param = conv(parameters[index]) if param_data.as_type == 'player' and param ~= nil then local player_name = param if type(player_name) ~= 'string' then return reject('Inputted value is not of type string. Valid values are: "string"') end local player_data = game.get_player(player_name) --[[@type LuaPlayer]] if not player_data then return reject('Player was not found.') end handled_parameters[index] = player_data index = index + 1 end if param_data.as_type == 'surface' and param ~= nil then local surface_name = param if type(surface_name) ~= 'string' then return reject('Inputted value is not of type string. Valid values are: "string"') end local surface_data = game.get_surface(surface_name) --[[@type LuaSurface]] if not surface_data then return reject('Surface was not found.') end handled_parameters[index] = surface_data index = index + 1 end if param_data.as_type == 'player-online' and param ~= nil then local player_name = param if type(player_name) ~= 'string' then return reject('Inputted value is not of type string. Valid values are: "string"') end local player_data = game.get_player(player_name) --[[@type LuaPlayer]] if not player_data or not player_data.valid then return reject('Player was not found.') end if not player_data.connected then return reject('Player is not online.') end handled_parameters[index] = player_data index = index + 1 end if param_data.as_type == 'player-admin' and param ~= nil then local player_name = param if type(player_name) ~= 'string' then return reject('Inputted value is not of type string. Valid values are: "string"') end local player_data = game.get_player(player_name) --[[@type LuaPlayer]] if not player_data or not player_data.valid then return reject('Player was not found.') end if not player_data.admin then return reject('Player is not an admin.') end handled_parameters[index] = player_data index = index + 1 end if param_data.as_type == 'server' and param ~= nil then local player_name = param if type(player_name) ~= 'string' then return reject('Inputted value is not of type string. Valid values are: "string"') end local player_data = game.get_player(player_name) --[[@type LuaPlayer]] if player_data and player_data.valid then return reject('Not running from server.') end handled_parameters[index] = player_data index = index + 1 end if (param_data.as_type == 'number' or param_data.as_type == 'integer') and param ~= nil then local num = tonumber(param) if not num then return reject('Inputted value is not of type number. Valid values are: 1, 2, 3, etc.') end handled_parameters[index] = num index = index + 1 end if param_data.as_type == 'string' and param ~= nil then if type(param) ~= 'string' then return reject('Inputted value is not of type string. Valid values are: "string"') end handled_parameters[index] = param index = index + 1 end if param_data.as_type == 'boolean' and param ~= nil then if not check_boolean[param] then return reject('Inputted value is not of type boolean. Valid values are: true, false.') end if command_data.command_activated and param == 'true' then return handle_error(output.command_is_active, 'utility/cannot_build') end if not command_data.command_activated and param == 'false' then return handle_error(output.command_is_inactive, 'utility/cannot_build') end handled_parameters[index] = param index = index + 1 end end end -- Run the command callback if everything is validated local callback = Task.get(command_data.callback) local success, err = pcall(callback, player, unpack(handled_parameters)) if internal_error(success, command_data.name, err) then return reject(output.command_failed) end -- Check if the command can only be run once local validate_activated = command_data.validate_activated or false if validate_activated then if not command_data.command_activated then command_data.command_activated = true else command_data.command_activated = false end end command_data.validated_command = false if err ~= nil then if type(err) == 'boolean' then if err == false then Core.output_message(output.command_failed, 'warning') else Core.output_message(output.command_success, 'success') end else Core.output_message(err) end else Core.output_message(output.command_success, 'success') end end --- Creates a new command. ---@param name string ---@param help string ---@return MetaCommand function Public.new(name, help) if this.commands[name] then error('Command already exists: ' .. name, 2) end if game then error('Cannot run new() when game is initialized : ' .. name, 2) end local command = setmetatable( { name = name, help = help, aliases = {}, parameters = {}, parameters_count = 0, parameters_required = 0, check_admin = false, check_server = false, check_backend = false, check_supporter = false, check_trusted = false, check_playtime = false, validate_self = false, validated_command = false }, Public.metatable ) this.commands[name] = command return command end --- Requires the player to validate the command before running it. ---@param custom_message? string ---@return MetaCommand function Public:require_validation(custom_message) self.validate_self = true if custom_message then self.custom_message = custom_message end return self end --- Requires the player to validate the command before running it. ---@return MetaCommand function Public:is_activated() self.validate_activated = true return self end --- Requires the player to be an admin to run the command. ---@return MetaCommand function Public:require_admin() self.check_admin = true return self end --- Requires that the command is not run from a player. ---@return MetaCommand function Public:require_server() self.check_server = true return self end --- Requires that the server is connected to a backend ---@return MetaCommand function Public:require_backend() self.check_backend = true return self end --- Requires the player to be a supporter to run the command. ---@return MetaCommand function Public:require_supporter() self.check_supporter = true return self end --- Requires the player to be trusted to run the command. ---@return MetaCommand function Public:require_trusted() self.check_trusted = true return self end --- Requires the player to have a minimum playtime to run the command. ---@param playtime integer|number ---@return MetaCommand function Public:require_playtime(playtime) self.check_playtime = playtime or nil return self end --- Adds a parameter to the command. ---@param name string ---@param optional boolean ---@param as_type? type|string ---@return MetaCommand function Public:add_parameter(name, optional, as_type) if self.parameters[name] then error('Parameter: ' .. name .. ' already exists for command: ' .. self.name, 2) end self.parameters[name] = { optional = optional, as_type = as_type } self.parameters_count = self.parameters_count + 1 if not optional then self.parameters_required = self.parameters_required + 1 end return self end --- Adds an alias to the command. ---@param name string ---@return MetaCommand function Public:add_alias(name) if self.aliases[name] then error('Alias: ' .. name .. ' already exists for command: ' .. self.name, 2) end self.aliases[name] = name return self end --- Sets the command as default if marking paramaters as optional. ---@param defaults any ---@return MetaCommand function Public:set_default(defaults) for name, value in pairs(defaults) do if self.parameters[name] then self.parameters[name].default = value end end return self end --- Restores the command_activated state for each command function Public.restore_states() for _, command in pairs(this.commands) do command.validated_command = false command.command_activated = false end end --- Registers the command to the game. Will return the player/server and the args as separate arguments. ---@param func function function Public:callback(func) -- Generates a description to be used local description = '' for param_name, param_details in pairs(self.parameters) do if param_details.optional then description = string.format('%s [%s]', description, param_name) else description = string.format('%s <%s>', description, param_name) end end self.description = description -- If command fails to run, notify the player/server local function command_error(err) internal_error(false, self.name, trace(err)) end -- Registers the command as a token local id = Task.register(func) self.callback = id -- Callback local function command_callback(event) event.name = self.name xpcall(execute, command_error, event) end -- Lastly, adds the command to the game local help = description .. ' - ' .. self.help commands.add_command(self.name, help, command_callback) -- Adds any aliases if any for _, alias in pairs(self.aliases) do if not commands.commands[alias] and not commands.game_commands[alias] then commands.add_command(alias, help, command_callback) end end end local directions = { [0] = 'defines.direction.north', [1] = 'defines.direction.northnortheast', [2] = 'defines.direction.northeast', [3] = 'defines.direction.eastnortheast', [4] = 'defines.direction.east', [5] = 'defines.direction.eastsoutheast', [6] = 'defines.direction.southeast', [7] = 'defines.direction.southsoutheast', [8] = 'defines.direction.south', [9] = 'defines.direction.southsouthwest', [10] = 'defines.direction.southwest', [11] = 'defines.direction.westsouthwest', [12] = 'defines.direction.west', [13] = 'defines.direction.westnorthwest', [14] = 'defines.direction.northwest', [15] = 'defines.direction.northnorthwest', } Public.new('get', 'Hover over an object to get its name.') :require_admin() :add_parameter('die', true, 'string') :add_alias('entity') :callback( function (player, action) local entity = player.selected if not entity or not entity.valid then return false end if action and action == 'die' then entity.die() return true end player.print('[color=orange]Name:[/color] ' .. entity.name) player.print('[color=orange]Type:[/color] ' .. entity.type) player.print('[color=orange]Force:[/color] ' .. entity.force.name) player.print('[color=orange]Direction:[/color] ' .. entity.direction .. ' (' .. directions[entity.direction] .. ')') player.print('[color=orange]Destructible:[/color] ' .. (entity.destructible and 'true' or 'false')) player.print('[color=orange]Minable:[/color] ' .. (entity.minable and 'true' or 'false')) player.print('[color=orange]Unit Number:[/color] ' .. (entity.unit_number or 'nil')) player.print('[color=orange]Position:[/color] ' .. serpent.line(entity.position)) return true end ) return Public