mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-09-21 17:04:00 +00:00
Move the seed into exp_scenario and seed the permission groups (#457)
The seed roles were owned by exp_roles, but they describe the scenario, so they now live in exp_scenario/seed.ts next to the permissions they grant. Each seed role names the permission group its holders belong to, and the five groups the legacy config defined (Admin, Trusted, Standard, Guest, Restricted) are seeded through exp_groups along with one role mapping per role. Mapping priorities follow how exp_roles ranks a player's highest role, so Jail lands in Restricted regardless of other roles. SeedRolesRequest becomes exp_scenario's SeedRequest behind a new exp_scenario.seed permission, and the button moves to the scenario's web plugin, which gets a web entrypoint for it. exp_roles no longer depends on exp_scenario, and exp_scenario gains controller tests around the seed. With groups seeded from the controller the legacy expcore.permission_groups module and its config are removed, along with the Group rcon static.
This commit is contained in:
@@ -24,6 +24,4 @@ return {
|
|||||||
"modules.gui.vlayer",
|
"modules.gui.vlayer",
|
||||||
|
|
||||||
"modules.graftorio.require", -- graftorio
|
"modules.graftorio.require", -- graftorio
|
||||||
--- Config Files
|
|
||||||
"config.expcore.permission_groups", -- loads some predefined permission groups
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
--- Use this file to add new permission groups to the game;
|
|
||||||
-- start with Permission_Groups.new_group('name');
|
|
||||||
-- then use either :allow_all() or :disallow_all() to set the default for non specified actions;
|
|
||||||
-- then use :allow{} and :disallow{} to specify certain actions to allow/disallow
|
|
||||||
-- @config Permission-Groups
|
|
||||||
|
|
||||||
-- local Event = require("modules/exp_legacy/utils/event") -- @dep utils.event
|
|
||||||
local Groups = require("modules.exp_legacy.expcore.permission_groups") --- @dep expcore.permission_groups
|
|
||||||
|
|
||||||
Groups.new_group("Admin")
|
|
||||||
:allow_all()
|
|
||||||
:disallow{
|
|
||||||
"add_permission_group", -- admin
|
|
||||||
"delete_permission_group",
|
|
||||||
"edit_permission_group",
|
|
||||||
"import_permissions_string",
|
|
||||||
"map_editor_action",
|
|
||||||
"toggle_map_editor",
|
|
||||||
"change_multiplayer_config",
|
|
||||||
"set_heat_interface_mode",
|
|
||||||
"set_heat_interface_temperature",
|
|
||||||
"set_infinity_container_filter_item",
|
|
||||||
"set_infinity_container_remove_unfiltered_items",
|
|
||||||
"set_infinity_pipe_filter",
|
|
||||||
}
|
|
||||||
|
|
||||||
Groups.new_group("Trusted")
|
|
||||||
:allow_all()
|
|
||||||
:disallow{
|
|
||||||
"add_permission_group", -- admin
|
|
||||||
"delete_permission_group",
|
|
||||||
"edit_permission_group",
|
|
||||||
"import_permissions_string",
|
|
||||||
"map_editor_action",
|
|
||||||
"toggle_map_editor",
|
|
||||||
"change_multiplayer_config",
|
|
||||||
"set_heat_interface_mode",
|
|
||||||
"set_heat_interface_temperature",
|
|
||||||
"set_infinity_container_filter_item",
|
|
||||||
"set_infinity_container_remove_unfiltered_items",
|
|
||||||
"set_infinity_pipe_filter",
|
|
||||||
"admin_action", -- trusted
|
|
||||||
}
|
|
||||||
|
|
||||||
Groups.new_group("Standard")
|
|
||||||
:allow_all()
|
|
||||||
:disallow{
|
|
||||||
"add_permission_group", -- admin
|
|
||||||
"delete_permission_group",
|
|
||||||
"edit_permission_group",
|
|
||||||
"import_permissions_string",
|
|
||||||
"map_editor_action",
|
|
||||||
"toggle_map_editor",
|
|
||||||
"change_multiplayer_config",
|
|
||||||
"set_heat_interface_mode",
|
|
||||||
"set_heat_interface_temperature",
|
|
||||||
"set_infinity_container_filter_item",
|
|
||||||
"set_infinity_container_remove_unfiltered_items",
|
|
||||||
"set_infinity_pipe_filter",
|
|
||||||
"admin_action", -- trusted
|
|
||||||
"change_programmable_speaker_alert_parameters", -- standard
|
|
||||||
"drop_item",
|
|
||||||
"open_new_platform_button_from_rocket_silo",
|
|
||||||
"set_rocket_silo_send_to_orbit_automated_mode",
|
|
||||||
}
|
|
||||||
|
|
||||||
Groups.new_group("Guest")
|
|
||||||
:allow_all()
|
|
||||||
:disallow{
|
|
||||||
"add_permission_group", -- admin
|
|
||||||
"delete_permission_group",
|
|
||||||
"edit_permission_group",
|
|
||||||
"import_permissions_string",
|
|
||||||
"map_editor_action",
|
|
||||||
"toggle_map_editor",
|
|
||||||
"change_multiplayer_config",
|
|
||||||
"set_heat_interface_mode",
|
|
||||||
"set_heat_interface_temperature",
|
|
||||||
"set_infinity_container_filter_item",
|
|
||||||
"set_infinity_container_remove_unfiltered_items",
|
|
||||||
"set_infinity_pipe_filter",
|
|
||||||
"admin_action", -- trusted
|
|
||||||
"change_programmable_speaker_alert_parameters", -- standard
|
|
||||||
"drop_item",
|
|
||||||
"open_new_platform_button_from_rocket_silo",
|
|
||||||
"set_rocket_silo_send_to_orbit_automated_mode",
|
|
||||||
"change_programmable_speaker_parameters", -- guest
|
|
||||||
"change_train_stop_station",
|
|
||||||
-- 'deconstruct',
|
|
||||||
"remove_cables",
|
|
||||||
"remove_train_station",
|
|
||||||
"reset_assembling_machine",
|
|
||||||
"rotate_entity",
|
|
||||||
-- 'use_artillery_remote', -- not in 2.0
|
|
||||||
"launch_rocket",
|
|
||||||
"cancel_research",
|
|
||||||
-- 'activate_cut', -- not in 2.0
|
|
||||||
"flush_opened_entity_fluid",
|
|
||||||
"flush_opened_entity_specific_fluid",
|
|
||||||
}
|
|
||||||
|
|
||||||
Groups.new_group("Restricted")
|
|
||||||
:disallow_all()
|
|
||||||
:allow("write_to_console")
|
|
||||||
|
|
||||||
--[[ These events are used until a role system is added to make it easier for our admins
|
|
||||||
|
|
||||||
local trusted_time = 60*60*60*10 -- 10 hour
|
|
||||||
local standard_time = 60*60*60*3 -- 3 hour
|
|
||||||
local function assign_group(player)
|
|
||||||
local current_group_name = player.permission_group and player.permission_group.name or 'None'
|
|
||||||
if player.admin then
|
|
||||||
Permission_Groups.set_player_group(player,'Admin')
|
|
||||||
elseif player.online_time > trusted_time or current_group_name == 'Trusted' then
|
|
||||||
Permission_Groups.set_player_group(player,'Trusted')
|
|
||||||
elseif player.online_time > standard_time or current_group_name == 'Standard' then
|
|
||||||
Permission_Groups.set_player_group(player,'Standard')
|
|
||||||
else
|
|
||||||
Permission_Groups.set_player_group(player,'Guest')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Event.add(defines.events.on_player_joined_game,function(event)
|
|
||||||
local player = game.players[event.player_index]
|
|
||||||
assign_group(player)
|
|
||||||
end)
|
|
||||||
|
|
||||||
Event.add(defines.events.on_player_promoted,function(event)
|
|
||||||
local player = game.players[event.player_index]
|
|
||||||
assign_group(player)
|
|
||||||
end)
|
|
||||||
|
|
||||||
Event.add(defines.events.on_player_demoted,function(event)
|
|
||||||
local player = game.players[event.player_index]
|
|
||||||
assign_group(player)
|
|
||||||
end)
|
|
||||||
|
|
||||||
local check_interval = 60*60*15 -- 15 minutes
|
|
||||||
Event.on_nth_tick(check_interval,function(event)
|
|
||||||
for _,player in pairs(game.connected_players) do
|
|
||||||
assign_group(player)
|
|
||||||
end
|
|
||||||
end)]]
|
|
||||||
@@ -1,355 +0,0 @@
|
|||||||
--[[-- Core Module - Permission Groups
|
|
||||||
- Permission group making for factorio so you never have to make one by hand again
|
|
||||||
@core Groups
|
|
||||||
@alias Permissions_Groups
|
|
||||||
|
|
||||||
@usage--- Example Group (Allow All)
|
|
||||||
-- here we will create an admin group however we do not want them to use the map editor or mess with the permission groups
|
|
||||||
Permission_Groups.new_group('Admin') -- this defines a new group called "Admin"
|
|
||||||
:allow_all() -- this makes the default to allow any input action unless set other wise
|
|
||||||
:disallow{ -- here we disallow the input action we don't want them to use
|
|
||||||
'add_permission_group',
|
|
||||||
'delete_permission_group',
|
|
||||||
'import_permissions_string',
|
|
||||||
'map_editor_action',
|
|
||||||
'toggle_map_editor'
|
|
||||||
}
|
|
||||||
|
|
||||||
@usage--- Example Group (Disallow All)
|
|
||||||
-- here we will create a group that cant do anything but talk in chat
|
|
||||||
Permission_Groups.new_group('Restricted') -- this defines a new group called "Restricted"
|
|
||||||
:disallow_all() -- this makes the default to disallow any input action unless set other wise
|
|
||||||
:allow('write_to_console') -- here we allow them to chat, {} can be used here if we had more than one action
|
|
||||||
|
|
||||||
]]
|
|
||||||
|
|
||||||
local Event = require("modules/exp_legacy/utils/event")
|
|
||||||
local Async = require("modules/exp_util/async")
|
|
||||||
|
|
||||||
local PermissionsGroups = {
|
|
||||||
groups = {}, -- store for the different groups that are created
|
|
||||||
_prototype = {}, -- stores functions that are used on group instances
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Async function to add players to permission groups
|
|
||||||
local add_to_permission_group_async =
|
|
||||||
Async.register(function(permission_group, player)
|
|
||||||
permission_group.add_player(player)
|
|
||||||
end)
|
|
||||||
PermissionsGroups.add_to_permission_group_async = add_to_permission_group_async
|
|
||||||
|
|
||||||
-- Async function to remove players from permission groups
|
|
||||||
local remove_from_permission_group_async =
|
|
||||||
Async.register(function(permission_group, player)
|
|
||||||
permission_group.remove_player(player)
|
|
||||||
end)
|
|
||||||
PermissionsGroups.remove_from_permission_group_async = remove_from_permission_group_async
|
|
||||||
|
|
||||||
--- Getters.
|
|
||||||
-- Functions that get permission groups
|
|
||||||
-- @section getters
|
|
||||||
|
|
||||||
--[[-- Defines a new permission group that can have it actions set in the config
|
|
||||||
@tparam string name the name of the new group
|
|
||||||
@treturn Permissions_Groups._prototype the new group made with function to allow and disallow actions
|
|
||||||
|
|
||||||
@usage-- Defining a new permission group
|
|
||||||
Groups.new_group('Admin')
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups.new_group(name)
|
|
||||||
local group = setmetatable({
|
|
||||||
name = name,
|
|
||||||
actions = {},
|
|
||||||
allow_all_actions = true,
|
|
||||||
}, {
|
|
||||||
__index = PermissionsGroups._prototype,
|
|
||||||
})
|
|
||||||
PermissionsGroups.groups[name] = group
|
|
||||||
return group
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[-- Returns the group with the given name, case sensitive
|
|
||||||
@tparam string name the name of the group to get
|
|
||||||
@treturn ?Permissions_Groups._prototype|nil the group with that name or nil if non found
|
|
||||||
|
|
||||||
@usage-- Getting a permision group
|
|
||||||
local admin_group = Groups.get_group_by_name('Admin')
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups.get_group_by_name(name)
|
|
||||||
return PermissionsGroups.groups[name]
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[-- Returns the group that a player is in
|
|
||||||
@tparam LuaPlayer player the player to get the group of can be name index etc
|
|
||||||
@treturn ?Permissions_Groups._prototype|nil the group with that player or nil if non found
|
|
||||||
|
|
||||||
@usage-- Get your permission group
|
|
||||||
local group = Groups.get_group_from_player(game.player)
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups.get_group_from_player(player)
|
|
||||||
local group = player.permission_group
|
|
||||||
if group then
|
|
||||||
return PermissionsGroups.groups[group.name]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Setters.
|
|
||||||
-- Functions that control all groups
|
|
||||||
-- @section players
|
|
||||||
|
|
||||||
--[[-- Reloads/creates all permission groups and sets them to they configured state
|
|
||||||
|
|
||||||
@usage-- Reload the permission groups, used internally
|
|
||||||
Groups.reload_permissions()
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups.reload_permissions()
|
|
||||||
for _, group in pairs(PermissionsGroups.groups) do
|
|
||||||
group:create()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[-- Sets a player's group to the one given, a player can only have one group at a time
|
|
||||||
@tparam LuaPlayer player the player to effect can be name index etc
|
|
||||||
@tparam string group the name of the group to give to the player
|
|
||||||
@treturn boolean true if the player was added successfully, false other wise
|
|
||||||
|
|
||||||
@usage-- Set your permission group
|
|
||||||
Groups.set_player_group(game.player, 'Admin')
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups.set_player_group(player, group)
|
|
||||||
group = PermissionsGroups.get_group_by_name(group)
|
|
||||||
if not group or not player then return false end
|
|
||||||
group:add_player(player)
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Actions.
|
|
||||||
-- Functions that control group actions
|
|
||||||
-- @section actions
|
|
||||||
|
|
||||||
--[[-- Sets the allow state of an action for this group, used internally but is safe to use else where
|
|
||||||
@tparam ?string|defines.input_action action the action that you want to set the state of
|
|
||||||
@tparam boolean state the state that you want to set it to, true = allow, false = disallow
|
|
||||||
@treturn Permissions_Groups._prototype returns self so function can be chained
|
|
||||||
|
|
||||||
@usage-- Set an action to be disallowed
|
|
||||||
group:set_action('toggle_map_editor', false)
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups._prototype:set_action(action, state)
|
|
||||||
local input_action = defines.input_action[action] --[[@as defines.input_action?]]
|
|
||||||
if input_action == nil then input_action = action end
|
|
||||||
assert(type(input_action) == "number", tostring(action) .. " is not a valid input action")
|
|
||||||
self.actions[input_action] = state
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[-- Sets an action or actions to be allowed for this group even with disallow_all triggered, Do not use in runtime
|
|
||||||
@tparam string|Array<string> actions the action or actions that you want to allow for this group
|
|
||||||
@treturn Permissions_Groups._prototype returns self so function can be chained
|
|
||||||
|
|
||||||
@usage-- Allow some actions
|
|
||||||
group:allow{
|
|
||||||
'write_to_console'
|
|
||||||
}
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups._prototype:allow(actions)
|
|
||||||
if type(actions) ~= "table" then
|
|
||||||
actions = { actions }
|
|
||||||
end
|
|
||||||
for _, action in pairs(actions) do
|
|
||||||
self:set_action(action, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[-- Sets an action or actions to be disallowed for this group even with allow_all triggered, Do not use in runtime
|
|
||||||
@tparam string|Array<string> actions the action or actions that you want to disallow for this group
|
|
||||||
@treturn Permissions_Groups._prototype returns self so function can be chained
|
|
||||||
|
|
||||||
@usage-- Disalow some actions
|
|
||||||
group:disallow{
|
|
||||||
'add_permission_group',
|
|
||||||
'delete_permission_group',
|
|
||||||
'import_permissions_string',
|
|
||||||
'map_editor_action',
|
|
||||||
'toggle_map_editor'
|
|
||||||
}
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups._prototype:disallow(actions)
|
|
||||||
if type(actions) ~= "table" then
|
|
||||||
actions = { actions }
|
|
||||||
end
|
|
||||||
for _, action in pairs(actions) do
|
|
||||||
self:set_action(action, false)
|
|
||||||
end
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[-- Sets the default state for any actions not given to be allowed, useful with :disallow
|
|
||||||
@treturn Permissions_Groups._prototype returns self so function can be chained
|
|
||||||
|
|
||||||
@usage-- Allow all actions unless given by disallow
|
|
||||||
group:allow_all()
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups._prototype:allow_all()
|
|
||||||
self.allow_all_actions = true
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[-- Sets the default state for any action not given to be disallowed, useful with :allow
|
|
||||||
@treturn Permissions_Groups._prototype returns self so function can be chained
|
|
||||||
|
|
||||||
@usage-- Disallow all actions unless given by allow
|
|
||||||
group:disallow_all()
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups._prototype:disallow_all()
|
|
||||||
self.allow_all_actions = false
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[-- Returns if an input action is allowed for this group
|
|
||||||
@tparam ?string|defines.input_action action the action that you want to test for
|
|
||||||
@treturn boolean true if the group is allowed the action, false other wise
|
|
||||||
|
|
||||||
@usage-- Test if a group is allowed an action
|
|
||||||
local allowed = group:is_allowed('write_to_console')
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups._prototype:is_allowed(action)
|
|
||||||
if type(action) == "string" then
|
|
||||||
action = defines.input_action[action]
|
|
||||||
end
|
|
||||||
local state = self.actions[action]
|
|
||||||
if state == nil then
|
|
||||||
state = self.allow_all_actions
|
|
||||||
end
|
|
||||||
return state
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Players.
|
|
||||||
-- Functions that control group players
|
|
||||||
-- @section players
|
|
||||||
|
|
||||||
--[[-- Creates or updates the permission group with the configured actions, used internally
|
|
||||||
@treturn LuaPermissionGroup the permission group that was created
|
|
||||||
|
|
||||||
@usage-- Create the permission group so players can be added, used internally
|
|
||||||
group:create()
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups._prototype:create()
|
|
||||||
local group = self:get_raw()
|
|
||||||
if not group then
|
|
||||||
group = game.permissions.create_group(self.name) --[[@as LuaPermissionGroup]]
|
|
||||||
end
|
|
||||||
for _, action in pairs(defines.input_action) do
|
|
||||||
group.set_allows_action(action, self:is_allowed(action))
|
|
||||||
end
|
|
||||||
|
|
||||||
return group
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[-- Returns the LuaPermissionGroup that was created with this group object, used internally
|
|
||||||
@treturn LuaPermissionGroup the raw lua permission group
|
|
||||||
|
|
||||||
@usage-- Get the factorio api permision group, used internally
|
|
||||||
local permission_group = group:get_raw()
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups._prototype:get_raw()
|
|
||||||
return game.permissions.get_group(self.name)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[-- Adds a player to this group
|
|
||||||
@tparam LuaPlayer player LuaPlayer the player you want to add to this group can be name or index etc
|
|
||||||
@treturn boolean true if the player was added successfully, false other wise
|
|
||||||
|
|
||||||
@usage-- Add a player to this permission group
|
|
||||||
group:add_player(game.player)
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups._prototype:add_player(player)
|
|
||||||
local group = self:get_raw()
|
|
||||||
if not group or not player then return false end
|
|
||||||
add_to_permission_group_async(group, player)
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[-- Removes a player from this group
|
|
||||||
@tparam LuaPlayer player LuaPlayer the player you want to remove from this group can be name or index etc
|
|
||||||
@treturn boolean true if the player was removed successfully, false other wise
|
|
||||||
|
|
||||||
@usage-- Remove a player from this permission group
|
|
||||||
group:remove_player(game.player)
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups._prototype:remove_player(player)
|
|
||||||
local group = self:get_raw()
|
|
||||||
if not group or not player then return false end
|
|
||||||
remove_from_permission_group_async(group, player)
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[-- Returns all player that are in this group with the option to filter to online/offline only
|
|
||||||
@tparam[opt] boolean online if nil returns all players, if true online players only, if false returns online players only
|
|
||||||
@treturn table a table of players that are in this group; filtered if online param is given
|
|
||||||
|
|
||||||
@usage-- Get all players in this group
|
|
||||||
local online_players = group:get_players()
|
|
||||||
|
|
||||||
@usage-- Get all online players in this group
|
|
||||||
local online_players = group:get_players(true)
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups._prototype:get_players(online)
|
|
||||||
local players = {}
|
|
||||||
local group = self:get_raw()
|
|
||||||
if group then
|
|
||||||
if online == nil then
|
|
||||||
return group.players
|
|
||||||
else
|
|
||||||
for _, player in pairs(group.players) do
|
|
||||||
if player.connected == online then
|
|
||||||
table.insert(player, player)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return players
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[-- Prints a message to every player in this group
|
|
||||||
@tparam string message the message that you want to send to the players
|
|
||||||
@treturn number the number of players that received the message
|
|
||||||
|
|
||||||
@usage-- Print a message to all players in thie group
|
|
||||||
group:print('Hello, World!')
|
|
||||||
|
|
||||||
]]
|
|
||||||
function PermissionsGroups._prototype:print(message)
|
|
||||||
local players = self:get_players(true)
|
|
||||||
for _, player in pairs(players) do
|
|
||||||
player.print(message)
|
|
||||||
end
|
|
||||||
|
|
||||||
return #players
|
|
||||||
end
|
|
||||||
|
|
||||||
-- when the game starts it will make the permission groups
|
|
||||||
Event.on_init(function()
|
|
||||||
PermissionsGroups.reload_permissions()
|
|
||||||
end)
|
|
||||||
|
|
||||||
return PermissionsGroups
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { BaseControllerPlugin, InstanceRecord } from "@clusterio/controller";
|
import { BaseControllerPlugin, InstanceRecord } from "@clusterio/controller";
|
||||||
import * as lib from "@clusterio/lib";
|
import * as lib from "@clusterio/lib";
|
||||||
import * as messages from "./messages";
|
import * as messages from "./messages";
|
||||||
import { SeedRole, seedRoles, flattenSeedPermissions } from "./seed";
|
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
|
|
||||||
export class ControllerPlugin extends BaseControllerPlugin {
|
export class ControllerPlugin extends BaseControllerPlugin {
|
||||||
@@ -35,7 +34,6 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
|||||||
|
|
||||||
this.controller.handle(messages.RoleListRequest, this.handleRoleListRequest.bind(this));
|
this.controller.handle(messages.RoleListRequest, this.handleRoleListRequest.bind(this));
|
||||||
this.controller.handle(messages.RoleMetaUpdateRequest, this.handleRoleMetaUpdateRequest.bind(this));
|
this.controller.handle(messages.RoleMetaUpdateRequest, this.handleRoleMetaUpdateRequest.bind(this));
|
||||||
this.controller.handle(messages.SeedRolesRequest, this.handleSeedRolesRequest.bind(this));
|
|
||||||
|
|
||||||
this.controller.handle(messages.AssignmentListRequest, this.handleAssignmentListRequest.bind(this));
|
this.controller.handle(messages.AssignmentListRequest, this.handleAssignmentListRequest.bind(this));
|
||||||
this.controller.handle(messages.AssignmentUpdateRequest, this.handleAssignmentUpdateRequest.bind(this));
|
this.controller.handle(messages.AssignmentUpdateRequest, this.handleAssignmentUpdateRequest.bind(this));
|
||||||
@@ -45,68 +43,6 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
|||||||
await this.roleMeta.save();
|
await this.roleMeta.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Seeding
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the roles the scenario shipped with. Roles which already exist
|
|
||||||
* by name are reused and only gain the seed permissions.
|
|
||||||
*/
|
|
||||||
async handleSeedRolesRequest() {
|
|
||||||
for (const [index, seedRole] of seedRoles.entries()) {
|
|
||||||
const role = this.seedRole(seedRole);
|
|
||||||
if (!role) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.roleMeta.set(new messages.RoleMetaRecord(
|
|
||||||
role.id,
|
|
||||||
index + 1,
|
|
||||||
seedRole.priority ?? 0,
|
|
||||||
seedRole.shortHand,
|
|
||||||
"",
|
|
||||||
seedRole.color,
|
|
||||||
seedRole.autoAssignHours === undefined ? null : seedRole.autoAssignHours * 3600000,
|
|
||||||
seedRole.blockAutoAssign ?? false,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.info(`Seeded ${seedRoles.length} roles`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Find or create the clusterio role for a seed role, returns undefined if it has no role to use. */
|
|
||||||
seedRole(seedRole: SeedRole) {
|
|
||||||
const roles = this.controller.roles;
|
|
||||||
if (seedRole.isAdmin) {
|
|
||||||
return roles.get(lib.Role.DefaultAdminRoleId);
|
|
||||||
}
|
|
||||||
if (seedRole.isDefault) {
|
|
||||||
const defaultRoleId = this.controller.config.get("controller.default_role_id");
|
|
||||||
return defaultRoleId !== null ? roles.get(defaultRoleId) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const permissions = flattenSeedPermissions(seedRole);
|
|
||||||
for (const permission of permissions) {
|
|
||||||
if (!lib.permissions.has(permission)) {
|
|
||||||
this.logger.warn(`Seed role ${seedRole.name} grants unknown permission ${permission}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let role = [...roles.valuesMutable()].find(other => other.name === seedRole.name);
|
|
||||||
if (role) {
|
|
||||||
for (const permission of permissions) {
|
|
||||||
role.permissions.add(permission);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const id = Math.max(5, ...[...roles.keys()].map(other => other + 1));
|
|
||||||
role = new lib.Role(id, seedRole.name, "", permissions);
|
|
||||||
this.logger.info(`Created role ${seedRole.name}`);
|
|
||||||
}
|
|
||||||
roles.set(role);
|
|
||||||
return role;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Role properties
|
Role properties
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export const plugin: lib.PluginDeclaration = {
|
|||||||
|
|
||||||
messages.RoleListRequest,
|
messages.RoleListRequest,
|
||||||
messages.RoleMetaUpdateRequest,
|
messages.RoleMetaUpdateRequest,
|
||||||
messages.SeedRolesRequest,
|
|
||||||
|
|
||||||
messages.AssignmentListRequest,
|
messages.AssignmentListRequest,
|
||||||
messages.AssignmentUpdateRequest,
|
messages.AssignmentUpdateRequest,
|
||||||
|
|||||||
@@ -365,18 +365,6 @@ export class RoleMetaUpdateRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create the roles the scenario shipped with, see seed.ts. */
|
|
||||||
export class SeedRolesRequest {
|
|
||||||
declare ["constructor"]: typeof SeedRolesRequest;
|
|
||||||
static plugin = "exp_roles" as const;
|
|
||||||
static type = "request" as const;
|
|
||||||
static src = "control" as const;
|
|
||||||
static dst = "controller" as const;
|
|
||||||
static permission = "core.role.create" as const;
|
|
||||||
|
|
||||||
constructor() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Assignment requests
|
Assignment requests
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
"@clusterio/host": "workspace:^",
|
"@clusterio/host": "workspace:^",
|
||||||
"@clusterio/lib": "workspace:^",
|
"@clusterio/lib": "workspace:^",
|
||||||
"@clusterio/web_ui": "workspace:^",
|
"@clusterio/web_ui": "workspace:^",
|
||||||
"@expcluster/scenario": "workspace:^",
|
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
"antd": "catalog:",
|
"antd": "catalog:",
|
||||||
|
|||||||
@@ -4,17 +4,12 @@ const lib = require("@clusterio/lib");
|
|||||||
const { Controller } = require("@clusterio/controller");
|
const { Controller } = require("@clusterio/controller");
|
||||||
const { ControllerPlugin } = require("../dist/node/controller");
|
const { ControllerPlugin } = require("../dist/node/controller");
|
||||||
const messages = require("../dist/node/messages");
|
const messages = require("../dist/node/messages");
|
||||||
const { seedRoles } = require("../dist/node/seed");
|
|
||||||
|
|
||||||
// Importing this defines the exp_scenario permissions the seed grants
|
|
||||||
require("@expcluster/scenario/dist/node/permissions");
|
|
||||||
|
|
||||||
// The controller validates message classes against the link registry
|
// The controller validates message classes against the link registry
|
||||||
lib.Link.register(messages.RoleUpdatedEvent);
|
lib.Link.register(messages.RoleUpdatedEvent);
|
||||||
lib.Link.register(messages.AssignmentUpdatedEvent);
|
lib.Link.register(messages.AssignmentUpdatedEvent);
|
||||||
lib.Link.register(messages.RoleListRequest);
|
lib.Link.register(messages.RoleListRequest);
|
||||||
lib.Link.register(messages.RoleMetaUpdateRequest);
|
lib.Link.register(messages.RoleMetaUpdateRequest);
|
||||||
lib.Link.register(messages.SeedRolesRequest);
|
|
||||||
lib.Link.register(messages.AssignmentListRequest);
|
lib.Link.register(messages.AssignmentListRequest);
|
||||||
lib.Link.register(messages.AssignmentUpdateRequest);
|
lib.Link.register(messages.AssignmentUpdateRequest);
|
||||||
|
|
||||||
@@ -229,22 +224,5 @@ t.test("class ControllerPlugin", t2 => {
|
|||||||
t3.strictSame(none, null, "nothing is replayed when up to date");
|
t3.strictSame(none, null, "nothing is replayed when up to date");
|
||||||
});
|
});
|
||||||
|
|
||||||
t2.test(".handleSeedRolesRequest() creates the roles and reuses them by name", async t3 => {
|
|
||||||
const { plugin, controller } = await startPlugin(t3, {
|
|
||||||
roles: [role(0, "Cluster Admin", ["core.admin"]), role(1, "Player")],
|
|
||||||
});
|
|
||||||
|
|
||||||
await plugin.handleSeedRolesRequest();
|
|
||||||
t3.strictSame(controller.roles.size, seedRoles.length, "every seed role exists");
|
|
||||||
|
|
||||||
const moderator = [...controller.roles.values()].find(other => other.name === "Moderator");
|
|
||||||
t3.ok(moderator.permissions.has("exp_scenario.command.jail"), "parent permissions are flattened in");
|
|
||||||
t3.ok(plugin.roleMeta.get(moderator.id), "the role properties are created");
|
|
||||||
t3.strictSame(plugin.roleMeta.get(moderator.id).shortHand, "Mod", "the properties match the seed");
|
|
||||||
|
|
||||||
await plugin.handleSeedRolesRequest();
|
|
||||||
t3.strictSame(controller.roles.size, seedRoles.length, "seeding again reuses the roles");
|
|
||||||
});
|
|
||||||
|
|
||||||
t2.end();
|
t2.end();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import React, { useContext, useState } from "react";
|
|
||||||
import { Button, Popconfirm } from "antd";
|
|
||||||
|
|
||||||
import { ControlContext, SectionHeader, useAccount, notifyErrorHandler } from "@clusterio/web_ui";
|
|
||||||
|
|
||||||
import { SeedRolesRequest } from "../../messages";
|
|
||||||
|
|
||||||
/** Button on the roles page which creates the roles the scenario shipped with. */
|
|
||||||
export default function SeedRoles() {
|
|
||||||
const control = useContext(ControlContext);
|
|
||||||
const account = useAccount();
|
|
||||||
const [seeding, setSeeding] = useState(false);
|
|
||||||
|
|
||||||
if (!account.hasPermission("core.role.create")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <SectionHeader
|
|
||||||
title="ExpGaming Roles"
|
|
||||||
extra={<Popconfirm
|
|
||||||
title="Create the ExpGaming roles?"
|
|
||||||
description="Roles which already exist by name are kept and only gain permissions."
|
|
||||||
onConfirm={() => {
|
|
||||||
setSeeding(true);
|
|
||||||
control.send(new SeedRolesRequest())
|
|
||||||
.catch(notifyErrorHandler("Error seeding roles"))
|
|
||||||
.finally(() => setSeeding(false));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button loading={seeding}>Seed roles</Button>
|
|
||||||
</Popconfirm>}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ import * as lib from "@clusterio/lib";
|
|||||||
import * as messages from "../messages";
|
import * as messages from "../messages";
|
||||||
|
|
||||||
import RoleProperties from "./components/RoleProperties";
|
import RoleProperties from "./components/RoleProperties";
|
||||||
import SeedRoles from "./components/SeedRoles";
|
|
||||||
|
|
||||||
export class WebPlugin extends BaseWebPlugin {
|
export class WebPlugin extends BaseWebPlugin {
|
||||||
roles = new lib.MapSubscriber(messages.RoleUpdatedEvent, this.control);
|
roles = new lib.MapSubscriber(messages.RoleUpdatedEvent, this.control);
|
||||||
@@ -15,7 +14,6 @@ export class WebPlugin extends BaseWebPlugin {
|
|||||||
// does not carry in its type
|
// does not carry in its type
|
||||||
this.componentExtra = {
|
this.componentExtra = {
|
||||||
RoleViewPage: RoleProperties as React.ComponentType,
|
RoleViewPage: RoleProperties as React.ComponentType,
|
||||||
RolesPage: SeedRoles,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,157 @@
|
|||||||
import * as lib from "@clusterio/lib";
|
import * as lib from "@clusterio/lib";
|
||||||
import { BaseControllerPlugin } from "@clusterio/controller";
|
import { BaseControllerPlugin } from "@clusterio/controller";
|
||||||
|
import type { ControllerPlugin as RolesPlugin } from "@expcluster/roles/dist/node/controller";
|
||||||
|
import type { ControllerPlugin as GroupsPlugin } from "@expcluster/permission-groups/dist/node/controller";
|
||||||
|
import { RoleMetaRecord } from "@expcluster/roles/dist/node/messages";
|
||||||
|
import { GroupRecord, GroupPermissions, RoleMappingRecord } from "@expcluster/permission-groups/dist/node/messages";
|
||||||
|
import * as messages from "./messages";
|
||||||
|
import { SeedRole, SeedGroup, seedRoles, seedGroups, flattenSeedPermissions } from "./seed";
|
||||||
|
|
||||||
export class ControllerPlugin extends BaseControllerPlugin {
|
export class ControllerPlugin extends BaseControllerPlugin {
|
||||||
|
async init() {
|
||||||
|
this.controller.handle(messages.SeedRequest, this.handleSeedRequest.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the roles and permission groups the scenario shipped with.
|
||||||
|
*
|
||||||
|
* Roles which already exist by name are reused and only gain the seed
|
||||||
|
* permissions, groups which already exist by name are reset to the seed.
|
||||||
|
*/
|
||||||
|
async handleSeedRequest() {
|
||||||
|
const rolesPlugin = this.controller.plugins.get("exp_roles") as RolesPlugin | undefined;
|
||||||
|
const groupsPlugin = this.controller.plugins.get("exp_groups") as GroupsPlugin | undefined;
|
||||||
|
if (!rolesPlugin || !groupsPlugin) {
|
||||||
|
throw new lib.RequestError("Seeding requires the exp_roles and exp_groups plugins");
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleIds = new Map<string, number>();
|
||||||
|
for (const [index, seedRole] of seedRoles.entries()) {
|
||||||
|
const role = this.seedRole(seedRole);
|
||||||
|
if (!role) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
roleIds.set(seedRole.name, role.id);
|
||||||
|
rolesPlugin.roleMeta.set(new RoleMetaRecord(
|
||||||
|
role.id,
|
||||||
|
index + 1,
|
||||||
|
seedRole.priority ?? 0,
|
||||||
|
seedRole.shortHand,
|
||||||
|
"",
|
||||||
|
seedRole.color,
|
||||||
|
seedRole.autoAssignHours === undefined ? null : seedRole.autoAssignHours * 3600000,
|
||||||
|
seedRole.blockAutoAssign ?? false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupIds = new Map<string, number>();
|
||||||
|
for (const seedGroup of seedGroups) {
|
||||||
|
groupIds.set(seedGroup.name, this.seedGroup(groupsPlugin, seedGroup).id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.seedRoleMappings(groupsPlugin, roleIds, groupIds);
|
||||||
|
this.logger.info(`Seeded ${roleIds.size} roles and ${groupIds.size} permission groups`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find or create the clusterio role for a seed role, returns undefined if it has no role to use. */
|
||||||
|
seedRole(seedRole: SeedRole) {
|
||||||
|
const roles = this.controller.roles;
|
||||||
|
if (seedRole.isAdmin) {
|
||||||
|
return roles.get(lib.Role.DefaultAdminRoleId);
|
||||||
|
}
|
||||||
|
if (seedRole.isDefault) {
|
||||||
|
const defaultRoleId = this.controller.config.get("controller.default_role_id");
|
||||||
|
return defaultRoleId !== null ? roles.get(defaultRoleId) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = flattenSeedPermissions(seedRole);
|
||||||
|
for (const permission of permissions) {
|
||||||
|
if (!lib.permissions.has(permission)) {
|
||||||
|
this.logger.warn(`Seed role ${seedRole.name} grants unknown permission ${permission}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let role = [...roles.valuesMutable()].find(other => other.name === seedRole.name);
|
||||||
|
if (role) {
|
||||||
|
for (const permission of permissions) {
|
||||||
|
role.permissions.add(permission);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const id = Math.max(5, ...[...roles.keys()].map(other => other + 1));
|
||||||
|
role = new lib.Role(id, seedRole.name, "", permissions);
|
||||||
|
this.logger.info(`Created role ${seedRole.name}`);
|
||||||
|
}
|
||||||
|
roles.set(role);
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find or create the permission group for a seed group. */
|
||||||
|
seedGroup(groupsPlugin: GroupsPlugin, seedGroup: SeedGroup) {
|
||||||
|
const permissions = new GroupPermissions(seedGroup.isBlacklist, [...seedGroup.inputActions]);
|
||||||
|
const existing = [...groupsPlugin.groups.values()].find(other => other.name === seedGroup.name);
|
||||||
|
const group = new GroupRecord(existing?.id ?? newId(groupsPlugin.groups), seedGroup.name, permissions);
|
||||||
|
if (!existing) {
|
||||||
|
this.logger.info(`Created permission group ${seedGroup.name}`);
|
||||||
|
}
|
||||||
|
groupsPlugin.groups.set(group);
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map each seed role onto its seed group.
|
||||||
|
*
|
||||||
|
* The mapping with the highest priority decides a player's group, so the
|
||||||
|
* priorities follow how exp_roles picks a player's highest role: role
|
||||||
|
* priority first, then the role order.
|
||||||
|
*/
|
||||||
|
seedRoleMappings(groupsPlugin: GroupsPlugin, roleIds: Map<string, number>, groupIds: Map<string, number>) {
|
||||||
|
// Lowest role first, so the mapping priority rises with the role
|
||||||
|
const ranked = seedRoles
|
||||||
|
.filter(seedRole => seedRole.group !== undefined && roleIds.has(seedRole.name))
|
||||||
|
.reverse()
|
||||||
|
.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
||||||
|
const seededRoleIds = new Set(ranked.map(seedRole => roleIds.get(seedRole.name)!));
|
||||||
|
|
||||||
|
// A mapping of a single seed role is reused, every other mapping keeps its priority
|
||||||
|
const existing = new Map<number, RoleMappingRecord>();
|
||||||
|
const taken = new Set<number>();
|
||||||
|
for (const mapping of groupsPlugin.roleMappings.values()) {
|
||||||
|
const [roleId] = mapping.roleIds;
|
||||||
|
if (mapping.roleIds.size === 1 && seededRoleIds.has(roleId)) {
|
||||||
|
existing.set(roleId, mapping);
|
||||||
|
} else {
|
||||||
|
taken.add(mapping.priority);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappings = [];
|
||||||
|
let priority = 0;
|
||||||
|
for (const seedRole of ranked) {
|
||||||
|
priority += 1;
|
||||||
|
while (taken.has(priority)) {
|
||||||
|
priority += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleId = roleIds.get(seedRole.name)!;
|
||||||
|
mappings.push(new RoleMappingRecord(
|
||||||
|
existing.get(roleId)?.id ?? newId(groupsPlugin.roleMappings),
|
||||||
|
new Set([roleId]),
|
||||||
|
groupIds.get(seedRole.group!)!,
|
||||||
|
priority,
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
groupsPlugin.roleMappings.setMany(mappings);
|
||||||
|
return mappings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function newId(datastore: { has(id: number): boolean }) {
|
||||||
|
let id = Math.random() * 2 ** 31 | 0;
|
||||||
|
while (datastore.has(id)) {
|
||||||
|
id = Math.random() * 2 ** 31 | 0;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as lib from "@clusterio/lib";
|
import * as lib from "@clusterio/lib";
|
||||||
// import * as Messages from "./messages";
|
import * as messages from "./messages";
|
||||||
|
|
||||||
// Defines a permission for every in game action and role flag used by the scenario
|
// Defines a permission for every in game action and role flag used by the scenario
|
||||||
import "./permissions";
|
import "./permissions";
|
||||||
@@ -16,6 +16,12 @@ lib.definePermission({
|
|||||||
description: "Edit the config for all submodules of ExpScenario",
|
description: "Edit the config for all submodules of ExpScenario",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
lib.definePermission({
|
||||||
|
name: "exp_scenario.seed",
|
||||||
|
title: "Seed ExpScenario roles and groups",
|
||||||
|
description: "Create the roles and permission groups the scenario shipped with",
|
||||||
|
});
|
||||||
|
|
||||||
declare module "@clusterio/lib" {
|
declare module "@clusterio/lib" {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -27,13 +33,9 @@ export const plugin: lib.PluginDeclaration = {
|
|||||||
controllerEntrypoint: "./dist/node/controller",
|
controllerEntrypoint: "./dist/node/controller",
|
||||||
instanceEntrypoint: "./dist/node/instance",
|
instanceEntrypoint: "./dist/node/instance",
|
||||||
|
|
||||||
/*
|
|
||||||
messages: [
|
messages: [
|
||||||
|
messages.SeedRequest,
|
||||||
],
|
],
|
||||||
|
|
||||||
webEntrypoint: "./web",
|
webEntrypoint: "./web",
|
||||||
routes: [
|
|
||||||
"/exp_scenario",
|
|
||||||
],
|
|
||||||
*/
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,95 +1,11 @@
|
|||||||
import { plainJson, jsonArray, JsonBoolean, JsonNumber, JsonString, StringEnum } from "@clusterio/lib";
|
/** Create the roles and permission groups the scenario shipped with, see seed.ts. */
|
||||||
import { Type, Static } from "@sinclair/typebox";
|
export class SeedRequest {
|
||||||
|
declare ["constructor"]: typeof SeedRequest;
|
||||||
export class PluginExampleEvent {
|
|
||||||
declare ["constructor"]: typeof PluginExampleEvent;
|
|
||||||
static type = "event" as const;
|
|
||||||
static src = ["host", "control"] as const;
|
|
||||||
static dst = ["controller", "host", "instance"] as const;
|
|
||||||
static plugin = "exp_scenario" as const;
|
static plugin = "exp_scenario" as const;
|
||||||
static permission = "exp_scenario.example.permission.event";
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public myString: string,
|
|
||||||
public myNumberArray: number[],
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
static jsonSchema = Type.Object({
|
|
||||||
"myString": Type.String(),
|
|
||||||
"myNumberArray": Type.Array(Type.Number()),
|
|
||||||
});
|
|
||||||
|
|
||||||
static fromJSON(json: Static<typeof PluginExampleEvent.jsonSchema>) {
|
|
||||||
return new PluginExampleEvent(json.myString, json.myNumberArray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PluginExampleRequest {
|
|
||||||
declare ["constructor"]: typeof PluginExampleRequest;
|
|
||||||
static type = "request" as const;
|
static type = "request" as const;
|
||||||
static src = ["host", "control"] as const;
|
static src = "control" as const;
|
||||||
static dst = ["controller", "host", "instance"] as const;
|
static dst = "controller" as const;
|
||||||
static plugin = "exp_scenario" as const;
|
static permission = "exp_scenario.seed" as const;
|
||||||
static permission = "exp_scenario.example.permission.request";
|
|
||||||
|
|
||||||
constructor(
|
constructor() {}
|
||||||
public myString: string,
|
|
||||||
public myNumberArray: number[],
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
static jsonSchema = Type.Object({
|
|
||||||
"myString": Type.String(),
|
|
||||||
"myNumberArray": Type.Array(Type.Number()),
|
|
||||||
});
|
|
||||||
|
|
||||||
static fromJSON(json: Static<typeof PluginExampleRequest.jsonSchema>) {
|
|
||||||
return new PluginExampleRequest(json.myString, json.myNumberArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Response = plainJson(Type.Object({
|
|
||||||
"myResponseString": Type.String(),
|
|
||||||
"myResponseNumbers": Type.Array(Type.Number()),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExampleSubscribableValue {
|
|
||||||
constructor(
|
|
||||||
public id: string,
|
|
||||||
public updatedAtMs: number,
|
|
||||||
public isDeleted: boolean,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
static jsonSchema = Type.Object({
|
|
||||||
id: Type.String(),
|
|
||||||
updatedAtMs: Type.Number(),
|
|
||||||
isDeleted: Type.Boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
|
||||||
return new this(json.id, json.updatedAtMs, json.isDeleted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExampleSubscribableUpdate {
|
|
||||||
declare ["constructor"]: typeof ExampleSubscribableUpdate;
|
|
||||||
static type = "event" as const;
|
|
||||||
static src = "controller" as const;
|
|
||||||
static dst = "control" as const;
|
|
||||||
static plugin = "exp_scenario" as const;
|
|
||||||
static permission = "exp_scenario.example.permission.subscribe";
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public updates: ExampleSubscribableValue[],
|
|
||||||
) { }
|
|
||||||
|
|
||||||
static jsonSchema = Type.Object({
|
|
||||||
"updates": Type.Array(ExampleSubscribableValue.jsonSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
|
||||||
return new this(json.updates.map(update => ExampleSubscribableValue.fromJSON(update)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ local add_static, add_dynamic = Commands.add_rcon_static, Commands.add_rcon_dyna
|
|||||||
|
|
||||||
add_static("Gui", require("modules/exp_gui"))
|
add_static("Gui", require("modules/exp_gui"))
|
||||||
|
|
||||||
add_static("Group", require("modules.exp_legacy.expcore.permission_groups"))
|
|
||||||
add_static("Roles", require("modules/exp_roles"))
|
add_static("Roles", require("modules/exp_roles"))
|
||||||
add_static("Datastore", require("modules.exp_legacy.expcore.datastore"))
|
add_static("Datastore", require("modules.exp_legacy.expcore.datastore"))
|
||||||
add_static("External", require("modules.exp_legacy.expcore.external"))
|
add_static("External", require("modules.exp_legacy.expcore.external"))
|
||||||
|
|||||||
@@ -7,15 +7,19 @@
|
|||||||
"repository": "explosivegaming/ExpCluster",
|
"repository": "explosivegaming/ExpCluster",
|
||||||
"main": "dist/node/index.js",
|
"main": "dist/node/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "tsc --build && webpack-cli --env production"
|
"prepare": "tsc --build && webpack-cli --env production",
|
||||||
|
"test": "tap --disable-coverage --allow-empty-coverage test/*.test.js",
|
||||||
|
"coverage": "tap --coverage-report=text test/*.test.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
"@clusterio/controller": "workspace:^",
|
||||||
"@clusterio/lib": "workspace:^"
|
"@clusterio/lib": "workspace:^"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@clusterio/controller": "workspace:^",
|
||||||
"@clusterio/lib": "workspace:^",
|
"@clusterio/lib": "workspace:^",
|
||||||
"@clusterio/web_ui": "workspace:^",
|
"@clusterio/web_ui": "workspace:^",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
@@ -23,6 +27,7 @@
|
|||||||
"antd": "catalog:",
|
"antd": "catalog:",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"react-dom": "catalog:",
|
"react-dom": "catalog:",
|
||||||
|
"tap": "^21.1.0",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"webpack": "catalog:",
|
"webpack": "catalog:",
|
||||||
"webpack-cli": "catalog:",
|
"webpack-cli": "catalog:",
|
||||||
@@ -32,6 +37,8 @@
|
|||||||
"@expcluster/lib_commands": "workspace:^",
|
"@expcluster/lib_commands": "workspace:^",
|
||||||
"@expcluster/lib_util": "workspace:^",
|
"@expcluster/lib_util": "workspace:^",
|
||||||
"@expcluster/lib_gui": "workspace:^",
|
"@expcluster/lib_gui": "workspace:^",
|
||||||
|
"@expcluster/permission-groups": "workspace:^",
|
||||||
|
"@expcluster/roles": "workspace:^",
|
||||||
"@sinclair/typebox": "catalog:"
|
"@sinclair/typebox": "catalog:"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { RoleColor } from "./messages";
|
import { RoleColor } from "@expcluster/roles/dist/node/messages";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A role created by the seed, as the scenario defined it before roles moved to
|
* A role created by the seed, as the scenario defined it before roles moved to
|
||||||
@@ -20,6 +20,22 @@ export interface SeedRole {
|
|||||||
/** Name of the role whose permissions are also granted, applied recursively. */
|
/** Name of the role whose permissions are also granted, applied recursively. */
|
||||||
parent?: string;
|
parent?: string;
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
|
/**
|
||||||
|
* Name of the seed group holders are placed in by their highest role.
|
||||||
|
* Without one the holders stay in Factorio's Default group.
|
||||||
|
*/
|
||||||
|
group?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Factorio permission group created by the seed, as the scenario defined it
|
||||||
|
* before groups moved to the controller.
|
||||||
|
*/
|
||||||
|
export interface SeedGroup {
|
||||||
|
name: string;
|
||||||
|
/** When true the input actions are the only ones disallowed, otherwise the only ones allowed. */
|
||||||
|
isBlacklist: boolean;
|
||||||
|
inputActions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const seedRoles: SeedRole[] = [
|
export const seedRoles: SeedRole[] = [
|
||||||
@@ -32,6 +48,7 @@ export const seedRoles: SeedRole[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Senior Administrator",
|
name: "Senior Administrator",
|
||||||
|
group: "Admin",
|
||||||
shortHand: "SAdmin",
|
shortHand: "SAdmin",
|
||||||
color: new RoleColor(233, 63, 233),
|
color: new RoleColor(233, 63, 233),
|
||||||
parent: "Administrator",
|
parent: "Administrator",
|
||||||
@@ -44,6 +61,7 @@ export const seedRoles: SeedRole[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Administrator",
|
name: "Administrator",
|
||||||
|
group: "Admin",
|
||||||
shortHand: "Admin",
|
shortHand: "Admin",
|
||||||
color: new RoleColor(233, 63, 233),
|
color: new RoleColor(233, 63, 233),
|
||||||
parent: "Moderator",
|
parent: "Moderator",
|
||||||
@@ -55,6 +73,7 @@ export const seedRoles: SeedRole[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Moderator",
|
name: "Moderator",
|
||||||
|
group: "Admin",
|
||||||
shortHand: "Mod",
|
shortHand: "Mod",
|
||||||
color: new RoleColor(0, 170, 0),
|
color: new RoleColor(0, 170, 0),
|
||||||
parent: "Trainee",
|
parent: "Trainee",
|
||||||
@@ -89,6 +108,7 @@ export const seedRoles: SeedRole[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Trainee",
|
name: "Trainee",
|
||||||
|
group: "Admin",
|
||||||
shortHand: "TrMod",
|
shortHand: "TrMod",
|
||||||
color: new RoleColor(0, 170, 0),
|
color: new RoleColor(0, 170, 0),
|
||||||
parent: "Veteran",
|
parent: "Veteran",
|
||||||
@@ -120,6 +140,7 @@ export const seedRoles: SeedRole[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Board Member",
|
name: "Board Member",
|
||||||
|
group: "Trusted",
|
||||||
shortHand: "Board",
|
shortHand: "Board",
|
||||||
color: new RoleColor(247, 246, 54),
|
color: new RoleColor(247, 246, 54),
|
||||||
parent: "Sponsor",
|
parent: "Sponsor",
|
||||||
@@ -133,6 +154,7 @@ export const seedRoles: SeedRole[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Senior Backer",
|
name: "Senior Backer",
|
||||||
|
group: "Trusted",
|
||||||
shortHand: "Backer",
|
shortHand: "Backer",
|
||||||
color: new RoleColor(238, 172, 44),
|
color: new RoleColor(238, 172, 44),
|
||||||
parent: "Sponsor",
|
parent: "Sponsor",
|
||||||
@@ -140,6 +162,7 @@ export const seedRoles: SeedRole[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Sponsor",
|
name: "Sponsor",
|
||||||
|
group: "Trusted",
|
||||||
shortHand: "Spon",
|
shortHand: "Spon",
|
||||||
color: new RoleColor(238, 172, 44),
|
color: new RoleColor(238, 172, 44),
|
||||||
parent: "Supporter",
|
parent: "Supporter",
|
||||||
@@ -158,6 +181,7 @@ export const seedRoles: SeedRole[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Supporter",
|
name: "Supporter",
|
||||||
|
group: "Trusted",
|
||||||
shortHand: "Sup",
|
shortHand: "Sup",
|
||||||
color: new RoleColor(230, 99, 34),
|
color: new RoleColor(230, 99, 34),
|
||||||
parent: "Veteran",
|
parent: "Veteran",
|
||||||
@@ -172,6 +196,7 @@ export const seedRoles: SeedRole[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Partner",
|
name: "Partner",
|
||||||
|
group: "Trusted",
|
||||||
shortHand: "Part",
|
shortHand: "Part",
|
||||||
color: new RoleColor(140, 120, 200),
|
color: new RoleColor(140, 120, 200),
|
||||||
parent: "Veteran",
|
parent: "Veteran",
|
||||||
@@ -183,6 +208,7 @@ export const seedRoles: SeedRole[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Veteran",
|
name: "Veteran",
|
||||||
|
group: "Trusted",
|
||||||
shortHand: "Vet",
|
shortHand: "Vet",
|
||||||
color: new RoleColor(140, 120, 200),
|
color: new RoleColor(140, 120, 200),
|
||||||
parent: "Member",
|
parent: "Member",
|
||||||
@@ -196,6 +222,7 @@ export const seedRoles: SeedRole[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Member",
|
name: "Member",
|
||||||
|
group: "Standard",
|
||||||
shortHand: "Mem",
|
shortHand: "Mem",
|
||||||
color: new RoleColor(24, 172, 188),
|
color: new RoleColor(24, 172, 188),
|
||||||
parent: "Regular",
|
parent: "Regular",
|
||||||
@@ -217,6 +244,7 @@ export const seedRoles: SeedRole[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Regular",
|
name: "Regular",
|
||||||
|
group: "Standard",
|
||||||
shortHand: "Reg",
|
shortHand: "Reg",
|
||||||
color: new RoleColor(79, 155, 163),
|
color: new RoleColor(79, 155, 163),
|
||||||
autoAssignHours: 3,
|
autoAssignHours: 3,
|
||||||
@@ -232,6 +260,7 @@ export const seedRoles: SeedRole[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Jail",
|
name: "Jail",
|
||||||
|
group: "Restricted",
|
||||||
shortHand: "Jail",
|
shortHand: "Jail",
|
||||||
color: new RoleColor(50, 50, 50),
|
color: new RoleColor(50, 50, 50),
|
||||||
priority: 1,
|
priority: 1,
|
||||||
@@ -240,6 +269,7 @@ export const seedRoles: SeedRole[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Guest",
|
name: "Guest",
|
||||||
|
group: "Guest",
|
||||||
shortHand: "",
|
shortHand: "",
|
||||||
color: new RoleColor(185, 187, 160),
|
color: new RoleColor(185, 187, 160),
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
@@ -247,6 +277,56 @@ export const seedRoles: SeedRole[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const adminDisallowed = [
|
||||||
|
"add_permission_group",
|
||||||
|
"delete_permission_group",
|
||||||
|
"edit_permission_group",
|
||||||
|
"import_permissions_string",
|
||||||
|
"map_editor_action",
|
||||||
|
"toggle_map_editor",
|
||||||
|
"change_multiplayer_config",
|
||||||
|
"set_heat_interface_mode",
|
||||||
|
"set_heat_interface_temperature",
|
||||||
|
"set_infinity_container_filter_item",
|
||||||
|
"set_infinity_container_remove_unfiltered_items",
|
||||||
|
"set_infinity_pipe_filter",
|
||||||
|
];
|
||||||
|
|
||||||
|
const trustedDisallowed = [
|
||||||
|
...adminDisallowed,
|
||||||
|
"admin_action",
|
||||||
|
];
|
||||||
|
|
||||||
|
const standardDisallowed = [
|
||||||
|
...trustedDisallowed,
|
||||||
|
"change_programmable_speaker_alert_parameters",
|
||||||
|
"drop_item",
|
||||||
|
"open_new_platform_button_from_rocket_silo",
|
||||||
|
"set_rocket_silo_send_to_orbit_automated_mode",
|
||||||
|
];
|
||||||
|
|
||||||
|
const guestDisallowed = [
|
||||||
|
...standardDisallowed,
|
||||||
|
"change_programmable_speaker_parameters",
|
||||||
|
"change_train_stop_station",
|
||||||
|
"remove_cables",
|
||||||
|
"remove_train_station",
|
||||||
|
"reset_assembling_machine",
|
||||||
|
"rotate_entity",
|
||||||
|
"launch_rocket",
|
||||||
|
"cancel_research",
|
||||||
|
"flush_opened_entity_fluid",
|
||||||
|
"flush_opened_entity_specific_fluid",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const seedGroups: SeedGroup[] = [
|
||||||
|
{ name: "Admin", isBlacklist: true, inputActions: adminDisallowed },
|
||||||
|
{ name: "Trusted", isBlacklist: true, inputActions: trustedDisallowed },
|
||||||
|
{ name: "Standard", isBlacklist: true, inputActions: standardDisallowed },
|
||||||
|
{ name: "Guest", isBlacklist: true, inputActions: guestDisallowed },
|
||||||
|
{ name: "Restricted", isBlacklist: false, inputActions: ["write_to_console"] },
|
||||||
|
];
|
||||||
|
|
||||||
/** The permissions a seed role grants, including those of its parents. */
|
/** The permissions a seed role grants, including those of its parents. */
|
||||||
export function flattenSeedPermissions(role: SeedRole, roles = seedRoles) {
|
export function flattenSeedPermissions(role: SeedRole, roles = seedRoles) {
|
||||||
const permissions = new Set<string>();
|
const permissions = new Set<string>();
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
"use strict";
|
||||||
|
const t = require("tap");
|
||||||
|
const lib = require("@clusterio/lib");
|
||||||
|
const { Controller } = require("@clusterio/controller");
|
||||||
|
const { ControllerPlugin } = require("../dist/node/controller");
|
||||||
|
const messages = require("../dist/node/messages");
|
||||||
|
const { seedRoles, seedGroups } = require("../dist/node/seed");
|
||||||
|
const roles = require("@expcluster/roles/dist/node");
|
||||||
|
const groups = require("@expcluster/permission-groups/dist/node");
|
||||||
|
const { ControllerPlugin: RolesPlugin } = require("@expcluster/roles/dist/node/controller");
|
||||||
|
const { ControllerPlugin: GroupsPlugin } = require("@expcluster/permission-groups/dist/node/controller");
|
||||||
|
const { GroupRecord, GroupPermissions, RoleMappingRecord } = require("@expcluster/permission-groups/dist/node/messages");
|
||||||
|
|
||||||
|
// Importing this defines the permissions the seed grants
|
||||||
|
require("../dist/node/permissions");
|
||||||
|
|
||||||
|
// The controller validates message classes against the link registry
|
||||||
|
for (const Message of [messages.SeedRequest, ...roles.plugin.messages, ...groups.plugin.messages]) {
|
||||||
|
lib.Link.register(Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = { child: () => logger, info: () => {}, warn: () => {}, error: () => {}, verbose: () => {} };
|
||||||
|
|
||||||
|
// Silence the singleton logger to avoid polluting the test output
|
||||||
|
lib.logger.silent = true;
|
||||||
|
t.after(() => {
|
||||||
|
lib.logger.silent = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Build the plugin around a real controller, which is side effect free while not started. */
|
||||||
|
async function startPlugin(t2, { withPlugins = true } = {}) {
|
||||||
|
const controllerConfig = new lib.ControllerConfig("controller", {
|
||||||
|
"controller.database_directory": t2.testdir(),
|
||||||
|
"controller.default_role_id": lib.Role.DefaultPlayerRoleId,
|
||||||
|
});
|
||||||
|
const controller = new Controller(logger, [], controllerConfig);
|
||||||
|
controller.roles.set(new lib.Role(0, "Cluster Admin", "", new Set(["core.admin"])));
|
||||||
|
controller.roles.set(new lib.Role(1, "Player", "", new Set()));
|
||||||
|
|
||||||
|
if (withPlugins) {
|
||||||
|
for (const [name, Plugin] of [["exp_roles", RolesPlugin], ["exp_groups", GroupsPlugin]]) {
|
||||||
|
const plugin = new Plugin({ name }, controller, undefined, logger);
|
||||||
|
await plugin.init();
|
||||||
|
controller.plugins.set(name, plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin = new ControllerPlugin({ name: "exp_scenario" }, controller, undefined, logger);
|
||||||
|
await plugin.init();
|
||||||
|
return { plugin, controller };
|
||||||
|
}
|
||||||
|
|
||||||
|
const byName = (datastore, name) => [...datastore.values()].find(other => other.name === name);
|
||||||
|
|
||||||
|
t.test("class ControllerPlugin", t2 => {
|
||||||
|
t2.test(".handleSeedRequest() throws when the seeded plugins are missing", async t3 => {
|
||||||
|
const { plugin } = await startPlugin(t3, { withPlugins: false });
|
||||||
|
await t3.rejects(
|
||||||
|
plugin.handleSeedRequest(),
|
||||||
|
{ message: "Seeding requires the exp_roles and exp_groups plugins" },
|
||||||
|
"the request is rejected",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
t2.test(".handleSeedRequest() creates the roles and reuses them by name", async t3 => {
|
||||||
|
const { plugin, controller } = await startPlugin(t3);
|
||||||
|
const rolesPlugin = controller.plugins.get("exp_roles");
|
||||||
|
|
||||||
|
await plugin.handleSeedRequest();
|
||||||
|
t3.strictSame(controller.roles.size, seedRoles.length, "every seed role exists");
|
||||||
|
|
||||||
|
const moderator = byName(controller.roles, "Moderator");
|
||||||
|
t3.ok(moderator.permissions.has("exp_scenario.command.jail"), "parent permissions are flattened in");
|
||||||
|
t3.strictSame(rolesPlugin.roleMeta.get(moderator.id).shortHand, "Mod", "the role properties match the seed");
|
||||||
|
|
||||||
|
await plugin.handleSeedRequest();
|
||||||
|
t3.strictSame(controller.roles.size, seedRoles.length, "seeding again reuses the roles");
|
||||||
|
});
|
||||||
|
|
||||||
|
t2.test(".handleSeedRequest() creates the groups and resets them by name", async t3 => {
|
||||||
|
const { plugin, controller } = await startPlugin(t3);
|
||||||
|
const groupsPlugin = controller.plugins.get("exp_groups");
|
||||||
|
|
||||||
|
await plugin.handleSeedRequest();
|
||||||
|
t3.strictSame(groupsPlugin.groups.size, seedGroups.length, "every seed group exists");
|
||||||
|
|
||||||
|
const restricted = byName(groupsPlugin.groups, "Restricted");
|
||||||
|
t3.strictSame(restricted.permissions.isBlacklist, false, "the group only allows the listed actions");
|
||||||
|
t3.strictSame(restricted.permissions.permissions, ["write_to_console"], "the actions match the seed");
|
||||||
|
|
||||||
|
const admin = byName(groupsPlugin.groups, "Admin");
|
||||||
|
groupsPlugin.groups.set(new GroupRecord(admin.id, admin.name, new GroupPermissions(true, [])));
|
||||||
|
await plugin.handleSeedRequest();
|
||||||
|
t3.strictSame(groupsPlugin.groups.size, seedGroups.length, "seeding again reuses the groups");
|
||||||
|
t3.ok(
|
||||||
|
byName(groupsPlugin.groups, "Admin").permissions.permissions.includes("toggle_map_editor"),
|
||||||
|
"seeding again resets the actions",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
t2.test(".handleSeedRequest() maps each role onto its group by rank", async t3 => {
|
||||||
|
const { plugin, controller } = await startPlugin(t3);
|
||||||
|
const groupsPlugin = controller.plugins.get("exp_groups");
|
||||||
|
|
||||||
|
await plugin.handleSeedRequest();
|
||||||
|
const mapped = seedRoles.filter(role => role.group !== undefined);
|
||||||
|
t3.strictSame(groupsPlugin.roleMappings.size, mapped.length, "every role with a group is mapped");
|
||||||
|
|
||||||
|
const mappingOf = name => [...groupsPlugin.roleMappings.values()]
|
||||||
|
.find(mapping => mapping.roleIds.has(byName(controller.roles, name).id));
|
||||||
|
const groupOf = name => groupsPlugin.groups.get(mappingOf(name).groupId).name;
|
||||||
|
t3.strictSame(groupOf("Player"), "Guest", "the default role maps to the guest group");
|
||||||
|
t3.strictSame(groupOf("Jail"), "Restricted", "jail maps to the restricted group");
|
||||||
|
t3.ok(mappingOf("Jail").priority > mappingOf("Senior Administrator").priority, "jail outranks every role");
|
||||||
|
t3.ok(mappingOf("Moderator").priority > mappingOf("Veteran").priority, "the role order sets the rank");
|
||||||
|
t3.ok(mappingOf("Veteran").priority > mappingOf("Player").priority, "the default role ranks lowest");
|
||||||
|
t3.strictSame(mappingOf("Cluster Admin"), undefined, "the admin role keeps the factorio default group");
|
||||||
|
|
||||||
|
const priorities = [...groupsPlugin.roleMappings.values()].map(mapping => mapping.priority);
|
||||||
|
t3.strictSame(priorities.length, new Set(priorities).size, "priorities are unique");
|
||||||
|
});
|
||||||
|
|
||||||
|
t2.test(".handleSeedRequest() reuses mappings and keeps clear of other mappings", async t3 => {
|
||||||
|
const { plugin, controller } = await startPlugin(t3);
|
||||||
|
const groupsPlugin = controller.plugins.get("exp_groups");
|
||||||
|
|
||||||
|
groupsPlugin.roleMappings.set(new RoleMappingRecord(99, new Set([0, 1]), 5, 2, true));
|
||||||
|
await plugin.handleSeedRequest();
|
||||||
|
const first = new Map([...groupsPlugin.roleMappings.values()].map(mapping => [mapping.id, mapping.priority]));
|
||||||
|
|
||||||
|
await plugin.handleSeedRequest();
|
||||||
|
const second = new Map([...groupsPlugin.roleMappings.values()].map(mapping => [mapping.id, mapping.priority]));
|
||||||
|
t3.strictSame(second, first, "seeding again reuses the mappings");
|
||||||
|
t3.strictSame(groupsPlugin.roleMappings.get(99).priority, 2, "other mappings are left alone");
|
||||||
|
t3.notOk([...first.values()].filter(priority => priority === 2).length > 1, "seed priorities skip taken ones");
|
||||||
|
});
|
||||||
|
|
||||||
|
t2.end();
|
||||||
|
});
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
const t = require("tap");
|
const t = require("tap");
|
||||||
const lib = require("@clusterio/lib");
|
const lib = require("@clusterio/lib");
|
||||||
const { seedRoles, flattenSeedPermissions } = require("../dist/node/seed");
|
const { seedRoles, seedGroups, flattenSeedPermissions } = require("../dist/node/seed");
|
||||||
|
|
||||||
// Importing this defines the exp_scenario permissions the seed grants
|
// Importing this defines the permissions the seed grants
|
||||||
require("@expcluster/scenario/dist/node/permissions");
|
require("../dist/node/permissions");
|
||||||
|
|
||||||
t.test("seedRoles[] grant only defined permissions", t2 => {
|
t.test("seedRoles[] grant only defined permissions", t2 => {
|
||||||
for (const role of seedRoles) {
|
for (const role of seedRoles) {
|
||||||
@@ -23,6 +23,29 @@ t.test("seedRoles[] are unique with one default and one admin", t2 => {
|
|||||||
t2.end();
|
t2.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
t.test("seedRoles[] are placed in defined groups", t2 => {
|
||||||
|
const groupNames = new Set(seedGroups.map(group => group.name));
|
||||||
|
for (const role of seedRoles) {
|
||||||
|
if (role.group !== undefined) {
|
||||||
|
t2.ok(groupNames.has(role.group), `${role.name} is placed in defined group ${role.group}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t2.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
t.test("seedGroups[] are unique", t2 => {
|
||||||
|
const names = seedGroups.map(group => group.name);
|
||||||
|
t2.strictSame(names.length, new Set(names).size, "names are unique");
|
||||||
|
for (const group of seedGroups) {
|
||||||
|
t2.strictSame(
|
||||||
|
group.inputActions.length,
|
||||||
|
new Set(group.inputActions).size,
|
||||||
|
`${group.name} lists each input action once`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
t2.end();
|
||||||
|
});
|
||||||
|
|
||||||
t.test("flattenSeedPermissions() inherits permissions from parent roles", t2 => {
|
t.test("flattenSeedPermissions() inherits permissions from parent roles", t2 => {
|
||||||
const byName = new Map(seedRoles.map(role => [role.name, role]));
|
const byName = new Map(seedRoles.map(role => [role.name, role]));
|
||||||
for (const role of seedRoles) {
|
for (const role of seedRoles) {
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React, { useContext, useState } from "react";
|
||||||
|
import { Button, Popconfirm } from "antd";
|
||||||
|
|
||||||
|
import { ControlContext, SectionHeader, useAccount, notifyErrorHandler } from "@clusterio/web_ui";
|
||||||
|
|
||||||
|
import { SeedRequest } from "../../messages";
|
||||||
|
|
||||||
|
/** Button on the roles page which creates the roles and permission groups the scenario shipped with. */
|
||||||
|
export default function Seed() {
|
||||||
|
const control = useContext(ControlContext);
|
||||||
|
const account = useAccount();
|
||||||
|
const [seeding, setSeeding] = useState(false);
|
||||||
|
|
||||||
|
if (!account.hasPermission("exp_scenario.seed")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SectionHeader
|
||||||
|
title="ExpGaming Roles"
|
||||||
|
extra={<Popconfirm
|
||||||
|
title="Create the ExpGaming roles and permission groups?"
|
||||||
|
description="Roles which already exist by name only gain permissions, groups are reset to the defaults."
|
||||||
|
onConfirm={() => {
|
||||||
|
setSeeding(true);
|
||||||
|
control.send(new SeedRequest())
|
||||||
|
.catch(notifyErrorHandler("Error seeding roles and groups"))
|
||||||
|
.finally(() => setSeeding(false));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button loading={seeding}>Seed roles and groups</Button>
|
||||||
|
</Popconfirm>}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { BaseWebPlugin } from "@clusterio/web_ui";
|
||||||
|
|
||||||
|
import Seed from "./components/Seed";
|
||||||
|
|
||||||
|
export class WebPlugin extends BaseWebPlugin {
|
||||||
|
async init() {
|
||||||
|
this.componentExtra = {
|
||||||
|
RolesPage: Seed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user