Refactor of commands

This commit is contained in:
Cooldude2606
2019-03-01 20:24:23 +00:00
parent e547f76d6f
commit 62dcfe8694
288 changed files with 5364 additions and 1067 deletions

View File

@@ -0,0 +1,298 @@
--- Command system that allows middle ware and auto validation of command arguments.
-- @module ExpGamingCore.Command@4.0.0
-- @author Cooldude2606
-- @license https://github.com/explosivegaming/scenario/blob/master/LICENSE
-- @alias commands
local Game = require('FactorioStdLib.Game')
local Color = require('FactorioStdLib.Color')
--- Used as an error constant for validation
-- @field commands.error
-- @usage return commands.error, 'err message'
-- @usage return commands.error('err message')
commands.error = setmetatable({},{__call=function(...) return ... end})
commands._add_command = commands.add_command
local commandDataStore = {}
local middleware = {}
--- Used to add middle ware to the command handler, functions should return true or false
-- @tparam function callback function(player,commandName,event) should return true to allow next middle ware to run
function commands.add_middleware(callback) if not is_type(callback,'function') then error('Callback is not a function',2) return end table.insert(middleware,callback) end
--- Index of all command data
-- @field commands.data
-- @usage commands.command_name -- returns command data
-- @usage commands.data -- returns all data
-- @tparam ?string|table|event key the command that will be returned: string is the name, table is the command data, event is event from add_command
-- @treturn table the command data
setmetatable(commands,{
__index=function(tbl,key) return is_type(key,'table') and (key.command and rawget(commandDataStore,key.name) or key) or key == 'data' and commandDataStore or rawget(commandDataStore,key) end
})
--- Collection of functions that can be used to validate inputs
-- @table commands.validate
-- @usage commands.validate[type](value,event,...)
-- @tparam string type the type that the value should be
-- @param value the value that will be tested
-- @param ... any other data that can be passed to the function
-- @return[1] the validated value
-- @return[2] error constant
-- @return[2] the err message
-- @field __comment replace _ with - the ldoc did not like me using - in the names
-- @field string basically does nothing but a type filed is required
-- @field string_inf same as string but is infinite in length, must be last arg
-- @field string_len same as string but can define a max length
-- @field number converts the input into a number
-- @field number_int converts the input to a number and floors it
-- @field number_range allows a number in a range min < X <= max
-- @field number_range allows a number in a range after it has been floored min < math.floor(X) <= max
-- @field player converts the input into a valid player
-- @field player_online converts the input to a player if the player is online
-- @field player_alive converts the input to a player if the player is online and alive
-- @field player_role converts the input to a player if the player is a lower rank than the user or if the person is not admin and the user is
-- @field player_role-online converts the input to a player if the player is a lower rank than the user and online
-- @field player_role_alive converts the input to a player if the player is a lower rank than the user and online and alive
commands.validate = {
['boolean']=function(value) value = value.lower() if value == 'true' or value == 'yes' or value == 'y' or value == '1' then return true else return false end end,
['string']=function(value) return tostring(value) end,
['string-inf']=function(value) return tostring(value) end,
['string-list']=function(value,event,list)
local rtn = tostring(value) and table.includes(list,tostring(value)) and tostring(value) or nil
if not rtn then return commands.error{'ExpGamingCore_Command.error-string-list',table.concat(list,', ')} end return rtn end,
['string-len']=function(value,event,max)
local rtn = tostring(value) and tostring(value):len() <= max and tostring(value) or nil
if not rtn then return commands.error{'ExpGamingCore_Command.error-string-len',max} end return rtn end,
['number']=function(value)
local rtn = tonumber(value) or nil
if not rtn then return commands.error{'ExpGamingCore_Command.error-number'} end return rtn end,
['number-int']=function(value)
local rtn = tonumber(value) and math.floor(tonumber(value)) or nil
if not rtn then return commands.error{'ExpGamingCore_Command.error-number'} end return rtn end,
['number-range']=function(value,event,min,max)
local rtn = tonumber(value) and tonumber(value) > min and tonumber(value) <= max and tonumber(value) or nil
if not rtn then return commands.error{'ExpGamingCore_Command.error-number-range',min,max} end return rtn end,
['number-range-int']=function(value,event,min,max)
local rtn = tonumber(value) and math.floor(tonumber(value)) > min and math.floor(tonumber(value)) <= max and math.floor(tonumber(value)) or nil
if not rtn then return commands.error{'ExpGamingCore_Command.error-number-range',min,max} end return rtn end,
['player']=function(value)
local rtn = Game.get_player(value) or nil
if not rtn then return commands.error{'ExpGamingCore_Command.error-player',value} end return rtn end,
['player-online']=function(value)
local player,err = commands.validate['player'](value)
if err then return commands.error(err) end
local rtn = player.connected and player or nil
if not rtn then return commands.error{'ExpGamingCore_Command.error-player-online'} end return rtn end,
['player-alive']=function(value)
local player,err = commands.validate['player-online'](value)
if err then return commands.error(err) end
local rtn = player.character and player.character.health > 0 and player or nil
if not rtn then return commands.error{'ExpGamingCore_Command.error-player-alive'} end return rtn end,
['player-rank']=function(value,event)
local player,err = commands.validate['player'](value)
if err then return commands.error(err) end
local rtn = player.admin and Game.get_player(event).admin and player or nil
if not rtn then return commands.error{'ExpGamingCore_Command.error-player-rank'} end return rtn end,
['player-rank-online']=function(value)
local player,err = commands.validate['player-online'](value)
if err then return commands.error(err) end
local player,err = commands.validate['player-rank'](player)
if err then return commands.error(err) end return player end,
['player-rank-alive']=function(value)
local player,err = commands.validate['player-alive'](value)
if err then return commands.error(err) end
local player,err = commands.validate['player-rank'](player)
if err then return commands.error(err) end return player end,
}
--- Adds a function to the validation list
-- @tparam string name the name of the validation
-- @tparam function callback function(value,event) which returns either the value to be used or commands.error{'error-message'}
function commands.add_validation(name,callback) if not is_type(callback,'function') then error('Callback is not a function',2) return end commands.validate[name]=callback end
--- Returns the inputs of this command as a formated string
-- @usage commands.format_inputs('interface') -- returns <code> (if you have ExpGamingCore.Server)
-- @tparam ?string|table|event command the command to get the inputs of
-- @treturn string the formated string for the inputs
function commands.format_inputs(command)
command = commands[command]
if not is_type(command,'table') then error('Command is not valid',2) end
local rtn = ''
for name,data in pairs(command.inputs) do
if data[1] == false then rtn=rtn..string.format('[%s] ',name)
else rtn=rtn..string.format('<%s> ',name) end
end
return rtn
end
--- Used to validate the arguments of a command, will understand strings with "" as a single param else spaces divede the params
-- @usage commands.validate_args(event) -- returns args table
-- @tparam table event this is the event created by add_command not on_console_command
-- @treturn[1] table the args for this command
-- @return[2] command.error
-- @treturn string the error that happend while parsing the args
function commands.validate_args(event)
local command = commands[event.name]
if not is_type(command,'table') then error('Command not valid',2) end
local rtn = {}
local count = 0
local count_opt = 0
for _,data in pairs(command.inputs) do count = count + 1 if data[1] == false then count_opt = count_opt + 1 end end
-- checks that there is some args given if there is meant to be
if not event.parameter then
if count == count_opt then return rtn
else return commands.error('invalid-inputs') end
end
-- splits the args into words so that it can be used to assign values
local words = string.split(event.parameter,' ')
local index = 0
for _,word in pairs(words) do
index = index+1
if not word then break end
local pos, _pos = word:find('"')
local hasSecond = pos and word:find('"',pos+1) or nil
while not hasSecond and pos and pos == _pos do
local next = table.remove(words,index+1)
if not next then return commands.error('invalid-parse') end
words[index] = words[index]..' '..next
_pos = words[index]:find('"',pos+1)
end
end
-- assigns the values from the words to the args
index = 0
for name,data in pairs(command.inputs) do
index = index+1
local arg = words[index]
if not arg and data[1] then return commands.error('invalid-inputs') end
if data[2] == 'string-inf' then rtn[name] = table.concat(words,' ',index) break end
local valid = is_type(data[2],'function') and data[2] or commands.validate[data[2]] or error('Invalid validation ("'..tostring(data[2])..'") for command: "'..command.name..'/'..name..'"')
local temp_tbl = table.deepcopy(data) table.remove(temp_tbl,1) table.remove(temp_tbl,1)
local value, err = valid(arg,event,unpack(temp_tbl))
if value == commands.error then return value, err end
rtn[name] = is_type(value,'string') and value:gsub('"','') or value
end
return rtn
end
--- Used to return all the commands a player can use
-- @usage get_commands(1) -- return table of command data for each command that the player can use
-- @tparam ?index|name|player| player the player to test as
-- @treturn table a table containg all the commands the player can use
function commands.get_commands(player)
player = Game.get_player(player)
local commands = {}
if not player then return error('Invalid player',2) end
for name,data in pairs(commandDataStore) do
if #middleware > 0 then for _,callback in pairs(middleware) do
local success, err = pcall(callback,player,name,data)
if not success then error(err)
elseif err then table.insert(commands,data) end
end elseif data.default_admin_only == true and player.admin then table.insert(commands,data) end
end
return commands
end
local function logMessage(player_name,command,message,args)
game.write_file('commands.log',
game.tick
..' Player: "'..player_name..'"'
..' '..message..': "'..command.name..'"'
..' With args of: '..table.tostring(args)
..'\n'
, true, 0)
end
--- Used to call the custom commands
-- @usage You dont its an internal command
-- @tparam table command the event rasied by the command
local function run_custom_command(command)
local data = commands.data[command.name]
local player = Game.get_player(command) or SERVER
-- runs all middle ware if any, if there is no middle where then it relies on .default_admin_only
if #middleware > 0 then for _,callback in pairs(middleware) do
local success, err = pcall(callback,player,command.name,command)
if not success then error(err)
elseif not err then
player_return({'ExpGamingCore_Command.command-fail',{'ExpGamingCore_Command.unauthorized'}},defines.textcolor.crit)
logMessage(player.name,command,'Failed to use command (Unauthorized)',commands.validate_args(command))
game.player.play_sound{path='utility/cannot_build'}
return
end
end elseif data.default_admin_only == true and player and not player.admin then
player_return({'ExpGamingCore_Command.command-fail',{'ExpGamingCore_Command.unauthorized'}},defines.textcolor.crit)
logMessage(player.name,command,'Failed to use command (Unauthorized)',commands.validate_args(command))
game.player.play_sound{path='utility/cannot_build'}
return
end
-- gets the args for the command
local args, err = commands.validate_args(command)
if args == commands.error then
if is_type(err,'table') then table.insert(err,command.name) table.insert(err,commands.format_inputs(data))
player_return({'ExpGamingCore_Command.command-fail',err},defines.textcolor.high) else player_return({'ExpGamingCore_Command.command-fail',{'ExpGamingCore_Command.invalid-inputs',command.name,commands.format_inputs(data)}},defines.textcolor.high) end
logMessage(player.name,command,'Failed to use command (Invalid Args)',args)
player.play_sound{path='utility/deconstruct_big'}
return
end
-- runs the command
local success, err = pcall(data.callback,command,args)
if not success then error(err) end
if err ~= commands.error then player_return({'ExpGamingCore_Command.command-ran'},defines.textcolor.info) end
logMessage(player.name,command,'Used command',args)
end
--- Used to define commands
-- @usage --see examples in file
-- @tparam string name the name of the command
-- @tparam[opt='No Description'] string description the description of the command
-- @tparam[opt=an infinite string] table inputs a table of the inputs to be used, last index being true makes the last parameter open ended (longer than one word)
-- @tparam function callback the function to call on the event
commands.add_command = function(name, description, inputs, callback)
if commands[name] then error('That command is already registered',2) end
if not is_type(name,'string') then error('Command name has not been given') end
if not is_type(callback,'function') or not is_type(inputs,'table') then
if is_type(inputs,'function') then commands._add_command(name,description,inputs)
else error('Invalid args given to add_command') end
end
verbose('Created Command: '..name)
-- test for string and then test for locale string
description = is_type(description,'string') and description
or is_type(description,'table') and is_type(description[1],'string') and string.find(description[1],'.+[.].+') and {description,''}
or 'No Description'
inputs = is_type(inputs,'table') and inputs or {['param']={false,'string-inf'}}
commandDataStore[name] = {
name=name,
description=description,
inputs=inputs,
callback=callback,
admin_only=false
}
local help = is_type(description,'string') and commands.format_inputs(name)..'- '..description
or is_type(description,'table') and is_type(description[1],'string') and string.find(description[1],'.+[.].+') and {description,commands.format_inputs(name)..'- '}
or commands.format_inputs(name)
commandDataStore[name].help = help
commands._add_command(name,help,function(...)
local success, err = Manager.sandbox(run_custom_command,{},...)
if not success then error(err) end
end)
return commandDataStore[name]
end
return commands
--[[
command example
**locale file**
[foo]
description=__1__ this is a command
**control.lua**
commands.add_command('foo',{'foo.description'},{
['player']={true,'player'}, -- a required arg that must be a valid player
['number']={true,'number-range',0,10}, -- a required arg that must be a number 0<X<=10
['pwd']={true,function(value,event) if value == 'password123' then return true else return commands.error('Invalid Password') end} -- a required arg pwd that has custom validation
['reason']={false,'string-inf'} -- an optional arg that is and infinite length (useful for reasons)
},function(event,args)
args.player.print(args.number)
if args.reasons then args.player.print(args.reason) end
end)
]]

View File

@@ -0,0 +1,21 @@
{
"name": "ExpGamingCore.Command",
"version": "4.0.0",
"description": "A better command handler than the base game.",
"location": "FSM_ARCHIVE",
"keywords": [
"Library",
"Lib",
"ExpGaming",
"Core",
"Commands"
],
"dependencies": {
"ExpGamingLib": "^4.0.0",
"FactorioStdLib.Table": "^0.8.0",
"FactorioStdLib.Color": "^0.8.0",
"FactorioStdLib.Game": "^0.8.0"
},
"collection": "ExpGamingCore@4.0.0",
"submodules": {}
}

View File

@@ -0,0 +1,81 @@
-- defines for groups
Group{
name='Admin',
disallow={
'edit_permission_group',
'delete_permission_group',
'add_permission_group'
}
}
Group{
name='HiMember',
disallow={
'edit_permission_group',
'delete_permission_group',
'add_permission_group'
}
}
Group{
name='Member',
disallow={
'edit_permission_group',
'delete_permission_group',
'add_permission_group',
'set_auto_launch_rocket',
'change_programmable_speaker_alert_parameters',
'drop_item'
}
}
Group{
name='User',
disallow={
'edit_permission_group',
'delete_permission_group',
'add_permission_group',
'set_auto_launch_rocket',
'change_programmable_speaker_alert_parameters',
'drop_item',
'build_terrain',
'remove_cables',
'launch_rocket',
'reset_assembling_machine',
'cancel_research'
}
}
Group{
name='Jail',
disallow={
'set_allow_commands',
'edit_permission_group',
'delete_permission_group',
'add_permission_group',
'open_character_gui',
'begin_mining',
'start_walking',
'player_leave_game',
'open_blueprint_library_gui',
'build_item',
'use_item',
'select_item',
'rotate_entity',
'open_train_gui',
'open_train_station_gui',
'open_gui',
'open_item',
'deconstruct',
'build_rail',
'cancel_research',
'start_research',
'set_train_stopped',
'select_gun',
'open_technology_gui',
'open_trains_gui',
'edit_custom_tag',
'craft',
'setup_assembling_machine',
}
}

View File

@@ -0,0 +1,189 @@
--- Adds a system to manage and auto-create permission groups.
-- @module ExpGamingCore@Group
-- @author Cooldude2606
-- @license https://github.com/explosivegaming/scenario/blob/master/LICENSE
-- @alias Group
-- Module Require
local Game = require('FactorioStdLib.Game')
-- Module Define
local module_verbose = false
--- Used as an interface for factorio permissions groups
-- @type Group
-- @field _prototype the prototype of this class
-- @field groups a table of all groups, includes auto complete on the indexing
local _GroupSelfRef
local Group = {
_prototype = {},
groups = setmetatable({},{
__index=table.autokey,
__newindex=function(tbl,key,value)
rawset(tbl,key,_GroupSelfRef.define(value))
end
}),
on_init = function()
if loaded_modules['ExpGamingCore.Server'] then require('ExpGamingCore.Server') end
end,
on_post = function(self)
-- creates a root role that the server can use
self{name='Root',disallow={}}
-- loads the groups in config
require(module_path..'/config',{Group=self})
end
}
_GroupSelfRef=Group
-- Function Define
--- Defines a new instance of a group
-- @usage Group.define{name='foo',disallow={'edit_permission_group','delete_permission_group','add_permission_group'}} -- returns new group
-- @usage Group{name='foo',disallow={'edit_permission_group','delete_permission_group','add_permission_group'}} -- returns new group
-- @tparam table obj contains string name and table disallow of defines.input_action
-- @treturn Group the group which has been made
function Group.define(obj)
if not type_error(game,nil,'Cant define Group during runtime.') then return end
if not type_error(obj.name,'string','Group creation is invalid: group.name is not a string') then return end
if not type_error(obj.disallow,'table','Group creation is invalid: group.disallow is not a table') then return end
verbose('Created Group: '..obj.name)
setmetatable(obj,{__index=function(tbl,key) return Group._prototype[key] or game and game.permissions.get_group(tbl.name)[key] or nil end})
obj.connected_players = setmetatable({self=obj},Group._prototype.connected_players_mt)
rawset(Group.groups,obj.name,obj)
return obj
end
--- Used to get the group of a player or the group by name
-- @usage Group.get('foo') -- returns group foo
-- @usage Group.get(player) -- returns group of player
-- @tparam ?LuaPlayer|pointerToPlayer|string mixed can either be the name or raw group of a group or a player indenifier
-- @treturn table the group which was found or nil
function Group.get(mixed)
if is_type(mixed,'table') and mixed.name then mixed = mixed.name end
if game and Game.get_player(mixed) then mixed = Game.get_player(mixed).permission_group.name end
local rtn = Group.groups[mixed]
if not rtn and game and is_type(mixed,'string') and game.permissions.get_group(mixed) then
rtn = setmetatable({disallow={},name=mixed},{
__index=function(tbl,key) return Group._prototype[key] or game and game.permissions.get_group(tbl.name)[key] or nil end
})
rtn.connected_players = setmetatable({self=rtn},Group._prototype.connected_players_mt)
end
return rtn
end
--- Used to place a player into a group
-- @usage Group.assign(player,group)
-- @tparam ?LuaPlayer|pointerToPlayer player the player to assign the group to
-- @tparam ?string|LuaPermissionGroup the group to add the player to
-- @treturn boolean was the player assigned
function Group.assign(player,group)
player = Game.get_player(player)
if not player then error('Invalid player #1 given to Group.assign.',2) return end
group = Group.get(group)
if not group then error('Invalid group #2 given to Group.assign.',2) return end
return group:add_player(player)
end
--- Used to get the factorio permission group linked to this group
-- @usage group:get_raw() -- returns LuaPermissionGroup of this group
-- @treturn LuaPermissionGroup the factorio group linked to this group
function Group._prototype:get_raw()
if not self_test(self,'group','get_raw') then return end
local _group = game.permissions.get_group(self.name)
if not _group or _group.valid == false then error('No permissions group found, please to not remove groups with /permissions',2) return end
return setmetatable({},{__index=_group})
end
--- Used to add a player to this group
-- @usage group:add_player(player) -- returns true if added
-- @tparam ?LuaPlayer|pointerToPlayer player the player to add to the group
-- @treturn boolean if the player was added
function Group._prototype:add_player(player)
if not self_test(self,'group','add_player') then return end
player = Game.get_player(player)
if not player then error('Invalid player #1 given to group.add_player.',2) return end
local raw_group = self:get_raw()
return raw_group.add_player(player)
end
--- Used to remove a player from this group
-- @usage group:remove_player(player) -- returns true if removed
-- @tparam ?LuaPlayer|pointerToPlayer player the player to remove from the group
-- @treturn boolean if the player was removed
function Group._prototype:remove_player(player)
if not self_test(self,'group','remove_player') then return end
player = Game.get_player(player)
if not player then error('Invalid player #1 given to group.remove_player.',2) return end
local raw_group = self:get_raw()
return raw_group.remove_player(player)
end
--- Gets all players in this group
-- @usage group:get_players() -- returns table of players
-- @usage group.players -- returns table of players
-- @usage group.connected_players -- returns table of online players
-- @tparam[opt=false] boolean online if true returns only online players
-- @treturn table table of players
function Group._prototype:get_players(online)
if not self_test(self,'group','get_players') then return end
if online and not type_error(online,'boolean','Invalid argument #1 to group:get_players, online is not a boolean.') then return end
local raw_group = self:get_raw()
local rtn = {}
if online then for _,player in pairs(raw_group.players) do if player.connected then table.insert(rtn,player) end end end
return online and rtn or raw_group.players
end
-- this is used to create a connected_players table
Group._prototype.connected_players_mt = {
__call=function(tbl) return tbl.self:get_players(true) end,
__pairs=function(self)
local players = self.self:get_players(true)
local function next_pair(tbl,key)
local k, v = next(players, key)
if v then return k,v end
end
return next_pair, players, nil
end,
__ipairs=function(self)
local players = self.self:get_players(true)
local function next_pair(tbl,key)
local k, v = next(players, key)
if v then return k,v end
end
return next_pair, players, nil
end
}
--- Prints a message or value to all online players in this group
-- @usage group.print('Hello, World!')
-- @param rtn any value you wish to print, string not required
-- @param colour the colour to print the message in
-- @treturn number the number of players who received the message
function Group._prototype:print(rtn,colour)
if not self_test(self,'group','print') then return end
if colour and not type_error(colour,'table','Invalid argument #2 to group:print, colour is not a table.') then return end
local players = self:get_players()
local ctn = 0
for _,player in pairs(players) do if player.connected then player_return(rtn,colour,player) ctn=ctn+1 end end
return ctn
end
-- Event Handlers Define
-- creates all permission groups and links them
Event.add('on_init',function()
for name,group in pairs(Group.groups) do
local _group = game.permissions.create_group(name)
verbose('Created Permission Group: '..name)
local count = 0
for _,to_remove in pairs(group.disallow) do
count=count+1
_group.set_allows_action(defines.input_action[to_remove],false)
end
verbose('Disalowed '..count..' input actions.')
end
end)
-- Module Return
-- calling will attempt to define a new group
return setmetatable(Group,{__call=function(tbl,...) tbl.define(...) end})

