Files
factorio-scenario-ExpCluster/exp_legacy/module/expcore/roles.lua
T
bbassieandClaude Opus 5 3f46055b70 Enable need-check-nil and start the burn down
`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>
2026-08-07 15:31:43 +00:00

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