--[[-- 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 "" 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 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=] 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 "" role_change.silent = silent end else assign_later[role.name] = { count = 1, by_player_name = by_player_name or "", 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=] 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 "" role_change.silent = silent end else assign_later[role.name] = { count = -1, by_player_name = by_player_name or "", 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 return 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 } else local group = Groups.get_group_by_name(name) if not group then return end 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 if highest.permission_group[1] then local group = game.permissions.get_group(highest.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