View File

@@ -0,0 +1,22 @@
{
"name": "ExpGamingCore.Group",
"version": "4.0.0",
"description": "Adds a system to manage and auto-create permission groups.",
"location": "FSM_ARCHIVE",
"keywords": [
"Groups",
"ExpGaming",
"System",
"Management",
"Manage",
"Permissions"
],
"dependencies": {
"FactorioStdLib": "^0.8.0",
"ExpGamingCore.Server": "?^4.0.0",
"ExpGamingLib": "^4.0.0",
"FactorioStdLib.Game": "^0.8.0"
},
"collection": "ExpGamingCore@4.0.0",
"submodules": {}
}

View File

@@ -0,0 +1,221 @@
--- Adds a uniform preset for guis in the center of the screen which allow for different tabs to be opened
-- @module ExpGamingCore.Gui.center
-- @alias center
-- @author Cooldude2606
-- @license https://github.com/explosivegaming/scenario/blob/master/LICENSE
local Game = require('FactorioStdLib.Game')
local Color = require('FactorioStdLib.Color')
local Gui = require('ExpGamingCore.Gui')
local mod_gui = require('mod-gui')
local center = {}
center._prototype = {}
--- Adds a new obj to the center gui
-- @usage Gui.center.add{name='foo',caption='Foo',tooltip='Testing',draw=function}
-- @usage return_value(player) -- opens the center gui for that player
-- @param obj contains the new object, needs name, frame is opt and is function(root_frame)
-- @return the object made, used to add tabs, calling the returned value will open the center for the given player
function center.add(obj)
if not is_type(obj,'table') then return end
if not is_type(obj.name,'string') then return end
verbose('Created Center Gui: '..obj.name)
setmetatable(obj,{__index=center._prototype,__call=function(self,player,...) return center.open(player,self.name,...) end})
obj.tabs = {}
obj._tabs = {}
Gui.data('center',obj.name,obj)
if Gui.toolbar then Gui.toolbar(obj.name,obj.caption,obj.tooltip,function(event) return obj:open(event.player_index) end) end
return obj
end
--- Used to get the center frame of the player, used mainly in script
-- @usage Gui.center.get_flow(player) -- returns gui element
-- @param player a player identifier to get the flow for
-- @treturn table the gui element flow
function center.get_flow(player)
player = Game.get_player(player)
if not player then error('Invalid player',2) end
return player.gui.center.exp_center or player.gui.center.add{name='exp_center',type='flow'}
end
--- Used to open a center frame for a player, extra params are sent to open event
-- @usage Gui.center.open(player,'server-info') -- return true
-- @param player a player identifier to get the flow for
-- @tparam string center_name the name of the center frame to open
-- @treturn boolean based on if it succeeded or not
function center.open(player,center_name,...)
player = Game.get_player(player)
if not player then error('Invalid player',2) return false end
Gui.center.clear(player)
if not Gui.data.center[center_name] then return false end
local self = Gui.data.center[center_name]
-- this function is the draw function passed to the open event
self:open(player,function(...) Gui.center._draw(self,...) end,...)
return true
end
-- used as a piece of middle ware for the open event
function center._draw(self,frame,...)
game.players[frame.player_index].opened=frame
if is_type(self.draw,'function') then
local success, err = pcall(self.draw,self,frame,...)
if not success then error(err) end
else error('No Callback on center frame '..self.name) end
end
--- Used to open a center frame for a player
-- @usage Gui.center.open_tab(player,'readme','rules') -- return true
-- @param player a player identifier to get the flow for
-- @tparam string center the name of the center frame to open
-- @tparam string tab the name of the tab to open
-- @treturn boolean based on if it succeeded or not
function center.open_tab(player,center_name,tab)
player = Game.get_player(player)
if not player then error('Invalid player',2) end
if not Gui.center.open(player,center_name) then return false end
local name = center_name..'_'..tab
if not Gui.data.inputs_button[name] then return false end
Gui.data.inputs_button[name].events[defines.events.on_gui_click]{
element=Gui.center.get_flow(player)[center_name].tab_bar.tab_bar_scroll.tab_bar_scroll_flow[name],
}
return true
end
--- Used to clear the center frame of the player, used mainly in script
-- @usage Gui.center.clear(player)
-- @param player a player identifier to get the flow for
function center.clear(player)
player = Game.get_player(player)
center.get_flow(player).clear()
end
-- opens this gui for this player, draw is the draw function when event is called from center.open
-- this is the default function it can be overridden when the gui is defined, simply call draw on the frame you create
-- extra values passed to draw will also be passed to the draw event
-- extra values from center.draw and passed to the open event
function center._prototype:open(player,draw,...)
player = Game.get_player(player)
draw = draw or function(...) center._draw(self,...) end
local center_flow = center.get_flow(player)
if center_flow[self.name] then Gui.center.clear(player) return end
local center_frame = center_flow.add{
name=self.name,
type='frame',
caption=self.caption,
direction='vertical',
style=mod_gui.frame_style
}
if is_type(center_frame.caption,'string') and player.gui.is_valid_sprite_path(center_frame.caption) then center_frame.caption = '' end
draw(center_frame,...)
end
-- this is the default draw function if one is not provided, can be overridden
-- not recommended for direct use see Gui.center.open
function center._prototype:draw(frame)
Gui.bar(frame,510)
local tab_bar = frame.add{
type='frame',
name='tab_bar',
style='image_frame',
direction='vertical'
}
tab_bar.style.width = 510
tab_bar.style.height = 65
local tab_bar_scroll = tab_bar.add{
type='scroll-pane',
name='tab_bar_scroll',
horizontal_scroll_policy='auto-and-reserve-space',
vertical_scroll_policy='never'
}
tab_bar_scroll.style.vertically_squashable = false
tab_bar_scroll.style.vertically_stretchable = true
tab_bar_scroll.style.width = 500
local tab_bar_scroll_flow = tab_bar_scroll.add{
type='flow',
name='tab_bar_scroll_flow',
direction='horizontal'
}
Gui.bar(frame,510)
local tab = frame.add{
type ='frame',
name='tab',
direction='vertical',
style='image_frame'
}
tab.style.width = 510
tab.style.height = 305
local tab_scroll = tab.add{
type ='scroll-pane',
name='tab_scroll',
horizontal_scroll_policy='never',
vertical_scroll_policy='auto'
}
tab_scroll.style.vertically_squashable = false
tab_scroll.style.vertically_stretchable = true
tab_scroll.style.width = 500
local tab_scroll_flow = tab_scroll.add{
type='flow',
name='tab_scroll_flow',
direction='vertical'
}
tab_scroll_flow.style.width = 480
Gui.bar(frame,510)
local first_tab = nil
for name,button in pairs(self.tabs) do
first_tab = first_tab or name
button(tab_bar_scroll_flow).style.font_color = defines.color.white
end
self._tabs[self.name..'_'..first_tab](tab_scroll_flow)
tab_bar_scroll_flow.children[1].style.font_color = defines.color.orange
frame.parent.add{type='frame',name='temp'}.destroy()--recenter the GUI
end
--- If default draw is used then you can add tabs to the gui with this function
-- @usage _center:add_tab('foo','Foo','Just a tab',function)
-- @tparam string name this is the name of the tab
-- @tparam string caption this is the words that appear on the tab button
-- @tparam[opt] string tooltip the tooltip that is on the button
-- @tparam function callback this is called when button is pressed with function(root_frame)
-- @return self to allow chaining of _center:add_tab
function center._prototype:add_tab(name,caption,tooltip,callback)
verbose('Created Tab: '..self.name..'/'..name)
self._tabs[self.name..'_'..name] = callback
self.tabs[name] = Gui.inputs.add{
type='button',
name=self.name..'_'..name,
caption=caption,
tooltip=tooltip
}:on_event('click',function(event)
local tab = event.element.parent.parent.parent.parent.tab.tab_scroll.tab_scroll_flow
tab.clear()
local frame_name = tab.parent.parent.parent.name
local _center = Gui.data.center[frame_name]
local _tab = _center._tabs[event.element.name]
if is_type(_tab,'function') then
for _,button in pairs(event.element.parent.children) do
if button.name == event.element.name then
button.style.font_color = defines.color.orange
else
button.style.font_color = defines.color.white
end
end
local success, err = pcall(_tab,tab)
if not success then error(err) end
end
end)
return self
end
-- used so that when gui close key is pressed this will close the gui
Event.add(defines.events.on_gui_closed,function(event)
if event.element and event.element.valid then event.element.destroy() end
end)
Event.add(defines.events.on_player_respawned,center.clear)
function center.on_init()
if loaded_modules['ExpGamingCore.Role'] then Event.add(defines.events.on_role_change,center.clear) end
end
-- calling will attempt to add a new gui
return setmetatable(center,{__call=function(self,...) return self.add(...) end})

View File

@@ -0,0 +1,24 @@
{
"name": "ExpGamingCore.Gui.center",
"version": "4.0.0",
"description": "Adds a pre-made center gui format.",
"author": "Cooldude2606",
"contact": "Discord: Cooldude2606#5241",
"license": "https://github.com/explosivegaming/scenario/blob/master/LICENSE",
"location": "FSM_ARCHIVE",
"keywords": [
"Gui",
"Center"
],
"collection": "ExpGamingCore.Gui@4.0.0",
"dependencies": {
"mod-gui": "*",
"FactorioStdLib.Game": "^0.8.0",
"FactorioStdLib.Color": "^0.8.0",
"ExpGamingCore.Role": "?^4.0.0",
"ExpGamingCore.Gui": "^4.0.0",
"ExpGamingCore.Gui.inputs": "^4.0.0",
"ExpGamingCore.Gui.toolbar": "?^4.0.0"
},
"submodules": {}
}

View File

@@ -0,0 +1,190 @@
--- Adds a objective version to custom guis.
-- @module ExpGamingCore.Gui
-- @alias Gui
-- @author Cooldude2606
-- @license https://github.com/explosivegaming/scenario/blob/master/LICENSE
local Game = require('FactorioStdLib.Game')
local Color = require('FactorioStdLib.Color')
local Server -- ExpGamingCore.Server@?^4.0.0
local Gui = {}
local global = {}
Global.register(global,function(tbl) global = tbl end)
--- Used to set and get data about different guis
-- @usage Gui.data[location] -- returns the gui data for that gui location ex center
-- @usage Gui.data(location,gui_name,gui_data) -- adds gui data for a gui at a location
-- @tparam string location the location to get/set the data, center left etc...
-- @tparam[opt] string key the name of the gui to set the value of
-- @param[opt] value the data that will be set can be any value but table advised
-- @treturn[1] table all the gui data that is located in that location
Gui.data = setmetatable({},{
__call=function(tbl,location,key,value)
if not location then return tbl end
if not key then return rawget(tbl,location) or rawset(tbl,location,{}) and rawget(tbl,location) end
if game then error('New guis cannot be added during runtime',2) end
if not rawget(tbl,location) then rawset(tbl,location,{}) end
rawset(rawget(tbl,location),key,value)
end
})
--- Add a white bar to any gui frame
-- @usage Gui.bar(frame,100)
-- @param frame the frame to draw the line to
-- @param[opt=10] width the width of the bar
-- @return the line that was made type is progress bar
function Gui.bar(frame,width)
local line = frame.add{
type='progressbar',
size=1,
value=1
}
line.style.height = 3
line.style.width = width or 10
line.style.color = defines.color.white
return line
end
--- Adds a label that is centered
-- @usage Gui.centered_label(frame, 'Hello, world!')
-- @tparam LuaGuiElement frame the parent frame to add the label to
-- @tparam string string the string that the label will have
function Gui.centered_label(frame, string)
local flow = frame.add {frame = 'flow'}
local flow_style = flow.style
flow_style.align = 'center'
flow_style.horizontally_stretchable = true
local label = flow.add {type = 'label', caption = string}
local label_style = label.style
label_style.align = 'center'
label_style.single_line = false
return label
end
--- Used to set the index of a drop down to a certain item
-- @usage Gui.set_dropdown_index(dropdown,player.name) -- will select the index with the players name as the value
-- @param dropdown the dropdown that is to be effected
-- @param _item this is the item to look for
-- @return returns the dropdown if it was successful
function Gui.set_dropdown_index(dropdown,_item)
if not dropdown or not dropdown.valid or not dropdown.items or not _item then return end
local _index = 1
for index, item in pairs(dropdown.items) do
if item == _item then _index = index break end
end
dropdown.selected_index = _index
return dropdown
end
--- Prams for Gui.cam_link
-- @table ParametersForCamLink
-- @field entity this is the entity that the camera will follow
-- @field cam a camera that you already have in the gui
-- @field frame the frame to add the camera to, no effect if cam param is given
-- @field zoom the zoom to give the new camera
-- @field width the width to give the new camera
-- @field height the height to give the new camera
-- @field surface this will over ride the surface that the camera follows on, allowing for a 'ghost surface' while keeping same position
-- @field respawn_open if set to true then the camera will auto re link to the player after a respawn
--- Adds a camera that updates every tick (or less depending on how many are opening) it will move to follow an entity
-- @usage Gui.cam_link{entity=game.player.character,frame=frame,width=50,hight=50,zoom=1}
-- @usage Gui.cam_link{entity=game.player.character,cam=frame.camera,surface=game.surfaces['testing']}
-- @tparam table data contains all other params given below
-- @return the camera that the function used be it made or given as a param
function Gui.cam_link(data)
if not data.entity or not data.entity.valid then return end
if is_type(data.cam,'table') and data.cam.__self and data.cam.valid then
data.cam = data.cam
elseif data.frame then
data.cam={}
data.cam.type='camera'
data.cam.name='camera'
data.cam.position= data.entity.position
data.cam.surface_index= data.surface and data.surface.index or data.entity.surface.index
data.cam.zoom = data.zoom
data.cam = data.frame.add(data.cam)
data.cam.style.width = data.width or 100
data.cam.style.height = data.height or 100
else return end
if not Server or not Server.get_thread('camera-follow') then
if not global.cams then
global.cams = {}
global.cam_index = 1
end
if data.cam then
local surface = data.surface and data.surface.index or nil
table.insert(global.cams,{cam=data.cam,entity=data.entity,surface=surface})
end
if not global.players then
global.players = {}
end
if data.respawn_open then
if data.entity.player then
if not global.players[data.entity.player.index] then global.players[data.entity.player.index] = {} end
table.insert(global.players[data.entity.player.index],data.cam)
end
end
else
local thread = Server.get_thread('camera-follow')
local surface = data.surface and data.surface.index or nil
table.insert(thread.data.cams,{cam=data.cam,entity=data.entity,surface=surface})
if data.respawn_open then
if data.entity.player then
if not thread.data.players[data.entity.player.index] then thread.data.players[data.entity.player.index] = {} end
table.insert(thread.data.players[data.entity.player.index],data.cam)
end
end
end
return data.cam
end
Event.add('on_tick', function(event)
if loaded_modules['ExpGamingCore.Server'] then return end
if global.cams and is_type(global.cams,'table') and #global.cams > 0 then
local update = 4
if global.cam_index >= #global.cams then global.cam_index = 1 end
if update > #global.cams then update = #global.cams end
for cam_offset = 0,update do
local _cam = global.cams[global.cam_index+cam_offset]
if not _cam then break end
if not _cam.cam.valid then table.remove(global.cams,global.cam_index)
elseif not _cam.entity.valid then table.remove(global.cams,global.cam_index)
else _cam.cam.position = _cam.entity.position if not _cam.surface then _cam.cam.surface_index = _cam.entity.surface.index end global.cam_index = global.cam_index+1
end
end
global.cam_index = global.cam_index+update
end
end)
Event.add('on_player_respawned',function(event)
if loaded_modules['ExpGamingCore.Server'] then return end
if global.players and is_type(global.players,'table') and #global.players > 0 and global.players[event.player_index] then
local remove = {}
local player = Game.get_player(event)
for index,cam in pairs(global.players[event.player_index]) do
if cam.valid then table.insert(global.cams,{cam=cam,entity=player.character,surface=player.surface})
else table.insert(remove,index) end
end
for n,index in pairs(remove) do
table.remove(global.players[event.player_index],index-n+1)
end
end
end)
function Gui:on_init()
if loaded_modules['ExpGamingCore.Server'] then
Server = require('ExpGamingCore.Server')
verbose('ExpGamingCore.Server is installed; Loading server src')
script.on_init(require(module_path..'/src/server',{Gui=self}))
end
end
function Gui.on_post()
Gui.test = require(module_path..'/src/test',{Gui=Gui})
end
return Gui

View File

