1
0
mirror of https://github.com/ComfyFactory/ComfyFactorio.git synced 2025-11-25 22:32:18 +02:00
Files
ComfyFactorio/utils/commands.lua
Gerkiz 595b8caeb4 II: tweaks and changes
Some techs are now locked behind islands.
Text is printed out to chat when a new tech is unlocked.

Basic loot is added to the ammo chest whenever the player progresses manually towards the next island.

Fixed an issue with biters attacking an island too soon.

Fixed an issue with terrain generation.
2025-11-04 22:30:36 +01:00

662 lines
21 KiB
Lua

---@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 of %s 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 validate_types =
{
['string'] = true,
['number'] = true,
['integer'] = true,
['boolean'] = true,
['player'] = true,
['player-online'] = true,
['player-admin'] = true,
['server'] = true,
['surface'] = true
}
local check_boolean =
{
['true'] = true,
['false'] = true
}
---@class MetaCommand
local Public = {}
Public.metatable = { __index = Public }
Global.register(
this,
function (tbl)
this = tbl
end
)
script.register_metatable('CommandData', Public.metatable)
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_script_data('[ERROR] Command failed to run: ' .. name .. ' - ' .. message)
else
Server.output_script_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 = '<server>',
position = { x = 0, y = 0 },
surface = game.get_surface('nauvis'),
force = game.forces.player,
print = Server.output_script_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(string.format(output.playtime_is_required, Core.get_formatted_playtime(check_playtime)))
return
end
if playtime < check_playtime then
reject(string.format(output.playtime_is_required, Core.get_formatted_playtime(check_playtime)))
return
end
end
-- Check for parameters
if command_data.parameters_required > 0 and not event.parameter then
reject(output.param_is_required)
return
end
local is_multiplayer = game.is_multiplayer()
-- 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 and is_multiplayer 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
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
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
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
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 ]]
if param == 'true' then
param = true
else
param = false
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
---@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
---@alias ParamName
---| '"string"'
---| '"number"'
---| '"integer"'
---| '"boolean"'
---| '"player"'
---| '"player-online"'
---| '"player-admin"'
---| '"server"'
---| '"surface"'
--- Adds a parameter to the command.
---@param name string
---@param optional boolean
---@param as_type? ParamName
---@return MetaCommand
function Public:add_parameter(name, optional, as_type)
if not validate_types[as_type] then
error('Invalid type: ' .. as_type .. ' for parameter: ' .. name, 2)
end
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))
player.print('[color=orange]Active:[/color] ' .. (entity.active and 'true' or 'false'))
return true
end
)
return Public