mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2025-12-27 03:25:23 +09:00
Move files to exp_legacy
This commit is contained in:
101
exp_legacy/module/expcore/async.lua
Normal file
101
exp_legacy/module/expcore/async.lua
Normal file
@@ -0,0 +1,101 @@
|
||||
--[[-- Core Module - Async
|
||||
- An extention of task and token to allow a single require to register and run async functions.
|
||||
@core Async
|
||||
@alias Async
|
||||
|
||||
@usage
|
||||
-- To use Async you must register the allowed functions when the file is loaded, often this will just be giving access to
|
||||
-- some functions within a module if you expect that at part may be blocked by in game permissions or a custom system you have made
|
||||
-- you may also want to register functions that you want to have a time delay, such as waiting 2 seconds before printing a message
|
||||
|
||||
-- When player.admin is called (either command or gui element event) by a player who isnt admin then it will error
|
||||
-- here we register the function to promote the player so that it will run async and outside the player scope
|
||||
local promote_player =
|
||||
Async.register(function(player)
|
||||
player.admin = true
|
||||
end)
|
||||
|
||||
-- This will allow us to bypass the error by running one tick later outside of any player scope
|
||||
Async(promote_player, game.player)
|
||||
|
||||
-- Here we make an sync function that we want to have a delay, note the delay is not defined here
|
||||
local print_message =
|
||||
Async.register(function(player, message)
|
||||
player.print(message)
|
||||
end)
|
||||
|
||||
-- We can then call the async function with a delay using the wait function
|
||||
Async.wait(60, print_message, game.player, 'One second has passed!')
|
||||
|
||||
]]
|
||||
local Task = require 'utils.task' --- @dep utils.task
|
||||
local Token = require 'utils.token' --- @dep utils.token
|
||||
|
||||
local Async = {}
|
||||
|
||||
local internal_run =
|
||||
Token.register(function(params)
|
||||
local func = Token.get(params.token)
|
||||
return func(table.unpack(params.params))
|
||||
end)
|
||||
|
||||
--[[-- Register a new async function, must called when the file is loaded
|
||||
@function register
|
||||
@tparam function callback the function that can be called as an async function
|
||||
@treturn string the uid of the async function which can be passed to Async.run and Async.wait
|
||||
|
||||
@usage-- Registering a function to set the admin state of a player
|
||||
local set_admin =
|
||||
Async.register(function(player, state)
|
||||
if player.valid then
|
||||
player.admin = state
|
||||
end
|
||||
end)
|
||||
|
||||
@usage-- Registering a function to print to a player
|
||||
local print_to_player =
|
||||
Async.register(function(player, message)
|
||||
if player.valid then
|
||||
player.print(message)
|
||||
end
|
||||
end)
|
||||
|
||||
]]
|
||||
Async.register = Token.register
|
||||
|
||||
--[[-- Runs an async function, you may supply any number of arguments as required by that function
|
||||
@tparam string token the token of the async function you want to run
|
||||
@tparam[opt] any ... the other params that you want to pass to your function
|
||||
|
||||
@usage-- Make a player admin regardless of if you are admin
|
||||
Async.run(set_admin, player, true)
|
||||
|
||||
]]
|
||||
function Async.run(token, ...)
|
||||
Task.queue_task(internal_run, {
|
||||
token = token,
|
||||
params = {...}
|
||||
})
|
||||
end
|
||||
|
||||
--[[-- Runs an async function after the given number of ticks, you may supply any number of arguments as required by that function
|
||||
@tparam number ticks the number of ticks that you want the function to run after
|
||||
@tparam string token the token of the async function you want to run
|
||||
@tparam[opt] any ... the other params that you want to pass to your function
|
||||
|
||||
@usage-- Print a message to a player after 5 seconds
|
||||
Async.wait(300, print_to_player, 'Hello, World!')
|
||||
|
||||
]]
|
||||
function Async.wait(ticks, token, ...)
|
||||
Task.set_timeout_in_ticks(ticks, internal_run, {
|
||||
token = token,
|
||||
params = {...}
|
||||
})
|
||||
end
|
||||
|
||||
return setmetatable(Async, {
|
||||
__call = function(self, ...)
|
||||
self.run(...)
|
||||
end
|
||||
})
|
||||
883
exp_legacy/module/expcore/commands.lua
Normal file
883
exp_legacy/module/expcore/commands.lua
Normal file
@@ -0,0 +1,883 @@
|
||||
--[[-- Core Module - Commands
|
||||
- Factorio command making module that makes commands with better parse and more modularity
|
||||
@core Commands
|
||||
@alias Commands
|
||||
|
||||
@usage--- Full code example, see below for explanation
|
||||
Commands.new_command('repeat-name', 'Will repeat you name a number of times in chat.')
|
||||
:add_param('repeat-count', 'number-range-int', 1, 5) -- required int in range 1 to 5 inclusive
|
||||
:add_param('smiley', true, function(input, player, reject) -- optional boolean default false
|
||||
if not input then return end
|
||||
input = input:lower()
|
||||
if input == 'true' or input == 'yes' then return true end
|
||||
return false
|
||||
end)
|
||||
:set_defaults{ smiley = false }
|
||||
:set_flag('admin_only') -- command is admin only
|
||||
:add_alias('name', 'rname') -- allow alias: name and rname
|
||||
:register(function(player, repeat_count, smiley, raw)
|
||||
game.print(player.name..' used a command with input: '..raw)
|
||||
|
||||
local msg = ') '..player.name
|
||||
if smiley then
|
||||
msg = ':'..msg
|
||||
end
|
||||
|
||||
for 1 = 1, repeat_count do
|
||||
Command.print(1..msg)
|
||||
end
|
||||
end)
|
||||
|
||||
@usage--- Example Command Explanation:
|
||||
-- Making commands basics, the commands can be set up with any number of params and flags that you want,
|
||||
-- you can add aliases for the commands and set default values for optional params and of course register your command callback.
|
||||
-- In our example we will have a command that will repeat the users name in chat X amount of times and only allow admins to use it.
|
||||
|
||||
-- First we create the new command, note this will not register the command to the game this is done at the end.
|
||||
-- We will call the command "repeat-name" and set the help message as follows:
|
||||
Commands.new_command('repeat-name', 'Will repeat you name a number of times in chat.')
|
||||
|
||||
-- Now for our first param, we have named it "repeat-count" and it will be a required value, between 1 and 5 inclusive:
|
||||
-- By using "number-range-int" we are saying to use this parser to convert our input text, common ones exist in config.expcore.command_general_parse
|
||||
:add_param('repeat-count', 'number-range-int', 1, 5)
|
||||
|
||||
-- Our second param needs a custom parser, meaning it isnt defined with add_parser, this is an option for when it is unlikely for
|
||||
-- any other command to use the same input type. In the example it is a boolean type and we are just showing it here as part of the example.
|
||||
-- As for the param its self it will be called "smiley" and will be optional with a default value of false:
|
||||
:add_param('smiley', true, function(input, player, reject)
|
||||
-- Since it is optional the input can be nil, in which case we just return
|
||||
if not input then return end
|
||||
-- If it is not nil then we check for a truthy value
|
||||
if input == 'true' or input == 'yes' then return true end
|
||||
-- Note that because we did not return nil or reject then false will be passed to command callback, see example parse
|
||||
return false
|
||||
end)
|
||||
|
||||
-- Once all params are defined you can add some default values for your optional params, the default value will be used only
|
||||
-- when no value is given as input, if an invalid value is given then the command will fail and the default will not be used, the
|
||||
-- default can also be a function which is passed the player as an argument and should return a value to be the default.
|
||||
-- Here we set the default for "smiley" to false:
|
||||
:set_defaults{smiley=false}
|
||||
|
||||
-- Another example of defaults if we have: item, amount[opt], player[opt]
|
||||
:set_defaults{
|
||||
amount = 50, -- More than one value can be set at a time
|
||||
player = function(player) return player end -- Default is the player using the command
|
||||
}
|
||||
|
||||
-- Now the params are set up we can alter how the command works, we can set auth flags, add aliases, or enable "auto concat":
|
||||
:set_flag('admin_only') -- In our case we want "admin_only" to be set to true so only admins can use the command
|
||||
:add_alias('name', 'rname') -- We also add two aliases here: "name" and "rname" which point to this command
|
||||
-- :enable_auto_concat() -- We do not use this in our case but this can also be used to enable the "auto concat" feature
|
||||
|
||||
-- And finally we want to register a callback to this command, the callback is what defines what the command does, can be as complex as you
|
||||
-- want it to be, or as simple as our example; the command receives two params plus all param you have defined:
|
||||
-- 1) the player who used the command
|
||||
-- 2) in our case repeat_count which will be a number
|
||||
-- 3) in our case smiley which will be a boolean
|
||||
-- 4) the raw input; this param is always last as is always present as a catch all
|
||||
:register(function(player, repeat_count, smiley, raw)
|
||||
-- This is to show the value for raw as this is an example command, the log file will also show this
|
||||
game.print(player.name..' used a command with input: '..raw)
|
||||
local msg = ') '..player.name
|
||||
|
||||
if smiley then
|
||||
msg = ':'..msg
|
||||
end
|
||||
|
||||
for 1 = 1, repeat_count do
|
||||
-- this print function will return ANY value to the user in a desync safe manor, this includes if the command was used through rcon
|
||||
Command.print(1..msg)
|
||||
end
|
||||
-- See below for what can be used here
|
||||
end)
|
||||
|
||||
-- Values that can be returned from register callback
|
||||
Commands.print(any, colour[opt]) -- This will return any value value to the user including if it is ran through rcon console
|
||||
Commands.error(message[opt]) -- This returns a warning to the user, aka an error that does not prevent execution of the command
|
||||
return Commands.error(message[opt]) -- This returns an error to the user, and will halt the command execution, ie no success message is returned
|
||||
Commands.success(message[opt]) -- Used to return a success message however don't use this method, see below
|
||||
return Commands.success(message[opt]) -- Will return the success message to the user and your given message, halts execution
|
||||
return <any> -- If any value is returned then it will be returned to the player via a Commands.success call
|
||||
|
||||
@usage--- Example Authenticator:
|
||||
-- The command system is best used when you can control who uses commands;
|
||||
-- to do this you need to define an authenticator which is ran every time a command is run;
|
||||
-- in this example I will show a simple one that requires certain commands to require the user to be a game admin.
|
||||
|
||||
-- For our admin only example we will set a flag to true when we want it to be admin only;
|
||||
-- when we define the command will will use :set_flag('admin_only');
|
||||
-- then inside the authenticator we will test if the flag is present using: if flags.admin_only then
|
||||
|
||||
-- When the authenticator is called by the command handler it will be passed 4 arguments:
|
||||
-- 1) player - the player who used the command
|
||||
-- 2) command - the name of the command that is being used
|
||||
-- 3) flags - the flags which have been set for this command, flags are set with :set_flag(name, value)
|
||||
-- 4) reject - the reject function which is the preferred method to prevent execution of the command
|
||||
|
||||
-- No return is required to allow the command to execute but it is best practice to return true;
|
||||
-- we do this in two cases in our authenticator:
|
||||
-- 1) when the "admin_only" flag is not set, which we take assume that any one can use it
|
||||
-- 2) when the "admin_only" flag is set, and the player is admin
|
||||
|
||||
-- When want to prevent execution of the command we must reject it, listed is how that can be done:
|
||||
-- 1) return false -- this is the most basic rejection and should only be used while testing
|
||||
-- 2) return reject -- returning the reject function is as a fail safe in case you forget to call it, same as returning false
|
||||
-- 3) reject() -- this will block execution while allowing further code to be ran in your authenticator
|
||||
-- 4) reject('This command is for admins only!') -- using reject as a function allows a error message to be returned
|
||||
-- 5) return reject() -- using return on either case above is best practice as you should execute all your code before rejecting
|
||||
|
||||
-- Example Code:
|
||||
Commands.add_authenticator(function(player, command, flags, reject)
|
||||
-- Check if the command is admin only
|
||||
if flags.admin_only then
|
||||
-- Return true if player is admin, or reject and return error message
|
||||
return player.admin or reject('This command is for admins only!')
|
||||
else
|
||||
-- Return true if command was not admin only
|
||||
return true
|
||||
end
|
||||
end)
|
||||
|
||||
@usage--- Example Parser:
|
||||
-- Before you make a command it is important to understand the most powerful feature of this command handler;
|
||||
-- when you define a command you are able to type the params and have then be parsed and validated before your command is executed;
|
||||
-- This module should be paired with a general command parse but you may want to create your own.
|
||||
|
||||
-- For our example we will create a parse to accept only integer numbers in a given range:
|
||||
-- 1) we will give it the name "number-range-int" this is the "type" that the input is expected to be
|
||||
-- 2) when we define the type we will also define the min and max of the range so we can use the function more than once
|
||||
:add_param('repeat_count', 'number-range-int', 5, 10) -- "repeat_count" is a required "number-range-int" in a range 5 to 10 inclusive
|
||||
|
||||
-- The command parse will be passed 3 arguments plus any other which you define, in our case:
|
||||
-- 1) input - the input that has been given by the user for this param, the role of this function is to transform this value
|
||||
-- nb: the input is a string but can be nil if the param is marked as optional
|
||||
-- 2) player - the player who is using the command, this is always present
|
||||
-- 3) reject - the reject function to throw an error to the user, this is always present
|
||||
-- 4) range_min - the range min, this is user defined and has the value given when the param is defined
|
||||
-- 5) range_max - the range max, this is user defined and has the value given when the param is defined
|
||||
|
||||
-- When returning from the param parse you have a few options with how to do this:
|
||||
-- 1) you return the new value for the param (any non nil value) this value is then passed to the command callback
|
||||
-- 2) not returning will cause a generic invalid error and the command is rejected, not recommenced
|
||||
-- 3) return reject -- this is just a failsafe in case the function is not called, same as no return
|
||||
-- 4) return reject() -- will give a shorter error message as you pass a nil custom error
|
||||
-- 5) return reject('Number entered is not in range: '..range_min..', '..range_max) -- returns a custom error to the user
|
||||
-- nb: if you do not return reject after you call it then you will still be returning nil so there will be a duplicate error message
|
||||
|
||||
-- It should be noted that if you want to expand on an existing parse you can use Commands.parse(type, input, player, reject)
|
||||
-- this function will either return a new value for the input or nil, if it is nil you should return nil to prevent duplicate
|
||||
-- error messages to the user:
|
||||
input = Commands.parse('number-int', input, player, reject)
|
||||
if not input then return end -- nil check
|
||||
|
||||
-- Example Code:
|
||||
Commands.add_parse('number-range-int', function(input, player, reject, range_min, range_max)
|
||||
local rtn = tonumber(input) and math.floor(tonumber(input)) or nil -- converts input to number
|
||||
if not rtn or rtn < range_min or rtn > range_max then
|
||||
-- the input is either not a number or is outside the range
|
||||
return reject('Number entered is not in range: '..range_min..', '..range_max)
|
||||
else
|
||||
-- returns the input as a number value rather than a string, thus the param is now the correct type
|
||||
return rtn
|
||||
end
|
||||
end)
|
||||
|
||||
]]
|
||||
|
||||
local Game = require 'utils.game' --- @dep utils.game
|
||||
local player_return, write_json = _C.player_return, _C.write_json --- @dep expcore.common
|
||||
local trace = debug.traceback
|
||||
|
||||
local Commands = {
|
||||
--- Constant values used by the command system
|
||||
defines = {
|
||||
error = 'CommandError',
|
||||
unauthorized = 'CommandErrorUnauthorized',
|
||||
success = 'CommandSuccess'
|
||||
},
|
||||
--- An array of all custom commands that are registered
|
||||
commands = {},
|
||||
--- When true any authenticator error will result in authorization failure, more secure
|
||||
authorization_failure_on_error = false,
|
||||
--- An array of all custom authenticators that are registered
|
||||
authenticators = {},
|
||||
--- Used to store default functions which are common parse function such as player or number in range
|
||||
parsers = {},
|
||||
--- Returns a value to the player, different to success as this does not signal the end of your command
|
||||
print = player_return,
|
||||
--- The command prototype which stores all command defining functions
|
||||
_prototype = {},
|
||||
}
|
||||
|
||||
--- Authentication.
|
||||
-- Functions that control who can use commands
|
||||
-- @section auth
|
||||
|
||||
--[[-- Adds an authorization function, function used to check if a player if allowed to use a command
|
||||
@tparam function authenticator The function you want to register as an authenticator
|
||||
@treturn number The index it was inserted at, used to remove the authenticator
|
||||
|
||||
@usage-- If the admin_only flag is set, then make sure the player is an admin
|
||||
local admin_authenticator =
|
||||
Commands.add_authenticator(function(player, command, flags, reject)
|
||||
if flags.admin_only and not player.admin then
|
||||
return reject('This command is for admins only!')
|
||||
else
|
||||
return true
|
||||
end
|
||||
end)
|
||||
|
||||
]]
|
||||
function Commands.add_authenticator(authenticator)
|
||||
local next_index = #Commands.authenticators + 1
|
||||
Commands.authenticators[next_index] = authenticator
|
||||
return next_index
|
||||
end
|
||||
|
||||
--[[-- Removes an authorization function, can use the index or the function value
|
||||
@tparam function|number authenticator The authenticator to remove, either the index return from add_authenticator or the function used
|
||||
@treturn boolean If the authenticator was found and removed successfully
|
||||
|
||||
@usage-- Removing the admin authenticator, can not be done during runtime
|
||||
Commands.remove_authenticator(admin_authenticator)
|
||||
|
||||
]]
|
||||
function Commands.remove_authenticator(authenticator)
|
||||
if type(authenticator) == 'number' then
|
||||
-- If a number is passed then it is assumed to be the index
|
||||
if Commands.authenticators[authenticator] then
|
||||
Commands.authenticators[authenticator] = nil
|
||||
return true
|
||||
end
|
||||
else
|
||||
-- will search the array and remove the key
|
||||
for index, value in pairs(Commands.authenticators) do
|
||||
if value == authenticator then
|
||||
Commands.authenticators[index] = nil
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--[[-- Mostly used internally, calls all authenticators, returns if the player is authorized
|
||||
@tparam LuaPlayer player The player who is using the command, passed to authenticators
|
||||
@tparam string command_name The name of the command being used, passed to authenticators
|
||||
@treturn[1] boolean true Player is authorized
|
||||
@treturn[1] string commands Define value for success
|
||||
@treturn[2] boolean false Player is unauthorized
|
||||
@treturn[2] string|locale_string The reason given by the failed authenticator
|
||||
|
||||
@usage-- Test if a player can use "repeat-name"
|
||||
local authorized, status = Commands.authorize(game.player, 'repeat-name')
|
||||
|
||||
]]
|
||||
function Commands.authorize(player, command_name)
|
||||
local command_data = Commands.commands[command_name]
|
||||
if not command_data then return false end
|
||||
if not player then return true end
|
||||
|
||||
-- This is the reject function given to authenticators
|
||||
local failure_message
|
||||
local function reject(message)
|
||||
failure_message = message or {'expcore-commands.unauthorized'}
|
||||
return Commands.defines.unauthorized
|
||||
end
|
||||
|
||||
-- This is the internal error function used when an authenticator errors
|
||||
local function authenticator_error(err)
|
||||
log('[ERROR] Authorization failed: '..trace(err))
|
||||
if Commands.authorization_failure_on_error then
|
||||
return reject('Internal Error')
|
||||
end
|
||||
end
|
||||
|
||||
-- Loops over each authenticator, if any return false then then command will not be ran
|
||||
for _, authenticator in pairs(Commands.authenticators) do
|
||||
-- player: LuaPlayer, command: string, flags: table, reject: function(error_message: string)
|
||||
local _, rtn = xpcall(authenticator, authenticator_error, player, command_name, command_data.flags, reject)
|
||||
if rtn == false or rtn == Commands.defines.unauthorized or rtn == reject or failure_message ~= nil then
|
||||
if failure_message == nil then failure_message = {'expcore-commands.unauthorized'} end
|
||||
return false, failure_message
|
||||
end
|
||||
end
|
||||
|
||||
return true, Commands.defines.success
|
||||
end
|
||||
|
||||
--- Parse.
|
||||
-- Functions that help with parsing
|
||||
-- @section parse
|
||||
|
||||
--[[-- Adds a parse function which can be called by name (used in add_param)
|
||||
nb: this is not required as you can use the callback directly this just allows it to be called by name
|
||||
@tparam string name The name of the parse, should describe a type of input such as number or player, must be unique
|
||||
@tparam function parser The function that is ran to parse the input string
|
||||
@treturn boolean Was the parse added, will be false if the name is already used
|
||||
|
||||
@usage-- Adding a parse to validate integers in a given range
|
||||
Commands.add_parse('number-range-int', function(input, player, reject, range_min, range_max)
|
||||
local rtn = tonumber(input) and math.floor(tonumber(input)) or nil -- converts input to number
|
||||
if not rtn or rtn < range_min or rtn > range_max then
|
||||
-- The input is either not a number or is outside the range
|
||||
return reject('Number entered is not in range: '..range_min..', '..range_max)
|
||||
else
|
||||
-- Returns the input as a number rather than a string, thus the param is now the correct type
|
||||
return rtn
|
||||
end
|
||||
end)
|
||||
|
||||
]]
|
||||
function Commands.add_parse(name, parser)
|
||||
if Commands.parsers[name] then return false end
|
||||
Commands.parsers[name] = parser
|
||||
return true
|
||||
end
|
||||
|
||||
--[[-- Removes a parse function, see add_parse for adding them, cant be done during runtime
|
||||
@tparam string name The name of the parse to remove
|
||||
|
||||
@usage-- Removing a parse
|
||||
Commands.remove_parse('number-range-int')
|
||||
|
||||
]]
|
||||
function Commands.remove_parse(name)
|
||||
Commands.parsers[name] = nil
|
||||
end
|
||||
|
||||
--[[-- Intended to be used within other parse functions, runs a parse and returns success and new value
|
||||
@tparam string name The name of the parse to call, must be a registered parser
|
||||
@tparam string input The input to pass to the parse, must be a string but not necessarily the original input
|
||||
@tparam LuaPlayer player The player that is using the command, pass directly from your arguments
|
||||
@tparam function reject The reject function, pass directly from your arguments
|
||||
@treturn any The new value for the input, if nil is return then either there was an error or the input was nil
|
||||
|
||||
@usage-- Parsing an int after first checking it is a number
|
||||
Commands.add_parse('number', function(input, player, reject)
|
||||
local number = tonumber(input)
|
||||
if number then return number end
|
||||
return reject('Input must be a number value')
|
||||
end)
|
||||
|
||||
Commands.add_parse('number-int', function(input, player, reject)
|
||||
local number = Commands.parse('number', input, player, reject)
|
||||
if not number then return end
|
||||
return math.floor(number)
|
||||
end)
|
||||
|
||||
]]
|
||||
function Commands.parse(name, input, player, reject, ...)
|
||||
if not Commands.parsers[name] then return end
|
||||
local success, rtn = pcall(Commands.parsers[name], input, player, reject, ...)
|
||||
if not success then error(rtn, 2) return end
|
||||
if not rtn or rtn == Commands.defines.error then return end
|
||||
return rtn
|
||||
end
|
||||
|
||||
--- Getters.
|
||||
-- Functions that get commands
|
||||
-- @section getters
|
||||
|
||||
--[[-- Gets all commands that a player is allowed to use, game commands are not included
|
||||
@tparam[opt] LuaPlayer player The player that you want to get commands of, nil will return all commands
|
||||
@treturn table All commands that that player is allowed to use, or all commands
|
||||
|
||||
@usage-- Get the commands you are allowed to use
|
||||
local commands = Commands.get(game.player)
|
||||
|
||||
@usage-- Get all commands that are registered
|
||||
local commands = Commands.get()
|
||||
|
||||
]]
|
||||
function Commands.get(player)
|
||||
player = Game.get_player_from_any(player)
|
||||
|
||||
if not player then
|
||||
return Commands.commands
|
||||
end
|
||||
|
||||
local allowed = {}
|
||||
|
||||
for name, command_data in pairs(Commands.commands) do
|
||||
if Commands.authorize(player, name) then
|
||||
allowed[name] = command_data
|
||||
end
|
||||
end
|
||||
|
||||
return allowed
|
||||
end
|
||||
|
||||
--[[-- Searches command names and help messages to find possible commands, game commands are included
|
||||
@tparam string keyword The word which you are trying to find in your search
|
||||
@tparam[opt] LuaPlayer player The player to get allowed commands of, if nil all commands are searched
|
||||
@treturn table All commands that contain the key word, and allowed by the player if a player was given
|
||||
|
||||
@usage-- Get all commands which "repeat"
|
||||
local commands = Commands.search('repeat')
|
||||
|
||||
@usage-- Get all commands which "repeat" and you are allowed to use
|
||||
local commands = Commands.search('repeat', game.player)
|
||||
|
||||
]]
|
||||
function Commands.search(keyword, player)
|
||||
local custom_commands = Commands.get(player)
|
||||
local matches = {}
|
||||
keyword = keyword:lower()
|
||||
|
||||
-- Loops over custom commands
|
||||
for name, command_data in pairs(custom_commands) do
|
||||
-- combines name help and aliases into one message to be searched
|
||||
local search = string.format('%s %s %s %s', name, command_data.help, command_data.searchable_description, table.concat(command_data.aliases, ' '))
|
||||
|
||||
if search:lower():match(keyword) then
|
||||
matches[name] = command_data
|
||||
end
|
||||
end
|
||||
|
||||
-- Loops over the names of game commands
|
||||
for name, description in pairs(commands.game_commands) do
|
||||
if name:lower():match(keyword) then
|
||||
-- because game commands lack some stuff that the custom ones have they are formatted
|
||||
matches[name] = {
|
||||
name = name,
|
||||
help = description,
|
||||
description = '',
|
||||
aliases = {}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return matches
|
||||
end
|
||||
|
||||
--- Creation.
|
||||
-- Functions that create a new command
|
||||
-- @section creation
|
||||
|
||||
--[[-- Creates a new command object to added details to, this does not register the command to the game api
|
||||
@tparam string name The name of the command to be created
|
||||
@tparam string help The help message for the command
|
||||
@treturn table This will be used with other functions to define the new command
|
||||
|
||||
@usage-- Define a new command
|
||||
Commands.new_command('repeat-name', 'Will repeat you name a number of times in chat.')
|
||||
|
||||
]]
|
||||
function Commands.new_command(name, help, descr)
|
||||
local command = setmetatable({
|
||||
name = name,
|
||||
help = help,
|
||||
searchable_description = descr or '',
|
||||
callback = function() Commands.internal_error(false, name, 'No callback registered') end,
|
||||
auto_concat = false,
|
||||
min_param_count = 0,
|
||||
max_param_count = 0,
|
||||
flags = {}, -- stores flags that can be used by auth
|
||||
aliases = {}, -- stores aliases to this command
|
||||
params = {}, -- [param_name] = {optional: boolean, default: any, parse: function, parse_args: table}
|
||||
}, {
|
||||
__index = Commands._prototype
|
||||
})
|
||||
|
||||
Commands.commands[name] = command
|
||||
|
||||
return command
|
||||
end
|
||||
|
||||
--[[-- Adds a new param to the command this will be displayed in the help and used to parse the input
|
||||
@tparam string name The name of the new param that is being added to the command
|
||||
@tparam[opt=false] boolean optional Is this param optional, these must be added after all required params
|
||||
@tparam[opt] ?string|function parse This function will take the input and return a new value, if not given no parse is done
|
||||
@tparam[opt] any ... Extra args you want to pass to the parse function; for example if the parse is general use
|
||||
@treturn table Pass through to allow more functions to be called
|
||||
|
||||
@usage-- Adding a required param which has a parser pre-defined
|
||||
command:add_param('repeat-count', 'number-range-int', 1, 5)
|
||||
|
||||
@usage-- Adding an optional param which has a custom parse, see Commands.add_parse for details
|
||||
command:add_param('smiley', true, function(input, player, reject)
|
||||
if not input then return end
|
||||
return input:lower() == 'true' or input:lower() == 'yes' or false
|
||||
end)
|
||||
|
||||
]]
|
||||
function Commands._prototype:add_param(name, optional, parse, ...)
|
||||
local parse_args = {...}
|
||||
if type(optional) ~= 'boolean' then
|
||||
parse_args = {parse, ...}
|
||||
parse = optional
|
||||
optional = false
|
||||
end
|
||||
|
||||
self.params[name] = {
|
||||
optional = optional,
|
||||
parse = parse or function(string) return string end,
|
||||
parse_args = parse_args
|
||||
}
|
||||
|
||||
self.max_param_count = self.max_param_count + 1
|
||||
if not optional then
|
||||
self.min_param_count = self.min_param_count + 1
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Add default values to params, only as an effect if the param is optional, if default value is a function it is called with the acting player
|
||||
@tparam table defaults A table which is keyed by the name of the param and the value is the default value for that param
|
||||
@treturn table Pass through to allow more functions to be called
|
||||
|
||||
@usage-- Adding default values
|
||||
command:set_defaults{
|
||||
smiley = false,
|
||||
-- not in example just used to show arguments given
|
||||
player_name = function(player)
|
||||
return player.name
|
||||
end
|
||||
}
|
||||
|
||||
]]
|
||||
function Commands._prototype:set_defaults(defaults)
|
||||
for name, value in pairs(defaults) do
|
||||
if self.params[name] then
|
||||
self.params[name].default = value
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Adds a flag to the command which is passed via the flags param to the authenticators, can be used to assign command roles or usage type
|
||||
@tparam string name The name of the flag to be added, set to true if no value is given
|
||||
@tparam[opt=true] any value The value for the flag, can be anything that the authenticators are expecting
|
||||
@treturn table Pass through to allow more functions to be called
|
||||
|
||||
@usage-- Setting a custom flag
|
||||
command:set_flag('admin_only', true)
|
||||
|
||||
@usage-- When value is true it does not need to be given
|
||||
command:set_flag('admin_only')
|
||||
|
||||
]]
|
||||
function Commands._prototype:set_flag(name, value)
|
||||
self.flags[name] = value or true
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Adds an alias, or multiple, that will be registered to this command, eg /teleport can be used as /tp
|
||||
@tparam string ... Any amount of aliases that you want this command to be callable with
|
||||
@treturn table Pass through to allow more functions to be called
|
||||
|
||||
@usage-- Added multiple aliases to a command
|
||||
command:add_alias('name', 'rname')
|
||||
|
||||
]]
|
||||
function Commands._prototype:add_alias(...)
|
||||
local start_index = #self.aliases
|
||||
for index, alias in ipairs{...} do
|
||||
self.aliases[start_index+index] = alias
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Enables auto concatenation for this command, all params after the last are added to the last param, useful for reasons or other long text input
|
||||
nb: this will disable max param checking as they will be concatenated onto the end of that last param
|
||||
@treturn table Pass through to allow more functions to be called
|
||||
|
||||
@usage-- Enable auto concat for a command
|
||||
command:enable_auto_concat()
|
||||
|
||||
]]
|
||||
function Commands._prototype:enable_auto_concat()
|
||||
self.auto_concat = true
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Adds the callback to the command and registers: aliases, params and help message with the base game api
|
||||
nb: this must be the last function ran on the command and must be done for the command to work
|
||||
@tparam function callback The callback for the command, will receive the player running command, and any params added with add_param
|
||||
|
||||
@usage-- Registering your command to the base game api
|
||||
command:register(function(player, repeat_count, smiley, raw)
|
||||
local msg = ') '..player.name
|
||||
if smiley then msg = ':'..msg end
|
||||
|
||||
for 1 = 1, repeat_count do
|
||||
Command.print(1..msg)
|
||||
end
|
||||
end)
|
||||
|
||||
]]
|
||||
function Commands._prototype:register(callback)
|
||||
self.callback = callback
|
||||
|
||||
-- Generates a description to be used
|
||||
local description = ''
|
||||
for param_name, param_details in pairs(self.params) do
|
||||
if param_details.optional then
|
||||
description = string.format('%s [%s]', description, param_name)
|
||||
else
|
||||
description = string.format('%s <%s>', description, param_name)
|
||||
end
|
||||
end
|
||||
self.description = description
|
||||
|
||||
-- Last resort error handler for commands
|
||||
local function command_error(err)
|
||||
Commands.internal_error(false, self.name, trace(err))
|
||||
end
|
||||
|
||||
-- Callback that the game will call
|
||||
local function command_callback(event)
|
||||
event.name = self.name
|
||||
xpcall(Commands.run_command, command_error, event)
|
||||
end
|
||||
|
||||
-- Registers the command under its own name
|
||||
local help = {'expcore-commands.command-help', description, self.help}
|
||||
commands.add_command(self.name, help, command_callback)
|
||||
|
||||
-- Adds any aliases that it has
|
||||
for _, alias in pairs(self.aliases) do
|
||||
if not commands.commands[alias] and not commands.game_commands[alias] then
|
||||
commands.add_command(alias, help, command_callback)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Status.
|
||||
-- Functions that indicate status
|
||||
-- @section status
|
||||
|
||||
--[[-- Sends a value to the player, followed by a command complete message, returning a value will trigger this automatically
|
||||
@tparam[opt] any value The value to return to the player, if nil then only the success message is returned
|
||||
@treturn Commands.defines.success Return this to the command handler to prevent two success messages
|
||||
|
||||
@usage-- Print a custom success message
|
||||
return Commands.success('Your message has been printed')
|
||||
|
||||
@usage-- Returning the value has the same result
|
||||
return 'Your message has been printed'
|
||||
|
||||
]]
|
||||
function Commands.success(value)
|
||||
if value ~= nil then player_return(value) end
|
||||
player_return({'expcore-commands.command-ran'}, 'cyan')
|
||||
return Commands.defines.success
|
||||
end
|
||||
|
||||
--[[-- Sends a value to the player, different to success as this does not signal the end of your command
|
||||
@function print
|
||||
@tparam any value The value that you want to return to the player
|
||||
@tparam table colour The colour of the message that the player sees
|
||||
|
||||
@usage-- Output a message to the player
|
||||
Commands.print('Your command is in progress')
|
||||
|
||||
]]
|
||||
|
||||
--[[-- Sends an error message to the player and when returned will stop execution of the command
|
||||
nb: this is for non fatal errors meaning there is no log of this event, use during register callback
|
||||
@tparam[opt=''] string error_message An optional error message that can be sent to the user
|
||||
@tparam[opt=utility/wire_pickup] string play_sound The sound to play for the error
|
||||
@treturn Commands.defines.error Return this to command handler to terminate execution
|
||||
|
||||
@usage-- Send an error message to the player, and stops further code running
|
||||
return Commands.error('The player you selected is offline')
|
||||
|
||||
]]
|
||||
function Commands.error(error_message, play_sound)
|
||||
error_message = error_message or ''
|
||||
player_return({'expcore-commands.command-fail', error_message}, 'orange_red')
|
||||
if play_sound ~= false then
|
||||
play_sound = play_sound or 'utility/wire_pickup'
|
||||
if game.player then game.player.play_sound{path=play_sound} end
|
||||
end
|
||||
return Commands.defines.error
|
||||
end
|
||||
|
||||
--[[-- Sends an error to the player and logs the error, used internally please avoid direct use
|
||||
nb: use error(error_message) within your callback to trigger do not trigger directly as code execution may still continue
|
||||
@tparam boolean success The success value returned from pcall, or just false to trigger error
|
||||
@tparam string command_name The name of the command this is used within the log
|
||||
@tparam string error_message The error returned by pcall or some other error, this is logged and not returned to player
|
||||
@treturn boolean The opposite of success so true means to cancel execution, used internally
|
||||
|
||||
@usage-- Used in the command system to log handler errors
|
||||
local success, err = pcall(command_data.callback, player, table.unpack(params))
|
||||
if Commands.internal_error(success, command_data.name, err) then
|
||||
return command_log(player, command_data, 'Internal Error: Command Callback Fail', raw_params, command_event.parameter, err)
|
||||
end
|
||||
|
||||
]]
|
||||
function Commands.internal_error(success, command_name, error_message)
|
||||
if not success then
|
||||
Commands.error('Internal Error, Please contact an admin', 'utility/cannot_build')
|
||||
log{'expcore-commands.command-error-log-format', command_name, error_message}
|
||||
end
|
||||
return not success
|
||||
end
|
||||
|
||||
--- Logs command usage to file
|
||||
local function command_log(player, command, comment, params, raw, details)
|
||||
local player_name = player and player.name or '<Server>'
|
||||
write_json('log/commands.log', {
|
||||
player_name = player_name,
|
||||
command_name = command.name,
|
||||
comment = comment,
|
||||
details = details,
|
||||
params = params,
|
||||
raw = raw
|
||||
})
|
||||
end
|
||||
|
||||
--- Main event function that is ran for all commands, used internally please avoid direct use
|
||||
-- @tparam table command_event Passed directly from the add_command function
|
||||
-- @usage Commands.run_command(event)
|
||||
function Commands.run_command(command_event)
|
||||
local command_data = Commands.commands[command_event.name]
|
||||
-- Player can be nil when it is the server
|
||||
local player
|
||||
if command_event.player_index and command_event.player_index > 0 then
|
||||
player = game.players[command_event.player_index]
|
||||
end
|
||||
|
||||
-- Check if the player is allowed to use the command
|
||||
local authorized, auth_fail = Commands.authorize(player, command_data.name)
|
||||
if not authorized then
|
||||
command_log(player, command_data, 'Failed Auth', {}, command_event.parameter)
|
||||
Commands.error(auth_fail, 'utility/cannot_build')
|
||||
return
|
||||
end
|
||||
|
||||
-- Check for parameter being nil
|
||||
if command_data.min_param_count > 0 and not command_event.parameter then
|
||||
command_log(player, command_data, 'No Params Given', {}, command_event.parameter)
|
||||
Commands.error{'expcore-commands.invalid-inputs', command_data.name, command_data.description}
|
||||
return
|
||||
end
|
||||
|
||||
-- Extract quoted arguments
|
||||
local raw_input = command_event.parameter or ''
|
||||
local quote_params = {}
|
||||
local input_string = raw_input:gsub('"[^"]-"', function(word)
|
||||
local no_spaces = word:gsub('%s', '%%s')
|
||||
quote_params[no_spaces] = word:sub(2, -2)
|
||||
return ' '..no_spaces..' '
|
||||
end)
|
||||
|
||||
-- Extract unquoted arguments
|
||||
local raw_params = {}
|
||||
local last_index = 0
|
||||
local param_number = 0
|
||||
for word in input_string:gmatch('%S+') do
|
||||
param_number = param_number + 1
|
||||
if param_number > command_data.max_param_count then
|
||||
-- there are too many params given to the command
|
||||
if not command_data.auto_concat then
|
||||
-- error as they should not be more
|
||||
command_log(player, command_data, 'Invalid Input: Too Many Params', raw_params, raw_input)
|
||||
Commands.error{'expcore-commands.invalid-inputs', command_data.name, command_data.description}
|
||||
return
|
||||
else
|
||||
-- concat to the last param
|
||||
if quote_params[word] then
|
||||
raw_params[last_index] = raw_params[last_index]..' "'..quote_params[word]..'"'
|
||||
else
|
||||
raw_params[last_index] = raw_params[last_index]..' '..word
|
||||
end
|
||||
end
|
||||
else
|
||||
-- new param that needs to be added
|
||||
if quote_params[word] then
|
||||
last_index = last_index + 1
|
||||
raw_params[last_index] = quote_params[word]
|
||||
else
|
||||
last_index = last_index + 1
|
||||
raw_params[last_index] = word
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check the param count
|
||||
local param_count = #raw_params
|
||||
if param_count < command_data.min_param_count then
|
||||
command_log(player, command_data, 'Invalid Input: Not Enough Params', raw_params, raw_input)
|
||||
Commands.error{'expcore-commands.invalid-inputs', command_data.name, command_data.description}
|
||||
return
|
||||
end
|
||||
|
||||
-- Parse the arguments
|
||||
local index = 1
|
||||
local params = {}
|
||||
for param_name, param_data in pairs(command_data.params) do
|
||||
local parse_callback = param_data.parse
|
||||
-- If its a string this get it from the parser table
|
||||
if type(parse_callback) == 'string' then
|
||||
parse_callback = Commands.parsers[parse_callback]
|
||||
end
|
||||
|
||||
-- If its not a function throw and error
|
||||
if type(parse_callback) ~= 'function' then
|
||||
Commands.internal_error(false, command_data.name, 'Invalid param parse '..tostring(param_data.parse))
|
||||
command_log(player, command_data, 'Internal Error: Invalid Param Parse', params, raw_input, tostring(param_data.parse))
|
||||
return
|
||||
end
|
||||
|
||||
-- This is the reject function given to parse callbacks
|
||||
local function reject(error_message)
|
||||
error_message = error_message or ''
|
||||
command_log(player, command_data, 'Invalid Param Given', raw_params, input_string)
|
||||
return Commands.error{'expcore-commands.invalid-param', param_name, error_message}
|
||||
end
|
||||
|
||||
-- input: string, player: LuaPlayer, reject: function, ... extra args
|
||||
local success, param_parsed = pcall(parse_callback, raw_params[index], player, reject, table.unpack(param_data.parse_args))
|
||||
if Commands.internal_error(success, command_data.name, param_parsed) then
|
||||
return command_log(player, command_data, 'Internal Error: Param Parse Fail', params, raw_input, param_parsed)
|
||||
end
|
||||
|
||||
if param_data.optional == true and raw_params[index] == nil then
|
||||
-- If the param is optional and nil then it is set to default
|
||||
param_parsed = param_data.default
|
||||
if type(param_parsed) == 'function' then
|
||||
success, param_parsed = pcall(param_parsed, player)
|
||||
if Commands.internal_error(success, command_data.name, param_parsed) then
|
||||
return command_log(player, command_data, 'Internal Error: Default Value Fail', params, raw_input, param_parsed)
|
||||
end
|
||||
end
|
||||
|
||||
elseif param_parsed == nil or param_parsed == Commands.defines.error or param_parsed == reject then
|
||||
-- No value was returned or error was returned, if nil then give generic error
|
||||
if param_parsed ~= Commands.defines.error then
|
||||
command_log(player, command_data, 'Invalid Param Given', raw_params, raw_input, param_name)
|
||||
Commands.error{'expcore-commands.command-error-param-format', param_name, 'please make sure it is the correct type'}
|
||||
end
|
||||
return
|
||||
|
||||
end
|
||||
|
||||
-- Add the param to the table to be passed to the command callback
|
||||
params[index] = param_parsed
|
||||
index = index + 1
|
||||
end
|
||||
|
||||
-- Run the command
|
||||
-- player: LuaPlayer, ... command params, raw: string
|
||||
params[command_data.max_param_count+1] = raw_input
|
||||
local success, rtn = pcall(command_data.callback, player, table.unpack(params))
|
||||
if Commands.internal_error(success, command_data.name, rtn) then
|
||||
return command_log(player, command_data, 'Internal Error: Command Callback Fail', raw_params, command_event.parameter, rtn)
|
||||
end
|
||||
|
||||
-- Give output to the player
|
||||
if rtn == Commands.defines.error or rtn == Commands.error then
|
||||
return command_log(player, command_data, 'Custom Error', raw_params, raw_input)
|
||||
elseif rtn ~= Commands.defines.success and rtn ~= Commands.success then
|
||||
Commands.success(rtn)
|
||||
end
|
||||
command_log(player, command_data, 'Success', raw_params, raw_input)
|
||||
end
|
||||
|
||||
return Commands
|
||||
825
exp_legacy/module/expcore/common.lua
Normal file
825
exp_legacy/module/expcore/common.lua
Normal file
@@ -0,0 +1,825 @@
|
||||
--[[-- Core Module - Common
|
||||
- Adds some commonly used functions used in many modules
|
||||
@core Common
|
||||
@alias Common
|
||||
]]
|
||||
|
||||
local Colours = require 'utils.color_presets' --- @dep utils.color_presets
|
||||
local Game = require 'utils.game' --- @dep utils.game
|
||||
|
||||
local Common = {}
|
||||
|
||||
--- Type Checking.
|
||||
-- @section typeCheck
|
||||
|
||||
--[[-- Asserts the argument is of type test_type
|
||||
@tparam any value the value to be tested
|
||||
@tparam[opt=nil] string test_type the type to test for if not given then it tests for nil
|
||||
@treturn boolean is v of type test_type
|
||||
|
||||
@usage-- Check for a string value
|
||||
local is_string = type_check(value, 'string')
|
||||
|
||||
@usage-- Check for a nil value
|
||||
local is_nil = type_check(value)
|
||||
|
||||
]]
|
||||
function Common.type_check(value, test_type)
|
||||
return test_type and value and type(value) == test_type or not test_type and not value or false
|
||||
end
|
||||
|
||||
--[[-- Raises an error if the value is of the wrong type
|
||||
@tparam any value the value that you want to test the type of
|
||||
@tparam string test_type the type that the value should be
|
||||
@tparam string error_message the error message that is returned
|
||||
@tparam number level the level to call the error on (level = 1 is the caller)
|
||||
@treturn boolean true if no error was called
|
||||
|
||||
@usage-- Raise error if value is not a number
|
||||
type_error(value, 'number', 'Value must be a number')
|
||||
|
||||
]]
|
||||
function Common.type_error(value, test_type, error_message, level)
|
||||
level = level and level+1 or 2
|
||||
return Common.type_check(value, test_type) or error(error_message, level)
|
||||
end
|
||||
|
||||
--[[-- Asserts the argument is one of type test_types
|
||||
@param value the variable to check
|
||||
@param test_types the type as a table of strings
|
||||
@treturn boolean true if value is one of test_types
|
||||
|
||||
@usage-- Check for a string or table
|
||||
local is_string_or_table = multi_type_check(value, {'string', 'table'})
|
||||
|
||||
]]
|
||||
function Common.multi_type_check(value, test_types)
|
||||
local vtype = type(value)
|
||||
for _, arg_type in ipairs(test_types) do
|
||||
if vtype == arg_type then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--[[-- Raises an error if the value is of the wrong type
|
||||
@tparam any value the value that you want to test the type of
|
||||
@tparam table test_types the type as a table of strings
|
||||
@tparam string error_message the error message that is returned
|
||||
@tparam number level the level to call the error on (level = 1 is the caller)
|
||||
@treturn boolean true if no error was called
|
||||
|
||||
@usage-- Raise error if value is not a string or table
|
||||
multi_type_error('foo', {'string', 'table'}, 'Value must be a string or table')
|
||||
|
||||
]]
|
||||
function Common.multi_type_error(value, test_types, error_message, level)
|
||||
level = level and level+1 or 2
|
||||
return Common.mult_type_check(value, test_types) or error(error_message, level)
|
||||
end
|
||||
|
||||
--[[-- Raises an error when the value is the incorrect type, uses a consistent error message format
|
||||
@tparam any value the value that you want to test the type of
|
||||
@tparam string test_type the type that the value should be
|
||||
@tparam number param_number the number param it is
|
||||
@tparam[opt] string param_name the name of the param
|
||||
@treturn boolean true if no error was raised
|
||||
|
||||
@usage-- Output: "Bad argument #2 to "<anon>"; argument is of type string expected number"
|
||||
validate_argument_type(value, 'number', 2)
|
||||
|
||||
@usage-- Output: "Bad argument #2 to "<anon>"; "repeat_count" is of type string expected number"
|
||||
validate_argument_type(value, 'number', 2, 'repeat_count')
|
||||
|
||||
]]
|
||||
function Common.validate_argument_type(value, test_type, param_number, param_name)
|
||||
if not Common.test_type(value, test_type) then
|
||||
local function_name = debug.getinfo(2, 'n').name or '<anon>'
|
||||
local error_message
|
||||
if param_name then
|
||||
error_message = string.format('Bad argument #%d to %q; %q is of type %s expected %s', param_number, function_name, param_name, type(value), test_type)
|
||||
else
|
||||
error_message = string.format('Bad argument #%d to %q; argument is of type %s expected %s', param_number, function_name, type(value), test_type)
|
||||
end
|
||||
return error(error_message, 3)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--[[-- Raises an error when the value is the incorrect type, uses a consistent error message format
|
||||
@tparam any value the value that you want to test the type of
|
||||
@tparam string test_types the types that the value should be
|
||||
@tparam number param_number the number param it is
|
||||
@tparam[opt] string param_name the name of the param
|
||||
@treturn boolean true if no error was raised
|
||||
|
||||
@usage-- Output: "Bad argument #2 to "<anon>"; argument is of type number expected string or table"
|
||||
validate_argument_type(value, {'string', 'table'}, 2)
|
||||
|
||||
@usage-- Output: "Bad argument #2 to "<anon>"; "player" is of type number expected string or table"
|
||||
validate_argument_type(value, {'string', 'table'}, 2, 'player')
|
||||
|
||||
]]
|
||||
function Common.validate_argument_multi_type(value, test_types, param_number, param_name)
|
||||
if not Common.multi_type_check(value, test_types) then
|
||||
local function_name = debug.getinfo(2, 'n').name or '<anon>'
|
||||
local error_message
|
||||
if param_name then
|
||||
error_message = string.format('Bad argument #%2d to %q; %q is of type %s expected %s', param_number, function_name, param_name, type(value), table.concat(test_types, ' or '))
|
||||
else
|
||||
error_message = string.format('Bad argument #%2d to %q; argument is of type %s expected %s', param_number, function_name, type(value), table.concat(test_types, ' or '))
|
||||
end
|
||||
return error(error_message, 3)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Will raise an error if called during runtime
|
||||
-- @usage error_if_runtime()
|
||||
function Common.error_if_runtime()
|
||||
if _LIFECYCLE == 8 then
|
||||
local function_name = debug.getinfo(2, 'n').name or '<anon>'
|
||||
error(function_name..' can not be called during runtime', 3)
|
||||
end
|
||||
end
|
||||
|
||||
--- Will raise an error if the function is a closure
|
||||
-- @usage error_if_runetime_closure(func)
|
||||
function Common.error_if_runetime_closure(func)
|
||||
if _LIFECYCLE == 8 and Debug.is_closure(func) then
|
||||
local function_name = debug.getinfo(2, 'n').name or '<anon>'
|
||||
error(function_name..' can not be called during runtime with a closure', 3)
|
||||
end
|
||||
end
|
||||
|
||||
--- Value Returns.
|
||||
-- @section valueReturns
|
||||
|
||||
--[[-- Tests if a string contains a given substring.
|
||||
@tparam string s the string to check for the substring
|
||||
@tparam string contains the substring to test for
|
||||
@treturn boolean true if the substring was found in the string
|
||||
|
||||
@usage-- Test if a string contains a sub string
|
||||
local found = string_contains(str, 'foo')
|
||||
|
||||
]]
|
||||
function Common.string_contains(s, contains)
|
||||
return s and string.find(s, contains) ~= nil
|
||||
end
|
||||
|
||||
--[[-- Used to resolve a value that could also be a function returning that value
|
||||
@tparam any value the value which you want to test is not nil and if it is a function then call the function
|
||||
@treturn any the value given or returned by value if it is a function
|
||||
|
||||
@usage-- Default value handling
|
||||
-- if default value is not a function then it is returned
|
||||
-- if default value is a function then it is called with the first argument being self
|
||||
local value = Common.resolve_value(self.defaut_value, self)
|
||||
|
||||
]]
|
||||
function Common.resolve_value(value, ...)
|
||||
return value and type(value) == 'function' and value(...) or value
|
||||
end
|
||||
|
||||
--- Converts a varible into its boolean value, nil and false return false
|
||||
-- @treturn boolean the boolean form of the varible
|
||||
-- @usage local bool = cast_bool(var)
|
||||
function Common.cast_bool(var)
|
||||
return var and true or false
|
||||
end
|
||||
|
||||
--- Returns either the second or third argument based on the first argument
|
||||
-- @usage ternary(input_string == 'test', 'Input is test', 'Input is not test')
|
||||
function Common.ternary(c, t, f)
|
||||
return c and t or f
|
||||
end
|
||||
|
||||
--- Returns a string for a number with comma seperators
|
||||
-- @usage comma_value(input_number)
|
||||
function Common.comma_value(n) -- credit http://richard.warburton.it
|
||||
local left, num, right = string.match(n, '^([^%d]*%d)(%d*)(.-)$')
|
||||
return left .. (num:reverse():gsub('(%d%d%d)', '%1, '):reverse()) .. right
|
||||
end
|
||||
|
||||
--[[-- Sets a table element to value while also returning value.
|
||||
@tparam table tbl to change the element of
|
||||
@tparam string key the key to set the value of
|
||||
@tparam any value the value to set the key as
|
||||
@treturn any the value that was set
|
||||
|
||||
@usage-- Set and return value
|
||||
local value = set_and_return(players, player.name, player.online_time)
|
||||
|
||||
]]
|
||||
function Common.set_and_return(tbl, key, value)
|
||||
tbl[key] = value
|
||||
return value
|
||||
end
|
||||
|
||||
--[[-- Writes a table object to a file in json format
|
||||
@tparam string path the path of the file to write include / to use dir
|
||||
@tparam table tbl the table that will be converted to a json string and wrote to file
|
||||
|
||||
@usage-- Write a lua table as a json to script-outpt/dump
|
||||
write_json('dump', tbl)
|
||||
|
||||
]]
|
||||
function Common.write_json(path, tbl)
|
||||
game.write_file(path, game.table_to_json(tbl)..'\n', true, 0)
|
||||
end
|
||||
|
||||
--[[-- Calls a require that will not error if the file is not found
|
||||
@usage local file = opt_require('file.not.present') -- will not cause any error
|
||||
@tparam string path the path that you want to require
|
||||
@return the returns from that file or nil, error if not loaded
|
||||
|
||||
@usage-- Require a file without causing errors, for when a file might not exist
|
||||
local Module = opt_require 'expcore.common'
|
||||
|
||||
]]
|
||||
function Common.opt_require(path)
|
||||
local success, rtn = pcall(require, path)
|
||||
if success then return rtn
|
||||
else return nil, rtn end
|
||||
end
|
||||
|
||||
--[[-- Returns a desync safe file path for the current file
|
||||
@tparam[opt=0] number offset the offset in the stack to get, 0 is current file
|
||||
@treturn string the file path
|
||||
|
||||
@usage-- Get the current file path
|
||||
local file_path = get_file_path()
|
||||
|
||||
]]
|
||||
function Common.get_file_path(offset)
|
||||
offset = offset or 0
|
||||
return debug.getinfo(offset+2, 'S').source:match('^.+/currently%-playing/(.+)$'):sub(1, -5)
|
||||
end
|
||||
|
||||
--[[-- Converts a table to an enum
|
||||
@tparam table tbl table the that will be converted
|
||||
@treturn table the new table that acts like an enum
|
||||
|
||||
@usage-- Make an enum
|
||||
local colors = enum{
|
||||
'red',
|
||||
'green',
|
||||
'blue'
|
||||
}
|
||||
|
||||
]]
|
||||
function Common.enum(tbl)
|
||||
local rtn = {}
|
||||
for k, v in pairs(tbl) do
|
||||
if type(k) ~= 'number' then
|
||||
rtn[v]=k
|
||||
end
|
||||
end
|
||||
for k, v in pairs(tbl) do
|
||||
if type(k) == 'number' then
|
||||
table.insert(rtn, v)
|
||||
end
|
||||
end
|
||||
for k, v in pairs(rtn) do
|
||||
rtn[v]=k
|
||||
end
|
||||
return rtn
|
||||
end
|
||||
|
||||
--[[-- Returns the closest match to the input
|
||||
@tparam table options table a of options for the auto complete
|
||||
@tparam string input string the input that will be completed
|
||||
@tparam[opt=false] boolean use_key when true the keys of options will be used as the options
|
||||
@tparam[opt=false] boolean rtn_key when true the the key will be returned rather than the value
|
||||
@return the list item found that matches the input
|
||||
|
||||
@usage-- Get the element that includes "foo"
|
||||
local value = auto_complete(tbl, "foo")
|
||||
|
||||
@usage-- Get the element with a key that includes "foo"
|
||||
local value = auto_complete(tbl, "foo", true)
|
||||
|
||||
@usage-- Get the key with that includes "foo"
|
||||
local key = auto_complete(tbl, "foo", true, true)
|
||||
|
||||
]]
|
||||
function Common.auto_complete(options, input, use_key, rtn_key)
|
||||
if type(input) ~= 'string' then return end
|
||||
input = input:lower()
|
||||
for key, value in pairs(options) do
|
||||
local check = use_key and key or value
|
||||
if Common.string_contains(string.lower(check), input) then
|
||||
return rtn_key and key or value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Formatting.
|
||||
-- @section formatting
|
||||
|
||||
--[[-- Returns a valid string with the name of the actor of a command.
|
||||
@tparam string player_name the name of the player to use rather than server, used only if game.player is nil
|
||||
@treturn string the name of the current actor
|
||||
|
||||
@usage-- Get the current actor
|
||||
local player_name = get_actor()
|
||||
|
||||
]]
|
||||
function Common.get_actor(player_name)
|
||||
return game.player and game.player.name or player_name or '<server>'
|
||||
end
|
||||
|
||||
--[[-- Returns a message with valid chat tags to change its colour
|
||||
@tparam string message the message that will be in the output
|
||||
@tparam table color a color which contains r, g, b as its keys
|
||||
@treturn string the message with the color tags included
|
||||
|
||||
@usage-- Use factorio tags to color a chat message
|
||||
local message = format_chat_colour('Hello, World!', { r=355, g=100, b=100 })
|
||||
|
||||
]]
|
||||
function Common.format_chat_colour(message, color)
|
||||
color = color or Colours.white
|
||||
local color_tag = '[color='..math.round(color.r, 3)..', '..math.round(color.g, 3)..', '..math.round(color.b, 3)..']'
|
||||
return string.format('%s%s[/color]', color_tag, message)
|
||||
end
|
||||
|
||||
--[[-- Returns a message with valid chat tags to change its colour, using localization
|
||||
@tparam ?string|table message the message that will be in the output
|
||||
@tparam table color a color which contains r, g, b as its keys
|
||||
@treturn table the message with the color tags included
|
||||
|
||||
@usage-- Use factorio tags and locale strings to color a chat message
|
||||
local message = format_chat_colour_localized('Hello, World!', { r=355, g=100, b=100 })
|
||||
|
||||
]]
|
||||
function Common.format_chat_colour_localized(message, color)
|
||||
color = color or Colours.white
|
||||
color = math.round(color.r, 3)..', '..math.round(color.g, 3)..', '..math.round(color.b, 3)
|
||||
return {'color-tag', color, message}
|
||||
end
|
||||
|
||||
--[[-- Returns the players name in the players color
|
||||
@tparam LuaPlayer player the player to use the name and color of
|
||||
@tparam[opt=false] boolean raw_string when true a string is returned rather than a localized string
|
||||
@treturn table the players name with tags for the players color
|
||||
|
||||
@usage-- Format a players name using the players color as a string
|
||||
local message = format_chat_player_name(game.player, true)
|
||||
|
||||
]]
|
||||
function Common.format_chat_player_name(player, raw_string)
|
||||
player = Game.get_player_from_any(player)
|
||||
local player_name = player and player.name or '<Server>'
|
||||
local player_chat_colour = player and player.chat_color or Colours.white
|
||||
if raw_string then
|
||||
return Common.format_chat_colour(player_name, player_chat_colour)
|
||||
else
|
||||
return Common.format_chat_colour_localized(player_name, player_chat_colour)
|
||||
end
|
||||
end
|
||||
|
||||
--[[-- Will return a value of any type to the player/server console, allows colour for in-game players
|
||||
@tparam any value a value of any type that will be returned to the player or console
|
||||
@tparam[opt=defines.colour.white] ?defines.color|string colour the colour of the text for the player, ignored when printing to console
|
||||
@tparam[opt=game.player] LuaPlayer player the player that return will go to, if no game.player then returns to server
|
||||
|
||||
@usage-- Return a value to the current actor, rcon included
|
||||
player_return('Hello, World!')
|
||||
|
||||
@usage-- Return a value to the current actor, with color
|
||||
player_return('Hello, World!', 'green')
|
||||
|
||||
@usage-- Return to a player other than the current
|
||||
player_return('Hello, World!', nil, player)
|
||||
|
||||
]]
|
||||
function Common.player_return(value, colour, player)
|
||||
colour = Common.type_check(colour, 'table') and colour or Colours[colour] ~= Colours.white and Colours[colour] or Colours.white
|
||||
player = player or game.player
|
||||
-- converts the value to a string
|
||||
local returnAsString
|
||||
if Common.type_check(value, 'table') or type(value) == 'userdata' then
|
||||
if Common.type_check(value.__self, 'userdata') or type(value) == 'userdata' then
|
||||
-- value is userdata
|
||||
returnAsString = 'Cant Display Userdata'
|
||||
elseif Common.type_check(value[1], 'string') and string.find(value[1], '.+[.].+') and not string.find(value[1], '%s') then
|
||||
-- value is a locale string
|
||||
returnAsString = value
|
||||
elseif getmetatable(value) ~= nil and not tostring(value):find('table: 0x') then
|
||||
-- value has a tostring meta method
|
||||
returnAsString = tostring(value)
|
||||
else
|
||||
-- value is a table
|
||||
returnAsString = table.inspect(value, {depth=5, indent=' ', newline='\n'})
|
||||
end
|
||||
elseif Common.type_check(value, 'function') then
|
||||
-- value is a function
|
||||
returnAsString = 'Cant Display Functions'
|
||||
else returnAsString = tostring(value) end
|
||||
-- returns to the player or the server
|
||||
if player then
|
||||
-- allows any valid player identifier to be used
|
||||
player = Game.get_player_from_any(player)
|
||||
if not player then error('Invalid Player given to player_return', 2) end
|
||||
-- plays a nice sound that is different to normal message sound
|
||||
player.play_sound{path='utility/scenario_message'}
|
||||
player.print(returnAsString, colour)
|
||||
else rcon.print(returnAsString) end
|
||||
end
|
||||
|
||||
--[[-- Formats tick into a clean format, denominations from highest to lowest
|
||||
-- time will use : separates
|
||||
-- when a denomination is false it will overflow into the next one
|
||||
@tparam number ticks the number of ticks that represents a time
|
||||
@tparam table options table a of options to use for the format
|
||||
@treturn string a locale string that can be used
|
||||
|
||||
@usage-- Output: "0h 5m"
|
||||
local time = format_time(18000, { hours=true, minutes=true, string=true })
|
||||
|
||||
@usage-- Output: "0 hours and 5 minutes"
|
||||
local time = format_time(18000, { hours=true, minutes=true, string=true, long=true })
|
||||
|
||||
@usage-- Output: "00:05:00"
|
||||
local time = format_time(18000, { hours=true, minutes=true, seconds=true, string=true })
|
||||
|
||||
@usage-- Output: "--:--:--"
|
||||
local time = format_time(18000, { hours=true, minutes=true, seconds=true, string=true, null=true })
|
||||
|
||||
]]
|
||||
function Common.format_time(ticks, options)
|
||||
-- Sets up the options
|
||||
options = options or {
|
||||
days=false,
|
||||
hours=true,
|
||||
minutes=true,
|
||||
seconds=false,
|
||||
long=false,
|
||||
time=false,
|
||||
string=false,
|
||||
null=false
|
||||
}
|
||||
-- Basic numbers that are used in calculations
|
||||
local max_days, max_hours, max_minutes, max_seconds = ticks/5184000, ticks/216000, ticks/3600, ticks/60
|
||||
local days, hours = max_days, max_hours-math.floor(max_days)*24
|
||||
local minutes, seconds = max_minutes-math.floor(max_hours)*60, max_seconds-math.floor(max_minutes)*60
|
||||
-- Handles overflow of disabled denominations
|
||||
local rtn_days, rtn_hours, rtn_minutes, rtn_seconds = math.floor(days), math.floor(hours), math.floor(minutes), math.floor(seconds)
|
||||
if not options.days then
|
||||
rtn_hours = rtn_hours + rtn_days*24
|
||||
end
|
||||
if not options.hours then
|
||||
rtn_minutes = rtn_minutes + rtn_hours*60
|
||||
end
|
||||
if not options.minutes then
|
||||
rtn_seconds = rtn_seconds + rtn_minutes*60
|
||||
end
|
||||
-- Creates the null time format, does not work with long
|
||||
if options.null and not options.long then
|
||||
rtn_days='--'
|
||||
rtn_hours='--'
|
||||
rtn_minutes='--'
|
||||
rtn_seconds='--'
|
||||
end
|
||||
-- Format options
|
||||
local suffix = 'time-symbol-'
|
||||
local suffix_2 = '-short'
|
||||
if options.long then
|
||||
suffix = ''
|
||||
suffix_2 = ''
|
||||
end
|
||||
local div = options.string and ' ' or 'time-format.simple-format-tagged'
|
||||
if options.time then
|
||||
div = options.string and ':' or 'time-format.simple-format-div'
|
||||
suffix = false
|
||||
end
|
||||
-- Adds formatting
|
||||
if suffix ~= false then
|
||||
if options.string then
|
||||
-- format it as a string
|
||||
local long = suffix == ''
|
||||
rtn_days = long and rtn_days..' days' or rtn_days..'d'
|
||||
rtn_hours = long and rtn_hours..' hours' or rtn_hours..'h'
|
||||
rtn_minutes = long and rtn_minutes..' minutes' or rtn_minutes..'m'
|
||||
rtn_seconds = long and rtn_seconds..' seconds' or rtn_seconds..'s'
|
||||
else
|
||||
rtn_days = {suffix..'days'..suffix_2, rtn_days}
|
||||
rtn_hours = {suffix..'hours'..suffix_2, rtn_hours}
|
||||
rtn_minutes = {suffix..'minutes'..suffix_2, rtn_minutes}
|
||||
rtn_seconds = {suffix..'seconds'..suffix_2, rtn_seconds}
|
||||
end
|
||||
elseif not options.null then
|
||||
-- weather string or not it has same format
|
||||
rtn_days = string.format('%02d', rtn_days)
|
||||
rtn_hours = string.format('%02d', rtn_hours)
|
||||
rtn_minutes = string.format('%02d', rtn_minutes)
|
||||
rtn_seconds = string.format('%02d', rtn_seconds)
|
||||
end
|
||||
-- The final return is construed
|
||||
local rtn
|
||||
local append = function(dom, value)
|
||||
if dom and options.string then
|
||||
rtn = rtn and rtn..div..value or value
|
||||
elseif dom then
|
||||
rtn = rtn and {div, rtn, value} or value
|
||||
end
|
||||
end
|
||||
append(options.days, rtn_days)
|
||||
append(options.hours, rtn_hours)
|
||||
append(options.minutes, rtn_minutes)
|
||||
append(options.seconds, rtn_seconds)
|
||||
return rtn
|
||||
end
|
||||
|
||||
--- Factorio.
|
||||
-- @section factorio
|
||||
|
||||
--[[-- Copies items to the position and stores them in the closest entity of the type given
|
||||
-- Copies the items by prototype name, but keeps them in the original inventory
|
||||
@tparam table items items which are to be added to the chests, an array of LuaItemStack
|
||||
@tparam[opt=navies] LuaSurface surface the surface that the items will be copied to
|
||||
@tparam[opt={0, 0}] table position the position that the items will be copied to {x=100, y=100}
|
||||
@tparam[opt=32] number radius the radius in which the items are allowed to be placed
|
||||
@tparam[opt=iron-chest] string chest_type the chest type that the items should be copied into
|
||||
@treturn LuaEntity the last chest that had items inserted into it
|
||||
|
||||
@usage-- Copy all the items in a players inventory and place them in chests at {0, 0}
|
||||
copy_items_stack(game.player.get_main_inventory().get_contents())
|
||||
|
||||
]]
|
||||
function Common.copy_items_stack(items, surface, position, radius, chest_type)
|
||||
chest_type = chest_type or 'iron-chest'
|
||||
surface = surface or game.surfaces[1]
|
||||
if position and type(position) ~= 'table' then return end
|
||||
if type(items) ~= 'table' then return end
|
||||
-- Finds all entities of the given type
|
||||
local p = position or {x=0, y=0}
|
||||
local r = radius or 32
|
||||
local entities = surface.find_entities_filtered{area={{p.x-r, p.y-r}, {p.x+r, p.y+r}}, name=chest_type} or {}
|
||||
local count = #entities
|
||||
local current = 1
|
||||
-- Makes a new empty chest when it is needed
|
||||
local function make_new_chest()
|
||||
local pos = surface.find_non_colliding_position(chest_type, position, 32, 1)
|
||||
local chest = surface.create_entity{name=chest_type, position=pos, force='neutral'}
|
||||
table.insert(entities, chest)
|
||||
count = count + 1
|
||||
return chest
|
||||
end
|
||||
-- Function used to round robin the items into all chests
|
||||
local function next_chest(item)
|
||||
local chest = entities[current]
|
||||
if count == 0 then return make_new_chest() end
|
||||
if chest.get_inventory(defines.inventory.chest).can_insert(item) then
|
||||
-- If the item can be inserted then the chest is returned
|
||||
current = current+1
|
||||
if current > count then current = 1 end
|
||||
return chest
|
||||
else
|
||||
-- Other wise it is removed from the list
|
||||
table.remove(entities, current)
|
||||
count = count - 1
|
||||
end
|
||||
end
|
||||
-- Inserts the items into the chests
|
||||
local last_chest
|
||||
for i=1,#items do
|
||||
local item = items[i]
|
||||
if item.valid_for_read then
|
||||
local chest = next_chest(item)
|
||||
if not chest or not chest.valid then return error(string.format('Cant move item %s to %s{%s, %s} no valid chest in radius', item.name, surface.name, p.x, p.y)) end
|
||||
chest.insert(item)
|
||||
last_chest = chest
|
||||
end
|
||||
end
|
||||
return last_chest
|
||||
end
|
||||
|
||||
--[[-- Moves items to the position and stores them in the closest entity of the type given
|
||||
-- Differs from move_items by accepting a table of LuaItemStack and transferring them into the inventory - not copying
|
||||
@tparam table items items which are to be added to the chests, an array of LuaItemStack
|
||||
@tparam[opt=navies] LuaSurface surface the surface that the items will be moved to
|
||||
@tparam[opt={0, 0}] table position the position that the items will be moved to {x=100, y=100}
|
||||
@tparam[opt=32] number radius the radius in which the items are allowed to be placed
|
||||
@tparam[opt=iron-chest] string chest_type the chest type that the items should be moved into
|
||||
@treturn LuaEntity the last chest that had items inserted into it
|
||||
|
||||
@usage-- Copy all the items in a players inventory and place them in chests at {0, 0}
|
||||
move_items_stack(game.player.get_main_inventory())
|
||||
|
||||
]]
|
||||
function Common.move_items_stack(items, surface, position, radius, chest_type)
|
||||
chest_type = chest_type or 'steel-chest'
|
||||
surface = surface or game.surfaces[1]
|
||||
|
||||
if position and type(position) ~= 'table' then
|
||||
return
|
||||
end
|
||||
|
||||
if type(items) ~= 'table' then
|
||||
return
|
||||
end
|
||||
|
||||
-- Finds all entities of the given type
|
||||
local p = position or {x=0, y=0}
|
||||
local r = radius or 32
|
||||
local entities = surface.find_entities_filtered{area={{p.x - r, p.y - r}, {p.x + r, p.y + r}}, name={chest_type, 'iron-chest'}} or {}
|
||||
local count = #entities
|
||||
local current = 0
|
||||
local last_entity = nil
|
||||
|
||||
-- ipairs does not work on LuaInventory
|
||||
for i = 1, #items do
|
||||
local item = items[i]
|
||||
if item.valid_for_read then
|
||||
local inserted = false
|
||||
|
||||
-- Attempt to insert the items
|
||||
for j = 1, count do
|
||||
local entity = entities[((current + j - 1) % count) + 1]
|
||||
|
||||
if entity.can_insert(item) then
|
||||
last_entity = entity
|
||||
current = current + 1
|
||||
entity.insert(item)
|
||||
inserted = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- If it was not inserted then a new entity is needed
|
||||
if not inserted then
|
||||
--[[
|
||||
if not options.allow_creation then
|
||||
error('Unable to insert items into a valid entity, consider enabling allow_creation')
|
||||
end
|
||||
|
||||
if options.name == nil then
|
||||
error('Name must be provided to allow creation of new entities')
|
||||
end
|
||||
|
||||
if options.position then
|
||||
pos = surface.find_non_colliding_position(chest_type, p, r, 1, true)
|
||||
|
||||
elseif options.area then
|
||||
pos = surface.find_non_colliding_position_in_box(chest_type, options.area, 1, true)
|
||||
|
||||
else
|
||||
pos = surface.find_non_colliding_position(chest_type, {0,0}, 0, 1, true)
|
||||
end
|
||||
]]
|
||||
|
||||
local pos = surface.find_non_colliding_position(chest_type, p, r, 1, true)
|
||||
last_entity = surface.create_entity{name=chest_type, position=pos, force='neutral'}
|
||||
|
||||
count = count + 1
|
||||
entities[count] = last_entity
|
||||
|
||||
last_entity.insert(item)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
-- Makes a new empty chest when it is needed
|
||||
local function make_new_chest()
|
||||
local pos = surface.find_non_colliding_position(chest_type, position, 32, 1)
|
||||
local chest = surface.create_entity{name=chest_type, position=pos, force='neutral'}
|
||||
table.insert(entities, chest)
|
||||
count = count + 1
|
||||
|
||||
return chest
|
||||
end
|
||||
|
||||
-- Function used to round robin the items into all chests
|
||||
local function next_chest(item)
|
||||
local chest = entities[current]
|
||||
|
||||
if count == 0 then
|
||||
return make_new_chest()
|
||||
end
|
||||
|
||||
if chest.get_inventory(defines.inventory.chest).can_insert(item) then
|
||||
-- If the item can be inserted then the chest is returned
|
||||
current = current + 1
|
||||
if current > count then
|
||||
current = 1
|
||||
end
|
||||
|
||||
return chest
|
||||
|
||||
else
|
||||
-- Other wise it is removed from the list
|
||||
table.remove(entities, current)
|
||||
count = count - 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Inserts the items into the chests
|
||||
local last_chest
|
||||
|
||||
for i=1,#items do
|
||||
local item = items[i]
|
||||
|
||||
if item.valid_for_read then
|
||||
local chest = next_chest(item)
|
||||
|
||||
if not chest or not chest.valid then
|
||||
return error(string.format('Cant move item %s to %s{%s, %s} no valid chest in radius', item.name, surface.name, p.x, p.y))
|
||||
end
|
||||
|
||||
local empty_stack = chest.get_inventory(defines.inventory.chest).find_empty_stack(item.name)
|
||||
|
||||
if not empty_stack then
|
||||
return error(string.format('Cant move item %s to %s{%s, %s} no valid chest in radius', item.name, surface.name, p.x, p.y))
|
||||
end
|
||||
|
||||
empty_stack.transfer_stack(item)
|
||||
last_chest = chest
|
||||
end
|
||||
end
|
||||
return last_chest
|
||||
]]
|
||||
|
||||
return last_entity
|
||||
end
|
||||
|
||||
--[[-- Prints a colored value on a location, color is based on the value.
|
||||
nb: src is below but the gradent has been edited
|
||||
https://github.com/Refactorio/RedMew/blob/9184b2940f311d8c9c891e83429fc57ec7e0c4a2/map_gen/maps/diggy/debug.lua#L31
|
||||
@tparam number value the value to show must be between -1 and 1, scale can be used to achive this
|
||||
@tparam LuaSurface surface the surface to palce the value on
|
||||
@tparam table position {x, y} the possition to palce the value at
|
||||
@tparam[opt=1] number scale how much to scale the colours by
|
||||
@tparam[opt=0] number offset the offset in the +x +y direction
|
||||
@tparam[opt=false] boolean immutable if immutable, only set, never do a surface lookup, values never change
|
||||
|
||||
@usage-- Place a 0 at {0, 0}
|
||||
print_grid_value(0, game.player.surface, { x=0, y=0 })
|
||||
|
||||
]]
|
||||
function Common.print_grid_value(value, surface, position, scale, offset, immutable)
|
||||
local is_string = type(value) == 'string'
|
||||
local color = Colours.white
|
||||
local text = value
|
||||
|
||||
if type(immutable) ~= 'boolean' then
|
||||
immutable = false
|
||||
end
|
||||
|
||||
if not is_string then
|
||||
scale = scale or 1
|
||||
offset = offset or 0
|
||||
position = {x = position.x + offset, y = position.y + offset}
|
||||
local r = math.clamp(-value/scale, 0, 1)
|
||||
local g = math.clamp(1-math.abs(value)/scale, 0, 1)
|
||||
local b = math.clamp(value/scale, 0, 1)
|
||||
|
||||
color = { r = r, g = g, b = b}
|
||||
|
||||
-- round at precision of 2
|
||||
text = math.floor(100 * value) * 0.01
|
||||
|
||||
if (0 == text) then
|
||||
text = '0.00'
|
||||
end
|
||||
end
|
||||
|
||||
if not immutable then
|
||||
local text_entity = surface.find_entity('flying-text', position)
|
||||
|
||||
if text_entity then
|
||||
text_entity.text = text
|
||||
text_entity.color = color
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
surface.create_entity{
|
||||
name = 'flying-text',
|
||||
color = color,
|
||||
text = text,
|
||||
position = position
|
||||
}.active = false
|
||||
end
|
||||
|
||||
--[[-- Clears all flying text entities on a surface
|
||||
@tparam LuaSurface surface the surface to clear
|
||||
|
||||
@usage-- Remove all flying text on the surface
|
||||
clear_flying_text(game.player.surface)
|
||||
|
||||
]]
|
||||
function Common.clear_flying_text(surface)
|
||||
local entities = surface.find_entities_filtered{name ='flying-text'}
|
||||
for _, entity in pairs(entities) do
|
||||
if entity and entity.valid then
|
||||
entity.destroy()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return Common
|
||||
864
exp_legacy/module/expcore/datastore.lua
Normal file
864
exp_legacy/module/expcore/datastore.lua
Normal file
@@ -0,0 +1,864 @@
|
||||
--[[-- Core Module - Datastore
|
||||
- A module used to store data in the global table with the option to have it sync to an external source.
|
||||
@core Datastore
|
||||
@alias DatastoreManager
|
||||
|
||||
@usage-- Types of Datastore
|
||||
-- This datastore will not save data externally and can be used to watch for updates on values within it
|
||||
-- A common use might be to store data for a gui and only update the gui when a value changes
|
||||
local LocalDatastore = Datastore.connect('LocalDatastore')
|
||||
|
||||
-- This datastore will allow you to use the save and request method, this allows you to have persistent data
|
||||
-- Should be used over auto save as it creates less save requests, but this means you need to tell the data to be saved
|
||||
-- We use this type for player data as we know the data only needs to be saved when the player leaves
|
||||
local PersistentDatastore = Datastore.connect('PersistentDatastore', true) -- save_to_disk
|
||||
|
||||
-- This datastore is the same as above but the save method will be called automatically when ever you change a value
|
||||
-- An auto save datastore should be used if the data does not change often, this can be global settings and things of that sort
|
||||
-- If it is at all possible to setup events to unload and/or save the data then this is preferable
|
||||
local AutosaveDatastore = Datastore.connect('AutosaveDatastore', true, true) -- save_to_disk, auto_save
|
||||
|
||||
-- Finally you can have a datastore that propagates its changes to all other connected servers, this means request does not need to be used
|
||||
-- This should be used when you might have data conflicts while saving, this is done by pushing the saved value to all active servers
|
||||
-- The request method has little use after server start as any external changes to the value will be pushed automatically
|
||||
-- Auto save can also be used with this type and you should follow the same guidelines above for when this should be avoided
|
||||
local PropagateDatastore = Datastore.connect('PropagateDatastore', true, false, true) -- save_to_disk, propagate_changes
|
||||
|
||||
@usage-- Using Datastores Locally
|
||||
-- Once you have your datastore connection setup, any further requests with connect will return the same datastore
|
||||
-- This is important to know because the settings passed as parameters you have an effect when it is first created
|
||||
|
||||
-- One useful thing that you might want to set up before runtime is a serializer, this will convert non string keys into strings
|
||||
-- This serializer will allow use to pass a player object and still have it serialized to the players name
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:set_serializer(function(rawKey)
|
||||
return rawKey.name
|
||||
end)
|
||||
|
||||
-- If we want to get data from the datastore we can use get or get_all
|
||||
local value = ExampleData:get(player, defaultValue)
|
||||
local values = ExampleData:get_all()
|
||||
|
||||
-- If we want to set data then we can use set, increment, update, or update_all
|
||||
ExampleData:set(player, 10)
|
||||
ExampleData:increment(player)
|
||||
ExampleData:update(player, function(player_name, value)
|
||||
return value * 2
|
||||
end)
|
||||
ExampleData:update_all(function(player_name, value)
|
||||
return value * 2
|
||||
end)
|
||||
|
||||
-- If we want to remove data then we use remove
|
||||
ExampleData:remove(player)
|
||||
|
||||
-- We can also listen for updates to a value done by any of the above methods with on_update
|
||||
ExampleData:on_update(function(player_name, value)
|
||||
game.print(player_name..' has had their example data updated to '..tostring(value))
|
||||
end)
|
||||
|
||||
@usage-- Using Datastore Externally
|
||||
-- If save_to_disk is used then this opens up the option for persistent data which you can request, save, and remove
|
||||
-- All of the local methods are still usable put now there is the option for extra events
|
||||
-- In order for this to work there must be an external script to read datastore.pipe and inject with Datastore.ingest
|
||||
|
||||
-- To request data you would use request and the on_load event, this event can be used to modify data before it is used
|
||||
ExampleData:request(player)
|
||||
ExampleData:on_load(function(player_name, value)
|
||||
game.print('Loaded example data for '..player_name)
|
||||
-- A value can be returned here to overwrite the received value
|
||||
end)
|
||||
|
||||
-- To save data you would use save and the on_save event, this event can be used to modify data before it is saved
|
||||
ExampleData:save(player)
|
||||
ExampleData:on_save(function(player_name, value)
|
||||
game.print('Saved example data for '..player_name)
|
||||
-- A value can be returned here to overwrite the value which is saved
|
||||
end)
|
||||
|
||||
-- To remove data locally but not externally, like if a player logs off, you would use unload and on_unload
|
||||
ExampleData:unload(player)
|
||||
ExampleData:on_unload(function(player_name, value)
|
||||
game.print('Unloaded example data for '..player_name)
|
||||
-- Any return is ignored, this is event is for cleaning up other data
|
||||
end)
|
||||
|
||||
@usage-- Using Datastore Messaging
|
||||
-- The message action can be used regardless of save_to_disk being set as no data is saved, but an external script is still required
|
||||
-- These messages can be used to send data to other servers which doesnt need to be saved such as shouts or commands
|
||||
-- Using messages is quite simple only using message and on_message
|
||||
ExampleData:message(key, message)
|
||||
ExampleData:on_message(function(key, message)
|
||||
game.print('Received message '..message)
|
||||
end)
|
||||
|
||||
@usage-- Combined Datastores
|
||||
-- A combined datastore is a datastore which stores its data inside of another datastore
|
||||
-- This means that the data is stored more efficiently in the external database and less requests need to be made
|
||||
-- To understand how combined datastores work think of each key in the parent as a table where the sub datastore is a key in that table
|
||||
-- Player data is the most used version of the combined datastore, below is how the player data module is setup
|
||||
local PlayerData = Datastore.connect('PlayerData', true) -- saveToDisk
|
||||
PlayerData:set_serializer(Datastore.name_serializer) -- use player name as key
|
||||
PlayerData:combine('Statistics')
|
||||
PlayerData:combine('Settings')
|
||||
PlayerData:combine('Required')
|
||||
|
||||
-- You can then further combine datastores to any depth, below we add some possible settings and statistics that we might use
|
||||
-- Although we dont in this example, each of these functions returns the datastore object which you should use as a local value
|
||||
PlayerData.Settings:combine('Color')
|
||||
PlayerData.Settings:combine('Quickbar')
|
||||
PlayerData.Settings:combine('JoinMessage')
|
||||
PlayerData.Statistics:combine('Playtime')
|
||||
PlayerData.Statistics:combine('JoinCount')
|
||||
|
||||
-- Because sub datastore work just like a normal datastore you dont need any special code, using get and set will still return as if it wasnt a sub datastore
|
||||
-- Things like the serializer and the datastore settings are always the same as the parent so you dont need to worry about setting up the serializer each time
|
||||
-- And because save, request, and unload methods all point to the root datastore you are able to request and save your data as normal
|
||||
|
||||
-- If you used get_all on PlayerData this is what you would get:
|
||||
{
|
||||
Cooldude2606 = {
|
||||
Settings = {
|
||||
Color = 'ColorValue',
|
||||
Quickbar = 'QuickbarValue',
|
||||
JoinMessage = 'JoinMessageValue'
|
||||
},
|
||||
Statistics = {
|
||||
Playtime = 'PlaytimeValue',
|
||||
JoinCount = 'JoinCountValue'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-- If you used get_all on PlayerData.Settings this is what you would get:
|
||||
{
|
||||
Cooldude2606 = {
|
||||
Color = 'ColorValue',
|
||||
Quickbar = 'QuickbarValue',
|
||||
JoinMessage = 'JoinMessageValue'
|
||||
}
|
||||
}
|
||||
|
||||
-- If you used get_all on PlayerData.Settings.Color this is what you would get:
|
||||
{
|
||||
Cooldude2606 = 'ColorValue'
|
||||
}
|
||||
|
||||
]]
|
||||
|
||||
local Event = require 'utils.event' --- @dep utils.event
|
||||
|
||||
local DatastoreManager = {}
|
||||
local Datastores = {}
|
||||
local Datastore = {}
|
||||
local Data = {}
|
||||
local copy = table.deep_copy
|
||||
local trace = debug.traceback
|
||||
|
||||
--- Save datastores in the global table
|
||||
global.datastores = Data
|
||||
Event.on_load(function()
|
||||
Data = global.datastores
|
||||
for datastoreName, datastore in pairs(Datastores) do
|
||||
datastore.data = Data[datastoreName]
|
||||
end
|
||||
end)
|
||||
|
||||
----- Datastore Manager
|
||||
-- @section datastoreManager
|
||||
|
||||
--- Metatable used on datastores
|
||||
DatastoreManager.metatable = {
|
||||
__index = function(self, key) return rawget(self.children, key) or rawget(Datastore, key) end,
|
||||
__newidnex = function(_, _, _) error('Datastore can not be modified', 2) end,
|
||||
__call = function(self, ...) return self:get(...) end
|
||||
}
|
||||
|
||||
--[[-- Make a new datastore connection, if a connection already exists then it is returned
|
||||
@tparam string datastoreName The name that you want the new datastore to have, this can not have any whitespace
|
||||
@tparam[opt=false] boolean saveToDisk When set to true, using the save method with write the data to datastore.pipe
|
||||
@tparam[opt=false] boolean autoSave When set to true, using any method which modifies data will cause the data to be saved
|
||||
@tparam[opt=false] boolean propagateChanges When set to true, using the save method will send the data to all other connected servers
|
||||
@treturn table The new datastore connection that can be used to access and modify data in the datastore
|
||||
|
||||
@usage-- Connecting to the test datastore which will allow saving to disk
|
||||
local ExampleData = Datastore.connect('ExampleData', true) -- saveToDisk
|
||||
|
||||
]]
|
||||
function DatastoreManager.connect(datastoreName, saveToDisk, autoSave, propagateChanges)
|
||||
if Datastores[datastoreName] then return Datastores[datastoreName] end
|
||||
if _LIFECYCLE ~= _STAGE.control then
|
||||
-- Only allow this function to be called during the control stage
|
||||
error('New datastore connection can not be created during runtime', 2)
|
||||
end
|
||||
|
||||
local new_datastore = {
|
||||
name = datastoreName,
|
||||
value_name = datastoreName,
|
||||
auto_save = autoSave or false,
|
||||
save_to_disk = saveToDisk or false,
|
||||
propagate_changes = propagateChanges or false,
|
||||
serializer = false,
|
||||
parent = false,
|
||||
children = {},
|
||||
metadata = {},
|
||||
events = {},
|
||||
data = {}
|
||||
}
|
||||
|
||||
Data[datastoreName] = new_datastore.data
|
||||
Datastores[datastoreName] = new_datastore
|
||||
return setmetatable(new_datastore, DatastoreManager.metatable)
|
||||
end
|
||||
|
||||
--[[-- Make a new datastore that stores its data inside of another one
|
||||
@tparam string datastoreName The name of the datastore that will contain the data for the new datastore
|
||||
@tparam string subDatastoreName The name of the new datastore, this name will also be used as the key inside the parent datastore
|
||||
@treturn table The new datastore connection that can be used to access and modify data in the datastore
|
||||
|
||||
@usage-- Setting up a datastore which stores its data inside of another datastore
|
||||
local BarData = Datastore.combine('ExampleData', 'Bar')
|
||||
|
||||
]]
|
||||
function DatastoreManager.combine(datastoreName, subDatastoreName)
|
||||
local datastore = assert(Datastores[datastoreName], 'Datastore not found '..tostring(datastoreName))
|
||||
return datastore:combine(subDatastoreName)
|
||||
end
|
||||
|
||||
--[[-- Ingest the result from a request, this is used through a rcon interface to sync data
|
||||
@tparam string action The action that should be done, can be: remove, message, propagate, or request
|
||||
@tparam string datastoreName The name of the datastore that should have the action done to it
|
||||
@tparam string key The key of that datastore that is having the action done to it
|
||||
@tparam string valueJson The json string for the value being ingested, remove does not require a value
|
||||
|
||||
@usage-- Replying to a data request
|
||||
Datastore.ingest('request', 'ExampleData', 'TestKey', 'Foo')
|
||||
|
||||
]]
|
||||
function DatastoreManager.ingest(action, datastoreName, key, valueJson)
|
||||
local datastore = assert(Datastores[datastoreName], 'Datastore ingest error, Datastore not found '..tostring(datastoreName))
|
||||
assert(type(action) == 'string', 'Datastore ingest error, Action is not a string got: '..type(action))
|
||||
assert(type(key) == 'string', 'Datastore ingest error, Key is not a string got: '..type(key))
|
||||
|
||||
if action == 'remove' then
|
||||
datastore:raw_set(key)
|
||||
|
||||
elseif action == 'message' then
|
||||
local success, value = pcall(game.json_to_table, valueJson)
|
||||
if not success or value == nil then value = tonumber(valueJson) or valueJson end
|
||||
datastore:raise_event('on_message', key, value)
|
||||
|
||||
elseif action == 'propagate' or action == 'request' then
|
||||
local success, value = pcall(game.json_to_table, valueJson)
|
||||
if not success or value == nil then value = tonumber(valueJson) or valueJson end
|
||||
local old_value = datastore:raw_get(key)
|
||||
value = datastore:raise_event('on_load', key, value, old_value)
|
||||
datastore:set(key, value)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--[[-- Debug, Use to get all datastores, or return debug info on a datastore
|
||||
@tparam[opt] string datastoreName The name of the datastore to get the debug info of
|
||||
|
||||
@usage-- Get all the datastores
|
||||
local datastores = Datastore.debug()
|
||||
|
||||
@usage-- Getting the debug info for a datastore
|
||||
local debug_info = Datastore.debug('ExampleData')
|
||||
|
||||
]]
|
||||
function DatastoreManager.debug(datastoreName)
|
||||
if not datastoreName then return Datastores end
|
||||
local datastore = assert(Datastores[datastoreName], 'Datastore not found '..tostring(datastoreName))
|
||||
return datastore:debug()
|
||||
end
|
||||
|
||||
--[[-- Commonly used serializer, returns the name of the object
|
||||
@tparam any rawKey The raw key that will be serialized, this can be things like player, force, surface, etc
|
||||
@treturn string The name of the object that was passed
|
||||
|
||||
@usage-- Using the name serializer for your datastore
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:set_serializer(Datastore.name_serializer)
|
||||
|
||||
]]
|
||||
function DatastoreManager.name_serializer(rawKey)
|
||||
return rawKey.name
|
||||
end
|
||||
|
||||
----- Datastore Internal
|
||||
-- @section datastore-internal
|
||||
|
||||
--[[-- Debug, Get the debug info for this datastore
|
||||
@treturn table The debug info for this datastore, contains stuff like parent, settings, children, etc
|
||||
|
||||
@usage-- Get the debug info for a datastore
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
local debug_info = ExampleData:debug()
|
||||
|
||||
]]
|
||||
function Datastore:debug()
|
||||
local debug_info = {}
|
||||
|
||||
if self.parent then
|
||||
debug_info.parent = self.parent.name
|
||||
else
|
||||
debug_info.settings = { auto_save = self.auto_save, save_to_disk = self.save_to_disk, propagate_changes = self.propagate_changes, serializer = not not self.serializer }
|
||||
end
|
||||
|
||||
local children = {}
|
||||
for name in pairs(self.children) do children[#children+1] = name end
|
||||
if #children > 0 then debug_info.children = children end
|
||||
|
||||
local events = {}
|
||||
for name, handlers in pairs(self.events) do events[name] = #handlers end
|
||||
if next(events) then debug_info.events = events end
|
||||
|
||||
if next(self.metadata) then debug_info.metadata = self.metadata end
|
||||
debug_info.data = self:get_all()
|
||||
|
||||
return debug_info
|
||||
end
|
||||
|
||||
--[[-- Internal, Get data following combine logic
|
||||
@tparam string key The key to get the value of from this datastore
|
||||
@tparam[opt=false] boolean fromChild If the get request came from a child of this datastore
|
||||
@treturn any The value that was stored at this key in this datastore
|
||||
|
||||
@usage-- Internal, Get the data from a datastore
|
||||
local value = self:raw_get('TestKey')
|
||||
|
||||
]]
|
||||
function Datastore:raw_get(key, fromChild)
|
||||
local data = self.data
|
||||
if self.parent then
|
||||
data = self.parent:raw_get(key, true)
|
||||
key = self.value_name
|
||||
end
|
||||
local value = data[key]
|
||||
if value ~= nil then return value end
|
||||
if fromChild then value = {} end
|
||||
data[key] = value
|
||||
return value
|
||||
end
|
||||
|
||||
--[[-- Internal, Set data following combine logic
|
||||
@tparam string key The key to set the value of in this datastore
|
||||
@tparam any value The value that will be set at this key
|
||||
|
||||
@usage-- Internal, Set the value in a datastore
|
||||
self:raw_set('TestKey', 'Foo')
|
||||
|
||||
]]
|
||||
function Datastore:raw_set(key, value)
|
||||
if self.parent then
|
||||
local data = self.parent:raw_get(key, true)
|
||||
data[self.value_name] = value
|
||||
else
|
||||
self.data[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
local function serialize_error(err) error('An error ocurred in a datastore serializer: '..trace(err)) end
|
||||
--[[-- Internal, Return the serialized key
|
||||
@tparam any rawKey The key that needs to be serialized, if it is already a string then it is returned
|
||||
@treturn string The key after it has been serialized
|
||||
|
||||
@usage-- Internal, Ensure that the key is a string
|
||||
key = self:serialize(key)
|
||||
|
||||
]]
|
||||
function Datastore:serialize(rawKey)
|
||||
if type(rawKey) == 'string' then return rawKey end
|
||||
assert(self.serializer, 'Datastore does not have a serializer and received non string key')
|
||||
local success, key = xpcall(self.serializer, serialize_error, rawKey)
|
||||
return success and key or nil
|
||||
end
|
||||
|
||||
--[[-- Internal, Writes an event to the output file to be saved and/or propagated
|
||||
@tparam string action The action that should be wrote to datastore.pipe, can be request, remove, message, save, propagate
|
||||
@tparam string key The key that the action is being preformed on
|
||||
@tparam any value The value that should be used with the action
|
||||
|
||||
@usage-- Write a data request to datastore.pipe
|
||||
self:write_action('request', 'TestKey')
|
||||
|
||||
@usage-- Write a data save to datastore.pipe
|
||||
self:write_action('save', 'TestKey', 'Foo')
|
||||
|
||||
]]
|
||||
function Datastore:write_action(action, key, value)
|
||||
local data = {action, self.name, key}
|
||||
if value ~= nil then
|
||||
data[4] = type(value) == 'table' and game.table_to_json(value) or value
|
||||
end
|
||||
game.write_file('ext/datastore.out', table.concat(data, ' ')..'\n', true, 0)
|
||||
end
|
||||
|
||||
----- Datastore Local
|
||||
-- @section datastore-local
|
||||
|
||||
--[[-- Create a new datastore which is stores its data inside of this datastore
|
||||
@tparam string subDatastoreName The name of the datastore that will have its data stored in this datastore
|
||||
@treturn table The new datastore that was created inside of this datastore
|
||||
|
||||
@usage-- Add a new sub datastore
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
local BarData = ExampleData:combine('Bar')
|
||||
|
||||
]]
|
||||
function Datastore:combine(subDatastoreName)
|
||||
local new_datastore = DatastoreManager.connect(self.name..'.'..subDatastoreName)
|
||||
self.children[subDatastoreName] = new_datastore
|
||||
new_datastore.value_name = subDatastoreName
|
||||
new_datastore.serializer = self.serializer
|
||||
new_datastore.auto_save = self.auto_save
|
||||
new_datastore.parent = self
|
||||
Data[new_datastore.name] = nil
|
||||
new_datastore.data = nil
|
||||
return new_datastore
|
||||
end
|
||||
|
||||
--[[-- Set a callback that will be used to serialize keys which aren't strings
|
||||
@tparam function callback The function that will be used to serialize non string keys passed as an argument
|
||||
|
||||
@usage-- Set a custom serializer, this would be the same as Datastore.name_serializer
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:set_serializer(function(rawKey)
|
||||
return rawKey.name
|
||||
end)
|
||||
|
||||
]]
|
||||
function Datastore:set_serializer(callback)
|
||||
assert(type(callback) == 'function', 'Callback must be a function')
|
||||
self.serializer = callback
|
||||
end
|
||||
|
||||
--[[-- Set a default value to be returned by get if no other default is given, using will mean get will never return nil, set using the default will set to nil to save space
|
||||
@tparam any value The value that will be deep copied by get if the value is nil and no other default is given
|
||||
@tparam boolean allowSet When true if the default is passed as the value for set it will be set rather than setting nil
|
||||
|
||||
@usage-- Set a default value to be returned by get
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:set_default('Foo')
|
||||
|
||||
]]
|
||||
function Datastore:set_default(value, allowSet)
|
||||
self.default = value
|
||||
self.allow_set_to_default = allowSet
|
||||
end
|
||||
|
||||
--[[-- Set metadata tags on this datastore which can be accessed by other scripts
|
||||
@tparam table tags A table of tags that you want to set in the metadata for this datastore
|
||||
|
||||
@usage-- Adding metadata that could be used by a gui to help understand the stored data
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:set_metadata{
|
||||
caption = 'Test Data',
|
||||
tooltip = 'Data used for testing datastores',
|
||||
type = 'table'
|
||||
}
|
||||
|
||||
]]
|
||||
function Datastore:set_metadata(tags)
|
||||
local metadata = self.metadata
|
||||
for key, value in pairs(tags) do
|
||||
metadata[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
--[[-- Get a value from local storage, option to have a default value, do not edit the data returned as changes may not save, use update if you want to make changes
|
||||
@tparam any key The key that you want to get the value of, must be a string unless a serializer is set
|
||||
@tparam[opt] any default The default value that will be returned if no value is found in the datastore
|
||||
|
||||
@usage-- Get a key from the datastore, the default will be deep copied if no value exists in the datastore
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
local value = ExampleData:get('TestKey')
|
||||
|
||||
]]
|
||||
function Datastore:get(key, default)
|
||||
key = self:serialize(key)
|
||||
local value = self:raw_get(key)
|
||||
if value ~= nil then return value end
|
||||
return copy(default or self.default)
|
||||
end
|
||||
|
||||
--[[-- Set a value in local storage, will trigger on_update then on_save, save_to_disk and auto_save is required for on_save
|
||||
@tparam any key The key that you want to set the value of, must be a string unless a serializer is set
|
||||
@tparam any value The value that you want to set for this key
|
||||
|
||||
@usage-- Set a value in the datastore, this will trigger on_update, if auto_save is true then will trigger save
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:set('TestKey', 'Foo')
|
||||
|
||||
]]
|
||||
function Datastore:set(key, value)
|
||||
key = self:serialize(key)
|
||||
local old_value = self:raw_get(key)
|
||||
if value == self.default and not self.allow_set_to_default then
|
||||
self:raw_set(key)
|
||||
else
|
||||
self:raw_set(key, value)
|
||||
end
|
||||
self:raise_event('on_update', key, value, old_value)
|
||||
if self.auto_save then self:save(key) end
|
||||
return value
|
||||
end
|
||||
|
||||
--[[-- Increment the value in local storage, only works for number values, will trigger on_update then on_save, save_to_disk and auto_save is required for on_save
|
||||
@tparam any key The key that you want to increment the value of, must be a string unless a serializer is set
|
||||
@tparam[opt=1] number delta The amount that you want to increment the value by, can be negative or a decimal
|
||||
|
||||
@usage-- Increment a value in a datastore, the value must be a number or nil, if nil 0 is used as the start value
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:increment('TestNumber')
|
||||
|
||||
]]
|
||||
function Datastore:increment(key, delta)
|
||||
key = self:serialize(key)
|
||||
local value = self:raw_get(key) or 0
|
||||
return self:set(key, value + (delta or 1))
|
||||
end
|
||||
|
||||
local function update_error(err) log('An error occurred in datastore update:\n\t'..trace(err)) end
|
||||
--[[-- Use a function to update the value locally, will trigger on_update then on_save, save_to_disk and auto_save is required for on_save
|
||||
@tparam any key The key that you want to apply the update to, must be a string unless a serializer is set
|
||||
@tparam function callback The function that will be used to update the value at this key
|
||||
|
||||
@usage-- Using a function to update a value, if a value is returned then this will be the new value
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:increment('TestKey', function(key, value)
|
||||
return value..value
|
||||
end)
|
||||
|
||||
]]
|
||||
function Datastore:update(key, callback)
|
||||
key = self:serialize(key)
|
||||
local value = self:get(key)
|
||||
local raw_value = self:raw_get(key)
|
||||
local old_value = copy(self:raw_get(key))
|
||||
local success, new_value = xpcall(callback, update_error, key, value)
|
||||
if not success then
|
||||
self:raw_set(key, old_value)
|
||||
elseif new_value ~= nil then
|
||||
self:set(key, new_value)
|
||||
elseif raw_value == nil then
|
||||
self:set(key, value)
|
||||
else
|
||||
self:raise_event('on_update', key, value, old_value)
|
||||
if self.auto_save then self:save(key) end
|
||||
end
|
||||
end
|
||||
|
||||
--[[-- Remove a value locally and on the external source, works regardless of propagateChanges, requires save_to_disk for external changes
|
||||
@tparam any key The key that you want to remove locally and externally, must be a string unless a serializer is set
|
||||
|
||||
@usage-- Remove a key locally and externally
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:remove('TestKey')
|
||||
|
||||
]]
|
||||
function Datastore:remove(key)
|
||||
key = self:serialize(key)
|
||||
local old_value = self:raw_get(key)
|
||||
self:raw_set(key)
|
||||
self:raise_event('on_update', key, nil, old_value)
|
||||
if self.save_to_disk then self:write_action('remove', key) end
|
||||
if self.parent and self.parent.auto_save then return self.parent:save(key) end
|
||||
end
|
||||
|
||||
local function filter_error(err) log('An error ocurred in a datastore filter:\n\t'..trace(err)) end
|
||||
--[[-- Internal, Used to filter elements from a table
|
||||
@tparam table tbl The table that will have the filter applied to it
|
||||
@tparam[opt] function callback The function that will be used as a filter, if none giving then the provided table is returned
|
||||
@treturn table The table which has only the key values pairs which passed the filter
|
||||
|
||||
@usage-- Internal, Filter a table by the values it contains, return true to keep the key value pair
|
||||
local filtered_table = filter({5,3,4,1,2}, function(key, value)
|
||||
return value > 2
|
||||
end)
|
||||
|
||||
]]
|
||||
local function filter(tbl, callback)
|
||||
if not callback then return tbl end
|
||||
local rtn = {}
|
||||
for key, value in pairs(tbl) do
|
||||
local success, add = xpcall(callback, filter_error, key, value)
|
||||
if success and add then rtn[key] = value end
|
||||
end
|
||||
return rtn
|
||||
end
|
||||
|
||||
--[[-- Get all keys in this datastore, optional filter callback
|
||||
@tparam[opt] function callback The filter function that can be used to filter the results returned
|
||||
@treturn table All the data that is in this datastore, filtered if a filter was provided
|
||||
|
||||
@usage-- Get all the data in this datastore
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
local data = ExampleData:get_all()
|
||||
|
||||
@usage-- Get all the data in this datastore, with a filter
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
local data = ExampleData:get_all(function(key, value)
|
||||
return type(value) == 'string'
|
||||
end)
|
||||
|
||||
]]
|
||||
function Datastore:get_all(callback)
|
||||
if not self.parent then
|
||||
return filter(self.data, callback)
|
||||
else
|
||||
local data, value_name = {}, self.value_name
|
||||
for key, value in pairs(self.parent:get_all()) do
|
||||
data[key] = value[value_name]
|
||||
end
|
||||
return filter(data, callback)
|
||||
end
|
||||
end
|
||||
|
||||
--[[-- Update all keys in this datastore using the same update function
|
||||
@tparam function callback The update function that will be applied to each key
|
||||
|
||||
@usage-- Get all the data in this datastore, with a filter
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:update_all(function(key, value)
|
||||
return value..value
|
||||
end)
|
||||
|
||||
]]
|
||||
function Datastore:update_all(callback)
|
||||
local data = self:get_all()
|
||||
for key, value in pairs(data) do
|
||||
local old_value = copy(value)
|
||||
local success, new_value = xpcall(callback, update_error, key, value)
|
||||
if success and new_value ~= nil then
|
||||
self:set(key, new_value)
|
||||
else
|
||||
self:raise_event('on_update', key, value, old_value)
|
||||
if self.auto_save then self:save(key) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
----- Datastore External
|
||||
-- @section datastore-external
|
||||
|
||||
--[[-- Request a value from an external source, will trigger on_load when data is received
|
||||
@tparam any key The key that you want to request from an external source, must be a string unless a serializer is set
|
||||
|
||||
@usage-- Request a key from an external source, on_load is triggered when data is received
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:request('TestKey')
|
||||
|
||||
]]
|
||||
function Datastore:request(key)
|
||||
if self.parent then return self.parent:request(key) end
|
||||
key = self:serialize(key)
|
||||
self:write_action('request', key)
|
||||
end
|
||||
|
||||
--[[-- Save a value to an external source, will trigger on_save before data is saved, save_to_disk must be set to true
|
||||
@tparam any key The key that you want to save to an external source, must be a string unless a serializer is set
|
||||
|
||||
@usage-- Save a key to an external source, save_to_disk must be set to true for there to be any effect
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:save('TestKey')
|
||||
|
||||
]]
|
||||
function Datastore:save(key)
|
||||
if self.parent then self.parent:save(key) end
|
||||
if not self.save_to_disk then return end
|
||||
key = self:serialize(key)
|
||||
local value = self:raise_event('on_save', key, copy(self:raw_get(key)))
|
||||
local action = self.propagate_changes and 'propagate' or 'save'
|
||||
self:write_action(action, key, value)
|
||||
end
|
||||
|
||||
--[[-- Save a value to an external source and remove locally, will trigger on_unload then on_save, save_to_disk is not required for on_unload
|
||||
@tparam any key The key that you want to unload from the datastore, must be a string unless a serializer is set
|
||||
|
||||
@usage-- Unload a key from the datastore, get will now return nil and value will be saved externally if save_to_disk is set to true
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:unload('TestKey')
|
||||
|
||||
]]
|
||||
function Datastore:unload(key)
|
||||
if self.parent then return self.parent:unload(key) end
|
||||
key = self:serialize(key)
|
||||
self:raise_event('on_unload', key, copy(self:raw_get(key)))
|
||||
self:save(key)
|
||||
self:raw_set(key)
|
||||
end
|
||||
|
||||
--[[-- Use to send a message over the connection, works regardless of saveToDisk and propagateChanges
|
||||
@tparam any key The key that you want to send a message over, must be a string unless a serializer is set
|
||||
@tparam any message The message that you want to send to other connected servers, or external source
|
||||
|
||||
@usage-- Send a message to other servers on this key, can listen for messages with on_message
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:message('TestKey', 'Foo')
|
||||
|
||||
]]
|
||||
function Datastore:message(key, message)
|
||||
key = self:serialize(key)
|
||||
self:write_action('message', key, message)
|
||||
end
|
||||
|
||||
--[[-- Save all the keys in the datastore, optional filter callback
|
||||
@tparam[opt] function callback The filter function that can be used to filter the keys saved
|
||||
|
||||
@usage-- Save all the data in this datastore
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
local data = ExampleData:save_all()
|
||||
|
||||
@usage-- Save all the data in this datastore, with a filter
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:save_all(function(key, value)
|
||||
return type(value) == 'string'
|
||||
end)
|
||||
|
||||
]]
|
||||
function Datastore:save_all(callback)
|
||||
local data = self:get_all(callback)
|
||||
for key in pairs(data) do self:save(key) end
|
||||
end
|
||||
|
||||
--[[-- Unload all the keys in the datastore, optional filter callback
|
||||
@tparam[opt] function callback The filter function that can be used to filter the keys unloaded
|
||||
|
||||
@usage-- Unload all the data in this datastore
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:unload_all()
|
||||
|
||||
@usage-- Unload all the data in this datastore, with a filter
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:unload_all(function(key, value)
|
||||
return type(value) == 'string'
|
||||
end)
|
||||
|
||||
]]
|
||||
function Datastore:unload_all(callback)
|
||||
local data = self:get_all(callback)
|
||||
for key in pairs(data) do self:unload(key) end
|
||||
end
|
||||
|
||||
----- Events
|
||||
-- @section events
|
||||
|
||||
local function event_error(err) log('An error ocurred in a datastore event handler:\n\t'..trace(err)) end
|
||||
--[[-- Internal, Raise an event on this datastore
|
||||
@tparam string event_name The name of the event to raise for this datastore
|
||||
@tparam string key The key that this event is being raised for
|
||||
@tparam[opt] any value The current value that this key has, might be a deep copy of the value
|
||||
@tparam[opt] any old_value The previous value that this key has, might be a deep copy of the value
|
||||
@tparam[opt] string source Where this call came from, used to do event recursion so can be parent or child
|
||||
@treturn any The value that is left after being passed through all the event handlers
|
||||
|
||||
@usage-- Internal, Getting the value that should be saved
|
||||
value = self:raise_event('on_save', key, value)
|
||||
|
||||
]]
|
||||
function Datastore:raise_event(event_name, key, value, old_value, source)
|
||||
-- Raise the event for the children of this datastore
|
||||
if source ~= 'child' and next(self.children) then
|
||||
if type(value) ~= 'table' then value = {} end
|
||||
for value_name, child in pairs(self.children) do
|
||||
local old_child_value = old_value and old_value[value_name] or nil
|
||||
value[value_name] = child:raise_event(event_name, key, value[value_name], old_child_value, 'parent')
|
||||
end
|
||||
end
|
||||
|
||||
-- Raise the event for this datastore
|
||||
local handlers = self.events[event_name]
|
||||
if handlers then
|
||||
for _, handler in ipairs(handlers) do
|
||||
local success, new_value = xpcall(handler, event_error, key, value, old_value)
|
||||
if success and new_value ~= nil then value = new_value end
|
||||
end
|
||||
end
|
||||
|
||||
-- Raise the event for the parent of this datastore
|
||||
if source ~= 'parent' and self.parent then
|
||||
local parent_value = self.parent:raw_get(key, true)
|
||||
self.parent:raise_event(event_name, key, parent_value, parent_value, 'child')
|
||||
end
|
||||
|
||||
-- If this is the save event and the table is empty then return nil
|
||||
if event_name == 'on_save' and next(self.children) and not next(value) then return end
|
||||
return value
|
||||
end
|
||||
|
||||
--[[-- Internal, Returns a function which will add a callback to an event
|
||||
@tparam string event_name The name of the event that this should create a handler adder for
|
||||
@treturn function The function that can be used to add handlers to this event
|
||||
|
||||
@usage-- Internal, Get the function to add handlers to on_load
|
||||
Datastore.on_load = event_factory('on_load')
|
||||
|
||||
]]
|
||||
local function event_factory(event_name)
|
||||
return function(self, callback)
|
||||
assert(type(callback) == 'function', 'Handler must be a function')
|
||||
local handlers = self.events[event_name]
|
||||
if not handlers then
|
||||
self.events[event_name] = { callback }
|
||||
else
|
||||
handlers[#handlers+1] = callback
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[-- Register a callback that triggers when data is loaded from an external source, returned value is saved locally
|
||||
@tparam function callback The handler that will be registered to the on_load event
|
||||
@usage-- Adding a handler to on_load, returned value will be saved locally, can be used to deserialize the value beyond a normal json
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:on_load(function(key, value)
|
||||
game.print('Test data loaded for: '..key)
|
||||
end)
|
||||
]]
|
||||
Datastore.on_load = event_factory('on_load')
|
||||
|
||||
--[[-- Register a callback that triggers before data is saved, returned value is saved externally
|
||||
@tparam function callback The handler that will be registered to the on_load event
|
||||
@usage-- Adding a handler to on_save, returned value will be saved externally, can be used to serialize the value beyond a normal json
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:on_save(function(key, value)
|
||||
game.print('Test data saved for: '..key)
|
||||
end)
|
||||
]]
|
||||
Datastore.on_save = event_factory('on_save')
|
||||
|
||||
--[[-- Register a callback that triggers before data is unloaded, returned value is ignored
|
||||
@tparam function callback The handler that will be registered to the on_load event
|
||||
@usage-- Adding a handler to on_unload, returned value is ignored, can be used to clean up guis or local values related to this data
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:on_load(function(key, value)
|
||||
game.print('Test data unloaded for: '..key)
|
||||
end)
|
||||
]]
|
||||
Datastore.on_unload = event_factory('on_unload')
|
||||
|
||||
--[[-- Register a callback that triggers when a message is received, returned value is ignored
|
||||
@tparam function callback The handler that will be registered to the on_load event
|
||||
@usage-- Adding a handler to on_message, returned value is ignored, can be used to receive messages from other connected servers without saving data
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:on_message(function(key, value)
|
||||
game.print('Test data message for: '..key)
|
||||
end)
|
||||
]]
|
||||
Datastore.on_message = event_factory('on_message')
|
||||
|
||||
--[[-- Register a callback that triggers any time a value is changed, returned value is ignored
|
||||
@tparam function callback The handler that will be registered to the on_load event
|
||||
@usage-- Adding a handler to on_update, returned value is ignored, can be used to update guis or send messages when data is changed
|
||||
local ExampleData = Datastore.connect('ExampleData')
|
||||
ExampleData:on_update(function(key, value)
|
||||
game.print('Test data updated for: '..key)
|
||||
end)
|
||||
]]
|
||||
Datastore.on_update = event_factory('on_update')
|
||||
|
||||
----- Module Return
|
||||
return DatastoreManager
|
||||
153
exp_legacy/module/expcore/external.lua
Normal file
153
exp_legacy/module/expcore/external.lua
Normal file
@@ -0,0 +1,153 @@
|
||||
--[[-- Core Module - External
|
||||
- A module used to make accessing externally set data easier.
|
||||
@core External
|
||||
@alias External
|
||||
|
||||
@usage-- Printing all server to chat
|
||||
local External = require 'expcore.external' --- @dep expcore.external
|
||||
|
||||
local message = 'id: %s name: %s version: %s status: %s'
|
||||
for server_id, server in pairs(External.get_servers()) do
|
||||
local status = External.get_server_status(server_id)
|
||||
game.print(message:format(server_id, server.name, server.version, status))
|
||||
end
|
||||
|
||||
]]
|
||||
|
||||
local ext, var
|
||||
local concat = table.concat
|
||||
|
||||
local External = {}
|
||||
|
||||
--[[-- Checks that local links are valid, will try to add the links if invalid
|
||||
@treturn boolean If the external data is valid, if false you should not call any other methods from External
|
||||
|
||||
@usage-- Check that external data is valid
|
||||
if not External.valid() then
|
||||
-- error code here
|
||||
end
|
||||
|
||||
]]
|
||||
function External.valid()
|
||||
if global.ext == nil then return false end
|
||||
if ext == global.ext and var == ext.var then
|
||||
return var ~= nil
|
||||
else
|
||||
ext = global.ext
|
||||
var = ext.var
|
||||
return var ~= nil
|
||||
end
|
||||
end
|
||||
|
||||
--[[-- Gets a table of all the servers, key is the server id, value is the server details
|
||||
@treturn table A table containing all the servers, key is the server id, value is the server details
|
||||
|
||||
@usage-- Get all servers
|
||||
local servers = External.get_servers()
|
||||
|
||||
]]
|
||||
function External.get_servers()
|
||||
assert(ext, 'No external data was found, use External.valid() to ensure external data exists.')
|
||||
return assert(ext.servers, 'No server list was found, please ensure that the external service is running')
|
||||
end
|
||||
|
||||
--[[-- Gets a table of all the servers filtered by name, key is the server id, value is the server details
|
||||
@tparam string search The string to search for, names, short_names and ids are checked for this string.
|
||||
@treturn table A table containing all the servers filtered by name, key is the server id, value is the server details
|
||||
|
||||
@usage-- Get all servers with public in the name
|
||||
local servers = External.get_servers_filtered(public)
|
||||
|
||||
]]
|
||||
function External.get_servers_filtered(search)
|
||||
assert(ext, 'No external data was found, use External.valid() to ensure external data exists.')
|
||||
local servers = assert(ext.servers, 'No server list was found, please ensure that the external service is running')
|
||||
local found_servers = {}
|
||||
search = search:lower()
|
||||
for server_id, server in pairs(servers) do
|
||||
local str = concat{server.name, server.short_name, server.id}
|
||||
if str:lower():find(search, 1, true) then found_servers[server_id] = server end
|
||||
end
|
||||
return found_servers
|
||||
end
|
||||
|
||||
--[[-- Gets the details of the current server
|
||||
@treturn table The details of the current server
|
||||
|
||||
@usage-- Get the details of the current server
|
||||
local server = External.get_current_server()
|
||||
|
||||
]]
|
||||
function External.get_current_server()
|
||||
assert(ext, 'No external data was found, use External.valid() to ensure external data exists.')
|
||||
local servers = assert(ext.servers, 'No server list was found, please ensure that the external service is running')
|
||||
local server_id = assert(ext.current, 'No current id was found, please ensure that the external service is running')
|
||||
return servers[server_id]
|
||||
end
|
||||
|
||||
--[[-- Gets the details of the given server
|
||||
@tparam string server_id The internal server if for the server you want the details of
|
||||
@treturn table The details of the given server
|
||||
|
||||
@usage-- Get the details of the given server
|
||||
local server = External.get_server_details('eu-01')
|
||||
|
||||
]]
|
||||
function External.get_server_details(server_id)
|
||||
assert(ext, 'No external data was found, use External.valid() to ensure external data exists.')
|
||||
local servers = assert(ext.servers, 'No server list was found, please ensure that the external service is running')
|
||||
return servers[server_id]
|
||||
end
|
||||
|
||||
--[[-- Gets the status of the given server
|
||||
@tparam string server_id The internal server if for the server you want the status of
|
||||
@tparam boolean raw When true Current will not be returned as status but rather the raw status for the server
|
||||
@treturn string The status of the given server, one of: Online, Modded, Protected, Current, Offline
|
||||
|
||||
@usage-- Get the status of the given server
|
||||
local status = External.get_server_status('eu-01')
|
||||
|
||||
]]
|
||||
function External.get_server_status(server_id, raw)
|
||||
assert(var, 'No external data was found, use External.valid() to ensure external data exists.')
|
||||
local servers = assert(var.status, 'No server status was found, please ensure that the external service is running')
|
||||
local current = assert(ext.current, 'No current id was found, please ensure that the external service is running')
|
||||
return not raw and server_id == current and 'Current' or servers[server_id]
|
||||
end
|
||||
|
||||
--[[-- Gets the ups of the current server
|
||||
@usage-- Get the ups of the current server
|
||||
local server_ups = External.get_server_ups()
|
||||
|
||||
]]
|
||||
function External.get_server_ups()
|
||||
assert(var, 'No external data was found, use External.valid() to ensure external data exists.')
|
||||
return assert(var.server_ups, 'No server ups was found, please ensure that the external service is running')
|
||||
end
|
||||
|
||||
--[[-- Connect a player to the given server
|
||||
@tparam LuaPlayer player The player that you want to request to join a different server
|
||||
@tparam string server_id The internal id of the server to connect to, can also be any address but this will show Unknown Server
|
||||
@tparam[opt=false] boolean self_requested If the player requested the join them selfs, this will hide the message about being asked to switch
|
||||
|
||||
@usage-- Request that a player joins a different server
|
||||
External.request_connection(player, 'eu-01')
|
||||
|
||||
@usage-- Request that a player joins a different server, by own request
|
||||
External.request_connection(player, 'eu-01', true)
|
||||
|
||||
]]
|
||||
function External.request_connection(player, server_id, self_requested)
|
||||
local server = { address = server_id, name = 'Unknown Server', description = 'This server is not ran by us, please check the address of the server.' }
|
||||
if ext and ext.servers and ext.servers[server_id] then server = ext.servers[server_id] end
|
||||
local message = 'Please press the connect button below to join.'
|
||||
if not self_requested then message = 'You have been asked to switch to a different server.\n'..message end
|
||||
player.connect_to_server{
|
||||
address = server.address,
|
||||
name = '\n[color=orange][font=heading-1]'..server.name..'[/font][/color]\n',
|
||||
description = server.description..'\n'..message
|
||||
}
|
||||
end
|
||||
|
||||
--- Module return
|
||||
return External
|
||||
1
exp_legacy/module/expcore/gui.lua
Normal file
1
exp_legacy/module/expcore/gui.lua
Normal file
@@ -0,0 +1 @@
|
||||
return require 'expcore.gui._require'
|
||||
144
exp_legacy/module/expcore/gui/_require.lua
Normal file
144
exp_legacy/module/expcore/gui/_require.lua
Normal file
@@ -0,0 +1,144 @@
|
||||
--[[-- Core Module - Gui
|
||||
- Used to simplify gui creation using factory functions called element defines
|
||||
@core Gui
|
||||
@alias Gui
|
||||
|
||||
@usage-- To draw your element you only need to call the factory function
|
||||
-- You are able to pass any other arguments that are used in your custom functions but the first is always the parent element
|
||||
local example_button_element = example_button(parent_element)
|
||||
|
||||
@usage-- Making a factory function for a button with the caption "Example Button"
|
||||
-- This method has all the same features as LuaGuiElement.add
|
||||
local example_button =
|
||||
Gui.element{
|
||||
type = 'button',
|
||||
caption = 'Example Button'
|
||||
}
|
||||
|
||||
@usage-- Making a factory function for a button which is contained within a flow
|
||||
-- This method is for when you still want to register event handlers but cant use the table method
|
||||
local example_flow_with_button =
|
||||
Gui.element(function(definition, parent, ...)
|
||||
-- ... shows that all other arguments from the factory call are passed to this function
|
||||
-- Here we are adding a flow which we will then later add a button to
|
||||
local flow =
|
||||
parent.add{ -- paraent is the element which is passed to the factory function
|
||||
name = 'example_flow',
|
||||
type = 'flow'
|
||||
}
|
||||
|
||||
-- Now we add the button to the flow that we created earlier
|
||||
local element = definition:triggers_event(
|
||||
flow.add{
|
||||
type = 'button',
|
||||
caption = 'Example Button'
|
||||
}
|
||||
)
|
||||
|
||||
-- You must return a new element, this is so styles can be applied and returned to the caller
|
||||
-- You may return any of your elements that you added, consider the context in which it will be used for which should be returned
|
||||
return element
|
||||
end)
|
||||
|
||||
@usage-- Styles can be added to any element define, simplest way mimics LuaGuiElement.style[key] = value
|
||||
local example_button =
|
||||
Gui.element{
|
||||
type = 'button',
|
||||
caption = 'Example Button',
|
||||
style = 'forward_button' -- factorio styles can be applied here
|
||||
}
|
||||
:style{
|
||||
height = 25, -- same as element.style.height = 25
|
||||
width = 100 -- same as element.style.width = 25
|
||||
}
|
||||
|
||||
@usage-- Styles can also have a custom function when the style is dynamic and depends on other factors
|
||||
-- Use this method if your style is dynamic and depends on other factors
|
||||
local example_button =
|
||||
Gui.element{
|
||||
type = 'button',
|
||||
caption = 'Example Button',
|
||||
style = 'forward_button' -- factorio styles can be applied here
|
||||
}
|
||||
:style(function(style, element, ...)
|
||||
-- style is the current style object for the elemenent
|
||||
-- element is the element that is being changed
|
||||
-- ... shows that all other arguments from the factory call are passed to this function
|
||||
local player = game.players[element.player_index]
|
||||
style.height = 25
|
||||
style.width = 100
|
||||
style.font_color = player.color
|
||||
end)
|
||||
|
||||
@usage-- You are able to register event handlers to your elements, these can be factorio events or custom ones
|
||||
-- All events are checked to be valid before raising any handlers, this means element.valid = true and player.valid = true
|
||||
Gui.element{
|
||||
type = 'button',
|
||||
caption = 'Example Button'
|
||||
}
|
||||
:on_click(function(player, element, event)
|
||||
-- player is the player who interacted with the element to cause the event
|
||||
-- element is a refrence to the element which caused the event
|
||||
-- event is a raw refrence to the event data if player and element are not enough
|
||||
player.print('Clicked: '..element.name)
|
||||
end)
|
||||
|
||||
@usage-- Example from core_defines, Gui.core_defines.hide_left_flow, called like: hide_left_flow(parent_element)
|
||||
--- Button which hides the elements in the left flow, shows inside the left flow when frames are visible
|
||||
-- @element hide_left_flow
|
||||
local hide_left_flow =
|
||||
Gui.element{
|
||||
type = 'sprite-button',
|
||||
sprite = 'utility/close_black',
|
||||
style = 'tool_button',
|
||||
tooltip = {'expcore-gui.left-button-tooltip'}
|
||||
}
|
||||
:style{
|
||||
padding = -3,
|
||||
width = 18,
|
||||
height = 20
|
||||
}
|
||||
:on_click(function(player, _,_)
|
||||
Gui.hide_left_flow(player)
|
||||
end)
|
||||
|
||||
@usage-- Eample from defines, Gui.alignment, called like: Gui.alignment(parent, name, horizontal_align, vertical_align)
|
||||
-- Notice how _ are used to blank arguments that are not needed in that context and how they line up with above
|
||||
Gui.alignment =
|
||||
Gui.element(function(_, parent, name, _,_)
|
||||
return parent.add{
|
||||
name = name or 'alignment',
|
||||
type = 'flow',
|
||||
}
|
||||
end)
|
||||
:style(function(style, _,_, horizontal_align, vertical_align)
|
||||
style.padding = {1, 2}
|
||||
style.vertical_align = vertical_align or 'center'
|
||||
style.horizontal_align = horizontal_align or 'right'
|
||||
style.vertically_stretchable = style.vertical_align ~= 'center'
|
||||
style.horizontally_stretchable = style.horizontal_align ~= 'center'
|
||||
end)
|
||||
|
||||
]]
|
||||
|
||||
local Gui = require 'expcore.gui.prototype'
|
||||
require 'expcore.gui.helper_functions'
|
||||
require 'expcore.gui.core_defines'
|
||||
require 'expcore.gui.top_flow'
|
||||
require 'expcore.gui.left_flow'
|
||||
require 'expcore.gui.defines'
|
||||
|
||||
local Roles = _C.opt_require('expcore.roles')
|
||||
local Event = _C.opt_require('utils.event')
|
||||
|
||||
if Roles and Event then
|
||||
Event.add(Roles.events.on_role_assigned, function(e)
|
||||
Gui.update_top_flow(game.get_player(e.player_index))
|
||||
end)
|
||||
Event.add(Roles.events.on_role_unassigned, function(e)
|
||||
Gui.update_top_flow(game.get_player(e.player_index))
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
return Gui
|
||||
89
exp_legacy/module/expcore/gui/core_defines.lua
Normal file
89
exp_legacy/module/expcore/gui/core_defines.lua
Normal file
@@ -0,0 +1,89 @@
|
||||
--[[-- Core Module - Gui
|
||||
- Gui defines that are used internally by the gui system
|
||||
@module Gui
|
||||
]]
|
||||
|
||||
local Gui = require 'expcore.gui.prototype'
|
||||
local Event = require 'utils.event'
|
||||
|
||||
--- Core Defines.
|
||||
-- @section coreDefines
|
||||
|
||||
--- Button which toggles the top flow elements, version which shows inside the top flow when top flow is visible
|
||||
-- @element hide_top_flow
|
||||
local hide_top_flow =
|
||||
Gui.element{
|
||||
type = 'sprite-button',
|
||||
sprite = 'utility/preset',
|
||||
style = 'tool_button',
|
||||
tooltip = {'gui_util.button_tooltip'},
|
||||
name = Gui.unique_static_name
|
||||
}
|
||||
:style{
|
||||
padding = -2,
|
||||
width = 18,
|
||||
height = 36
|
||||
}
|
||||
:on_click(function(player, _,_)
|
||||
Gui.toggle_top_flow(player, false)
|
||||
end)
|
||||
Gui.core_defines.hide_top_flow = hide_top_flow
|
||||
|
||||
--- Button which toggles the top flow elements, version which shows inside the left flow when top flow is hidden
|
||||
-- @element show_top_flow
|
||||
local show_top_flow =
|
||||
Gui.element{
|
||||
type = 'sprite-button',
|
||||
sprite = 'utility/preset',
|
||||
style = 'tool_button',
|
||||
tooltip = {'gui_util.button_tooltip'},
|
||||
name = Gui.unique_static_name
|
||||
}
|
||||
:style{
|
||||
padding = -2,
|
||||
width = 18,
|
||||
height = 20
|
||||
}
|
||||
:on_click(function(player, _,_)
|
||||
Gui.toggle_top_flow(player, true)
|
||||
end)
|
||||
Gui.core_defines.show_top_flow = show_top_flow
|
||||
|
||||
--- Button which hides the elements in the left flow, shows inside the left flow when frames are visible
|
||||
-- @element hide_left_flow
|
||||
local hide_left_flow =
|
||||
Gui.element{
|
||||
type = 'sprite-button',
|
||||
sprite = 'utility/close_black',
|
||||
style = 'tool_button',
|
||||
tooltip = {'expcore-gui.left-button-tooltip'},
|
||||
name = Gui.unique_static_name
|
||||
}
|
||||
:style{
|
||||
padding = -3,
|
||||
width = 18,
|
||||
height = 20
|
||||
}
|
||||
:on_click(function(player, _,_)
|
||||
Gui.hide_left_flow(player)
|
||||
end)
|
||||
Gui.core_defines.hide_left_flow = hide_left_flow
|
||||
|
||||
--- Draw the core elements when a player joins the game
|
||||
Event.add(defines.events.on_player_created, function(event)
|
||||
local player = game.players[event.player_index]
|
||||
|
||||
-- Draw the top flow
|
||||
local top_flow = Gui.get_top_flow(player)
|
||||
hide_top_flow(top_flow)
|
||||
Gui.update_top_flow(player)
|
||||
|
||||
-- Draw the left flow
|
||||
local left_flow = Gui.get_left_flow(player)
|
||||
local button_flow = left_flow.add{ type = 'flow', name = 'gui_core_buttons', direction = 'vertical' }
|
||||
local show_top = show_top_flow(button_flow)
|
||||
local hide_left = hide_left_flow(button_flow)
|
||||
show_top.visible = false
|
||||
hide_left.visible = false
|
||||
Gui.draw_left_flow(player)
|
||||
end)
|
||||
298
exp_legacy/module/expcore/gui/defines.lua
Normal file
298
exp_legacy/module/expcore/gui/defines.lua
Normal file
@@ -0,0 +1,298 @@
|
||||
--[[-- Core Module - Gui
|
||||
- Common defines that are used by other modules, non of these are used internally
|
||||
@module Gui
|
||||
]]
|
||||
|
||||
local Gui = require 'expcore.gui.prototype'
|
||||
|
||||
--- Defines.
|
||||
-- @section defines
|
||||
|
||||
--[[-- Draw a flow used to align its child elements, default is right align
|
||||
@element Gui.alignment
|
||||
@tparam LuaGuiElement parent the parent element to which the alignment will be added
|
||||
@tparam[opt='alignment'] string name the name of the alignment flow which is added
|
||||
@tparam[opt='right'] string horizontal_align the horizontal alignment of the elements in the flow
|
||||
@tparam[opt='center'] string vertical_align the vertical alignment of the elements in the flow
|
||||
@treturn LuaGuiElement the alignment flow that was created
|
||||
|
||||
@usage-- Adding a right align flow
|
||||
local alignment = Gui.alignment(element, 'example_right_alignment')
|
||||
|
||||
@usage-- Adding a horizontal center and top align flow
|
||||
local alignment = Gui.alignment(element, 'example_center_top_alignment', 'center', 'top')
|
||||
|
||||
]]
|
||||
Gui.alignment =
|
||||
Gui.element(function(_, parent, name, _,_)
|
||||
return parent.add{
|
||||
name = name or 'alignment',
|
||||
type = 'flow',
|
||||
}
|
||||
end)
|
||||
:style(function(style, _,_, horizontal_align, vertical_align)
|
||||
style.padding = {1, 2}
|
||||
style.vertical_align = vertical_align or 'center'
|
||||
style.horizontal_align = horizontal_align or 'right'
|
||||
style.vertically_stretchable = style.vertical_align ~= 'center'
|
||||
style.horizontally_stretchable = style.horizontal_align ~= 'center'
|
||||
end)
|
||||
|
||||
--[[-- Draw a scroll pane that has a table inside of it
|
||||
@element Gui.scroll_table
|
||||
@tparam LuaGuiElement parent the parent element to which the scroll table will be added
|
||||
@tparam number height the maximum height for the scroll pane
|
||||
@tparam number column_count the number of columns that the table will have
|
||||
@tparam[opt='scroll'] string name the name of the scroll pane that is added, the table is always called "table"
|
||||
@treturn LuaGuiElement the table that was created
|
||||
|
||||
@usage-- Adding a scroll table with max height of 200 and column count of 3
|
||||
local scroll_table = Gui.scroll_table(element, 200, 3)
|
||||
|
||||
]]
|
||||
Gui.scroll_table =
|
||||
Gui.element(function(_, parent, height, column_count, name)
|
||||
-- Draw the scroll
|
||||
local scroll_pane =
|
||||
parent.add{
|
||||
name = name or 'scroll',
|
||||
type = 'scroll-pane',
|
||||
direction = 'vertical',
|
||||
horizontal_scroll_policy = 'never',
|
||||
vertical_scroll_policy = 'auto',
|
||||
style = 'scroll_pane_under_subheader'
|
||||
}
|
||||
|
||||
-- Set the style of the scroll pane
|
||||
local scroll_style = scroll_pane.style
|
||||
scroll_style.padding = {1, 3}
|
||||
scroll_style.maximal_height = height
|
||||
scroll_style.horizontally_stretchable = true
|
||||
|
||||
-- Draw the table
|
||||
local scroll_table =
|
||||
scroll_pane.add{
|
||||
type = 'table',
|
||||
name = 'table',
|
||||
column_count = column_count
|
||||
}
|
||||
|
||||
-- Return the scroll table
|
||||
return scroll_table
|
||||
end)
|
||||
:style{
|
||||
padding = 0,
|
||||
cell_padding = 0,
|
||||
vertical_align = 'center',
|
||||
horizontally_stretchable = true
|
||||
}
|
||||
|
||||
--[[-- Used to add a frame with the header style, has the option for a right alignment flow for buttons
|
||||
@element Gui.header
|
||||
@tparam LuaGuiElement parent the parent element to which the header will be added
|
||||
@tparam ?string|Concepts.LocalizedString caption the caption that will be shown on the header
|
||||
@tparam[opt] ?string|Concepts.LocalizedString tooltip the tooltip that will be shown on the header
|
||||
@tparam[opt=false] boolean add_alignment when true an alignment flow will be added to the header
|
||||
@tparam[opt='header'] string name the name of the header that is being added, the alignment is always called "alignment"
|
||||
@treturn LuaGuiElement either the header or the header alignment if add_alignment is true
|
||||
|
||||
@usage-- Adding a custom header with a label
|
||||
local header = Gui.header(
|
||||
element,
|
||||
'Example Caption',
|
||||
'Example Tooltip'
|
||||
)
|
||||
|
||||
]]
|
||||
Gui.header =
|
||||
Gui.element(function(_, parent, caption, tooltip, add_alignment, name, label_name)
|
||||
-- Draw the header
|
||||
local header =
|
||||
parent.add{
|
||||
name = name or 'header',
|
||||
type = 'frame',
|
||||
style = 'subheader_frame'
|
||||
}
|
||||
|
||||
-- Change the style of the header
|
||||
local style = header.style
|
||||
style.padding = {2, 4}
|
||||
style.use_header_filler = false
|
||||
style.horizontally_stretchable = true
|
||||
|
||||
-- Draw the caption label
|
||||
if caption then
|
||||
header.add{
|
||||
name = label_name or 'header_label',
|
||||
type = 'label',
|
||||
style = 'heading_1_label',
|
||||
caption = caption,
|
||||
tooltip = tooltip
|
||||
}
|
||||
end
|
||||
|
||||
-- Return either the header or the added alignment
|
||||
return add_alignment and Gui.alignment(header) or header
|
||||
end)
|
||||
|
||||
--[[-- Used to add a frame with the footer style, has the option for a right alignment flow for buttons
|
||||
@element Gui.footer
|
||||
@tparam LuaGuiElement parent the parent element to which the footer will be added
|
||||
@tparam ?string|Concepts.LocalizedString caption the caption that will be shown on the footer
|
||||
@tparam[opt] ?string|Concepts.LocalizedString tooltip the tooltip that will be shown on the footer
|
||||
@tparam[opt=false] boolean add_alignment when true an alignment flow will be added to the footer
|
||||
@tparam[opt='footer'] string name the name of the footer that is being added, the alignment is always called "alignment"
|
||||
@treturn LuaGuiElement either the footer or the footer alignment if add_alignment is true
|
||||
|
||||
@usage-- Adding a custom footer with a label
|
||||
local footer = Gui.footer(
|
||||
element,
|
||||
'Example Caption',
|
||||
'Example Tooltip'
|
||||
)
|
||||
|
||||
]]
|
||||
Gui.footer =
|
||||
Gui.element(function(_, parent, caption, tooltip, add_alignment, name)
|
||||
-- Draw the header
|
||||
local footer =
|
||||
parent.add{
|
||||
name = name or 'footer',
|
||||
type = 'frame',
|
||||
style = 'subfooter_frame'
|
||||
}
|
||||
|
||||
-- Change the style of the footer
|
||||
local style = footer.style
|
||||
style.padding = {2, 4}
|
||||
style.use_header_filler = false
|
||||
style.horizontally_stretchable = true
|
||||
|
||||
-- Draw the caption label
|
||||
if caption then
|
||||
footer.add{
|
||||
name = 'footer_label',
|
||||
type = 'label',
|
||||
style = 'heading_1_label',
|
||||
caption = caption,
|
||||
tooltip = tooltip
|
||||
}
|
||||
end
|
||||
|
||||
-- Return either the footer or the added alignment
|
||||
return add_alignment and Gui.alignment(footer) or footer
|
||||
end)
|
||||
|
||||
--[[-- Used for left frames to give them a nice boarder
|
||||
@element Gui.container
|
||||
@tparam LuaGuiElement parent the parent element to which the container will be added
|
||||
@tparam string name the name that you want to give to the outer frame, often just event_trigger
|
||||
@tparam number width the minimal width that the frame will have
|
||||
|
||||
@usage-- Adding a container as a base
|
||||
local container = Gui.container(parent, 'my_container', 200)
|
||||
|
||||
]]
|
||||
Gui.container =
|
||||
Gui.element(function(_, parent, name, _)
|
||||
-- Draw the external container
|
||||
local frame =
|
||||
parent.add{
|
||||
name = name,
|
||||
type = 'frame'
|
||||
}
|
||||
|
||||
-- Return the container
|
||||
return frame.add{
|
||||
name = 'container',
|
||||
type = 'frame',
|
||||
direction = 'vertical',
|
||||
style = 'window_content_frame_packed'
|
||||
}
|
||||
end)
|
||||
:style(function(style, element, _,width)
|
||||
style.vertically_stretchable = false
|
||||
local frame_style = element.parent.style
|
||||
frame_style.padding = 2
|
||||
frame_style.minimal_width = width
|
||||
end)
|
||||
|
||||
--[[-- Used to make a solid white bar in a gui
|
||||
@element Gui.bar
|
||||
@tparam LuaGuiElement parent the parent element to which the bar will be added
|
||||
@tparam number width the width of the bar that will be made, if not given bar will strech to fill the parent
|
||||
|
||||
@usage-- Adding a bar to a gui
|
||||
local bar = Gui.bar(parent, 100)
|
||||
|
||||
]]
|
||||
Gui.bar =
|
||||
Gui.element(function(_, parent)
|
||||
return parent.add{
|
||||
type = 'progressbar',
|
||||
size = 1,
|
||||
value = 1
|
||||
}
|
||||
end)
|
||||
:style(function(style, _,width)
|
||||
style.height = 3
|
||||
style.color = {r=255, g=255, b=255}
|
||||
if width then style.width = width
|
||||
else style.horizontally_stretchable = true end
|
||||
end)
|
||||
|
||||
--[[-- Used to make a label which is centered and of a certian size
|
||||
@element Gui.centered_label
|
||||
@tparam LuaGuiElement parent the parent element to which the label will be added
|
||||
@tparam number width the width of the label, must be given in order to center the caption
|
||||
@tparam ?string|Concepts.LocalizedString caption the caption that will be shown on the label
|
||||
@tparam[opt] ?string|Concepts.LocalizedString tooltip the tooltip that will be shown on the label
|
||||
|
||||
@usage-- Adding a centered label
|
||||
local label = Gui.centered_label(parent, 100, 'This is centered')
|
||||
|
||||
]]
|
||||
Gui.centered_label =
|
||||
Gui.element(function(_, parent, width, caption, tooltip)
|
||||
local label = parent.add{
|
||||
type = 'label',
|
||||
caption = caption,
|
||||
tooltip = tooltip,
|
||||
style = 'description_label'
|
||||
}
|
||||
|
||||
local style = label.style
|
||||
style.horizontal_align = 'center'
|
||||
style.single_line = false
|
||||
style.width = width
|
||||
|
||||
return label
|
||||
end)
|
||||
|
||||
--[[-- Used to make a title which has two bars on either side
|
||||
@element Gui.title_label
|
||||
@tparam LuaGuiElement parent the parent element to which the label will be added
|
||||
@tparam number width the width of the first bar, this can be used to position the label
|
||||
@tparam ?string|Concepts.LocalizedString caption the caption that will be shown on the label
|
||||
@tparam[opt] ?string|Concepts.LocalizedString tooltip the tooltip that will be shown on the label
|
||||
|
||||
@usage-- Adding a centered label
|
||||
local label = Gui.centered_label(parent, 100, 'This is centered')
|
||||
|
||||
]]
|
||||
Gui.title_label =
|
||||
Gui.element(function(_, parent, width, caption, tooltip)
|
||||
local title_flow = parent.add{ type='flow' }
|
||||
title_flow.style.vertical_align = 'center'
|
||||
|
||||
Gui.bar(title_flow, width)
|
||||
local title_label = title_flow.add{
|
||||
type = 'label',
|
||||
caption = caption,
|
||||
tooltip = tooltip,
|
||||
style = 'heading_1_label'
|
||||
}
|
||||
Gui.bar(title_flow)
|
||||
|
||||
return title_label
|
||||
end)
|
||||
91
exp_legacy/module/expcore/gui/helper_functions.lua
Normal file
91
exp_legacy/module/expcore/gui/helper_functions.lua
Normal file
@@ -0,0 +1,91 @@
|
||||
--[[-- Core Module - Gui
|
||||
- Functions used to help with the use of guis
|
||||
@module Gui
|
||||
]]
|
||||
|
||||
local Gui = require 'expcore.gui.prototype'
|
||||
|
||||
--- Helper Functions.
|
||||
-- @section helperFunctions
|
||||
|
||||
--[[-- Get the player that owns a gui element
|
||||
@tparam LuaGuiElement element the element to get the owner of
|
||||
@treturn LuaPlayer the player that owns this element
|
||||
|
||||
@usage-- Geting the owner of an element
|
||||
local player = Gui.get_player_from_element(element)
|
||||
|
||||
]]
|
||||
function Gui.get_player_from_element(element)
|
||||
if not element or not element.valid then return end
|
||||
return game.players[element.player_index]
|
||||
end
|
||||
|
||||
--[[-- Will toggle the enabled state of an element or set it to the one given
|
||||
@tparam LuaGuiElement element the element to toggle/set the enabled state of
|
||||
@tparam[opt] boolean state with given will set the state, else state will be toggled
|
||||
@treturn boolean the new enabled state that the element has
|
||||
|
||||
@usage-- Toggling the the enabled state
|
||||
local new_enabled_state = Gui.toggle_enabled_state(element)
|
||||
|
||||
]]
|
||||
function Gui.toggle_enabled_state(element, state)
|
||||
if not element or not element.valid then return end
|
||||
if state == nil then state = not element.enabled end
|
||||
element.enabled = state
|
||||
return state
|
||||
end
|
||||
|
||||
--[[-- Will toggle the visible state of an element or set it to the one given
|
||||
@tparam LuaGuiElement element the element to toggle/set the visible state of
|
||||
@tparam[opt] boolean state with given will set the state, else state will be toggled
|
||||
@treturn boolean the new visible state that the element has
|
||||
|
||||
@usage-- Toggling the the visible state
|
||||
local new_visible_state = Gui.toggle_visible_state(element)
|
||||
|
||||
]]
|
||||
function Gui.toggle_visible_state(element, state)
|
||||
if not element or not element.valid then return end
|
||||
if state == nil then state = not element.visible end
|
||||
element.visible = state
|
||||
return state
|
||||
end
|
||||
|
||||
--[[-- Destory a gui element without causing any errors, often because the element was already removed
|
||||
@tparam LuaGuiElement element the element that you want to remove
|
||||
@treturn boolean true if the element was valid and has been removed
|
||||
|
||||
@usage-- Remove a child element if it exists
|
||||
Gui.destroy_if_valid(element[child_name])
|
||||
|
||||
]]
|
||||
function Gui.destroy_if_valid(element)
|
||||
if not element or not element.valid then return false end
|
||||
element.destroy()
|
||||
return true
|
||||
end
|
||||
|
||||
--[[-- Returns a table to be used as the style for a sprite buttons, produces a sqaure button
|
||||
@tparam number size the size that you want the button to be
|
||||
@tparam[opt=-2] number padding the padding that you want on the sprite
|
||||
@tparam[opt] table style any extra style settings that you want to have
|
||||
@treturn table the style table to be used with element_define:style()
|
||||
|
||||
@usage-- Adding a sprite button with size 20
|
||||
local button =
|
||||
Gui.element{
|
||||
type = 'sprite-button',
|
||||
sprite = 'entity/inserter'
|
||||
}
|
||||
:style(Gui.sprite_style(20))
|
||||
|
||||
]]
|
||||
function Gui.sprite_style(size, padding, style)
|
||||
style = style or {}
|
||||
style.padding = padding or -2
|
||||
style.height = size
|
||||
style.width = size
|
||||
return style
|
||||
end
|
||||
275
exp_legacy/module/expcore/gui/left_flow.lua
Normal file
275
exp_legacy/module/expcore/gui/left_flow.lua
Normal file
@@ -0,0 +1,275 @@
|
||||
--[[-- Core Module - Gui
|
||||
- Used to define new gui elements and gui event handlers
|
||||
@module Gui
|
||||
]]
|
||||
|
||||
local Gui = require 'expcore.gui.prototype'
|
||||
local mod_gui = require 'mod-gui'
|
||||
|
||||
local hide_left_flow = Gui.core_defines.hide_left_flow.name
|
||||
|
||||
--- Left Flow.
|
||||
-- @section leftFlow
|
||||
|
||||
-- Triggered when a user changed the visibility of a left flow element by clicking a button
|
||||
Gui.events.on_visibility_changed_by_click = 'on_visibility_changed_by_click'
|
||||
|
||||
--- Contains the uids of the elements that will shown on the left flow and their join functions
|
||||
-- @table left_elements
|
||||
Gui.left_elements = {}
|
||||
|
||||
--[[-- Gets the flow refered to as the left flow, each player has one left flow
|
||||
@function Gui.get_left_flow(player)
|
||||
@tparam LuaPlayer player the player that you want to get the left flow for
|
||||
@treturn LuaGuiElement the left element flow
|
||||
|
||||
@usage-- Geting your left flow
|
||||
local left_flow = Gui.get_left_flow(game.player)
|
||||
|
||||
]]
|
||||
Gui.get_left_flow = mod_gui.get_frame_flow
|
||||
|
||||
--[[-- Sets an element define to be drawn to the left flow when a player joins, includes optional check
|
||||
@tparam[opt] ?boolean|function open_on_join called during first darw to decide if the element should be visible
|
||||
@treturn table the new element define that is used to register events to this element
|
||||
|
||||
@usage-- Adding the example button
|
||||
example_flow_with_button:add_to_left_flow(true)
|
||||
|
||||
]]
|
||||
function Gui._prototype_element:add_to_left_flow(open_on_join)
|
||||
_C.error_if_runtime()
|
||||
if not self.name then error("Elements for the top flow must have a static name") end
|
||||
self.open_on_join = open_on_join or false
|
||||
table.insert(Gui.left_elements, self)
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Creates a button on the top flow which will toggle the given element define, the define must exist in the left flow
|
||||
@tparam string sprite the sprite that you want to use on the button
|
||||
@tparam ?string|Concepts.LocalizedString tooltip the tooltip that you want the button to have
|
||||
@tparam table element_define the element define that you want to have toggled by this button, define must exist on the left flow
|
||||
@tparam[opt] function authenticator used to decide if the button should be visible to a player
|
||||
|
||||
@usage-- Add a button to toggle a left element
|
||||
local toolbar_button =
|
||||
Gui.left_toolbar_button('entity/inserter', 'Nothing to see here', example_flow_with_button, function(player)
|
||||
return player.admin
|
||||
end)
|
||||
|
||||
]]
|
||||
function Gui.left_toolbar_button(sprite, tooltip, element_define, authenticator)
|
||||
local button = Gui.toolbar_button(sprite, tooltip, authenticator)
|
||||
|
||||
-- Add on_click handler to handle click events comming from the player
|
||||
button:on_click(function(player, _, _)
|
||||
-- Raise custom event that tells listening elements if the element has changed visibility by a player clicking
|
||||
-- Used in warp gui to handle the keep open logic
|
||||
button:raise_event{
|
||||
name = Gui.events.on_visibility_changed_by_click,
|
||||
element = Gui.get_top_element(player, button),
|
||||
state = Gui.toggle_left_element(player, element_define)
|
||||
}
|
||||
end)
|
||||
|
||||
-- Add property to the left flow element with the name of the button
|
||||
-- This is for the ability to reverse lookup the button from the left flow element
|
||||
element_define.toolbar_button = button
|
||||
button.left_flow_element = element_define
|
||||
return button
|
||||
end
|
||||
|
||||
Gui._left_flow_order_src = "<default>"
|
||||
--- Get the order of elements in the left flow, first argument is player but is unused in the default method
|
||||
function Gui.get_left_flow_order(_)
|
||||
return Gui.left_elements
|
||||
end
|
||||
|
||||
--- Inject a custom left flow order provider, this should accept a player and return a list of elements definitions to draw
|
||||
function Gui.inject_left_flow_order(provider)
|
||||
Gui.get_left_flow_order = provider
|
||||
local debug_info = debug.getinfo(2, "Sn")
|
||||
local file_name = debug_info.source:match('^.+/currently%-playing/(.+)$'):sub(1, -5)
|
||||
local func_name = debug_info.name or ("<anonymous:"..debug_info.linedefined..">")
|
||||
Gui._left_flow_order_src = file_name..":"..func_name
|
||||
end
|
||||
|
||||
--[[-- Draw all the left elements onto the left flow, internal use only with on join
|
||||
@tparam LuaPlayer player the player that you want to draw the elements for
|
||||
|
||||
@usage-- Draw all the left elements
|
||||
Gui.draw_left_flow(player)
|
||||
|
||||
]]
|
||||
function Gui.draw_left_flow(player)
|
||||
local left_flow = Gui.get_left_flow(player)
|
||||
local hide_button = left_flow.gui_core_buttons[hide_left_flow]
|
||||
local show_hide_button = false
|
||||
|
||||
-- Get the order to draw the elements in
|
||||
local flow_order = Gui.get_left_flow_order(player)
|
||||
if #flow_order ~= #Gui.left_elements then
|
||||
error(string.format("Left flow order provider (%s) did not return the correct element count, expect %d got %d",
|
||||
Gui._left_flow_order_src, #Gui.left_elements, #flow_order
|
||||
))
|
||||
end
|
||||
|
||||
for _, element_define in ipairs(flow_order) do
|
||||
-- Draw the element to the left flow
|
||||
local draw_success, left_element = xpcall(function()
|
||||
return element_define(left_flow)
|
||||
end, debug.traceback)
|
||||
|
||||
if not draw_success then
|
||||
log('There as been an error with an element draw function: '..element_define.defined_at..'\n\t'..left_element)
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Check if it should be open by default
|
||||
local open_on_join = element_define.open_on_join
|
||||
local visible = type(open_on_join) == 'boolean' and open_on_join or false
|
||||
if type(open_on_join) == 'function' then
|
||||
local success, err = xpcall(open_on_join, debug.traceback, player)
|
||||
if not success then
|
||||
log('There as been an error with an open on join hander for a gui element:\n\t'..err)
|
||||
goto continue
|
||||
end
|
||||
visible = err
|
||||
end
|
||||
|
||||
-- Set the visible state of the element
|
||||
left_element.visible = visible
|
||||
show_hide_button = show_hide_button or visible
|
||||
|
||||
-- Check if the the element has a button attached
|
||||
if element_define.toolbar_button then
|
||||
Gui.toggle_toolbar_button(player, element_define.toolbar_button, visible)
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
|
||||
hide_button.visible = show_hide_button
|
||||
end
|
||||
|
||||
--- Reorder the left flow elements to match that returned by the provider, uses a method equivalent to insert sort
|
||||
function Gui.reorder_left_flow(player)
|
||||
local left_flow = Gui.get_left_flow(player)
|
||||
|
||||
-- Get the order to draw the elements in
|
||||
local flow_order = Gui.get_left_flow_order(player)
|
||||
if #flow_order ~= #Gui.left_elements then
|
||||
error(string.format("Left flow order provider (%s) did not return the correct element count, expect %d got %d",
|
||||
Gui._left_flow_order_src, #Gui.left_elements, #flow_order
|
||||
))
|
||||
end
|
||||
|
||||
-- Reorder the elements, index 1 is the core ui buttons so +1 is required
|
||||
for index, element_define in ipairs(flow_order) do
|
||||
local element = left_flow[element_define.name]
|
||||
left_flow.swap_children(index+1, element.get_index_in_parent())
|
||||
end
|
||||
end
|
||||
|
||||
--[[-- Update the visible state of the hide button, can be used to check if any frames are visible
|
||||
@tparam LuaPlayer player the player to update the left flow for
|
||||
@treturn boolean true if any left element is visible
|
||||
|
||||
@usage-- Check if any left elements are visible
|
||||
local visible = Gui.update_left_flow(player)
|
||||
|
||||
]]
|
||||
function Gui.update_left_flow(player)
|
||||
local left_flow = Gui.get_left_flow(player)
|
||||
local hide_button = left_flow.gui_core_buttons[hide_left_flow]
|
||||
for _, element_define in ipairs(Gui.left_elements) do
|
||||
local left_element = left_flow[element_define.name]
|
||||
if left_element.visible then
|
||||
hide_button.visible = true
|
||||
return true
|
||||
end
|
||||
end
|
||||
hide_button.visible = false
|
||||
return false
|
||||
end
|
||||
|
||||
--[[-- Hides all left elements for a player
|
||||
@tparam LuaPlayer player the player to hide the elements for
|
||||
|
||||
@usage-- Hide your left elements
|
||||
Gui.hide_left_flow(game.player)
|
||||
|
||||
]]
|
||||
function Gui.hide_left_flow(player)
|
||||
local top_flow = Gui.get_top_flow(player)
|
||||
local left_flow = Gui.get_left_flow(player)
|
||||
local hide_button = left_flow.gui_core_buttons[hide_left_flow]
|
||||
|
||||
-- Set the visible state of all elements in the flow
|
||||
hide_button.visible = false
|
||||
for _, element_define in ipairs(Gui.left_elements) do
|
||||
left_flow[element_define.name].visible = false
|
||||
|
||||
-- Check if the the element has a toobar button attached
|
||||
if element_define.toolbar_button then
|
||||
-- Check if the topflow contains the button
|
||||
local button = top_flow[element_define.toolbar_button.name]
|
||||
if button then
|
||||
-- Style the button
|
||||
Gui.toggle_toolbar_button(player, element_define.toolbar_button, false)
|
||||
-- Raise the custom event if all of the top checks have passed
|
||||
element_define.toolbar_button:raise_event{
|
||||
name = Gui.events.on_visibility_changed_by_click,
|
||||
element = button,
|
||||
state = false
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Checks if an element is loaded, used internally when the normal left gui assumptions may not hold
|
||||
function Gui.left_flow_loaded(player, element_define)
|
||||
local left_flow = Gui.get_left_flow(player)
|
||||
return left_flow[element_define.name] ~= nil
|
||||
end
|
||||
|
||||
--[[-- Get the element define that is in the left flow, use in events without an element refrence
|
||||
@tparam LuaPlayer player the player that you want to get the element for
|
||||
@tparam table element_define the element that you want to get
|
||||
@treturn LuaGuiElement the gui element linked to this define for this player
|
||||
|
||||
@usage-- Get your left element
|
||||
local frame = Gui.get_left_element(game.player, example_flow_with_button)
|
||||
|
||||
]]
|
||||
function Gui.get_left_element(player, element_define)
|
||||
local left_flow = Gui.get_left_flow(player)
|
||||
return assert(left_flow[element_define.name], "Left element failed to load")
|
||||
end
|
||||
|
||||
--[[-- Toggles the visible state of a left element for a given player, can be used to set the visible state
|
||||
@tparam LuaPlayer player the player that you want to toggle the element for
|
||||
@tparam table element_define the element that you want to toggle
|
||||
@tparam[opt] boolean state with given will set the state, else state will be toggled
|
||||
@treturn boolean the new visible state of the element
|
||||
|
||||
@usage-- Toggle your example button
|
||||
Gui.toggle_top_flow(game.player, example_flow_with_button)
|
||||
|
||||
@usage-- Show your example button
|
||||
Gui.toggle_top_flow(game.player, example_flow_with_button, true)
|
||||
|
||||
]]
|
||||
function Gui.toggle_left_element(player, element_define, state)
|
||||
-- Set the visible state
|
||||
local element = Gui.get_left_element(player, element_define)
|
||||
if state == nil then state = not element.visible end
|
||||
element.visible = state
|
||||
Gui.update_left_flow(player)
|
||||
|
||||
-- Check if the the element has a button attached
|
||||
if element_define.toolbar_button then
|
||||
Gui.toggle_toolbar_button(player, element_define.toolbar_button, state)
|
||||
end
|
||||
return state
|
||||
end
|
||||
412
exp_legacy/module/expcore/gui/prototype.lua
Normal file
412
exp_legacy/module/expcore/gui/prototype.lua
Normal file
@@ -0,0 +1,412 @@
|
||||
--[[-- Core Module - Gui
|
||||
- Used to simplify gui creation using factory functions called element defines
|
||||
@module Gui
|
||||
]]
|
||||
|
||||
local Event = require 'utils.event' --- @dep utils.event
|
||||
|
||||
local Gui = {
|
||||
--- The current highest uid that is being used by a define, will not increase during runtime
|
||||
uid = 0,
|
||||
--- Used to automatically assign a unique static name to an element
|
||||
unique_static_name = {},
|
||||
--- String indexed table used to avoid conflict with custom event names, similar to how defines.events works
|
||||
events = {},
|
||||
--- Uid indexed array that stores all the factory functions that were defined, no new values will be added during runtime
|
||||
defines = {},
|
||||
--- An string indexed table of all the defines which are used by the core of the gui system, used for internal reference
|
||||
core_defines = {},
|
||||
--- Used to store the file names where elements were defined, this can be useful to find the uid of an element, mostly for debugging
|
||||
file_paths = {},
|
||||
--- Used to store extra information about elements as they get defined such as the params used and event handlers registered to them
|
||||
debug_info = {},
|
||||
--- The prototype used to store the functions of an element define
|
||||
_prototype_element = {},
|
||||
--- The prototype metatable applied to new element defines
|
||||
_mt_element = {}
|
||||
}
|
||||
|
||||
--- Allow access to the element prototype methods
|
||||
Gui._mt_element.__index = Gui._prototype_element
|
||||
|
||||
--- Allows the define to be called to draw the element
|
||||
function Gui._mt_element.__call(self, parent, ...)
|
||||
local element, no_events = self._draw(self, parent, ...)
|
||||
if self._style then self._style(element.style, element, ...) end
|
||||
|
||||
-- Asserts to catch common errors
|
||||
if element then
|
||||
if self.name and self.name ~= element.name then
|
||||
error("Static name \""..self.name.."\" expected but got: "..tostring(element.name))
|
||||
end
|
||||
local event_triggers = element.tags and element.tags.ExpGui_event_triggers
|
||||
if event_triggers and table.array_contains(event_triggers, self.uid) then
|
||||
error("Element::triggers_events should not be called on the value you return from the definition")
|
||||
end
|
||||
elseif self.name then
|
||||
error("Static name \""..self.name.."\" expected but no element was returned from the definition")
|
||||
end
|
||||
|
||||
-- Register events by default, but allow skipping them
|
||||
if no_events == self.no_events then
|
||||
return element
|
||||
else
|
||||
return element and self:triggers_events(element)
|
||||
end
|
||||
end
|
||||
|
||||
--- Get where a function was defined as a string
|
||||
local function get_defined_at(level)
|
||||
local debug_info = debug.getinfo(level, "Sn")
|
||||
local file_name = debug_info.source:match('^.+/currently%-playing/(.+)$'):sub(1, -5)
|
||||
local func_name = debug_info.name or ("<anonymous:"..debug_info.linedefined..">")
|
||||
return file_name..":"..func_name
|
||||
end
|
||||
|
||||
--- Element Define.
|
||||
-- @section elementDefine
|
||||
|
||||
--[[-- Used to define new elements for your gui, can be used like LuaGuiElement.add or a custom function
|
||||
@tparam ?table|function element_define the define information for the gui element, same data as LuaGuiElement.add, or a custom function may be used
|
||||
@treturn table the new element define, this can be considered a factory for the element which can be called to draw the element to any other element
|
||||
|
||||
@usage-- Using element defines like LuaGuiElement.add
|
||||
-- This returns a factory function to draw a button with the caption "Example Button"
|
||||
local example_button =
|
||||
Gui.element{
|
||||
type = 'button',
|
||||
caption = 'Example Button'
|
||||
}
|
||||
|
||||
@usage-- Using element defines with a custom factory function
|
||||
-- This method can be used if you still want to be able register event handlers but it is too complex to be compatible with LuaGuiElement.add
|
||||
local example_flow_with_button =
|
||||
Gui.element(function(event_trigger, parent, ...)
|
||||
-- ... shows that all other arguments from the factory call are passed to this function
|
||||
-- parent is the element which was passed to the factory function where you should add your new element
|
||||
-- here we are adding a flow which we will then later add a button to
|
||||
local flow =
|
||||
parent.add{
|
||||
name = 'example_flow',
|
||||
type = 'flow'
|
||||
}
|
||||
|
||||
-- event_trigger should be the name of any elements you want to trigger your event handlers, such as on_click or on_state_changed
|
||||
-- now we add the button to the flow that we created earlier
|
||||
local element =
|
||||
flow.add{
|
||||
name = event_trigger,
|
||||
type = 'button',
|
||||
caption = 'Example Button'
|
||||
}
|
||||
|
||||
-- you must return your new element, this is so styles can be applied and returned to the caller
|
||||
-- you may return any of your elements that you add, consider the context in which it will be used for what should be returned
|
||||
return element
|
||||
end)
|
||||
|
||||
]]
|
||||
function Gui.element(element_define)
|
||||
_C.error_if_runtime()
|
||||
-- Set the metatable to allow access to register events
|
||||
local element = setmetatable({}, Gui._mt_element)
|
||||
|
||||
-- Increment the uid counter
|
||||
local uid = Gui.uid + 1
|
||||
Gui.uid = uid
|
||||
element.uid = uid
|
||||
Gui.debug_info[uid] = { draw = 'None', style = 'None', events = {} }
|
||||
|
||||
-- Add the definition function
|
||||
if type(element_define) == 'table' then
|
||||
Gui.debug_info[uid].draw = element_define
|
||||
if element_define.name == Gui.unique_static_name then
|
||||
element_define.name = "ExpGui_"..tostring(uid)
|
||||
end
|
||||
for k, v in pairs(element_define) do
|
||||
if element[k] == nil then
|
||||
element[k] = v
|
||||
end
|
||||
end
|
||||
element._draw = function(_, parent)
|
||||
return parent.add(element_define)
|
||||
end
|
||||
else
|
||||
Gui.debug_info[uid].draw = get_defined_at(element_define)
|
||||
element._draw = element_define
|
||||
end
|
||||
|
||||
-- Add the define to the base module
|
||||
element.defined_at = get_defined_at(3)
|
||||
Gui.file_paths[uid] = element.defined_at
|
||||
Gui.defines[uid] = element
|
||||
|
||||
-- Return the element so event handers can be accessed
|
||||
return element
|
||||
end
|
||||
|
||||
--[[-- Used to extent your element define with a style factory, this style will be applied to your element when created, can also be a custom function
|
||||
@tparam ?table|function style_define style table where each key and value pair is treated like LuaGuiElement.style[key] = value, a custom function can be used
|
||||
@treturn table the element define is returned to allow for event handlers to be registered
|
||||
|
||||
@usage-- Using the table method of setting the style
|
||||
local example_button =
|
||||
Gui.element{
|
||||
type = 'button',
|
||||
caption = 'Example Button',
|
||||
style = 'forward_button' -- factorio styles can be applied here
|
||||
}
|
||||
:style{
|
||||
height = 25, -- same as element.style.height = 25
|
||||
width = 100 -- same as element.style.width = 25
|
||||
}
|
||||
|
||||
@usage-- Using the function method to set the style
|
||||
-- Use this method if your style is dynamic and depends on other factors
|
||||
local example_button =
|
||||
Gui.element{
|
||||
type = 'button',
|
||||
caption = 'Example Button',
|
||||
style = 'forward_button' -- factorio styles can be applied here
|
||||
}
|
||||
:style(function(style, element, ...)
|
||||
-- style is the current style object for the elemenent
|
||||
-- element is the element that is being changed
|
||||
-- ... shows that all other arguments from the factory call are passed to this function
|
||||
local player = game.players[element.player_index]
|
||||
style.height = 25
|
||||
style.width = 100
|
||||
style.font_color = player.color
|
||||
end)
|
||||
|
||||
]]
|
||||
function Gui._prototype_element:style(style_define)
|
||||
_C.error_if_runtime()
|
||||
-- Add the definition function
|
||||
if type(style_define) == 'table' then
|
||||
Gui.debug_info[self.uid].style = style_define
|
||||
self._style = function(style)
|
||||
for key, value in pairs(style_define) do
|
||||
style[key] = value
|
||||
end
|
||||
end
|
||||
else
|
||||
Gui.debug_info[self.uid].style = get_defined_at(style_define)
|
||||
self._style = style_define
|
||||
end
|
||||
|
||||
-- Return the element so event handers can be accessed
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Enforce the fact the element has a static name, this is required for the cases when a function define is used
|
||||
@tparam[opt] string element The element that will trigger calls to the event handlers
|
||||
@treturn table the element define is returned to allow for event handlers to be registered
|
||||
]]
|
||||
function Gui._prototype_element:static_name(name)
|
||||
_C.error_if_runtime()
|
||||
if name == Gui.unique_static_name then
|
||||
self.name = "ExpGui_"..tostring(self.uid)
|
||||
else
|
||||
self.name = name
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Used to link an element to an element define such that any event on the element will call the handlers on the element define
|
||||
-- You should not call this on the element you return from your constructor because this is done automatically
|
||||
@tparam LuaGuiElement element The element that will trigger calls to the event handlers
|
||||
@treturn LuaGuiElement The element passed as the argument to allow for cleaner returns
|
||||
]]
|
||||
function Gui._prototype_element:triggers_events(element)
|
||||
if not self._has_events then return element end
|
||||
local tags = element.tags
|
||||
if not tags then
|
||||
element.tags = { ExpGui_event_triggers = { self.uid } }
|
||||
return element
|
||||
elseif not tags.ExpGui_event_triggers then
|
||||
tags.ExpGui_event_triggers = { self.uid }
|
||||
elseif table.array_contains(tags.ExpGui_event_triggers, self.uid) then
|
||||
error("Element::triggers_events called multiple times on the same element with the same definition")
|
||||
else
|
||||
table.insert(tags.ExpGui_event_triggers, self.uid)
|
||||
end
|
||||
-- To modify a set of tags, the whole table needs to be written back to the respective property.
|
||||
element.tags = tags
|
||||
return element
|
||||
end
|
||||
|
||||
--- Explicitly skip events on the element returned by your definition function
|
||||
function Gui._prototype_element:no_events(element)
|
||||
return element, self.no_events
|
||||
end
|
||||
|
||||
--[[-- Set the handler which will be called for a custom event, only one handler can be used per event per element
|
||||
@tparam string event_name the name of the event you want to handler to be called on, often from Gui.events
|
||||
@tparam function handler the handler that you want to be called when the event is raised
|
||||
@treturn table the element define so more handleres can be registered
|
||||
|
||||
@usage-- Register a handler to "my_custom_event" for this element
|
||||
element_deinfe:on_event('my_custom_event', function(event)
|
||||
event.player.print(player.name)
|
||||
end)
|
||||
|
||||
]]
|
||||
function Gui._prototype_element:on_event(event_name, handler)
|
||||
_C.error_if_runtime()
|
||||
table.insert(Gui.debug_info[self.uid].events, event_name)
|
||||
Gui.events[event_name] = event_name
|
||||
self[event_name] = handler
|
||||
self._has_events = true
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Raise the handler which is attached to an event; external use should be limited to custom events
|
||||
@tparam table event the event table passed to the handler, must contain fields: name, element
|
||||
@treturn table the element define so more events can be raised
|
||||
|
||||
@usage Raising a custom event
|
||||
element_define:raise_event{
|
||||
name = 'my_custom_event',
|
||||
element = element
|
||||
}
|
||||
|
||||
]]
|
||||
function Gui._prototype_element:raise_event(event)
|
||||
-- Check the element is valid
|
||||
local element = event.element
|
||||
if not element or not element.valid then
|
||||
return self
|
||||
end
|
||||
|
||||
-- Get the event handler for this element
|
||||
local handler = self[event.name]
|
||||
if not handler then
|
||||
return self
|
||||
end
|
||||
|
||||
-- Get the player for this event
|
||||
local player_index = event.player_index or element.player_index
|
||||
local player = game.players[player_index]
|
||||
if not player or not player.valid then
|
||||
return self
|
||||
end
|
||||
event.player = player
|
||||
|
||||
local success, err = xpcall(handler, debug.traceback, player, element, event)
|
||||
if not success then
|
||||
error('There as been an error with an event handler for a gui element:\n\t'..err)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
-- This function is used to link element define events and the events from the factorio api
|
||||
local function event_handler_factory(event_name)
|
||||
Event.add(event_name, function(event)
|
||||
local element = event.element
|
||||
if not element or not element.valid then return end
|
||||
local event_triggers = element.tags.ExpGui_event_triggers
|
||||
if not event_triggers then return end
|
||||
for _, uid in pairs(event_triggers) do
|
||||
local element_define = Gui.defines[uid]
|
||||
if element_define then
|
||||
element_define:raise_event(event)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
Gui.events[event_name] = event_name
|
||||
return function(self, handler)
|
||||
return self:on_event(event_name, handler)
|
||||
end
|
||||
end
|
||||
|
||||
--- Element Events.
|
||||
-- @section elementEvents
|
||||
|
||||
--- Called when the player opens a GUI.
|
||||
-- @tparam function handler the event handler which will be called
|
||||
-- @usage element_define:on_open(function(event)
|
||||
-- event.player.print(table.inspect(event))
|
||||
--end)
|
||||
Gui._prototype_element.on_open = event_handler_factory(defines.events.on_gui_opened)
|
||||
|
||||
--- Called when the player closes the GUI they have open.
|
||||
-- @tparam function handler the event handler which will be called
|
||||
-- @usage element_define:on_close(function(event)
|
||||
-- event.player.print(table.inspect(event))
|
||||
--end)
|
||||
Gui._prototype_element.on_close = event_handler_factory(defines.events.on_gui_closed)
|
||||
|
||||
--- Called when LuaGuiElement is clicked.
|
||||
-- @tparam function handler the event handler which will be called
|
||||
-- @usage element_define:on_click(function(event)
|
||||
-- event.player.print(table.inspect(event))
|
||||
--end)
|
||||
Gui._prototype_element.on_click = event_handler_factory(defines.events.on_gui_click)
|
||||
|
||||
--- Called when a LuaGuiElement is confirmed, for example by pressing Enter in a textfield.
|
||||
-- @tparam function handler the event handler which will be called
|
||||
-- @usage element_define:on_confirmed(function(event)
|
||||
-- event.player.print(table.inspect(event))
|
||||
--end)
|
||||
Gui._prototype_element.on_confirmed = event_handler_factory(defines.events.on_gui_confirmed)
|
||||
|
||||
--- Called when LuaGuiElement checked state is changed (related to checkboxes and radio buttons).
|
||||
-- @tparam function handler the event handler which will be called
|
||||
-- @usage element_define:on_checked_changed(function(event)
|
||||
-- event.player.print(table.inspect(event))
|
||||
--end)
|
||||
Gui._prototype_element.on_checked_changed = event_handler_factory(defines.events.on_gui_checked_state_changed)
|
||||
|
||||
--- Called when LuaGuiElement element value is changed (related to choose element buttons).
|
||||
-- @tparam function handler the event handler which will be called
|
||||
-- @usage element_define:on_elem_changed(function(event)
|
||||
-- event.player.print(table.inspect(event))
|
||||
--end)
|
||||
Gui._prototype_element.on_elem_changed = event_handler_factory(defines.events.on_gui_elem_changed)
|
||||
|
||||
--- Called when LuaGuiElement element location is changed (related to frames in player.gui.screen).
|
||||
-- @tparam function handler the event handler which will be called
|
||||
-- @usage element_define:on_location_changed(function(event)
|
||||
-- event.player.print(table.inspect(event))
|
||||
--end)
|
||||
Gui._prototype_element.on_location_changed = event_handler_factory(defines.events.on_gui_location_changed)
|
||||
|
||||
--- Called when LuaGuiElement selected tab is changed (related to tabbed-panes).
|
||||
-- @tparam function handler the event handler which will be called
|
||||
-- @usage element_define:on_tab_changed(function(event)
|
||||
-- event.player.print(table.inspect(event))
|
||||
--end)
|
||||
Gui._prototype_element.on_tab_changed = event_handler_factory(defines.events.on_gui_selected_tab_changed)
|
||||
|
||||
--- Called when LuaGuiElement selection state is changed (related to drop-downs and listboxes).
|
||||
-- @tparam function handler the event handler which will be called
|
||||
-- @usage element_define:on_selection_changed(function(event)
|
||||
-- event.player.print(table.inspect(event))
|
||||
--end)
|
||||
Gui._prototype_element.on_selection_changed = event_handler_factory(defines.events.on_gui_selection_state_changed)
|
||||
|
||||
--- Called when LuaGuiElement switch state is changed (related to switches).
|
||||
-- @tparam function handler the event handler which will be called
|
||||
-- @usage element_define:on_switch_changed(function(event)
|
||||
-- event.player.print(table.inspect(event))
|
||||
--end)
|
||||
Gui._prototype_element.on_switch_changed = event_handler_factory(defines.events.on_gui_switch_state_changed)
|
||||
|
||||
--- Called when LuaGuiElement text is changed by the player.
|
||||
-- @tparam function handler the event handler which will be called
|
||||
-- @usage element_define:on_text_changed(function(event)
|
||||
-- event.player.print(table.inspect(event))
|
||||
--end)
|
||||
Gui._prototype_element.on_text_changed = event_handler_factory(defines.events.on_gui_text_changed)
|
||||
|
||||
--- Called when LuaGuiElement slider value is changed (related to the slider element).
|
||||
-- @tparam function handler the event handler which will be called
|
||||
-- @usage element_define:on_value_changed(function(event)
|
||||
-- event.player.print(table.inspect(event))
|
||||
--end)
|
||||
Gui._prototype_element.on_value_changed = event_handler_factory(defines.events.on_gui_value_changed)
|
||||
|
||||
-- Module return
|
||||
return Gui
|
||||
315
exp_legacy/module/expcore/gui/top_flow.lua
Normal file
315
exp_legacy/module/expcore/gui/top_flow.lua
Normal file
@@ -0,0 +1,315 @@
|
||||
--[[-- Core Module - Gui
|
||||
- Controls the elements on the top flow
|
||||
@module Gui
|
||||
]]
|
||||
|
||||
local Gui = require 'expcore.gui.prototype'
|
||||
local mod_gui = require 'mod-gui' --- @dep mod-gui
|
||||
|
||||
local toolbar_button_size = 36
|
||||
local hide_top_flow = Gui.core_defines.hide_top_flow.name
|
||||
local show_top_flow = Gui.core_defines.show_top_flow.name
|
||||
|
||||
--- Top Flow.
|
||||
-- @section topFlow
|
||||
|
||||
-- Triggered when a user changed the visibility of a left flow element by clicking a button
|
||||
Gui.events.on_toolbar_button_toggled = 'on_toolbar_button_toggled'
|
||||
|
||||
--- Contains the uids of the elements that will shown on the top flow and their auth functions
|
||||
-- @table top_elements
|
||||
Gui.top_elements = {}
|
||||
|
||||
--- The style that should be used for buttons on the top flow
|
||||
-- @field Gui.top_flow_button_style
|
||||
Gui.top_flow_button_style = mod_gui.button_style
|
||||
|
||||
--- The style that should be used for buttons on the top flow when their flow is visible
|
||||
-- @field Gui.top_flow_button_toggled_style
|
||||
Gui.top_flow_button_toggled_style = 'menu_button_continue'
|
||||
|
||||
--[[-- Styles a top flow button depending on the state given
|
||||
@tparam LuaGuiElement button the button element to style
|
||||
@tparam boolean state The state the button is in
|
||||
|
||||
@usage-- Sets the button to the visible style
|
||||
Gui.toolbar_button_style(button, true)
|
||||
|
||||
@usage-- Sets the button to the hidden style
|
||||
Gui.toolbar_button_style(button, false)
|
||||
|
||||
]]
|
||||
function Gui.toolbar_button_style(button, state, size)
|
||||
---@cast button LuaGuiElement
|
||||
if state then
|
||||
button.style = Gui.top_flow_button_toggled_style
|
||||
else
|
||||
button.style = Gui.top_flow_button_style
|
||||
end
|
||||
button.style.minimal_width = size or toolbar_button_size
|
||||
button.style.height = size or toolbar_button_size
|
||||
button.style.padding = -2
|
||||
end
|
||||
|
||||
--[[-- Gets the flow refered to as the top flow, each player has one top flow
|
||||
@function Gui.get_top_flow(player)
|
||||
@tparam LuaPlayer player the player that you want to get the flow for
|
||||
@treturn LuaGuiElement the top element flow
|
||||
|
||||
@usage-- Geting your top flow
|
||||
local top_flow = Gui.get_top_flow(game.player)
|
||||
|
||||
]]
|
||||
Gui.get_top_flow = mod_gui.get_button_flow
|
||||
|
||||
--[[-- Sets an element define to be drawn to the top flow when a player joins, includes optional authenticator
|
||||
@tparam[opt] function authenticator called during toggle or update to decide weather the element should be visible
|
||||
@treturn table the new element define to allow event handlers to be registered
|
||||
|
||||
@usage-- Adding an element to the top flow on join
|
||||
example_button:add_to_top_flow(function(player)
|
||||
-- example button will only be shown if the player is an admin
|
||||
-- note button will not update its state when player.admin is changed Gui.update_top_flow must be called for this
|
||||
return player.admin
|
||||
end)
|
||||
|
||||
]]
|
||||
function Gui._prototype_element:add_to_top_flow(authenticator)
|
||||
_C.error_if_runtime()
|
||||
if not self.name then error("Elements for the top flow must have a static name") end
|
||||
self.authenticator = authenticator or true
|
||||
table.insert(Gui.top_elements, self)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Returns true if the top flow has visible elements
|
||||
function Gui.top_flow_has_visible_elements(player)
|
||||
local top_flow = Gui.get_top_flow(player)
|
||||
|
||||
for _, child in pairs(top_flow.children) do
|
||||
if child.name ~= hide_top_flow then
|
||||
if child.visible then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
Gui._top_flow_order_src = "<default>"
|
||||
--- Get the order of elements in the top flow, first argument is player but is unused in the default method
|
||||
function Gui.get_top_flow_order(_)
|
||||
return Gui.top_elements
|
||||
end
|
||||
|
||||
--- Inject a custom top flow order provider, this should accept a player and return a list of elements definitions to draw
|
||||
function Gui.inject_top_flow_order(provider)
|
||||
Gui.get_top_flow_order = provider
|
||||
local debug_info = debug.getinfo(2, "Sn")
|
||||
local file_name = debug_info.source:match('^.+/currently%-playing/(.+)$'):sub(1, -5)
|
||||
local func_name = debug_info.name or ("<anonymous:"..debug_info.linedefined..">")
|
||||
Gui._top_flow_order_src = file_name..":"..func_name
|
||||
end
|
||||
|
||||
--[[-- Updates the visible state of all the elements on the players top flow, uses authenticator
|
||||
@tparam LuaPlayer player the player that you want to update the top flow for
|
||||
|
||||
@usage-- Update your top flow
|
||||
Gui.update_top_flow(game.player)
|
||||
|
||||
]]
|
||||
function Gui.update_top_flow(player)
|
||||
local top_flow = Gui.get_top_flow(player)
|
||||
|
||||
-- Get the order to draw the elements in
|
||||
local flow_order = Gui.get_top_flow_order(player)
|
||||
if #flow_order ~= #Gui.top_elements then
|
||||
error(string.format("Top flow order provider (%s) did not return the correct element count, expect %d got %d",
|
||||
Gui._top_flow_order_src, #Gui.top_elements, #flow_order
|
||||
))
|
||||
end
|
||||
|
||||
-- Set the visible state of all elements in the flow
|
||||
for index, element_define in ipairs(flow_order) do
|
||||
-- Ensure the element exists
|
||||
local element = top_flow[element_define.name]
|
||||
if not element then
|
||||
element = element_define(top_flow)
|
||||
else
|
||||
top_flow.swap_children(index+1, element.get_index_in_parent())
|
||||
end
|
||||
|
||||
-- Set the visible state
|
||||
local allowed = element_define.authenticator
|
||||
if type(allowed) == 'function' then allowed = allowed(player) end
|
||||
element.visible = allowed or false
|
||||
|
||||
-- If its not visible and there is a left element, then hide it
|
||||
if element_define.left_flow_element and not element.visible and Gui.left_flow_loaded(player, element_define.left_flow_element) then
|
||||
Gui.toggle_left_element(player, element_define.left_flow_element, false)
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if there are any visible elements in the top flow
|
||||
if not Gui.top_flow_has_visible_elements(player) then
|
||||
-- None are visible so hide the top_flow and its show button
|
||||
Gui.toggle_top_flow(player, false)
|
||||
local left_flow = Gui.get_left_flow(player)
|
||||
local show_button = left_flow.gui_core_buttons[show_top_flow]
|
||||
show_button.visible = false
|
||||
end
|
||||
end
|
||||
|
||||
--- Reorder the top flow elements to match that returned by the provider, uses a method equivalent to insert sort
|
||||
function Gui.reorder_top_flow(player)
|
||||
local top_flow = Gui.get_top_flow(player)
|
||||
|
||||
-- Get the order to draw the elements in
|
||||
local flow_order = Gui.get_top_flow_order(player)
|
||||
if #flow_order ~= #Gui.top_elements then
|
||||
error(string.format("Top flow order provider (%s) did not return the correct element count, expect %d got %d",
|
||||
Gui._top_flow_order_src, #Gui.top_elements, #flow_order
|
||||
))
|
||||
end
|
||||
|
||||
-- Reorder the elements, index 1 is the core ui buttons so +1 is required
|
||||
for index, element_define in ipairs(flow_order) do
|
||||
local element = top_flow[element_define.name]
|
||||
top_flow.swap_children(index+1, element.get_index_in_parent())
|
||||
end
|
||||
end
|
||||
|
||||
--[[-- Toggles the visible state of all the elements on a players top flow, effects all elements
|
||||
@tparam LuaPlayer player the player that you want to toggle the top flow for
|
||||
@tparam[opt] boolean state if given then the state will be set to this
|
||||
@treturn boolean the new visible state of the top flow
|
||||
|
||||
@usage-- Toggle your flow
|
||||
Gui.toggle_top_flow(game.player)
|
||||
|
||||
@usage-- Open your top flow
|
||||
Gui.toggle_top_flow(game.player, true)
|
||||
|
||||
]]
|
||||
function Gui.toggle_top_flow(player, state)
|
||||
-- Get the top flow, we need the parent as we want to toggle the outer frame
|
||||
local top_flow = Gui.get_top_flow(player).parent
|
||||
if state == nil then state = not top_flow.visible end
|
||||
|
||||
-- Get the show button for the top flow
|
||||
local left_flow = Gui.get_left_flow(player)
|
||||
local show_button = left_flow.gui_core_buttons[show_top_flow]
|
||||
|
||||
-- Change the visibility of the top flow and show top flow button
|
||||
show_button.visible = not state
|
||||
top_flow.visible = state
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
--[[-- Get the element define that is in the top flow, use in events without an element refrence
|
||||
@tparam LuaPlayer player the player that you want to get the element for
|
||||
@tparam table element_define the element that you want to get
|
||||
@treturn LuaGuiElement the gui element linked to this define for this player
|
||||
|
||||
@usage-- Get your top element
|
||||
local button = Gui.get_top_element(game.player, example_button)
|
||||
|
||||
]]
|
||||
function Gui.get_top_element(player, element_define)
|
||||
local top_flow = Gui.get_top_flow(player)
|
||||
return assert(top_flow[element_define.name], "Top element failed to load")
|
||||
end
|
||||
|
||||
--[[-- Toggles the state of a toolbar button for a given player, can be used to set the visual state
|
||||
@tparam LuaPlayer player the player that you want to toggle the element for
|
||||
@tparam table element_define the element that you want to toggle
|
||||
@tparam[opt] boolean state with given will set the state, else state will be toggled
|
||||
@treturn boolean the new visible state of the element
|
||||
|
||||
@usage-- Toggle your example button
|
||||
Gui.toggle_toolbar_button(game.player, toolbar_button)
|
||||
|
||||
@usage-- Show your example button
|
||||
Gui.toggle_toolbar_button(game.player, toolbar_button, true)
|
||||
|
||||
]]
|
||||
function Gui.toggle_toolbar_button(player, element_define, state)
|
||||
local toolbar_button = Gui.get_top_element(player, element_define)
|
||||
if state == nil then state = toolbar_button.style.name ~= Gui.top_flow_button_toggled_style end
|
||||
Gui.toolbar_button_style(toolbar_button, state, toolbar_button.style.minimal_width)
|
||||
element_define:raise_event{
|
||||
name = Gui.events.on_toolbar_button_toggled,
|
||||
element = toolbar_button,
|
||||
player = player,
|
||||
state = state
|
||||
}
|
||||
return state
|
||||
end
|
||||
|
||||
--[[-- Creates a button on the top flow with consistent styling
|
||||
@tparam string sprite the sprite that you want to use on the button
|
||||
@tparam ?string|Concepts.LocalizedString tooltip the tooltip that you want the button to have
|
||||
@tparam[opt] function authenticator used to decide if the button should be visible to a player
|
||||
|
||||
@usage-- Add a button to the toolbar
|
||||
local toolbar_button =
|
||||
Gui.left_toolbar_button('entity/inserter', 'Nothing to see here', function(player)
|
||||
return player.admin
|
||||
end)
|
||||
|
||||
]]
|
||||
function Gui.toolbar_button(sprite, tooltip, authenticator)
|
||||
return Gui.element{
|
||||
type = 'sprite-button',
|
||||
sprite = sprite,
|
||||
tooltip = tooltip,
|
||||
style = Gui.top_flow_button_style,
|
||||
name = Gui.unique_static_name
|
||||
}
|
||||
:style{
|
||||
minimal_width = toolbar_button_size,
|
||||
height = toolbar_button_size,
|
||||
padding = -2
|
||||
}
|
||||
:add_to_top_flow(authenticator)
|
||||
end
|
||||
|
||||
--[[-- Creates a toggle button on the top flow with consistent styling
|
||||
@tparam string sprite the sprite that you want to use on the button
|
||||
@tparam ?string|Concepts.LocalizedString tooltip the tooltip that you want the button to have
|
||||
@tparam[opt] function authenticator used to decide if the button should be visible to a player
|
||||
|
||||
@usage-- Add a button to the toolbar
|
||||
local toolbar_button =
|
||||
Gui.toolbar_toggle_button('entity/inserter', 'Nothing to see here', function(player)
|
||||
return player.admin
|
||||
end)
|
||||
:on_event(Gui.events.on_toolbar_button_toggled, function(player, element, event)
|
||||
game.print(table.inspect(event))
|
||||
end)
|
||||
|
||||
]]
|
||||
function Gui.toolbar_toggle_button(sprite, tooltip, authenticator)
|
||||
local button =
|
||||
Gui.element{
|
||||
type = 'sprite-button',
|
||||
sprite = sprite,
|
||||
tooltip = tooltip,
|
||||
style = Gui.top_flow_button_style,
|
||||
name = Gui.unique_static_name
|
||||
}
|
||||
:style{
|
||||
minimal_width = toolbar_button_size,
|
||||
height = toolbar_button_size,
|
||||
padding = -2
|
||||
}
|
||||
:add_to_top_flow(authenticator)
|
||||
|
||||
button:on_click(function(player, _, _)
|
||||
Gui.toggle_toolbar_button(player, button)
|
||||
end)
|
||||
|
||||
return button
|
||||
end
|
||||
358
exp_legacy/module/expcore/permission_groups.lua
Normal file
358
exp_legacy/module/expcore/permission_groups.lua
Normal file
@@ -0,0 +1,358 @@
|
||||
--[[-- Core Module - Permission Groups
|
||||
- Permission group making for factorio so you never have to make one by hand again
|
||||
@core Groups
|
||||
@alias Permissions_Groups
|
||||
|
||||
@usage--- Example Group (Allow All)
|
||||
-- here we will create an admin group however we do not want them to use the map editor or mess with the permission groups
|
||||
Permission_Groups.new_group('Admin') -- this defines a new group called "Admin"
|
||||
:allow_all() -- this makes the default to allow any input action unless set other wise
|
||||
:disallow{ -- here we disallow the input action we don't want them to use
|
||||
'add_permission_group',
|
||||
'delete_permission_group',
|
||||
'import_permissions_string',
|
||||
'map_editor_action',
|
||||
'toggle_map_editor'
|
||||
}
|
||||
|
||||
@usage--- Example Group (Disallow All)
|
||||
-- here we will create a group that cant do anything but talk in chat
|
||||
Permission_Groups.new_group('Restricted') -- this defines a new group called "Restricted"
|
||||
:disallow_all() -- this makes the default to disallow any input action unless set other wise
|
||||
:allow('write_to_console') -- here we allow them to chat, {} can be used here if we had more than one action
|
||||
|
||||
]]
|
||||
|
||||
|
||||
local Game = require 'utils.game' --- @dep utils.game
|
||||
local Event = require 'utils.event' --- @dep utils.event
|
||||
local Async = require 'expcore.async' --- @dep expcore.async
|
||||
|
||||
local Permissions_Groups = {
|
||||
groups={}, -- store for the different groups that are created
|
||||
_prototype={} -- stores functions that are used on group instances
|
||||
}
|
||||
|
||||
-- Async function to add players to permission groups
|
||||
local add_to_permission_group =
|
||||
Async.register(function(permission_group, player)
|
||||
permission_group.add_player(player)
|
||||
end)
|
||||
Permissions_Groups.async_token_add_to_permission_group = add_to_permission_group
|
||||
|
||||
-- Async function to remove players from permission groups
|
||||
local remove_from_permission_group =
|
||||
Async.register(function(permission_group, player)
|
||||
permission_group.remove_player(player)
|
||||
end)
|
||||
Permissions_Groups.async_token_remove_from_permission_group = remove_from_permission_group
|
||||
|
||||
--- Getters.
|
||||
-- Functions that get permission groups
|
||||
-- @section getters
|
||||
|
||||
--[[-- Defines a new permission group that can have it actions set in the config
|
||||
@tparam string name the name of the new group
|
||||
@treturn Permissions_Groups._prototype the new group made with function to allow and disallow actions
|
||||
|
||||
@usage-- Defining a new permission group
|
||||
Groups.new_group('Admin')
|
||||
|
||||
]]
|
||||
function Permissions_Groups.new_group(name)
|
||||
local group = setmetatable({
|
||||
name=name,
|
||||
actions={},
|
||||
allow_all_actions=true
|
||||
}, {
|
||||
__index= Permissions_Groups._prototype
|
||||
})
|
||||
Permissions_Groups.groups[name] = group
|
||||
return group
|
||||
end
|
||||
|
||||
--[[-- Returns the group with the given name, case sensitive
|
||||
@tparam string name the name of the group to get
|
||||
@treturn ?Permissions_Groups._prototype|nil the group with that name or nil if non found
|
||||
|
||||
@usage-- Getting a permision group
|
||||
local admin_group = Groups.get_group_by_name('Admin')
|
||||
|
||||
]]
|
||||
function Permissions_Groups.get_group_by_name(name)
|
||||
return Permissions_Groups.groups[name]
|
||||
end
|
||||
|
||||
--[[-- Returns the group that a player is in
|
||||
@tparam LuaPlayer player the player to get the group of can be name index etc
|
||||
@treturn ?Permissions_Groups._prototype|nil the group with that player or nil if non found
|
||||
|
||||
@usage-- Get your permission group
|
||||
local group = Groups.get_group_from_player(game.player)
|
||||
|
||||
]]
|
||||
function Permissions_Groups.get_group_from_player(player)
|
||||
player = Game.get_player_from_any(player)
|
||||
if not player then return end
|
||||
local group = player.permission_group
|
||||
if group then
|
||||
return Permissions_Groups.groups[group.name]
|
||||
end
|
||||
end
|
||||
|
||||
--- Setters.
|
||||
-- Functions that control all groups
|
||||
-- @section players
|
||||
|
||||
--[[-- Reloads/creates all permission groups and sets them to they configured state
|
||||
|
||||
@usage-- Reload the permission groups, used internally
|
||||
Groups.reload_permissions()
|
||||
|
||||
]]
|
||||
function Permissions_Groups.reload_permissions()
|
||||
for _, group in pairs(Permissions_Groups.groups) do
|
||||
group:create()
|
||||
end
|
||||
end
|
||||
|
||||
--[[-- Sets a player's group to the one given, a player can only have one group at a time
|
||||
@tparam LuaPlayer player the player to effect can be name index etc
|
||||
@tparam string group the name of the group to give to the player
|
||||
@treturn boolean true if the player was added successfully, false other wise
|
||||
|
||||
@usage-- Set your permission group
|
||||
Groups.set_player_group(game.player, 'Admin')
|
||||
|
||||
]]
|
||||
function Permissions_Groups.set_player_group(player, group)
|
||||
player = Game.get_player_from_any(player)
|
||||
group = Permissions_Groups.get_group_by_name(group)
|
||||
if not group or not player then return false end
|
||||
group:add_player(player)
|
||||
return true
|
||||
end
|
||||
|
||||
--- Actions.
|
||||
-- Functions that control group actions
|
||||
-- @section actions
|
||||
|
||||
--[[-- Sets the allow state of an action for this group, used internally but is safe to use else where
|
||||
@tparam ?string|defines.input_action action the action that you want to set the state of
|
||||
@tparam boolean state the state that you want to set it to, true = allow, false = disallow
|
||||
@treturn Permissions_Groups._prototype returns self so function can be chained
|
||||
|
||||
@usage-- Set an action to be disallowed
|
||||
group:set_action('toggle_map_editor', false)
|
||||
|
||||
]]
|
||||
function Permissions_Groups._prototype:set_action(action, state)
|
||||
local input_action = defines.input_action[action]
|
||||
if input_action == nil then input_action = action end
|
||||
assert(type(input_action) == 'number', tostring(action)..' is not a valid input action')
|
||||
self.actions[input_action] = state
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Sets an action or actions to be allowed for this group even with disallow_all triggered, Do not use in runtime
|
||||
@tparam string|Array<string> actions the action or actions that you want to allow for this group
|
||||
@treturn Permissions_Groups._prototype returns self so function can be chained
|
||||
|
||||
@usage-- Allow some actions
|
||||
group:allow{
|
||||
'write_to_console'
|
||||
}
|
||||
|
||||
]]
|
||||
function Permissions_Groups._prototype:allow(actions)
|
||||
if type(actions) ~= 'table' then
|
||||
actions = {actions}
|
||||
end
|
||||
for _, action in pairs(actions) do
|
||||
self:set_action(action, true)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Sets an action or actions to be disallowed for this group even with allow_all triggered, Do not use in runtime
|
||||
@tparam string|Array<string> actions the action or actions that you want to disallow for this group
|
||||
@treturn Permissions_Groups._prototype returns self so function can be chained
|
||||
|
||||
@usage-- Disalow some actions
|
||||
group:disallow{
|
||||
'add_permission_group',
|
||||
'delete_permission_group',
|
||||
'import_permissions_string',
|
||||
'map_editor_action',
|
||||
'toggle_map_editor'
|
||||
}
|
||||
|
||||
]]
|
||||
function Permissions_Groups._prototype:disallow(actions)
|
||||
if type(actions) ~= 'table' then
|
||||
actions = {actions}
|
||||
end
|
||||
for _, action in pairs(actions) do
|
||||
self:set_action(action, false)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Sets the default state for any actions not given to be allowed, useful with :disallow
|
||||
@treturn Permissions_Groups._prototype returns self so function can be chained
|
||||
|
||||
@usage-- Allow all actions unless given by disallow
|
||||
group:allow_all()
|
||||
|
||||
]]
|
||||
function Permissions_Groups._prototype:allow_all()
|
||||
self.allow_all_actions = true
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Sets the default state for any action not given to be disallowed, useful with :allow
|
||||
@treturn Permissions_Groups._prototype returns self so function can be chained
|
||||
|
||||
@usage-- Disallow all actions unless given by allow
|
||||
group:disallow_all()
|
||||
|
||||
]]
|
||||
function Permissions_Groups._prototype:disallow_all()
|
||||
self.allow_all_actions = false
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Returns if an input action is allowed for this group
|
||||
@tparam ?string|defines.input_action action the action that you want to test for
|
||||
@treturn boolean true if the group is allowed the action, false other wise
|
||||
|
||||
@usage-- Test if a group is allowed an action
|
||||
local allowed = group:is_allowed('write_to_console')
|
||||
|
||||
]]
|
||||
function Permissions_Groups._prototype:is_allowed(action)
|
||||
if type(action) == 'string' then
|
||||
action = defines.input_action[action]
|
||||
end
|
||||
local state = self.actions[action]
|
||||
if state == nil then
|
||||
state = self.allow_all_actions
|
||||
end
|
||||
return state
|
||||
end
|
||||
|
||||
--- Players.
|
||||
-- Functions that control group players
|
||||
-- @section players
|
||||
|
||||
--[[-- Creates or updates the permission group with the configured actions, used internally
|
||||
@treturn LuaPermissionGroup the permission group that was created
|
||||
|
||||
@usage-- Create the permission group so players can be added, used internally
|
||||
group:create()
|
||||
|
||||
]]
|
||||
function Permissions_Groups._prototype:create()
|
||||
local group = self:get_raw()
|
||||
if not group then
|
||||
group = game.permissions.create_group(self.name)
|
||||
end
|
||||
for _, action in pairs(defines.input_action) do
|
||||
group.set_allows_action(action, self:is_allowed(action))
|
||||
end
|
||||
return group
|
||||
end
|
||||
|
||||
--[[-- Returns the LuaPermissionGroup that was created with this group object, used internally
|
||||
@treturn LuaPermissionGroup the raw lua permission group
|
||||
|
||||
@usage-- Get the factorio api permision group, used internally
|
||||
local permission_group = group:get_raw()
|
||||
|
||||
]]
|
||||
function Permissions_Groups._prototype:get_raw()
|
||||
return game.permissions.get_group(self.name)
|
||||
end
|
||||
|
||||
--[[-- Adds a player to this group
|
||||
@tparam LuaPlayer player LuaPlayer the player you want to add to this group can be name or index etc
|
||||
@treturn boolean true if the player was added successfully, false other wise
|
||||
|
||||
@usage-- Add a player to this permission group
|
||||
group:add_player(game.player)
|
||||
|
||||
]]
|
||||
function Permissions_Groups._prototype:add_player(player)
|
||||
player = Game.get_player_from_any(player)
|
||||
local group = self:get_raw()
|
||||
if not group or not player then return false end
|
||||
Async(add_to_permission_group, group, player)
|
||||
return true
|
||||
end
|
||||
|
||||
--[[-- Removes a player from this group
|
||||
@tparam LuaPlayer player LuaPlayer the player you want to remove from this group can be name or index etc
|
||||
@treturn boolean true if the player was removed successfully, false other wise
|
||||
|
||||
@usage-- Remove a player from this permission group
|
||||
group:remove_player(game.player)
|
||||
|
||||
]]
|
||||
function Permissions_Groups._prototype:remove_player(player)
|
||||
player = Game.get_player_from_any(player)
|
||||
local group = self:get_raw()
|
||||
if not group or not player then return false end
|
||||
Async(remove_from_permission_group, group, player)
|
||||
return true
|
||||
end
|
||||
|
||||
--[[-- Returns all player that are in this group with the option to filter to online/offline only
|
||||
@tparam[opt] boolean online if nil returns all players, if true online players only, if false returns online players only
|
||||
@treturn table a table of players that are in this group; filtered if online param is given
|
||||
|
||||
@usage-- Get all players in this group
|
||||
local online_players = group:get_players()
|
||||
|
||||
@usage-- Get all online players in this group
|
||||
local online_players = group:get_players(true)
|
||||
|
||||
]]
|
||||
function Permissions_Groups._prototype:get_players(online)
|
||||
local players = {}
|
||||
local group = self:get_raw()
|
||||
if group then
|
||||
if online == nil then
|
||||
return group.players
|
||||
else
|
||||
for _, player in pairs(group.players) do
|
||||
if player.connected == online then
|
||||
table.insert(player, player)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return players
|
||||
end
|
||||
|
||||
--[[-- Prints a message to every player in this group
|
||||
@tparam string message the message that you want to send to the players
|
||||
@treturn number the number of players that received the message
|
||||
|
||||
@usage-- Print a message to all players in thie group
|
||||
group:print('Hello, World!')
|
||||
|
||||
]]
|
||||
function Permissions_Groups._prototype:print(message)
|
||||
local players = self:get_players(true)
|
||||
for _, player in pairs(players) do
|
||||
player.print(message)
|
||||
end
|
||||
return #players
|
||||
end
|
||||
|
||||
-- when the game starts it will make the permission groups
|
||||
Event.on_init(function()
|
||||
Permissions_Groups.reload_permissions()
|
||||
end)
|
||||
|
||||
return Permissions_Groups
|
||||
152
exp_legacy/module/expcore/player_data.lua
Normal file
152
exp_legacy/module/expcore/player_data.lua
Normal file
@@ -0,0 +1,152 @@
|
||||
--[[-- Core Module - PlayerData
|
||||
- A module used to store player data in a central datastore to minimize data requests and saves.
|
||||
@core PlayerData
|
||||
|
||||
@usage-- Adding a colour setting for players
|
||||
local PlayerData = require 'expcore.player_data'
|
||||
local PlayerColors = PlayerData.Settings:combine('Color')
|
||||
|
||||
-- Set the players color when their data is loaded
|
||||
PlayerColors:on_load(function(player_name, color)
|
||||
local player = game.players[player_name]
|
||||
player.color = color
|
||||
end)
|
||||
|
||||
-- Overwrite the saved color with the players current color
|
||||
PlayerColors:on_save(function(player_name, _)
|
||||
local player = game.players[player_name]
|
||||
return player.color -- overwrite existing data with the current color
|
||||
end)
|
||||
|
||||
@usage-- Add a playtime statistic for players
|
||||
local Event = require 'utils.event'
|
||||
local PlayerData = require 'expcore.player_data'
|
||||
local Playtime = PlayerData.Statistics:combine('Playtime')
|
||||
|
||||
-- When playtime reaches an hour interval tell the player and say thanks
|
||||
Playtime:on_update(function(player_name, playtime)
|
||||
if playtime % 60 == 0 then
|
||||
local hours = playtime / 60
|
||||
local player = game.players[player_name]
|
||||
player.print('Thanks for playing on our servers, you have played for '..hours..' hours!')
|
||||
end
|
||||
end)
|
||||
|
||||
-- Update playtime for players, data is only loaded for online players so update_all can be used
|
||||
Event.add_on_nth_tick(3600, function()
|
||||
Playtime:update_all(function(player_name, playtime)
|
||||
return playtime + 1
|
||||
end)
|
||||
end)
|
||||
|
||||
]]
|
||||
|
||||
local Event = require 'utils.event' --- @dep utils.event
|
||||
local Async = require 'expcore.async' --- @dep expcore.async
|
||||
local Datastore = require 'expcore.datastore' --- @dep expcore.datastore
|
||||
local Commands = require 'expcore.commands' --- @dep expcore.commands
|
||||
require 'config.expcore.command_general_parse' --- @dep config.expcore.command_general_parse
|
||||
|
||||
--- Common player data that acts as the root store for player data
|
||||
local PlayerData = Datastore.connect('PlayerData', true) -- saveToDisk
|
||||
PlayerData:set_serializer(Datastore.name_serializer) -- use player name
|
||||
|
||||
--- Store and enum for the data saving preference
|
||||
local DataSavingPreference = PlayerData:combine('DataSavingPreference')
|
||||
local PreferenceEnum = { 'All', 'Statistics', 'Settings', 'Required' }
|
||||
for k,v in ipairs(PreferenceEnum) do PreferenceEnum[v] = k end
|
||||
DataSavingPreference:set_default('All')
|
||||
DataSavingPreference:set_metadata{
|
||||
name = {'expcore-data.preference'},
|
||||
tooltip = {'expcore-data.preference-tooltip'},
|
||||
value_tooltip ={'expcore-data.preference-value-tooltip'}
|
||||
}
|
||||
|
||||
--- Sets your data saving preference
|
||||
-- @command set-data-preference
|
||||
Commands.new_command('set-preference', 'Allows you to set your data saving preference')
|
||||
:add_param('option', false, 'string-options', PreferenceEnum)
|
||||
:register(function(player, option)
|
||||
DataSavingPreference:set(player, option)
|
||||
return {'expcore-data.set-preference', option}
|
||||
end)
|
||||
|
||||
--- Gets your data saving preference
|
||||
-- @command data-preference
|
||||
Commands.new_command('preference', 'Shows you what your current data saving preference is')
|
||||
:register(function(player)
|
||||
return {'expcore-data.get-preference', DataSavingPreference:get(player)}
|
||||
end)
|
||||
|
||||
--- Gets your data and writes it to a file
|
||||
Commands.new_command('save-data', 'Writes all your player data to a file on your computer')
|
||||
:register(function(player)
|
||||
player.print{'expcore-data.get-data'}
|
||||
game.write_file('expgaming_player_data.json', game.table_to_json(PlayerData:get(player, {})), false, player.index)
|
||||
end)
|
||||
|
||||
--- Async function called after 5 seconds with no player data loaded
|
||||
local check_data_loaded = Async.register(function(player)
|
||||
local player_data = PlayerData:get(player)
|
||||
if not player_data or not player_data.valid then
|
||||
player.print{'expcore-data.data-failed'}
|
||||
Datastore.ingest('request', 'PlayerData', player.name, '{"valid":false}')
|
||||
end
|
||||
end)
|
||||
|
||||
--- When player data loads tell the player if the load had failed previously
|
||||
PlayerData:on_load(function(player_name, player_data, existing_data)
|
||||
if not player_data or player_data.valid == false then return end
|
||||
if existing_data and existing_data.valid == false then
|
||||
game.players[player_name].print{'expcore-data.data-restore'}
|
||||
end
|
||||
player_data.valid = true
|
||||
end)
|
||||
|
||||
--- Remove data that the player doesnt want to have stored
|
||||
PlayerData:on_save(function(player_name, player_data)
|
||||
local dataPreference = DataSavingPreference:get(player_name)
|
||||
dataPreference = PreferenceEnum[dataPreference]
|
||||
if dataPreference == PreferenceEnum.All then
|
||||
player_data.valid = nil
|
||||
return player_data
|
||||
end
|
||||
|
||||
local saved_player_data = { PlayerRequired = player_data.PlayerRequired, DataSavingPreference = PreferenceEnum[dataPreference] }
|
||||
if dataPreference <= PreferenceEnum.Settings then saved_player_data.PlayerSettings = player_data.PlayerSettings end
|
||||
if dataPreference <= PreferenceEnum.Statistics then saved_player_data.PlayerStatistics = player_data.PlayerStatistics end
|
||||
|
||||
return saved_player_data
|
||||
end)
|
||||
|
||||
--- Display your data preference when your data loads
|
||||
DataSavingPreference:on_load(function(player_name, dataPreference)
|
||||
game.players[player_name].print{'expcore-data.get-preference', dataPreference or DataSavingPreference.default}
|
||||
end)
|
||||
|
||||
--- Load player data when they join
|
||||
Event.add(defines.events.on_player_joined_game, function(event)
|
||||
local player = game.players[event.player_index]
|
||||
Async.wait(300, check_data_loaded, player)
|
||||
PlayerData:raw_set(player.name)
|
||||
PlayerData:request(player)
|
||||
end)
|
||||
|
||||
--- Unload player data when they leave
|
||||
Event.add(defines.events.on_player_left_game, function(event)
|
||||
local player = game.players[event.player_index]
|
||||
local player_data = PlayerData:get(player)
|
||||
if player_data and player_data.valid == true then
|
||||
PlayerData:unload(player)
|
||||
else PlayerData:raw_set(player.name) end
|
||||
end)
|
||||
|
||||
----- Module Return -----
|
||||
return {
|
||||
All = PlayerData, -- Root for all of a players data
|
||||
Statistics = PlayerData:combine('Statistics'), -- Common place for stats
|
||||
Settings = PlayerData:combine('Settings'), -- Common place for settings
|
||||
Required = PlayerData:combine('Required'), -- Common place for required data
|
||||
DataSavingPreference = DataSavingPreference, -- Stores what data groups will be saved
|
||||
PreferenceEnum = PreferenceEnum -- Enum for the allowed options for data saving preference
|
||||
}
|
||||
1142
exp_legacy/module/expcore/roles.lua
Normal file
1142
exp_legacy/module/expcore/roles.lua
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user