@@ -0,0 +1,375 @@
--- Adds a clean way of making new inputs for a gui allowing for sliders and text inputs to be hanndleded with custom events
-- @module ExpGamingCore.Gui.Inputs
-- @alias inputs
-- @author Cooldude2606
-- @license https://github.com/explosivegaming/scenario/blob/master/LICENSE
--- This is a submodule of ExpGamingCore.Gui but for ldoc reasons it is under its own module
-- @function _comment
local Game = require('FactorioStdLib.Game')
local Color = require('FactorioStdLib.Color')
local mod_gui = require('mod-gui')
local Gui = require('ExpGamingCore.Gui')
local inputs = {}
inputs._prototype = {}
-- these are just so you can have short cuts to this
inputs.events = {
--error={}, -- this is added after event calls are added as it is not a script event
state=defines.events.on_gui_checked_state_changed,
click=defines.events.on_gui_click,
elem=defines.events.on_gui_elem_changed,
selection=defines.events.on_gui_selection_state_changed,
text=defines.events.on_gui_text_changed,
slider=defines.events.on_gui_value_changed
}
--- Sets the input to trigger on an certain event
-- @usage button:on_event(defines.events.on_gui_click,player_return)
-- @param event the event to raise callback on | can be number of the event | can be a key of inputs.events
-- @tparam function callback the function you want to run on the event
-- @treturn table returns self so you can chain together
function inputs._prototype:on_event(event,callback)
if not is_type(callback,'function') then return self end
if inputs.events[event] then event = inputs.events[event] end
if event == inputs.events.error then self._error = callback return self end
self.events[event] = callback
return self
end
--- Draw the input into the root element
-- @usage button:draw(frame)
-- @param root the element you want to add the input to
-- @return returns the element that was added
function inputs._prototype:draw(root)
local player = Game.get_player(root.player_index)
if is_type(self.draw_data.caption,'string') and player.gui.is_valid_sprite_path(self.draw_data.caption) then
local data = table.deepcopy(self.draw_data)
data.type = 'sprite-button'
data.sprite = data.caption
data.caption = nil
return root.add(data)
elseif is_type(self.draw_data.sprite,'string') and player.gui.is_valid_sprite_path(self.draw_data.sprite) then
local data = table.deepcopy(self.draw_data)
data.type = 'sprite-button'
return root.add(data)
elseif is_type(self.data._state,'function') then
local data = table.deepcopy(self.draw_data)
local success, err = pcall(self.data._state,player,root)
if success then data.state = err else error(err) end
return root.add(data)
elseif is_type(self.data._start,'function') then
local data = table.deepcopy(self.draw_data)
local success, err = pcall(self.data._start,player,root)
if success then data.value = err else error(err) end
return root.add(data)
elseif is_type(self.data._index,'function') then
local data = table.deepcopy(self.draw_data)
local success, err = pcall(self.data._index,player,root)
if success then data.selected_index = err else error(err) end
if is_type(self.data._items,'function') then
local success, err = pcall(self.data._items,player,root)
if success then data.items = err else error(err) end
end
return root.add(data)
elseif is_type(self.data._items,'function') then
local data = table.deepcopy(self.draw_data)
local success, err = pcall(self.data._items,player,root)
if success then data.items = err else error(err) end
if is_type(self.data._index,'function') then
local _success, _err = pcall(self.data._index,player,root)
if _success then data.selected_index = _err else error(_err) end
end
return root.add(data)
else
return root.add(self.draw_data)
end
end
--- Add a new input, this is the same as doing frame.add{} but returns a different object
-- @usage Gui.inputs.add{type='button',name='test',caption='Test'}
-- @usage return_value(frame) -- draws the button onto that frame
-- @tparam table obj the new element to add if caption is a sprite path then sprite is used
-- @treturn table the custom input object, calling the returned value will draw the button
function inputs.add(obj)
if not is_type(obj,'table') then return end
if not is_type(obj.type,'string') then return end
local type = obj.type
if type ~= 'button'
and type ~= 'sprite-button'
and type ~= 'choose-elem-button'
and type ~= 'checkbox'
and type ~= 'radiobutton'
and type ~= 'textfield'
and type ~= 'text-box'
and type ~= 'slider'
and type ~= 'drop-down'
then return end
verbose('Created Input: '..obj.name..' ('..obj.type..')')
if obj.type == 'button' or obj.type == 'sprite-button' then obj.style = mod_gui.button_style end
obj.draw_data = table.deepcopy(obj)
obj.data = {}
obj.events = {}
setmetatable(obj,{__index=inputs._prototype,__call=function(self,...) return self:draw(...) end})
Gui.data('inputs_'..type,obj.name,obj)
return obj
end
-- this just runs the events given to inputs
function inputs._event_handler(event)
if not event.element then return end
local elements = Gui.data['inputs_'..event.element.type] or {}
local element = elements[event.element.name]
if not element and event.element.type == 'sprite-button' then
elements = Gui.data.inputs_button or {}
element = elements[event.element.name]
end
if element then
verbose('There was a gui event ('..Event.names[event.name]..') with element: '..event.element.name)
if not is_type(element.events[event.name],'function') then return end
local success, err = Manager.sandbox(element.events[event.name],{},event)
if not success then
if is_type(element._error,'function') then pcall(element._error)
else error(err) end
end
end
end
Event.add(inputs.events,inputs._event_handler)
inputs.events.error = {}
-- the following functions are just to make inputs easier but if what you want is not include use inputs.add(obj)
--- Used to define a button, can have many function
-- @usage Gui.inputs.add_button('test','Test','Just for testing',{{condition,callback},...})
-- @tparam string name the name of this button
-- @tparam string the display for this button, either text or sprite path
-- @tparam string tooltip the tooltip to show on the button
-- @param callbacks can either be a single function or a list of function pairs see examples at bottom
-- @treturn table the button object that was made, to allow a custom error event if wanted
function inputs.add_button(name,display,tooltip,callbacks)
local rtn_button = inputs.add{
type='button',
name=name,
caption=display,
tooltip=tooltip
}
rtn_button.data._callbacks = callbacks
rtn_button:on_event('click',function(event)
local elements = Gui.data['inputs_'..event.element.type] or {}
local button = elements[event.element.name]
if not button and event.element.type == 'sprite-button' then
elements = Gui.data.inputs_button or {}
button = elements[event.element.name]
end
local player = Game.get_player(event)
local mouse = event.button
local keys = {alt=event.alt,ctrl=event.control,shift=event.shift}
local element = event.element
local btn_callbacks = button.data._callbacks
if is_type(btn_callbacks,'function') then btn_callbacks = {{function() return true end,btn_callbacks}} end
for _,data in pairs(btn_callbacks) do
if is_type(data[1],'function') and is_type(data[2],'function') then
local success, err = pcall(data[1],player,mouse,keys,event)
if success and err == true then
local _success, _err = pcall(data[2],player,element,event)
if not _success then error(_err) end
elseif not success then error(err) end
else error('Invalid Callback Condition Format') end
end
end)
return rtn_button
end
--- Used to define a choose-elem-button callback only on elem_changed
-- @usage Gui.inputs.add_elem_button('test','Test','Just for testing',function)
-- @tparam string name the name of this button
-- @tparam string elem_type the display for this button, either text or sprite path
-- @tparam string tooltip the tooltip to show on the button
-- @tparam function callback the callback to call on change function(player,element,elem)
-- @treturn table the button object that was made, to allow a custom error event if wanted
function inputs.add_elem_button(name,elem_type,tooltip,callback)
local button = inputs.add{
type='choose-elem-button',
name=name,
elem_type=elem_type,
tooltip=tooltip
}
button.data._callback = callback
button:on_event('elem',function(event)
local button = Gui.data['inputs_'..event.element.type][event.element.name]
local player = Game.get_player(event)
local element = event.element or {elem_type=nil,elem_value=nil}
local elem = {type=element.elem_type,value=element.elem_value}
if is_type(button.data._callback,'function') then
local success, err = pcall(button.data._callback,player,element,elem)
if not success then error(err) end
else error('Invalid Callback') end
end)
return button
end
--- Used to define a checkbox callback only on state_changed
-- @usage Gui.inputs.add_checkbox('test',false,'Just for testing',function,function,funvtion)
-- @tparam string name the name of this button
-- @tparam boolean radio if this is a radio button
-- @tparam string display the display for this button, either text or sprite path
-- @tparam function default the callback which choses the default check state
-- @tparam function callback_true the callback to call when changed to true
-- @tparam function callback_false the callback to call when changed to false
-- @treturn table the button object that was made, to allow a custom error event if wanted
function inputs.add_checkbox(name,radio,display,default,callback_true,callback_false)
local type = 'checkbox'; if radio then type='radiobutton' end
local state = false; if is_type(default,'boolean') then state = default end
local rtn_checkbox = inputs.add{
type=type,
name=name,
caption=display,
state=state
}
if is_type(default,'function') then rtn_checkbox.data._state = default end
rtn_checkbox.data._true = callback_true
rtn_checkbox.data._false = callback_false
rtn_checkbox:on_event('state',function(event)
local checkbox = Gui.data['inputs_'..event.element.type][event.element.name]
local player = Game.get_player(event)
if event.element.state then
if is_type(checkbox.data._true,'function') then
local success, err = pcall(checkbox.data._true,player,event.element)
if not success then error(err) end
else error('Invalid Callback') end
else
if is_type(checkbox.data._false,'function') then
local success, err = pcall(checkbox.data._false,player,event.element)
if not success then error(err) end
else error('Invalid Callback') end
end
end)
return rtn_checkbox
end
--- Used to reset the state of radio buttons, recommended to be called on_state_change to reset any radio buttons it is meant to work with.
-- @usage Gui.inputs.reset_radio{radio1,radio2,...}
-- @param elements can be a list of elements or a single element
function inputs.reset_radio(elements)
if #elements > 0 then
for _,element in pairs(elements) do
if element.valid then
local _elements = Gui.data['inputs_'..element.type] or {}
local _element = _elements[element.name]
local player = Game.get_player(element.player_index)
local state = false
local success, err = pcall(_element.data._state,player,element.parent)
if success then state = err else error(err) end
element.state = state
end
end
else
if elements.valid then
local _elements = Gui.data['inputs_'..elements.type] or {}
local _element = _elements[elements.name]
local player = Game.get_player(elements.player_index)
local state = false
local success, err = pcall(_element.data._state,player,elements.parent)
if success then state = err else error(err) end
elements.state = state
end
end
end
--- Used to define a text callback only on text_changed
-- @usage Gui.inputs.add_text('test',false,'Just for testing',function)
-- @tparam string name the name of this button
-- @tparam boolean box is it a text box rather than a text field
-- @tparam string text the starting text
-- @tparam function callback the callback to call on change function(player,text,element)
-- @treturn table the text object that was made, to allow a custom error event if wanted
function inputs.add_text(name,box,text,callback)
local type = 'textfield'; if box then type='text-box' end
local rtn_textbox = inputs.add{
type=type,
name=name,
text=text
}
rtn_textbox.data._callback = callback
rtn_textbox:on_event('text',function(event)
local textbox = Gui.data['inputs_'..event.element.type][event.element.name]
local player = Game.get_player(event)
local element = event.element
local event_callback = textbox.data._callback
if is_type(event_callback,'function') then
local success, err = pcall(event_callback,player,element.text,element)
if not success then error(err) end
else error('Invalid Callback Condition Format') end
end)
return rtn_textbox
end
--- Used to define a slider callback only on value_changed
-- @usage Gui.inputs.add_slider('test','horizontal',1,10,5,function)
-- @tparam string name the name of this button
-- @tparam string orientation direction of the slider
-- @tparam number min the lowest number
-- @tparam number max the highest number
-- @tparam function start_callback either a number or a function to return a number
-- @tparam function callback the function to be called on value_changed function(player,value,percent,element)
-- @treturn table the slider object that was made, to allow a custom error event if wanted
function inputs.add_slider(name,orientation,min,max,start_callback,callback)
local slider = inputs.add{
type='slider',
name=name,
orientation=orientation,
minimum_value=min,
maximum_value=max,
value=start_callback
}
slider.data._start = start_callback
slider.data._callback = callback
slider:on_event('slider',function(event)
local slider = Gui.data['inputs_'..event.element.type][event.element.name]
local player = Game.get_player(event)
local value = event.element.slider_value
local data = slider.data
local percent = value/event.element.get_slider_maximum()
if is_type(data._callback,'function') then
local success, err = pcall(data._callback,player,value,percent,event.element)
if not success then error(err) end
else error('Invalid Callback Condition Format') end
end)
return slider
end
--- Used to define a drop down callback only on value_changed
-- @usage Gui.inputs.add_drop_down('test',{1,2,3},1,function)
-- @tparam string name name of the drop down
-- @param items either a list or a function which returns a list
-- @param index either a number or a function which returns a number
-- @tparam function callback the callback which is called when a new index is selected function(player,selected,items,element)
-- @treturn table the drop-down object that was made, to allow a custom error event if wanted
function inputs.add_drop_down(name,items,index,callback)
local rtn_dropdown = inputs.add{
type='drop-down',
name=name,
items=items,
selected_index=index
}
rtn_dropdown.data._items = items
rtn_dropdown.data._index = index
rtn_dropdown.data._callback = callback
rtn_dropdown:on_event('selection',function(event)
local dropdown = Gui.data['inputs_'..event.element.type][event.element.name]
local player = Game.get_player(event)
local element = event.element
local drop_items = element.items
local selected = drop_items[element.selected_index]
local drop_callback = dropdown.data._callback
if is_type(drop_callback,'function') then
local success, err = pcall(drop_callback,player,selected,drop_items,element)
if not success then error(err) end
else error('Invalid Callback Condition Format') end
end)
return rtn_dropdown
end
-- calling will attempt to add a new input
return setmetatable(inputs,{__call=function(self,...) return self.add(...) end})

View File

@@ -0,0 +1,23 @@
{
"name": "ExpGamingCore.Gui.inputs",
"version": "4.0.0",
"description": "Addds an event manager for gui inputs and easy input creation",
"author": "Cooldude2606",
"contact": "Discord: Cooldude2606#5241",
"license": "https://github.com/explosivegaming/scenario/blob/master/LICENSE",
"location": "FSM_ARCHIVE",
"keywords": [
"Gui",
"Inputs",
"Buttons",
"Text Fields"
],
"collection": "ExpGamingCore.Gui@4.0.0",
"dependencies": {
"mod-gui": "*",
"FactorioStdLib.Color": "^0.8.0",
"FactorioStdLib.Game": "^0.8.0",
"ExpGamingCore.Gui": "^4.0.0"
},
"submodules": {}
}

View File

@@ -0,0 +1,246 @@
--- Adds a organiser for left gui elements which will automatically update there information and have open requirements
-- @module ExpGamingCore.Gui.Left
-- @alias left
-- @author Cooldude2606
-- @license https://github.com/explosivegaming/scenario/blob/master/LICENSE
--- This is a submodule of ExpGamingCore.Gui but for ldoc reasons it is under its own module
-- @function _comment
local Game = require('FactorioStdLib.Game')
local Server = require('ExpGamingCore.Server')
local Color = require('FactorioStdLib.Color')
local mod_gui = require('mod-gui')
local Gui = require('ExpGamingCore.Gui')
local order_config = require(module_path..'/order_config')
local Role -- this is optional and is handled by it being present, it is loaded on init
local left = {}
left._prototype = {}
left.hide = Gui.inputs{
name='gui-left-hide',
type='button',
caption='<'
}:on_event('click',function(event)
for _,child in pairs(event.element.parent.children) do
if child.name ~= 'popups' then child.style.visible = false end
end
end)
local global = {}
Global.register(global,function(tbl) global = tbl end)
-- used for debugging
function left.override_open(state)
global.over_ride_left_can_open = state
end
--- Used to add a left gui frame
-- @usage Gui.left.add{name='foo',caption='Foo',tooltip='just testing',open_on_join=true,can_open=function,draw=function}
-- @usage return_value(player) -- toggles visibility for that player, if no player then updates for all players
-- @param obj this is what will be made, needs a name and a draw function(root_frame), open_on_join can be used to set the default state true/false, can_open is a test to block it from opening but is not needed
-- @return the object that is made, calling the returned value with out a param will update the gui, else will toggle visibility for that player
function left.add(obj)
if not is_type(obj,'table') then return end
if not is_type(obj.name,'string') then return end
verbose('Created Left Gui: '..obj.name)
setmetatable(obj,{__index=left._prototype,__call=function(self,player) if player then return self:toggle(player) else return left.update(self.name) end end})
Gui.data('left',obj.name,obj)
if Gui.toolbar then Gui.toolbar(obj.name,obj.caption,obj.tooltip,function(event) obj:toggle(event) end) end
return obj
end
--- This is used to update all the guis of connected players, good idea to use our thread system as it as nested for loops
-- @usage Gui.left.update()
-- @tparam[opt] string frame this is the name of a frame if you only want to update one
-- @param[opt] players the player to update for, if not given all players are updated, can be one player
function left.update(frame,players)
if not Server or not Server._thread then
players = is_type(players,'table') and #players > 0 and {unpack(players)} or is_type(players,'table') and {players} or Game.get_player(players) and {Game.get_player(players)} or game.connected_players
for _,player in pairs(players) do
local frames = Gui.data.left or {}
if frame then frames = {[frame]=frames[frame]} or {} end
for _,left_frame in pairs(frames) do
if left_frame then left_frame:first_open(player) end
end
end
else
local frames = Gui.data.left or {}
if frame then frames = {[frame]=frames[frame]} or {} end
players = is_type(players,'table') and #players > 0 and {unpack(players)} or is_type(players,'table') and {players} or Game.get_player(players) and {Game.get_player(players)} or game.connected_players
Server.new_thread{
data={players=players,frames=frames}
}:on_event('tick',function(thread)
if #thread.data.players == 0 then thread:close() return end
local player = table.remove(thread.data.players,1)
Server.new_thread{
data={player=player,frames=thread.data.frames}
}:on_event('resolve',function(thread)
for _,left_frame in pairs(thread.data.frames) do
if left_frame then left_frame:first_open(thread.data.player) end
end
end):queue()
end):open()
end
end
--- Used to open the left gui of every player
-- @usage Gui.left.open('foo')
-- @tparam string left_name this is the gui that you want to open
-- @tparam[opt] LuaPlayer the player to open the gui for
function left.open(left_name,player)
local players = player and {player} or game.connected_players
local _left = Gui.data.left[left_name]
if not _left then return end
if not Server or not Server._thread then
for _,next_player in pairs(players) do _left:open(next_player) end
else
Server.new_thread{
data={players=players}
}:on_event('tick',function(thread)
if #thread.data.players == 0 then thread:close() return end
local next_player = table.remove(thread.data.players,1)
_left:open(next_player)
end):open()
end
end
--- Used to close the left gui of every player
-- @usage Gui.left.close('foo')
-- @tparam string left_name this is the gui that you want to close
-- @tparam[opt] LuaPlayer the player to close the gui for
function left.close(left_name,player)
local players = player and {player} or game.connected_players
local _left = Gui.data.left[left_name]
if not _left then return end
if not Server or not Server._thread or player then
for _,next_player in pairs(players) do _left:close(next_player) end
else
Server.new_thread{
data={players=players}
}:on_event('tick',function(thread)
if #thread.data.players == 0 then thread:close() return end
local next_player = table.remove(thread.data.players,1)
_left:close(next_player)
end):open()
end
end
--- Used to force the gui open for the player
-- @usage left:open(player)
-- @tparam luaPlayer player the player to open the gui for
function left._prototype:open(player)
player = Game.get_player(player)
if not player then error('Invalid Player') end
local left_flow = mod_gui.get_frame_flow(player)
if not left_flow[self.name] then self:first_open(player) end
left_flow[self.name].style.visible = true
if left_flow['gui-left-hide'] then left_flow['gui-left-hide'].style.visible = true end
end
--- Used to force the gui closed for the player
-- @usage left:open(player)
-- @tparam luaPlayer player the player to close the gui for
function left._prototype:close(player)
player = Game.get_player(player)
if not player then error('Invalid Player') end
local left_flow = mod_gui.get_frame_flow(player)
if not left_flow[self.name] then self:first_open(player) end
left_flow[self.name].style.visible = false
local count = 0
for _,child in pairs(left_flow.children) do if child.style.visible then count = count+1 end if count > 1 then break end end
if count == 1 and left_flow['gui-left-hide'] then left_flow['gui-left-hide'].style.visible = false end
end
--- When the gui is first made or is updated this function is called, used by the script
-- @usage left:first_open(player) -- returns the frame
-- @tparam LuaPlayer player the player to draw the gui for
-- @treturn LuaFrame the frame made/updated
function left._prototype:first_open(player)
player = Game.get_player(player)
local left_flow = mod_gui.get_frame_flow(player)
local frame
if left_flow[self.name] then
frame = left_flow[self.name]
frame.clear()
else
if not left_flow['gui-left-hide'] then left.hide(left_flow).style.maximal_width=15 end
frame = left_flow.add{type='frame',name=self.name,style=mod_gui.frame_style,caption=self.caption,direction='vertical'}
frame.style.visible = false
if is_type(self.open_on_join,'boolean') then frame.style.visible = self.open_on_join left_flow['gui-left-hide'].style.visible = true end
end
if is_type(self.draw,'function') then self:draw(frame) else frame.style.visible = false error('No Callback On '..self.name) end
return frame
end
--- Toggles the visibility of the gui based on some conditions
-- @usage left:toggle(player) -- returns new state
-- @tparam LuaPlayer player the player to toggle the gui for, remember there are condition which need to be met
-- @treturn boolean the new state that the gui is in
function left._prototype:toggle(player)
player = Game.get_player(player)
local left_flow = mod_gui.get_frame_flow(player)
if not left_flow[self.name] then self:first_open(player) end
local left_frame = left_flow[self.name]
local open = false
if is_type(self.can_open,'function') then
local success, err = pcall(self.can_open,player)
if not success then error(err)
elseif err == true then open = true
elseif global.over_ride_left_can_open then
if is_type(Role,'table') then
if Role.allowed(player,self.name) then open = true
else open = {'ExpGamingCore_Gui.unauthorized'} end
else open = true end
else open = err end
else
if is_type(Role,'table') then
if Role.allowed(player,self.name) then open = true
else open = {'ExpGamingCore_Gui.unauthorized'} end
else open = true end
end
if open == true and left_frame.style.visible ~= true then
left_frame.style.visible = true
left_flow['gui-left-hide'].style.visible = true
else
left_frame.style.visible = false
local count = 0
for _,child in pairs(left_flow.children) do if child.style.visible then count = count+1 end if count > 1 then break end end
if count == 1 and left_flow['gui-left-hide'] then left_flow['gui-left-hide'].style.visible = false end
end
if open == false then player_return({'ExpGamingCore_Gui.cant-open-no-reason'},defines.textcolor.crit,player) player.play_sound{path='utility/cannot_build'}
elseif open ~= true then player_return({'ExpGamingCore_Gui.cant-open',open},defines.textcolor.crit,player) player.play_sound{path='utility/cannot_build'} end
return left_frame.style.visible
end
Event.add(defines.events.on_player_joined_game,function(event)
-- draws the left guis when a player first joins, fake_event is just because i am lazy
local player = Game.get_player(event)
local frames = Gui.data.left or {}
local left_flow = mod_gui.get_frame_flow(player)
if not left_flow['gui-left-hide'] then left.hide(left_flow).style.maximal_width=15 end
local done = {}
for _,name in pairs(order_config) do
local left_frame = Gui.data.left[name]
if left_frame then
done[name] = true
left_frame:first_open(player)
end
end
for name,left_frame in pairs(frames) do
if not done[name] then left_frame:first_open(player) end
end
end)
Event.add(defines.events.on_tick,function(event)
if ((event.tick+10)/(3600*game.speed)) % 15 == 0 then
left.update()
end
end)
function left.on_init()
if loaded_modules['ExpGamingCore.Role'] then Role = require('ExpGamingCore.Role') end
end
-- calling will attempt to add a new gui
return setmetatable(left,{__call=function(self,...) return self.add(...) end})

