Move files to exp_legacy

This commit is contained in:
Cooldude2606
2024-09-23 15:55:28 +01:00
parent 446e87b610
commit 65145b5d34
266 changed files with 73 additions and 0 deletions

View 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
})

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1 @@
return require 'expcore.gui._require'

View 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

View 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)

View 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)

View 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

View 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

View 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

View 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

View 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

View 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
}

File diff suppressed because it is too large Load Diff