--- Factorio role system to manage custom permissions -- @author Cooldude2606 -- @module Commands --[[ >>>>Using Role System (Frontend): 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 >>>>Using Role System (Backend): 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 >>>>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 >>>>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 dont 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' } >>>>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 linier 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. >>>>Functions List (see function for more detail): Roles.debug() --- Returns a string which contains all roles in index order displaying all data for them Roles.get_role_by_name(name) --- Get a role for the given name Roles.get_role_by_order(index) --- Get a role with the given order index Roles.get_role_from_any(any) --- Gets a role from a name,index or role object (where it is just returned) Roles.get_player_roles(player) --- Gets all the roles of the given player, this will always contain the default role Roles.get_player_highest_role(player) --- Gets the highest role which the player has, can be used to compeer one player to another Roles.assign_player(player,roles,by_player_name) --- Gives a player the given role(s) with an option to pass a by player name used in the log Roles.unassign_player(player,roles,by_player_name) --- Removes a player from the given role(s) with an option to pass a by player name used in the log Roles.override_player_roles(roles) --- Overrides all player roles with the given table of roles, useful to mass set roles on game start Roles.player_has_role(player,search_role) --- A test for weather a player has the given role Roles.player_has_flag(player,flag_name) --- A test for weather a player has the given flag true for at least one of they roles Roles.player_allowed(player,action) --- A test for weather a player has at least one role which is allowed the given action Roles.define_role_order(order) --- Used to set the role order, higher in the list is better, must be called at least once in config Roles.define_flag_trigger(name,callback) --- Defines a new trigger for when a tag is added or removed from a player Roles.set_default(name) --- Sets the default role which every player will have, this needs to be called at least once Roles.set_root(name) --- Sets the root role which will always have all permissions, any server actions act from this role Roles.new_role(name,short_hand) --- Defines a new role and returns the prototype to allow configuration Roles._prototype:set_allow_all(state) --- Sets the default allow state of the role, true will allow all actions Roles._prototype:allow(actions) --- Sets the allow actions for this role, actions in this list will be allowed for this role Roles._prototype:disallow(actions) --- Sets the disallow actions for this role, will prevent actions from being allowed regardless of inheritance Roles._prototype:is_allowed(action) --- Test for if a role is allowed the given action, mostly internal see Roles.player_allowed Roles._prototype:set_flag(name,value) --- Sets the state of a flag for a role, flags can be used to apply effects to players Roles._prototype:clear_flags() --- Clears all flags from this role, individual flags can be removed with set_flag(name,false) Roles._prototype:has_flag(name) --- A test for if the role has a flag set Roles._prototype:set_custom_tag(tag) --- Sets a custom player tag for the role, can be accessed by other code Roles._prototype:set_custom_color(color) --- Sets a custom colour for the role, can be accessed by other code Roles._prototype:set_permission_group(name,use_factorio_api) --- Sets the permission group for this role, players will be moved to the group of they highest role Roles._prototype:set_parent(role) --- Sets the parent for a role, any action not in allow or disallow will be looked for in its parents Roles._prototype:set_auto_promote_condition(callback) --- Sets an auto promote condition that is checked every 5 seconds, if true is returned then the player will recive the role Roles._prototype:set_block_auto_promote(state) --- Sets the role to not allow players to have auto promote effect them, useful to keep people locked to a punishment Roles._prototype:add_player(player,skip_check,skip_event) --- Adds a player to this role, players can have more than one role at a time, used internally see Roles.assign Roles._prototype:remove_player(player,skip_check,skip_event) --- Removes a player from this role, players can have more than one role at a time, used internally see Roles.unassign Roles._prototype:get_players(online) --- Returns an array of all the players who have this role, can be filtered by online status Roles._prototype:print(message) --- Will print a message to all players with this role ]] local Game = require 'utils.game' local Global = require 'utils.global' local Event = require 'utils.event' local Groups = require 'expcore.permission_groups' local Colours = require 'resources.color_presets' local Roles = { 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={} }, player_role_assigned=script.generate_event_name(), player_role_unassigned=script.generate_event_name(), _prototype={} } --- Internal function used to trigger a few different things when roles are changed local function emit_player_roles_updated(player,type,roles,by_player_name) by_player_name = game.player and game.player.name or by_player_name or '' local event = Roles.player_role_assigned if type == 'unassign' then event = Roles.player_role_unassigned end local by_player = Game.get_player_from_any(by_player_name) local by_player_index = by_player and by_player.index or 0 local role_names = {} for index,role in pairs(roles) do role = Roles.get_role_from_any(role) if role then roles[index] = role table.insert(role_names,role.name) end end game.print({'expcore-roles.game-message-'..type,player.name,table.concat(role_names,', '),by_player_name},Colours.cyan) script.raise_event(event,{ name=Roles.player_roles_updated, tick=game.tick, player_index=player.index, by_player_index=by_player_index, roles=roles }) game.write_file('log/roles.log',game.table_to_json{ player_name=player.name, by_player_name=by_player_name, type=type, roles_changed=role_names }..'\n',true,0) end --- When global is loaded it will have the metatable re-assigned to the roles Global.register(Roles.config,function(tbl) Roles.config = tbl for _,role in pairs(Roles.config.roles) do setmetatable(role,{__index=Roles._prototype}) local parent = Roles.config.roles[role.parent] if parent then setmetatable(role.allowed_actions, {__index=parent.allowed_actions}) end end end) --- Returns a string which contains all roles in index order displaying all data for them -- @treturn string the debug output string function Roles.debug() local output = '' for index,role_name in pairs(Roles.config.order) do local role = Roles.config.roles[role_name] local color = role.custom_color or Colours.white color = string.format('[color=%d,%d,%d]',color.r,color.g,color.b) output = output..string.format('\n%s %s) %s[/color]',color,index,serpent.line(role)) end return output end --- Get a role for the given name -- @tparam name string the name of the role to get -- @treturn Roles._prototype the role with that name or nil function Roles.get_role_by_name(name) return Roles.config.roles[name] end --- Get a role with the given order index -- @tparam index number the place in the oder list of the role to get -- @treturn Roles._prototype the role with that index in the order list or nil 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 any ?number|string|table the value used to find the role -- @treturn Roles._prototype the role that was found or nil see above function Roles.get_role_from_any(any) local tany = type(any) if tany == 'number' or tonumber(any) then any = tonumber(any) return Roles.get_role_by_order(any) elseif tany == 'string' then return Roles.get_role_by_name(any) elseif tany == 'table' then return Roles.get_role_by_name(any.name) end end --- Gets all the roles of the given player, this will always contain the default role -- @tparam player LuaPlayer the player to get the roles of -- @treturn table a table where the values are the roles which the player has function Roles.get_player_roles(player) player = Game.get_player_from_any(player) if not player then return end local roles = Roles.config.players[player.name] or {} local default = Roles.config.roles[Roles.config.internal.default] local rtn = {default} for _,role_name in pairs(roles) do table.insert(rtn,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 player LuaPlayer the player to get the highest role of -- @treturn the role with the highest order index which this player has function Roles.get_player_highest_role(player) local roles = Roles.get_player_roles(player) if not roles then return end local highest for _,role in pairs(roles) do if not highest or role.index < highest.index then highest = role end end return highest end --- Gives a player the given role(s) with an option to pass a by player name used in the log -- @tparam player LuaPlayer the player that will be assigned the roles -- @tparam role table a table of roles that the player will be given, can be one role and can be role names -- @tparam[opt=] by_player_name string the name of the player that will be shown in the log function Roles.assign_player(player,roles,by_player_name) player = Game.get_player_from_any(player) if not player then return end if type(roles) ~= 'table' or roles.name then roles = {roles} end for _,role in pairs(roles) do role = Roles.get_role_from_any(role) if role then role:add_player(player,false,true) end end emit_player_roles_updated(player,'assign',roles,by_player_name) end --- Removes a player from the given role(s) with an option to pass a by player name used in the log -- @tparam player LuaPlayer the player that will have the roles removed -- @tparam roles table a table of roles to be removed from the player, can be one role and can be role names -- @tparam[opt=] by_player_name string the name of the player that will be shown in the logs function Roles.unassign_player(player,roles,by_player_name) player = Game.get_player_from_any(player) if not player then return end if type(roles) ~= 'table' or roles.name then roles = {roles} end for _,role in pairs(roles) do role = Roles.get_role_from_any(role) if role then role:remove_player(player,false,true) end end emit_player_roles_updated(player,'unassign',roles,by_player_name) end --- Overrides all player roles with the given table of roles, useful to mass set roles on game start -- @tparam roles table a table which is indexed by case sensitive player names and has the value of a table of role names function Roles.override_player_roles(roles) Roles.config.players = roles end --- A test for weather a player has the given role -- @tparam player LuaPlayer the player to test the roles of -- @tparam search_role ?string|number|table a pointer to the role that is being searched for -- @treturn boolean true if the player has the role, false otherwise, nil for errors 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 pairs(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 player LuaPlayer the player to test the roles of -- @tparam flag_name string 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 function Roles.player_has_flag(player,flag_name) local roles = Roles.get_player_roles(player) if not roles then return end for _,role in pairs(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 player LuaPlayer the player to test the roles of -- @tparam action string 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 function Roles.player_allowed(player,action) local roles = Roles.get_player_roles(player) if not roles then return end for _,role in pairs(roles) do if role:is_allowed(action) then return true end end return false end --- 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 order table a table which is keyed only by numbers (start 1) and values are roles in order with highest first function Roles.define_role_order(order) -- Clears and then rebuilds the order table Roles.config.order = {} for _,role in ipairs(order) do if type(role) == 'table' and role.name then table.insert(Roles.config.order,role.name) else table.insert(Roles.config.order,role) end end -- Re-links roles to they parents as this is called at the end of the config for index,role in pairs(Roles.config.order) do role = Roles.config.roles[role] 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 name string the name of the flag which the roles will have -- @tparam callback function the function that is called when roles are assigned -- flag param - player - the player that has had they roles changed -- flag param - state - the state of the flag, aka if the flag is present function Roles.define_flag_trigger(name,callback) Roles.config.flags[name] = callback -- this can desync if there are upvalues end --- Sets the default role which every player will have, this needs to be called at least once -- @tparam name string the name of the default role 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 name string the name of the root role 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 name string the name of the new role, must be unique -- @tparam[opt=name] shirt_hand string the shortened version of the name -- @treturn Roles._prototype the start of the config chain for this role function Roles.new_role(name,short_hand) 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 --- Sets the default allow state of the role, true will allow all actions -- @tparam[opt=true] strate boolean true will allow all actions -- @treturn Roles._prototype allows chaining 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 actions table indexed with numbers and is an array of action names, order has no effect -- @treturn Roles._prototype allows chaining function Roles._prototype:allow(actions) if type(actions) ~= 'table' then actions = {actions} end for _,action in pairs(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 actions table indexed with numbers and is an array of action names, order has no effect -- @treturn Roles._prototype allows chaining function Roles._prototype:disallow(actions) if type(actions) ~= 'table' then actions = {actions} end for _,action in pairs(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 action string the name of the action to test if it is allowed -- @treturn boolean true if action is allowed, false otherwise function Roles._prototype:is_allowed(action) local is_root = Roles.config.internal.root.name == self.name return self.allowed_actions[action] or self.allow_all_actions or is_root end --- Sets the state of a flag for a role, flags can be used to apply effects to players -- @tparam name string the name of the flag to set the value of -- @tparam[opt=true] value boolean the state to set the flag to -- @treturn Roles._prototype allows chaining 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 function Roles._prototype:clear_flags() self.flags = {} return self end --- A test for if the role has a flag set -- @tparam name string the name of the flag to test for -- @treturn boolean true if the flag is set, false otherwise function Roles._prototype:has_flag(name) return self.flags[name] or false end --- Sets a custom player tag for the role, can be accessed by other code -- @tparam tag string the value that the tag will be -- @treturn Roles._prototype allows chaining 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 color ?string|table can either be and rgb colour table or the name of a colour defined in the presets -- @treturn Roles._prototype allows chaining 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 name string the name of the permission group to have players moved to -- @tparam[opt=false] use_factorio_api boolean when true the custom permission group module is ignored -- @treturn Roles._prototype allows chaining function Roles._prototype:set_permission_group(name,use_factorio_api) 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 role string the name of the role that will be the parent; has imminent effect if role is already defined -- @treturn Roles._prototype allows chaining function Roles._prototype:set_parent(role) 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 promote condition that is checked every 5 seconds, if true is returned then the player will recive the role -- nb: this is one way, failing false after already gaining the role will not revoke the role -- @tparam callback function receives only one param which is player to promote, return true to promote the player -- @treturn Roles._prototype allows chaining function Roles._prototype:set_auto_promote_condition(callback) self.auto_promote_condition = callback return self end --- Sets the role to not allow players to have auto promote effect them, useful to keep people locked to a punishment -- @tparam[opt=true] state boolean when true the players with this role will not be auto promoted -- @treturn Roles._prototype allows chaining function Roles._prototype:set_block_auto_promote(state) if state == nil then state = true end self.block_auto_promote = not not state -- forces a boolean value return self end --- Adds a player to this role, players can have more than one role at a time, used internally see Roles.assign -- @tparam player LuaPlayer the player that will be given this role -- @tparam skip_check boolean when true player will be taken as the player name (use when player has not yet joined) -- @tparam skip_event boolean when true the event emit will be skipped, this is used internally with Roles.assign -- @treturn boolean true if the player was added successfully function Roles._prototype:add_player(player,skip_check,skip_event) player = Game.get_player_from_any(player) -- Check the player is valid, can be skipped but a name must be given if not player then if skip_check then player = {name=player} else return false end 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 pairs(player_roles) do if role_name == self.name then return false end end table.insert(player_roles,self.name) else Roles.config.players[player.name] = {self.name} end -- Emits event if required if not skip_event then emit_player_roles_updated(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 player LuaPlayer the player that will lose this role -- @tparam skip_check boolean when true player will be taken as the player name (use when player has not yet joined) -- @tparam skip_event boolean when true the event emit will be skipped, this is used internally with Roles.unassign -- @treturn boolean true if the player was removed successfully function Roles._prototype:remove_player(player,skip_check,skip_event) player = Game.get_player_from_any(player) -- Check the player is valid, can be skipped but a name must be given if not player then if skip_check then player = {name=player} else return false end end -- Remove the role from the players roles local player_roles = Roles.config.players[player.name] local rtn = false if player_roles then for index,role_name in pairs(player_roles) do if role_name == self.name then table.remove(player_roles,index) rtn = true break end end if #player_roles == 0 then Roles.config.players[player.name] = nil end end -- Emits event if required if not skip_event then emit_player_roles_updated(player,'unassign',{self}) end return rtn end --- Returns an array of all the players who have this role, can be filtered by online status -- @tparam[opt=nil] online boolean 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 function Roles._prototype:get_players(online) local players = {} -- Gets all players that have this role for player_name,player_roles in pairs(Roles.config.players) do for _,role_name in pairs(player_roles) do if role_name == self.name then table.insert(players,player_name) end end end -- Convert the player names to LuaPlayer for index,player_name in pairs(players) do players[index] = Game.get_player_from_any(player_name) end -- Filter by online if param is defined if online == nil then return players else local filtered = {} for _,player in pairs(players) do if player.connected == online then table.insert(filtered,player) end end return filtered end end --- Will print a message to all players with this role -- @tparam message string the message that will be printed to the players -- @treturn number the number of players who received the message function Roles._prototype:print(message) local players = self:get_players(true) for _,player in pairs(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.get_player_by_index(event.player_index) -- Updates flags given to the player for flag,callback in pairs(Roles.config.flags) do local state = Roles.player_has_flag(player,flag) local success,err = pcall(callback,player,state) if not success then log{'expcore-roles.error-log-format-flag',flag,err} end 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 group.add_player(player) end else Groups.set_player_group(player,highest.permission_group) end end end --- When a player joined or has a role change then the update is triggered Event.add(Roles.player_role_assigned,role_update) Event.add(Roles.player_role_unassigned,role_update) Event.add(defines.events.on_player_joined_game,role_update) -- Every 60 seconds the auto promote check is preformed Event.on_nth_tick(3600,function() local promotes = {} for _,player in pairs(game.connected_players) do for _,role in pairs(Roles.config.roles) do if role.auto_promote_condition then local success,err = pcall(role.auto_promote_condition,player) if not success then log{'expcore-roles.error-log-format-promote',role.name,err} else if err == true and not Roles.player_has_role(player,role) then if promotes[player.name] then table.insert(promotes[player.name],role.name) else promotes[player.name] = {role.name} end end end end end end for player_name,roles in pairs(promotes) do Roles.assign_player(player_name,roles) end end) -- Return Roles return Roles