View File

@@ -0,0 +1,13 @@
return {
'server-info',
'readme',
'science',
'rockets',
'player-list',
'tasklist',
'warp-list',
'polls',
'announcements',
'admin-commands',
'game-settings'
}

View File

@@ -0,0 +1,26 @@
{
"name": "ExpGamingCore.Gui.left",
"version": "4.0.0",
"description": "Adds an easy way to create auto updating left guis with toggle buttons.",
"author": "Cooldude2606",
"contact": "Discord: Cooldude2606#5241",
"license": "https://github.com/explosivegaming/scenario/blob/master/LICENSE",
"location": "FSM_ARCHIVE",
"keywords": [
"Gui",
"Left",
"AutoUpdate"
],
"collection": "ExpGamingCore.Gui@4.0.0",
"dependencies": {
"mod-gui": "*",
"FactorioStdLib.Game": "^0.8.0",
"ExpGamingCore.Server": "^4.0.0",
"FactorioStdLib.Color": "^0.8.0",
"ExpGamingCore.Gui": "^4.0.0",
"ExpGamingCore.Gui.inputs": "^4.0.0",
"ExpGamingCore.Gui.toolbar": "?^4.0.0",
"ExpGamingCore.Role": "?^4.0.0"
},
"submodules": {}
}

View File

@@ -0,0 +1,4 @@
[ExpGamingCore_Gui]
unauthorized=401 - Unbefugt: Du hast keinen Zugriff auf diese Befehle!
cant-open=Du kannst dieses Menü nicht öffnen, Grund: __1__
cant-open-no-reason=Du kannst dieses Menü gerade nicht öffnen.

View File

@@ -0,0 +1,4 @@
[ExpGamingCore_Gui]
unauthorized=401 - Unauthorized: Access is denied due to invalid credentials
cant-open=You can't open this panel right now, reason: __1__
cant-open-no-reason=You can't open this panel right now

View File

@@ -0,0 +1,4 @@
[ExpGamingCore_Gui]
unauthorized=401 - Unauthorized: Access is denied due to invalid credentials
cant-open=You can not open this panel right now, reason: __1__
cant-open-no-reason=You can not open this panel right now

View File

@@ -0,0 +1,4 @@
[ExpGamingCore_Gui]
unauthorized=401 - Onbevoegd: toegang wordt geweigerd vanwege ongeldige inloggegevens
cant-open=Je kan dit momenteel niet openen. Reden: __1__
cant-open-no-reason=Je kan dit momenteel niet openen.

View File

@@ -0,0 +1,4 @@
[ExpGamingCore_Gui]
unauthorized=401 -Otillåten: Tillgång nekas på grund av otillräcklig säkerhetsprövning.
cant-open=Du kan inte öppna den här panelen just nu, orsak: __1__
cant-open-no-reason=Du kan inte öppna den här panelen just nu

View File

@@ -0,0 +1,130 @@
--- Adds a location for popups which can be dismissed by a player and created from other scripts
-- @module ExpGamingCore.Gui.Popup
-- @alias popup
-- @author Cooldude2606
-- @license https://github.com/explosivegaming/scenario/blob/master/LICENSE
--- This is a submodule of ExpGamingCore.Gui but for ldoc reasons it is under its own module
-- @function _comment
local Game = require('FactorioStdLib.Game')
local mod_gui = require('mod-gui')
local Gui = require('ExpGamingCore.Gui')
local Server -- loaded on_init
local popup = {}
popup._prototype = {}
--- Used to add a popup gui style
-- @usage Gui.left.add{name='foo',caption='Foo',draw=function}
-- @usage return_value(data,player) -- opens popup for one player use popup.open to open for more than one player
-- @param obj this is what will be made, needs a name and a draw function(root_frame,data)
-- @return the object that is made, calling the returned value will open the popup for that player
function popup.add(obj)
if not is_type(obj,'table') then return end
if not is_type(obj.name,'string') then return end
verbose('Created Popup Gui: '..obj.name)
setmetatable(obj,{__index=popup._prototype,__call=function(self,data,player) local players = player and {player} or nil return popup.open(self.name,data,players) end})
local name = obj.name; obj.name = nil
Gui.data('popup',name,obj)
obj.name = name
return obj
end
-- this is used by the script to find the popup flow
function popup.flow(player)
player = Game.get_player(player)
if not player then error('Invalid Player',2) end
local flow = mod_gui.get_frame_flow(player).popups
if not flow then flow = mod_gui.get_frame_flow(player).add{name='popups',type='flow',direction='vertical'} flow.style.visible=false end
return flow
end
--- Use to open a popup for these players
-- @usage Gui.popup.open('ban',nil,{player=1,reason='foo'})
-- @tparam string style this is the name you gave to the popup when added
-- @param data this is the data that is sent to the draw function
-- @tparam[opt=game.connected_players] table players the players to open the popup for
function popup.open(style,data,players)
local _popup = Gui.data.popup[style]
players = players or game.connected_players
data = data or {}
if not _popup then return end
if not Server or not Server._thread then
for _,player in pairs(players) do
if _popup.left then _popup.left:close(player) end
local flow = popup.flow(player)
flow.style.visible=true
local _frame = flow.add{
type='frame',
direction='horizontal',
style=mod_gui.frame_style
}
local frame = _frame.add{
type='frame',
name='inner_frame',
direction='vertical',
style='image_frame'
}
_popup.close(_frame)
if is_type(_popup.draw,'function') then
local success, err = pcall(_popup.draw,_popup,frame,data)
if not success then error(err) end
else error('No Draw On Popup '.._popup.name) end
end
else
Server.new_thread{
data={players=players,popup=_popup,data=data}
}:on_event('tick',function(self)
if #self.data.players == 0 then self:close() return end
local player = table.remove(self.data.players,1)
if self.data.popup.left then self.data.popup.left:close(player) end
local flow = popup.flow(player)
flow.style.visible=true
local _frame = flow.add{
type='frame',
direction='horizontal',
style=mod_gui.frame_style
}
local frame = _frame.add{
type='frame',
name='inner_frame',
direction='vertical',
style='image_frame'
}
self.data.popup.close(_frame)
if is_type(self.data.popup.draw,'function') then
local success, err = pcall(self.data.popup.draw,self.data.popup,frame,self.data.data)
if not success then error(err) end
else error('No Draw On Popup '..self.data.popup.name) end
end):open()
end
end
function popup._prototype:add_left(obj)
if not Gui.left then return end
obj.name = obj.name or self.name
self.left = Gui.left(obj)
end
function popup.on_init()
if loaded_modules['ExpGamingCore.Server'] then Server = require('ExpGamingCore.Server') end
end
function popup.on_post()
popup._prototype.close = Gui.inputs.add{
type='button',
name='popup-close',
caption='utility/set_bar_slot',
tooltip='Close This Popup'
}:on_event('click',function(event)
local frame = event.element.parent
local parent = frame.parent
if frame and frame.valid then frame.destroy() if #parent.children == 0 then parent.style.visible = false end end
end)
end
Event.add(defines.events.on_player_joined_game,popup.flow)
-- calling will attempt to add a new popup style
return setmetatable(popup,{__call=function(self,...) return self.add(...) end})

View File

@@ -0,0 +1,24 @@
{
"name": "ExpGamingCore.Gui.popup",
"version": "4.0.0",
"description": "Adds popup guis to the gui system which builds upon the left gui system.",
"author": "Cooldude2606",
"contact": "Discord: Cooldude2606#5241",
"license": "https://github.com/explosivegaming/scenario/blob/master/LICENSE",
"location": "FSM_ARCHIVE",
"keywords": [
"Gui",
"Popup",
"Left"
],
"collection": "ExpGamingCore.Gui@4.0.0",
"dependencies": {
"mod-gui": "*",
"FactorioStdLib.Game": "^0.8.0",
"ExpGamingCore.Gui": "^4.0.0",
"ExpGamingCore.Gui.inputs": "^4.0.0",
"ExpGamingCore.Server": "?^4.0.0",
"ExpGamingCore.Gui.left": "?^4.0.0"
},
"submodules": {}
}

View File

@@ -0,0 +1,29 @@
{
"name": "ExpGamingCore.Gui",
"version": "4.0.0",
"description": "Adds a objective version to custom guis.",
"location": "FSM_ARCHIVE",
"keywords": [
"Library",
"Lib",
"ExpGaming",
"Core",
"Gui",
"ExpGui"
],
"dependencies": {
"ExpGamingLib": "^4.0.0",
"FactorioStdLib.Table": "^0.8.0",
"FactorioStdLib.Color": "^0.8.0",
"FactorioStdLib.Game": "^0.8.0",
"ExpGamingCore.Server": "?^4.0.0"
},
"collection": "ExpGamingCore@4.0.0",
"submodules": {
"ExpGamingCore.Gui.center": "4.0.0",
"ExpGamingCore.Gui.inputs": "4.0.0",
"ExpGamingCore.Gui.left": "4.0.0",
"ExpGamingCore.Gui.popup": "4.0.0",
"ExpGamingCore.Gui.toolbar": "4.0.0"
}
}

View File

@@ -0,0 +1,40 @@
--- This file will be loaded when ExpGamingCore.Server is present
-- @function _comment
local Game = require('FactorioStdLib.Game')
local Server = require('ExpGamingCore.Server')
Server.add_module_to_interface('ExpGui','ExpGamingCore.Gui')
--- Adds a server thread that allows the camera follows to be toggled off and on
return function()
Server.new_thread{
name='camera-follow',
data={cams={},cam_index=1,players={}}
}:on_event('tick',function(self)
local update = 4
if self.data.cam_index >= #self.data.cams then self.data.cam_index = 1 end
if update > #self.data.cams then update = #self.data.cams end
for cam_offset = 0,update do
local _cam = self.data.cams[self.data.cam_index+cam_offset]
if not _cam then break end
if not _cam.cam.valid then table.remove(self.data.cams,self.data.cam_index)
elseif not _cam.entity.valid then table.remove(self.data.cams,self.data.cam_index)
else _cam.cam.position = _cam.entity.position if not _cam.surface then _cam.cam.surface_index = _cam.entity.surface.index end
end
end
self.data.cam_index = self.data.cam_index+update
end):on_event(defines.events.on_player_respawned,function(self,event)
if self.data.players[event.player_index] then
local remove = {}
local player = Game.get_player(event)
for index,cam in pairs(self.data.players[event.player_index]) do
if cam.valid then table.insert(self.data.cams,{cam=cam,entity=player.character,surface=player.surface})
else table.insert(remove,index) end
end
for n,index in pairs(remove) do
table.remove(self.data.players[event.player_index],index-n+1)
end
end
end):open()
end

View File

@@ -0,0 +1,187 @@
--- Used to test all gui elements and parts can be used in game via Gui.test()
-- @module ExpGamingCore.Gui.Test
-- @author Cooldude2606
-- @license https://github.com/explosivegaming/scenario/blob/master/LICENSE
--- This is a submodule of ExpGamingCore.Gui but for ldoc reasons it is under its own module
-- @function _comment
local Game = require('FactorioStdLib.Game')
local Gui = Gui -- this is to force gui to remain in the ENV
local mod_gui = require("mod-gui")
local gui_test_close = Gui.inputs{
name='gui-test-close',
type='button',
caption='Close Test Gui'
}:on_event('click',function(event) event.element.parent.destroy() end)
local caption_test = Gui.inputs{
name='text-button',
type='button',
caption='Test'
}:on_event('click',function(event) game.print('test') end)
local sprite_test = Gui.inputs{
name='sprite-button',
type='button',
sprite='item/lab'
}:on_event('click',function(event) game.print('test') end)
local input_test = Gui.inputs.add_button('test-inputs','Try RMB','alt,ctrl,shift and mouse buttons',{
{
function(player,mouse,keys) return mouse == defines.mouse_button_type.left and keys.alt end,
function(player,element) player_return('Left: Alt',nil,player) end
},
{
function(player,mouse,keys) return mouse == defines.mouse_button_type.left and keys.ctrl end,
function(player,element) player_return('Left: Ctrl',nil,player) end
},
{
function(player,mouse,keys) return mouse == defines.mouse_button_type.left and keys.shift end,
function(player,element) player_return('Left: Shift',nil,player) end
},
{
function(player,mouse,keys) return mouse == defines.mouse_button_type.right and keys.alt end,
function(player,element) player_return('Right: Alt',nil,player) end
},
{
function(player,mouse,keys) return mouse == defines.mouse_button_type.right and keys.ctrl end,
function(player,element) player_return('Right: Ctrl',nil,player) end
},
{
function(player,mouse,keys) return mouse == defines.mouse_button_type.right and keys.shift end,
function(player,element) player_return('Right: Shift',nil,player) end
}
}):on_event('error',function(err) game.print('this is error handling') end)
local elem_test = Gui.inputs.add_elem_button('test-elem','item','Testing Elems',function(player,element,elem)
player_return(elem.type..' '..elem.value,nil,player)
end)
local check_test = Gui.inputs.add_checkbox('test-check',false,'Cheat Mode',function(player,parent)
return game.players[parent.player_index].cheat_mode
end,function(player,element)
player.cheat_mode = true
end,function(player,element)
player.cheat_mode = false
end)
local radio_test = Gui.inputs.add_checkbox('test-radio',true,'Kill Self',function(player,parent)
return false
end,function(player,element)
if player.character then player.character.die() end
Gui.inputs.reset_radio(element.parent['test-radio-reset'])
end)
local radio_test_reset = Gui.inputs.add_checkbox('test-radio-reset',true,'Reset Kill Self',function(player,parent)
return not parent['test-radio'].state
end,function(player,element)
Gui.inputs.reset_radio(element.parent['test-radio'])
end)
local text_test = Gui.inputs.add_text('test-text',false,'default text',function(player,text,element)
player_return(text,nil,player)
end)
local box_test = Gui.inputs.add_text('test-box',true,'default text but a box',function(player,text,element)
player_return(text,nil,player)
end)
local slider_test = Gui.inputs.add_slider('test-slider','vertical',0,5,function(player,parent)
return player.character_running_speed_modifier
end,function(player,value,percent,element)
player.character_running_speed_modifier = value
player_return('Value In Percent of Max '..math.floor(percent*100)-(math.floor(percent*100)%5),nil,player)
end)
local drop_test = Gui.inputs.add_drop_down('test-drop',table.keys(defines.color),1,function(player,selected,items,element)
player.color = defines.color[selected]
player.chat_color = defines.color[selected]
end)
--- The funcation that is called when calling Gui.test
-- @function Gui.test
-- @usage Gui.test() -- draws test gui
-- @param[opt=game.player] player a pointer to a player to draw the test gui for
local function test_gui(player)
local player = game.player or Game.get_player(player) or nil
if not player then return end
if mod_gui.get_frame_flow(player)['gui-test'] then mod_gui.get_frame_flow(player)['gui-test'].destroy() end
local frame = mod_gui.get_frame_flow(player).add{type='frame',name='gui-test',direction='vertical'}
gui_test_close(frame)
caption_test(frame)
sprite_test(frame)
input_test(frame)
elem_test(frame)
check_test(frame)
radio_test(frame)
radio_test_reset(frame)
text_test(frame)
box_test(frame)
slider_test(frame)
drop_test(frame)
end
Gui.toolbar.add('open-gui-test','Open Test Gui','Opens the test gui with every input',test_gui)
-- testing the center gui
Gui.center{
name='test-center',
caption='Gui Center',
tooltip='Just a gui test'
}:add_tab('tab-1','Tab 1','Just a tab',function(frame)
frame.add{type='label',caption='Test'}
end):add_tab('tab-2','Tab 2','Just a tab',function(frame)
for i = 1,100 do
frame.add{type='label',caption='Test 2'}
end
end)
-- testing the left gui
Gui.left{
name='test-left',
caption='Gui Left',
tooltip='just testing',
draw=function(self,frame)
for _,player in pairs(game.connected_players) do
frame.add{type='label',caption=player.name}
end
end,
can_open=function(player) return player.index == 1 end
}
local text_popup = Gui.inputs.add_text('test-popup-text',true,'Message To Send',function(player,text,element)
element.text = text
end)
local send_popup = Gui.inputs{
type='button',
name='test-popup-send',
caption='Send Message'
}:on_event('click',function(event)
local player = Game.get_player(event)
local message = event.element.parent['test-popup-text'].text
Gui.popup.open('test-popup',{message=message,player=player.name})
end)
Gui.popup{
name='test-popup',
caption='Gui Popup',
draw=function(self,frame,data)
frame.add{type='label',caption='Opened by: '..data.player}
frame.add{type='label',caption='Message: '..data.message}
end
}:add_left{
caption='Gui Left w/ Popup',
tooltip='Send a message',
draw=function(self,frame)
text_popup:draw(frame)
send_popup:draw(frame)
end
}
return test_gui

View File

