mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-08-13 00:45:11 +09:00
`Roles.new_role` returned the result of `error`, which made it nullable, and `set_permission_group` returned nil for an unknown group, which broke the config chain with an index error rather than saying so. Positions and bounding boxes read back from the game always use the named members, so they are narrowed once where they enter a function. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1174 lines
43 KiB
Lua
1174 lines
43 KiB
Lua
--[[-- Core Module - Roles
|
|
- Factorio role system to manage custom permissions.
|
|
@core Roles
|
|
@alias Roles
|
|
|
|
@usage--- Using Role System (assignment):
|
|
--When a map first starts you will want to define on mass all the players you expect to join and the roles to give them:
|
|
Roles.override_player_roles{
|
|
Cooldude2606 = {'Owner', 'Admin', 'Member'},
|
|
NotCooldude2606 = {'Member'}
|
|
}
|
|
|
|
--Once the game is running you still want to be able to give role and remove them which is when you would use:
|
|
Roles.assign_player(player, 'Admin', by_player_name) -- this will give the "Admin" role to the player
|
|
Roles.unassign_player(player, {'Admin', 'Moderator'}, by_player_name) -- this will remove "Admin" and "Moderator" role in one go
|
|
|
|
@usage--- Using Role System (role testing):
|
|
--To comparer two players you can comparer the index of they highest roles, can be used when you want to allow a "write" down type system:
|
|
Roles.get_player_highest_role(playerOne).index < Roles.get_player_highest_role(playerTwo).index -- remember that less means a higher role
|
|
|
|
--Listing all of a players roles can also be useful which is when you would want to use:
|
|
Roles.get_player_roles(player) -- the return is an array that can be looped over however this is not in particular order
|
|
|
|
--Finally you may want to test if a player has a certain role, flag or action allowed which is when you would use:
|
|
Roles.player_has_role(player, 'Admin') -- you can provide a role name if you only want a name based system
|
|
Roles.player_has_flag(player, 'is_donator') -- your roles can be grouped together with flags such as is_donator
|
|
Roles.player_allowed(player, 'game modifiers') -- or you can have an action based system where each action is something the player can do
|
|
|
|
@usage--- Example Flag Define:
|
|
--Flags can be used to group multiple roles and actions under one catch all, for example if you want a piece of code to only
|
|
--be active for your donators then you would add a "is_donator" flag to all your donator roles and then in the code test if
|
|
--a player has that tag present:
|
|
|
|
-- give you donators a speed boost when they join; these functions aren't required but can be useful
|
|
Roles.define_flag_trigger('is_donator', function(player, state)
|
|
if state then
|
|
player.character_running_speed_modifier = 1.5
|
|
else
|
|
player.character_running_speed_modifier = 1
|
|
end
|
|
end)
|
|
|
|
-- then on all your donator roles you would add
|
|
Roles.new_role('Donator')
|
|
:set_flag('is_donator')
|
|
|
|
-- and in your code you would test for
|
|
if Roles.player_has_flag(player, 'is_donator') then
|
|
-- some donator only code
|
|
end
|
|
|
|
@usage--- Example Role Define:
|
|
--You can't use a role system without any roles so first you must define your roles; each role has a minimum of a name with
|
|
--the option for a shorthand:
|
|
Roles.new_role('Administrator', 'Admin')
|
|
|
|
--Next you will want to add any extras you want to have, such as a tag, colour, permission group or any custom flags:
|
|
Roles.new_role('Administrator', 'Admin')
|
|
:set_custom_tag('[Admin]')
|
|
:set_custom_color('red') -- this can be {r=0, g=0, b=0} or a predefined value
|
|
:set_permission_group('Staff') -- a second argument can be added if you have not used the custom permission group config
|
|
:set_flag('is_admin')
|
|
|
|
--You will then want to decide if you want to allow all actions, this should of course be used sparely:
|
|
Roles.new_role('Administrator', 'Admin')
|
|
...extras...
|
|
:set_allow_all()
|
|
|
|
--If you don't do this want this as i would advise you do then you will want to define what the role can do; this comes with
|
|
--an optional inheritance system if you like those sort of things in which case disallow may also be of some use to you:
|
|
Roles.new_role('Administrator', 'Admin')
|
|
...extras...
|
|
:set_parent('Moderator') -- the admin can do anything that a moderator can do
|
|
:allow{ -- these actions can be anything just try to keep them without conflicts
|
|
'command/kill',
|
|
'gui/game settings'
|
|
}
|
|
|
|
--Here is what the finished admin role would look like:
|
|
Roles.new_role('Administrator', 'Admin')
|
|
:set_custom_tag('[Admin]')
|
|
:set_custom_color('red')
|
|
:set_permission_group('Staff')
|
|
:set_flag('is_admin')
|
|
:set_parent('Moderator')
|
|
:allow{
|
|
'command/kill',
|
|
'gui/game settings'
|
|
}
|
|
|
|
@usage--- Example System Define:
|
|
--Once all roles are defined these steps must be done to ensure the system is ready to use, this includes setting a default
|
|
--role, assigning a root (all permission) role that the server/system will use and the linear order that the roles fall into:
|
|
|
|
Roles.set_default('Guest')
|
|
Roles.set_root('System')
|
|
|
|
Roles.define_role_order{
|
|
'System',
|
|
'Administrator',
|
|
'Moderator',
|
|
'Donator',
|
|
'Guest'
|
|
}
|
|
|
|
--Just remember that in this example all these roles have not been defined; so make sure all your roles that are used are defined
|
|
--before hand; a config file on load is useful for this to ensure that its loaded before the first player even joins.
|
|
|
|
]]
|
|
|
|
local ExpUtil = require("modules/exp_util")
|
|
local Async = require("modules/exp_util/async")
|
|
local Storage = require("modules/exp_util/storage")
|
|
local Event = require("modules/exp_legacy/utils/event")
|
|
local Groups = require("modules.exp_legacy.expcore.permission_groups")
|
|
|
|
local Colours = ExpUtil.color
|
|
local write_json = ExpUtil.write_json
|
|
|
|
local Roles = {
|
|
_prototype = {},
|
|
config = {
|
|
order = {}, -- Contains the order of the roles, lower index is better
|
|
roles = {}, -- Contains the raw info for the roles, indexed by role name
|
|
flags = {}, -- Contains functions that run when a flag is added/removed from a player
|
|
internal = {}, -- Contains all internally accessed roles, such as root, default
|
|
players = {}, -- Contains the roles that players have
|
|
auto_assign = {}, -- Contains references to all roles which have auto assign conditions
|
|
deferred_roles = {}, -- Contains the roles that are to be assigned to players when they are unjailed
|
|
},
|
|
events = {
|
|
on_role_assigned = script.generate_event_name(),
|
|
on_role_unassigned = script.generate_event_name(),
|
|
},
|
|
}
|
|
|
|
--- When global is loaded it will have the metatable re-assigned to the roles
|
|
Storage.register({
|
|
Roles.config.players,
|
|
Roles.config.deferred_roles,
|
|
}, function(tbl)
|
|
Roles.config.players = tbl[1]
|
|
Roles.config.deferred_roles = tbl[2]
|
|
end)
|
|
|
|
--- Getter.
|
|
-- Functions which get roles
|
|
-- @section get
|
|
|
|
--- Internal function used to trigger a few different things when roles are changed
|
|
-- this is the raw internal trigger as the other function is called at other times
|
|
-- there is a second half called role_update which triggers after the event call, it also is called when a player joins
|
|
local function emit_player_roles_updated(player, type, roles, by_player_name, skip_game_print)
|
|
by_player_name = by_player_name or game.player and game.player.name or "<server>"
|
|
local by_player = game.players[by_player_name]
|
|
local by_player_index = by_player and by_player.index or 0
|
|
-- get the event id from the type of emit
|
|
local event = Roles.events.on_role_assigned
|
|
if type == "unassign" then
|
|
event = Roles.events.on_role_unassigned
|
|
end
|
|
-- Get the names of the roles
|
|
local role_names = {}
|
|
for index, role in ipairs(roles) do
|
|
role_names[index] = role.name
|
|
end
|
|
|
|
-- output to all the different locations: game print, player sound, event trigger and role log
|
|
if not skip_game_print then
|
|
game.print({ "expcore-roles.game-message-" .. type, player.name, table.concat(role_names, ", "), by_player_name }, Colours.cyan)
|
|
end
|
|
if type == "assign" then
|
|
player.play_sound{ path = "utility/achievement_unlocked" }
|
|
else
|
|
player.play_sound{ path = "utility/game_lost" }
|
|
end
|
|
script.raise_event(event, {
|
|
name = event,
|
|
tick = game.tick,
|
|
player_index = player.index,
|
|
by_player_index = by_player_index,
|
|
roles = role_names,
|
|
})
|
|
write_json("log/roles.log", {
|
|
player_name = player.name,
|
|
by_player_name = by_player_name,
|
|
type = type,
|
|
roles_changed = role_names,
|
|
})
|
|
end
|
|
|
|
--[[-- Returns a string which contains all roles in index order displaying all data for them
|
|
@treturn string the debug output string
|
|
|
|
@usage-- Print the debug string
|
|
game.player.print(Roles.debug())
|
|
|
|
]]
|
|
function Roles.debug()
|
|
local output = ""
|
|
for index, role_name in ipairs(Roles.config.order) do
|
|
local role = Roles.config.roles[role_name]
|
|
local color = (role.custom_color or Colours.white) --[[@as Color.struct]]
|
|
local color_str = string.format("[color=%d, %d, %d]", color.r, color.g, color.b)
|
|
output = output .. string.format("\n%s %s) %s[/color]", color_str, index, serpent.line(role))
|
|
end
|
|
|
|
return output
|
|
end
|
|
|
|
--[[-- Prints a message to all players in the given roles, may send duplicate message however factorio blocks spam
|
|
@tparam table roles table a of roles which to send the message to
|
|
@tparam string message the message to send to the players
|
|
|
|
@usage-- Print a message to the given roles
|
|
Roles.print_to_roles({'Administrator', 'Moderator'}, 'Hello, World!')
|
|
|
|
]]
|
|
function Roles.print_to_roles(roles, message)
|
|
for _, role in ipairs(roles) do
|
|
role = Roles.get_role_from_any(role)
|
|
if role then role:print(message) end
|
|
end
|
|
end
|
|
|
|
--[[-- Prints a message to all players who have the given role or one which is higher (excluding default)
|
|
@tparam string role the name of the role to send the message to
|
|
@tparam string message the message to send to the players
|
|
|
|
@usage-- Print a message to the roles above this role, includes the given role
|
|
Roles.print_to_roles_higher('Moderator', 'Hello, World!')
|
|
|
|
]]
|
|
function Roles.print_to_roles_higher(role, message)
|
|
role = Roles.get_role_from_any(role)
|
|
if not role then return end
|
|
local roles = {}
|
|
for index, role_name in ipairs(Roles.config.order) do
|
|
if index <= role.index and role_name ~= Roles.config.internal.default then
|
|
roles[#roles + 1] = role_name
|
|
end
|
|
end
|
|
|
|
Roles.print_to_roles(roles, message)
|
|
end
|
|
|
|
--[[-- Prints a message to all players who have the given role or one which is lower (excluding default)
|
|
@tparam string role the name of the role to send the message to
|
|
@tparam string message the message to send to the players
|
|
|
|
@usage-- Print a message to the roles below this role, includes the given role
|
|
Roles.print_to_roles_higher('Moderator', 'Hello, World!')
|
|
|
|
]]
|
|
function Roles.print_to_roles_lower(role, message)
|
|
role = Roles.get_role_from_any(role)
|
|
if not role then return end
|
|
local roles = {}
|
|
for index, role_name in ipairs(Roles.config.order) do
|
|
if index >= role.index and role_name ~= Roles.config.internal.default then
|
|
roles[#roles + 1] = role_name
|
|
end
|
|
end
|
|
|
|
Roles.print_to_roles(roles, message)
|
|
end
|
|
|
|
--[[-- Get a role for the given name
|
|
@tparam string name the name of the role to get
|
|
@treturn Roles._prototype the role with that name or nil
|
|
|
|
@usage-- Get a role by its name
|
|
local role = Roles.get_role_by_name('Moderator')
|
|
|
|
]]
|
|
function Roles.get_role_by_name(name)
|
|
return Roles.config.roles[name]
|
|
end
|
|
|
|
--[[-- Get a role with the given order index
|
|
@tparam number index the place in the order list of the role to get
|
|
@treturn Roles._prototype the role with that index in the order list or nil
|
|
|
|
@usage-- Get a role by its index in the order list
|
|
local role = Roles.get_role_by_name(2)
|
|
|
|
]]
|
|
function Roles.get_role_by_order(index)
|
|
local name = Roles.config.order[index]
|
|
return Roles.config.roles[name]
|
|
end
|
|
|
|
--[[-- Gets a role from a name, index or role object (where it is just returned)
|
|
nb: this function is used for the input for most outward facing functions
|
|
@tparam ?number|string|table any the value used to find the role
|
|
@treturn Roles._prototype the role that was found or nil see above
|
|
|
|
@usage-- Get a role by its name or order
|
|
local role = Roles.get_role_from_any('Moderator')
|
|
|
|
]]
|
|
function Roles.get_role_from_any(any)
|
|
local t_any = type(any)
|
|
if t_any == "number" or tonumber(any) then
|
|
any = tonumber(any)
|
|
return Roles.get_role_by_order(any)
|
|
elseif t_any == "string" then
|
|
return Roles.get_role_by_name(any)
|
|
elseif t_any == "table" then
|
|
return Roles.get_role_by_name(any.name)
|
|
end
|
|
end
|
|
|
|
--- Returns all roles in order of index
|
|
function Roles.get_roles_ordered()
|
|
local rtn = {}
|
|
for index, role_name in ipairs(Roles.config.order) do
|
|
rtn[index] = Roles.config.roles[role_name]
|
|
end
|
|
return rtn
|
|
end
|
|
|
|
--[[-- Gets all the roles of the given player, this will always contain the default role
|
|
@tparam LuaPlayer player the player to get the roles of
|
|
@treturn table a table where the values are the roles which the player has
|
|
|
|
@usage-- Get the roles that a player has
|
|
local roles = Roles.get_player_roles(game.player)
|
|
|
|
]]
|
|
function Roles.get_player_roles(player)
|
|
if not player or player.index == 0 then return { Roles.config.roles[Roles.config.internal.root] } end
|
|
local roles = Roles.config.players[player.name] or {}
|
|
local default = Roles.config.roles[Roles.config.internal.default]
|
|
local rtn = { default }
|
|
for index, role_name in ipairs(roles) do
|
|
rtn[index + 1] = Roles.config.roles[role_name]
|
|
end
|
|
|
|
return rtn
|
|
end
|
|
|
|
--[[-- Gets the highest role which the player has, can be used to compeer one player to another
|
|
@tparam LuaPlayer player the player to get the highest role of
|
|
@treturn the role with the highest order index which this player has
|
|
|
|
@usage-- Get the highest role that a player has
|
|
local role = Roles.get_player_highest_role(game.player)
|
|
|
|
]]
|
|
function Roles.get_player_highest_role(player)
|
|
local roles = Roles.get_player_roles(player)
|
|
local highest
|
|
for _, role in ipairs(roles) do
|
|
if not highest or role.index < highest.index then
|
|
highest = role
|
|
end
|
|
end
|
|
|
|
return highest
|
|
end
|
|
|
|
--- Assignment.
|
|
-- Functions for changing player's roles
|
|
-- @section assignment
|
|
|
|
--[[-- Gives a player the given role(s) with an option to pass a by player name used in the log
|
|
@tparam LuaPlayer player the player that will be assigned the roles
|
|
@tparam table roles table a of roles that the player will be given, can be one role and can be role names
|
|
@tparam[opt=<server>] string by_player_name the name of the player that will be shown in the log
|
|
@tparam[opt=false] boolean skip_checks when true there will be no checks are done for if the player is valid
|
|
@tparam[opt=false] boolean silent when true there will be no game message printed
|
|
|
|
@usage-- Assign a player to the Moderator role
|
|
Roles.assign_player(game.player, 'Moderator')
|
|
|
|
@usage-- Assign a player to the Moderator role, even if the player has never been on the map
|
|
Roles.assign_player('Cooldude2606', 'Moderator', nil, true)
|
|
|
|
]]
|
|
function Roles.assign_player(player, roles, by_player_name, skip_checks, silent)
|
|
local valid_player = type(player) == "userdata" and player or game.get_player(player)
|
|
if not skip_checks and not valid_player then return end
|
|
|
|
-- Convert the roles into a table (allows for optional array)
|
|
if type(roles) ~= "table" or roles.name then
|
|
roles = { roles }
|
|
end
|
|
|
|
-- Convert to role objects
|
|
local role_objects = {}
|
|
for _, role in ipairs(roles) do
|
|
local role_object = Roles.get_role_from_any(role)
|
|
if role_object then table.insert(role_objects, role_object) end
|
|
end
|
|
|
|
-- If the player has a role that needs to defer the role changes, save the roles that need to be assigned later into a table
|
|
if valid_player and Roles.player_has_flag(valid_player, "defer_role_changes") then
|
|
local assign_later = Roles.config.deferred_roles[valid_player.name] or {}
|
|
for _, role in ipairs(role_objects) do
|
|
local role_change = assign_later[role.name]
|
|
if role_change then
|
|
role_change.count = role_change.count + 1
|
|
if role_change.count == 1 then
|
|
role_change.by_player_name = by_player_name or "<server>"
|
|
role_change.silent = silent
|
|
end
|
|
else
|
|
assign_later[role.name] = {
|
|
count = 1, by_player_name = by_player_name or "<server>", silent = silent,
|
|
}
|
|
end
|
|
end
|
|
|
|
Roles.config.deferred_roles[valid_player.name] = assign_later
|
|
return
|
|
end
|
|
|
|
for _, role in ipairs(role_objects) do
|
|
role:add_player(valid_player or player, valid_player == nil, true)
|
|
end
|
|
|
|
if valid_player then
|
|
emit_player_roles_updated(valid_player, "assign", role_objects, by_player_name, silent)
|
|
end
|
|
end
|
|
|
|
--[[-- Removes a player from the given role(s) with an option to pass a by player name used in the log
|
|
@tparam LuaPlayer player the player that will have the roles removed
|
|
@tparam table roles table a of roles to be removed from the player, can be one role and can be role names
|
|
@tparam[opt=<server>] string by_player_name the name of the player that will be shown in the logs
|
|
@tparam[opt=false] boolean skip_checks when true there will be no checks are done for if the player is valid
|
|
@tparam[opt=false] boolean silent when true there will be no game message printed
|
|
|
|
@usage-- Unassign a player from the Moderator role
|
|
Roles.unassign_player(game.player, 'Moderator')
|
|
|
|
@usage-- Unassign a player from the Moderator role, even if the player has never been on the map
|
|
Roles.unassign_player('Cooldude2606', 'Moderator', nil, true)
|
|
|
|
]]
|
|
function Roles.unassign_player(player, roles, by_player_name, skip_checks, silent)
|
|
local valid_player = type(player) == "userdata" and player or game.get_player(player)
|
|
if not skip_checks and not valid_player then return end
|
|
if not player then return end
|
|
|
|
-- Convert the roles into a table (allows for optional array)
|
|
if type(roles) ~= "table" or roles.name then
|
|
roles = { roles }
|
|
end
|
|
|
|
-- Convert to role objects
|
|
local role_objects = {}
|
|
for _, role in ipairs(roles) do
|
|
local role_object = Roles.get_role_from_any(role)
|
|
if role_object then table.insert(role_objects, role_object) end
|
|
end
|
|
|
|
-- If the player has a role that needs to defer the role changes, save the roles that need to be unassigned later into a table
|
|
local defer_changes = Roles.player_has_flag(player, "defer_role_changes")
|
|
if defer_changes then
|
|
local assign_later = Roles.config.deferred_roles[valid_player.name] or {}
|
|
for _, role in ipairs(role_objects) do
|
|
local role_change = assign_later[role.name]
|
|
if role_change then
|
|
role_change.count = role_change.count - 1
|
|
if role_change.count == -1 then
|
|
role_change.by_player_name = by_player_name or "<server>"
|
|
role_change.silent = silent
|
|
end
|
|
else
|
|
assign_later[role.name] = {
|
|
count = -1, by_player_name = by_player_name or "<server>", silent = silent,
|
|
}
|
|
end
|
|
end
|
|
|
|
Roles.config.deferred_roles[valid_player.name] = assign_later
|
|
end
|
|
|
|
-- Remove the player from roles
|
|
local role_changes = {}
|
|
for _, role in ipairs(role_objects) do
|
|
if not defer_changes or role:has_flag("defer_role_changes") then
|
|
role:remove_player(valid_player or player, valid_player == nil, true)
|
|
table.insert(role_changes, role)
|
|
end
|
|
end
|
|
|
|
-- If there are deferred role changes, apply them now
|
|
if defer_changes and not Roles.player_has_flag(valid_player, "defer_role_changes") then
|
|
local assign_later = Roles.config.deferred_roles[player.name] or {}
|
|
local assigns, unassigns = {}, {}
|
|
for role_name, details in pairs(assign_later) do
|
|
local role = Roles.get_role_from_any(role_name)
|
|
if role then
|
|
if details.count > 0 then
|
|
role:add_player(valid_player or player, valid_player == nil, true)
|
|
if not assigns[details.by_player_name] then assigns[details.by_player_name] = {} end
|
|
if not details.silent then table.insert(assigns[details.by_player_name], role) end
|
|
elseif details.count < 0 then
|
|
role:remove_player(valid_player or player, valid_player == nil, true)
|
|
if not unassigns[details.by_player_name] then unassigns[details.by_player_name] = {} end
|
|
if not details.silent then table.insert(unassigns[details.by_player_name], role) end
|
|
end
|
|
end
|
|
end
|
|
|
|
for assign_by_player_name, assign_roles in pairs(assigns) do
|
|
if #assign_roles > 0 then emit_player_roles_updated(valid_player, "assign", assign_roles, assign_by_player_name) end
|
|
end
|
|
|
|
for unassign_by_player_name, unassign_roles in pairs(unassigns) do
|
|
if #unassign_roles > 0 then emit_player_roles_updated(valid_player, "unassign", unassign_roles, unassign_by_player_name) end
|
|
end
|
|
|
|
Roles.config.deferred_roles[player.name] = nil
|
|
end
|
|
|
|
if valid_player and #role_changes > 0 then
|
|
emit_player_roles_updated(valid_player, "unassign", role_changes, by_player_name, silent)
|
|
end
|
|
end
|
|
|
|
--[[-- Overrides all player roles with the given table of roles, useful to mass set roles on game start
|
|
@tparam[opt] string player_name the player to set the roles for, if not given all roles are overriden
|
|
@tparam table roles table a which is indexed by case sensitive player names and has the value of a table of role names
|
|
|
|
@usage-- Override the roles of a single player, other users are not effected
|
|
Roles.override_player_roles('Cooldude2606', {'Moderator'})
|
|
|
|
@usage-- Override all existing roles, effects all users not just ones listed
|
|
Roles.override_player_roles{
|
|
['Cooldude2606'] = {'Administrator', 'Moderator'},
|
|
['arty714'] = {'Administrator', 'Moderator'},
|
|
}
|
|
|
|
]]
|
|
function Roles.override_player_roles(player_name, roles)
|
|
local player_roles = Roles.config.players
|
|
if not roles then
|
|
for k in pairs(player_roles) do player_roles[k] = nil end
|
|
|
|
for k, new_roles in pairs(player_name) do player_roles[k] = new_roles end
|
|
else
|
|
Roles.config.players[player_name] = roles
|
|
end
|
|
end
|
|
|
|
--- Checks.
|
|
-- Functions for checking player's roles
|
|
-- @section checks
|
|
|
|
--[[-- A test for weather a player has the given role
|
|
@tparam LuaPlayer player the player to test the roles of
|
|
@tparam ?string|number|table search_role a pointer to the role that is being searched for
|
|
@treturn boolean true if the player has the role, false otherwise, nil for errors
|
|
|
|
@usage-- Test if a player has a role
|
|
local has_role = Roles.player_has_role(game.player, 'Moderator')
|
|
|
|
]]
|
|
function Roles.player_has_role(player, search_role)
|
|
local roles = Roles.get_player_roles(player)
|
|
if not roles then return end
|
|
search_role = Roles.get_role_from_any(search_role)
|
|
if not search_role then return end
|
|
for _, role in ipairs(roles) do
|
|
if role.name == search_role.name then return true end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
--[[-- A test for weather a player has the given flag true for at least one of they roles
|
|
@tparam LuaPlayer player the player to test the roles of
|
|
@tparam string flag_name the name of the flag that is being looked for
|
|
@treturn boolean true if the player has at least one role which has the flag set to true, false otherwise, nil for errors
|
|
|
|
@usage-- Test if a player has a role
|
|
local has_flag = Roles.player_has_flag(game.player, 'is_donator')
|
|
|
|
]]
|
|
function Roles.player_has_flag(player, flag_name)
|
|
local roles = Roles.get_player_roles(player)
|
|
if not roles then return end
|
|
for _, role in ipairs(roles) do
|
|
if role:has_flag(flag_name) then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
--[[-- A test for weather a player has at least one role which is allowed the given action
|
|
@tparam LuaPlayer player the player to test the roles of
|
|
@tparam string action the name of the action that is being tested for
|
|
@treturn boolean true if the player has at least one role which is allowed this action, false otherwise, nil for errors
|
|
|
|
@usage-- Test if a player has a role
|
|
local has_flag = Roles.player_has_flag(game.player, 'is_donator')
|
|
|
|
]]
|
|
function Roles.player_allowed(player, action)
|
|
local roles = Roles.get_player_roles(player)
|
|
for _, role in ipairs(roles) do
|
|
if role:is_allowed(action) then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
--- Definitions.
|
|
-- Functions which are used to define roles
|
|
-- @section checks
|
|
|
|
--[[-- Used to set the role order, higher in the list is better, must be called at least once in config
|
|
-- nb: function also re links parents due to expected position in the config file
|
|
@tparam table order table a which is keyed only by numbers (start 1) and values are roles in order with highest first
|
|
|
|
@usage-- Define which roles are higher than others
|
|
Roles.define_role_order{
|
|
'System',
|
|
'Administrator',
|
|
'Moderator',
|
|
'Donator',
|
|
'Guest'
|
|
}
|
|
|
|
]]
|
|
function Roles.define_role_order(order)
|
|
-- Clears and then rebuilds the order table
|
|
ExpUtil.assert_not_runtime()
|
|
Roles.config.order = {}
|
|
local done = {}
|
|
for index, role in ipairs(order) do
|
|
if type(role) == "table" and role.name then
|
|
done[role.name] = true
|
|
Roles.config.order[index] = role.name
|
|
else
|
|
done[role] = true
|
|
Roles.config.order[index] = role
|
|
end
|
|
end
|
|
|
|
-- Check no roles were missed
|
|
for role_name in pairs(Roles.config.roles) do
|
|
if not done[role_name] then
|
|
error("Role missing " .. role_name .. " from role order, all defined roles must be included.", 2)
|
|
end
|
|
end
|
|
|
|
-- Re-links roles to they parents as this is called at the end of the config
|
|
for index, role_name in pairs(Roles.config.order) do
|
|
local role = Roles.config.roles[role_name]
|
|
if not role then
|
|
error("Role with name " .. role_name .. " has not beed defined, either define it or remove it from the order list.", 2)
|
|
end
|
|
role.index = index
|
|
local parent = Roles.config.roles[role.parent]
|
|
if parent then
|
|
setmetatable(role.allowed_actions, { __index = parent.allowed_actions })
|
|
end
|
|
end
|
|
end
|
|
|
|
--[[-- Defines a new trigger for when a tag is added or removed from a player
|
|
@tparam string name the name of the flag which the roles will have
|
|
@tparam function callback the function that is called when roles are assigned
|
|
|
|
@usage-- Defining a flag trigger
|
|
Roles.define_flag_trigger('is_donator', function(player, state)
|
|
player.character_running_speed_modifier = state and 1.5 or 1
|
|
end)
|
|
|
|
]]
|
|
function Roles.define_flag_trigger(name, callback)
|
|
Roles.config.flags[name] = Async.register(callback)
|
|
end
|
|
|
|
--[[-- Sets the default role which every player will have, this needs to be called at least once
|
|
@tparam string name the name of the default role
|
|
|
|
@usage-- Setting the default role
|
|
Roles.set_default('Guest')
|
|
|
|
]]
|
|
function Roles.set_default(name)
|
|
local role = Roles.config.roles[name]
|
|
if not role then return end
|
|
Roles.config.internal.default = name
|
|
end
|
|
|
|
--[[-- Sets the root role which will always have all permissions, any server actions act from this role
|
|
@tparam string name the name of the root role
|
|
|
|
@usage-- Setting the root role
|
|
Roles.set_root('System')
|
|
|
|
]]
|
|
function Roles.set_root(name)
|
|
local role = Roles.config.roles[name]
|
|
if not role then return end
|
|
role:set_allow_all(true)
|
|
Roles.config.internal.root = name
|
|
end
|
|
|
|
--[[-- Defines a new role and returns the prototype to allow configuration
|
|
@tparam string name the name of the new role, must be unique
|
|
@tparam[opt=name] string short_hand the shortened version of the name
|
|
@treturn Roles._prototype the start of the config chain for this role
|
|
|
|
@usage-- Defining a new role
|
|
local role = Roles.new_role('Moderator', 'Mod')
|
|
|
|
]]
|
|
function Roles.new_role(name, short_hand)
|
|
ExpUtil.assert_not_runtime()
|
|
if Roles.config.roles[name] then error("Role name is non unique") end
|
|
local role = setmetatable({
|
|
name = name,
|
|
short_hand = short_hand or name,
|
|
allowed_actions = {},
|
|
allow_all_actions = false,
|
|
flags = {},
|
|
}, { __index = Roles._prototype })
|
|
Roles.config.roles[name] = role
|
|
return role
|
|
end
|
|
|
|
--- Role Actions.
|
|
-- Functions for using the role action system
|
|
-- @section actions
|
|
|
|
--[[-- Sets the default allow state of the role, true will allow all actions
|
|
@tparam[opt=true] boolean state true will allow all actions
|
|
@treturn Roles._prototype allows chaining
|
|
|
|
@usage-- Allow all actions for this role, useful for root like roles
|
|
role:set_allow_all()
|
|
|
|
]]
|
|
function Roles._prototype:set_allow_all(state)
|
|
if state == nil then state = true end
|
|
self.allow_all_actions = not not state -- not not forces a boolean value
|
|
return self
|
|
end
|
|
|
|
--[[-- Sets the allow actions for this role, actions in this list will be allowed for this role
|
|
@tparam table actions indexed with numbers and is an array of action names, order has no effect
|
|
@treturn Roles._prototype allows chaining
|
|
|
|
@usage-- Allow some actions for a role
|
|
role:allow{
|
|
'command/kill',
|
|
'gui/game settings'
|
|
}
|
|
|
|
]]
|
|
function Roles._prototype:allow(actions)
|
|
if type(actions) ~= "table" then
|
|
actions = { actions }
|
|
end
|
|
for _, action in ipairs(actions) do
|
|
self.allowed_actions[action] = true
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
--[[-- Sets the disallow actions for this role, will prevent actions from being allowed regardless of inheritance
|
|
@tparam table actions indexed with numbers and is an array of action names, order has no effect
|
|
@treturn Roles._prototype allows chaining
|
|
|
|
@usage-- Disallow an action for a role, useful if inherit an action from a parent
|
|
role:disallow{
|
|
'command/kill',
|
|
'gui/game settings'
|
|
}
|
|
|
|
]]
|
|
function Roles._prototype:disallow(actions)
|
|
if type(actions) ~= "table" then
|
|
actions = { actions }
|
|
end
|
|
for _, action in ipairs(actions) do
|
|
self.allowed_actions[action] = false
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
--[[-- Test for if a role is allowed the given action, mostly internal see Roles.player_allowed
|
|
@tparam string action the name of the action to test if it is allowed
|
|
@treturn boolean true if action is allowed, false otherwise
|
|
|
|
@usage-- Test if a role is allowed an action
|
|
local allowed = role:is_allowed('command/kill')
|
|
|
|
]]
|
|
function Roles._prototype:is_allowed(action)
|
|
local is_root = Roles.config.internal.root == self.name
|
|
return self.allowed_actions[action] or self.allow_all_actions or is_root
|
|
end
|
|
|
|
--- Role Flags.
|
|
-- Functions for using the role flag system
|
|
-- @section flags
|
|
|
|
--[[-- Sets the state of a flag for a role, flags can be used to apply effects to players
|
|
@tparam string name the name of the flag to set the value of
|
|
@tparam[opt=true] boolean value the state to set the flag to
|
|
@treturn Roles._prototype allows chaining
|
|
|
|
@usage-- Set a flag for a role
|
|
role:set_flag('is_admin')
|
|
|
|
]]
|
|
function Roles._prototype:set_flag(name, value)
|
|
if value == nil then value = true end
|
|
self.flags[name] = not not value -- not not forces a boolean value
|
|
return self
|
|
end
|
|
|
|
--[[-- Clears all flags from this role, individual flags can be removed with set_flag(name, false)
|
|
@treturn Roles._prototype allows chaining
|
|
|
|
@usage-- Remove all flags from a role
|
|
role:clear_flags()
|
|
|
|
]]
|
|
function Roles._prototype:clear_flags()
|
|
self.flags = {}
|
|
return self
|
|
end
|
|
|
|
--[[-- A test for if the role has a flag set
|
|
@tparam string name the name of the flag to test for
|
|
@treturn boolean true if the flag is set, false otherwise
|
|
|
|
@usage-- Test if a role has a flag
|
|
local has_flag = role:has_flag('is_admin')
|
|
|
|
]]
|
|
function Roles._prototype:has_flag(name)
|
|
return self.flags[name] or false
|
|
end
|
|
|
|
--- Role Properties.
|
|
-- Functions for changing other properties
|
|
-- @section properties
|
|
|
|
--[[-- Sets a custom player tag for the role, can be accessed by other code
|
|
@tparam string tag the value that the tag will be
|
|
@treturn Roles._prototype allows chaining
|
|
|
|
@usage-- Set a custom tag for this role, other code is required to set the tag
|
|
role:set_custom_tag('Mod')
|
|
|
|
]]
|
|
function Roles._prototype:set_custom_tag(tag)
|
|
self.custom_tag = tag
|
|
return self
|
|
end
|
|
|
|
--[[-- Sets a custom colour for the role, can be accessed by other code
|
|
@tparam table color ?string|table can either be and rgb colour or the name of a colour defined in the presets
|
|
@treturn Roles._prototype allows chaining
|
|
|
|
@usage-- Set a custom colour for this role, other code is required to use this value
|
|
role:set_custom_color{ r=255, g=100, b=100}
|
|
|
|
]]
|
|
function Roles._prototype:set_custom_color(color)
|
|
if type(color) ~= "table" then
|
|
color = Colours[color]
|
|
end
|
|
self.custom_color = color
|
|
return self
|
|
end
|
|
|
|
--[[-- Sets the permission group for this role, players will be moved to the group of they highest role
|
|
@tparam string name the name of the permission group to have players moved to
|
|
@tparam[opt=false] boolean use_factorio_api when true the custom permission group module is ignored
|
|
@treturn Roles._prototype allows chaining
|
|
|
|
@usage-- Set the permission group for this role, see permission_groups.lua
|
|
role:set_permission_group('Admin')
|
|
|
|
]]
|
|
function Roles._prototype:set_permission_group(name, use_factorio_api)
|
|
ExpUtil.assert_not_runtime()
|
|
if use_factorio_api then
|
|
self.permission_group = { true, name } --[[@as [boolean, string] ]]
|
|
else
|
|
assert(Groups.get_group_by_name(name), "Permission group not found: " .. name)
|
|
self.permission_group = name
|
|
end
|
|
return self
|
|
end
|
|
|
|
--[[-- Sets the parent for a role, any action not in allow or disallow will be looked for in its parents
|
|
nb: this is a recursive action, and changing the allows and disallows will effect all children roles
|
|
@tparam string role the name of the role that will be the parent; has imminent effect if role is already defined
|
|
@treturn Roles._prototype allows chaining
|
|
|
|
@usage-- Set the parent for this role to inherit all actions allowed
|
|
role:set_parent('Guest')
|
|
|
|
]]
|
|
function Roles._prototype:set_parent(role)
|
|
ExpUtil.assert_not_runtime()
|
|
self.parent = role
|
|
role = Roles.get_role_from_any(role)
|
|
if not role then return self end
|
|
setmetatable(self.allowed_actions, { __index = role.allowed_actions })
|
|
return self
|
|
end
|
|
|
|
--[[-- Sets an auto assign condition that is checked every 60 seconds, if true is returned then the player will receive the role
|
|
nb: this is one way, failing false after already gaining the role will not revoke the role
|
|
@tparam function callback receives only one param which is player to assign, return true to assign the player
|
|
@treturn Roles._prototype allows chaining
|
|
|
|
@usage-- Give this role to a user if there are admin, ran every 60 seconds
|
|
role:set_auto_assign_condition(function(player)
|
|
return player.admin
|
|
end)
|
|
|
|
]]
|
|
function Roles._prototype:set_auto_assign_condition(callback)
|
|
ExpUtil.assert_not_runtime()
|
|
self.auto_assign_condition = true
|
|
Roles.config.auto_assign[self.name] = callback
|
|
return self
|
|
end
|
|
|
|
--[[-- Get the auto assign condition for this role, returns nil if no condition is set
|
|
@treturn function The callback which was assigned as the auto assign condition
|
|
|
|
@usage-- Give this role to a user if there are admin, ran every 60 seconds
|
|
local condition = role:get_auto_assign_condition()
|
|
|
|
]]
|
|
function Roles._prototype:get_auto_assign_condition()
|
|
return Roles.config.auto_assign[self.name]
|
|
end
|
|
|
|
--[[-- Sets the role to not allow players to have auto assign effect them, useful to keep people locked to a role
|
|
@tparam[opt=true] boolean state when true the players with this role will not be auto assigned to other roles
|
|
@treturn Roles._prototype allows chaining
|
|
|
|
@usage-- Make a role stop players from being auto assigned to other roles
|
|
role:set_block_auto_assign()
|
|
|
|
]]
|
|
function Roles._prototype:set_block_auto_assign(state)
|
|
if state == nil then state = true end
|
|
self.block_auto_assign = not not state -- forces a boolean value
|
|
return self
|
|
end
|
|
|
|
--- Role Players.
|
|
-- Functions that control players in a role
|
|
-- @section players
|
|
|
|
--[[-- Adds a player to this role, players can have more than one role at a time, used internally see Roles.assign
|
|
@tparam LuaPlayer player the player that will be given this role
|
|
@tparam boolean skip_check when true player will be taken as the player name (use when player has not yet joined)
|
|
@tparam boolean skip_event when true the event emit will be skipped, this is used internally with Roles.assign
|
|
@treturn boolean true if the player was added successfully
|
|
|
|
@usage-- Assign a player to this role
|
|
role:add_player(game.player)
|
|
|
|
]]
|
|
function Roles._prototype:add_player(player, skip_check, skip_event)
|
|
local valid_player = type(player) == "userdata" and player or game.get_player(player)
|
|
-- Default role cant have players added or removed
|
|
if self.name == Roles.config.internal.default then return end
|
|
-- Check the player is valid, can be skipped but a name must be given
|
|
local player_name
|
|
if valid_player then
|
|
player_name = valid_player.name
|
|
elseif skip_check then
|
|
player_name = player
|
|
else
|
|
return false
|
|
end
|
|
-- Add the role name to the player's roles
|
|
local player_roles = Roles.config.players[player_name]
|
|
if player_roles then
|
|
for _, role_name in ipairs(player_roles) do
|
|
if role_name == self.name then return false end
|
|
end
|
|
|
|
player_roles[#player_roles + 1] = self.name
|
|
else
|
|
Roles.config.players[player_name] = { self.name }
|
|
end
|
|
-- Emits event if required
|
|
if valid_player and not skip_event then
|
|
emit_player_roles_updated(valid_player, "assign", { self })
|
|
end
|
|
return true
|
|
end
|
|
|
|
--[[-- Removes a player from this role, players can have more than one role at a time, used internally see Roles.unassign
|
|
@tparam LuaPlayer player the player that will lose this role
|
|
@tparam boolean skip_check when true player will be taken as the player name (use when player has not yet joined)
|
|
@tparam boolean skip_event when true the event emit will be skipped, this is used internally with Roles.unassign
|
|
@treturn boolean true if the player was removed successfully
|
|
|
|
@usage-- Unassign a player from this role
|
|
role:remove_player(game.player)
|
|
|
|
]]
|
|
function Roles._prototype:remove_player(player, skip_check, skip_event)
|
|
local valid_player = type(player) == "userdata" and player or game.get_player(player)
|
|
-- Default role cant have players added or removed
|
|
if self.name == Roles.config.internal.default then return end
|
|
-- Check the player is valid, can be skipped but a name must be given
|
|
local player_name
|
|
if valid_player then
|
|
player_name = valid_player.name
|
|
elseif skip_check then
|
|
player_name = player
|
|
else
|
|
return false
|
|
end
|
|
-- Remove the role from the players roles
|
|
local player_roles = Roles.config.players[player_name]
|
|
local found = false
|
|
if player_roles then
|
|
for index, role_name in ipairs(player_roles) do
|
|
if role_name == self.name then
|
|
player_roles[index] = player_roles[#player_roles]
|
|
player_roles[#player_roles] = nil
|
|
found = true
|
|
break
|
|
end
|
|
end
|
|
|
|
if #player_roles == 0 then
|
|
Roles.config.players[player_name] = nil
|
|
end
|
|
end
|
|
-- Emits event if required
|
|
if valid_player and not skip_event then
|
|
emit_player_roles_updated(valid_player, "unassign", { self })
|
|
end
|
|
return found
|
|
end
|
|
|
|
--[[-- Returns an array of all the players who have this role, can be filtered by online status
|
|
@tparam[opt=nil] boolean online when given will filter by this online state, nil will return all players
|
|
@treturn table all the players who have this role, indexed order is meaningless
|
|
|
|
@usage-- Get all the players with this role
|
|
local players = role:get_players()
|
|
|
|
@usage-- Get all online players with this role
|
|
local players = role:get_players(true)
|
|
|
|
]]
|
|
function Roles._prototype:get_players(online)
|
|
local players = {}
|
|
-- Search all players to check if they have this role
|
|
for player_name, player_roles in pairs(Roles.config.players) do
|
|
for _, role_name in ipairs(player_roles) do
|
|
if role_name == self.name then
|
|
local player = game.players[player_name]
|
|
-- Filter by online state if required
|
|
if player and (online == nil or player.connected == online) then
|
|
players[#players + 1] = player
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
return players
|
|
end
|
|
|
|
--[[-- Will print a message to all players with this role
|
|
@tparam string message the message that will be printed to the players
|
|
@treturn number the number of players who received the message
|
|
|
|
@usage-- Print a message to all players with this role
|
|
role:print('Hello, World!')
|
|
|
|
]]
|
|
function Roles._prototype:print(message)
|
|
local players = self:get_players(true)
|
|
for _, player in ipairs(players) do
|
|
player.print(message)
|
|
end
|
|
|
|
return #players
|
|
end
|
|
|
|
--- Used internally to be the first trigger on an event change, would be messy to include this in 4 different places
|
|
local function role_update(event)
|
|
local player = game.players[event.player_index]
|
|
-- Updates flags given to the player
|
|
for flag, async_function in pairs(Roles.config.flags) do
|
|
local state = Roles.player_has_flag(player, flag)
|
|
async_function(player, state)
|
|
end
|
|
|
|
-- Updates the players permission group
|
|
local highest = Roles.get_player_highest_role(player)
|
|
if highest.permission_group then
|
|
local permission_group = highest.permission_group --[[@as [boolean, string] ]]
|
|
if permission_group[1] then
|
|
local group = game.permissions.get_group(permission_group[2])
|
|
if group then
|
|
Groups.add_to_permission_group_async(group, player)
|
|
end
|
|
else
|
|
Groups.set_player_group(player, highest.permission_group)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Used internally to test if a player should be auto assigned a role
|
|
local function auto_assign(event)
|
|
local player = game.players[event.player_index]
|
|
local roles = Roles.config.players[player.name] or {}
|
|
|
|
local lookup = {}
|
|
-- Create a lookup table for existing roles, and check for block auto assign
|
|
for _, role in ipairs(roles) do
|
|
if role.block_auto_assign then return end
|
|
lookup[role] = true
|
|
end
|
|
|
|
local assigns, ctn = {}, 0
|
|
-- Check roles with auto assign conditions and run the handler
|
|
for role, condition in pairs(Roles.config.auto_assign) do
|
|
if not lookup[role] then
|
|
local success, rtn = pcall(condition, player)
|
|
if not success then
|
|
log{ "expcore-roles.error-log-format-assign", role.name, rtn }
|
|
elseif rtn == true then
|
|
ctn = ctn + 1
|
|
assigns[ctn] = role
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Assign new roles if any were passed
|
|
if ctn > 0 then Roles.assign_player(player, assigns) end
|
|
end
|
|
|
|
--- When a player joined or has a role change then the update is triggered
|
|
Event.add(Roles.events.on_role_assigned, role_update)
|
|
Event.add(Roles.events.on_role_unassigned, role_update)
|
|
Event.add(defines.events.on_player_joined_game, role_update)
|
|
|
|
--- Every 60 seconds and on join auto role assignment is checked
|
|
Event.add(defines.events.on_player_joined_game, auto_assign)
|
|
Event.on_nth_tick(3600, function()
|
|
for _, player in ipairs(game.connected_players) do
|
|
auto_assign{ player_index = player.index }
|
|
end
|
|
end)
|
|
|
|
-- Return Roles
|
|
return Roles
|