Improvements for ExpCommands

This commit is contained in:
Cooldude2606
2024-11-08 12:59:12 +00:00
parent c61c931b58
commit c9bf85835f
9 changed files with 142 additions and 137 deletions

View File

@@ -9,13 +9,14 @@ The default permission authorities controlled by the flags: admin_only, system_o
]] ]]
local Storage = require("modules/exp_util/storage") local Storage = require("modules/exp_util/storage")
local Commands = require("modules/exp_commands")
local Commands = require("modules/exp_commands") --- @class Commands
local add, allow, deny = Commands.add_permission_authority, Commands.status.success, Commands.status.unauthorised local add, allow, deny = Commands.add_permission_authority, Commands.status.success, Commands.status.unauthorised
local authorities = {} local authorities = {}
local system_players = {} local system_players = {} --- @type table<string, boolean>
local disabled_commands = {} local disabled_commands = {} --- @type table<string, boolean>
Storage.register({ Storage.register({
system_players, system_players,
disabled_commands, disabled_commands,
@@ -100,7 +101,7 @@ authorities.system_only =
--- If Commands.disable was called then no one can use the command --- If Commands.disable was called then no one can use the command
authorities.disabled = authorities.disabled =
add(function(_player, command) add(function(player, command)
if disabled_commands[command.name] then if disabled_commands[command.name] then
return deny{ "exp-commands-authorities.disabled" } return deny{ "exp-commands-authorities.disabled" }
else else

View File

@@ -1,6 +1,5 @@
--[[-- Command Module - Help --[[-- Commands - Help
Game command to list and search all registered commands in a nice format Game command to list and search all registered commands in a nice format
@commands _system-ipc
--- Get all messages related to banning a player --- Get all messages related to banning a player
/commands ban /commands ban
@@ -13,13 +12,16 @@ local Commands = require("modules/exp_commands")
local PAGE_SIZE = 5 local PAGE_SIZE = 5
local search_cache = {} --- @alias ResultsPage LocalisedString[]
--- @class HelpCacheEntry: { keyword: string, pages: ResultsPage[], found: number }
local search_cache = {} --- @type table<number, HelpCacheEntry>
Storage.register(search_cache, function(tbl) Storage.register(search_cache, function(tbl)
search_cache = tbl search_cache = tbl
end) end)
--- Format commands into a strings across multiple pages --- Format commands into a strings across multiple pages
--- @param commands { [string]: Commands.Command } The commands to split into pages --- @param commands table<string, Commands.Command> The commands to split into pages
--- @param page_size number The number of requests to show per page --- @param page_size number The number of requests to show per page
--- @return LocalisedString[][], number --- @return LocalisedString[][], number
local function format_as_pages(commands, page_size) local function format_as_pages(commands, page_size)

View File

@@ -1,6 +1,5 @@
--[[-- Command Module - IPC --[[-- Commands - IPC
System command which sends an object to the clustorio api, should be used for debugging / echo commands System command which sends an object to the clustorio api, should be used for debugging / echo commands
@commands _system-ipc
--- Send a message on your custom channel, message is a json string --- Send a message on your custom channel, message is a json string
/_ipc myChannel { "myProperty": "foo", "playerName": "Cooldude2606" } /_ipc myChannel { "myProperty": "foo", "playerName": "Cooldude2606" }

View File

@@ -1,6 +1,5 @@
--[[-- Command Module - Rcon --[[-- Commands - Rcon
System command which runs arbitrary code within a custom (not sandboxed) environment System command which runs arbitrary code within a custom (not sandboxed) environment
@commands _system-rcon
--- Get the names of all online players, using rcon --- Get the names of all online players, using rcon
/_system-rcon local names = {}; for index, player in pairs(game.connected_player) do names[index] = player.name end; return names; /_system-rcon local names = {}; for index, player in pairs(game.connected_player) do names[index] = player.name end; return names;
@@ -12,53 +11,60 @@ System command which runs arbitrary code within a custom (not sandboxed) environ
local ExpUtil = require("modules/exp_util") local ExpUtil = require("modules/exp_util")
local Async = require("modules/exp_util/async") local Async = require("modules/exp_util/async")
local Storage = require("modules/exp_util/storage") local Storage = require("modules/exp_util/storage")
local Commands = require("modules/exp_commands")
local Clustorio = require("modules/clusterio/api") local Clustorio = require("modules/clusterio/api")
local rcon_env = {} local Commands = require("modules/exp_commands") --- @class Commands
local rcon_statics = {}
local rcon_callbacks = {} local rcon_env = {} --- @type table<string, any>
setmetatable(rcon_statics, { __index = _G }) local rcon_static = {} --- @type table<string, any>
setmetatable(rcon_env, { __index = rcon_statics }) local rcon_dynamic = {} --- @type table<string, ExpCommand.RconDynamic>
setmetatable(rcon_static, { __index = _G })
setmetatable(rcon_env, { __index = rcon_static })
--- Some common static values which can be added now --- Some common static values which can be added now
--- @diagnostic disable: name-style-check --- @diagnostic disable: name-style-check
rcon_statics.Async = Async rcon_static.Async = Async
rcon_statics.ExpUtil = ExpUtil rcon_static.ExpUtil = ExpUtil
rcon_statics.Commands = Commands rcon_static.Commands = Commands
rcon_statics.Clustorio = Clustorio rcon_static.Clustorio = Clustorio
rcon_statics.print = Commands.print rcon_static.print = Commands.print
rcon_statics.ipc = Clustorio.send_json rcon_static.ipc = Clustorio.send_json
--- @diagnostic enable: name-style-check --- @diagnostic enable: name-style-check
--- Some common callback values which are useful when a player uses the command --- Some common callback values which are useful when a player uses the command
function rcon_callbacks.player(player) return player end --- @alias ExpCommand.RconDynamic fun(player: LuaPlayer?): any
function rcon_callbacks.surface(player) return player and player.surface end function rcon_dynamic.player(player) return player end
function rcon_callbacks.force(player) return player and player.force end function rcon_dynamic.surface(player) return player and player.surface end
function rcon_callbacks.position(player) return player and player.position end function rcon_dynamic.force(player) return player and player.force end
function rcon_callbacks.entity(player) return player and player.selected end function rcon_dynamic.position(player) return player and player.position end
function rcon_callbacks.tile(player) return player and player.surface.get_tile(player.position) end function rcon_dynamic.entity(player) return player and player.selected end
function rcon_dynamic.tile(player) return player and player.surface.get_tile(player.position.x, player.position.y) end
--- The rcon env is saved between command runs to prevent desyncs --- The rcon env is saved between command runs to prevent desyncs
Storage.register(rcon_env, function(tbl) Storage.register(rcon_env, function(tbl)
rcon_env = setmetatable(tbl, { __index = rcon_statics }) rcon_env = setmetatable(tbl, { __index = rcon_static })
end) end)
--- Static values can be added to the rcon env which are not stored in global such as modules --- Static values can be added to the rcon env which are not stored in global such as modules
--- @param name string Name of the value as it will appear in the rcon environment
--- @param value any Value it is have
function Commands.add_rcon_static(name, value) function Commands.add_rcon_static(name, value)
ExpUtil.assert_not_runtime() ExpUtil.assert_not_runtime()
rcon_statics[name] = value rcon_static[name] = value
end end
--- Callback values can be added to the rcon env, these are called on each invocation and should return one value --- Callback values can be added to the rcon env, these are called on each invocation and should return one value
function Commands.add_rcon_callback(name, callback) --- @param name string Name of the value as it will appear in the rcon environment
--- @param callback ExpCommand.RconDynamic Callback called to get the current value
function Commands.add_rcon_dynamic(name, callback)
ExpUtil.assert_not_runtime() ExpUtil.assert_not_runtime()
rcon_callbacks[name] = callback rcon_dynamic[name] = callback
end end
Commands.new("_rcon", { "exp-commands_rcon.description" }) Commands.new("_rcon", { "exp-commands_rcon.description" })
@@ -70,7 +76,7 @@ Commands.new("_rcon", { "exp-commands_rcon.description" })
-- Construct the environment the command will run within -- Construct the environment the command will run within
local env = setmetatable({}, { __index = rcon_env, __newindex = rcon_env }) local env = setmetatable({}, { __index = rcon_env, __newindex = rcon_env })
for name, callback in pairs(rcon_callbacks) do for name, callback in pairs(rcon_dynamic) do
local _, rtn = pcall(callback, player.index > 0 and player or nil) local _, rtn = pcall(callback, player.index > 0 and player or nil)
rawset(env, name, rtn) rawset(env, name, rtn)
end end

View File

@@ -1,6 +1,5 @@
--[[-- Command Module - Sudo --[[-- Commands - Sudo
System command to execute a command as another player using their permissions (except for permissions group actions) System command to execute a command as another player using their permissions (except for permissions group actions)
@commands _system-sudo
--- Run the example command as another player --- Run the example command as another player
-- As Cooldude2606: /repeat 5 -- As Cooldude2606: /repeat 5

View File

@@ -25,10 +25,9 @@ local Commands = require("modules/exp_commands")
local add, parse = Commands.add_data_type, Commands.parse_input local add, parse = Commands.add_data_type, Commands.parse_input
local valid, invalid = Commands.status.success, Commands.status.invalid_input local valid, invalid = Commands.status.success, Commands.status.invalid_input
local types = {} local types = {} --- @class Commands._types
--- A boolean value where true is one of: yes, y, true, 1 --- A boolean value where true is one of: yes, y, true, 1
--- @type Commands.InputParser
types.boolean = types.boolean =
add("boolean", function(input) add("boolean", function(input)
input = input:lower() input = input:lower()
@@ -43,14 +42,12 @@ types.boolean =
end) end)
--- A string, validation does nothing but it is a requirement --- A string, validation does nothing but it is a requirement
--- @type Commands.InputParser
types.string = types.string =
add("string", function(input) add("string", function(input)
return valid(input) return valid(input)
end) end)
--- A string from a set of options, takes one argument which is an array of options --- A string from a set of options, takes one argument which is an array of options
--- @type Commands.InputParserFactory
types.enum = types.enum =
add("enum", function(options) add("enum", function(options)
--- @cast options string[] --- @cast options string[]
@@ -65,10 +62,9 @@ types.enum =
end) end)
--- A string which is the key of a table, takes one argument which is an map of string keys to values --- A string which is the key of a table, takes one argument which is an map of string keys to values
--- @type Commands.InputParserFactory
types.key_of = types.key_of =
add("key_of", function(map) add("key_of", function(map)
--- @cast map { [string]: any } --- @cast map table<string, any>
return function(input) return function(input)
local option = auto_complete(map, input, true) local option = auto_complete(map, input, true)
if option == nil then if option == nil then
@@ -80,7 +76,6 @@ types.key_of =
end) end)
--- A string with a maximum length, takes one argument which is the maximum length of a string --- A string with a maximum length, takes one argument which is the maximum length of a string
--- @type Commands.InputParserFactory
types.string_max_length = types.string_max_length =
add("string_max_length", function(maximum) add("string_max_length", function(maximum)
--- @cast maximum number --- @cast maximum number
@@ -94,7 +89,6 @@ types.string_max_length =
end) end)
--- A number --- A number
--- @type Commands.InputParser
types.number = types.number =
add("number", function(input) add("number", function(input)
local number = tonumber(input) local number = tonumber(input)
@@ -106,7 +100,6 @@ types.number =
end) end)
--- An integer, number which has been floored --- An integer, number which has been floored
--- @type Commands.InputParser
types.integer = types.integer =
add("integer", function(input) add("integer", function(input)
local number = tonumber(input) local number = tonumber(input)
@@ -118,7 +111,6 @@ types.integer =
end) end)
--- A number in a given inclusive range --- A number in a given inclusive range
--- @type Commands.InputParserFactory
types.number_range = types.number_range =
add("number_range", function(minimum, maximum) add("number_range", function(minimum, maximum)
--- @cast minimum number --- @cast minimum number
@@ -137,7 +129,6 @@ types.number_range =
end) end)
--- An integer in a given inclusive range --- An integer in a given inclusive range
--- @type Commands.InputParserFactory
types.integer_range = types.integer_range =
add("integer_range", function(minimum, maximum) add("integer_range", function(minimum, maximum)
--- @cast minimum number --- @cast minimum number
@@ -156,7 +147,6 @@ types.integer_range =
end) end)
--- A player who has joined the game at least once --- A player who has joined the game at least once
--- @type Commands.InputParser
types.player = types.player =
add("player", function(input) add("player", function(input)
local player = game.get_player(input) local player = game.get_player(input)
@@ -168,7 +158,6 @@ types.player =
end) end)
--- A player who is online --- A player who is online
--- @type Commands.InputParser
types.player_online = types.player_online =
add("player_online", function(input, player) add("player_online", function(input, player)
local success, status, result = parse(input, player, Commands.types.player) local success, status, result = parse(input, player, Commands.types.player)
@@ -183,8 +172,7 @@ types.player_online =
end) end)
--- A player who is online and alive --- A player who is online and alive
--- @type Commands.InputParser types.player_alive =
types.player_online =
add("player_alive", function(input, player) add("player_alive", function(input, player)
local success, status, result = parse(input, player, Commands.types.player_online) local success, status, result = parse(input, player, Commands.types.player_online)
--- @cast result LuaPlayer --- @cast result LuaPlayer
@@ -198,7 +186,6 @@ types.player_online =
end) end)
--- A force within the game --- A force within the game
--- @type Commands.InputParser
types.force = types.force =
add("force", function(input) add("force", function(input)
local force = game.forces[input] local force = game.forces[input]
@@ -210,7 +197,6 @@ types.force =
end) end)
--- A surface within the game --- A surface within the game
--- @type Commands.InputParser
types.surface = types.surface =
add("surface", function(input) add("surface", function(input)
local surface = game.surfaces[input] local surface = game.surfaces[input]
@@ -222,7 +208,6 @@ types.surface =
end) end)
--- A planet within the game --- A planet within the game
--- @type Commands.InputParser
types.planet = types.planet =
add("planet", function(input) add("planet", function(input)
local surface = game.planets[input] local surface = game.planets[input]
@@ -234,7 +219,6 @@ types.planet =
end) end)
--- A name of a color from the predefined list, too many colours to use string-key --- A name of a color from the predefined list, too many colours to use string-key
--- @type Commands.InputParser
types.color = types.color =
add("color", function(input) add("color", function(input)
local color = auto_complete(Commands.color, input, true) local color = auto_complete(Commands.color, input, true)

View File

@@ -1,5 +1,3 @@
color-tag=[color=__1__]__2__[/color]
[exp-commands] [exp-commands]
help=__1__- __2____3__ help=__1__- __2____3__
aliases=\n Aliaies: __1__ aliases=\n Aliaies: __1__

View File

@@ -57,6 +57,7 @@ end)
local ExpUtil = require("modules/exp_util") local ExpUtil = require("modules/exp_util")
local Search = require("modules/exp_commands/search") local Search = require("modules/exp_commands/search")
--- @class Commands
local Commands = { local Commands = {
color = ExpUtil.color, color = ExpUtil.color,
format_rich_text_color = ExpUtil.format_rich_text_color, format_rich_text_color = ExpUtil.format_rich_text_color,
@@ -64,10 +65,8 @@ local Commands = {
format_player_name = ExpUtil.format_player_name, format_player_name = ExpUtil.format_player_name,
format_player_name_locale = ExpUtil.format_player_name_locale, format_player_name_locale = ExpUtil.format_player_name_locale,
types = {}, --- @type { [string]: Commands.InputParser | Commands.InputParserFactory } Stores all input parsers and validators for different data types registered_commands = {}, --- @type table<string, Commands.ExpCommand> Stores a reference to all registered commands
registered_commands = {}, --- @type { [string]: Commands.ExpCommand } Stores a reference to all registered commands
permission_authorities = {}, --- @type Commands.PermissionAuthority[] Stores a reference to all active permission authorities permission_authorities = {}, --- @type Commands.PermissionAuthority[] Stores a reference to all active permission authorities
status = {}, -- Contains the different status values a command can return
--- @package Stores the event handlers --- @package Stores the event handlers
events = { events = {
@@ -77,6 +76,14 @@ local Commands = {
}, },
} }
--- @class Commands._status: table<string, Commands.Status>
--- Contains the different status values a command can return
Commands.status = {}
--- @class Commands._types: table<string, Commands.InputParser | Commands.InputParserFactory>
--- Stores all input parsers and validators for different data types
Commands.types = {}
--- @package --- @package
function Commands.on_init() Search.prepare(Commands.registered_commands) end function Commands.on_init() Search.prepare(Commands.registered_commands) end
@@ -91,7 +98,7 @@ end
--- @class Commands.Argument --- @class Commands.Argument
--- @field name string The name of the argument --- @field name string The name of the argument
--- @field description LocalisedString The description of the argument --- @field description LocalisedString? The description of the argument
--- @field input_parser Commands.InputParser The input parser for the argument --- @field input_parser Commands.InputParser The input parser for the argument
--- @field optional boolean True when the argument is optional --- @field optional boolean True when the argument is optional
--- @field default any? The default value of the argument --- @field default any? The default value of the argument
@@ -135,15 +142,17 @@ Commands.server = setmetatable({
show_on_map = false, show_on_map = false,
valid = true, valid = true,
object_name = "LuaPlayer", object_name = "LuaPlayer",
print = rcon.print,
}, { }, {
-- To prevent unnecessary logging Commands.error is called here and error is filtered by command_callback
__index = function(_, key) __index = function(_, key)
if key == "__self" or type(key) == "number" then return nil end if key == "__self" or type(key) == "number" then return nil end
Commands.error("Command does not support rcon usage, requires reading player." .. key) Commands.error("Command does not support rcon usage, requires LuaPlayer." .. key)
error("Command does not support rcon usage, requires reading player." .. key) error("Command does not support rcon usage, requires LuaPlayer." .. key)
end, end,
__newindex = function(_, key) __newindex = function(_, key)
Commands.error("Command does not support rcon usage, requires reading player." .. key) Commands.error("Command does not support rcon usage, requires LuaPlayer." .. key)
error("Command does not support rcon usage, requires setting player." .. key) error("Command does not support rcon usage, requires LuaPlayer." .. key)
end, end,
}) })
@@ -155,7 +164,6 @@ Commands.server = setmetatable({
--- Used to signal success from a command, data type parser, or permission authority --- Used to signal success from a command, data type parser, or permission authority
--- @param msg LocalisedString? An optional message to be included when a command completes (only has an effect in command callbacks) --- @param msg LocalisedString? An optional message to be included when a command completes (only has an effect in command callbacks)
--- @return Commands.Status, LocalisedString # Should be returned directly without modification --- @return Commands.Status, LocalisedString # Should be returned directly without modification
--- @type Commands.Status
function Commands.status.success(msg) function Commands.status.success(msg)
return Commands.status.success, msg or { "exp-commands.success" } return Commands.status.success, msg or { "exp-commands.success" }
end end
@@ -164,7 +172,6 @@ end
--- For data type parsers and permission authority, an error return will prevent the command from being executed --- For data type parsers and permission authority, an error return will prevent the command from being executed
--- @param msg LocalisedString? An optional error message to be included in the output, a generic message is used if not provided --- @param msg LocalisedString? An optional error message to be included in the output, a generic message is used if not provided
--- @return Commands.Status, LocalisedString # Should be returned directly without modification --- @return Commands.Status, LocalisedString # Should be returned directly without modification
--- @type Commands.Status
function Commands.status.error(msg) function Commands.status.error(msg)
return Commands.status.error, { "exp-commands.error", msg or { "exp-commands.error-default" } } return Commands.status.error, { "exp-commands.error", msg or { "exp-commands.error-default" } }
end end
@@ -173,7 +180,6 @@ end
--- For permission authorities, an unauthorised return will prevent the command from being executed --- For permission authorities, an unauthorised return will prevent the command from being executed
--- @param msg LocalisedString? An optional error message to be included in the output, a generic message is used if not provided --- @param msg LocalisedString? An optional error message to be included in the output, a generic message is used if not provided
--- @return Commands.Status, LocalisedString # Should be returned directly without modification --- @return Commands.Status, LocalisedString # Should be returned directly without modification
--- @type Commands.Status
function Commands.status.unauthorised(msg) function Commands.status.unauthorised(msg)
return Commands.status.unauthorised, msg or { "exp-commands.unauthorized", msg or { "exp-commands.unauthorized-default" } } return Commands.status.unauthorised, msg or { "exp-commands.unauthorized", msg or { "exp-commands.unauthorized-default" } }
end end
@@ -182,7 +188,6 @@ end
--- For data type parsers, an invalid_input return will prevent the command from being executed --- For data type parsers, an invalid_input return will prevent the command from being executed
--- @param msg LocalisedString? An optional error message to be included in the output, a generic message is used if not provided --- @param msg LocalisedString? An optional error message to be included in the output, a generic message is used if not provided
--- @return Commands.Status, LocalisedString # Should be returned directly without modification --- @return Commands.Status, LocalisedString # Should be returned directly without modification
--- @type Commands.Status
function Commands.status.invalid_input(msg) function Commands.status.invalid_input(msg)
return Commands.status.invalid_input, msg or { "exp-commands.invalid-input" } return Commands.status.invalid_input, msg or { "exp-commands.invalid-input" }
end end
@@ -191,12 +196,11 @@ end
--- @param msg LocalisedString A message detailing the error which has occurred, will be logged and outputted --- @param msg LocalisedString A message detailing the error which has occurred, will be logged and outputted
--- @return Commands.Status, LocalisedString # Should be returned directly without modification --- @return Commands.Status, LocalisedString # Should be returned directly without modification
--- @package --- @package
--- @type Commands.Status
function Commands.status.internal_error(msg) function Commands.status.internal_error(msg)
return Commands.status.internal_error, { "exp-commands.internal-error", msg } return Commands.status.internal_error, { "exp-commands.internal-error", msg }
end end
--- @type { [Commands.Status]: string } --- @type table<Commands.Status, string>
local valid_command_status = {} -- Hashmap lookup for testing if a status is valid local valid_command_status = {} -- Hashmap lookup for testing if a status is valid
for name, status in pairs(Commands.status) do for name, status in pairs(Commands.status) do
valid_command_status[status] = name valid_command_status[status] = name
@@ -205,7 +209,7 @@ end
--- Permission Authority. --- Permission Authority.
-- Functions that control who can use commands -- Functions that control who can use commands
--- @alias Commands.PermissionAuthority fun(player: LuaPlayer, command: Commands.ExpCommand): boolean | Commands.Status, LocalisedString? --- @alias Commands.PermissionAuthority fun(player: LuaPlayer, command: Commands.ExpCommand): boolean|Commands.Status, LocalisedString?
--- Add a permission authority, a permission authority is a function which provides access control for commands, multiple can be active at once --- Add a permission authority, a permission authority is a function which provides access control for commands, multiple can be active at once
--- When multiple are active, all authorities must give permission for the command to execute, if any deny access then the command is not ran --- When multiple are active, all authorities must give permission for the command to execute, if any deny access then the command is not ran
@@ -241,7 +245,7 @@ end
--- @param player LuaPlayer? The player to test the permission of, nil represents the server and always returns true --- @param player LuaPlayer? The player to test the permission of, nil represents the server and always returns true
--- @param command Commands.ExpCommand The command the player is attempting to use --- @param command Commands.ExpCommand The command the player is attempting to use
--- @return boolean # True if the player has permission to use the command --- @return boolean # True if the player has permission to use the command
--- @return LocalisedString # When permission is denied, this is the reason permission was denied --- @return LocalisedString? # When permission is denied, this is the reason permission was denied
function Commands.player_has_permission(player, command) function Commands.player_has_permission(player, command)
if player == nil or player == Commands.server then return true end if player == nil or player == Commands.server then return true end
@@ -257,9 +261,7 @@ function Commands.player_has_permission(player, command)
return false, msg return false, msg
end end
else else
local class_name = ExpUtil.get_class_name(status) error("Permission authority returned unexpected value: " .. ExpUtil.get_class_name(status))
local _, rtn_msg = Commands.status.internal_error("Permission authority returned unexpected value: " .. class_name)
return false, rtn_msg
end end
end end
@@ -269,16 +271,14 @@ end
--- Data Type Parsing. --- Data Type Parsing.
-- Functions that parse and validate player input -- Functions that parse and validate player input
--- @generic T --- @alias Commands.InputParser<T> fun(input: string, player: LuaPlayer): Commands.Status, (T | LocalisedString)
--- @alias Commands.InputParser (fun(input: string, player: LuaPlayer): T) | (fun(input: string, player: LuaPlayer): Commands.Status, LocalisedString | T)
--- @generic T --- @alias Commands.InputParserFactory<T> fun(...: any): Commands.InputParser<T>
--- @alias Commands.InputParserFactory fun(...: any): Commands.InputParser<T>
--- Add a new input parser to the command library, this method validates that it does not already exist --- Add a new input parser to the command library, this method validates that it does not already exist
--- @generic T : Commands.InputParser | Commands.InputParserFactory --- @generic T : Commands.InputParser | Commands.InputParserFactory
--- @param data_type string The name of the data type the input parser reads in and validates, becomes a key of Commands.types --- @param data_type string The name of the data type the input parser reads in and validates, becomes a key of Commands.types
--- @param input_parser `T` The function used to parse and validate the data type --- @param input_parser T The function used to parse and validate the data type
--- @return T # The function which was provided as the second argument --- @return T # The function which was provided as the second argument
function Commands.add_data_type(data_type, input_parser) function Commands.add_data_type(data_type, input_parser)
if Commands.types[data_type] then if Commands.types[data_type] then
@@ -310,33 +310,25 @@ end
--- @return Commands.Status status, T | LocalisedString result # If success is false then Remaining values should be returned directly without modification --- @return Commands.Status status, T | LocalisedString result # If success is false then Remaining values should be returned directly without modification
function Commands.parse_input(input, player, input_parser) function Commands.parse_input(input, player, input_parser)
local status, status_msg = input_parser(input, player) local status, status_msg = input_parser(input, player)
if status == nil then if status == nil or not valid_command_status[status] then
local data_type = table.get_key(Commands.types, input_parser) or ExpUtil.get_function_name(input_parser, true) local data_type = table.get_key(Commands.types, input_parser) or ExpUtil.get_function_name(input_parser, true)
local rtn_status, rtn_msg = Commands.status.internal_error("Parser for data type \"" .. data_type .. "\" returned a nil value") error("Parser for data type \"" .. data_type .. "\" did not return a valid status got: " .. ExpUtil.get_class_name(status))
return false, rtn_status, rtn_msg
elseif valid_command_status[status] then
if status ~= Commands.status.success then
return false, status, status_msg
else
return true, status, status_msg -- status_msg is the parsed data
end
else
return true, Commands.status.success, status -- status is the parsed data
end end
return status == Commands.status.success, status, status_msg
end end
--- List and Search --- List and Search
-- Functions used to list and search for commands -- Functions used to list and search for commands
--- Returns a list of all registered custom commands --- Returns a list of all registered custom commands
--- @return { [string]: Commands.ExpCommand } # A dictionary of commands --- @return table<string,Commands.ExpCommand> # A dictionary of commands
function Commands.list_all() function Commands.list_all()
return Commands.registered_commands return Commands.registered_commands
end end
--- Returns a list of all registered custom commands which the given player has permission to use --- Returns a list of all registered custom commands which the given player has permission to use
--- @param player LuaPlayer? The player to get the command of, nil represents the server but list_all should be used --- @param player LuaPlayer? The player to get the command of, nil represents the server but list_all should be used
--- @return { [string]: Commands.ExpCommand } # A dictionary of commands --- @return table<string,Commands.ExpCommand> # A dictionary of commands
function Commands.list_for_player(player) function Commands.list_for_player(player)
local rtn = {} local rtn = {}
@@ -351,7 +343,7 @@ end
--- Searches all custom commands and game commands for the given keyword --- Searches all custom commands and game commands for the given keyword
--- @param keyword string The keyword to search for --- @param keyword string The keyword to search for
--- @return { [string]: Commands.Command } # A dictionary of commands --- @return table<string,Commands.Command> # A dictionary of commands
function Commands.search_all(keyword) function Commands.search_all(keyword)
return Search.search_commands(keyword, Commands.list_all(), "en") return Search.search_commands(keyword, Commands.list_all(), "en")
end end
@@ -359,7 +351,7 @@ end
--- Searches custom commands allowed for this player and all game commands for the given keyword --- Searches custom commands allowed for this player and all game commands for the given keyword
--- @param keyword string The keyword to search for --- @param keyword string The keyword to search for
--- @param player LuaPlayer? The player to search the commands of, nil represents server but search_all should be used --- @param player LuaPlayer? The player to search the commands of, nil represents server but search_all should be used
--- @return { [string]: Commands.Command } # A dictionary of commands --- @return table<string,Commands.Command> # A dictionary of commands
function Commands.search_for_player(keyword, player) function Commands.search_for_player(keyword, player)
return Search.search_commands(keyword, Commands.list_for_player(player), player and player.locale) return Search.search_commands(keyword, Commands.list_for_player(player), player and player.locale)
end end
@@ -368,7 +360,13 @@ end
-- Prints output to the player or rcon connection -- Prints output to the player or rcon connection
local print_format_options = { max_line_count = 20 } local print_format_options = { max_line_count = 20 }
local print_default_settings = { sound_path = "utility/scenario_message" } local print_settings_default = { sound_path = "utility/scenario_message", color = ExpUtil.color.white }
local print_settings_error = { sound_path = "utility/wire_pickup", color = ExpUtil.color.orange_red }
Commands.print_settings = {
default = print_settings_default,
error = print_settings_error,
}
--- Print a message to the user of a command, accepts any value and will print in a readable and safe format --- Print a message to the user of a command, accepts any value and will print in a readable and safe format
--- @param message any The message / value to be printed --- @param message any The message / value to be printed
@@ -379,9 +377,9 @@ function Commands.print(message, settings)
rcon.print(ExpUtil.format_any(message)) rcon.print(ExpUtil.format_any(message))
else else
if not settings then if not settings then
settings = print_default_settings settings = print_settings_default
elseif not settings.sound_path then elseif not settings.sound_path then
settings.sound_path = print_default_settings.sound_path settings.sound_path = print_settings_default.sound_path
end end
player.print(ExpUtil.format_any(message, print_format_options), settings) player.print(ExpUtil.format_any(message, print_format_options), settings)
end end
@@ -390,18 +388,21 @@ end
--- Print an error message to the user of a command, accepts any value and will print in a readable and safe format --- Print an error message to the user of a command, accepts any value and will print in a readable and safe format
--- @param message any The message / value to be printed --- @param message any The message / value to be printed
function Commands.error(message) function Commands.error(message)
Commands.print(message, { Commands.print(message, print_settings_error)
color = ExpUtil.color.orange_red,
sound_path = "utility/wire_pickup",
})
end end
--- Command Prototype --- Command Prototype
-- The prototype definition for command objects -- The prototype definition for command objects
local function assert_command_mutable(command)
if not Commands.registered_commands[command.name] then
error("Command cannot be modified after being registered.", 3)
end
end
--- Returns a new command object, this will not register the command but act as a way to start construction --- Returns a new command object, this will not register the command but act as a way to start construction
--- @param name string The name of the command as it will be registered later --- @param name string The name of the command as it will be registered later
--- @param description LocalisedString The description of the command displayed in the help message --- @param description LocalisedString? The description of the command displayed in the help message
--- @return Commands.ExpCommand --- @return Commands.ExpCommand
function Commands.new(name, description) function Commands.new(name, description)
ExpUtil.assert_argument_type(name, "string", 1, "name") ExpUtil.assert_argument_type(name, "string", 1, "name")
@@ -411,7 +412,7 @@ function Commands.new(name, description)
return setmetatable({ return setmetatable({
name = name, name = name,
description = description, description = description or "",
help_text = description, -- Will be replaced in command:register help_text = description, -- Will be replaced in command:register
callback = default_command_callback, -- Will be replaced in command:register callback = default_command_callback, -- Will be replaced in command:register
defined_at = ExpUtil.safe_file_path(2), defined_at = ExpUtil.safe_file_path(2),
@@ -426,10 +427,11 @@ end
--- Add a new required argument to the command of the given data type --- Add a new required argument to the command of the given data type
--- @param name string The name of the argument being added --- @param name string The name of the argument being added
--- @param description LocalisedString The description of the argument being added --- @param description LocalisedString? The description of the argument being added
--- @param input_parser Commands.InputParser The input parser to be used for the argument --- @param input_parser Commands.InputParser The input parser to be used for the argument
--- @return Commands.ExpCommand --- @return Commands.ExpCommand
function Commands._prototype:argument(name, description, input_parser) function Commands._prototype:argument(name, description, input_parser)
assert_command_mutable(self)
if self.min_arg_count ~= self.max_arg_count then if self.min_arg_count ~= self.max_arg_count then
error("Can not have required arguments after optional arguments", 2) error("Can not have required arguments after optional arguments", 2)
end end
@@ -446,10 +448,11 @@ end
--- Add a new optional argument to the command of the given data type --- Add a new optional argument to the command of the given data type
--- @param name string The name of the argument being added --- @param name string The name of the argument being added
--- @param description LocalisedString The description of the argument being added --- @param description LocalisedString? The description of the argument being added
--- @param input_parser Commands.InputParser The input parser to be used for the argument --- @param input_parser Commands.InputParser The input parser to be used for the argument
--- @return Commands.ExpCommand --- @return Commands.ExpCommand
function Commands._prototype:optional(name, description, input_parser) function Commands._prototype:optional(name, description, input_parser)
assert_command_mutable(self)
self.max_arg_count = self.max_arg_count + 1 self.max_arg_count = self.max_arg_count + 1
self.arguments[#self.arguments + 1] = { self.arguments[#self.arguments + 1] = {
name = name, name = name,
@@ -461,9 +464,10 @@ function Commands._prototype:optional(name, description, input_parser)
end end
--- Set the defaults for optional arguments, any not provided will have their value as nil --- Set the defaults for optional arguments, any not provided will have their value as nil
--- @param defaults table The default values for the optional arguments, the key is the name of the argument --- @param defaults table<string, (fun(player: LuaPlayer): any) | any> The default values for the optional arguments, the key is the name of the argument
--- @return Commands.ExpCommand --- @return Commands.ExpCommand
function Commands._prototype:defaults(defaults) function Commands._prototype:defaults(defaults)
assert_command_mutable(self)
local matched = {} local matched = {}
for _, argument in ipairs(self.arguments) do for _, argument in ipairs(self.arguments) do
if defaults[argument.name] then if defaults[argument.name] then
@@ -489,6 +493,7 @@ end
--- @param flags table An array of strings or a dictionary of flag names and values, when an array is used the flags values are set to true --- @param flags table An array of strings or a dictionary of flag names and values, when an array is used the flags values are set to true
--- @return Commands.ExpCommand --- @return Commands.ExpCommand
function Commands._prototype:add_flags(flags) function Commands._prototype:add_flags(flags)
assert_command_mutable(self)
for name, value in pairs(flags) do for name, value in pairs(flags) do
if type(name) == "number" then if type(name) == "number" then
self.flags[value] = true self.flags[value] = true
@@ -504,6 +509,7 @@ end
--- @param aliases string[] An array of string names to use as aliases to this command --- @param aliases string[] An array of string names to use as aliases to this command
--- @return Commands.ExpCommand --- @return Commands.ExpCommand
function Commands._prototype:add_aliases(aliases) function Commands._prototype:add_aliases(aliases)
assert_command_mutable(self)
local start_index = #self.aliases local start_index = #self.aliases
for index, alias in ipairs(aliases) do for index, alias in ipairs(aliases) do
self.aliases[start_index + index] = alias self.aliases[start_index + index] = alias
@@ -515,32 +521,41 @@ end
--- Enable concatenation of all arguments after the last, this should be used for user provided reason text --- Enable concatenation of all arguments after the last, this should be used for user provided reason text
--- @return Commands.ExpCommand --- @return Commands.ExpCommand
function Commands._prototype:enable_auto_concatenation() function Commands._prototype:enable_auto_concatenation()
assert_command_mutable(self)
self.auto_concat = true self.auto_concat = true
return self return self
end end
--- Register the command to the game with the given callback, this must be the final step as the object becomes immutable afterwards --- Register the command to the game with the given callback, this must be the final step as the object becomes immutable afterwards
--- @param callback Commands.Callback The function which is called to perform the command action --- @param callback Commands.Callback The function which is called to perform the command action
--- @return Commands.ExpCommand
function Commands._prototype:register(callback) function Commands._prototype:register(callback)
assert_command_mutable(self)
Commands.registered_commands[self.name] = self Commands.registered_commands[self.name] = self
self.callback = callback self.callback = callback
-- Generates a description to be used -- Generates a description to be used
local argument_names = { "" } --- @type LocalisedString local argument_names = { "" } --- @type LocalisedString
local argument_help_text = { "", "" } --- @type LocalisedString local argument_verbose = { "" } --- @type LocalisedString
local help_text = { "exp-commands.help", argument_names, self.description, argument_help_text } --- @type LocalisedString self.help_text = { "exp-commands.help", argument_names, self.description, argument_verbose } --- @type LocalisedString
self.help_text = help_text
if next(self.aliases) then if next(self.aliases) then
argument_help_text[2] = { "exp-commands.aliases", table.concat(self.aliases, ", ") } argument_verbose[2] = { "exp-commands.aliases", table.concat(self.aliases, ", ") }
end end
local verbose_index = #argument_verbose
for index, argument in pairs(self.arguments) do for index, argument in pairs(self.arguments) do
if argument.optional then if argument.optional then
argument_names[index + 2] = { "exp-commands.optional", argument.name } argument_names[index + 1] = { "exp-commands.optional", argument.name }
argument_help_text[index + 2] = { "exp-commands.optional-verbose", argument.name, argument.description } if argument.description and argument.description ~= "" then
verbose_index = verbose_index + 1
argument_verbose[verbose_index] = { "exp-commands.optional-verbose", argument.name, argument.description }
end
else else
argument_names[index + 2] = { "exp-commands.argument", argument.name } argument_names[index + 1] = { "exp-commands.argument", argument.name }
argument_help_text[index + 2] = { "exp-commands.argument-verbose", argument.name, argument.description } if argument.description and argument.description ~= "" then
verbose_index = verbose_index + 1
argument_verbose[verbose_index] = { "exp-commands.argument-verbose", argument.name, argument.description }
end
end end
end end
@@ -565,6 +580,8 @@ function Commands._prototype:register(callback)
for _, alias in ipairs(self.aliases) do for _, alias in ipairs(self.aliases) do
commands.add_command(alias, self.help_text, command_callback) commands.add_command(alias, self.help_text, command_callback)
end end
return self
end end
--- Command Runner --- Command Runner
@@ -633,7 +650,6 @@ end
--- Internal event handler for the command event --- Internal event handler for the command event
--- @param event CustomCommandData --- @param event CustomCommandData
--- @return nil --- @return nil
--- @package
function Commands._event_handler(event) function Commands._event_handler(event)
local command = Commands.registered_commands[event.name] local command = Commands.registered_commands[event.name]
if command == nil then if command == nil then
@@ -695,20 +711,20 @@ function Commands._event_handler(event)
end end
end end
-- Run the command, dont need xpcall here because errors are caught in command_callback -- Run the command, don't need xpcall here because errors are caught in command_callback
local status, status_msg = command.callback(player, table.unpack(arguments)) local status, status_msg = command.callback(player, table.unpack(arguments))
if status and valid_command_status[status] then if status == nil then
if status ~= Commands.status.success then
log_command("Custom Error", command, player, event.parameter, status_msg)
return Commands.error(status_msg)
else
log_command("Command Ran", command, player, event.parameter)
return Commands.print(status_msg)
end
else
log_command("Command Ran", command, player, event.parameter) log_command("Command Ran", command, player, event.parameter)
local _, msg = Commands.status.success() local _, msg = Commands.status.success()
return Commands.print(msg) return Commands.print(msg)
elseif not valid_command_status[status] then
error("Command \"" .. command.name .. "\" did not return a valid status got: " .. ExpUtil.get_class_name(status))
elseif status ~= Commands.status.success then
log_command("Custom Error", command, player, event.parameter, status_msg)
return Commands.error(status_msg)
else
log_command("Command Ran", command, player, event.parameter)
return Commands.print(status_msg)
end end
end end

View File

@@ -3,8 +3,8 @@ local Search = {}
local Storage = require("modules/exp_util/storage") local Storage = require("modules/exp_util/storage")
--- Setup the storage to contain the pending translations and the completed ones --- Setup the storage to contain the pending translations and the completed ones
local pending = {} --- @type { [uint]: { [1]: string, [2]: string }? } local pending = {} --- @type { [1]: string, [2]: string }[]
local translations = {} --- @type { [string]: { [string]: string } } local translations = {} --- @type table<string, table<string, string>>
Storage.register({ Storage.register({
pending, pending,
translations, translations,
@@ -14,13 +14,13 @@ Storage.register({
end) end)
local command_names = {} --- @type string[] local command_names = {} --- @type string[]
local command_objects = {} --- @type { [string]: Commands.Command } local command_objects = {} --- @type table<string, Commands.Command>
local required_translations = {} --- @type LocalisedString[] local required_translations = {} --- @type LocalisedString[]
--- Gets the descriptions of all commands, not including their aliases --- Gets the descriptions of all commands, not including their aliases
--- @param custom_commands { [string]: Commands.ExpCommand } The complete list of registered custom commands --- @param custom_commands table<string, Commands.ExpCommand> The complete list of registered custom commands
function Search.prepare(custom_commands) function Search.prepare(custom_commands)
local known_aliases = {} --- @type { [string]: string } local known_aliases = {} --- @type table<string, string>
for name, command in pairs(custom_commands) do for name, command in pairs(custom_commands) do
for _, alias in ipairs(command.aliases) do for _, alias in ipairs(command.aliases) do
known_aliases[alias] = name known_aliases[alias] = name
@@ -85,11 +85,11 @@ end
--- Searches all game commands and the provided custom commands for the given keyword --- Searches all game commands and the provided custom commands for the given keyword
--- @param keyword string The keyword to search for --- @param keyword string The keyword to search for
--- @param custom_commands { [string]: Commands.ExpCommand } A dictionary of commands to search --- @param custom_commands table<string, Commands.ExpCommand> A dictionary of commands to search
--- @param locale string? The local to search, default is english ("en") --- @param locale string? The local to search, default is english ("en")
--- @return { [string]: Commands.Command } # A dictionary of commands --- @return table<string, Commands.Command> # A dictionary of commands
function Search.search_commands(keyword, custom_commands, locale) function Search.search_commands(keyword, custom_commands, locale)
local rtn = {} --- @type { [string]: Commands.Command } local rtn = {} --- @type table<string, Commands.Command>
keyword = keyword:lower() keyword = keyword:lower()
locale = locale or "en" locale = locale or "en"