@@ -0,0 +1,90 @@
--- Adds a toolbar to the top left of the screen
-- @module ExpGamingCore.Gui.Toolbar
-- @alias toolbar
-- @author Cooldude2606
-- @license https://github.com/explosivegaming/scenario/blob/master/LICENSE
--- This is a submodule of ExpGamingCore.Gui but for ldoc reasons it is under its own module
-- @function _comment
local Game = require('FactorioStdLib.Game')
local mod_gui = require('mod-gui')
local Gui = require('ExpGamingCore.Gui')
local order_config = require(module_path..'/order_config')
local Role -- this is optional and is handled by it being present, it is loaded on init
local toolbar = {}
toolbar.hide = Gui.inputs{
name='gui-toolbar-hide',
type='button',
caption='<'
}:on_event('click',function(event)
if event.element.caption == '<' then
event.element.caption = '>'
for _,child in pairs(event.element.parent.children) do
if child.name ~= event.element.name then child.style.visible = false end
end
else
event.element.caption = '<'
for _,child in pairs(event.element.parent.children) do
if child.name ~= event.element.name then child.style.visible = true end
end
end
end)
--- Add a button to the toolbar, ranks need to be allowed to use these buttons if ranks is preset
-- @usage toolbar.add('foo','Foo','Test',function() game.print('test') end)
-- @tparam string name the name of the button
-- @tparam string caption can be a sprite path or text to show
-- @tparam string tooltip the help to show for the button
-- @tparam function callback the function which is called on_click
-- @treturn table the button object that was made, calling the returned value will draw the toolbar button added
function toolbar.add(name,caption,tooltip,callback)
verbose('Created Toolbar Button: '..name)
local button = Gui.inputs.add{type='button',name=name,caption=caption,tooltip=tooltip}
button:on_event(Gui.inputs.events.click,callback)
Gui.data('toolbar',name,button)
return button
end
--- Draws the toolbar for a certain player
-- @usage toolbar.draw(1)
-- @param player the player to draw the tool bar of
function toolbar.draw(player)
player = Game.get_player(player)
if not player then return end
local toolbar_frame = mod_gui.get_button_flow(player)
toolbar_frame.clear()
if not Gui.data.toolbar then return end
toolbar.hide(toolbar_frame).style.maximal_width = 15
local done = {}
for _,name in pairs(order_config) do
local button = Gui.data.toolbar[name]
if button then
done[name] = true
if is_type(Role,'table') then
if Role.allowed(player,name) then
button(toolbar_frame)
end
else button(toolbar_frame) end
end
end
for name,button in pairs(Gui.data.toolbar) do
if not done[name] then
if is_type(Role,'table') then
if Role.allowed(player,name) then
button(toolbar_frame)
end
else button(toolbar_frame) end
end
end
end
function toolbar.on_init()
if loaded_modules['ExpGamingCore.Role'] then Role = require('ExpGamingCore.Role') end
end
Event.add({defines.events.on_role_change,defines.events.on_player_joined_game},toolbar.draw)
-- calling with only a player will draw the toolbar for that player, more params will attempt to add a button
return setmetatable(toolbar,{__call=function(self,player,extra,...) if extra then return self.add(player,extra,...) else self.draw(player) end end})

View File

@@ -0,0 +1,13 @@
return {
'server-info',
'readme',
'science',
'rockets',
'player-list',
'tasklist',
'warp-list',
'polls',
'announcements',
'admin-commands',
'game-settings'
}

View File

@@ -0,0 +1,23 @@
{
"name": "ExpGamingCore.Gui.toolbar",
"version": "4.0.0",
"description": "Adds a toolbar at the top of the screen",
"author": "Cooldude2606",
"contact": "Discord: Cooldude26060#5241",
"license": "https://github.com/explosivegaming/scenario/blob/master/LICENSE",
"location": "FSM_ARCHIVE",
"keywords": [
"Gui",
"Toolbar",
"Buttons"
],
"collection": "ExpGamingCore.Gui@4.0.0",
"dependencies": {
"mod-gui": "*",
"FactorioStdLib.Game": "^0.8.0",
"ExpGamingCore.Gui": "^4.0.0",
"ExpGamingCore.Gui.inputs": "^4.0.0",
"ExpGamingCore.Role": "?^4.0.0"
},
"submodules": {}
}

View File

@@ -0,0 +1,284 @@
-- the two below are used internally by the role system and should not be REMOVED, name must be kept the same and used at least once
Role.add_flag('is_default') -- this must be included in at least one role
Role.add_flag('is_root',function(player,state)
if state then game.print('--- !!!ALERT!!! --- '..player.name..' has been given a role with ROOT ACCESS --- !!!ALERT!!! ---') end
if player.character then player.character.destructible = not state end
end) -- the SERVER role will have root but you may assign this to other roles
-- the two below are ised internally by the role system and should not be RENAMED, name must be kept the same but does not need to be used
Role.add_flag('block_auto_promote') -- is not required but name is used internally to block time based promotions
Role.add_flag('is_antiroot',function(player,state) if player.character then player.character.destructible = not state end end) -- not required but setting true will disallow everything for that role
-- all below are not required and are not used internally
Role.add_flag('is_admin',function(player,state) player.admin = state end) -- highly recommenced but not required
Role.add_flag('is_spectator',function(player,state) player.spectator = state end)
Role.add_flag('is_jail',function(player,state) if player.character then player.character.active = not state end end)
Role.add_flag('allow_afk_kick')
Role.add_flag('is_donator')
Role.add_flag('is_timed')
Role.add_flag('is_verified')
Role.add_flag('not_reportable')
-- Root
Role{
name='Root',
short_hand='Root',
tag='[Root]',
group='Root',
colour={r=255,b=255,g=255},
is_root=true,
is_admin=true,
is_spectator=true,
not_reportable=true,
allow={}
}
Role{
name='Community Manager',
short_hand='Com Mngr',
tag='[Com Mngr]',
group='Root',
colour={r=150,g=68,b=161},
is_admin=true,
is_spectator=true,
is_donator=true,
not_reportable=true,
allow={}
}
Role{
name='Developer',
short_hand='Dev',
tag='[Dev]',
group='Root',
colour={r=179,g=125,b=46},
is_admin=true,
is_spectator=true,
is_donator=true,
not_reportable=true,
allow={
['interface']=true,
['cheat-mode']=true
}
}
-- Admin
Role{
name='Administrator',
short_hand='Admin',
tag='[Admin]',
group='Admin',
colour={r=233,g=63,b=233},
is_admin=true,
is_spectator=true,
is_verified=true,
not_reportable=true,
allow={
['game-settings']=true,
['always-warp']=true,
['admin-items']=true
}
}
Role{
name='Moderator',
short_hand='Mod',
tag='[Mod]',
group='Admin',
colour={r=0,g=170,b=0},
is_admin=true,
is_spectator=true,
is_verified=true,
not_reportable=true,
allow={
['set-home']=true,
['home']=true,
['return']=true,
['bonus']=true,
['announcements']=true,
['rank-changer']=true,
}
}
Role{
name='Trainee',
short_hand='TrMod',
tag='[TrMod]',
group='Admin',
colour={r=0,g=196,b=137},
is_spectator=true,
is_verified=true,
not_reportable=true,
allow={
['go-to']=true,
['bring']=true,
['set-home']=false,
['home']=false,
['return']=false,
['bonus']=false,
['admin-commands']=true,
['warn']=true,
['temp-ban']=true,
['clear-warnings']=true,
['clear-reports']=true,
['clear-all']=true,
['clear-inv']=true,
}
}
-- High Member
Role{
name='Sponsor',
short_hand='Spon',
tag='[Sponsor]',
group='HiMember',
colour={r=247,g=246,b=54},
is_spectator=true,
is_donator=true,
allow={}
}
Role{
name='Pay to Win',
short_hand='P2W',
tag='[P2W]',
group='HiMember',
colour={r=238,g=172,b=44},
is_donator=true,
allow={
['jail']=true,
['unjail']=true,
['bonus']=true,
['bonus-respawn']=true
}
}
Role{
name='Donator',
short_hand='Don',
tag='[Donator]',
group='HiMember',
colour={r=230,g=99,b=34},
is_donator=true,
allow_afk_kick=true,
allow={
['set-home']=true,
['home']=true,
['return']=true,
}
}
Role{
name='Partner',
short_hand='Part',
tag='[Partner]',
group='HiMember',
colour={r=140,g=120,b=200},
allow_afk_kick=false,
is_spectator=true,
allow={
['global-chat']=true,
}
}
Role{
name='Veteran',
short_hand='Vet',
tag='[Veteran]',
group='HiMember',
colour={r=140,g=120,b=200},
is_timed=true,
is_verified=true,
allow_afk_kick=true,
time=600, -- 10 hours
allow={
['tree-decon']=true,
['create-poll']=true,
['repair']=true
}
}
-- Member
Role{
name='Member',
short_hand='Mem',
tag='[Member]',
group='Member',
colour={r=24,g=172,b=188},
is_verified=true,
allow_afk_kick=true,
allow={
['edit-tasklist']=true,
['make-warp']=true,
['nuke']=true,
['verified']=true
}
}
Role{
name='Regular',
short_hand='Reg',
tag='[Regular]',
group='Member',
colour={r=79,g=155,b=163},
allow_afk_kick=true,
is_timed=true,
time=180, -- 3 hours
allow={
['kill']=true,
['decon']=true,
['capsules']=true
}
}
-- Guest
Role{
name='Guest',
short_hand='',
tag='',
group='User',
colour={r=185,g=187,b=160},
allow_afk_kick=true,
is_default=true,
allow={
['player-list']=true,
['readme']=true,
['rockets']=true,
['science']=true,
['tasklist']=true,
['report']=true,
['warp-list']=true,
['polls']=true,
['tag']=true,
['tag-clear']=true,
['report']=true
}
}
-- Jail
Role{
name='Jail',
short_hand='Jail',
tag='[Jail]',
group='Jail',
colour={r=50,g=50,b=50},
is_jail=true,
is_antiroot=true,
block_auto_promote=true,
allow={}
}
Role.order = {
'Root',
'Community Manager',
'Developer',
'Administrator',
'Moderator',
'Trainee',
'Sponsor',
'Pay to Win',
'Donator',
'Partner',
'Veteran',
'Member',
'Regular',
'Guest',
'Jail'
}
Role.set_preassign{
["cooldude2606"]={"Developer","Admin","Mod"},
["aldldl"]={"Sponsor","Admin","Donator","Sponsor","Member","Mod"},
["arty714"]={"Admin","Community Manager","Member","Mod"},
["drahc_pro"]={"Admin","Member","Mod"},
["mark9064"]={"Admin","Member","Mod"}
}

View File

