mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2025-12-27 03:25:23 +09:00
Add Clusterio Plugins
This commit is contained in:
8
exp_commands/index.ts
Normal file
8
exp_commands/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
export const plugin: lib.PluginDeclaration = {
|
||||
name: "exp_commands",
|
||||
title: "exp_commands",
|
||||
description: "Example Description. Plugin. Change me in index.ts",
|
||||
instanceEntrypoint: "./dist/node/instance",
|
||||
};
|
||||
7
exp_commands/instance.ts
Normal file
7
exp_commands/instance.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { BaseInstancePlugin } from "@clusterio/host";
|
||||
|
||||
export class InstancePlugin extends BaseInstancePlugin {
|
||||
// This class is empty because an instance plugin must be defined for a module to be injected
|
||||
// This requirement may change in the future to allow for standalone modules
|
||||
}
|
||||
179
exp_commands/module/commands/data_types.lua
Normal file
179
exp_commands/module/commands/data_types.lua
Normal file
@@ -0,0 +1,179 @@
|
||||
--[[-- Command Module - Default data types
|
||||
The default data types that are available to all commands
|
||||
|
||||
@usage Adds parsers for:
|
||||
boolean
|
||||
string-options - options: array of strings
|
||||
string-key - map: table of string keys and any values
|
||||
string-max-length - maximum: number
|
||||
number
|
||||
integer
|
||||
number-range - minimum: number, maximum: number
|
||||
integer-range - minimum: number, maximum: number
|
||||
player
|
||||
player-online
|
||||
player-alive
|
||||
force
|
||||
surface
|
||||
color
|
||||
]]
|
||||
|
||||
local ExpUtil = require("modules/exp_util")
|
||||
local Commands = require("modules/exp_commands")
|
||||
local add, parse = Commands.add_data_type, Commands.parse_data_type
|
||||
local valid, invalid = Commands.status.success, Commands.status.invalid_input
|
||||
|
||||
--- A boolean value where true is one of: yes, y, true, 1
|
||||
add("boolean", function(input)
|
||||
input = input:lower()
|
||||
if input == "yes"
|
||||
or input == "y"
|
||||
or input == "true"
|
||||
or input == "1" then
|
||||
return valid(true)
|
||||
else
|
||||
return valid(false)
|
||||
end
|
||||
end)
|
||||
|
||||
--- A string, validation does nothing but it is a requirement
|
||||
add("string", function(input)
|
||||
return valid(input)
|
||||
end)
|
||||
|
||||
--- A string from a set of options, takes one argument which is an array of options
|
||||
add("string-options", function(input, _, options)
|
||||
local option = ExpUtil.auto_complete(options, input)
|
||||
if option == nil then
|
||||
return invalid{"exp-commands-parse.string-options", table.concat(options, ", ")}
|
||||
else
|
||||
return valid(option)
|
||||
end
|
||||
end)
|
||||
|
||||
--- A string which is the key of a table, takes one argument which is an map of string keys to values
|
||||
add("string-key", function(input, _, map)
|
||||
local option = ExpUtil.auto_complete(map, input, true)
|
||||
if option == nil then
|
||||
return invalid{"exp-commands-parse.string-options", table.concat(table.get_keys(map), ", ")}
|
||||
else
|
||||
return valid(option)
|
||||
end
|
||||
end)
|
||||
|
||||
--- A string with a maximum length, takes one argument which is the maximum length of a string
|
||||
add("string-max-length", function(input, _, maximum)
|
||||
if input:len() > maximum then
|
||||
return invalid{"exp-commands-parse.string-max-length", maximum}
|
||||
else
|
||||
return valid(input)
|
||||
end
|
||||
end)
|
||||
|
||||
--- A number
|
||||
add("number", function(input)
|
||||
local number = tonumber(input)
|
||||
if number == nil then
|
||||
return invalid{"exp-commands-parse.number"}
|
||||
else
|
||||
return valid(number)
|
||||
end
|
||||
end)
|
||||
|
||||
--- An integer, number which has been floored
|
||||
add("integer", function(input)
|
||||
local number = tonumber(input)
|
||||
if number == nil then
|
||||
return invalid{"exp-commands-parse.number"}
|
||||
else
|
||||
return valid(math.floor(number))
|
||||
end
|
||||
end)
|
||||
|
||||
--- A number in a given inclusive range
|
||||
add("number-range", function(input, _, minimum, maximum)
|
||||
local success, status, number = parse("number", input)
|
||||
if not success then
|
||||
return status, number
|
||||
elseif number < minimum or number > maximum then
|
||||
return invalid{"exp-commands-parse.number-range", minimum, maximum}
|
||||
else
|
||||
return valid(number)
|
||||
end
|
||||
end)
|
||||
|
||||
--- An integer in a given inclusive range
|
||||
add("integer-range", function(input, _, minimum, maximum)
|
||||
local success, status, number = parse("integer", input)
|
||||
if not success then
|
||||
return status, number
|
||||
elseif number < minimum or number > maximum then
|
||||
return invalid{"exp-commands-parse.number-range", minimum, maximum}
|
||||
else
|
||||
return valid(number)
|
||||
end
|
||||
end)
|
||||
|
||||
--- A player who has joined the game at least once
|
||||
add("player", function(input)
|
||||
local player = game.get_player(input)
|
||||
if player == nil then
|
||||
return invalid{"exp-commands-parse.player", input}
|
||||
else
|
||||
return valid(player)
|
||||
end
|
||||
end)
|
||||
|
||||
--- A player who is online
|
||||
add("player-online", function(input)
|
||||
local success, status, player = parse("player", input)
|
||||
if not success then
|
||||
return status, player
|
||||
elseif player.connected == false then
|
||||
return invalid{"exp-commands-parse.player-online"}
|
||||
else
|
||||
return valid(player)
|
||||
end
|
||||
end)
|
||||
|
||||
--- A player who is online and alive
|
||||
add("player-alive", function(input)
|
||||
local success, status, player = parse("player-online", input)
|
||||
if not success then
|
||||
return status, player
|
||||
elseif player.character == nil or player.character.health <= 0 then
|
||||
return invalid{"exp-commands-parse.player-alive"}
|
||||
else
|
||||
return valid(player)
|
||||
end
|
||||
end)
|
||||
|
||||
--- A force within the game
|
||||
add("force", function(input)
|
||||
local force = game.forces[input]
|
||||
if force == nil then
|
||||
return invalid{"exp-commands-parse.force"}
|
||||
else
|
||||
return valid(force)
|
||||
end
|
||||
end)
|
||||
|
||||
--- A surface within the game
|
||||
add("surface", function(input)
|
||||
local surface = game.surfaces[input]
|
||||
if surface == nil then
|
||||
return invalid{"exp-commands-parse.surface"}
|
||||
else
|
||||
return valid(surface)
|
||||
end
|
||||
end)
|
||||
|
||||
--- A name of a color from the predefined list, too many colours to use string-key
|
||||
add("color", function(input)
|
||||
local color = ExpUtil.auto_complete(Commands.color, input, true)
|
||||
if color == nil then
|
||||
return invalid{"exp-commands-parse.color"}
|
||||
else
|
||||
return valid(color)
|
||||
end
|
||||
end)
|
||||
81
exp_commands/module/commands/help.lua
Normal file
81
exp_commands/module/commands/help.lua
Normal file
@@ -0,0 +1,81 @@
|
||||
--[[-- Command Module - Help
|
||||
Game command to list and search all registered commands in a nice format
|
||||
@commands _system-ipc
|
||||
|
||||
@usage-- Get all messages related to banning a player
|
||||
/commands ban
|
||||
-- Get the second page of results
|
||||
/commands ban 2
|
||||
]]
|
||||
|
||||
local Global = require("modules/exp_util/global")
|
||||
local Commands = require("modules/exp_commands")
|
||||
|
||||
local PAGE_SIZE = 5
|
||||
|
||||
local search_cache = {}
|
||||
Global.register(search_cache, function(tbl)
|
||||
search_cache = tbl
|
||||
end)
|
||||
|
||||
--- Format commands into a strings across multiple pages
|
||||
local function format_as_pages(commands, page_size)
|
||||
local pages = { {} }
|
||||
local page_length = 0
|
||||
local current_page = 1
|
||||
local total = 0
|
||||
|
||||
for _, command in pairs(commands) do
|
||||
total = total + 1
|
||||
page_length = page_length + 1
|
||||
if page_length > page_size then
|
||||
current_page = current_page + 1
|
||||
pages[current_page] = {}
|
||||
page_length = 1
|
||||
end
|
||||
|
||||
local aliases = #command.aliases > 0 and {"exp-commands-help.aliases", table.concat(command.aliases, ", ")} or ""
|
||||
pages[current_page][page_length] = { "exp-commands-help.format", command.name, command.description, command.help, aliases }
|
||||
end
|
||||
|
||||
return pages, total
|
||||
end
|
||||
|
||||
Commands.new("commands", "List and search all commands for a keyword")
|
||||
:add_aliases{ "chelp", "helpp" }
|
||||
:argument("keyword", "string")
|
||||
:optional("page", "integer")
|
||||
:defaults{ page = 1 }
|
||||
:register(function(player, keyword, page)
|
||||
keyword = keyword:lower()
|
||||
local pages, found
|
||||
local cache = search_cache[player.index]
|
||||
if cache and cache.keyword == keyword then
|
||||
-- Cached value found, no search is needed
|
||||
pages = cache.pages
|
||||
found = cache.found
|
||||
else
|
||||
-- No cached value, so a search needs to be done
|
||||
local commands = Commands.search_for_player(keyword, player)
|
||||
pages, found = format_as_pages(commands, PAGE_SIZE)
|
||||
search_cache[player.index] = { keyword = keyword, pages = pages, found = found }
|
||||
end
|
||||
|
||||
-- Error if no pages found
|
||||
if found == 0 then
|
||||
return Commands.status.success{ "exp-commands-help.no-results" }
|
||||
end
|
||||
|
||||
local page_data = pages[page]
|
||||
if page_data == nil then
|
||||
-- Page number was out of range for this search
|
||||
return Commands.status.invalid_input{"exp-commands-help.out-of-range", page, #pages }
|
||||
end
|
||||
|
||||
-- Print selected page to the player
|
||||
Commands.print{ "exp-commands-help.header", keyword == '' and '<all>' or keyword }
|
||||
for _, command in pairs(page_data) do
|
||||
Commands.print(command)
|
||||
end
|
||||
return Commands.status.success{ "exp-commands-help.footer", found, page, #pages }
|
||||
end)
|
||||
25
exp_commands/module/commands/ipc.lua
Normal file
25
exp_commands/module/commands/ipc.lua
Normal file
@@ -0,0 +1,25 @@
|
||||
--[[-- Command Module - IPC
|
||||
System command which sends an object to the clustorio api, should be used for debugging / echo commands
|
||||
@commands _system-ipc
|
||||
|
||||
@usage-- Send a message on your custom channel, message is a json string
|
||||
/_ipc myChannel { "myProperty": "foo", "playerName": "Cooldude2606" }
|
||||
]]
|
||||
|
||||
local Commands = require("modules/exp_commands")
|
||||
local Clustorio = require("modules/clusterio/api")
|
||||
|
||||
Commands.new("_ipc", "Send an IPC message on the selected channel")
|
||||
:add_flags{ "system_only" }
|
||||
:enable_auto_concatenation()
|
||||
:argument("channel", "string")
|
||||
:argument("message", "string")
|
||||
:register(function(_, channel, message)
|
||||
local tbl = game.json_to_table(message)
|
||||
if tbl == nil then
|
||||
return Commands.status.invalid_input("Invalid json string")
|
||||
else
|
||||
Clustorio.send_json(channel, tbl)
|
||||
return Commands.status.success()
|
||||
end
|
||||
end)
|
||||
91
exp_commands/module/commands/permission_authorities.lua
Normal file
91
exp_commands/module/commands/permission_authorities.lua
Normal file
@@ -0,0 +1,91 @@
|
||||
--[[-- Command Module - Default permission authorities
|
||||
The default permission authorities controlled by the flags: admin_only, system_only, no_rcon, disabled
|
||||
|
||||
@usage-- Unlock system commands for debugging purposes
|
||||
/c require("modules/exp-commands").unlock_system_commands(game.player)
|
||||
|
||||
@usage-- Disable a command for all players because it is not functioning correctly
|
||||
/c require("modules/exp-commands").disable("my-command")
|
||||
]]
|
||||
|
||||
local Global = require("modules/exp_util/global")
|
||||
local Commands = require("modules/exp_commands")
|
||||
local add, allow, deny = Commands.add_permission_authority, Commands.status.success, Commands.status.unauthorised
|
||||
|
||||
local permission_authorities = {}
|
||||
|
||||
local system_players = {}
|
||||
local disabled_commands = {}
|
||||
Global.register({
|
||||
system_players,
|
||||
disabled_commands,
|
||||
}, function(tbl)
|
||||
system_players = tbl[1]
|
||||
disabled_commands = tbl[2]
|
||||
end)
|
||||
|
||||
--- Allow a player access to system commands, use for debug purposes only
|
||||
-- @tparam[opt] string player_name The name of the player to give access to, default is the current player
|
||||
function Commands.unlock_system_commands(player_name)
|
||||
system_players[player_name or game.player.name] = true
|
||||
end
|
||||
|
||||
--- Remove access from system commands for a player, use for debug purposes only
|
||||
-- @tparam[opt] string player_name The name of the player to give access to, default is the current player
|
||||
function Commands.lock_system_commands(player_name)
|
||||
system_players[player_name or game.player.name] = nil
|
||||
end
|
||||
|
||||
--- Get a list of all players who have system commands unlocked
|
||||
function Commands.get_system_command_players()
|
||||
return table.get_keys(system_players)
|
||||
end
|
||||
|
||||
--- Stops a command from be used by any one
|
||||
-- @tparam string command_name The name of the command to disable
|
||||
function Commands.disable(command_name)
|
||||
disabled_commands[command_name] = true
|
||||
end
|
||||
|
||||
--- Allows a command to be used again after disable was used
|
||||
-- @tparam string command_name The name of the command to enable
|
||||
function Commands.enable(command_name)
|
||||
disabled_commands[command_name] = nil
|
||||
end
|
||||
|
||||
--- Get a list of all players who have system commands unlocked
|
||||
function Commands.get_disabled_commands()
|
||||
return table.get_keys(disabled_commands)
|
||||
end
|
||||
|
||||
--- If a command has the flag "admin_only" then only admins can use the command#
|
||||
permission_authorities.admin_only =
|
||||
add(function(player, command)
|
||||
if command.flags.admin_only and not player.admin then
|
||||
return deny{"exp-commands-permissions.admin-only"}
|
||||
else
|
||||
return allow()
|
||||
end
|
||||
end)
|
||||
|
||||
--- If a command has the flag "system_only" then only rcon connections can use the command
|
||||
permission_authorities.system_only =
|
||||
add(function(player, command)
|
||||
if command.flags.system_only and not system_players[player.name] then
|
||||
return deny{"exp-commands-permissions.system-only"}
|
||||
else
|
||||
return allow()
|
||||
end
|
||||
end)
|
||||
|
||||
--- If Commands.disable was called then no one can use the command
|
||||
permission_authorities.disabled =
|
||||
add(function(_, command)
|
||||
if disabled_commands[command.name] then
|
||||
return deny{"exp-commands-permissions.disabled"}
|
||||
else
|
||||
return allow()
|
||||
end
|
||||
end)
|
||||
|
||||
return permission_authorities
|
||||
82
exp_commands/module/commands/rcon.lua
Normal file
82
exp_commands/module/commands/rcon.lua
Normal file
@@ -0,0 +1,82 @@
|
||||
--[[-- Command Module - Rcon
|
||||
System command which runs arbitrary code within a custom (not sandboxed) environment
|
||||
@commands _system-rcon
|
||||
|
||||
@usage-- 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;
|
||||
|
||||
@usage-- Get the names of all online players, using clustorio ipcs
|
||||
/_system-rcon local names = {}; for index, player in pairs(game.connected_player) do names[index] = player.name end; ipc("online-players", names);
|
||||
]]
|
||||
|
||||
local ExpUtil = require("modules/exp_util")
|
||||
local Async = require("modules/exp_util/async")
|
||||
local Global = require("modules/exp_util/global")
|
||||
local Commands = require("modules/exp_commands")
|
||||
local Clustorio = require("modules/clusterio/api")
|
||||
|
||||
local rcon_env = {}
|
||||
local rcon_statics = {}
|
||||
local rcon_callbacks = {}
|
||||
setmetatable(rcon_statics, { __index = _G })
|
||||
setmetatable(rcon_env, { __index = rcon_statics })
|
||||
|
||||
--- Some common static values which can be added now
|
||||
rcon_statics.Async = Async
|
||||
rcon_statics.ExpUtil = ExpUtil
|
||||
rcon_statics.Commands = Commands
|
||||
rcon_statics.Clustorio = Clustorio
|
||||
rcon_statics.output = Commands.print
|
||||
rcon_statics.ipc = Clustorio.send_json
|
||||
|
||||
--- Some common callback values which are useful when a player uses the command
|
||||
function rcon_callbacks.player(player) return player end
|
||||
function rcon_callbacks.surface(player) return player and player.surface end
|
||||
function rcon_callbacks.force(player) return player and player.force end
|
||||
function rcon_callbacks.position(player) return player and player.position end
|
||||
function rcon_callbacks.entity(player) return player and player.selected end
|
||||
function rcon_callbacks.tile(player) return player and player.surface.get_tile(player.position) end
|
||||
|
||||
--- The rcon env is saved between command runs to prevent desyncs
|
||||
Global.register(rcon_env, function(tbl)
|
||||
rcon_env = setmetatable(tbl, { __index = rcon_statics })
|
||||
end)
|
||||
|
||||
--- Static values can be added to the rcon env which are not stored in global such as modules
|
||||
function Commands.add_rcon_static(name, value)
|
||||
ExpUtil.assert_not_runtime()
|
||||
rcon_statics[name] = value
|
||||
end
|
||||
|
||||
--- 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)
|
||||
ExpUtil.assert_not_runtime()
|
||||
rcon_callbacks[name] = callback
|
||||
end
|
||||
|
||||
Commands.new("_rcon", "Execute arbitrary code within a custom environment")
|
||||
:add_flags{ "system_only" }
|
||||
:enable_auto_concatenation()
|
||||
:argument("invocation", "string")
|
||||
:register(function(player, invocation_string)
|
||||
-- Construct the environment the command will run within
|
||||
local env = setmetatable({}, { __index = rcon_env, __newindex = rcon_env })
|
||||
for name, callback in pairs(rcon_callbacks) do
|
||||
local _, rtn = pcall(callback, player.index > 0 and player or nil)
|
||||
rawset(env, name, rtn)
|
||||
end
|
||||
|
||||
-- Compile and run the invocation string
|
||||
local invocation, compile_error = load(invocation_string, "rcon-invocation", "t", env)
|
||||
if compile_error then
|
||||
return Commands.status.invalid_input(compile_error)
|
||||
else
|
||||
local success, rtn = xpcall(invocation, debug.traceback)
|
||||
if success == false then
|
||||
local err = rtn:gsub('%.%.%..-/temp/currently%-playing/', '')
|
||||
return Commands.status.error(err)
|
||||
else
|
||||
return Commands.status.success(rtn)
|
||||
end
|
||||
end
|
||||
end)
|
||||
25
exp_commands/module/commands/sudo.lua
Normal file
25
exp_commands/module/commands/sudo.lua
Normal file
@@ -0,0 +1,25 @@
|
||||
--[[-- Command Module - Sudo
|
||||
System command to execute a command as another player using their permissions (except for permissions group actions)
|
||||
@commands _system-sudo
|
||||
|
||||
@usage-- Run the example command as another player
|
||||
-- As Cooldude2606: /repeat 5
|
||||
/_system-sudo Cooldude2606 repeat 5
|
||||
]]
|
||||
|
||||
local Commands = require("modules/exp_commands")
|
||||
|
||||
Commands.new("_sudo", "Run a command as another player")
|
||||
:add_flags{ "system_only" }
|
||||
:enable_auto_concatenation()
|
||||
:argument("player", "player")
|
||||
:argument("command", "string-key", Commands.registered_commands)
|
||||
:argument("arguments", "string")
|
||||
:register(function(_, player, command, parameter)
|
||||
return Commands._event_handler{
|
||||
name = command.name,
|
||||
tick = game.tick,
|
||||
player_index = player.index,
|
||||
parameter = parameter
|
||||
}
|
||||
end)
|
||||
38
exp_commands/module/locale/en.cfg
Normal file
38
exp_commands/module/locale/en.cfg
Normal file
@@ -0,0 +1,38 @@
|
||||
color-tag=[color=__1__]__2__[/color]
|
||||
|
||||
[exp-commands]
|
||||
command-help=__1__ - __2__
|
||||
success=Command Complete.
|
||||
error=Command failed to run: __1__
|
||||
error-default=Please check you gave the correct arguments.
|
||||
unauthorized=Unauthorized: __1__
|
||||
unauthorized-default=Access is denied due to insufficient permissions.
|
||||
invalid-usage=Wrong number of arguments, usage: /__1__ __2__
|
||||
invalid-input=Invalid input: Please ensure everything is formated correctly.
|
||||
invalid-argument=Invalid argument "__1__"; __2__
|
||||
internal-error=Internal Error, Please contact an admin: __1__
|
||||
|
||||
[exp-commands-parse]
|
||||
string-options=Invalid Option, Must be one of: __1__
|
||||
string-max-length=Invalid Length, Max: __1__
|
||||
number=Invalid Number.
|
||||
number-range=Invalid Range, Min (inclusive): __1__, Max (inclusive): __2__
|
||||
player=Invalid Player Name, __1__ ,try using tab key to auto-complete the name
|
||||
player-online=Player is offline.
|
||||
player-alive=Player is dead.
|
||||
force=Invalid Force Name.
|
||||
surface=Invalid Surface Name.
|
||||
color=Invalid Color Name.
|
||||
|
||||
[exp-commands-permissions]
|
||||
admin-only=This command is for game admins only.
|
||||
system-only=This command can not be ran by players.
|
||||
disabled=This command is currently disabled.
|
||||
|
||||
[exp-commands-help]
|
||||
header=Help results for "__1__":
|
||||
footer=[__1__ results found: page __2__ of __3__]
|
||||
format=/__1__ __2__ - __3__ __4__
|
||||
aliases=Aliaies: __1__
|
||||
out-of-range=__1__ is an invalid page number. Last page: __2__
|
||||
no-results=No commands were found
|
||||
15
exp_commands/module/module.json
Normal file
15
exp_commands/module/module.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "exp_commands",
|
||||
"require": [
|
||||
"commands/data_types.lua",
|
||||
"commands/permission_authorities.lua",
|
||||
"commands/help.lua",
|
||||
"commands/rcon.lua",
|
||||
"commands/sudo.lua",
|
||||
"commands/ipc.lua"
|
||||
],
|
||||
"dependencies": {
|
||||
"clusterio": "*",
|
||||
"exp_util": "*"
|
||||
}
|
||||
}
|
||||
686
exp_commands/module/module_exports.lua
Normal file
686
exp_commands/module/module_exports.lua
Normal file
@@ -0,0 +1,686 @@
|
||||
--[[-- Core Module - Commands
|
||||
- Factorio command making module that makes commands with better parse and more modularity
|
||||
@core Commands
|
||||
@alias Commands
|
||||
|
||||
@usage-- Adding a permission authority
|
||||
-- You are only required to return a boolean, but by using the unauthorised status you can provide better feedback to the user
|
||||
Commands.add_permission_authority(function(player, command)
|
||||
if command.flags.admin_only and not player.admin then
|
||||
return Commands.status.unauthorised("This command requires in-game admin")
|
||||
end
|
||||
return Commands.status.success()
|
||||
end)
|
||||
|
||||
@usage-- Adding a data type
|
||||
-- You can not return nil from this function, doing so will raise an error, you must return a status
|
||||
Commands.add_data_type("integer", function(input, player)
|
||||
local number = tonumber(input)
|
||||
if number == nil then
|
||||
return Commands.status.invalid_input("Value must be a valid number")
|
||||
else
|
||||
return Commands.status.success(number)
|
||||
end
|
||||
end)
|
||||
|
||||
-- It is recommend to use exiting parsers within your own to simplify checks, but make sure to propagate failures
|
||||
Commands.add_data_type("integer-range", function(input, player, minimum, maximum)
|
||||
local success, status, integer = Commands.parse_data_type("integer", input, player)
|
||||
if not success then return status, number end
|
||||
|
||||
if integer < minimum or integer > maximum then
|
||||
return Commands.status.invalid_input(string.format("Integer must be in range: %d to %d", minimum, maximum))
|
||||
else
|
||||
return Commands.status.success(integer)
|
||||
end
|
||||
end)
|
||||
|
||||
@usage-- Adding a command
|
||||
Commands.new("repeat", "This is my new command, it will repeat a message a number of times")
|
||||
:add_flags{ "admin_only" } -- Using the permission authority above, this makes the command admin only
|
||||
:add_aliases{ "repeat-message" } -- You can add as many aliases as you want
|
||||
:enable_auto_concatenation() -- This allows the final argument to be any length
|
||||
:argument("count", "integer-range", 1, 10) -- Allow any value between 1 and 10
|
||||
:optional("message", "string") -- This is an optional argument
|
||||
:defaults{
|
||||
-- Defaults don't need to be functions, one is used here to demonstrate their use, remember player can be nil for the server
|
||||
message = function(player)
|
||||
return player and "Hello, "..player.name or "Hello, World!"
|
||||
end
|
||||
}
|
||||
:register(function(player, count, message)
|
||||
for i = 1, count do
|
||||
Commands.print("#"..i.." "..message)
|
||||
end
|
||||
end)
|
||||
|
||||
]]
|
||||
|
||||
local ExpUtil = require("modules/exp_util")
|
||||
local Color = require("modules/exp_util/include/color")
|
||||
|
||||
local Commands = {
|
||||
color = Color, -- A useful reference to the color utils to be used with command outputs
|
||||
_prototype = {}, -- Contains the methods for the command object
|
||||
registered_commands = {}, -- Stores a reference to all registered commands
|
||||
permission_authorities = {}, -- Stores a reference to all active permission authorities
|
||||
data_types = {}, -- Stores all input parsers and validators for different data types
|
||||
status = {}, -- Contains the different status values a command can return
|
||||
}
|
||||
|
||||
Commands._metatable = {
|
||||
__index = Commands._prototype,
|
||||
__class = "ExpCommand"
|
||||
}
|
||||
|
||||
Commands.player_server = setmetatable({
|
||||
index = 0,
|
||||
color = Color.white,
|
||||
chat_color = Color.white,
|
||||
name = "<server>",
|
||||
tag = "",
|
||||
connected = true,
|
||||
admin = true,
|
||||
afk_time = 0,
|
||||
online_time = 0,
|
||||
last_online = 0,
|
||||
spectator = true,
|
||||
show_on_map = false,
|
||||
valid = true,
|
||||
object_name = "LuaPlayer"
|
||||
}, {
|
||||
__index = function(_, key)
|
||||
if key == "__self" or type(key) == "number" then return nil end
|
||||
Commands.error("Command does not support rcon usage, requires reading player." .. key)
|
||||
error("Command does not support rcon usage, requires reading player." .. key)
|
||||
end,
|
||||
__newindex = function(_, key)
|
||||
Commands.error("Command does not support rcon usage, requires reading player." .. key)
|
||||
error("Command does not support rcon usage, requires setting player." .. key)
|
||||
end
|
||||
})
|
||||
|
||||
--- Status Returns.
|
||||
-- Return values used by command callbacks
|
||||
-- @section command-status
|
||||
|
||||
--- Used to signal success from a command, data type parser, or permission authority
|
||||
-- @tparam[opt] LocaleString|string msg An optional message to be included when a command completes (only has an effect in command callbacks)
|
||||
function Commands.status.success(msg)
|
||||
return Commands.status.success, msg or {'exp-commands.success'}
|
||||
end
|
||||
|
||||
--- Used to signal an error has occurred in a command, data type parser, or permission authority
|
||||
-- For data type parsers and permission authority, an error return will prevent the command from being executed
|
||||
-- @tparam[opt] LocaleString|string msg An optional error message to be included in the output, a generic message is used if not provided
|
||||
function Commands.status.error(msg)
|
||||
return Commands.status.error, {'exp-commands.error', msg or {'exp-commands.error-default'}}
|
||||
end
|
||||
|
||||
--- Used to signal the player is unauthorised to use a command, primarily used by permission authorities but can be used in a command callback
|
||||
-- For permission authorities, an error return will prevent the command from being executed
|
||||
-- @tparam[opt] LocaleString|string msg An optional error message to be included in the output, a generic message is used if not provided
|
||||
function Commands.status.unauthorised(msg)
|
||||
return Commands.status.unauthorised, msg or {'exp-commands.unauthorized', msg or {'exp-commands.unauthorized-default'}}
|
||||
end
|
||||
|
||||
--- Used to signal the player provided invalid input to an command, primarily used by data type parsers but can be used in a command callback
|
||||
-- For data type parsers, an error return will prevent the command from being executed
|
||||
-- @tparam[opt] LocaleString|string msg An optional error message to be included in the output, a generic message is used if not provided
|
||||
function Commands.status.invalid_input(msg)
|
||||
return Commands.status.invalid_input, msg or {'exp-commands.invalid-input'}
|
||||
end
|
||||
|
||||
--- Used to signal an internal error has occurred, this is reserved for internal use
|
||||
-- @tparam LocaleString|string msg A message detailing the error which has occurred, will be logged and outputted
|
||||
function Commands.status.internal_error(msg)
|
||||
return Commands.status.internal_error, {'exp-commands.internal-error', msg}
|
||||
end
|
||||
|
||||
local valid_command_status = {} -- Hashmap lookup for testing if a status is valid
|
||||
for name, status in pairs(Commands.status) do
|
||||
valid_command_status[status] = name
|
||||
end
|
||||
|
||||
--- Permission Authority.
|
||||
-- Functions that control who can use commands
|
||||
-- @section permission-authority
|
||||
|
||||
--- 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
|
||||
-- @tparam function permission_authority The function to provide access control to commands, see module usage.
|
||||
-- @treturn function The function which was provided as the first argument
|
||||
function Commands.add_permission_authority(permission_authority)
|
||||
local next_index = #Commands.permission_authorities + 1
|
||||
Commands.permission_authorities[next_index] = permission_authority
|
||||
return permission_authority
|
||||
end
|
||||
|
||||
--- Remove a permission authority, must be the same function reference which was passed to add_permission_authority
|
||||
-- @tparam function permission_authority The access control function to remove as a permission authority
|
||||
function Commands.remove_permission_authority(permission_authority)
|
||||
local pms = Commands.permission_authorities
|
||||
for index, value in pairs(pms) do
|
||||
if value == permission_authority then
|
||||
local last = #pms
|
||||
pms[index] = pms[last]
|
||||
pms[last] = nil
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Check if a player has permission to use a command, calling all permission authorities
|
||||
-- @tparam LuaPlayer player The player to test the permission of, nil represents the server and always returns true
|
||||
-- @tparam Command command The command the player is attempting to use
|
||||
-- @treturn boolean true if the player has permission to use the command
|
||||
-- @treturn LocaleString|string when permission is denied, this is the reason permission was denied
|
||||
function Commands.player_has_permission(player, command)
|
||||
if player == nil or player == Commands.player_server then return true end
|
||||
|
||||
for _, permission_authority in ipairs(Commands.permission_authorities) do
|
||||
local status, msg = permission_authority(player, command)
|
||||
if type(status) == "boolean" then
|
||||
if status == false then
|
||||
local _, rtn_msg = Commands.status.unauthorised(msg)
|
||||
return false, rtn_msg
|
||||
end
|
||||
elseif valid_command_status[status] then
|
||||
if status ~= Commands.status.success then
|
||||
return false, msg
|
||||
end
|
||||
else
|
||||
return false, "Permission authority returned unexpected value"
|
||||
end
|
||||
end
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
--- Data Type Parsing.
|
||||
-- Functions that parse and validate player input
|
||||
-- @section input-parse-and-validation
|
||||
|
||||
--- Add a new input parser to the command library, this allows use of a data type without needing to pass the function directly
|
||||
-- @tparam string data_type The name of the data type the input parser reads in and validates
|
||||
-- @tparam function parser The function used to parse and validate the data type
|
||||
-- @treturn string The data type passed as the first argument
|
||||
function Commands.add_data_type(data_type, parser)
|
||||
if Commands.data_types[data_type] then
|
||||
error("Data type \""..tostring(data_type).."\" already has a parser registered", 2)
|
||||
end
|
||||
Commands.data_types[data_type] = parser
|
||||
return data_type
|
||||
end
|
||||
|
||||
--- Remove an input parser for a data type, must be the same string that was passed to add_input_parser
|
||||
-- @tparam string data_type The data type for which you want to remove the input parser of
|
||||
function Commands.remove_data_type(data_type)
|
||||
Commands.data_types[data_type] = nil
|
||||
end
|
||||
|
||||
--- Parse and validate an input string as a given data type
|
||||
-- @tparam string|function data_type The name of the data type parser to use to read and validate the input text
|
||||
-- @tparam string input The input string that will be read by the parser
|
||||
-- @param ... Any other arguments that the parser is expecting
|
||||
-- @treturn boolean true when the input was successfully parsed and validated to be the correct type
|
||||
-- @return When The error status for why parsing failed, otherwise it is the parsed value
|
||||
-- @return When first is false, this is the error message, otherwise this is the parsed value
|
||||
function Commands.parse_data_type(data_type, input, ...)
|
||||
local parser = Commands.data_types[data_type]
|
||||
if type(data_type) == "function" then
|
||||
parser = data_type
|
||||
elseif parser == nil then
|
||||
return false, Commands.status.internal_error, {"exp-commands.internal-error" , "Data type \""..tostring(data_type).."\" does not have a registered parser"}
|
||||
end
|
||||
|
||||
local status, parsed = parser(input, ...)
|
||||
if status == nil then
|
||||
return Commands.status.internal_error, {"exp-commands.internal-error" , "Parser for data type \""..tostring(data_type).."\" returned a nil value"}
|
||||
elseif valid_command_status[status] then
|
||||
if status ~= Commands.status.success then
|
||||
return false, status, parsed -- error_type, error_msg
|
||||
else
|
||||
return true, status, parsed -- success, parsed_data
|
||||
end
|
||||
else
|
||||
return true, Commands.status.success, status -- success, parsed_data
|
||||
end
|
||||
end
|
||||
|
||||
--- List and Search
|
||||
-- Functions used to list and search for commands
|
||||
-- @section list-and-search
|
||||
|
||||
--- Returns a list of all registered custom commands
|
||||
-- @treturn table An array of registered commands
|
||||
function Commands.list_all()
|
||||
return Commands.registered_commands
|
||||
end
|
||||
|
||||
--- Returns a list of all registered custom commands which the given player has permission to use
|
||||
-- @treturn table An array of registered commands
|
||||
function Commands.list_for_player(player)
|
||||
local rtn = {}
|
||||
|
||||
for name, command in pairs(Commands.registered_commands) do
|
||||
if Commands.player_has_permission(player, command) then
|
||||
rtn[name] = command
|
||||
end
|
||||
end
|
||||
|
||||
return rtn
|
||||
end
|
||||
|
||||
--- Searches all game commands and the provided custom commands for the given keyword
|
||||
local function search_commands(keyword, custom_commands)
|
||||
keyword = keyword:lower()
|
||||
local rtn = {}
|
||||
|
||||
-- Search all custom commands
|
||||
for name, command in pairs(custom_commands) do
|
||||
local search = string.format('%s %s %s', name, command.help, table.concat(command.aliases, ' '))
|
||||
if search:lower():match(keyword) then
|
||||
rtn[name] = command
|
||||
end
|
||||
end
|
||||
|
||||
-- Search all game commands
|
||||
for name, description in pairs(commands.game_commands) do
|
||||
local search = string.format('%s %s', name, description)
|
||||
if search:lower():match(keyword) then
|
||||
rtn[name] = {
|
||||
name = name,
|
||||
help = description,
|
||||
description = "",
|
||||
aliases = {}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return rtn
|
||||
end
|
||||
|
||||
--- Searches all custom commands and game commands for the given keyword
|
||||
-- @treturn table An array of registered commands
|
||||
function Commands.search_all(keyword)
|
||||
return search_commands(keyword, Commands.list_all())
|
||||
end
|
||||
|
||||
--- Searches custom commands allowed for this player and all game commands for the given keyword
|
||||
-- @treturn table An array of registered commands
|
||||
function Commands.search_for_player(keyword, player)
|
||||
return search_commands(keyword, Commands.list_for_player(player))
|
||||
end
|
||||
|
||||
--- Command Output
|
||||
-- Prints output to the player or rcon connection
|
||||
-- @section player-print
|
||||
|
||||
--- Set the color of a message using rich text chat
|
||||
-- @tparam string message The message to set the color of
|
||||
-- @tparam Color color The color that the message should be
|
||||
-- @treturn string The string which can be printed to game chat
|
||||
function Commands.set_chat_message_color(message, color)
|
||||
local color_tag = math.round(color.r, 3)..', '..math.round(color.g, 3)..', '..math.round(color.b, 3)
|
||||
return string.format('[color=%s]%s[/color]', color_tag, message)
|
||||
end
|
||||
|
||||
--- Set the color of a locale message using rich text chat
|
||||
-- @tparam LocaleString message The message to set the color of
|
||||
-- @tparam Color color The color that the message should be
|
||||
-- @treturn LocaleString The locale string which can be printed to game chat
|
||||
function Commands.set_locale_chat_message_color(message, color)
|
||||
local color_tag = math.round(color.r, 3)..', '..math.round(color.g, 3)..', '..math.round(color.b, 3)
|
||||
return {'color-tag', color_tag, message}
|
||||
end
|
||||
|
||||
--- Get a string representing the name of the given player in their chat colour
|
||||
-- @tparam LuaPlayer player The player to use the name and color of, nil represents the server
|
||||
-- @treturn string The players name formatted as a string in their chat color
|
||||
function Commands.format_player_name(player)
|
||||
local player_name = player and player.name or "<server>"
|
||||
local player_color = player and player.chat_color or Color.white
|
||||
local color_tag = math.round(player_color.r, 3)..', '..math.round(player_color.g, 3)..', '..math.round(player_color.b, 3)
|
||||
return string.format('[color=%s]%s[/color]', color_tag, player_name)
|
||||
end
|
||||
|
||||
--- Get a locale string representing the name of the given player in their chat colour
|
||||
-- @tparam LuaPlayer player The player to use the name and color of, nil represents the server
|
||||
-- @treturn LocaleString The players name formatted as a locale string in their chat color
|
||||
function Commands.format_locale_player_name(player)
|
||||
local player_name = player and player.name or "<server>"
|
||||
local player_color = player and player.chat_color or Color.white
|
||||
local color_tag = math.round(player_color.r, 3)..', '..math.round(player_color.g, 3)..', '..math.round(player_color.b, 3)
|
||||
return {'color-tag', color_tag, player_name}
|
||||
end
|
||||
|
||||
--- Print a message to the user of a command, accepts any value and will print in a readable and safe format
|
||||
-- @tparam any message The message / value to be printed
|
||||
-- @tparam[opt] Color color The color the message should be printed in
|
||||
-- @tparam[opt] string sound The sound path to be played when the message is printed
|
||||
function Commands.print(message, color, sound)
|
||||
local player = game.player
|
||||
if not player then
|
||||
rcon.print(ExpUtil.format_any(message))
|
||||
else
|
||||
local formatted = ExpUtil.format_any(message, nil, 20)
|
||||
player.print(formatted, color or Color.white)
|
||||
player.play_sound{ path = sound or 'utility/scenario_message' }
|
||||
end
|
||||
end
|
||||
|
||||
--- Print an error message to the user of a command, accepts any value and will print in a readable and safe format
|
||||
-- @tparam any message The message / value to be printed
|
||||
function Commands.error(message)
|
||||
return Commands.print(message, Color.orange_red, 'utility/wire_pickup')
|
||||
end
|
||||
|
||||
--- Command Prototype
|
||||
-- The prototype defination for command objects
|
||||
-- @section command-prototype
|
||||
|
||||
--- This is a default callback that should never be called
|
||||
local function default_command_callback()
|
||||
return Commands.status.internal_error('No callback registered')
|
||||
end
|
||||
|
||||
--- Returns a new command object, this will not register the command to the game
|
||||
-- @tparam string name The name of the command as it will be registered later
|
||||
-- @tparam string help The help message / description of the command
|
||||
-- @treturn Command A new command object which can be registered
|
||||
function Commands.new(name, help)
|
||||
ExpUtil.assert_argument_type(name, "string", 1, "name")
|
||||
ExpUtil.assert_argument_type(help, "string", 2, "help")
|
||||
if Commands.registered_commands[name] then
|
||||
error("Command is already defined at: "..Commands.registered_commands[name].defined_at, 2)
|
||||
end
|
||||
|
||||
return setmetatable({
|
||||
name = name,
|
||||
help = help,
|
||||
callback = default_command_callback,
|
||||
defined_at = ExpUtil.safe_file_path(2),
|
||||
auto_concat = false,
|
||||
min_arg_count = 0,
|
||||
max_arg_count = 0,
|
||||
flags = {}, -- stores flags that can be used by auth
|
||||
aliases = {}, -- stores aliases to this command
|
||||
arguments = {}, -- [{name: string, optional: boolean, default: any, data_type: function, parse_args: table}]
|
||||
}, Commands._metatable)
|
||||
end
|
||||
|
||||
--- Get the data type parser from a name, will raise an error if it doesnt exist
|
||||
local function get_parser(data_type)
|
||||
local rtn = Commands.data_types[data_type]
|
||||
if rtn == nil then
|
||||
error("Unknown data type: "..tostring(data_type), 3)
|
||||
end
|
||||
return data_type, rtn
|
||||
end
|
||||
|
||||
--- Add a new required argument to the command of the given data type
|
||||
-- @tparam string name The name of the argument being added
|
||||
-- @tparam string data_type The data type of this argument, must have previously been registered with add_data_type
|
||||
-- @treturn Command The command object to allow chaining method calls
|
||||
function Commands._prototype:argument(name, data_type, ...)
|
||||
if self.min_arg_count ~= self.max_arg_count then
|
||||
error("Can not have required arguments after optional arguments", 2)
|
||||
end
|
||||
self.min_arg_count = self.min_arg_count + 1
|
||||
self.max_arg_count = self.max_arg_count + 1
|
||||
self.arguments[#self.arguments + 1] = {
|
||||
name = name,
|
||||
optional = false,
|
||||
data_type = data_type,
|
||||
data_type_parser = get_parser(data_type),
|
||||
parse_args = {...}
|
||||
}
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a new optional argument to the command of the given data type
|
||||
-- @tparam string name The name of the argument being added
|
||||
-- @tparam string data_type The data type of this argument, must have previously been registered with add_data_type
|
||||
-- @treturn Command The command object to allow chaining method calls
|
||||
function Commands._prototype:optional(name, data_type, ...)
|
||||
self.max_arg_count = self.max_arg_count + 1
|
||||
self.arguments[#self.arguments + 1] = {
|
||||
name = name,
|
||||
optional = true,
|
||||
data_type = data_type,
|
||||
data_type_parser = get_parser(data_type),
|
||||
parse_args = {...}
|
||||
}
|
||||
return self
|
||||
end
|
||||
|
||||
--- Set the defaults for optional arguments, any not provided will have their value as nil
|
||||
-- @tparam table defaults A table who's keys are the argument names and values are the defaults or function which returns a default
|
||||
-- @treturn Command The command object to allow chaining method calls
|
||||
function Commands._prototype:defaults(defaults)
|
||||
local matched = {}
|
||||
for _, argument in ipairs(self.arguments) do
|
||||
if defaults[argument.name] then
|
||||
if not argument.optional then
|
||||
error("Attempting to set default value for required argument: " .. argument.name)
|
||||
end
|
||||
argument.default = defaults[argument.name]
|
||||
matched[argument.name] = true
|
||||
end
|
||||
end
|
||||
-- Check that there are no extra values in the table
|
||||
for name in pairs(defaults) do
|
||||
if not matched[name] then
|
||||
error("No argument with name: " .. name)
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Set the flags for the command, these can be accessed by permission authorities to check who should use a command
|
||||
-- @tparam table flags An array of string flags, or a table who's keys are the flag names and values are the flag values
|
||||
-- @treturn Command The command object to allow chaining method calls
|
||||
function Commands._prototype:add_flags(flags)
|
||||
for name, value in pairs(flags) do
|
||||
if type(name) == "number" then
|
||||
self.flags[value] = true
|
||||
else
|
||||
self.flags[name] = value
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Set the aliases for the command, these are alternative names that the command can be ran under
|
||||
-- @tparam table aliases An array of string names to use as aliases to this command
|
||||
-- @treturn Command The command object to allow chaining method calls
|
||||
function Commands._prototype:add_aliases(aliases)
|
||||
local start_index = #self.aliases
|
||||
for index, alias in ipairs(aliases) do
|
||||
self.aliases[start_index + index] = alias
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Enable concatenation of all arguments after the last, this should be used for user provided reason text
|
||||
-- @treturn Command The command object to allow chaining method calls
|
||||
function Commands._prototype:enable_auto_concatenation()
|
||||
self.auto_concat = true
|
||||
return self
|
||||
end
|
||||
|
||||
--- Register the command to the game with the given callback, this must be the final step as the object becomes immutable afterwards
|
||||
-- @tparam function callback The function which is called to perform the command action
|
||||
function Commands._prototype:register(callback)
|
||||
Commands.registered_commands[self.name] = self
|
||||
self.callback = callback
|
||||
|
||||
-- Generates a description to be used
|
||||
local description = {}
|
||||
for index, argument in pairs(self.arguments) do
|
||||
if argument.optional then
|
||||
description[index] = "["..argument.name.."]"
|
||||
else
|
||||
description[index] = "<"..argument.name..">"
|
||||
end
|
||||
end
|
||||
self.description = table.concat(description, " ")
|
||||
|
||||
-- Callback which is called by the game engine
|
||||
local function command_callback(event)
|
||||
event.name = self.name
|
||||
local success, traceback = xpcall(Commands._event_handler, debug.traceback, event)
|
||||
if not success and not traceback:find("Command does not support rcon usage") then
|
||||
local _, msg = Commands.status.internal_error(event.tick)
|
||||
Commands.error(msg)
|
||||
log("Internal Command Error " .. event.tick .. "\n" .. traceback)
|
||||
end
|
||||
end
|
||||
|
||||
-- Registers the command under its own name
|
||||
local help = {'exp-commands.command-help', self.description, self.help}
|
||||
commands.add_command(self.name, help, command_callback)
|
||||
|
||||
-- Registers the command under its aliases
|
||||
for _, alias in ipairs(self.aliases) do
|
||||
commands.add_command(alias, help, command_callback)
|
||||
end
|
||||
end
|
||||
|
||||
--- Command Runner
|
||||
-- Used internally to run commands
|
||||
-- @section command-runner
|
||||
|
||||
--- Log that a command was attempted and its outcome (error / success)
|
||||
local function log_command(comment, command, player, args, detail)
|
||||
local player_name = player and player.name or '<Server>'
|
||||
ExpUtil.write_json('log/commands.log', {
|
||||
comment = comment,
|
||||
detail = detail,
|
||||
player_name = player_name,
|
||||
command_name = command.name,
|
||||
args = args
|
||||
})
|
||||
end
|
||||
|
||||
--- Extract the arguments from a string input string
|
||||
local function extract_arguments(raw_input, max_args, auto_concat)
|
||||
-- nil check when no input given
|
||||
if raw_input == nil then return {} end
|
||||
|
||||
-- Extract quoted arguments
|
||||
local quoted_arguments = {}
|
||||
local input_string = raw_input:gsub('"[^"]-"', function(word)
|
||||
local no_spaces = word:gsub('%s', '%%s')
|
||||
quoted_arguments[no_spaces] = word:sub(2, -2)
|
||||
return ' '..no_spaces..' '
|
||||
end)
|
||||
|
||||
-- Extract all arguments
|
||||
local index = 0
|
||||
local arguments = {}
|
||||
for word in input_string:gmatch('%S+') do
|
||||
index = index + 1
|
||||
if index > max_args then
|
||||
-- concat the word onto the last argument
|
||||
if auto_concat == false then
|
||||
return nil -- too many args, exit early
|
||||
elseif quoted_arguments[word] then
|
||||
arguments[max_args] = arguments[max_args]..' "'..quoted_arguments[word]..'"'
|
||||
else
|
||||
arguments[max_args] = arguments[max_args]..' '..word
|
||||
end
|
||||
else
|
||||
-- new argument to be added
|
||||
if quoted_arguments[word] then
|
||||
arguments[index] = quoted_arguments[word]
|
||||
else
|
||||
arguments[index] = word
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return arguments
|
||||
end
|
||||
|
||||
--- Internal event handler for the command event
|
||||
function Commands._event_handler(event)
|
||||
local command = Commands.registered_commands[event.name]
|
||||
if command == nil then
|
||||
error("Command not recognised: "..event.name)
|
||||
end
|
||||
|
||||
local player = nil -- nil represents the server until the command is called
|
||||
if event.player_index then
|
||||
player = game.get_player(event.player_index)
|
||||
end
|
||||
|
||||
-- Check if the player is allowed to use the command
|
||||
local allowed, failure_msg = Commands.player_has_permission(player, command)
|
||||
if not allowed then
|
||||
log_command("Command not allowed", command, player, event.parameter)
|
||||
return Commands.error(failure_msg)
|
||||
end
|
||||
|
||||
-- Check the edge case of parameter being nil
|
||||
if command.min_arg_count > 0 and event.parameter == nil then
|
||||
log_command("Too few arguments", command, player, event.parameter, { minimum = command.min_arg_count, maximum = command.max_arg_count })
|
||||
return Commands.error{'exp-commands.invalid-usage', command.name, command.description}
|
||||
end
|
||||
|
||||
-- Get the arguments for the command, returns nil if there are too many arguments
|
||||
local raw_arguments = extract_arguments(event.parameter, command.max_arg_count, command.auto_concat)
|
||||
if raw_arguments == nil then
|
||||
log_command("Too many arguments", command, player, event.parameter, { minimum = command.min_arg_count, maximum = command.max_arg_count })
|
||||
return Commands.error{'exp-commands.invalid-usage', command.name, command.description}
|
||||
end
|
||||
|
||||
-- Check the minimum number of arguments is fullfiled
|
||||
if #raw_arguments < command.min_arg_count then
|
||||
log_command("Too few arguments", command, player, event.parameter, { minimum = command.min_arg_count, maximum = command.max_arg_count })
|
||||
return Commands.error{'exp-commands.invalid-usage', command.name, command.description}
|
||||
end
|
||||
|
||||
-- Parse the arguments, optional arguments will attempt to use a default if provided
|
||||
local arguments = {}
|
||||
for index, argument in ipairs(command.arguments) do
|
||||
local input = raw_arguments[index]
|
||||
if input == nil then
|
||||
-- We know this is an optional argument because the mimimum count is satisfied
|
||||
assert(argument.optional == true, "Argument was required")
|
||||
if type(argument.default) == "function" then
|
||||
arguments[index] = argument.default(player)
|
||||
else
|
||||
arguments[index] = argument.default
|
||||
end
|
||||
else
|
||||
-- Parse the raw argument to get the correct data type
|
||||
local success, status, parsed = Commands.parse_data_type(argument.data_type_parser, input, player, table.unpack(argument.parse_args))
|
||||
if success == false then
|
||||
log_command("Input parse failed", command, player, event.parameter, { status = valid_command_status[status], index = index, argument = argument, reason = parsed })
|
||||
return Commands.error{'exp-commands.invalid-argument', argument.name, parsed}
|
||||
else
|
||||
arguments[index] = parsed
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Run the command, dont need xpcall here because errors are caught in command_callback
|
||||
local status, status_msg = command.callback(player or Commands.player_server, table.unpack(arguments))
|
||||
if valid_command_status[status] 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)
|
||||
local _, msg = Commands.status.success()
|
||||
return Commands.print(msg)
|
||||
end
|
||||
end
|
||||
|
||||
return Commands
|
||||
33
exp_commands/package.json
Normal file
33
exp_commands/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@explosivegaming/commands",
|
||||
"version": "0.0.1",
|
||||
"description": "Clusterio module for command handling",
|
||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||
"license": "MIT",
|
||||
"repository": "explosivegaming/clustorio-exp-commands",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@clusterio/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/lib": "workspace:*",
|
||||
"@types/node": "^20.14.9",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "^0.30.4"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"clusterio",
|
||||
"factorio"
|
||||
]
|
||||
}
|
||||
6
exp_commands/tsconfig.json
Normal file
6
exp_commands/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
8
exp_commands/tsconfig.node.json
Normal file
8
exp_commands/tsconfig.node.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.node.json",
|
||||
"references": [
|
||||
{ "path": "../../../packages/lib/tsconfig.node.json" },
|
||||
],
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["test/*", "./dist/*"],
|
||||
}
|
||||
32
exp_commands/webpack.config.js
Normal file
32
exp_commands/webpack.config.js
Normal file
@@ -0,0 +1,32 @@
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const { merge } = require("webpack-merge");
|
||||
|
||||
const common = require("@clusterio/web_ui/webpack.common");
|
||||
|
||||
module.exports = (env = {}) => merge(common(env), {
|
||||
context: __dirname,
|
||||
entry: "./web/index.jsx",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist", "web"),
|
||||
},
|
||||
plugins: [
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
name: "exp_commands",
|
||||
library: { type: "var", name: "plugin_exp_commands" },
|
||||
exposes: {
|
||||
"./info": "./dist/plugin/info.js",
|
||||
"./package.json": "./package.json",
|
||||
"./web": "./web/index.jsx",
|
||||
},
|
||||
shared: {
|
||||
"@clusterio/lib": { import: false },
|
||||
"@clusterio/web_ui": { import: false },
|
||||
"antd": { import: false },
|
||||
"react": { import: false },
|
||||
"react-dom": { import: false },
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user