@@ -0,0 +1,532 @@
--- Adds roles where a player can have more than one role
-- @module ExpGamingCore.Role@4.0.0
-- @author Cooldude2606
-- @license https://github.com/explosivegaming/scenario/blob/master/LICENSE
-- @alias Role
-- Module Require
local Group = require('ExpGamingCore.Group')
local Game = require('FactorioStdLib.Game')
-- Local Variables
local role_change_event_id = script.generate_event_name('on_role_change')
local RoleGlobal
-- Module Define
local _RoleSelfReference
local module_verbose = false
local Role = {
_prototype={},
order={},
flags={},
actions={},
preassign={},
meta={times={},groups={},count=0},
roles=setmetatable({},{
__index=table.autokey,
__newindex=function(tbl,key,value)
rawset(tbl,key,_RoleSelfReference.define(value))
end
}),
on_init=function(self)
if loaded_modules['ExpGamingCore.Server'] then require('ExpGamingCore.Server').add_module_to_interface('Role','ExpGamingCore.Role') end
if loaded_modules['ExpGamingCore.Command'] then require(module_path..'/src/commands',{self=self}) end
if loaded_modules['ExpGamingCore.Sync'] then require(module_path..'/src/sync',{self=self,RoleGlobal=RoleGlobal}) end
end,
on_post=function(self)
-- creates a server role with root access
self.meta.server = self{name='SERVER',group='Root',is_root=true,allow={}}
-- loads the roles in config
require(module_path..'/config',{Role=self})
self.order[0] = 'SERVER'
-- joins role allows into a chain
local previous
for index,role_name in pairs(self.order) do
local role = self.get(role_name)
if not role then error('Invalid role name in order listing: '..role_name) return end
if role.is_default then self.meta.default = role end
if role.is_timed then self.meta.times[role.name] = {index,role.time*3600} end
if not self.meta.groups[role.group.name] then self.meta.groups[role.group.name] = {lowest=index,highest=index} end
if self.meta.groups[role.group.name].highest > index then self.meta.groups[role.group.name].highest = index end
if self.meta.groups[role.group.name].lowest < index then self.meta.groups[role.group.name].lowest = index end
if previous then setmetatable(previous.allow,{__index=role.allow}) end
self.meta.count = self.meta.count+1
role.index = index
previous = role
end
if previous then setmetatable(previous.allow,{__index=function(tbl,key) return false end})
else error('Invalid roles, no roles to load.') end
end
}
_RoleSelfReference=Role
-- Global Define
local global = {
change_cache_length=15,
changes={},
latest_change={},
preassign={},
players={},
roles={}
}
Global.register(global,function(tbl) RoleGlobal = tbl end)
-- Function Define
--- Used to set default roles for players who join
-- @usage Role.set_preassign{name={roles}}
function Role.set_preassign(tbl) if game then global.preassign = tbl else Role.preassign = tbl end end
--- Defines a new instance of a role
-- @usage Role.define{name='Root',short_hand='Root',tag='[Root]',group='Root',colour={r=255,b=255,g=255},is_root=true,allow={}} -- returns new role
-- @usage Role{name='Root',short_hand='Root',tag='[Root]',group='Root',colour={r=255,b=255,g=255},is_root=true,allow={}} -- returns new role
-- @tparam table obj contains the strings: name,short_hand,tag a table called allow a table called colour and a pointer to a permission group
-- @treturn Role the role which has been made
function Role.define(obj)
if not type_error(game,nil,'Cant define Role during runtime.') then return end
if not type_error(obj.name,'string','Role creation is invalid: role.name is not a string') then return end
if not is_type(obj.short_hand,'string') then obj.short_hand = obj.name:sub(1,3) end
if not is_type(obj.tag,'string') then obj.tag = '['..obj.short_hand..']' end
if not is_type(obj.colour,'table') then obj.colour = {r=255,b=255,g=255} end
if not type_error(obj.allow,'table','Role creation is invalid: role.allow is not a table') then return end
obj.group = Group.get(obj.group)
if not type_error(obj.group,'table','Role creation is invalid: role.group is invalid') then return end
if obj.time and not type_error(obj.time,'number','Role creation is invalid: role.time is not a number') then return end
verbose('Created Role: '..obj.name)
setmetatable(obj,{__index=Role._prototype})
obj.connected_players = setmetatable({self=obj,connected=true},Role._prototype.players_mt)
obj.players = setmetatable({self=obj},Role._prototype.players_mt)
rawset(Role.roles,obj.name,obj)
table.insert(Role.order,obj.name)
return obj
end
--- Used to get the role of a player or the role by name
-- @usage Role.get('foo') -- returns group foo
-- @usage Role.get(player) -- returns group of player
-- @tparam ?LuaPlayer|pointerToPlayer|string mixed can either be the name of the role or a player identifier
-- @treturn table the group which was found or nil
function Role.get(mixed,forceIsRole)
local player = game and Game.get_player(mixed)
if player == SERVER then return {Role.meta.server} end
if not forceIsRole and player then
local rtn = {}
if not global.players[player.index] then return Role.meta.default and {Role.meta.default} or {} end
for _,role in pairs(global.players[player.index]) do table.insert(rtn,Role.get(role)) end
return rtn
elseif is_type(mixed,'table') and mixed.group then return mixed
elseif is_type(mixed,'string') then return Role.roles[mixed]
elseif is_type(mixed,'number') then
for _,role in pairs(Role.roles) do
if role.index == mixed then return role end
end
end
end
--- Used to place a player into a role(s)
-- @usage Role.assign(player,'Root')
-- @usage Role.assign(player,{'Root','Foo'})
-- @tparam ?LuaPlayer|pointerToPlayer player the player to assign the role to
-- @tparam ?string|role|table the role to add the player to, if its a table then it will act recursively though the table
-- @tparam[opt='<server>'] ?LuaPlayer|pointerToPlayer by_player the player who assigned the roles to the player
-- @tparam[opt] table batch this is used internally to prevent multiple event calls, contains {role_index_in_batch,batch}
-- @treturn boolean was the player assigned the roles
function Role.assign(player,role,by_player,batch)
player = Game.get_player(player)
if not player then error('Invalid player #1 given to Role.assign.',2) return end
verbose('Assigning Roles: '..serpent.line(role)..' to: '..player.name)
-- this loops over a table of role if given; will return if ipairs returns, else will assume it was meant to be a role and error
if is_type(role,'table') and not role.name then
local ctn = 0
for n,_role in ipairs(role) do ctn=ctn+1 Role.assign(player,_role,by_player,{n,role}) end
if ctn > 0 then if not batch then table.insert(global.changes[player.index],{'assign',role}) global.latest_change = {player.index,'assign',role} end return end
end
role = Role.get(role)
if not role then error('Invalid role #2 given to Role.assign.',2) return end
-- this acts as a way to prevent the global table getting too full
if not global.changes[player.index] then global.changes[player.index]={} end
if #global.changes[player.index] > global.change_cache_length then table.remove(global.changes[player.index],1) end
if not batch then table.insert(global.changes[player.index],{'assign',role.name}) global.latest_change = {player.index,'assign',role.name} end
return role:add_player(player,by_player,batch)
end
--- Used to remove a player from a role(s)
-- @usage Role.unassign(player,'Root')
-- @tparam ?LuaPlayer|pointerToPlayer player the player to unassign the role to
-- @tparam ?string|role|table role the role to remove the player from, if its a table then it will act recursively though the table
-- @tparam[opt='<server>'] ?LuaPlayer|pointerToPlayer by_player the player who unassigned the roles from the player
-- @tparam[opt] table batch this is used internally to prevent multiple event calls
-- @treturn boolean was the player unassigned the roles
function Role.unassign(player,role,by_player,batch)
player = Game.get_player(player)
if not player then error('Invalid player #1 given to Role.unassign.',2) return end
verbose('Assigning Roles: '..serpent.line(role)..' to: '..player.name)
-- this loops over a table of role if given; will return if ipairs returns, else will assume it was meant to be a role and error
if is_type(role,'table') and not role.name then
local ctn = 0
for n,_role in ipairs(role) do ctn=ctn+1 Role.unassign(player,_role,by_player,{n,role}) end
if ctn > 0 then if not batch then table.insert(global.changes[player.index],{'unassign',role}) global.latest_change = {player.index,'unassign',role} end return end
end
role = Role.get(role)
if not role then error('Invalid role #2 given to Role.unassign.',2) return end
if not global.changes[player.index] then global.changes[player.index]={} end
-- this acts as a way to prevent the global table getting too full
if #global.changes[player.index] > global.change_cache_length then table.remove(global.changes[player.index],1) end
if not batch then table.insert(global.changes[player.index],{'unassign',role.name}) global.latest_change = {player.index,'unassign',role.name} end
return role:remove_player(player,by_player,batch)
end
--- Returns the highest role given in a list, if a player is passed then it returns the highest role of the player
-- @usage Role.get_highest{'Root','Admin','Mod'} -- returns Root (given that root is highest)
-- @usage Role.get_highest(player) -- returns the players highest role
-- @tparam ?table|LuaPlayer|pointerToPlayer options table of options or a player
-- @treturn role the highest role given in the options
function Role.get_highest(options)
local player = Game.get_player(options)
if player then options = Role.get(player) end
if not type_error(options,'table','Invalid argument #1 to Role.get_highest, options is not a table of roles.') then return end
local highest_index = -1
local highest
for _,role_name in pairs(options) do
local role = Role.get(role_name)
if not role then error('Invalid role inside options: '..serpent.line(role_name)) return end
if highest_index == -1 or role.index < highest_index then highest_index,highest = role.index,role end
end
return highest
end
--- Uses the change cache to revert changes to players roles
-- @usage Role.revert(player) -- reverts the last change to the players roles
-- @tparam ?LuaPlayer|pointerToPlayer player the player to revert the changes of
-- @tparam[opt] ?LuaPlayer|pointerToPlayer the player who preformed the role revert
-- @tparam[opt=1] count the number of reverts to do, if 0 all changes cached are reverted
-- @treturn number the number of changes that occurred
function Role.revert(player,by_player,count)
player = Game.get_player(player)
if not player then error('Invalid player #1 given to Role.revert.',2) return end
if count and not type_error(count,'number','Invalid argument #2 to Role.revert, count is not a number.') then return end
local changes = global.changes[player.index] or {}
if #changes == 0 then error('Player has no role changes logged, can not revert.') end
count = count or 1
local ctn = 0
if count > #changes or count == 0 then count = #changes end
for i = 1,count do
local change = table.remove(changes)
if not change then break end
local batch = is_type(change[2],'table') and change[2] or {change[2]}
if change[1] == 'assign' then Role.unassign(player,change[2],by_player,batch) end
if change[1] == 'unassign' then Role.assign(player,change[2],by_player,batch) end
ctn=ctn+1
end
return ctn
end
--- Adds a flag which can be set on roles; these flags act as a quick way to access general role changes
-- @usage Role.add_flag('is_admin',function(player,state) player.admin = state end) -- the function is passed player and if the flag is true or false
-- @tparam string flag the name of the flag that is being added
-- @tparam[opt] function callback the function(player,state) which is called when a player loses or gains a flag, if nil no function is called
function Role.add_flag(flag,callback)
if not type_error(flag,'string','Invalid argument #1 to Role.add_flag, flag is not a string.') then return end
if callback and not type_error(callback,'function','Invalid argument #2 to Role.add_flag, callback is not a function.') then return end
verbose('Added flag: '..flag)
Role.flags[flag] = callback or true
end
--- Checks if a player or role has the requested flag, if player all roles of player are checked (true has priority)
-- @usage Role.has_flag(role,'is_admin') -- returns true if this role has is_admin set
-- @tparam role|LuaPlayer|pointerToPlayer mixed the player or role that will be tested
-- @tparam string flag the flag to test for
-- @treturn boolean if the flag was true or false, false if nil
function Role.has_flag(mixed,flag)
if not type_error(flag,'string','Invalid argument #2 to Role.has_flag, flag is not a string.') then return end
local roles = Role.get(mixed)
if not type_error(roles,'table','Invalid argument #1 to Role.has_flag, mixed is not a role or player.') then return end
if #roles > 0 then for _,role in pairs(roles) do
if role:has_flag(flag) then return true end
end elseif roles:has_flag(flag) then return true end
return false
end
--- Adds a action to be used by the role system
-- @usage Role.add_action('foo')
-- @tparam string action the name of the action that will be added
function Role.add_action(action)
if not type_error(action,'string','Invalid argument #1 to Role.add_action, action is not a string.') then return end
verbose('Added action: '..action)
table.insert(Role.actions,action)
end
--- Checks if a player or role is allowed the requested action, if player all roles of player are checked (true has priority)
-- @usage Role.allowed(role,'foo') -- returns true if this role is allowed 'foo'
-- @tparam ?role|LuaPlayer|pointerToPlayer mixed the player or role that will be tested
-- @tparam string action the action to test for
-- @treturn boolean if the action is allowed for the player or role
function Role.allowed(mixed,action)
if not type_error(action,'string','Invalid argument #2 to Role.allowed, action is not a string.') then return end
local roles = Role.get(mixed)
if not type_error(roles,'table','Invalid argument #1 to Role.allowed, mixed is not a role or player.') then return end
if #roles > 0 then for _,role in pairs(roles) do
if role:allowed(action) then return true end
end elseif roles:allowed(action) then return true end
return false
end
--- Prints to all roles and players of those roles which are greater than the given role (or if inv then all below)
-- @usage Role.print('Admin','Hello, World!') -- returns the number of players who received the message
-- @tparam ?role|string role the role which acts as the turning point of the print (always included regardless of inv value)
-- @param rtn the value that will be returned to the players
-- @tparam[opt] table colour the colour that you want the message printed in
-- @tparam[opt=false] boolean inv true to print to roles below, false to print to roles above
-- @treturn number the number of players who received the message
function Role.print(role,rtn,colour,inv)
role = Role.get(role,true)
if not type_error(role,'table','Invalid argument #1 to Role.print, role is invalid.') then return end
if colour and not type_error(colour,'table','Invalid argument #3 to Role.print, colour is not a table.') then return end
if inv and not type_error(inv,'boolean','Invalid argument #4 to Role.print, inv is not a boolean.') then return end
local message = inv and {'ExpGamingCore-Role.default-print',rtn} or {'ExpGamingCore-Role.print',role.name,rtn}
local change = inv and 1 or -1
local ctn = 0
local i = role.index
while true do
local _role = Role.get(i,true)
if not _role then break end
ctn=ctn+_role:print(message,colour)
i=i+change
end
return ctn
end
--- Prints all registered roles and there important information (debug)
-- @tparam[opt] ?role|string the role to print the info of, if nil then all roles are printed in order of power
-- @tparam[opt=game.player] ?LuaPlayer|pointerToPlayer the player to print the info to, default the player who ran command
function Role.debug_output(role,player)
player = Game.get_player(player) or game.player
if not player then error('Invalid player #2 given to Role.debug_output.',2) return end
local function _output(_role)
local flags = {};for flag in pairs(Role.flags) do if _role:has_flag(flag) then table.insert(flags,flag) end end
local rtn = string.format('%s) %q-%q || Tag: %s Short Hand: %q Time: %s Flags: %s',
_role.index,_role.group.name,_role.name,_role.tag,_role.short_hand,tostring(_role.time),table.concat(flags,', '))
player_return(rtn,_role.colour,player)
end
if role then
role = Role.get(role)
if not type_error(role,'table','Invalid argument #1 to Role.print, role is invalid.') then return end
_output(role)
else for index,_role in pairs(Role.roles) do _output(_role) end end
end
--- Used to test if a role has a flag set
-- @usage role:has_flag('is_admin') -- returns true if the role has the flag 'is_admin'
-- @tparam string flag the flag to test for
-- @treturn boolean true if the flag is set else false
function Role._prototype:has_flag(flag)
if not self_test(self,'role','has_flag') then return end
if not type_error(flag,'string','Invalid argument #1 to role:has_flag, flag is not a string.') then return end
return self[flag] or false
end
--- Used to test if a role is allowed an action
-- @usage role:allowed('foo') -- returns true if the role is allowed 'foo'
-- @tparam string action the action to test for
-- @treturn boolean true if the action is allowed else false
function Role._prototype:allowed(action)
if not self_test(self,'role','allowed') then return end
if not type_error(action,'string','Invalid argument #1 to role:allowed, action is not a string.') then return end
if self.is_antiroot then return false end
return self.allow[action] or self.is_root or false -- still include is_root exception flag
end
--- Returns the players who have this role
-- @usage role:get_player() -- returns table of players
-- @usage role.players -- returns table of players
-- @usage role.connected_players -- returns table of online players
-- @tparam[opt] boolean online if true only returns online players
function Role._prototype:get_players(online)
if not self_test(self,'role','get_players') then return end
if online and not type_error(online,'boolean','Invalid argument #1 to role:get_players, online is not a boolean.') then return end
if not global.roles[self.name] then global.roles[self.name] = {} end
if self.is_default then if online then return game.connected_players else return game.players end end
local rtn = {}
for _,player_index in pairs(global.roles[self.name]) do
local player = game.players[player_index]
if player and not online or player.connected then table.insert(rtn,player) end
end
return rtn
end
-- this is used to create a connected_players table
Role._prototype.players_mt = {
__call=function(tbl) return tbl.self:get_players(tbl.connected) end,
__pairs=function(tbl)
local players = tbl.self:get_players(tbl.connected)
local function next_pair(tbl,key)
local k, v = next(players, key)
if v then return k,v end
end
return next_pair, players, nil
end,
__ipairs=function(tbl)
local players = tbl.self:get_players(tbl.connected)
local function next_pair(tbl,key)
local k, v = next(players, key)
if v then return k,v end
end
return next_pair, players, nil
end
}
--- Prints a message to all players who have this role
-- @usage role:print('Hello, World!') -- returns number of players who received the message
-- @param rtn the message to print to the players
-- @tparam[opt] table colour the colour to print the message in
-- @treturn number the number of players who received the message
function Role._prototype:print(rtn,colour)
if not self_test(self,'role','print') then return end
if colour and not type_error(colour,'table','Invalid argument #2 to Role.print, colour is not a table.') then return end
local ctn = 0
for _,player in pairs(self:get_players(true)) do ctn=ctn+1 player_return(rtn,colour,player) end
return ctn
end
--- Returns a table that describes all the permissions and which this role is allowed
-- @usage role:get_permissions() -- returns table of permissions
-- @treturn table a table of permissions, only includes ones which were defined with Role.add_action
function Role._prototype:get_permissions()
if not self_test(self,'role','get_permissions') then return end
local rtn = {}
for _,action in pairs(Role.actions) do rtn[action] = self:allowed(action) end
return rtn
end
--- Adds a player to this role (players can have more than one role)
-- @usage role:add_player(player)
-- @tparam ?LuaPlayer|PointerToPlayer player the player to add
-- @tparam[opt] ?LuaPlayer|PointerToPlayer by_player the player who ran the command
-- @tparam[opt] table batch this is used internally to prevent multiple event calls
function Role._prototype:add_player(player,by_player,batch)
if not self_test(self,'role','add_player') then return end
player = Game.get_player(player)
if not player then error('Invalid player #1 given to role:add_player.',2) return end
by_player = Game.get_player(by_player) or SERVER
if not global.roles[self.name] then global.roles[self.name] = {} end
if not global.players[player.index] then global.players[player.index] = {} end
local highest = Role.get_highest(player) or Role.meta.default
for _,player_index in pairs(global.roles[self.name]) do if player_index == player.index then return end end
table.insert(global.roles[self.name],player.index)
table.insert(global.players[player.index],self.name)
script.raise_event(role_change_event_id,{
name=role_change_event_id,
tick=game.tick,
player_index=player.index,
by_player_index=by_player.index,
old_highest=highest.name,
role_name=self.name,
batch=batch and batch[2] or {self.name},
batch_index=batch and batch[1] or 1,
effect='assign'
})
end
--- Removes a player from this role (players can have more than one role)
-- @usage role:remove_player(player)
-- @tparam ?LuaPlayer|PointerToPlayer player the player to remove
-- @tparam[opt] ?LuaPlayer|PointerToPlayer by_player the player who ran the command
-- @tparam[opt] table batch this is used internally to prevent multiple event calls
function Role._prototype:remove_player(player,by_player,batch)
if not self_test(self,'role','add_player') then return end
player = Game.get_player(player)
if not player then error('Invalid player #1 given to role:remove_player.',2) return end
by_player = Game.get_player(by_player) or SERVER
if not global.roles[self.name] then global.roles[self.name] = {} end
if not global.players[player.index] then global.players[player.index] = {} end
local highest = Role.get_highest(player) or Role.meta.default
local index = 0
for _index,player_index in pairs(global.roles[self.name]) do if player_index == player.index then index=_index break end end
table.remove(global.roles[self.name],index)
for _index,role_name in pairs(global.players[player.index]) do if role_name == self.name then index=_index break end end
table.remove(global.players[player.index],index)
script.raise_event(role_change_event_id,{
name=role_change_event_id,
tick=game.tick,
player_index=player.index,
by_player_index=by_player.index,
old_highest=highest.name,
role_name=self.name,
batch=batch and batch[2] or {self.name},
batch_index=batch and batch[1] or 1,
effect='unassign'
})
end
-- Event Handlers Define
Event.add(role_change_event_id,function(event)
-- variable init
local player = Game.get_player(event)
local by_player = Game.get_player(event.by_player_index) or SERVER
local role = Role.get(event.role_name)
local highest = Role.get_highest(player)
if not highest then Role.meta.default:add_player(player) highest = Role.meta.default end
-- gets the flags the player currently has
for flag,callback in pairs(Role.flags) do if is_type(callback,'function') then callback(player,Role.has_flag(player,flag)) end end
-- assign new tag and group of highest role
Group.assign(player,highest.group)
local old_highest_tag = Role.get(event.old_highest).tag or ''
local start, _end = string.find(player.tag,old_highest_tag,1,true)
if start and old_highest_tag ~= highest.tag then player.tag = string.sub(player.tag,0,start-1)..highest.tag..string.sub(player.tag,_end+1) end
if not start then player.tag = highest.tag player_return({'ExpGamingCore-Role.tag-reset'},nil,player) end
if player.online_time > 60 then
-- send a message to other players
if event.batch_index == 1 then
local names = {}
for _,name in pairs(event.batch) do local next_role = Role.get(name) if next_role then table.insert(names,next_role.name) end end
if event.effect == 'assign' then
if not role.is_jail then player.play_sound{path='utility/achievement_unlocked'} end
game.print{'ExpGamingCore-Role.default-print',{'ExpGamingCore-Role.assign',player.name,table.concat(names,', '),by_player.name}}
else
player.play_sound{path='utility/game_lost'}
game.print{'ExpGamingCore-Role.default-print',{'ExpGamingCore-Role.unassign',player.name,table.concat(names,', '),by_player.name}}
end
end
-- log change to file
game.write_file('role-change.json',
table.json({
tick=game.tick,
effect=event.effect,
role_name=role.name,
player_name=player.name,
by_player_name=by_player.name,
play_time=player.online_time,
highest_role_name=highest.name,
old_highest=event.highest,
batch_count=#event.batch,
batch_index=event.batch_index
})..'\n'
, true, 0)
end
end)
Event.add(defines.events.on_player_created,function(event)
local player = Game.get_player(event)
local highest = Role.get_highest(player) or Role.meta.default
Group.assign(player,highest.group)
player.tag=highest.tag
if global.preassign[player.name:lower()] then Role.assign(player,global.preassign[player.name:lower()]) end
if Role.preassign[player.name:lower()] then Role.assign(player,Role.preassign[player.name:lower()]) end
end)
Event.add(defines.events.on_tick,function(event)
if game.tick%(3600*5) ~= 0 then return end -- every 5 minutes
for _,player in pairs(game.connected_players) do
if not Role.has_flag(player,'block_auto_promote') then
local highest = Role.get_highest(player)
for role_name, time in pairs(Role.meta.times) do
if highest.index > time[1] and (player.online_time) > time[2] then Role.assign(player,role_name) end
end
end
end
end)
-- Module Return
-- calling will attempt to define a new role
return setmetatable(Role,{__call=function(tbl,...) return tbl.define(...) end})

View File

@@ -0,0 +1,6 @@
[ExpGamingCore-Role]
default-print=[Everyone]: __1__
print=[__1__]: __2__
assign=__1__ was assigned to __2__ by __3__
unassign=__1__ was unassigned from __2__ by __3__
tag-reset=Your Tag was reset due to a role change

View File

@@ -0,0 +1,24 @@
{
"name": "ExpGamingCore.Role",
"version": "4.0.0",
"description": "Adds roles where a player can have more than one role",
"location": "FSM_ARCHIVE",
"keywords": [
"Role",
"Ranks",
"Permissions",
"Alowed",
"Admin"
],
"dependencies": {
"ExpGamingLib": "^4.0.0",
"FactorioStdLib.Table": "^0.8.0",
"FactorioStdLib.Game": "^0.8.0",
"ExpGamingCore.Group": "^4.0.0",
"ExpGamingCore.Server": "?^4.0.0",
"ExpGamingCore.Command": "?^4.0.0",
"ExpGamingCore.Sync": "?^4.0.0"
},
"collection": "ExpGamingCore@4.0.0",
"submodules": {}
}

View File

@@ -0,0 +1,28 @@
local Role = self
commands.add_validation('player-rank',function(value,event)
local player,err = commands.validate['player'](value)
if err then return commands.error(err) end
local rtn = Role.get_highest(player).index > Role.get_highest(event).index and player or player.index == event.player_index and player or nil
if not rtn then return commands.error{'ExpGamingCore_Command.error-player-rank'} end return rtn
end)
commands.add_validation('player-rank-online',function(value,event)
local player,err = commands.validate['player-online'](value)
if err then return commands.error(err) end
local player,err = commands.validate['player-rank'](player)
if err then return commands.error(err) end
return player
end)
commands.add_validation('player-rank-alive',function(value,event)
local player,err = commands.validate['player-alive'](value)
if err then return commands.error(err) end
local player,err = commands.validate['player-rank'](player,event)
if err then return commands.error(err) end
return player
end)
commands.add_middleware(function(player,command_name,event)
return Role.allowed(player,command_name)
end)

View File

@@ -0,0 +1,82 @@
local Role = self
local RoleGlobal = RoleGlobal
local Sync = require('ExpGamingCore.Sync')
local Game = require('FactorioStdLib.Game')
local Color = require('FactorioStdLib.Color')
-- just to hard reset the role sync
function Sync.set_roles(...)
Role.set_preassign(...)
end
-- used to assign the role if the player is online, or add to the the preassign
function Sync.assign_role(player_name,roles,by_player_name)
if not game then return end
local preassign = RoleGlobal.preassign
local player_roles = preassign[player_name]
if Game.get_player(player_name) then Role.assign(player_name,roles,by_player_name)
else
if not is_type(roles,'table') then roles = {roles} end
if not player_roles then preassign[player_name] = roles return end
for _,role_name in pairs(roles) do
if not table.includes(player_roles,role_name) then table.insert(player_roles,role_name) end
end
end
end
-- used to unassign the role if the player is online, or removes the preassign
function Sync.unassign_role(player_name,roles,by_player_name)
if not game then return end
local preassign = RoleGlobal.preassign
local player_roles = preassign[player_name]
if Game.get_player(player_name) then Role.unassign(player_name,roles,by_player_name)
else
if not is_type(roles,'table') then roles = {roles} end
if not player_roles then preassign[player_name] = nil return end
for _,role_name in pairs(roles) do
local index = table.index(player_roles,role_name)
if index then table.remove(player_roles,index) end
end
end
end
Sync.add_update('roles',function()
if not game then return {'Offline'} end
local _rtn = {}
for _,role in pairs(Role.roles) do
local players = role:get_players()
local _players = {}
for k,player in pairs(players) do _players[k] = player.name end
local online = role:get_players(true)
local _online = {}
for k,player in pairs(online) do _online[k] = player.name end
_rtn[role.name] = {players=_players,online=_online,n_players=#_players,n_online=#_online}
end
return _rtn
end)
-- Adds a caption to the info gui that shows the rank given to the player
if Sync.add_to_gui then
Sync.add_to_gui(function(player)
local names = {}
for _,role in pairs(Role.get(player)) do table.insert(names,role.name) end
return 'You have been assigned the roles: '..table.concat(names,', ')
end)
end
-- adds a discord emit for rank changing
Event.add('on_role_change',function(event)
local role = Role.get(event.role_name)
local player = Game.get_player(event)
local by_player = Game.get_player(event.by_player_index) or SERVER
if role.is_jail and RoleGlobal.last_change[1] ~= player.index then
Sync.emit_embedded{
title='Player Jail',
color=Color.to_hex(defines.textcolor.med),
description='There was a player jailed.',
['Player:']='<<inline>>'..player.name,
['By:']='<<inline>>'..by_player.name,
['Reason:']='No Reason'
}
end
end)

View File

@@ -0,0 +1,562 @@
--- Adds a thread system and event listening and a admin bypass (recommend to disable /c and use optional /interface)
-- @module ExpGamingCore.Server
-- @alias Server
-- @author Cooldude2606
-- @license https://github.com/explosivegaming/scenario/blob/master/LICENSE
local Game = require('FactorioStdLib.Game')
local Color = require('FactorioStdLib.Color')
local Server = {}
local module_verbose = false --true|false
--- Global Table
-- @table global
-- @field all a list of every thread (indexed by uuid)
-- @field queue an index for threads which will be resolved (contains uuids)
-- @field tick an index for threads which will run every tick (contains uuids)
-- @field timeout an index for threads which will timeout (contains uuids)
-- @field events an index of threads based on event ids (contains uuids)
-- @field paused an index of paused threads (contains uuids)
-- @field named a name index for thread uuids
-- @field print_to contains players that event details will be printed to
-- @field uuid contains the random number generator for the uuid system
local pre_load_uuid = 0
local global = {
all={_n=0},
queue={},
tick={},
timeout={},
events={},
paused={},
named={},
print_to={},
uuid=0
}
Global.register(global,function(tbl) global = tbl end)
--- Used to generate a new uuid for the thread system
-- @usage local uuid = tostring(Server.uuid) -- calling tostring locks the value
-- @treturn string the new uuid
Server.uuid = add_metatable({},function()
-- when it is called as a function
local uuid
if game then
global.uuid=global.uuid+1
uuid=global.uuid+pre_load_uuid
verbose('Game UUID Increased: '..global.uuid..' ('..uuid..')')
else
pre_load_uuid=pre_load_uuid+1
uuid=pre_load_uuid
verbose('Load UUID Increased: '..pre_load_uuid..' ('..uuid..')')
end
return add_metatable({uuid},nil,function(tbl) return string.format("%x", tostring(tbl[1])) end)
end,function()
-- when it is treated as a string
return tostring(Server.uuid())
end)
--- Redirect to the thread index
-- @usage Server.threads -- return #global.all
-- @usage Server.threads -- return global.all
-- @treturn[1] number the number of threads
-- @treturn[2] table table of all threads
Server.threads = setmetatable({},{
__metatable=false,
__call=function(tbl) return global.all._n end,
__len=function(tbl) return global.all._n end,
__index=function(tbl,key) return rawget(global.all,key) end,
__newindex=function(tbl,key,value) rawset(global.all,key,value) end,
__pairs=function(tbl)
local tbl = global.all
local function next_pair(tbl,key)
local k, v = next(tbl, key)
if type(v) ~= nil and k ~= '_n' then return k,v
else return next(tbl, k) end
end
return next_pair, tbl, nil
end
})
--- Generates a new thread object
-- @usage Server.new_thread{name='foo',data={}}
-- @tparam table obj the attributes to give to the thread
-- @treturn Server._thread the new thread created
function Server.new_thread(obj) return Server._thread.create(obj) end
--- Used to get a thread via uuid or name (if one is assigned)
-- @usage Server.get_thread('decon') -- return thread
-- @param mixed either a uuid or the name given to a thread
-- @treturn[1] Server._thread the thread by that name or uuid
-- @treturn[2] boolean if false is returned then no thread exists
function Server.get_thread(mixed)
local threads = global
if threads.named[mixed] then return threads.all[threads.named[mixed]]
elseif threads.paused[mixed] then return threads.all[threads.paused[mixed]]
elseif threads.all[mixed] then return threads.all[mixed]
else return false end
end
--- Adds a thread into the resolve queue, can be used to lower lag
-- @usage Server.queue_thread(thread) -- return true/false
-- @tparam Server._thread thread_to_queue the thread to be added to the queue, must be open and have a on_resolve function
-- @treturn boolean was it added successfully
function Server.queue_thread(thread_to_queue)
if not thread_to_queue and not thread_to_queue.valid and not thread_to_queue:valid() then return false end
if not thread_to_queue._resolve then return false end
table.insert(global.queue,thread_to_queue.uuid)
return true
end
--- Closes all active threads, can use force if it causes errors
-- @usage Server.close_all_threads() -- asks all threads to close
-- @usage Server.close_all_threads(true) -- forcefully close all threads
-- @tparam boolean with_force use force when closing
function Server.close_all_threads(with_force)
if not with_force then
for uuid,thread in pairs(Server.threads) do thread:close() end
else global.Server(true) end -- idk why you cant just use global even when global is defined at the top to be overridden
end
--- Runs all the threads which have opened with an on_tick event
-- @usage Server.run_tick_threads()
function Server.run_tick_threads()
table.each(global.tick,function(uuid)
local next_thread = Server.get_thread(uuid)
if next_thread and next_thread:valid() and next_thread._tick then
local success, err = Manager.sandbox(next_thread._tick,next_thread._env,next_thread)
if not success then next_thread:error(err) end
end
end)
end
--- Checks the timeout on all active timeout threads
-- @usage Server.check_timeouts()
function Server.check_timeouts()
table.each(global.timeout,function(uuid)
local next_thread = Server.get_thread(uuid)
if next_thread and next_thread:valid() then
next_thread:check_timeout()
end
end)
end
--- Used to print event info to a player
-- @usage Server._thread_debuger('Cooldude2606','on_player_died',true) -- will output event info to 'Cooldude2606' for 'on_player_died'
-- @tparam ?name|index|LuaPlayer player the player that the info will be returned to
-- @tparam ?name|index event the event that info will be returned fo
-- @tparam[opt=toggle] boolean state will info be returned, nil to toggle current state
function Server._thread_debuger(player,event,state)
player = Game.get_player(player)
event = tonumber(event) or Manager.event.names[event]
local print_to = global.print_to
print_to[player.index] = print_to[player.index] or {}
if state then print_to[player.index][event] = state
elseif print_to[player.index][event] then print_to[player.index][event] = false
else print_to[player.index][event] = true end
end
--- Calls all threads on a certain game event (used with Event.add)
-- @local Server._thread_handler
-- @usage Event.add(defines.events,Server._thread_handler) -- adds this handler
-- @tparam table event the event that is called
function Server._thread_handler(event)
-- returns to players who have set _thread_debuger to true
table.each(global.print_to,function(print_to,player_index,event)
if event.name == defines.events.on_tick then return true end
if print_to[event.name] then
player_return(event,defines.textcolor.bg,player_index)
end
end,event)
-- gets the thread uuids
local event_id = event.name
local threads = global.events[event_id]
if not threads then return end
-- loops over the uuids
table.each(threads,function(uuid)
local thread = Server.get_thread(uuid)
if thread and thread:valid() then
if is_type(thread._events[event_id],'function') then
-- runs the function in the same env it was created (avoids desyncs)
local success, err = Manager.sandbox(thread._events[event_id],thread._env,thread,event)
-- if there is an error it asks the thread to deal with it
if not success then thread:error(err) end
end
end
end)
end
--[[ cant be used V
--- Adds a event handler to tell threads about events
-- @usage Server.add_thread_handler(defines.event)
-- @tparam number event the event to run the thread handler on`
-- @treturn boolean if the handler was added
function Server.add_thread_handler(event)
if not is_type(event,'number') then return false end
local threads = global
if not threads.events[event] then
threads.events[event] = {}
Event.add(event,Server._thread_handler)
return true
end
return false
end
]]
--- Acts as a bypass for running functions, can accept a string
-- @usage Server.interface('local x = 1+1 print(x) return x') -- return 2
-- @usage Server.interface('local x = 1+1 print(x)',true) -- will create a thread to run as root (this is the bypass)
-- @tparam ?string|function callback function to be ran
-- @tparam[opt] ?Server._thread|true use_thread run the command on a pre-made thread or let it make its own
-- @tparam[opt] table env run the env to run the command in must have _env key as true to be
-- @param[opt] ... any args you want to pass to the function
-- @treturn[1] boolean if no thread then it will return a true for the success
-- @return[1] if no thread then it will return the value(s) returned by the callback
-- @treturn[2] boolean if no thread then it will return a false for the success
-- @treturn[2] string if no thread then it will return a an err value
function Server.interface(callback,use_thread,env,...)
if use_thread then
-- if use_thread is true then it makes a new thread
if use_thread == true then use_thread = Server.new_thread{data={callback,env,...}} end
-- creates the resolve function for the thread
use_thread:on_event('resolve',function(thread)
local thread_callback = table.remove(thread.data,1)
thread_callback = is_type(thread_callback,'function') and thread_callback or loadstring(thread_callback)
local thread_env = table.remove(thread.data,1)
if is_type(thread_env,'table') and not is_type(thread_env.__self,'userdata') and thread_env._env == true then
local success, err = Manager.sandbox(thread_callback,thread_env,unpack(thread.data))
if not success then error(err) end
return err
else
local success, err = Manager.sandbox(thread_callback,{},env,unpack(thread.data))
if not success then error(err) end
return err
end
end)
-- opens the thread and then queues it
use_thread:open()
Server.queue_thread(use_thread)
else
local _callback = callback
if not is_type(callback,'function') then
local rtn, err = loadstring(callback)
if err then return false, err end
_callback = rtn
end
if is_type(env,'table') and not is_type(env.__self,'userdata') and env._env == true then
local success, err = Manager.sandbox(_callback,env,...)
if not success then return success,err
else return success, unpack(err) end
else
local success, err = Manager.sandbox(_callback,{},env,...)
if not success then return success,err
else return success, unpack(err) end
end
end
end
--- The class for the server threads, allows ability to run async function
-- @type Thread
-- @alias Server._thread
-- @field name the name that is given to the thread, use for easy later indexing
-- @field timeout the time in ticks that the thread will have before it times out
-- @field reopen when true the thread will reopen itself until set to false, combine with timeout to create a long on_nth_tick wait
-- @field data any data that the thread will be able to access
Server._thread = {}
local _env_metatable = {
__index=function(tbl,key) if rawget(tbl,'_modules') and tbl._modules[key] then return require(tbl._modules[key]) else return rawget(_G,key) end end
}
--- Returns a new thread object
-- @usage new_thread = thread:create()
-- @tparam[opt={}] table obj all values are opt {timeout=int,name=str,data=any}
-- @treturn Server._thread the new thread object
function Server._thread.create(obj)
obj = obj or {}
setmetatable(obj,{__index=Server._thread})
obj.uuid = tostring(Server.uuid)
-- creates a variable containing all upvalus for the thread (stops desyncs)
obj._env = get_upvalues(2)
obj._env._env = true
obj._env._ENV = nil -- prevents infinite recursion
-- removes any modules from the _env to save space in global (less time to serialize)
obj._env._modules = {}
for name,value in pairs(obj._env) do
if is_type(value,'table') and value._moduleName and loaded_modules[value._moduleName] == value then
obj._env._modules[name] = value._moduleName obj._env[name] = nil
end
end
setmetatable(obj._env,_env_metatable)
local name = obj.name or 'Anon'
verbose('Created new thread: '..name..' ('..obj.uuid..')')
return obj
end
--- Opens and queues a thread
-- @usage Server._thread:queue() -- returns true/false
-- @treturn boolean was the thread queued successfully
-- @see Server.queue_thread
function Server._thread:queue()
self:open()
return Server.queue_thread(self)
end
--- Test if the thread has all required parts
-- @usage if thread:valid() then end -- basic test for valid
-- @tparam[opt=false] boolean skip_location_check true to skip the location checking
-- @treturn boolean is the thread valid
function Server._thread:valid(skip_location_check)
skip_location_check = skip_location_check or false
if is_type(self.uuid,'string') and
skip_location_check or is_type(self.opened,'number') and
skip_location_check or is_type(global.all[self.uuid],'table') and
is_type(self.timeout) or is_type(self.timeout,'number') and
is_type(self.name) or is_type(self.name,'string') and
is_type(self._close) or is_type(self._close,'function') and
is_type(self._timeout) or is_type(self._timeout,'function') and
is_type(self._tick) or is_type(self._tick,'function') and
is_type(self._resolve) or is_type(self._resolve,'function') and
is_type(self._success) or is_type(self._success,'function') and
is_type(self._error) or is_type(self._error,'function') then
-- all above must be true to be valid, must accept nil and function
return true
end
return false
end
--- Opens the thread; indexes this thread in the global index
-- @usage thread:open() -- return true
-- @treturn boolean if the thread was opened successfully
function Server._thread:open()
-- if the thread is valid and not already opened
if not self:valid(true) or self.opened then return false end
local uuid = self.uuid
-- sets the thread to open, this is the tick it was opened
self.opened = game.tick
-- creates the global index
global.all[uuid] = global.all[uuid] or self
global.all._n = global.all._n+1
-- indexes the thread in other places if it has more function
-- if it was paused before (ie did not run any events) then the index is removed from the paused index
if global.paused[self.name] then global.paused[self.name] = nil end
-- if it has a timeout or on_tick handler then it is indexed in those indexes
if is_type(self.timeout,'number') then table.insert(global.timeout,uuid) end
if is_type(self._tick,'function') then table.insert(global.tick,uuid) end
-- if the thread is given a name then a index from the name to uuid is made
if is_type(self.name,'string') then global.named[self.name] = global.named[self.name] or self.uuid end
-- if there are event handlers then it will loop over them and add them to the event index
if is_type(self._events,'table') then
table.each(self._events,function(callback,event,global,uuid)
-- cant be used V
--Server.add_thread_handler(event)
if not global.events[event] then global.events[event] = {} end
table.insert(global.events[event],uuid)
end,global,self.uuid)
end
local name = self.name or 'Anon'
verbose('Opened thread: '..name..' ('..self.uuid..')')
return true
end
--- Inverse of thread:open() - Removes all indexes to this thread, most cases this will cause it to become inaccessible
-- @usage thread:close() -- return true
-- @treturn boolean if the thread had a on_close function
function Server._thread:close()
local uuid = self.uuid
local _return = false
-- if there is a call to the threads on close event, will later return true
if is_type(self._close,'function') then Manager.sandbox(self._close,self._env,self) _return = true end
-- will search every possible location for this thread and remove it
local _,key = table.find(global.queue,function(v,k,uuid) return v == uuid end,uuid)
if key then table.remove(global.queue,key) end -- queue
_,key = table.find(global.timeout,function(v,k,uuid) return v == uuid end,uuid)
if key then table.remove(global.timeout,key) end -- timeouts
_,key = table.find(global.tick,function(v,k,uuid) return v == uuid end,uuid)
if key then table.remove(global.tick,key) end -- on_tick
-- then will look for it in the event handlers and remove it if found
if is_type(self._events,'table') then
table.each(self._events,function(callback,event)
if global.events[event] then
local _,key = table.find(global.events[event],function(v,k,uuid) return v == uuid end,uuid)
if key then table.remove(global.events[event],key) end
-- cant be used V
--if #global.events[event] == 0 then Event.remove(event,Server.game_event) global.events[event] = nil end
end
end)
end
-- sets the thread to closed
self.opened=nil
-- unless the thread has a name or is assigned to be reopened
if self.reopen == true then self:open() else
-- if it has a name but not assigned to reopen then it will become 'paused'
if is_type(self.name,'string') then global.paused[self.name]=self.uuid
-- else it will just be wiped from the global index
else global.all[uuid] = nil global.all._n = global.all._n-1 end
end
local name = self.name or 'Anon'
verbose('Closed thread: '..name..' ('..self.uuid..')')
return _return
end
--- Trigger the on_resolve function and closes the thread - error and success called based on result of pcall (useful for async)
-- @usage thread:resolve(x,y,z) -- return true
-- @param[opt] ... any arguments you want to pass to the resolve function
-- @treturn boolean true if the thread called on_success or on_error
function Server._thread:resolve(...)
local _return = false
-- checks if the resolve handler is still present
if is_type(self._resolve,'function') then
local success, err = Manager.sandbox(self._resolve,self._env,self,...)
if success then
-- if it was successful then it will attempt to call the success handler
if is_type(self._success,'function') then
-- interface is used as a way to delay the success call till the next tick
Server.interface(function(thread,err)
local _success, _err = Manager.sandbox(thread._success,thread._env,thread,err)
if not _success then thread:error(_err) end
end,true,self,err)
-- later returns true if there was a call to the success handler
_return = true
end
-- if there is an error the thread is asked to deal with it, returns true/false based on result of handler
else _return = self:error(err) end
end
-- closes the thread as it is no longer needed as its command has be ran
self:close()
return _return
end
--- Checks the timeout on a thread - if timed out then it calls on_timeout and closes
-- @usage thread:check_timeout() -- return true
-- @treturn boolean if the thread timed out
function Server._thread:check_timeout()
local _return = false
-- makes sure the thread is still valid
if not self:valid() then return false end
-- checks if the thread has been opened longer than its time out period
if is_type(self.timeout,'number') and game.tick >= (self.opened+self.timeout) then
if is_type(self._timeout,'function') then
-- we do not care if the time out has caused an error as it is in most cases an error in its own right
Manager.sandbox(self._timeout,self._env,self)
end
_return = true
-- closes the thread to prevent any further event calls
self:close()
end
return _return
end
--- Used to check and raise the error handler of the thread, if not present it raises an error
-- @usage thread:error(err) -- return true
-- @tparam string err the err to be raised
-- @treturn boolean did the thread have an error handler
function Server._thread:error(err)
local _return = false
if is_type(self._error,'function') then
Manager.sandbox(self._error,self._env,self,err)
_return = true
else
self:close() -- no matter what happens next this thread will be closed
local name = self.name or self.uuid
error('Thread Error (no handler) on <'..name..'>: '..err)
end
return _return
end
--- Set function to run then an event is triggered, none of them are 'needed' but you are advised to have at least one
-- @usage thread:on_event('close',function) -- if event is not one below then a game event is used
-- @usage thread_only_events = ['close','timeout','tick','resolve','success','error']
-- @tparam ?string|index event the name of the event that the function should be called on
-- @tparam function callback the function which is called by the event trigger
-- @treturn table returns self so that they can be chained together
function Server._thread:on_event(event,callback)
local events = {'close','timeout','tick','resolve','success','error'}
-- searches the above list for the event
local value = table.find(events,function(v,k,find) return v == string.lower(find) end,event)
if value and is_type(callback,'function') then
-- if it is a thread_only_event then it will add it to its core values
self['_'..value] = callback
elseif is_type(event,'number') and is_type(callback,'function') then
-- other wise it is appended to the event index of the thread
if not self._events then self._events = {} end
self._events[event] = callback
end
-- returns self to allowing chaining of on_event():on_event():on_event() etc..
return self
end
Event.add(defines.events.on_tick,function(event)
-- uses its own on_tick event so that other actions can be tested for
if event.tick < 10 then return end
if #global.tick > 0 then Server.run_tick_threads() end -- on tick events
if #global.timeout > 0 then Server.check_timeouts() end -- timeout checks
if #global.queue > 0 then -- resolve one thread
local current_thread = global.all[global.queue[1]]
if current_thread and current_thread:valid() then current_thread:resolve() end
end
end)
script.on_load(function(event)
-- sets up metatable again so that threads continue to work
for uuid,thread in pairs(Server.threads) do
setmetatable(thread,{__index=Server._thread})
setmetatable(thread._env,_env_metatable)
end
end)
function Server:on_init()
for name,id in pairs(defines.events) do if not script.get_event_handler(id) then Event.add(id,Server._thread_handler) end end
if loaded_modules['ExpGamingCore.Command'] then verbose('ExpGamingCore.Command is installed; Loading commands src') require(module_path..'/src/commands',{Server=Server}) end
end
return Server
--[[
Thread Example:
local thread = Server.new_thread{name='tree-decon',data={}}
-- user thread:on_event('tick') rather than thread:on_event(defines.events.on_tick) as it makes less lag
thread:on_event('tick',function(self)
local trees = self.data
if #trees == 0 then return end
local tree = table.remove(trees,1)
if tree.valid then tree.destroy() end
end)
thread:on_event('error',function(self,err)
-- cant see how this can cause an error
-- but this is where error handling goes
-- any event including on_resolve and on_tick can raise this
end)
thread:on_event(defines.events.on_marked_for_deconstruction,function(self,event)
if event.entity.type == 'tree' then
table.insert(self.data,event.entity)
end
end)
thread:open()
local thread = Server.new_thread{name='print-place',data={}}
thread:on_event(defines.events.on_built_entity,function(self,event)
game.print('Events')
end)
thread:open()
all on_event functions can be chained from the thread creation rather than use variables eg:
Server.new_thread{
name='tree-decon',
data={}
}:on_event('tick',function(self)
local trees = self.data
if #trees == 0 then return end
local tree = table.remove(trees,1)
if tree.valid then tree.destroy() end
end):on_event('error',function(self,err)
-- cant see how this can cause an error
-- but this is where error handling goes
-- any event including on_resolve and on_tick can raise this
end):on_event(defines.events.on_marked_for_deconstruction,function(self,event)
if event.entity.type == 'tree' then
table.insert(self.data,event.entity)
end
end):open()
]]

View File

@@ -0,0 +1,26 @@
{
"name": "ExpGamingCore.Server",
"version": "4.0.0",
"description": "Adds a thread system and event listening and a admin bypass (recommend to disable /c and use optional /interface)",
"location": "FSM_ARCHIVE",
"keywords": [
"Library",
"Lib",
"ExpGaming",
"Core",
"Server",
"Thread",
"Interface",
"Events"
],
"dependencies": {
"ExpGamingLib": "^4.0.0",
"FactorioStdLib.Table": "^0.8.0",
"FactorioStdLib.Color": "^0.8.0",
"FactorioStdLib.String": "^0.8.0",
"FactorioStdLib.Game": "^0.8.0",
"ExpGamingCore.Command": "?^4.0.0"
},
"collection": "ExpGamingCore@4.0.0",
"submodules": {}
}

View File

@@ -0,0 +1,49 @@
--- Adds a thread system and event listening and a admin bypass (recommend to disable /c and use optional /interface)
-- @submodule ExpGamingCore.Server
-- @alias Server
-- @author Cooldude2606
-- @license https://github.com/explosivegaming/scenario/blob/master/LICENSE
--- This file will be loaded when ExpGamingCore.Command is present
-- @function _comment
local Server = Server
Server.interfaceCallbacks = {}
function Server.add_to_interface(loadAs,callback) Server.interfaceCallbacks[loadAs] = callback end
function Server.add_module_to_interface(loadAs,moduleName)
local moduleName = moduleName or tostring(_G.moduleName) or nil
if not moduleName then error('No module name supplied for: '..loadAs,2) return end
Server.add_to_interface(loadAs,function() return require(moduleName) end)
end
Server.add_module_to_interface('Server','ExpGamingCore.Server')
--- Runs the given input from the script
-- @command interface
-- @param code The code that will be ran
commands.add_command('interface','Runs the given input from the script', {
['code']={true,'string-inf'}
}, function(event,args)
local callback = args.code
-- looks for spaces, if non the it will prefix the command with return
if not string.find(callback,'%s') and not string.find(callback,'return') then callback = 'return '..callback end
-- sets up an env for the command to run in
local env = {_env=true,}
if game.player then
env.player = game.player
env.surface = game.player.surface
env.force = game.player.force
env.position = game.player.position
env.entity = game.player.selected
env.tile = game.player.surface.get_tile(game.player.position)
end
-- adds custom callbacks to the interface
for name,custom_callback in pairs(Server.interfaceCallbacks) do env[name] = custom_callback() end
-- runs the function
local success, err = Server.interface(callback,false,env)
-- if there is an error then it will remove the stacktrace and return the error
if not success and is_type(err,'string') then local _end = string.find(err,':1:') if _end then err = string.sub(err,_end+4) end end
-- if there is a value returned that is not nill then it will return that value
if err or err == false then player_return(err) end
end)

View File

@@ -0,0 +1,338 @@
--- Allows syncing with an outside server and info panel.
-- @module ExpGamingCore.Sync
-- @alias Sync
-- @author Cooldude2606
-- @license https://github.com/explosivegaming/scenario/blob/master/LICENSE
local Game = require('FactorioStdLib.Game')
local Color = require('FactorioStdLib.Color')
local Sync = {}
local Sync_updates = {}
local module_verbose = false --true|false
--- Global Table
-- @table global
-- @field server_name the server name
-- @field server_description a short description of the server
-- @field reset_time the reset time of the server
-- @field time the last known irl time
-- @field time_set the last in game time that the time was set
-- @field last_update the last time that this info was updated
-- @field time_period how often this information is updated
-- @field players a list of different player related states
-- @field ranks a list of player ranks
-- @field rockets the number of rockets launched
-- @field mods the mods which are loaded
local global = {
server_name='Factorio Server',
server_description='A factorio server for everyone',
reset_time='On Demand',
time='Day Mth 00 00:00:00 UTC Year',
date='0000/00/00',
time_set={0,'0.00M'},
last_update={0,'0.00M'},
time_period={18000,'5.00M'},
game_speed=1.0,
players={
online={},
n_online=0,
all={},
n_all=0,
admins_online=0,
afk_players={},
times={}
},
roles={admin={online={},players={},n_online=0,n_players=0},user={online={},players={},n_online=0,n_players=0}},
rockets=0,
mods={'Offline'}
}
Global.register(global,function(tbl) global = tbl end)
--- Player sub-table
-- @table global.players
-- @field online list of all players online
-- @field n_online the number of players online
-- @field all list of all player on or offline
-- @field n_all the number of players who have joined the server
-- @field admins_online the number of admins online
-- @field afk_players the number of afk players
-- @field times the play times of every player
--- Used to standardise the tick format for any sync info
-- @usage Sync.tick_format(60) -- return {60,'1.00M'}
-- @treturn {number,string} table containing both the raw number and clean version of a time
function Sync.tick_format(tick)
if not is_type(tick,'number') then error('Tick was not given to Sync.tick_format',2) end
return {tick,tick_to_display_format(tick)}
end
--- Prints to chat as if it were a player
-- @usage Sync.print('Test','Cooldude2606')
-- @tparam string player_message the message to be printed in chat
-- @tparam string player_name the name of the player sending the message
-- @tparam[opt] string player_tag the tag applied to the player's name
-- @tparam[opt] string player_colour the colour of the message, either hex or named colour
-- @tparam[opt] string prefix add a prefix before the chat eg [IRC]
function Sync.print(player_message,player_name,player_tag,player_colour,prefix)
if not player_message then error('No message given to Sync.print',2) end
local player = game.player or game.players[player_name]
local tag = player_tag and player_tag ~= '' and ' '..player_tag or ''
local colour = type(player_colour) == 'string' and player_colour or '#FFFFFF'
prefix = prefix and prefix..' ' or ''
-- if it is an in game player it will over ride the given params
if player then
tag = ' '..player.tag
colour = player.chat_color
player_name = player.name
else
-- converts colour into the accepted factorio version
if colour:find('#') then colour = Color.from_hex(colour)
else colour = defines.color[player_colour] end
end
game.print(prefix..player_name..tag..': '..player_message,colour)
end
--- Outline of the parameters accepted by Sync.emit_embedded
-- @table EmitEmbededParamaters
-- @field title the tile of the embed
-- @field color the color given in hex you can use Color.to_hex{r=0,g=0,b=0}
-- @field description the description of the embed
-- @field server_detail sting to add onto the pre-set server detail
-- @field fieldone the filed to add to the embed (key is name) (value is text) (start value with &lt;&lt;inline&gt;&gt; to make inline)
-- @field fieldtwo the filed to add to the embed (key is name) (value is text) (start value with &lt;&lt;inline&gt;&gt; to make inline)
--- Logs an embed to the json.data we use a js script to add things we cant here
-- @usage Sync.emit_embedded{title='BAN',color='0x0',description='A player was banned' ... }
-- @tparam table args a table which contains everything that the embedded will use
-- @see EmitEmbededParamaters
function Sync.emit_embedded(args)
if not is_type(args,'table') then error('Args table not given to Sync.emit_embedded',2) end
if not game then error('Game has not loaded',2) end
local title = is_type(args.title,'string') and args.title or ''
local color = is_type(args.color,'string') and args.color:find("0x") and args.color or '0x0'
local description = is_type(args.description,'string') and args.description or ''
local server_detail = is_type(args.server_detail,'string') and args.server_detail or ''
local mods_online = 'Mods Online: '..Sync.info.players.admins_online
-- creates the first field given for every emit
local done, fields = {title=true,color=true,description=true,server_detail=true}, {{
name='Server Details',
value='Server Name: ${serverName} Online Players: '..#game.connected_players..' '..mods_online..' Server Time: '..tick_to_display_format(game.tick)..' '..server_detail
}}
-- for each value given in args it will create a new field for the embed
for key, value in pairs(args) do
if not done[key] then
done[key] = true
local f = {name=key,value='',inline=false}
-- if <<inline>> is present then it will cause the field to be inline if the previous
local value, inline = value:gsub("<<inline>>",'',1)
if inline > 0 then f.inline = true end
f.value = value
table.insert(fields,f)
end
end
-- forms the data that will be emitted to the file
local log_data = {
title=title,
description=description,
color=color,
fields=fields
}
game.write_file('embedded.json',table.json(log_data)..'\n',true,0)
end
--- The error handle setup by sync to emit a discord embed for any errors
-- @local here
-- @function errorHandler
-- @tparam string err the error passed by the err control
error.addHandler('Discord Emit',function(err)
if not game then return error(error()) end
local color = Color and Color.to_hex(defines.textcolor.bg) or '0x0'
Sync.emit_embedded{title='SCRIPT ERROR',color=color,description='There was an error in the script @Developers ',Error=err}
end)
--- Used to get the number of admins currently online
-- @usage Sync.count_admins() -- returns number
-- @treturn number the number of admins online
function Sync.count_admins()
-- game check
if not game then return 0 end
local _count = 0
for _,player in pairs(game.connected_players) do
if player.admin then _count=_count+1 end
end
return _count
end
--- Used to get the number of afk players defined by 2 min by default
-- @usage Sync.count_afk_times()
-- @tparam[opt=7200] int time in ticks that a player is called afk
-- @treturn number the number of afk players
function Sync.count_afk_times(time)
if not game then return 0 end
time = time or 7200
local rtn = {}
for _,player in pairs(game.connected_players) do
if player.afk_time > time then rtn[player.name] = Sync.tick_format(player.afk_time) end
end
return rtn
end
--- Used to get the number of players in each rank and currently online
-- @usage Sync.count_roles()
-- @treturn table contains the ranks and the players in that rank
function Sync.count_roles()
if not game then return {'Offline'} end
local _rtn = {admin={online={},players={}},user={online={},players={}}}
for index,player in pairs(game.players) do
if player.admin then
table.insert(_rtn.admin.players,player.name)
if player.connected then table.insert(_rtn.admin.online,player.name) end
else
table.insert(_rtn.user.players,player.name)
if player.connected then table.insert(_rtn.user.online,player.name) end
end
end
_rtn.admin.n_players,_rtn.admin.n_online=#_rtn.admin.players,#_rtn.admin.online
_rtn.user.n_players,_rtn.user.n_online=#_rtn.user.players,#_rtn.user.online
return _rtn
end
--- Used to get a list of every player name with the option to limit to only online players
-- @usage Sync.count_players()
-- @tparam boolean online true will get only the online players
-- @treturn table table of player names
function Sync.count_players(online)
if not game then return {'Offline'} end
local _players = {}
local players = {}
if online then _players = game.connected_players else _players = game.players end
for k,player in pairs(_players) do table.insert(players,player.name) end
return players
end
--- Used to get a list of every player name with the amount of time they have played for
-- @usage Sync.count_player_times()
-- @treturn table table indexed by player name, each value contains the raw tick and then the clean string
function Sync.count_player_times()
if not game then return {'Offline'} end
local _players = {}
for index,player in pairs(game.players) do
_players[player.name] = Sync.tick_format(player.online_time)
end
return _players
end
--- used to get the global list that has been defined, also used to set that list
-- @usage Sync.info{server_name='Factorio Server 2'} -- returns true
-- @usage Sync.info -- table of info
-- @tparam[opt=nil] table set keys to be replaced in the server info
-- @treturn boolean success was the data set
Sync.info = setmetatable({},{
__index=global,
__newindex=global,
__call=function(tbl,set)
if not is_type(set,'table') then return false end
for key,value in pairs(set) do global[key] = value end
return true
end,
__pairs=function(tbl)
tbl = global
local function next_pair(tbl,key)
local k, v = next(tbl, key)
if type(v) ~= nil then return k,v end
end
return next_pair, tbl, nil
end,
__ipairs=function(tbl)
tbl = global
local function next_pair(tbl, i)
i = i + 1
local v = tbl[i]
if v then return i, v end
end
return next_pair, tbl, 0
end
})
--- Called to update values inside of the info
-- @usage Sync.update()
-- @return all of the new info
function Sync.update()
local info = Sync.info
info.time_period[2] = tick_to_display_format(info.time_period[1])
info.last_update[1] = game.tick
info.last_update[2] = tick_to_display_format(game.tick)
info.game_speed = game.speed
info.players={
online=Sync.count_players(true),
n_online=#game.connected_players,
all=Sync.count_players(),
n_all=#game.players,
admins_online=Sync.count_admins(),
afk_players=Sync.count_afk_times(),
times=Sync.count_player_times()
}
info.roles = Sync.count_roles()
info.rockets = game.forces['player'].get_item_launched('satellite')
for key,callback in pairs(Sync_updates) do info[key] = callback() end
return info
end
--- Adds a callback to be called when the info is updated
-- @usage Sync.add_update('players',function() return #game.players end)
-- @tparam string key the key that the value will be stored in
-- @tparam function callback the function which will return this value
function Sync.add_update(key,callback)
if game then return end
if not is_type(callback,'function') then return end
Sync_updates[key] = callback
end
--- Outputs the curent server info into a file
-- @usage Sync.emit_data()
function Sync.emit_data()
local info = Sync.info
game.write_file('server-info.json',table.json(info),false,0)
end
--- Updates the info and emits the data to a file
-- @usage Sync.emit_update()
function Sync.emit_update()
Sync.update() Sync.emit_data()
end
--- Used to return and set the current IRL time; not very good need a better way to do this
-- @usage Sync.time('Sun Apr 1 18:44:30 UTC 2018')
-- @usage Sync.time -- string
-- @tparam[opt=nil] string set the date time to be set
-- @treturn boolean if the date time set was successful
Sync.time=add_metatable({},function(full,date)
local info = Sync.info
if not is_type(full,'string') then return false end
info.time = full
info.date = date
info.time_set[1] = Sync.tick_format(game.tick)
return true
end,function() local info = Sync.info return info.time..' (+'..(game.tick-info.time_set[1])..' Ticks)' end)
-- will auto replace the file every 5 min by default
Event.add('on_tick',function(event)
local time = Sync.info.time_period[1]
if (event.tick%time)==0 then Sync.emit_update() end
end)
Event.add('on_player_joined_game',Sync.emit_update)
Event.add('on_pre_player_left_game',Sync.emit_update)
Event.add('on_rocket_launched',Sync.emit_update)
function Sync:on_init()
if loaded_modules['ExpGamingCore.Gui'] then verbose('ExpGamingCore.Gui is installed; Loading gui src') require(module_path..'/src/gui',{Sync=Sync,module_path=module_path}) end
if loaded_modules['ExpGamingCore.Server'] then require('ExpGamingCore.Server').add_module_to_interface('Sync','ExpGamingCore.Sync') end
end
function Sync:on_post()
Sync.info{mods=table.keys(loaded_modules)}
end
return Sync

View File

@@ -0,0 +1,27 @@
{
"name": "ExpGamingCore.Sync",
"version": "4.0.0",
"description": "Allows syncing with an outside server and info panle.",
"location": "FSM_ARCHIVE",
"keywords": [
"Library",
"Lib",
"ExpGaming",
"Core",
"Info",
"Sync",
"External",
"Discord"
],
"dependencies": {
"ExpGamingLib": "^4.0.0",
"FactorioStdLib.Color": "^0.8.0",
"FactorioStdLib.Game": "^0.8.0",
"FactorioStdLib.Table": "^0.8.0",
"ExpGamingCore.Role": "?^4.0.0",
"ExpGamingCore.Gui": "?^4.0.0",
"ExpGamingCore.Server": "?^4.0.0"
},
"collection": "ExpGamingCore@4.0.0",
"submodules": {}
}

View File

@@ -0,0 +1,106 @@
--- Allows syncing with an outside server and info panel.
-- @submodule ExpGamingCore.Sync
-- @alias Sync
-- @author Cooldude2606
-- @license https://github.com/explosivegaming/scenario/blob/master/LICENSE
--- This file will be loaded when ExpGamingCore.Gui is present
-- @function _comment
local Gui = require('ExpGamingCore.Gui')
local Sync = Sync -- this is to force sync to remain in the ENV
local Sync_gui_functions = {}
local logo_sprite_path = 'file'..string.sub(module_path,2)..'/src/logo.png'
--- Adds a element to the sever info gui
-- @usage Sync.add_to_gui('string') -- return true
-- @param element see examples before for what can be used, it can also be a return from Gui.inputs.add
-- @treturn boolean based on weather it was successful or not
function Sync.add_to_gui(element,...)
if game then return false end
if is_type(element,'function') then
table.insert(Sync_gui_functions,{'function',element,...})
elseif is_type(element,'table') then
if element.draw then table.insert(Sync_gui_functions,{'gui',element})
else table.insert(Sync_gui_functions,{'table',element}) end
else table.insert(Sync_gui_functions,{'string',element}) end
return true
end
Sync.add_to_gui('Welcome to the Explosive Gaming community! This is one of many servers which we host.')
Sync.add_to_gui(function(player,frame) return 'This server\'s next reset: '..Sync.info.reset_time end)
--- Formats a label to be a certain format
-- @local label_format
local function label_format(label,width)
label.style.width = width
label.style.align = 'center'
label.style.single_line = false
end
--- Creates a center gui that will appear on join
-- @gui server-info
Sync.info_gui = Gui.center{
name='server-info',
caption=logo_sprite_path,
tooltip='Basic info about the current server',
draw=function(self,frame)
frame.caption = ''
local info = Sync.info
frame = frame.add{type='flow',direction='vertical'}
local h_flow = frame.add{type='flow'}
h_flow.add{type='sprite',sprite=logo_sprite_path}
local v_flow = h_flow.add{type='flow',direction='vertical'}
h_flow.add{type='sprite',sprite=logo_sprite_path}
local _flow = v_flow.add{type='flow'}
label_format(v_flow.add{
type='label',
caption=info.server_description,style='description_label'
},412)
Gui.bar(_flow,110)
label_format(_flow.add{
type='label',
caption='Welcome To '..info.server_name,
style='caption_label'
},180)
Gui.bar(_flow,110)
Gui.bar(frame,600)
local _frame = frame
frame = frame.add{
type='frame',
direction='vertical',
style='image_frame'
}
frame.style.width = 600
local text_flow = frame.add{type='flow',direction='vertical'}
local button_flow = frame.add{type='table',column_count=3}
for _,element in pairs(table.deepcopy(Sync_gui_functions)) do
local _type = table.remove(element,1)
if _type == 'function' then
local success, err = pcall(table.remove(element,1),frame.player_index,frame,unpack(element))
if not success then error(err) else
if is_type(err,'table') then
if element.draw then element:draw(button_flow).style.width = 195
else label_format(text_flow.add{type='label',caption=err},585) end
else label_format(text_flow.add{type='label',caption=tostring(err)},585) end
end
elseif _type == 'gui' then element[1]:draw(button_flow).style.width = 195
elseif _type == 'string' then label_format(text_flow.add{type='label',caption=tostring(element[1])},585)
elseif _type == 'table' then label_format(text_flow.add{type='label',caption=element[1]},585) end
end
_frame.add{
type='label',
caption='Press Ecs or E to close; this is only visible once!',
style='fake_disabled_label'
}.style.font='default-small'
end}
Event.add(defines.events.on_gui_click,function(event)
local element = event.element
if element and element.valid and element.caption and element.caption == 'Press Ecs or E to close; this is only visible once!' then
Gui.center.clear(event)
end
end)
Event.add(defines.events.on_player_joined_game,function(event) Sync.info_gui(event) end)

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,24 @@
{
"name": "ExpGamingCore",
"version": "4.0.0",
"description": "Explosive Gaming Core Files",
"location": "FSM_ARCHIVE",
"keywords": [
"Library",
"Lib",
"ExpGaming",
"Core"
],
"author": "Cooldude2606",
"contact": "Discord: Cooldude2606#5241",
"license": "https://github.com/explosivegaming/scenario/blob/master/LICENSE",
"submodules": {
"ExpGamingCore.Gui": "4.0.0",
"ExpGamingCore.Server": "4.0.0",
"ExpGamingCore.Sync": "4.0.0",
"ExpGamingCore.Command": "4.0.0",
"ExpGamingCore.Group": "4.0.0",
"ExpGamingCore.Role": "4.0.0"
},
"dependencies": {}
}