This commit is contained in:
2026-09-03 17:07:07 +09:00
82 changed files with 3021 additions and 2116 deletions
+1
View File
@@ -2,3 +2,4 @@ dist/
node_modules/ node_modules/
.vscode .vscode
.luarc.json .luarc.json
.tap/
@@ -22,10 +22,8 @@ return {
--- GUI --- GUI
"modules.gui.warp-list", "modules.gui.warp-list",
"modules.gui.vlayer", "modules.gui.vlayer",
"modules.gui._role_updates",
"modules.graftorio.require", -- graftorio "modules.graftorio.require", -- graftorio
--- Config Files --- Config Files
"config.expcore.permission_groups", -- loads some predefined permission groups "config.expcore.permission_groups", -- loads some predefined permission groups
"config.expcore.roles", -- loads some predefined roles
} }
+3 -2
View File
@@ -1,4 +1,4 @@
local Roles = require("modules.exp_legacy.expcore.roles") local Roles = require("modules/exp_roles")
return { return {
admin_as_active = true, --- @setting admin_as_active When true admins will be treated as active regardless of afk time admin_as_active = true, --- @setting admin_as_active When true admins will be treated as active regardless of afk time
@@ -8,6 +8,7 @@ return {
trust_time = 3600 * 60 * 10, --- @setting trust_time The time in ticks that a player must be online for to count as trusted trust_time = 3600 * 60 * 10, --- @setting trust_time The time in ticks that a player must be online for to count as trusted
update_time = 3600 * 30, --- @setting update_time How often in ticks the script checks for active players update_time = 3600 * 30, --- @setting update_time How often in ticks the script checks for active players
custom_active_check = function(player) custom_active_check = function(player)
return Roles.get_player_highest_role(player).index <= Roles.get_role_from_any("Veteran").index local veteran = Roles.get_role_by_name("Veteran")
return veteran ~= nil and not Roles.get_player_highest_role(player):is_lower_than(veteran)
end, end,
} }
+1 -1
View File
@@ -65,7 +65,7 @@ return {
}, },
allow_command_prefix_for_messages = true, --- @setting allow_command_prefix_for_messages when true any message trigger will print to all player when prefixed allow_command_prefix_for_messages = true, --- @setting allow_command_prefix_for_messages when true any message trigger will print to all player when prefixed
command_admin_only = false, --- @setting command_admin_only when true will only allow chat commands for admins command_admin_only = false, --- @setting command_admin_only when true will only allow chat commands for admins
command_permission = "command/chat-commands", --- @setting command_permission the permission used to allow command prefixes command_permission = "exp_scenario.chat.commands", --- @setting command_permission the permission used to allow command prefixes
command_prefix = "!", --- @setting command_prefix prefix used for commands below and to print to all players (if enabled above) command_prefix = "!", --- @setting command_prefix prefix used for commands below and to print to all players (if enabled above)
--- @type table<string, LocalisedString | fun(player: LuaPlayer, is_command: boolean): LocalisedString> --- @type table<string, LocalisedString | fun(player: LuaPlayer, is_command: boolean): LocalisedString>
commands = { --- @setting commands will trigger only when command prefix is given commands = { --- @setting commands will trigger only when command prefix is given
@@ -7,7 +7,7 @@
local ExpUtil = require("modules/exp_util") local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles local Roles = require("modules/exp_roles")
local Reports = require("modules.exp_legacy.modules.control.reports") --- @dep modules.control.reports local Reports = require("modules.exp_legacy.modules.control.reports") --- @dep modules.control.reports
local Warnings = require("modules.exp_legacy.modules.control.warnings") --- @dep modules.control.warnings local Warnings = require("modules.exp_legacy.modules.control.warnings") --- @dep modules.control.warnings
local Jail = require("modules.exp_legacy.modules.control.jail") --- @dep modules.control.jail local Jail = require("modules.exp_legacy.modules.control.jail") --- @dep modules.control.jail
@@ -21,15 +21,6 @@ local function set_accessors(player_getter, action_setter)
get_selected_player, set_selected_action = player_getter, action_setter get_selected_player, set_selected_action = player_getter, action_setter
end end
-- auth that will only allow when on player's of lower roles
local function auth_lower_role(player, selected_player_name)
local player_highest = Roles.get_player_highest_role(player)
local action_player_highest = Roles.get_player_highest_role(selected_player_name)
if player_highest.index < action_player_highest.index then
return true
end
end
-- gets the action player and a coloured name for the action to be used on -- gets the action player and a coloured name for the action to be used on
local function get_action_player(player) local function get_action_player(player)
local selected_player = get_selected_player(player) --[[@as LuaPlayer]] local selected_player = get_selected_player(player) --[[@as LuaPlayer]]
@@ -94,7 +85,7 @@ local report_player = new_button("utility/spawn_flag", { "exp-gui_player-list.re
if Reports.is_reported(selected_player.name, player.name) then if Reports.is_reported(selected_player.name, player.name) then
player.print({ "exp-commands_report.already-reported" }, Colors.orange_red) player.print({ "exp-commands_report.already-reported" }, Colors.orange_red)
else else
set_selected_action(player, "command/report") set_selected_action(player, "exp_scenario.command.create_report")
end end
end) end)
@@ -102,7 +93,10 @@ local function report_player_callback(player, reason)
local selected_player, selected_player_color = get_action_player(player) local selected_player, selected_player_color = get_action_player(player)
local by_player_name_color = format_player_name(player) local by_player_name_color = format_player_name(player)
game.print{ "exp-commands_reports.response", selected_player_color, reason } game.print{ "exp-commands_reports.response", selected_player_color, reason }
Roles.print_to_roles_higher("Trainee", { "exp-commands_reports.response-admin", selected_player_color, by_player_name_color, reason }) local trainee = Roles.get_role_by_name("Trainee")
for _, role in ipairs(trainee and Roles.get_higher_roles(trainee) or {}) do
role:print{ "exp-commands_reports.response-admin", selected_player_color, by_player_name_color, reason }
end
Reports.report_player(selected_player.name, player.name, reason) Reports.report_player(selected_player.name, player.name, reason)
end end
@@ -110,7 +104,7 @@ end
-- @element warn_player -- @element warn_player
local warn_player = new_button("utility/spawn_flag", { "exp-gui_player-list.warn-player" }) local warn_player = new_button("utility/spawn_flag", { "exp-gui_player-list.warn-player" })
:on_click(function(def, player, element) :on_click(function(def, player, element)
set_selected_action(player, "command/give-warning") set_selected_action(player, "exp_scenario.command.create_warning")
end) end)
local function warn_player_callback(player, reason) local function warn_player_callback(player, reason)
@@ -128,7 +122,7 @@ local jail_player = new_button("utility/multiplayer_waiting_icon", { "exp-gui_pl
if Jail.is_jailed(selected_player.name) then if Jail.is_jailed(selected_player.name) then
player.print({ "exp-commands_jail.already-jailed", selected_player_color }, Colors.orange_red) player.print({ "exp-commands_jail.already-jailed", selected_player_color }, Colors.orange_red)
else else
set_selected_action(player, "command/jail") set_selected_action(player, "exp_scenario.command.jail")
end end
end) end)
@@ -143,7 +137,7 @@ end
-- @element kick_player -- @element kick_player
local kick_player = new_button("utility/warning_icon", { "exp-gui_player-list.kick-player" }) local kick_player = new_button("utility/warning_icon", { "exp-gui_player-list.kick-player" })
:on_click(function(def, player, element) :on_click(function(def, player, element)
set_selected_action(player, "command/kick") set_selected_action(player, "exp_scenario.gui.player_list.kick")
end) end)
local function kick_player_callback(player, reason) local function kick_player_callback(player, reason)
@@ -155,7 +149,7 @@ end
-- @element ban_player -- @element ban_player
local ban_player = new_button("utility/danger_icon", { "exp-gui_player-list.ban-player" }) local ban_player = new_button("utility/danger_icon", { "exp-gui_player-list.ban-player" })
:on_click(function(def, player, element) :on_click(function(def, player, element)
set_selected_action(player, "command/ban") set_selected_action(player, "exp_scenario.gui.player_list.ban")
end) end)
local function ban_player_callback(player, reason) local function ban_player_callback(player, reason)
@@ -166,40 +160,40 @@ end
return { return {
set_accessors = set_accessors, set_accessors = set_accessors,
buttons = { buttons = {
["command/teleport"] = { ["exp_scenario.command.teleport"] = {
auth = function(player, selected_player) auth = function(player, selected_player)
return player.name ~= selected_player.name return player.name ~= selected_player.name
end, -- cant teleport to your self end, -- cant teleport to your self
goto_player, goto_player,
bring_player, bring_player,
}, },
["command/report"] = { ["exp_scenario.command.create_report"] = {
auth = function(player, selected_player) auth = function(player, selected_player)
if player == selected_player then return false end if player == selected_player then return false end
if not Roles.player_allowed(player, "command/give-warning") then if not Roles.player_has_permission(player, "exp_scenario.command.create_warning") then
return not Roles.player_has_flag(selected_player, "report-immune") return not Roles.player_has_permission(selected_player, "exp_scenario.bypass.reports")
end end
end, -- can report any player that isn't immune and you aren't able to give warnings end, -- can report any player that isn't immune and you aren't able to give warnings
reason_callback = report_player_callback, reason_callback = report_player_callback,
report_player, report_player,
}, },
["command/give-warning"] = { ["exp_scenario.command.create_warning"] = {
auth = auth_lower_role, -- warn a lower user, replaces report auth = Roles.player_outranks, -- warn a lower user, replaces report
reason_callback = warn_player_callback, reason_callback = warn_player_callback,
warn_player, warn_player,
}, },
["command/jail"] = { ["exp_scenario.command.jail"] = {
auth = auth_lower_role, auth = Roles.player_outranks,
reason_callback = jail_player_callback, reason_callback = jail_player_callback,
jail_player, jail_player,
}, },
["command/kick"] = { ["exp_scenario.gui.player_list.kick"] = {
auth = auth_lower_role, auth = Roles.player_outranks,
reason_callback = kick_player_callback, reason_callback = kick_player_callback,
kick_player, kick_player,
}, },
["command/ban"] = { ["exp_scenario.gui.player_list.ban"] = {
auth = auth_lower_role, auth = Roles.player_outranks,
reason_callback = ban_player_callback, reason_callback = ban_player_callback,
ban_player, ban_player,
}, },
+2 -2
View File
@@ -39,9 +39,9 @@ return {
allow_zoom_to_map = true, --- @setting allow_zoom_to_map false will disable the zoom to map feature allow_zoom_to_map = true, --- @setting allow_zoom_to_map false will disable the zoom to map feature
allow_remote_launch = true, --- @setting allow_remote_launch false removes the remote launch button for all players allow_remote_launch = true, --- @setting allow_remote_launch false removes the remote launch button for all players
remote_launch_admins_only = false, --- @setting remote_launch_admins_only true will remove the remote launch button for all non (game) admins remote_launch_admins_only = false, --- @setting remote_launch_admins_only true will remove the remote launch button for all non (game) admins
remote_launch_role_permission = "gui/rocket-info/remote_launch", --- @setting remote_launch_role_permission value used by custom permission system to allow or disallow the button remote_launch_role_permission = "exp_scenario.gui.rocket_info.remote_launch", --- @setting remote_launch_role_permission value used by custom permission system to allow or disallow the button
allow_toggle_active = true, --- @setting allow_toggle_active false removes the remote toggle auto launch button for all players allow_toggle_active = true, --- @setting allow_toggle_active false removes the remote toggle auto launch button for all players
toggle_active_admins_only = false, --- @setting toggle_active_admins_only true will remove the toggle auto launch button for all non (game) admins toggle_active_admins_only = false, --- @setting toggle_active_admins_only true will remove the toggle auto launch button for all non (game) admins
toggle_active_role_permission = "gui/rocket-info/toggle-active", --- @setting toggle_active_role_permission value used by custom permission system to allow or disallow the button toggle_active_role_permission = "exp_scenario.gui.rocket_info.toggle_active", --- @setting toggle_active_role_permission value used by custom permission system to allow or disallow the button
}, },
} }
+4 -4
View File
@@ -3,11 +3,11 @@
return { return {
-- Adding tasks -- Adding tasks
allow_add_task = "all", --- @setting allow_add_task dictates who is allowed to add new tasks; values: all, admin, expcore.roles, none allow_add_task = "all", --- @setting allow_add_task dictates who is allowed to add new tasks; values: all, admin, exp_roles, none
expcore_roles_allow_add_task = "gui/task-list/add", --- @setting expcore_roles_allow_add_task if expcore.roles is used then this is the required permission exp_roles_allow_add_task = "exp_scenario.gui.task_list.add", --- @setting exp_roles_allow_add_task if exp_roles is used then this is the required permission
-- Editing tasks -- Editing tasks
allow_edit_task = "expcore.roles", --- @setting allow_edit_task dictates who is allowed to edit existing tasks; values: all, admin, expcore.roles, none allow_edit_task = "exp_roles", --- @setting allow_edit_task dictates who is allowed to edit existing tasks; values: all, admin, exp_roles, none
expcore_roles_allow_edit_task = "gui/task-list/edit", --- @setting expcore_roles_allow_edit_task if expcore.roles is used then this is the required permission exp_roles_allow_edit_task = "exp_scenario.gui.task_list.edit", --- @setting exp_roles_allow_edit_task if exp_roles is used then this is the required permission
user_can_edit_own_tasks = true, --- @settings if true then the user who made the task can edit it regardless of the allow_edit_task setting user_can_edit_own_tasks = true, --- @settings if true then the user who made the task can edit it regardless of the allow_edit_task setting
} }
+8 -8
View File
@@ -8,23 +8,23 @@ return {
default_icon = { type = "item", name = "discharge-defense-equipment" }, --- @setting default_icon the default icon that will be used for warps default_icon = { type = "item", name = "discharge-defense-equipment" }, --- @setting default_icon the default icon that will be used for warps
-- Warp cooldowns -- Warp cooldowns
bypass_warp_cooldown = "expcore.roles", --- @setting bypass_warp_cooldown dictates who the warp cooldown is applied to; values: all, admin, expcore.roles, none bypass_warp_cooldown = "exp_roles", --- @setting bypass_warp_cooldown dictates who the warp cooldown is applied to; values: all, admin, exp_roles, none
expcore_roles_bypass_warp_cooldown = "gui/warp-list/bypass-cooldown", --- @setting expcore_roles_bypass_warp_cooldown if expcore.roles is used then this is the required permission exp_roles_bypass_warp_cooldown = "exp_scenario.gui.warp_list.bypass_cooldown", --- @setting exp_roles_bypass_warp_cooldown if exp_roles is used then this is the required permission
cooldown_duration = 60, --- @setting cooldown_duration the duration of the warp cooldown in seconds cooldown_duration = 60, --- @setting cooldown_duration the duration of the warp cooldown in seconds
-- Warp proximity -- Warp proximity
bypass_warp_proximity = "expcore.roles", --- @setting bypass_warp_proximity dictates who the warp proximity is applied to; values: all, admin, expcore.roles, none bypass_warp_proximity = "exp_roles", --- @setting bypass_warp_proximity dictates who the warp proximity is applied to; values: all, admin, exp_roles, none
expcore_roles_bypass_warp_proximity = "gui/warp-list/bypass-proximity", --- @setting expcore_roles_bypass_warp_proximity if expcore.roles is used then this is the required permission exp_roles_bypass_warp_proximity = "exp_scenario.gui.warp_list.bypass_proximity", --- @setting exp_roles_bypass_warp_proximity if exp_roles is used then this is the required permission
standard_proximity_radius = 4, --- @setting standard_proximity_radius the minimum distance a player is allowed to be to a warp in order to use it standard_proximity_radius = 4, --- @setting standard_proximity_radius the minimum distance a player is allowed to be to a warp in order to use it
spawn_proximity_radius = 20, --- @setting spawn_proximity_radius the minimum distance a player is allowed to be from they spawn point to use warps spawn_proximity_radius = 20, --- @setting spawn_proximity_radius the minimum distance a player is allowed to be from they spawn point to use warps
-- Adding warps -- Adding warps
allow_add_warp = "expcore.roles", --- @setting allow_add_warp dictates who is allowed to add warps; values: all, admin, expcore.roles, none allow_add_warp = "exp_roles", --- @setting allow_add_warp dictates who is allowed to add warps; values: all, admin, exp_roles, none
expcore_roles_allow_add_warp = "gui/warp-list/add", --- @setting expcore_roles_allow_add_warp if expcore.roles is used then this is the required permission exp_roles_allow_add_warp = "exp_scenario.gui.warp_list.add", --- @setting exp_roles_allow_add_warp if exp_roles is used then this is the required permission
-- Editing warps -- Editing warps
allow_edit_warp = "expcore.roles", --- @setting allow_edit_warp dictates who is allowed to edit warps; values: all, admin, expcore.roles, none allow_edit_warp = "exp_roles", --- @setting allow_edit_warp dictates who is allowed to edit warps; values: all, admin, exp_roles, none
expcore_roles_allow_edit_warp = "gui/warp-list/edit", --- @setting expcore_roles_allow_edit_warp if expcore.roles is used then this is the required permission exp_roles_allow_edit_warp = "exp_scenario.gui.warp_list.edit", --- @setting exp_roles_allow_edit_warp if exp_roles is used then this is the required permission
user_can_edit_own_warps = false, --- @settings user_can_edit_own_warps if true then the user who made the warp can edit it regardless of the allow_edit_warp setting user_can_edit_own_warps = false, --- @settings user_can_edit_own_warps if true then the user who made the warp can edit it regardless of the allow_edit_warp setting
-- Warp area generation -- Warp area generation
+1 -1
View File
@@ -25,6 +25,6 @@ return {
}, },
}, },
}, },
ignore_permission = "bypass-nukeprotect", -- @setting ignore_permission The permission that nukeprotect will ignore ignore_permission = "exp_scenario.bypass.nuke_protection", -- @setting ignore_permission The permission that nukeprotect will ignore
ignore_admins = true, -- @setting ignore_admins Ignore admins, true by default. Allows usage outside of the roles module ignore_admins = true, -- @setting ignore_admins Ignore admins, true by default. Allows usage outside of the roles module
} }
+1 -1
View File
@@ -1,6 +1,6 @@
return { return {
ignore_admins = true, --- @setting ignore_admins If admins are ignored by the protection filter ignore_admins = true, --- @setting ignore_admins If admins are ignored by the protection filter
ignore_permission = "bypass-entity-protection", --- @setting ignore_permission Players with this permission will be ignored by the protection filter, leave nil if expcore.roles is not used ignore_permission = "exp_scenario.bypass.entity_protection", --- @setting ignore_permission Players with this permission will be ignored by the protection filter, leave nil to protect against every player
repeat_count = 5, --- @setting repeat_count Number of protected entities that must be removed within repeat_lifetime in order to trigger repeated removal protection repeat_count = 5, --- @setting repeat_count Number of protected entities that must be removed within repeat_lifetime in order to trigger repeated removal protection
repeat_lifetime = 3600 * 20, --- @setting repeat_lifetime The length of time, in ticks, that protected removals will be remembered for repeat_lifetime = 3600 * 20, --- @setting repeat_lifetime The length of time, in ticks, that protected removals will be remembered for
refresh_rate = 3600 * 5, --- @setting refresh_rate How often the age of protected removals are checked against repeat_lifetime refresh_rate = 3600 * 5, --- @setting refresh_rate How often the age of protected removals are checked against repeat_lifetime
File diff suppressed because it is too large Load Diff
-8
View File
@@ -1,11 +1,3 @@
[expcore-roles]
error-log-format-flag=[ERROR] roleFlag/__1__ :: __2__
error-log-format-assign=[ERROR] rolePromote/__1__ :: __2__
game-message-assign=__1__ has been assigned to __2__ by __3__
game-message-unassign=__1__ has been unassigned from __2__ by __3__
reject-role=Invalid Role Name.
reject-player-role=Player has a higher role.
[expcore-data] [expcore-data]
description-preference=Allows you to set/get your data saving preference. description-preference=Allows you to set/get your data saving preference.
description-data=Writes all your player data to a file on your computer. description-data=Writes all your player data to a file on your computer.
@@ -1,11 +1,3 @@
[expcore-roles]
error-log-format-flag=[ERROR] roleFlag/__1__ :: __2__
error-log-format-assign=[ERROR] rolePromote/__1__ :: __2__
game-message-assign=__1__ 已被 __3__ 指派到身分組 __2__
game-message-unassign=__1__ 已被 __3__ 取消指派到身分組 __2__
reject-role=無效的身分組。
reject-player-role=用戶已有更高的身分組。
[expcore-data] [expcore-data]
description-preference=允許寫入資料。 description-preference=允許寫入資料。
description-data=把用戶資料寫入電腦 description-data=把用戶資料寫入電腦
@@ -1,11 +1,3 @@
[expcore-roles]
error-log-format-flag=[ERROR] roleFlag/__1__ :: __2__
error-log-format-assign=[ERROR] rolePromote/__1__ :: __2__
game-message-assign=__1__ 已被 __3__ 指派到身分組 __2__
game-message-unassign=__1__ 已被 __3__ 取消指派到身分組 __2__
reject-role=無效的身分組。
reject-player-role=用戶已有更高的身分組。
[expcore-data] [expcore-data]
description-preference=允許寫入資料。 description-preference=允許寫入資料。
description-data=把用戶資料寫入電腦 description-data=把用戶資料寫入電腦
+1
View File
@@ -8,6 +8,7 @@
"dependencies": { "dependencies": {
"clusterio": "*", "clusterio": "*",
"exp_util": "*", "exp_util": "*",
"exp_roles": "*",
"exp_gui": "*" "exp_gui": "*"
} }
} }
+18 -18
View File
@@ -7,25 +7,25 @@
-- import the module from the control modules -- import the module from the control modules
local Jail = require("modules.exp_legacy.modules.control.jail") --- @dep modules.control.jail local Jail = require("modules.exp_legacy.modules.control.jail") --- @dep modules.control.jail
-- This will move 'MrBiter' to the jail role and remove all other roles from them -- This will give 'MrBiter' the jail role, which suppresses all of their other roles
-- the player name and reason are only so they can be included in the event for user feedback -- the player name and reason are only so they can be included in the event for user feedback
Jail.jail_player('MrBiter', 'Cooldude2606', 'Likes biters too much') Jail.jail_player('MrBiter', 'Cooldude2606', 'Likes biters too much')
-- This will give 'MrBiter' all his roles back and remove him from jail -- This will remove the jail role from 'MrBiter', restoring their other roles
-- again as above the player name is only used in the event for user feedback -- again as above the player name is only used in the event for user feedback
Jail.unjail_player('MrBiter', 'Cooldude2606') Jail.unjail_player('MrBiter', 'Cooldude2606')
]] ]]
local Roles = require("modules.exp_legacy.expcore.roles") local Roles = require("modules/exp_roles")
local valid_player = function(p) return type(p) == "userdata" and p or game.get_player(p) end local valid_player = function(p) return type(p) == "userdata" and p or game.get_player(p) end
local assign_roles = Roles.assign_player
local unassign_roles = Roles.unassign_player --- The role given to jailed players, it has a higher priority than every other role
local has_role = Roles.player_has_role local function jail_role()
local get_roles = Roles.get_player_roles return assert(Roles.get_role_by_name("Jail"), "The Jail role does not exist")
end
local Jail = { local Jail = {
old_roles = {},
events = { events = {
--- When a player is assigned to jail --- When a player is assigned to jail
-- @event on_player_jailed -- @event on_player_jailed
@@ -64,10 +64,11 @@ end
-- @tparam LuaPlayer player the player to check if they are in jail -- @tparam LuaPlayer player the player to check if they are in jail
-- @treturn boolean whether the player is currently in jail -- @treturn boolean whether the player is currently in jail
function Jail.is_jailed(player) function Jail.is_jailed(player)
return has_role(valid_player(player), "Jail") local valid = valid_player(player)
return valid ~= nil and jail_role():has_player(valid)
end end
--- Moves a player to jail and removes all other roles --- Moves a player to jail, which suppresses all of their other roles
-- @tparam LuaPlayer player the player who will be jailed -- @tparam LuaPlayer player the player who will be jailed
-- @tparam string by_player_name the name of the player who is doing the jailing -- @tparam string by_player_name the name of the player who is doing the jailing
-- @tparam[opt='Non given.'] string reason the reason that the player is being jailed -- @tparam[opt='Non given.'] string reason the reason that the player is being jailed
@@ -79,8 +80,8 @@ function Jail.jail_player(player, by_player_name, reason)
reason = reason or "Non given." reason = reason or "Non given."
if has_role(player, "Jail") then return end local role = jail_role()
local roles = get_roles(player) if role:has_player(player) then return end
player.walking_state = { walking = false, direction = player.walking_state.direction } player.walking_state = { walking = false, direction = player.walking_state.direction }
player.riding_state = { acceleration = defines.riding.acceleration.nothing, direction = player.riding_state.direction } player.riding_state = { acceleration = defines.riding.acceleration.nothing, direction = player.riding_state.direction }
@@ -89,16 +90,14 @@ function Jail.jail_player(player, by_player_name, reason)
player.picking_state = false player.picking_state = false
player.repair_state = { repairing = false, position = player.repair_state.position } player.repair_state = { repairing = false, position = player.repair_state.position }
unassign_roles(player, roles, by_player_name, nil, true) role:assign(player, { by_player_name = by_player_name, silent = true })
assign_roles(player, "Jail", by_player_name, nil, true)
assign_roles(player, roles, by_player_name, nil, true)
event_emit(Jail.events.on_player_jailed, player, by_player_name, reason) event_emit(Jail.events.on_player_jailed, player, by_player_name, reason)
return true return true
end end
--- Moves a player out of jail and restores all roles previously removed --- Moves a player out of jail, which restores all of their other roles
-- @tparam LuaPlayer player the player that will be unjailed -- @tparam LuaPlayer player the player that will be unjailed
-- @tparam string by_player_name the name of the player that is doing the unjail -- @tparam string by_player_name the name of the player that is doing the unjail
-- @treturn boolean whether the player was unjailed successfully -- @treturn boolean whether the player was unjailed successfully
@@ -107,9 +106,10 @@ function Jail.unjail_player(player, by_player_name)
if not player then return end if not player then return end
if not by_player_name then return end if not by_player_name then return end
if not has_role(player, "Jail") then return end local role = jail_role()
if not role:has_player(player) then return end
unassign_roles(player, "Jail", by_player_name, nil, true) role:unassign(player, { by_player_name = by_player_name, silent = true })
event_emit(Jail.events.on_player_unjailed, player, by_player_name) event_emit(Jail.events.on_player_unjailed, player, by_player_name)
@@ -33,11 +33,7 @@ for _, config_key in ipairs{ "always_protected_names", "always_protected_types",
end end
end end
-- Require roles if a permission is assigned in the config local Roles = require("modules/exp_roles")
local Roles --- @type table<string, any>
if config.ignore_permission then
Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles
end
----- Storage Variables ----- ----- Storage Variables -----
--- Variables stored in the global table --- Variables stored in the global table
@@ -159,7 +155,7 @@ Event.add(defines.events.on_pre_player_mined_item, function(event)
-- Check if the player should be ignored -- Check if the player should be ignored
if config.ignore_admins and player.admin then return end if config.ignore_admins and player.admin then return end
if entity.last_user == nil or entity.last_user.index == player.index then return end if entity.last_user == nil or entity.last_user.index == player.index then return end
if config.ignore_permission and Roles.player_allowed(player, config.ignore_permission) then return end if config.ignore_permission and Roles.player_has_permission(player, config.ignore_permission) then return end
-- Check if the entity is protected -- Check if the entity is protected
if EntityProtection.is_entity_protected(entity) if EntityProtection.is_entity_protected(entity)
+1 -1
View File
@@ -8,7 +8,7 @@ local Commands = require("modules/exp_commands")
local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data
local CustomMessages = PlayerData.Settings:combine("JoinMessage") local CustomMessages = PlayerData.Settings:combine("JoinMessage")
CustomMessages:set_metadata{ CustomMessages:set_metadata{
permission = "command/join-message", permission = "exp_scenario.command.set_join_message",
} }
--- When a players data loads show their message --- When a players data loads show their message
+1 -1
View File
@@ -10,7 +10,7 @@ local config = require("modules.exp_legacy.config.preset_player_quickbar") --- @
local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data
local PlayerFilters = PlayerData.Settings:combine("QuickbarFilters") local PlayerFilters = PlayerData.Settings:combine("QuickbarFilters")
PlayerFilters:set_metadata{ PlayerFilters:set_metadata{
permission = "command/save-quickbar", permission = "exp_scenario.command.save_quickbar",
stringify = function(value) stringify = function(value)
if not value then return "No filters set" end if not value then return "No filters set" end
local count = 0 local count = 0
+4 -4
View File
@@ -4,17 +4,17 @@
]] ]]
local Commands = require("modules/exp_commands") local Commands = require("modules/exp_commands")
local Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles local Roles = require("modules/exp_roles")
--- Stores the tag for a player --- Stores the tag for a player
local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data
local PlayerTags = PlayerData.Settings:combine("Tag") local PlayerTags = PlayerData.Settings:combine("Tag")
local PlayerTagColors = PlayerData.Settings:combine("TagColor") local PlayerTagColors = PlayerData.Settings:combine("TagColor")
PlayerTags:set_metadata{ PlayerTags:set_metadata{
permission = "command/tag", permission = "exp_scenario.command.tag",
} }
PlayerTagColors:set_metadata{ PlayerTagColors:set_metadata{
permission = "command/tag-color", permission = "exp_scenario.command.tag_color",
} }
local set_tag = function(player, tag, color) local set_tag = function(player, tag, color)
@@ -72,7 +72,7 @@ Commands.new("tag-clear", "Clears your tag. Or another player if you are admin."
if other_player == player then if other_player == player then
-- No player given so removes your tag -- No player given so removes your tag
PlayerTags:remove(other_player) PlayerTags:remove(other_player)
elseif Roles.player_allowed(player, "command/clear-tag/always") then elseif Roles.player_has_permission(player, "exp_scenario.command.tag_clear.always") then
-- Player given and user is admin so clears that player's tag -- Player given and user is admin so clears that player's tag
PlayerTags:remove(other_player) PlayerTags:remove(other_player)
else else
@@ -1,7 +0,0 @@
local Gui = require("modules/exp_gui")
local Roles = require("modules.exp_legacy.expcore.roles")
local Event = require("modules/exp_legacy/utils/event")
--- @diagnostic disable: access-invisible
Event.add(Roles.events.on_role_assigned, Gui._ensure_consistency)
Event.add(Roles.events.on_role_unassigned, Gui._ensure_consistency)
+5 -6
View File
@@ -5,7 +5,7 @@
]] ]]
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles local Roles = require("modules/exp_roles")
local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event
local format_number = require("util").format_number --- @dep util local format_number = require("util").format_number --- @dep util
local config = require("modules.exp_legacy.config.vlayer") --- @dep config.vlayer local config = require("modules.exp_legacy.config.vlayer") --- @dep config.vlayer
@@ -457,7 +457,7 @@ local vlayer_control_set = Gui.define("vlayer_control_set")
vlayer_gui_control_see(disp) vlayer_gui_control_see(disp)
local b = vlayer_gui_control_build(disp) local b = vlayer_gui_control_build(disp)
local r = vlayer_gui_control_remove(disp) local r = vlayer_gui_control_remove(disp)
local v = Roles.player_allowed(player, "gui/vlayer-edit") local v = Roles.player_has_permission(player, "exp_scenario.gui.vlayer_edit")
b.visible = v b.visible = v
r.visible = v r.visible = v
@@ -484,22 +484,21 @@ Gui.toolbar.create_button{
sprite = "entity/solar-panel", sprite = "entity/solar-panel",
tooltip = { "vlayer.main-tooltip" }, tooltip = { "vlayer.main-tooltip" },
visible = function(player, element) visible = function(player, element)
return Roles.player_allowed(player, "gui/vlayer") return Roles.player_has_permission(player, "exp_scenario.gui.vlayer")
end end
} }
--- Update the visibly of the buttons based on a players roles --- Update the visibly of the buttons based on a players roles
local function role_update_event(event) local function role_update_event(event)
local player = game.players[event.player_index] local player = game.players[event.player_index]
local visible = Roles.player_allowed(player, "gui/vlayer-edit") local visible = Roles.player_has_permission(player, "exp_scenario.gui.vlayer_edit")
local container = Gui.get_left_element(vlayer_container, player) local container = Gui.get_left_element(vlayer_container, player)
local disp = container.frame["vlayer_st_2"].disp.table local disp = container.frame["vlayer_st_2"].disp.table
disp[vlayer_gui_control_build.name].visible = visible disp[vlayer_gui_control_build.name].visible = visible
disp[vlayer_gui_control_remove.name].visible = visible disp[vlayer_gui_control_remove.name].visible = visible
end end
Event.add(Roles.events.on_role_assigned, role_update_event) Event.add(Roles.events.on_player_roles_changed, role_update_event)
Event.add(Roles.events.on_role_unassigned, role_update_event)
Event.on_nth_tick(config.update_tick_gui, function(_) Event.on_nth_tick(config.update_tick_gui, function(_)
local stats = vlayer.get_statistics() local stats = vlayer.get_statistics()
+5 -6
View File
@@ -9,7 +9,7 @@ local Gui = require("modules/exp_gui")
local Datastore = require("modules.exp_legacy.expcore.datastore") --- @dep expcore.datastore local Datastore = require("modules.exp_legacy.expcore.datastore") --- @dep expcore.datastore
local Storage = require("modules/exp_util/storage") local Storage = require("modules/exp_util/storage")
local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event
local Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles local Roles = require("modules/exp_roles")
local Colors = require("modules/exp_util/include/color") local Colors = require("modules/exp_util/include/color")
local config = require("modules.exp_legacy.config.gui.warps") --- @dep config.gui.warps local config = require("modules.exp_legacy.config.gui.warps") --- @dep config.gui.warps
local Warps = require("modules.exp_legacy.modules.control.warps") --- @dep modules.control.warps local Warps = require("modules.exp_legacy.modules.control.warps") --- @dep modules.control.warps
@@ -68,8 +68,8 @@ local function check_player_permissions(player, action, warp)
return true return true
elseif action_config == "admin" then elseif action_config == "admin" then
return player.admin return player.admin
elseif action_config == "expcore.roles" then elseif action_config == "exp_roles" then
return Roles.player_allowed(player, config["expcore_roles_" .. action]) return Roles.player_has_permission(player, config["exp_roles_" .. action])
end end
-- Return false as all other conditions have not been met -- Return false as all other conditions have not been met
@@ -708,7 +708,7 @@ Gui.toolbar.create_button{
sprite = config.default_icon.type .. "/" .. config.default_icon.name, sprite = config.default_icon.type .. "/" .. config.default_icon.name,
tooltip = { "warp-list.main-tooltip" }, tooltip = { "warp-list.main-tooltip" },
visible = function(player, element) visible = function(player, element)
return Roles.player_allowed(player, "gui/warp-list") return Roles.player_has_permission(player, "exp_scenario.gui.warp_list")
end end
}:on_click(function(def, player, element) }:on_click(function(def, player, element)
-- Set gui keep open state for player that clicked the button: true if visible, false if invisible -- Set gui keep open state for player that clicked the button: true if visible, false if invisible
@@ -885,8 +885,7 @@ local function role_update_event(event)
add_new_warp_element.visible = allow_add_warp add_new_warp_element.visible = allow_add_warp
end end
Event.add(Roles.events.on_role_assigned, role_update_event) Event.add(Roles.events.on_player_roles_changed, role_update_event)
Event.add(Roles.events.on_role_unassigned, role_update_event)
--- When a chart tag is removed or edited make sure it is not one that belongs to a warp --- When a chart tag is removed or edited make sure it is not one that belongs to a warp
local function maintain_tag(event) local function maintain_tag(event)
+64
View File
@@ -1,6 +1,7 @@
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 {
@@ -34,6 +35,7 @@ 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));
@@ -43,6 +45,68 @@ 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
*/ */
+1
View File
@@ -27,6 +27,7 @@ export const plugin: lib.PluginDeclaration = {
messages.RoleListRequest, messages.RoleListRequest,
messages.RoleMetaUpdateRequest, messages.RoleMetaUpdateRequest,
messages.SeedRolesRequest,
messages.AssignmentListRequest, messages.AssignmentListRequest,
messages.AssignmentUpdateRequest, messages.AssignmentUpdateRequest,
+4
View File
@@ -48,6 +48,10 @@ export class InstancePlugin extends BaseInstancePlugin {
this.instance.sendTo("controller", new messages.AssignmentListRequest()), this.instance.sendTo("controller", new messages.AssignmentListRequest()),
]); ]);
if (!roles.some(role => role.isDefault)) {
this.logger.warn("No default role is set on the controller, players will hold no roles in game");
}
await this.luaSend("initialise", { await this.luaSend("initialise", {
...messages.encodeRolesForLua(roles), ...messages.encodeRolesForLua(roles),
assignments: assignments.map(assignment => assignment.toJSON()), assignments: assignments.map(assignment => assignment.toJSON()),
+12
View File
@@ -365,6 +365,18 @@ 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
*/ */
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -2,8 +2,8 @@
Wires the role sync into the factorio event handler. Wires the role sync into the factorio event handler.
This is kept apart from control.lua because `ExpRoles.events` holds the ids of This is kept apart from control.lua because `ExpRoles.events` holds the ids of
the events this module raises, which is the interface the legacy role system the events this module raises, while event_handler expects `events` to be the
had, and event_handler expects `events` to be the handlers to register. handlers to register.
]] ]]
local clusterio_api = require("modules/clusterio/api") local clusterio_api = require("modules/clusterio/api")
+3
View File
@@ -0,0 +1,3 @@
[exp-roles]
game-message-assign=__1__ 已被 __3__ 指派到身分組 __2__
game-message-unassign=__1__ 已被 __3__ 取消指派到身分組 __2__
+3
View File
@@ -0,0 +1,3 @@
[exp-roles]
game-message-assign=__1__ 已被 __3__ 指派到身分組 __2__
game-message-unassign=__1__ 已被 __3__ 取消指派到身分組 __2__
+6 -1
View File
@@ -7,7 +7,9 @@
"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"
@@ -24,6 +26,7 @@
"@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:",
@@ -32,6 +35,8 @@
"react-router-dom": "catalog:", "react-router-dom": "catalog:",
"typescript": "catalog:", "typescript": "catalog:",
"webpack": "catalog:", "webpack": "catalog:",
"fengari": "^0.1.5",
"tap": "^21.1.0",
"webpack-cli": "catalog:", "webpack-cli": "catalog:",
"webpack-merge": "catalog:" "webpack-merge": "catalog:"
}, },
+268
View File
@@ -0,0 +1,268 @@
import { RoleColor } from "./messages";
/**
* A role created by the seed, as the scenario defined it before roles moved to
* the controller. `parent` stands in for the inheritance
* the old config had and is flattened by the seed. The default and admin roles
* already exist, so those entries only provide the in game properties.
*/
export interface SeedRole {
name: string;
shortHand: string;
color: RoleColor | null;
priority?: number;
autoAssignHours?: number;
blockAutoAssign?: boolean;
/** Uses the controller's default role rather than creating one. */
isDefault?: boolean;
/** Uses the controller's admin role rather than creating one. */
isAdmin?: boolean;
/** Name of the role whose permissions are also granted, applied recursively. */
parent?: string;
permissions: string[];
}
export const seedRoles: SeedRole[] = [
{
name: "System",
shortHand: "SYS",
color: null,
isAdmin: true,
permissions: [],
},
{
name: "Senior Administrator",
shortHand: "SAdmin",
color: new RoleColor(233, 63, 233),
parent: "Administrator",
permissions: [
"exp_scenario.command._rcon",
"exp_scenario.command.debug",
"exp_scenario.command.set_cheat_mode",
"exp_scenario.command.research_all",
],
},
{
name: "Administrator",
shortHand: "Admin",
color: new RoleColor(233, 63, 233),
parent: "Moderator",
permissions: [
"exp_scenario.gui.warp_list.bypass_proximity",
"exp_scenario.gui.warp_list.bypass_cooldown",
"exp_scenario.command.connect_all",
],
},
{
name: "Moderator",
shortHand: "Mod",
color: new RoleColor(0, 170, 0),
parent: "Trainee",
permissions: [
"exp_scenario.player.instant_respawn",
"exp_scenario.command.assign_role",
"exp_scenario.command.unassign_role",
"exp_scenario.command.repair",
"exp_scenario.command.kill.always",
"exp_scenario.command.tag_clear.always",
"exp_scenario.command.spawn.always",
"exp_scenario.command.clear_reports",
"exp_scenario.command.clear_warnings",
"exp_scenario.command.clear_script_warnings",
"exp_scenario.command.clear_last_warnings",
"exp_scenario.command.clear_inventory",
"exp_scenario.command.kill_enemies",
"exp_scenario.command.remove_enemies",
"exp_scenario.command.home",
"exp_scenario.command.set_home",
"exp_scenario.command.get_home",
"exp_scenario.command.return",
"exp_scenario.command.connect_player",
"exp_scenario.command.set_bot_queue",
"exp_scenario.command.set_game_speed",
"exp_scenario.command.set_friendly_fire",
"exp_scenario.command.set_always_day",
"exp_scenario.command.set_pollution_enabled",
"exp_scenario.command.clear_pollution",
"exp_scenario.gui.rocket_info.toggle_active",
"exp_scenario.gui.rocket_info.remote_launch",
"exp_scenario.gui.bonus",
"exp_scenario.decon.fast_trees",
],
},
{
name: "Trainee",
shortHand: "TrMod",
color: new RoleColor(0, 170, 0),
parent: "Veteran",
permissions: [
"exp_scenario.player.admin",
"exp_scenario.player.spectator",
"exp_scenario.bypass.reports",
"exp_scenario.command.admin_chat",
"exp_scenario.command.goto",
"exp_scenario.command.teleport",
"exp_scenario.command.bring",
"exp_scenario.command.create_warning",
"exp_scenario.command.get_warnings",
"exp_scenario.command.get_reports",
"exp_scenario.command.protect_entity",
"exp_scenario.command.protect_area",
"exp_scenario.command.protect_tag",
"exp_scenario.command.jail",
"exp_scenario.command.unjail",
"exp_scenario.gui.player_list.kick",
"exp_scenario.gui.player_list.ban",
"exp_scenario.command.spectate",
"exp_scenario.command.follow",
"exp_scenario.command.search",
"exp_scenario.command.search_online",
"exp_scenario.command.search_amount",
"exp_scenario.command.search_recent",
"exp_scenario.command.clear_blueprints_surface",
"exp_scenario.gui.playerdata",
],
},
{
name: "Board Member",
shortHand: "Board",
color: new RoleColor(247, 246, 54),
parent: "Sponsor",
permissions: [
"exp_scenario.command.goto",
"exp_scenario.command.repair",
"exp_scenario.command.spectate",
"exp_scenario.command.follow",
"exp_scenario.gui.playerdata",
],
},
{
name: "Senior Backer",
shortHand: "Backer",
color: new RoleColor(238, 172, 44),
parent: "Sponsor",
permissions: [],
},
{
name: "Sponsor",
shortHand: "Spon",
color: new RoleColor(238, 172, 44),
parent: "Supporter",
permissions: [
"exp_scenario.bypass.reports",
"exp_scenario.player.instant_respawn",
"exp_scenario.gui.rocket_info.toggle_active",
"exp_scenario.gui.rocket_info.remote_launch",
"exp_scenario.gui.bonus",
"exp_scenario.command.home",
"exp_scenario.command.set_home",
"exp_scenario.command.get_home",
"exp_scenario.command.return",
"exp_scenario.decon.fast_trees",
],
},
{
name: "Supporter",
shortHand: "Sup",
color: new RoleColor(230, 99, 34),
parent: "Veteran",
permissions: [
"exp_scenario.player.spectator",
"exp_scenario.command.tag_color",
"exp_scenario.command.jail",
"exp_scenario.command.unjail",
"exp_scenario.command.set_join_message",
"exp_scenario.command.remove_join_message",
],
},
{
name: "Partner",
shortHand: "Part",
color: new RoleColor(140, 120, 200),
parent: "Veteran",
permissions: [
"exp_scenario.player.spectator",
"exp_scenario.command.jail",
"exp_scenario.command.unjail",
],
},
{
name: "Veteran",
shortHand: "Vet",
color: new RoleColor(140, 120, 200),
parent: "Member",
autoAssignHours: 10,
permissions: [
"exp_scenario.chat.commands",
"exp_scenario.command.clear_ground_items",
"exp_scenario.command.clear_blueprints",
"exp_scenario.command.set_trains_to_automatic",
],
},
{
name: "Member",
shortHand: "Mem",
color: new RoleColor(24, 172, 188),
parent: "Regular",
permissions: [
"exp_scenario.bypass.deconstruction_log",
"exp_scenario.gui.task_list.add",
"exp_scenario.gui.task_list.edit",
"exp_scenario.gui.warp_list.add",
"exp_scenario.gui.warp_list.edit",
"exp_scenario.gui.surveillance",
"exp_scenario.gui.vlayer_edit",
"exp_scenario.gui.tool",
"exp_scenario.command.save_quickbar",
"exp_scenario.command.vlayer_info",
"exp_scenario.command.lawnmower",
"exp_scenario.command.waterfill",
"exp_scenario.command.artillery",
],
},
{
name: "Regular",
shortHand: "Reg",
color: new RoleColor(79, 155, 163),
autoAssignHours: 3,
permissions: [
"exp_scenario.command.kill",
"exp_scenario.command.rainbow",
"exp_scenario.command.spawn",
"exp_scenario.command.me",
"exp_scenario.decon.standard",
"exp_scenario.bypass.entity_protection",
"exp_scenario.bypass.nuke_protection",
],
},
{
name: "Jail",
shortHand: "Jail",
color: new RoleColor(50, 50, 50),
priority: 1,
blockAutoAssign: true,
permissions: [],
},
{
name: "Guest",
shortHand: "",
color: new RoleColor(185, 187, 160),
isDefault: true,
permissions: [],
},
];
/** The permissions a seed role grants, including those of its parents. */
export function flattenSeedPermissions(role: SeedRole, roles = seedRoles) {
const permissions = new Set<string>();
const seen = new Set<string>();
let current: SeedRole | undefined = role;
while (current && !seen.has(current.name)) {
seen.add(current.name);
for (const permission of current.permissions) {
permissions.add(permission);
}
current = roles.find(other => other.name === current!.parent);
}
return permissions;
}
+250
View File
@@ -0,0 +1,250 @@
"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 } = 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
lib.Link.register(messages.RoleUpdatedEvent);
lib.Link.register(messages.AssignmentUpdatedEvent);
lib.Link.register(messages.RoleListRequest);
lib.Link.register(messages.RoleMetaUpdateRequest);
lib.Link.register(messages.SeedRolesRequest);
lib.Link.register(messages.AssignmentListRequest);
lib.Link.register(messages.AssignmentUpdateRequest);
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 a plugin around a real controller, which is side effect free while not started. */
async function startPlugin(t2, { roles = [] } = {}) {
const controllerConfig = new lib.ControllerConfig("controller", {
"controller.database_directory": t2.testdir(),
"controller.default_role_id": lib.Role.DefaultPlayerRoleId,
});
const controller = new Controller(logger, [], controllerConfig);
for (const role of roles) {
controller.roles.set(role);
}
// Spies which record and then defer to the real behaviour
const state = { broadcasts: [], permissionUpdates: [] };
const broadcast = controller.subscriptions.broadcast.bind(controller.subscriptions);
controller.subscriptions.broadcast = event => {
state.broadcasts.push(event);
broadcast(event);
};
const permissionsUpdated = controller.userPermissionsUpdated.bind(controller);
controller.userPermissionsUpdated = user => {
state.permissionUpdates.push(user.name);
permissionsUpdated(user);
};
const plugin = new ControllerPlugin({ name: "exp_roles" }, controller, undefined, logger);
await plugin.init();
return { plugin, controller, state };
}
const role = (id, name, permissions = []) => new lib.Role(id, name, "", new Set(permissions));
t.test("class ControllerPlugin", t2 => {
t2.test(".init() and .sweepRoleMeta() manage the role properties", async t3 => {
const { plugin } = await startPlugin(t3, { roles: [role(0, "Cluster Admin"), role(5, "Moderator")] });
t3.strictSame(plugin.roleMeta.get(0).order, 1, "the first role is ordered first");
t3.strictSame(plugin.roleMeta.get(5).order, 2, "the next role is ordered after it");
plugin.roleMeta.set(new messages.RoleMetaRecord(99, 3));
plugin.sweepRoleMeta();
t3.notOk(plugin.roleMeta.get(99), "properties without a role are removed");
t3.ok(plugin.roleMeta.get(5), "properties with a role are kept");
});
t2.test(".buildRoleRecord() combines the role with its properties", async t3 => {
const { plugin, controller } = await startPlugin(t3, {
roles: [role(1, "Player"), role(5, "Moderator", ["exp_scenario.command.kill"])],
});
plugin.roleMeta.set(new messages.RoleMetaRecord(5, 2, 1, "Mod"));
const record = plugin.buildRoleRecord(controller.roles.get(5));
t3.strictSame(record.name, "Moderator", "the name comes from the role");
t3.strictSame(record.permissions, ["exp_scenario.command.kill"], "the permissions come from the role");
t3.strictSame(record.meta.shortHand, "Mod", "the properties come from the datastore");
t3.notOk(record.isDefault, "not the default role");
t3.ok(plugin.buildRoleRecord(controller.roles.get(1)).isDefault, "the default role is marked");
});
t2.test(".rolesUpdated() and .roleMetaUpdated() broadcast to subscribers", async t3 => {
const { plugin, controller, state } = await startPlugin(t3, { roles: [role(1, "Player")] });
state.broadcasts.length = 0;
controller.roles.set(role(5, "Moderator"));
const updated = state.broadcasts
.filter(event => event instanceof messages.RoleUpdatedEvent)
.flatMap(event => event.updates.map(update => update.name));
t3.ok(updated.includes("Moderator"), "a new role is broadcast");
state.broadcasts.length = 0;
plugin.roleMeta.set(new messages.RoleMetaRecord(5, 2, 0, "Mod"));
t3.ok(state.broadcasts.some(
event => event instanceof messages.RoleUpdatedEvent && event.updates[0].meta.shortHand === "Mod"
), "a property change is broadcast as its role");
state.broadcasts.length = 0;
await plugin.onControllerConfigFieldChanged("controller.default_role_id");
t3.ok(
state.broadcasts.some(event => event instanceof messages.RoleUpdatedEvent),
"a default role change rebroadcasts",
);
});
t2.test(".handleRoleSubscription() replays only newer records", async t3 => {
const { plugin, controller } = await startPlugin(t3, { roles: [role(1, "Player")] });
controller.roles.set(role(5, "Moderator"));
const updatedAtMs = plugin.buildRoleRecord(controller.roles.get(5)).updatedAtMs;
const all = await plugin.handleRoleSubscription({ lastRequestTimeMs: 0 });
t3.strictSame(all.updates.length, 2, "everything is replayed from the start");
const none = await plugin.handleRoleSubscription({ lastRequestTimeMs: updatedAtMs });
t3.strictSame(none, null, "nothing is replayed when up to date");
});
t2.test(".listAssignmentRecords() mirrors users without the default role", async t3 => {
const { plugin, controller } = await startPlugin(t3, { roles: [role(1, "Player"), role(5, "Moderator")] });
controller.users.createUser("alice").set("roleIds", new Set([lib.Role.DefaultPlayerRoleId, 5]));
controller.users.createUser("bob");
const records = plugin.listAssignmentRecords();
t3.strictSame(records.length, 1, "users with only the default role are left out");
t3.strictSame(records[0].name, "alice");
t3.strictSame([...records[0].roleIds], [5], "the default role is not listed");
});
t2.test(".handleAssignmentUpdateRequest() validates the user and roles", async t3 => {
const { plugin, controller, state } = await startPlugin(t3, {
roles: [role(1, "Player"), role(5, "Moderator"), role(6, "Regular")],
});
controller.users.createUser("alice").set("roleIds", new Set([5]));
await t3.rejects(
plugin.handleAssignmentUpdateRequest(new messages.AssignmentUpdateRequest("nobody", [], [])),
{ message: /does not exist/ }, "an unknown user is refused",
);
await t3.rejects(
plugin.handleAssignmentUpdateRequest(new messages.AssignmentUpdateRequest("alice", [99], [])),
{ message: /does not exist/ }, "an unknown role is refused",
);
await plugin.handleAssignmentUpdateRequest(new messages.AssignmentUpdateRequest("alice", [6], [5]));
t3.strictSame([...controller.users.getByName("alice").roleIds], [6], "roles are added and removed");
t3.ok(state.permissionUpdates.includes("alice"), "permission changes are pushed to the user");
});
t2.test(".applyAutoAssign() and .onPlayerEvent() grant roles from online time", async t3 => {
const { plugin, controller } = await startPlugin(t3, {
roles: [role(1, "Player"), role(6, "Regular"), role(7, "Jail")],
});
const onlineTime = ms => new Map([[1, lib.PlayerStats.fromJSON({ online_time_ms: ms })]]);
controller.users.createUser("newcomer").set("instanceStats", onlineTime(3600000));
controller.users.createUser("veteran").set("instanceStats", onlineTime(7200000));
const jailed = controller.users.createUser("jailed");
jailed.set("roleIds", new Set([7]));
jailed.set("instanceStats", onlineTime(7200000));
const roleIds = name => controller.users.getByName(name).roleIds;
// The blocking role is marked first, since setting properties applies auto assignment
plugin.roleMeta.set(new messages.RoleMetaRecord(7, 3, 1, "", "", null, null, true));
plugin.roleMeta.set(new messages.RoleMetaRecord(6, 2, 0, "", "", null, 7200000));
t3.ok(roleIds("veteran").has(6), "enough online time grants the role");
t3.notOk(roleIds("newcomer").has(6), "not enough online time does not");
t3.notOk(roleIds("jailed").has(6), "a blocking role prevents the grant");
// Any user update also applies auto assignment, so the leave path is
// observed directly rather than through a role change
const applied = [];
plugin.applyAutoAssign = names => applied.push(names);
await plugin.onPlayerEvent(undefined, { type: "leave", name: "veteran" });
t3.strictSame(applied, [["veteran"]], "a leave applies auto assignment for that user");
await plugin.onPlayerEvent(undefined, { type: "join", name: "veteran" });
t3.strictSame(applied.length, 1, "other player events do not");
});
t2.test(".handleRoleListRequest() returns the current roles", async t3 => {
const { plugin } = await startPlugin(t3, {
roles: [role(1, "Player"), role(5, "Moderator")],
});
plugin.roleMeta.set(new messages.RoleMetaRecord(5, 2, 0, "Mod"));
const response = await plugin.handleRoleListRequest(new messages.RoleListRequest());
t3.strictSame(response.length, 2, "the current roles are returned");
t3.ok(response.some(record => record.name === "Moderator"));
t3.ok(response.some(record => record.name === "Player"));
});
t2.test(".handleRoleMetaUpdateRequest() validates the role", async t3 => {
const { plugin } = await startPlugin(t3, { roles: [role(1, "Player"), role(5, "Moderator")] });
await t3.rejects(
plugin.handleRoleMetaUpdateRequest(new messages.RoleMetaUpdateRequest(new messages.RoleMetaRecord(99, 1))),
{ message: /does not exist/ }, "properties for an unknown role are refused",
);
await plugin.handleRoleMetaUpdateRequest(new messages.RoleMetaUpdateRequest(
new messages.RoleMetaRecord(5, 2, 0, "Mod"),
));
t3.strictSame(plugin.roleMeta.get(5).shortHand, "Mod", "the properties are stored");
});
t2.test(".handleAssignmentListRequest() returns the current assignments", async t3 => {
const { plugin, controller } = await startPlugin(t3, {
roles: [role(1, "Player"), role(5, "Moderator")],
});
controller.users.createUser("alice").set("roleIds", new Set([5]));
controller.users.createUser("bob");
const response = await plugin.handleAssignmentListRequest(new messages.AssignmentListRequest());
t3.strictSame(response.length, 1, "the current assignments are returned");
t3.strictSame(response[0].name, "alice");
t3.strictSame(response[0].roleIds, new Set([5]), "the default role is not listed");
});
t2.test(".handleAssignmentSubscription() replays only newer records", async t3 => {
const { plugin, controller } = await startPlugin(t3, { roles: [role(1, "Player"), role(5, "Moderator")] });
controller.users.createUser("alice").set("roleIds", new Set([5]));
const updatedAtMs = plugin.listAssignmentRecords()[0].updatedAtMs;
const all = await plugin.handleAssignmentSubscription({ lastRequestTimeMs: 0 });
t3.strictSame(all.updates.length, 1, "everything is replayed from the start");
const none = await plugin.handleAssignmentSubscription({ lastRequestTimeMs: updatedAtMs });
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();
});
+185
View File
@@ -0,0 +1,185 @@
"use strict";
const t = require("tap");
const lib = require("@clusterio/lib");
const { Instance } = require("@clusterio/host");
const { InstancePlugin } = require("../dist/node/instance");
const { plugin: pluginDeclaration } = require("../dist/node/index");
const messages = require("../dist/node/messages");
// The instance validates message classes against the link registry, and the
// plugin's config fields must be defined before an InstanceConfig can set them
lib.Link.register(messages.RoleUpdatedEvent);
lib.Link.register(messages.AssignmentUpdatedEvent);
lib.Link.register(messages.RoleListRequest);
lib.Link.register(messages.AssignmentListRequest);
lib.Link.register(messages.AssignmentUpdateRequest);
lib.addPluginConfigFields([pluginDeclaration]);
const logger = { child: () => logger, info: () => {}, warn: () => {}, error: () => {}, verbose: () => {} };
class TestConnector extends lib.BaseConnector {
constructor() {
super(lib.Address.fromShorthand({ instanceId: 1 }), lib.Address.fromShorthand({ hostId: 1 }));
this.valid = true;
this.connected = true;
this.hasSession = true;
}
send() {}
}
const sampleRoles = () => [
new messages.RoleRecord(1, "Player", ["exp_scenario.gui.readme"], new messages.RoleMetaRecord(1, 2), true),
new messages.RoleRecord(5, "Moderator", ["exp_scenario.gui.readme", "core.admin"], new messages.RoleMetaRecord(5, 1)),
];
const sampleAssignments = () => [new messages.AssignmentRecord("alice", new Set([5]))];
/** Build a plugin around a real instance with spies on what leaves it, started on first use by each test. */
async function startPlugin(t2, { syncMode = "bidirectional", roles = sampleRoles() } = {}) {
const instanceConfig = new lib.InstanceConfig("host");
instanceConfig.set("instance.id", 1);
instanceConfig.set("instance.name", "test");
instanceConfig.set("exp_roles.sync_mode", syncMode);
const instance = new Instance(
{ assignGamePort: () => 1 }, new TestConnector(), t2.testdir(), "factorioDir", instanceConfig
);
// Spies which record the messages and commands leaving the instance
const state = { sent: [], rcons: [], warnings: [], failNext: null };
instance.server = {
handle: () => {},
sendRcon: async command => {
state.rcons.push(command);
return "";
},
};
instance.sendTo = async (dst, request) => {
state.sent.push(request);
if (state.failNext) {
const error = state.failNext;
state.failNext = null;
throw error;
}
if (request instanceof messages.RoleListRequest) {
return roles;
}
if (request instanceof messages.AssignmentListRequest) {
return sampleAssignments();
}
return undefined;
};
const plugin = new InstancePlugin({ name: "exp_roles" }, instance, {});
plugin.logger = { ...logger, warn: message => state.warnings.push(message) };
await plugin.init();
return { plugin, state };
}
/** The lua receiver and payload of a recorded rcon command. */
function decodeRcon(command) {
const match = command.match(/^\/sc exp_roles\.(\w+)\(helpers\.json_to_table\[=\[(.*)\]=\]\)$/s);
return { receiver: match[1], payload: JSON.parse(match[2]) };
}
t.test("class InstancePlugin", t2 => {
t2.test(".onStart() subscribes and initialises the lua module", async t3 => {
const { plugin, state } = await startPlugin(t3, { syncMode: "enabled" });
await plugin.onStart();
const subscribed = state.sent.filter(request => request instanceof lib.SubscriptionRequest);
t3.strictSame(subscribed.length, 2, "roles and assignments are subscribed to");
t3.ok(state.sent.some(request => request instanceof messages.RoleListRequest), "the roles are requested");
t3.ok(
state.sent.some(request => request instanceof messages.AssignmentListRequest),
"the assignments are requested",
);
const initialise = decodeRcon(state.rcons[0]);
t3.strictSame(initialise.receiver, "initialise", "the lua module is initialised");
t3.strictSame(initialise.payload.permission_names, ["exp_scenario.gui.readme", "core.admin"],
"each permission name is sent once");
t3.strictSame(initialise.payload.roles[1].permissions, [0, 1], "roles reference permissions by index");
t3.strictSame(initialise.payload.assignments, [{ name: "alice", role_ids: [5] }], "the assignments are sent");
const emit = decodeRcon(state.rcons[1]);
t3.strictSame([emit.receiver, emit.payload], ["set_emit_events", false], "enabled does not emit changes back");
t3.strictSame(state.warnings.length, 0, "no warnings with a default role");
});
t2.test(".onStart() with bidirectional emits changes back", async t3 => {
const { plugin, state } = await startPlugin(t3);
await plugin.onStart();
const emit = decodeRcon(state.rcons[state.rcons.length - 1]);
t3.strictSame([emit.receiver, emit.payload], ["set_emit_events", true]);
});
t2.test(".onStart() with disabled does nothing", async t3 => {
const { plugin, state } = await startPlugin(t3, { syncMode: "disabled" });
await plugin.onStart();
t3.strictSame(state.sent.length, 0, "nothing is sent");
t3.strictSame(state.rcons.length, 0, "no commands are run");
});
t2.test(".onStart() warns when no default role is set", async t3 => {
const roles = sampleRoles().map(record => { record.isDefault = false; return record; });
const { plugin, state } = await startPlugin(t3, { roles });
await plugin.onStart();
t3.ok(state.warnings.some(message => /default role/.test(message)), "the warning names the default role");
});
t2.test(".handleRoleUpdatedEvent() and .handleAssignmentUpdatedEvent() forward to lua", async t3 => {
const { plugin, state } = await startPlugin(t3);
await plugin.handleRoleUpdatedEvent(new messages.RoleUpdatedEvent(sampleRoles()));
const roles = decodeRcon(state.rcons[0]);
t3.strictSame(roles.receiver, "receive_role_updates", "role updates are forwarded");
t3.strictSame(roles.payload[0].name, "Player", "the records are sent in plain form");
await plugin.handleAssignmentUpdatedEvent(new messages.AssignmentUpdatedEvent(sampleAssignments()));
const assignments = decodeRcon(state.rcons[1]);
t3.strictSame(assignments.receiver, "receive_assignment_updates", "assignment updates are forwarded");
});
t2.test(".handleRoleUpdatedEvent() and .handleAssignmentUpdatedEvent() are gated while disabled", async t3 => {
const { plugin, state } = await startPlugin(t3, { syncMode: "disabled" });
await plugin.handleRoleUpdatedEvent(new messages.RoleUpdatedEvent(sampleRoles()));
await plugin.handleAssignmentUpdatedEvent(new messages.AssignmentUpdatedEvent(sampleAssignments()));
t3.strictSame(state.rcons.length, 0, "no commands are run");
});
t2.test(".handleAssignmentUpdateIPC() sends the change and rolls back a refusal", async t3 => {
const { plugin, state } = await startPlugin(t3);
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: [5], unassign: undefined });
const request = state.sent[0];
t3.ok(request instanceof messages.AssignmentUpdateRequest, "the change is sent as a request");
t3.strictSame([request.name, request.assign, request.unassign], ["alice", [5], []], "the request names the change");
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: undefined, unassign: [5] });
t3.strictSame(state.sent[1].unassign, [5], "removals are sent the same way");
state.failNext = new Error("User 'alice' does not exist");
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: [5], unassign: undefined });
const rollback = decodeRcon(state.rcons[0]);
t3.strictSame(rollback.receiver, "reject_assignment", "the refusal is rolled back in lua");
t3.strictSame(rollback.payload, { name: "alice", role_ids: [5] }, "the rollback names the refused roles");
state.failNext = new Error("User 'alice' does not exist");
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: undefined, unassign: [5] });
t3.strictSame(state.rcons.length, 1, "a refused removal has nothing to roll back");
});
t2.test(".handleAssignmentUpdateIPC() is gated unless bidirectional", async t3 => {
const { plugin, state } = await startPlugin(t3, { syncMode: "enabled" });
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: [5], unassign: undefined });
t3.strictSame(state.sent.length, 0, "nothing is sent");
});
t2.test(".onInstanceConfigFieldChanged() toggles emit with the sync mode", async t3 => {
const { plugin, state } = await startPlugin(t3);
await plugin.onInstanceConfigFieldChanged("exp_roles.sync_mode", "enabled", "bidirectional");
const emit = decodeRcon(state.rcons[0]);
t3.strictSame([emit.receiver, emit.payload], ["set_emit_events", false], "leaving bidirectional stops emitting");
});
t2.end();
});
+116
View File
@@ -0,0 +1,116 @@
"use strict";
const t = require("tap");
const messages = require("../dist/node/messages");
const { testMatrix, testRoundTripJsonSerialisable } = require("../../test/common");
const fullMeta = new messages.RoleMetaRecord(
7, 3, 1, "Mod", "[Mod]", new messages.RoleColor(1, 2, 3), 3600000, true, 12345, false,
);
t.test("class RoleColor", t2 => {
testRoundTripJsonSerialisable(t2, messages.RoleColor, testMatrix(
[0, 255], // r
[0, 128], // g
[0, 1], // b
));
t2.end();
});
t.test("class RoleMetaRecord", t2 => {
testRoundTripJsonSerialisable(t2, messages.RoleMetaRecord, testMatrix(
[7], // id
[3], // order
[0, 1], // priority
["", "Mod"], // shortHand
["", "[Mod]"], // tag
[null, new messages.RoleColor(1, 2, 3)], // color
[null, 3600000], // autoAssignOnlineTimeMs
[false, true], // blockAutoAssign
[0, 12345], // updatedAtMs
[false, true], // isDeleted
));
t2.end();
});
t.test("class RoleRecord", t2 => {
testRoundTripJsonSerialisable(t2, messages.RoleRecord, testMatrix(
[7], // id
["Moderator"], // name
[[], ["a.b", "c.d"]], // permissions
[new messages.RoleMetaRecord(7, 3), fullMeta], // meta
[false, true], // isDefault
[0, 12345], // updatedAtMs
[false, true], // isDeleted
));
t2.end();
});
t.test("class AssignmentRecord", t2 => {
testRoundTripJsonSerialisable(t2, messages.AssignmentRecord, testMatrix(
["alice"], // name
[new Set(), new Set([5, 6])], // roleIds
[0, 12345], // updatedAtMs
[false, true], // isDeleted
));
t2.test("get id()", t3 => {
const record = new messages.AssignmentRecord("alice", new Set([5]), 12345);
t3.equal(record.id, "alice");
t3.end();
});
t2.end();
});
const sampleRecord = new messages.RoleRecord(7, "Moderator", ["a.b"], fullMeta, true, 12345);
const sampleAssignment = new messages.AssignmentRecord("alice", new Set([5]), 12345);
t.test("class RoleUpdatedEvent", t2 => {
testRoundTripJsonSerialisable(t2, messages.RoleUpdatedEvent, testMatrix(
[[], [sampleRecord]], // updates
));
t2.end();
});
t.test("class AssignmentUpdatedEvent", t2 => {
testRoundTripJsonSerialisable(t2, messages.AssignmentUpdatedEvent, testMatrix(
[[], [sampleAssignment]], // updates
));
t2.end();
});
t.test("class RoleMetaUpdateRequest", t2 => {
testRoundTripJsonSerialisable(t2, messages.RoleMetaUpdateRequest, testMatrix(
[new messages.RoleMetaRecord(7, 3), fullMeta], // meta
));
t2.end();
});
t.test("class AssignmentUpdateRequest", t2 => {
testRoundTripJsonSerialisable(t2, messages.AssignmentUpdateRequest, testMatrix(
["alice"], // name
[[], [5]], // assign
[[], [6]], // unassign
));
t2.end();
});
t.test("encodeRolesForLua() sends each permission name once", t2 => {
const meta = id => new messages.RoleMetaRecord(id, id);
const encoded = messages.encodeRolesForLua([
new messages.RoleRecord(1, "A", ["p.one", "p.two"], meta(1)),
new messages.RoleRecord(2, "B", ["p.two", "p.three"], meta(2)),
]);
t2.strictSame(encoded.permission_names, ["p.one", "p.two", "p.three"], "names are deduplicated");
t2.strictSame(encoded.roles[0].permissions, [0, 1], "indexes are zero based");
t2.strictSame(encoded.roles[1].permissions, [1, 2], "shared names reuse their index");
t2.end();
});
+14
View File
@@ -0,0 +1,14 @@
"use strict";
const path = require("node:path");
const t = require("tap");
const { reportLuaTests } = require("../../test/lua/runner");
const envFile = path.join(__dirname, "module", "env.lua");
// Each file runs in its own lua state, and each test in a fresh environment
t.test("control.lua", t2 => {
for (const file of ["lookup.lua", "players.lua", "assignment.lua", "sync.lua", "holders.lua"]) {
t2.test(file, t3 => reportLuaTests(t3, envFile, path.join(__dirname, "module", file)));
}
t2.end();
});
+192
View File
@@ -0,0 +1,192 @@
--- Tests for the assignment methods of module/control.lua
local Suite = ... --- @type Suite<ExpRoles.TestEnv> The suite this file adds its tests to, see env.lua
local test, check, eq, empty = Suite.test, Suite.check, Suite.eq, Suite.empty
--- alice is a moderator and bob a regular, with changes sent to the controller
local function setup(env)
local players = {
alice = env.add_player("alice"),
bob = env.add_player("bob"),
}
env.initialise{
env.assignment("alice", { "Moderator" }),
env.assignment("bob", { "Regular" }),
}
env.Roles.set_emit_events(true)
env.reset_log()
return players
end
test(".assign() applies locally and is sent to the controller", function(env)
local players = setup(env)
local moderator = env.R("Moderator")
moderator:assign(players.bob, { by_player_name = "alice" })
eq(env.sent, {
{ channel = "exp_roles:assignment_update", data = { name = "bob", assign = { moderator.id } } },
}, "the assignment is sent to the controller")
check(moderator:has_player(players.bob), "the role applies before confirmation")
eq(env.events, {
{
name = env.Roles.events.on_player_roles_changed,
tick = 1,
player_index = players.bob.index,
by_player_index = players.alice.index,
assigned = { moderator.id },
unassigned = {},
},
}, "the event carries the change")
eq(env.printed, {
{ "exp-roles.game-message-assign", "bob", "Moderator", "alice" },
}, "the change is announced")
eq(env.sounds, { "bob:utility/achievement_unlocked" }, "the assign sound plays")
local sd = env.script_data()
eq(sd.local_players.bob, { moderator.id }, "the role is held locally")
eq(sd.pending.bob, { moderator.id }, "the role is pending")
env.reset_log()
moderator:assign(players.bob)
empty(env.sent, "assigning a held role sends nothing")
empty(env.events, "assigning a held role raises nothing")
end)
test(".assign() defaults the by player to game.player", function(env)
local players = setup(env)
game.player = players.alice
env.R("Jail"):assign(players.bob, { silent = true })
eq(env.events, {
{
name = env.Roles.events.on_player_roles_changed,
tick = 1,
player_index = players.bob.index,
by_player_index = players.alice.index,
assigned = { env.R("Jail").id },
unassigned = {},
},
}, "the acting player is game.player")
end)
test("receive_assignment_updates() releases the local hold once confirmed", function(env)
local players = setup(env)
env.R("Moderator"):assign(players.bob)
env.reset_log()
env.Roles.receive_assignment_updates{ env.assignment("bob", { "Regular", "Moderator" }) }
empty(env.events, "a confirmation raises no events")
empty(env.printed, "a confirmation announces nothing")
local sd = env.script_data()
check(sd.local_players.bob == nil and sd.pending.bob == nil, "the role is no longer held locally")
check(env.R("Moderator"):has_player(players.bob), "the role is still held after confirmation")
end)
test(".unassign() removes a synced role", function(env)
local players = setup(env)
local regular = env.R("Regular")
regular:unassign(players.bob, { by_player_name = "alice", silent = true })
eq(env.sent, {
{ channel = "exp_roles:assignment_update", data = { name = "bob", unassign = { regular.id } } },
}, "the unassignment is sent to the controller")
check(not regular:has_player(players.bob), "the role is removed locally")
eq(env.events, {
{
name = env.Roles.events.on_player_roles_changed,
tick = 1,
player_index = players.bob.index,
by_player_index = players.alice.index,
assigned = {},
unassigned = { regular.id },
},
}, "the event carries the change")
empty(env.printed, "silent suppresses the announcement")
eq(env.sounds, { "bob:utility/game_lost" }, "the unassign sound plays")
end)
test("reject_assignment() rolls the assignment back", function(env)
local players = setup(env)
local jail = env.R("Jail")
jail:assign(players.alice)
check(jail:has_player(players.alice), "the role applies before rejection")
env.reset_log()
env.Roles.reject_assignment{ name = "alice", role_ids = { jail.id } }
check(not jail:has_player(players.alice), "the rejected role is rolled back")
empty(env.sent, "the rollback is not sent to the controller")
eq(env.events, {
{
name = env.Roles.events.on_player_roles_changed,
tick = 1,
player_index = players.alice.index,
by_player_index = 0,
assigned = {},
unassigned = { jail.id },
},
}, "the rollback raises the event")
local sd = env.script_data()
check(sd.local_players.alice == nil and sd.pending.alice == nil, "the rollback clears the local state")
end)
test(".assign() with local_only never reaches the controller", function(env)
local players = setup(env)
local regular = env.R("Regular")
regular:assign(players.alice, { silent = true, local_only = true })
empty(env.sent, "the assignment is not sent")
check(regular:has_player(players.alice), "the role applies")
local sd = env.script_data()
check(sd.pending.alice == nil, "the role is not pending")
eq(sd.local_players.alice, { regular.id }, "the role is held locally")
env.Roles.receive_assignment_updates{ env.assignment("alice", { "Moderator" }) }
check(regular:has_player(players.alice), "the role survives controller updates")
env.reset_log()
regular:unassign(players.alice, { local_only = true })
check(not regular:has_player(players.alice), "the role is removed")
empty(env.sent, "the removal is not sent")
end)
test(".assign() of a higher priority role suppresses the rest", function(env)
local players = setup(env)
local jail = env.R("Jail")
jail:assign(players.alice, { by_player_name = "<server>", silent = true })
eq(Suite.names(env.Roles.get_player_roles(players.alice)), { "Jail" }, "only the jail role applies")
check(env.R("Moderator"):has_player(players.alice), "a suppressed role is still held")
check(not env.Roles.player_has_permission(players.alice, "exp_scenario.command.kill"), "permissions are lost")
check(not env.Roles.player_has_permission(players.alice, "exp_scenario.gui.readme"), "the default role is lost")
check(not env.Roles.player_outranks(players.alice, players.bob), "a jailed player no longer outranks")
eq(env.events, {
{
name = env.Roles.events.on_player_roles_changed,
tick = 1,
player_index = players.alice.index,
by_player_index = 0,
assigned = { jail.id },
unassigned = {},
},
}, "the event lists only the jail role")
env.reset_log()
jail:unassign(players.alice, { by_player_name = "<server>", silent = true })
eq(Suite.sorted(Suite.names(env.Roles.get_player_roles(players.alice))), { "Moderator", "Player" },
"unjail restores the roles")
eq(env.events, {
{
name = env.Roles.events.on_player_roles_changed,
tick = 1,
player_index = players.alice.index,
by_player_index = 0,
assigned = {},
unassigned = { jail.id },
},
}, "the unjail event lists only the jail role")
end)
test(".assign() ignores the server", function(env)
setup(env)
env.R("Moderator"):assign(env.server)
empty(env.sent, "assigning to the server sends nothing")
empty(env.events, "assigning to the server raises nothing")
end)
return Suite.run()
+99
View File
@@ -0,0 +1,99 @@
--[[-- Test environment for module/control.lua
The module specific extension between the shared stubs and the test files: it
extends each environment with a fresh copy of the roles module and the
fixtures below, and hands each test file a suite built around that.
Run as a chunk by test/lua/runner.js, receiving the shared folder. The suite
returned here becomes `...` in each test file.
]]
local shared_root = ... --- @type string
local source = assert(debug.getinfo(1, "S")).source:gsub("\\", "/")
local plugin_root = assert(source:match("^@(.*)/test/module/env%.lua$"))
local Framework = assert(loadfile(shared_root .. "/framework.lua"))() --- @type Framework
--- Standard fixture: permission names sent once and referenced by zero based index
local permission_names = {
"core.admin", -- 0
"exp_scenario.command.kill", -- 1
"exp_scenario.command.jail", -- 2
"exp_scenario.player.admin", -- 3
"exp_scenario.gui.readme", -- 4
"exp_scenario.command.assign_role", -- 5
}
local function meta(order, extra)
local m = { id = 0, order = order }
for k, v in pairs(extra or {}) do m[k] = v end
return m
end
--- Standard fixture: the roles as the controller would send them on initialise
local function role_records()
return {
{ id = 0, name = "Cluster Admin", permissions = { 0 }, meta = meta(1, { short_hand = "SYS" }) },
{ id = 5, name = "Moderator", permissions = { 1, 2, 3, 5 }, meta = meta(2, { color = { r = 0, g = 170, b = 0 } }) },
{ id = 6, name = "Regular", permissions = { 1 }, meta = meta(3) },
{ id = 7, name = "Jail", permissions = {}, meta = meta(4, { priority = 1, block_auto_assign = true }) },
{ id = 1, name = "Player", permissions = { 4 }, meta = meta(5), is_default = true },
{ id = 9, name = "Gone", permissions = {}, meta = meta(6), is_deleted = true },
}
end
--- Fixture role ids by name, avoiding a search of the roles on every lookup
local role_ids = {}
for _, record in ipairs(role_records()) do
role_ids[record.name] = record.id
end
--- The environment given to each test: the stubs extended with a fresh copy
--- of the roles module and the fixture helpers
--- @class ExpRoles.TestEnv : Stubs
--- @field Roles ExpRoles A fresh copy of the roles module
--- @field R fun(name: string): ExpRoles.Role Get a fixture role by name
--- @field assignment fun(player_name: string, role_names: string[]): { name: string, role_ids: number[], is_deleted: boolean? }
--- @field script_data fun(): ExpRoles.ScriptData The module's script data, for asserting on internal state
--- @field initialise fun(assignments: table[]?) Load the standard fixture and the given assignments
return Framework.suite(function(env)
--- @cast env ExpRoles.TestEnv
env.Roles = assert(loadfile(plugin_root .. "/module/control.lua"))() --- @type ExpRoles
env.Roles.on_server_startup()
--- Get a fixture role by name, failing the test when it does not exist
--- Roles added by a test are found by a search instead
function env.R(name)
local role_id = role_ids[name]
if role_id then
return (assert(env.Roles.get_role(role_id), "No role with id " .. role_id))
end
return (assert(env.Roles.get_role_by_name(name), "No role named " .. name))
end
--- An assignment record as the controller would send it, roles by name
function env.assignment(player_name, role_names)
local ids = {}
for index, role_name in ipairs(role_names) do
ids[index] = assert(role_ids[role_name], "No fixture role named " .. role_name)
end
return { name = player_name, role_ids = ids }
end
--- The module's script data, for asserting on internal state
function env.script_data()
--- @diagnostic disable-next-line: access-invisible
return env.Roles._script_data()
end
--- Load the standard fixture and the given assignments
function env.initialise(assignments)
env.Roles.initialise{
permission_names = permission_names,
roles = role_records(),
assignments = assignments or {},
}
end
return env
end)
+68
View File
@@ -0,0 +1,68 @@
--- Tests for listing and printing to the players who hold a role
local Suite = ... --- @type Suite<ExpRoles.TestEnv> The suite this file adds its tests to, see test/lua/framework.lua
local test, check, eq = Suite.test, Suite.check, Suite.eq
--- alice and dave are moderators with dave offline, zed has never joined
local function setup(env)
local players = {
alice = env.add_player("alice"),
bob = env.add_player("bob"),
dave = env.add_player("dave", false),
}
env.initialise{
env.assignment("alice", { "Moderator" }),
env.assignment("bob", { "Regular" }),
env.assignment("dave", { "Moderator" }),
env.assignment("zed", { "Moderator" }),
}
env.Roles.set_emit_events(true)
return players
end
test(".get_player_names() includes players not on this map", function(env)
setup(env)
eq(Suite.sorted(env.R("Moderator"):get_player_names()), { "alice", "dave", "zed" },
"every player given the role is listed")
end)
test(".get_player_names() counts a player in both lists once", function(env)
local players = setup(env)
env.R("Regular"):assign(players.alice, { silent = true, local_only = true })
env.Roles.receive_assignment_updates{ env.assignment("alice", { "Moderator", "Regular" }) }
local sd = env.script_data()
check(sd.local_players.alice ~= nil and sd.synced_players.alice ~= nil, "the role is held in both lists")
eq(Suite.sorted(env.R("Regular"):get_player_names()), { "alice", "bob" }, "the player is counted once")
end)
test(".get_players() is limited to this map and can be filtered", function(env)
setup(env)
local moderator = env.R("Moderator")
eq(Suite.sorted(Suite.names(moderator:get_players())), { "alice", "dave" }, "players are limited to this map")
eq(Suite.names(moderator:get_players(true)), { "alice" }, "filtered to connected players")
eq(Suite.names(moderator:get_players(false)), { "dave" }, "filtered to offline players")
end)
test(".print() reaches online holders", function(env)
setup(env)
env.reset_log()
eq(env.R("Moderator"):print("hello"), 1, "print returns the number of players reached")
eq(env.printed, { { "hello", to = "alice" } }, "only online holders receive the message")
eq(env.R("Jail"):print("hello"), 0, "a role with no online holders reaches nobody")
end)
test(".print() over get_higher_roles() can repeat", function(env)
local players = setup(env)
env.R("Regular"):assign(players.alice, { silent = true, local_only = true })
env.reset_log()
for _, role in ipairs(env.Roles.get_higher_roles(env.R("Regular"))) do
role:print("hello")
end
local got = {}
for _, entry in ipairs(env.printed) do got[#got + 1] = entry.to end
-- alice holds two of the roles, so like the legacy system the message can repeat
eq(Suite.sorted(got), { "alice", "alice", "bob" }, "the online holders of each role are reached")
end)
return Suite.run()
+71
View File
@@ -0,0 +1,71 @@
--- Tests for the role lookup functions of module/control.lua
local Suite = ... --- @type Suite<ExpRoles.TestEnv> The suite this file adds its tests to, see test/lua/framework.lua
local test, check, eq = Suite.test, Suite.check, Suite.eq
test("get_role() returns the role with the given clusterio id", function(env)
env.initialise{}
check(env.Roles.get_role(env.R("Regular").id) == env.R("Regular"), "the role is found by id")
eq(env.Roles.get_role(9), nil, "deleted roles are not known")
end)
test("get_role_by_name() searches the roles", function(env)
env.initialise{}
check(env.Roles.get_role_by_name("Moderator") == env.R("Moderator"), "the role is found by name")
eq(env.Roles.get_role_by_name("Gone"), nil, "deleted roles are not known")
end)
test("get_roles() returns every role", function(env)
env.initialise{}
eq(Suite.sorted(Suite.names(env.Roles.get_roles())),
{ "Cluster Admin", "Jail", "Moderator", "Player", "Regular" },
"every role is returned with deleted roles skipped")
end)
test("get_ordered_roles() returns the most privileged first", function(env)
env.initialise{}
eq(Suite.names(env.Roles.get_ordered_roles()), { "Cluster Admin", "Moderator", "Regular", "Jail", "Player" },
"roles follow their order")
end)
test("sort_roles() orders most privileged first and breaks ties on the id", function(env)
env.initialise{}
local sorted = env.Roles.sort_roles{ env.R("Player"), env.R("Cluster Admin"), env.R("Regular") }
eq(Suite.names(sorted), { "Cluster Admin", "Regular", "Player" }, "the list is sorted in place")
-- A role with the same order as Regular but a higher id sorts after it
env.Roles.receive_role_updates{
{ id = 10, name = "Twin", permissions = {}, meta = { id = 0, order = env.R("Regular").order } },
}
local tied = env.Roles.sort_roles{ env.R("Twin"), env.R("Regular") }
eq(Suite.names(tied), { "Regular", "Twin" }, "order ties break on the id")
end)
test("get_default_role() follows is_default", function(env)
env.initialise{}
check(env.Roles.get_default_role() == env.R("Player"), "the default role is known")
end)
test("get_higher_roles() and get_lower_roles() include the role and exclude the default", function(env)
env.initialise{}
eq(Suite.sorted(Suite.names(env.Roles.get_higher_roles(env.R("Regular")))),
{ "Cluster Admin", "Moderator", "Regular" }, "higher roles")
eq(Suite.sorted(Suite.names(env.Roles.get_lower_roles(env.R("Regular")))),
{ "Jail", "Regular" }, "lower roles")
end)
test(".is_higher_than() and .is_lower_than() compare on order", function(env)
env.initialise{}
check(env.R("Moderator"):is_higher_than(env.R("Player")), "a lower order is more privileged")
check(env.R("Player"):is_lower_than(env.R("Moderator")), "is_lower_than is the reverse")
check(not env.R("Moderator"):is_higher_than(env.R("Moderator")), "a role is not higher than itself")
end)
test("initialise() decodes the role records", function(env)
env.initialise{}
eq(env.R("Cluster Admin").short_hand, "SYS", "short hand decoded")
eq(env.R("Moderator").short_hand, "Moderator", "short hand falls back to the name")
eq(env.R("Moderator").color, { r = 0, g = 170, b = 0 }, "color decoded")
check(env.R("Jail").priority == 1 and env.R("Jail").block_auto_assign, "priority and block auto assign decoded")
end)
return Suite.run()
+95
View File
@@ -0,0 +1,95 @@
--- Tests for the player checks of module/control.lua
local Suite = ... --- @type Suite<ExpRoles.TestEnv> The suite this file adds its tests to, see test/lua/framework.lua
local test, check, eq = Suite.test, Suite.check, Suite.eq
--- alice is a moderator, bob is a regular, and carol has only the default role
local function setup(env)
local players = {
alice = env.add_player("alice"),
bob = env.add_player("bob"),
carol = env.add_player("carol"),
}
env.initialise{
env.assignment("alice", { "Moderator" }),
env.assignment("bob", { "Regular" }),
}
return players
end
test("get_player_roles() includes the default role", function(env)
local players = setup(env)
eq(Suite.sorted(Suite.names(env.Roles.get_player_roles(players.alice))), { "Moderator", "Player" },
"held roles and the default role")
eq(Suite.names(env.Roles.get_player_roles(players.carol)), { "Player" },
"a player with no roles has the default role")
end)
test("get_player_roles() treats nil and index 0 as the server", function(env)
setup(env)
eq(Suite.names(env.Roles.get_player_roles(nil)), { "<server>" }, "nil is the server")
eq(Suite.names(env.Roles.get_player_roles(env.server)), { "<server>" }, "index 0 is the server")
check(env.Roles.player_has_permission(nil, "anything.at.all"), "the server has every permission")
check(not env.R("Player"):has_player(nil), "the server does not have the default role")
end)
test("get_player_highest_role() returns the most privileged role", function(env)
local players = setup(env)
check(env.Roles.get_player_highest_role(players.alice) == env.R("Moderator"), "the highest held role")
check(env.Roles.get_player_highest_role(players.carol) == env.R("Player"), "the default role when none are held")
end)
test("get_player_highest_role() errors when a player has no roles", function(env)
local players = setup(env)
env.Roles.receive_role_updates{
{ id = env.R("Player").id, name = "Player", permissions = {}, meta = { id = 0, order = 5 }, is_deleted = true },
}
Suite.throws(function()
env.Roles.get_player_highest_role(players.carol)
end, "Player has no roles", "no default role and no held roles errors")
end)
test("player_has_permission()", function(env)
local players = setup(env)
check(env.Roles.player_has_permission(players.alice, "exp_scenario.command.kill"), "granted by a held role")
check(env.Roles.player_has_permission(players.alice, "exp_scenario.gui.readme"), "granted by the default role")
check(not env.Roles.player_has_permission(players.bob, "exp_scenario.command.jail"), "not granted")
end)
test("player_has_any_permission() and player_has_all_permission()", function(env)
local players = setup(env)
local jail, kill = "exp_scenario.command.jail", "exp_scenario.command.kill"
check(env.Roles.player_has_any_permission(players.bob, jail, kill), "any passes when one is granted")
check(not env.Roles.player_has_any_permission(players.bob, jail, "exp_scenario.command.assign_role"),
"any fails when none are granted")
check(not env.Roles.player_has_any_permission(players.bob), "any of none is false")
check(env.Roles.player_has_all_permission(players.alice, jail, kill), "all passes when every one is granted")
check(not env.Roles.player_has_all_permission(players.bob, jail, kill), "all fails when one is missing")
check(env.Roles.player_has_all_permission(players.bob), "all of none is true")
check(env.Roles.player_has_any_permission(nil, "anything.at.all"), "the server passes any")
check(env.Roles.player_has_all_permission(nil, "anything.at.all", "and.this.too"), "the server passes all")
end)
test(".has_permission()", function(env)
setup(env)
check(env.R("Moderator"):has_permission("exp_scenario.command.kill"), "granted")
check(not env.R("Regular"):has_permission("exp_scenario.command.jail"), "not granted")
check(env.R("Cluster Admin"):has_permission("anything.at.all"), "core.admin grants everything")
end)
test(".has_player()", function(env)
local players = setup(env)
check(env.R("Moderator"):has_player(players.alice), "a held role")
check(not env.R("Moderator"):has_player(players.bob), "a role not held")
check(env.R("Player"):has_player(players.carol), "everyone has the default role")
end)
test("player_outranks()", function(env)
local players = setup(env)
check(env.Roles.player_outranks(players.alice, players.bob), "a higher role outranks a lower one")
check(not env.Roles.player_outranks(players.bob, players.alice), "a lower role does not outrank a higher one")
check(not env.Roles.player_outranks(players.alice, players.alice), "nobody outranks themselves")
check(env.Roles.player_outranks(nil, players.alice), "the server outranks everyone")
check(not env.Roles.player_outranks(players.alice, nil), "nobody outranks the server")
end)
return Suite.run()
+174
View File
@@ -0,0 +1,174 @@
--- Tests for the sync entry points of module/control.lua
local Suite = ... --- @type Suite<ExpRoles.TestEnv> The suite this file adds its tests to, see test/lua/framework.lua
local test, check, eq, empty = Suite.test, Suite.check, Suite.eq, Suite.empty
--- The state given by the admin trigger, reset by setup for every test
local admin_state = {} --- @type table<string, boolean>
--- alice is a moderator and carol has only the default role, both connected
local function setup(env)
local players = {
alice = env.add_player("alice"),
carol = env.add_player("carol"),
}
admin_state = {}
env.Roles.define_permission_trigger("exp_scenario.player.admin", function(player, state)
admin_state[player.name] = state
end)
env.initialise{
env.assignment("alice", { "Moderator" }),
env.assignment("zed", { "Moderator", "Regular" }), -- has never joined this map
}
env.Roles.set_emit_events(true)
return players
end
test("initialise() applies triggers and raises empty events", function(env)
setup(env)
eq(admin_state, { alice = true, carol = false }, "triggers run for connected players")
eq(#env.events, 2, "the event is raised once per connected player")
for _, event in ipairs(env.events) do
empty(event.assigned, "the events assign nothing")
empty(event.unassigned, "the events unassign nothing")
end
empty(env.printed, "initialise is silent")
empty(env.sent, "initialise sends nothing")
empty(env.sounds, "initialise plays no sounds")
end)
test("initialise() skips deleted assignments", function(env)
local players = setup(env)
local record = env.assignment("carol", { "Moderator" })
record.is_deleted = true
env.initialise{ record }
check(not env.R("Moderator"):has_player(players.carol), "a deleted assignment is ignored")
end)
test("initialise() is authoritative for pending roles", function(env)
local players = setup(env)
env.R("Moderator"):assign(players.carol)
env.R("Jail"):assign(players.carol, { silent = true, local_only = true })
local sd = env.script_data()
eq(sd.pending.carol, { env.R("Moderator").id }, "the role is pending before initialise")
env.reset_log()
env.initialise{ env.assignment("alice", { "Moderator" }) }
eq(sd.pending.carol, nil, "pending roles are cleared")
check(not env.R("Moderator"):has_player(players.carol), "an unconfirmed role is given up")
eq(sd.local_players.carol, { env.R("Jail").id }, "local only roles are kept")
empty(env.sent, "initialise sends nothing")
end)
test("receive_assignment_updates() applies controller changes", function(env)
local players = setup(env)
env.reset_log()
env.Roles.receive_assignment_updates{ env.assignment("carol", { "Moderator" }) }
check(env.R("Moderator"):has_player(players.carol), "the assignment applies")
eq(env.events, {
{
name = env.Roles.events.on_player_roles_changed,
tick = 1,
player_index = players.carol.index,
by_player_index = 0,
assigned = { env.R("Moderator").id },
unassigned = {},
},
}, "the event carries the change with no by player")
eq(env.printed, {
{ "exp-roles.game-message-assign", "carol", "Moderator", "<server>" },
}, "the change is announced by the server")
check(admin_state.carol == true, "triggers follow the assignment")
env.reset_log()
env.Roles.receive_assignment_updates{ { name = "carol", role_ids = {}, is_deleted = true } }
check(not env.R("Moderator"):has_player(players.carol), "a removal applies")
eq(env.events, {
{
name = env.Roles.events.on_player_roles_changed,
tick = 1,
player_index = players.carol.index,
by_player_index = 0,
assigned = {},
unassigned = { env.R("Moderator").id },
},
}, "the removal raises the event")
check(admin_state.carol == false, "triggers follow the removal")
end)
test("receive_assignment_updates() for players not on this map raises nothing", function(env)
setup(env)
env.reset_log()
env.Roles.receive_assignment_updates{ env.assignment("zed", { "Moderator" }) }
empty(env.events, "no event is raised")
empty(env.printed, "nothing is announced")
end)
test("receive_role_updates() applies to the holders", function(env)
setup(env)
env.reset_log()
local regular_id = env.R("Regular").id
env.Roles.receive_role_updates{
{ id = regular_id, name = "Regular", permissions = { "exp_scenario.command.jail" }, meta = { id = 0, order = 3 } },
}
check(env.R("Regular"):has_permission("exp_scenario.command.jail"), "the permission change applies")
eq(#env.events, 2, "a role change raises for every connected player")
for _, event in ipairs(env.events) do
empty(event.assigned, "role change events are empty")
end
env.Roles.receive_role_updates{
{ id = regular_id, name = "Regular", permissions = {}, meta = { id = 0, order = 3 }, is_deleted = true },
}
eq(env.Roles.get_role(regular_id), nil, "a deleted role is removed")
end)
test("receive_role_updates() moves the default role", function(env)
local players = setup(env)
env.Roles.receive_role_updates{
{ id = env.R("Player").id, name = "Player", permissions = {}, meta = { id = 0, order = 5 } },
{ id = 8, name = "Guest", permissions = {}, meta = { id = 0, order = 7 }, is_default = true },
}
check(env.Roles.get_default_role() == env.R("Guest"), "the new default role is known")
eq(Suite.names(env.Roles.get_player_roles(players.carol)), { "Guest" }, "players pick up the new default")
end)
test("set_emit_events() gates what is sent and defaults to enabled", function(env)
local players = setup(env)
env.Roles.set_emit_events(false)
env.reset_log()
env.R("Regular"):assign(players.alice)
empty(env.sent, "nothing is sent while disabled")
check(env.R("Regular"):has_player(players.alice), "the assignment still applies locally")
env.Roles.set_emit_events()
env.R("Jail"):assign(players.alice, { silent = true })
eq(env.sent, {
{ channel = "exp_roles:assignment_update", data = { name = "alice", assign = { env.R("Jail").id } } },
}, "no argument enables sending")
end)
test("on_load() restores the state after a save and load", function(env)
local players = setup(env)
env.R("Jail"):assign(players.carol, { silent = true, local_only = true })
env.save_load()
env.Roles.on_load()
check(env.R("Moderator"):is_higher_than(env.R("Regular")), "role methods survive through the registered metatable")
check(env.R("Moderator"):has_player(players.alice), "synced roles survive")
check(env.R("Jail"):has_player(players.carol), "local roles survive")
check(env.Roles.player_has_permission(players.alice, "exp_scenario.command.kill"), "permission checks still answer")
end)
test("on_player_joined_game() runs the triggers", function(env)
local players = setup(env)
admin_state.alice = nil
env.Roles.on_player_joined_game{
name = defines.events.on_player_joined_game,
tick = game.tick,
player_index = players.alice.index,
}
check(admin_state.alice ~= nil, "triggers run when a player joins")
end)
return Suite.run()
+62
View File
@@ -0,0 +1,62 @@
"use strict";
const t = require("tap");
const lib = require("@clusterio/lib");
const { seedRoles, flattenSeedPermissions } = require("../dist/node/seed");
// Importing this defines the exp_scenario permissions the seed grants
require("@expcluster/scenario/dist/node/permissions");
t.test("seedRoles[] grant only defined permissions", t2 => {
for (const role of seedRoles) {
for (const permission of role.permissions) {
t2.ok(lib.permissions.has(permission), `${role.name} grants defined permission ${permission}`);
}
}
t2.end();
});
t.test("seedRoles[] are unique with one default and one admin", t2 => {
const names = seedRoles.map(role => role.name);
t2.strictSame(names.length, new Set(names).size, "names are unique");
t2.strictSame(seedRoles.filter(role => role.isDefault).length, 1, "one default role");
t2.strictSame(seedRoles.filter(role => role.isAdmin).length, 1, "one admin role");
t2.end();
});
t.test("flattenSeedPermissions() inherits permissions from parent roles", t2 => {
const byName = new Map(seedRoles.map(role => [role.name, role]));
for (const role of seedRoles) {
if (role.parent === undefined) {
continue;
}
const parent = byName.get(role.parent);
t2.ok(parent, `${role.name} has parent ${role.parent}`);
const flattened = flattenSeedPermissions(role);
for (const permission of flattenSeedPermissions(parent)) {
t2.ok(flattened.has(permission), `${role.name} inherits ${permission}`);
}
}
t2.end();
});
t.test("flattenSeedPermissions() does not inherit permissions when parent is undefined", t2 => {
for (const role of seedRoles) {
if (role.parent !== undefined) {
continue;
}
const flattened = flattenSeedPermissions(role);
for (const permission of role.permissions) {
t2.ok(flattened.has(permission), `${role.name} contains ${permission}`);
}
t2.equal(
flattened.size,
role.permissions.length,
`${role.name} has no inherited permissions`,
);
}
t2.end();
});
+33
View File
@@ -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 { 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>}
/>;
}
+2
View File
@@ -5,6 +5,7 @@ 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);
@@ -14,6 +15,7 @@ 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,26 +1,28 @@
--[[-- Command Authorities - Roles --[[-- Command Authorities - Roles
Adds a permission authority for exp roles Adds a permission authority which checks the clusterio permission for a command
]] ]]
local Commands = require("modules/exp_commands") local Commands = require("modules/exp_commands")
local add, allow, deny = Commands.add_permission_authority, Commands.status.success, Commands.status.unauthorised local add, allow, deny = Commands.add_permission_authority, Commands.status.success, Commands.status.unauthorised
local Roles = require("modules/exp_legacy/expcore/roles") local Roles = require("modules/exp_roles")
local player_allowed = Roles.player_allowed local player_has_permission = Roles.player_has_permission
local authorities = {} local authorities = {}
--- If a command has the flag "character_only" then the command can only be used outside of remote view --- Every command requires the permission `exp_scenario.command.<name>`, with
--- hyphens replaced by underscores, see permissions.ts for their definitions
authorities.exp_permission = authorities.exp_permission =
add(function(player, command) add(function(player, command)
if not player_allowed(player, command.flags.exp_permission or ("command/" .. command.name)) then local permission = "exp_scenario.command." .. command.name:gsub("%-", "_")
if not player_has_permission(player, permission) then
return deny{ "exp-commands-authorities_role.deny" } return deny{ "exp-commands-authorities_role.deny" }
else else
return allow() return allow()
end end
end) end)
Roles.define_flag_trigger("is_system", function(player, state) Roles.define_permission_trigger("core.admin", function(player, state)
if state then if state then
Commands.unlock_system_commands(player.name) Commands.unlock_system_commands(player.name)
else else
+1 -1
View File
@@ -8,6 +8,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("Group", require("modules.exp_legacy.expcore.permission_groups"))
add_static("Roles", require("modules.exp_legacy.expcore.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"))
+26 -21
View File
@@ -1,4 +1,3 @@
--[[-- Command Types - Roles --[[-- Command Types - Roles
The data types that are used with exp_roles The data types that are used with exp_roles
A lower role index indicates it is more privileged A lower role index indicates it is more privileged
@@ -11,28 +10,43 @@ Adds parsers for:
lower_role_player_alive lower_role_player_alive
]] ]]
local ExpUtil = require("modules/exp_util")
local auto_complete = ExpUtil.auto_complete
local Commands = require("modules/exp_commands") local Commands = require("modules/exp_commands")
local add, parse = Commands.add_data_type, Commands.parse_input local add, parse = Commands.add_data_type, Commands.parse_input
local valid, invalid = Commands.status.success, Commands.status.invalid_input local valid, invalid = Commands.status.success, Commands.status.invalid_input
local Roles = require("modules.exp_legacy.expcore.roles") local Roles = require("modules/exp_roles")
local highest_role = Roles.get_player_highest_role local player_outranks = Roles.player_outranks
local player_has_permission = Roles.player_has_permission
local types = {} --- @class (partial) Commands.types local types = {} --- @class (partial) Commands.types
--- A role defined by exp roles --- A role known to exp roles, matched on its name
types.role = add("role", Commands.types.key_of(Roles.config.roles)) types.role =
add("role", function(input)
local names = {}
for index, role in ipairs(Roles.get_ordered_roles()) do
names[index] = role.name
end
local name = auto_complete(names, input)
if name == nil then
return invalid{ "exp-commands-parse.string-options", table.concat(names, ", ") }
else
return valid(Roles.get_role_by_name(name))
end
end)
--- A role which is lower than the players highest role --- A role which is lower than the players highest role
types.lower_role = types.lower_role =
add("lower_role", function(input, player) add("lower_role", function(input, player)
local success, status, result = parse(input, player, types.role) local success, status, result = parse(input, player, types.role)
if not success then return status, result end if not success then return status, result end
--- @cast result any TODO role is not a defined type --- @cast result ExpRoles.Role
local player_highest = highest_role(player) if not player_has_permission(player, "core.admin") and not Roles.get_player_highest_role(player):is_higher_than(result) then
local is_root = Roles.config.internal.root == player_highest.name
if not is_root and player_highest.index >= result.index then
return invalid{ "exp-commands-parse_role.lower-role" } return invalid{ "exp-commands-parse_role.lower-role" }
else else
return valid(result) return valid(result)
@@ -46,10 +60,7 @@ types.lower_role_player =
if not success then return status, result end if not success then return status, result end
--- @cast result LuaPlayer --- @cast result LuaPlayer
local other_highest = highest_role(result) if not player_outranks(player, result) then
local player_highest = highest_role(player)
local is_root = Roles.config.internal.root == player_highest.name
if not is_root and player_highest.index >= other_highest.index then
return invalid{ "exp-commands-parse_role.lower-role-player" } return invalid{ "exp-commands-parse_role.lower-role-player" }
else else
return valid(result) return valid(result)
@@ -63,10 +74,7 @@ types.lower_role_player_online =
if not success then return status, result end if not success then return status, result end
--- @cast result LuaPlayer --- @cast result LuaPlayer
local other_highest = highest_role(result) if not player_outranks(player, result) then
local player_highest = highest_role(player)
local is_root = Roles.config.internal.root == player_highest.name
if not is_root and player_highest.index >= other_highest.index then
return invalid{ "exp-commands-parse_role.lower-role-player" } return invalid{ "exp-commands-parse_role.lower-role-player" }
else else
return valid(result) return valid(result)
@@ -80,10 +88,7 @@ types.lower_role_player_alive =
if not success then return status, result end if not success then return status, result end
--- @cast result LuaPlayer --- @cast result LuaPlayer
local other_highest = highest_role(result) if not player_outranks(player, result) then
local player_highest = highest_role(player)
local is_root = Roles.config.internal.root == player_highest.name
if not is_root and player_highest.index >= other_highest.index then
return invalid{ "exp-commands-parse_role.lower-role-player" } return invalid{ "exp-commands-parse_role.lower-role-player" }
else else
return valid(result) return valid(result)
+3 -3
View File
@@ -4,8 +4,8 @@ Adds a command that allows players to kill themselves and others
local Commands = require("modules/exp_commands") local Commands = require("modules/exp_commands")
local Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles local Roles = require("modules/exp_roles")
local highest_role = Roles.get_player_highest_role local player_outranks = Roles.player_outranks
--- Kills yourself or another player. --- Kills yourself or another player.
Commands.new("kill", { "exp-commands_kill.description" }) Commands.new("kill", { "exp-commands_kill.description" })
@@ -20,7 +20,7 @@ Commands.new("kill", { "exp-commands_kill.description" })
if other_player == nil then if other_player == nil then
-- Can only be nil if the target is the player and they are already dead -- Can only be nil if the target is the player and they are already dead
return Commands.status.error{ "exp-commands_kill.already-dead" } return Commands.status.error{ "exp-commands_kill.already-dead" }
elseif (other_player == player) or (highest_role(player).index < highest_role(other_player).index) then elseif other_player == player or player_outranks(player, other_player) then
-- You can always kill yourself or can kill lower role players -- You can always kill yourself or can kill lower role players
if script.active_mods["space-age"] then if script.active_mods["space-age"] then
other_player.surface.create_entity{ name = "lightning", position = { other_player.position.x, other_player.position.y - 16 }, target = other_player.character } other_player.surface.create_entity{ name = "lightning", position = { other_player.position.x, other_player.position.y - 16 }, target = other_player.character }
@@ -11,7 +11,7 @@ local expand_area = AABB.expand
local Commands = require("modules/exp_commands") local Commands = require("modules/exp_commands")
local format_player_name = Commands.format_player_name_locale local format_player_name = Commands.format_player_name_locale
local Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles local Roles = require("modules/exp_roles")
local EntityProtection = require("modules.exp_legacy.modules.control.protection") --- @dep modules.control.protection local EntityProtection = require("modules.exp_legacy.modules.control.protection") --- @dep modules.control.protection
local format_string = string.format local format_string = string.format
@@ -218,13 +218,16 @@ SelectEntities:on_stop(on_player_selection_stop)
--- When there is a repeat offence print it in chat --- When there is a repeat offence print it in chat
local function on_repeat_violation(event) local function on_repeat_violation(event)
Roles.print_to_roles_higher("Regular", { local regular = Roles.get_role_by_name("Regular")
"exp-commands_entity-protection.repeat-offence", for _, role in ipairs(regular and Roles.get_higher_roles(regular) or {}) do
format_player_name(event.player_index), role:print{
event.entity.localised_name, "exp-commands_entity-protection.repeat-offence",
event.entity.position.x, format_player_name(event.player_index),
event.entity.position.y event.entity.localised_name,
}) event.entity.position.x,
event.entity.position.y
}
end
end end
return { return {
+3 -3
View File
@@ -6,8 +6,8 @@ local Commands = require("modules/exp_commands")
local format_player_name = Commands.format_player_name_locale local format_player_name = Commands.format_player_name_locale
local parse_input = Commands.parse_input local parse_input = Commands.parse_input
local Roles = require("modules.exp_legacy.expcore.roles") local Roles = require("modules/exp_roles")
local player_has_flag = Roles.player_has_flag local player_has_permission = Roles.player_has_permission
local Reports = require("modules.exp_legacy.modules.control.reports") --- @dep modules.control.reports local Reports = require("modules.exp_legacy.modules.control.reports") --- @dep modules.control.reports
@@ -20,7 +20,7 @@ local function reportable_player(input, player)
if not success then return status, result end if not success then return status, result end
--- @cast result LuaPlayer --- @cast result LuaPlayer
if player_has_flag(input, "report-immune") then if player_has_permission(input, "exp_scenario.bypass.reports") then
return Commands.status.invalid_input{ "exp-commands_reports.player-immune" } return Commands.status.invalid_input{ "exp-commands_reports.player-immune" }
elseif player == input then elseif player == input then
return Commands.status.invalid_input{ "exp-commands_reports.self-report" } return Commands.status.invalid_input{ "exp-commands_reports.self-report" }
+9 -9
View File
@@ -6,8 +6,8 @@ local Commands = require("modules/exp_commands")
local format_player_name = Commands.format_player_name_locale local format_player_name = Commands.format_player_name_locale
local format_text = Commands.format_rich_text_color_locale local format_text = Commands.format_rich_text_color_locale
local Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles local Roles = require("modules/exp_roles")
local get_roles_ordered = Roles.get_roles_ordered local get_ordered_roles = Roles.get_ordered_roles
local get_player_roles = Roles.get_player_roles local get_player_roles = Roles.get_player_roles
--- Assigns a role to a player --- Assigns a role to a player
@@ -18,8 +18,8 @@ Commands.new("assign-role", { "exp-commands_roles.description-assign" })
:add_flags{ "admin_only" } :add_flags{ "admin_only" }
:register(function(player, other_player, role) :register(function(player, other_player, role)
--- @cast other_player LuaPlayer --- @cast other_player LuaPlayer
--- @cast role any -- TODO --- @cast role ExpRoles.Role
Roles.assign_player(other_player, role, player.name) role:assign(other_player, { by_player_name = player.name })
end) end)
--- Unassigns a role to a player --- Unassigns a role to a player
@@ -30,8 +30,8 @@ Commands.new("unassign-role", { "exp-commands_roles.description-unassign" })
:add_flags{ "admin_only" } :add_flags{ "admin_only" }
:register(function(player, other_player, role) :register(function(player, other_player, role)
--- @cast other_player LuaPlayer --- @cast other_player LuaPlayer
--- @cast role any -- TODO --- @cast role ExpRoles.Role
Roles.unassign_player(other_player, role, player.name) role:unassign(other_player, { by_player_name = player.name })
end) end)
--- Lists all roles in they correct order --- Lists all roles in they correct order
@@ -40,17 +40,17 @@ Commands.new("get-roles", { "exp-commands_roles.description-get" })
:add_aliases{ "roles" } :add_aliases{ "roles" }
:register(function(player, other_player) :register(function(player, other_player)
--- @cast other_player LuaPlayer? --- @cast other_player LuaPlayer?
local roles = get_roles_ordered() local roles = get_ordered_roles()
local roles_formatted = { "" } --- @type LocalisedString local roles_formatted = { "" } --- @type LocalisedString
local response = { "exp-commands_roles.list-roles", roles_formatted } --[[@as LocalisedString]] local response = { "exp-commands_roles.list-roles", roles_formatted } --[[@as LocalisedString]]
if other_player then if other_player then
roles = get_player_roles(other_player) roles = Roles.sort_roles(get_player_roles(other_player))
response[1] = "exp-commands_roles.list-player" response[1] = "exp-commands_roles.list-player"
response[3] = format_player_name(other_player) response[3] = format_player_name(other_player)
end end
for index, role in ipairs(roles) do for index, role in ipairs(roles) do
local role_name = format_text(role.name, role.custom_color or Commands.color.white) local role_name = format_text(role.name, role.color or Commands.color.white)
roles_formatted[index + 1] = { "exp-commands_roles.list-element", role_name } roles_formatted[index + 1] = { "exp-commands_roles.list-element", role_name }
end end
+3 -3
View File
@@ -7,8 +7,8 @@ local teleport_player = ExpUtil.teleport_player
local Commands = require("modules/exp_commands") local Commands = require("modules/exp_commands")
local Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles local Roles = require("modules/exp_roles")
local player_allowed = Roles.player_allowed local player_has_permission = Roles.player_has_permission
--- @class ExpCommands_Teleport.commands --- @class ExpCommands_Teleport.commands
local commands = {} local commands = {}
@@ -85,7 +85,7 @@ commands.spawn = Commands.new("spawn", { "exp-commands_teleport.description-spaw
if not teleport_player(player, game.surfaces.nauvis, { 0, 0 }, "dismount") then if not teleport_player(player, game.surfaces.nauvis, { 0, 0 }, "dismount") then
return Commands.status.error{ "exp-commands_teleport.unavailable" } return Commands.status.error{ "exp-commands_teleport.unavailable" }
end end
elseif player_allowed(player, "command/spawn/always") then elseif player_has_permission(player, "exp_scenario.command.spawn.always") then
if not teleport_player(other_player, game.surfaces.nauvis, { 0, 0 }, "dismount") then if not teleport_player(other_player, game.surfaces.nauvis, { 0, 0 }, "dismount") then
return Commands.status.error{ "exp-commands_teleport.unavailable" } return Commands.status.error{ "exp-commands_teleport.unavailable" }
end end
+1
View File
@@ -63,6 +63,7 @@ add(require("modules/exp_scenario/control/pollution_grading"))
add(require("modules/exp_scenario/control/protection_jail")) add(require("modules/exp_scenario/control/protection_jail"))
add(require("modules/exp_scenario/control/report_jail")) add(require("modules/exp_scenario/control/report_jail"))
add(require("modules/exp_scenario/control/research")) add(require("modules/exp_scenario/control/research"))
add(require("modules/exp_scenario/control/roles"))
add(require("modules/exp_scenario/control/spawn_area")) add(require("modules/exp_scenario/control/spawn_area"))
add(require("modules/exp_scenario/control/station_auto_name")) add(require("modules/exp_scenario/control/station_auto_name"))
+2 -2
View File
@@ -4,7 +4,7 @@ Various bonus related event handlers
TODO Refactor this fully, this is temp to get it out of the player bonus gui file TODO Refactor this fully, this is temp to get it out of the player bonus gui file
]] ]]
local Roles = require("modules/exp_legacy/expcore/roles") local Roles = require("modules/exp_roles")
local config = require("modules/exp_legacy/config/bonus") local config = require("modules/exp_legacy/config/bonus")
--- @param event EventData.on_force_created --- @param event EventData.on_force_created
@@ -26,7 +26,7 @@ end
--- @param event EventData.on_player_died --- @param event EventData.on_player_died
local function fast_respawn(event) local function fast_respawn(event)
local player = assert(game.get_player(event.player_index)) local player = assert(game.get_player(event.player_index))
if Roles.player_has_flag(player, "instant-respawn") then if Roles.player_has_permission(player, "exp_scenario.player.instant_respawn") then
player.ticks_to_respawn = 120 player.ticks_to_respawn = 120
end end
end end
@@ -2,7 +2,7 @@
Adds auto replies to chat messages, as well as chat commands Adds auto replies to chat messages, as well as chat commands
]] ]]
local Roles = require("modules.exp_legacy.expcore.roles") local Roles = require("modules/exp_roles")
local config = require("modules.exp_legacy.config.chat_reply") local config = require("modules.exp_legacy.config.chat_reply")
local prefix = config.command_prefix local prefix = config.command_prefix
local prefix_len = string.len(prefix) local prefix_len = string.len(prefix)
@@ -20,7 +20,7 @@ local function on_console_chat(event)
-- Check if the player can chat commands -- Check if the player can chat commands
local commands_allowed = true local commands_allowed = true
if config.command_admin_only and not player.admin then commands_allowed = false end if config.command_admin_only and not player.admin then commands_allowed = false end
if config.command_permission and not Roles.player_allowed(player, config.command_permission) then commands_allowed = false end if config.command_permission and not Roles.player_has_permission(player, config.command_permission) then commands_allowed = false end
-- Check if a key word appears in the message -- Check if a key word appears in the message
for key_word, reply in pairs(config.messages) do for key_word, reply in pairs(config.messages) do
@@ -3,7 +3,7 @@ Log certain actions into a file when events are triggered
]] ]]
local ExpUtil = require("modules/exp_util") local ExpUtil = require("modules/exp_util")
local Roles = require("modules.exp_legacy.expcore.roles") local Roles = require("modules/exp_roles")
local config = require("modules.exp_legacy.config.deconlog") local config = require("modules.exp_legacy.config.deconlog")
local seconds_time_format = ExpUtil.format_time_factory{ format = "short", hours = true, minutes = true, seconds = true } local seconds_time_format = ExpUtil.format_time_factory{ format = "short", hours = true, minutes = true, seconds = true }
@@ -79,7 +79,7 @@ end
local function get_log_player(event) local function get_log_player(event)
local player = assert(game.get_player(event.player_index)) local player = assert(game.get_player(event.player_index))
if Roles.player_has_flag(player, "deconlog-bypass") then if Roles.player_has_permission(player, "exp_scenario.bypass.deconstruction_log") then
return nil return nil
end end
@@ -4,7 +4,7 @@ Makes trees which are marked for decon "decay" quickly to allow faster building
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local Async = require("modules/exp_util/async") local Async = require("modules/exp_util/async")
local Roles = require("modules.exp_legacy.expcore.roles") local Roles = require("modules/exp_roles")
local PlayerData = require("modules.exp_legacy.expcore.player_data") local PlayerData = require("modules.exp_legacy.expcore.player_data")
local HasEnabledDecon = PlayerData.Settings:combine("HasEnabledDecon") local HasEnabledDecon = PlayerData.Settings:combine("HasEnabledDecon")
@@ -57,9 +57,9 @@ local remove_trees_async =
--- @param player LuaPlayer --- @param player LuaPlayer
--- @return "fast" | "allow" | "disallow" --- @return "fast" | "allow" | "disallow"
local function get_permission(player) local function get_permission(player)
if Roles.player_allowed(player, "fast-tree-decon") then if Roles.player_has_permission(player, "exp_scenario.decon.fast_trees") then
return HasEnabledDecon:get(player) and "fast" or "allow" return HasEnabledDecon:get(player) and "fast" or "allow"
elseif Roles.player_allowed(player, "standard-decon") then elseif Roles.player_has_permission(player, "exp_scenario.decon.standard") then
return "allow" return "allow"
else else
return "disallow" return "disallow"
@@ -101,7 +101,7 @@ Gui.toolbar.create_button{
tooltip = { "exp_fast-decon.tooltip-main" }, tooltip = { "exp_fast-decon.tooltip-main" },
auto_toggle = true, auto_toggle = true,
visible = function(player, _) visible = function(player, _)
return Roles.player_allowed(player, "fast-tree-decon") return Roles.player_has_permission(player, "exp_scenario.decon.fast_trees")
end end
}:on_click(function(def, player, element) }:on_click(function(def, player, element)
local state = Gui.toolbar.get_button_toggled_state(def, player) local state = Gui.toolbar.get_button_toggled_state(def, player)
@@ -3,7 +3,7 @@ Disable new players from having certain items in their inventory, most commonly
]] ]]
local ExpUtil = require("modules/exp_util") local ExpUtil = require("modules/exp_util")
local Roles = require("modules.exp_legacy.expcore.roles") local Roles = require("modules/exp_roles")
local config = require("modules.exp_legacy.config.nukeprotect") local config = require("modules.exp_legacy.config.nukeprotect")
--- Check all items in the given inventory --- Check all items in the given inventory
@@ -12,14 +12,14 @@ local config = require("modules.exp_legacy.config.nukeprotect")
--- @param banned_items string[] --- @param banned_items string[]
local function check_items(player, type, banned_items) local function check_items(player, type, banned_items)
-- If the player has perms to be ignored, then they should be -- If the player has perms to be ignored, then they should be
if config.ignore_permission and Roles.player_allowed(player, config.ignore_permission) then return end if config.ignore_permission and Roles.player_has_permission(player, config.ignore_permission) then return end
if config.ignore_admins and player.admin then return end if config.ignore_admins and player.admin then return end
local items = {} --- @type LuaItemStack[] local items = {} --- @type LuaItemStack[]
local inventory = assert(player.get_inventory(type)) local inventory = assert(player.get_inventory(type))
-- Check what items the player has -- Check what items the player has
for i = 1, #inventory do for i = 1, #inventory do
local item = inventory[i] local item = inventory[i --[[@as uint]]]
if item.valid_for_read and banned_items[item.name] then if item.valid_for_read and banned_items[item.name] then
player.print{ "exp_nuke-protection.chat-found", item.prototype.localised_name } player.print{ "exp_nuke-protection.chat-found", item.prototype.localised_name }
items[#items + 1] = item items[#items + 1] = item
+21
View File
@@ -0,0 +1,21 @@
--[[-- Control - Roles
Applies the in game effects of the player permissions defined by the scenario
]]
local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_roles")
Roles.define_permission_trigger("exp_scenario.player.admin", function(player, state)
player.admin = state
end)
Roles.define_permission_trigger("exp_scenario.player.spectator", function(player, state)
player.spectator = state
end)
return {
events = {
--- @diagnostic disable-next-line: access-invisible
[Roles.events.on_player_roles_changed] = Gui._ensure_consistency,
}
}
+35 -18
View File
@@ -5,8 +5,8 @@ Adds a custom spawn area with chests and afk turrets
local config = require("modules.exp_legacy.config.spawn_area") local config = require("modules.exp_legacy.config.spawn_area")
--- Apply an offset to a LuaPosition --- Apply an offset to a LuaPosition
--- @param position MapPosition --- @param position MapPosition.struct
--- @param offset MapPosition --- @param offset MapPosition.struct
--- @return MapPosition.struct --- @return MapPosition.struct
local function apply_offset(position, offset) local function apply_offset(position, offset)
return { return {
@@ -17,7 +17,7 @@ end
--- Apply offset to an array of positions --- Apply offset to an array of positions
--- @param positions table --- @param positions table
--- @param offset MapPosition --- @param offset MapPosition.struct
--- @param x_index number --- @param x_index number
--- @param y_index number --- @param y_index number
local function apply_offset_to_array(positions, offset, x_index, y_index) local function apply_offset_to_array(positions, offset, x_index, y_index)
@@ -97,14 +97,19 @@ local belt_details = {
--- Makes a 2x2 afk belt at the locations in the config --- Makes a 2x2 afk belt at the locations in the config
--- @param surface LuaSurface --- @param surface LuaSurface
--- @param offset MapPosition --- @param offset MapPosition.struct
local function create_belts(surface, offset) local function create_belts(surface, offset)
local belt_type = config.afk_belts.belt_type local belt_type = config.afk_belts.belt_type
local entity_pos = { x = 0, y = 0 }
local group_offset = { x = 0, y = 0 }
for _, position in pairs(config.afk_belts.locations) do for _, position in pairs(config.afk_belts.locations) do
position = apply_offset(position, offset) group_offset.x = position[1] + offset.x
group_offset.y = position[2] + offset.y
for _, belt in pairs(belt_details) do for _, belt in pairs(belt_details) do
local pos = apply_offset(position, belt) entity_pos.x = belt[1]
entity_pos.y = belt[2]
local pos = apply_offset(entity_pos, group_offset)
local entity = surface.create_entity{ name = belt_type, position = pos, force = "neutral", direction = belt[3] } local entity = surface.create_entity{ name = belt_type, position = pos, force = "neutral", direction = belt[3] }
if entity and config.afk_belts.protected then if entity and config.afk_belts.protected then
protect_entity(entity) protect_entity(entity)
@@ -115,13 +120,16 @@ end
-- Generates extra tiles in a set pattern as defined in the config -- Generates extra tiles in a set pattern as defined in the config
--- @param surface LuaSurface --- @param surface LuaSurface
--- @param offset MapPosition --- @param offset MapPosition.struct
local function create_pattern_tiles(surface, offset) local function create_pattern_tiles(surface, offset)
local tiles_to_make = {} local tiles_to_make = {}
local pattern_tile = config.pattern.pattern_tile local pattern_tile = config.pattern.pattern_tile
local tile_pos = { x = 0, y = 0 }
for index, position in pairs(config.pattern.locations) do for index, position in pairs(config.pattern.locations) do
tiles_to_make[index] = { name = pattern_tile, position = apply_offset(position, offset) } tile_pos.x = position[1]
tile_pos.y = position[2]
tiles_to_make[index] = { name = pattern_tile, position = apply_offset(tile_pos, offset) }
end end
surface.set_tiles(tiles_to_make) surface.set_tiles(tiles_to_make)
@@ -129,13 +137,16 @@ end
-- Generates extra water as defined in the config -- Generates extra water as defined in the config
--- @param surface LuaSurface --- @param surface LuaSurface
--- @param offset MapPosition --- @param offset MapPosition.struct
local function create_water_tiles(surface, offset) local function create_water_tiles(surface, offset)
local tiles_to_make = {} local tiles_to_make = {}
local water_tile = config.water.water_tile local water_tile = config.water.water_tile
local tile_pos = { x = 0, y = 0 }
for _, position in pairs(config.water.locations) do for _, position in pairs(config.water.locations) do
table.insert(tiles_to_make, { name = water_tile, position = apply_offset(position, offset) }) tile_pos.x = position[1]
tile_pos.y = position[2]
table.insert(tiles_to_make, { name = water_tile, position = apply_offset(tile_pos, offset) })
end end
surface.set_tiles(tiles_to_make) surface.set_tiles(tiles_to_make)
@@ -143,11 +154,17 @@ end
--- Generates the entities that are in the config --- Generates the entities that are in the config
--- @param surface LuaSurface --- @param surface LuaSurface
--- @param offset MapPosition --- @param offset MapPosition.struct
local function create_entities(surface, offset) local function create_entities(surface, offset)
local entity_pos = { x = 0, y = 0 }
for _, entity_details in pairs(config.entities.locations) do for _, entity_details in pairs(config.entities.locations) do
local pos = apply_offset({ entity_details[2], entity_details[3] }, offset) entity_pos.x = entity_details[2]
local entity = surface.create_entity{ name = entity_details[1], position = pos, force = "neutral" } entity_pos.y = entity_details[3]
local entity = surface.create_entity{
name = entity_details[1],
position = apply_offset(entity_pos, offset),
force = "neutral"
}
if entity then if entity then
if config.entities.protected then if config.entities.protected then
@@ -161,7 +178,7 @@ end
--- Generates an area with no water or entities, no water area is larger --- Generates an area with no water or entities, no water area is larger
--- @param surface LuaSurface --- @param surface LuaSurface
--- @param offset MapPosition --- @param offset MapPosition.struct
local function clear_spawn_area(surface, offset) local function clear_spawn_area(surface, offset)
local get_tile = surface.get_tile local get_tile = surface.get_tile
@@ -182,7 +199,7 @@ local function clear_spawn_area(surface, offset)
for y = -fill_radius, fill_radius do -- loop over y for y = -fill_radius, fill_radius do -- loop over y
local y_sqr = (y + 0.5) ^ 2 local y_sqr = (y + 0.5) ^ 2
local dst = x_sqr + y_sqr local dst = x_sqr + y_sqr
local pos = apply_offset({ x, y }, offset) local pos = apply_offset({ x = x, y = y }, offset)
if dst < tile_radius_sqr then if dst < tile_radius_sqr then
-- If it is inside the decon radius always set the tile -- If it is inside the decon radius always set the tile
tiles_to_make[#tiles_to_make + 1] = { name = decon_tile, position = pos } tiles_to_make[#tiles_to_make + 1] = { name = decon_tile, position = pos }
@@ -204,14 +221,14 @@ end
--- Spawn the resource tiles --- Spawn the resource tiles
--- @param surface LuaSurface --- @param surface LuaSurface
--- @param offset MapPosition --- @param offset MapPosition.struct
local function create_resources_tiles(surface, offset) local function create_resources_tiles(surface, offset)
for _, resource in ipairs(config.resource_tiles.resources) do for _, resource in ipairs(config.resource_tiles.resources) do
if resource.enabled then if resource.enabled then
local pos = apply_offset(resource.offset, offset) local pos = apply_offset(resource.offset, offset)
for x = pos.x, pos.x + resource.size[1] do for x = pos.x, pos.x + resource.size[1] do
for y = pos.y, pos.y + resource.size[2] do for y = pos.y, pos.y + resource.size[2] do
surface.create_entity{ name = resource.name, amount = resource.amount, position = { x, y } } surface.create_entity{ name = resource.name, amount = resource.amount, position = { x = x, y = y } }
end end
end end
end end
@@ -220,7 +237,7 @@ end
--- Spawn the resource entities --- Spawn the resource entities
--- @param surface LuaSurface --- @param surface LuaSurface
--- @param offset MapPosition --- @param offset MapPosition.struct
local function create_resource_patches(surface, offset) local function create_resource_patches(surface, offset)
for _, resource in ipairs(config.resource_patches.resources) do for _, resource in ipairs(config.resource_patches.resources) do
if resource.enabled then if resource.enabled then
+2 -2
View File
@@ -3,7 +3,7 @@ Adds a config menu for setting autofill of placed entities
]] ]]
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local Roles = require("modules.exp_legacy.expcore.roles") local Roles = require("modules/exp_roles")
local config = require("modules.exp_legacy.config.gui.autofill") local config = require("modules.exp_legacy.config.gui.autofill")
local FlyingText = require("modules/exp_util/flying_text") local FlyingText = require("modules/exp_util/flying_text")
@@ -330,7 +330,7 @@ Gui.toolbar.create_button{
sprite = config.icon, sprite = config.icon,
tooltip = { "exp-gui_autofill.tooltip-main" }, tooltip = { "exp-gui_autofill.tooltip-main" },
visible = function(player, element) visible = function(player, element)
return Roles.player_allowed(player, "gui/autofill") return Roles.player_has_permission(player, "exp_scenario.gui.autofill")
end end
} }
+3 -3
View File
@@ -4,7 +4,7 @@ Adds a Gui which creates an selection planner to insert modules into buildings
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local AABB = require("modules/exp_util/aabb") local AABB = require("modules/exp_util/aabb")
local Roles = require("modules/exp_legacy/expcore/roles") local Roles = require("modules/exp_roles")
local Selection = require("modules/exp_util/selection") local Selection = require("modules/exp_util/selection")
local SelectArea = Selection.connect("ModuleArea") local SelectArea = Selection.connect("ModuleArea")
@@ -259,7 +259,7 @@ Gui.toolbar.create_button{
sprite = "item/productivity-module-3", sprite = "item/productivity-module-3",
tooltip = { "exp-gui_module-inserter.tooltip-main" }, tooltip = { "exp-gui_module-inserter.tooltip-main" },
visible = function(player, element) visible = function(player, element)
return Roles.player_allowed(player, "gui/module") return Roles.player_has_permission(player, "exp_scenario.gui.module")
end end
} }
@@ -459,7 +459,7 @@ local function on_entity_settings_pasted(event)
-- Get the modules and add them to the planner -- Get the modules and add them to the planner
local all_modules = {} local all_modules = {}
for i = 1, #module_inventory do for i = 1, #module_inventory do
local slot = module_inventory[i] local slot = module_inventory[i --[[@as uint]]]
if slot.valid_for_read and slot.count > 0 then if slot.valid_for_read and slot.count > 0 then
all_modules[i] = { name = slot.name, quality = slot.quality.name } all_modules[i] = { name = slot.name, quality = slot.quality.name }
else else
+11 -8
View File
@@ -3,7 +3,7 @@ Adds a gui that allows players to apply various bonuses
]] ]]
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_legacy/expcore/roles") local Roles = require("modules/exp_roles")
local config = require("modules/exp_legacy/config/bonus") local config = require("modules/exp_legacy/config/bonus")
local vlayer = require("modules/exp_legacy/modules/control/vlayer") local vlayer = require("modules/exp_legacy/modules/control/vlayer")
local format_number = require("util").format_number local format_number = require("util").format_number
@@ -68,7 +68,11 @@ do local _points_limit = {} --- @type table<number, number>
--- @param player LuaPlayer --- @param player LuaPlayer
--- @return number --- @return number
function Elements.bonus_used._calculate_points_limit(player) function Elements.bonus_used._calculate_points_limit(player)
local role_diff = Roles.get_role_by_name(config.points.role_name).index - Roles.get_player_highest_role(player).index local roles = Roles.get_roles()
local positions = {}
for index, role in ipairs(roles) do positions[role.id] = index end
local role = assert(Roles.get_role_by_name(config.points.role_name), "Bonus points role does not exist")
local role_diff = positions[role.id] - positions[Roles.get_player_highest_role(player).id]
local points_limit = math.floor(config.points.base * (1 + config.points.increase_percentage_per_role_level * role_diff)) local points_limit = math.floor(config.points.base * (1 + config.points.increase_percentage_per_role_level * role_diff))
_points_limit[player.index] = points_limit _points_limit[player.index] = points_limit
return points_limit return points_limit
@@ -458,19 +462,19 @@ Gui.toolbar.create_button{
sprite = "item/exoskeleton-equipment", sprite = "item/exoskeleton-equipment",
tooltip = { "exp-gui_player-bonus.tooltip-main" }, tooltip = { "exp-gui_player-bonus.tooltip-main" },
visible = function(player, element) visible = function(player, element)
return Roles.player_allowed(player, "gui/bonus") return Roles.player_has_permission(player, "exp_scenario.gui.bonus")
end end
} }
--- Recalculate and apply the bonus for a player --- Recalculate and apply the bonus for a player
local function recalculate_bonus(event) local function recalculate_bonus(event)
local player = Gui.get_player(event) local player = Gui.get_player(event)
if event.name == Roles.events.on_role_assigned or event.name == Roles.events.on_role_unassigned then if event.name == Roles.events.on_player_roles_changed then
-- If the player's roles changed then we will need to recalculate their limit -- If the player's roles changed then we will need to recalculate their limit
Elements.bonus_used._clear_points_limit_cache(player) Elements.bonus_used._clear_points_limit_cache(player)
local bonus_cost = Elements.container.calculate_cost(player) local bonus_cost = Elements.container.calculate_cost(player)
local within_limit = Elements.bonus_used.refresh_player(player, bonus_cost) local within_limit = Elements.bonus_used.refresh_player(player, bonus_cost)
if not within_limit or not Roles.player_allowed(player, "gui/bonus") then if not within_limit or not Roles.player_has_permission(player, "exp_scenario.gui.bonus") then
Elements.container.clear_player_bonus(player) Elements.container.clear_player_bonus(player)
return return
end end
@@ -512,7 +516,7 @@ end
--- Apply the periodic bonus to all players --- Apply the periodic bonus to all players
local function apply_periodic_bonus_online() local function apply_periodic_bonus_online()
for _, player in pairs(game.connected_players) do for _, player in pairs(game.connected_players) do
if player.character and Roles.player_allowed(player, "gui/bonus") then if player.character and Roles.player_has_permission(player, "exp_scenario.gui.bonus") then
apply_personal_battery_recharge(player) apply_personal_battery_recharge(player)
end end
end end
@@ -524,8 +528,7 @@ return {
elements = Elements, elements = Elements,
events = { events = {
[e.on_player_respawned] = recalculate_bonus, [e.on_player_respawned] = recalculate_bonus,
[Roles.events.on_role_assigned] = recalculate_bonus, [Roles.events.on_player_roles_changed] = recalculate_bonus,
[Roles.events.on_role_unassigned] = recalculate_bonus,
}, },
on_nth_tick = { on_nth_tick = {
[config.periodic_bonus_rate] = apply_periodic_bonus_online, [config.periodic_bonus_rate] = apply_periodic_bonus_online,
+6 -6
View File
@@ -4,7 +4,7 @@ Adds a player list to show names and play time; also includes action buttons whi
local ExpUtil = require("modules/exp_util") local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_legacy/expcore/roles") local Roles = require("modules/exp_roles")
local config = require("modules/exp_legacy/config/gui/player_list_actions") local config = require("modules/exp_legacy/config/gui/player_list_actions")
--- @class ExpGui_PlayerList.elements --- @class ExpGui_PlayerList.elements
@@ -203,7 +203,8 @@ function Elements.player_table.calculate_row_data()
-- Flatten the roles into a single ordered list -- Flatten the roles into a single ordered list
local count = 0 local count = 0
local row_data = {} local row_data = {}
for _, role_name in pairs(Roles.config.order) do for _, role in ipairs(Roles.get_ordered_roles()) do
local role_name = role.name
if players[role_name] then if players[role_name] then
for _, player in pairs(players[role_name]) do for _, player in pairs(players[role_name]) do
count = count + 1 count = count + 1
@@ -360,7 +361,7 @@ function Elements.action_bar.refresh(action_bar, player, selected_player)
if buttons.auth and not buttons.auth(player, selected_player) then if buttons.auth and not buttons.auth(player, selected_player) then
flow.visible = false flow.visible = false
else else
flow.visible = Roles.player_allowed(player, action_name) flow.visible = Roles.player_has_permission(player, action_name)
end end
end end
end end
@@ -492,7 +493,7 @@ Gui.toolbar.create_button{
sprite = "entity/character", sprite = "entity/character",
tooltip = { "exp-gui_player-list.main-tooltip" }, tooltip = { "exp-gui_player-list.main-tooltip" },
visible = function(player, element) visible = function(player, element)
return Roles.player_allowed(player, "gui/player-list") return Roles.player_has_permission(player, "exp_scenario.gui.player_list")
end end
} }
@@ -534,8 +535,7 @@ return {
events = { events = {
[e.on_player_joined_game] = redraw_player_list, [e.on_player_joined_game] = redraw_player_list,
[e.on_player_left_game] = on_player_left_game, [e.on_player_left_game] = on_player_left_game,
[Roles.events.on_role_assigned] = redraw_player_list, [Roles.events.on_player_roles_changed] = redraw_player_list,
[Roles.events.on_role_unassigned] = redraw_player_list,
}, },
on_nth_tick = { on_nth_tick = {
[1800] = refresh_player_times, [1800] = refresh_player_times,
+2 -2
View File
@@ -5,7 +5,7 @@ Displays the player data for a player
local ExpUtil = require("modules/exp_util") local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local ElementsExtra = require("modules/exp_scenario/gui/elements") local ElementsExtra = require("modules/exp_scenario/gui/elements")
local Roles = require("modules/exp_legacy/expcore/roles") local Roles = require("modules/exp_roles")
require("modules/exp_legacy/modules/data/statistics") require("modules/exp_legacy/modules/data/statistics")
local PlayerData = require("modules/exp_legacy/expcore/player_data") local PlayerData = require("modules/exp_legacy/expcore/player_data")
@@ -214,7 +214,7 @@ Gui.toolbar.create_button{
tooltip = { "exp-gui_player-stats.tooltip-main" }, tooltip = { "exp-gui_player-stats.tooltip-main" },
left_element = Elements.container, left_element = Elements.container,
visible = function(player, element) visible = function(player, element)
return Roles.player_allowed(player, "gui/playerdata") return Roles.player_has_permission(player, "exp_scenario.gui.playerdata")
end end
} }
+2 -2
View File
@@ -3,7 +3,7 @@ Adds a Gui for displaying item production stats
]] ]]
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_legacy/expcore/roles") local Roles = require("modules/exp_roles")
--- @class ExpGui_ProductionStats.elements --- @class ExpGui_ProductionStats.elements
local Elements = {} local Elements = {}
@@ -250,7 +250,7 @@ Gui.toolbar.create_button{
sprite = "entity/assembling-machine-3", sprite = "entity/assembling-machine-3",
tooltip = { "exp-gui_production-stats.tooltip-main" }, tooltip = { "exp-gui_production-stats.tooltip-main" },
visible = function(player, element) visible = function(player, element)
return Roles.player_allowed(player, "gui/production") return Roles.player_has_permission(player, "exp_scenario.gui.production")
end end
} }
+3 -4
View File
@@ -4,7 +4,7 @@ Adds a few buttons for common actions
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local Commands = require("modules/exp_commands") local Commands = require("modules/exp_commands")
local Roles = require("modules/exp_legacy/expcore/roles") local Roles = require("modules/exp_roles")
local addon_artillery = require("modules/exp_scenario/commands/artillery") local addon_artillery = require("modules/exp_scenario/commands/artillery")
local addon_trains = require("modules/exp_scenario/commands/trains") local addon_trains = require("modules/exp_scenario/commands/trains")
@@ -116,7 +116,7 @@ Gui.toolbar.create_button{
sprite = "item/repair-pack", sprite = "item/repair-pack",
tooltip = { "exp-gui_quick-actions.tooltip-main" }, tooltip = { "exp-gui_quick-actions.tooltip-main" },
visible = function(player, element) visible = function(player, element)
return Roles.player_allowed(player, "gui/tool") return Roles.player_has_permission(player, "exp_scenario.gui.tool")
end end
} }
@@ -129,7 +129,6 @@ end
return { return {
elements = Elements, elements = Elements,
events = { events = {
[Roles.events.on_role_assigned] = on_role_changed, [Roles.events.on_player_roles_changed] = on_role_changed,
[Roles.events.on_role_unassigned] = on_role_changed,
} }
} }
+12 -10
View File
@@ -4,7 +4,7 @@ Adds a main gui that contains important information about the server.
local ExpUtil = require("modules/exp_util") local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local Roles = require("modules.exp_legacy.expcore.roles") local Roles = require("modules/exp_roles")
local Commands = require("modules/exp_commands") local Commands = require("modules/exp_commands")
local PlayerData = require("modules.exp_legacy.expcore.player_data") local PlayerData = require("modules.exp_legacy.expcore.player_data")
local External = require("modules.exp_legacy.expcore.external") local External = require("modules.exp_legacy.expcore.external")
@@ -214,7 +214,7 @@ define_tab(
container.add{ type = "flow" }.style.height = 4 container.add{ type = "flow" }.style.height = 4
local role_names = {} local role_names = {}
for i, role in ipairs(Roles.get_player_roles(player)) do for i, role in ipairs(Roles.sort_roles(Roles.get_player_roles(player))) do
role_names[i] = role.name role_names[i] = role.name
end end
@@ -392,14 +392,16 @@ define_tab(
local done = {} local done = {}
-- Fill groups from configured roles -- Fill groups from the players who hold the roles, a player can be in more than one group
for player_name, player_roles in pairs(Roles.config.players) do for _, group in ipairs(groups) do
for _, group in ipairs(groups) do local seen = {}
for _, role_name in ipairs(group.roles) do for _, role_name in ipairs(group.roles) do
if table.contains(player_roles, role_name) then local role = Roles.get_role_by_name(role_name)
for _, player_name in ipairs(role and role:get_player_names() or {}) do
if not seen[player_name] then
seen[player_name] = true
done[player_name] = true done[player_name] = true
group.players[#group.players + 1] = player_name group.players[#group.players + 1] = player_name
break
end end
end end
end end
@@ -463,7 +465,7 @@ local function render_data_category(opts)
for name, child in pairs(opts.children) do for name, child in pairs(opts.children) do
local metadata = child.metadata local metadata = child.metadata
if not metadata.permission or Roles.player_allowed(opts.player, metadata.permission) then if not metadata.permission or Roles.player_has_permission(opts.player, metadata.permission) then
local value = child:get(opts.player_name) local value = child:get(opts.player_name)
if value ~= nil or metadata.show_always then if value ~= nil or metadata.show_always then
@@ -663,7 +665,7 @@ Elements.toggle_button = Gui.toolbar.create_button{
sprite = "virtual-signal/signal-info", sprite = "virtual-signal/signal-info",
tooltip = { "exp-gui_readme.main-tooltip" }, tooltip = { "exp-gui_readme.main-tooltip" },
visible = function(player) visible = function(player)
return Roles.player_allowed(player, "gui/readme") return Roles.player_has_permission(player, "exp_scenario.gui.readme")
end, end,
} }
:on_click(function(_, player) :on_click(function(_, player)
@@ -4,7 +4,7 @@ Adds a gui for tracking research milestones
local ExpUtil = require("modules/exp_util") local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_legacy/expcore/roles") local Roles = require("modules/exp_roles")
local config = require("modules/exp_legacy/config/research") local config = require("modules/exp_legacy/config/research")
local table_to_json = helpers.table_to_json local table_to_json = helpers.table_to_json
@@ -294,9 +294,9 @@ end
--- Get the achieved time for a force --- Get the achieved time for a force
--- @param force LuaForce --- @param force LuaForce
--- @param research_index number --- @param research_index number
--- @return number --- @return number?
function Elements.container.get_achieved_time(force, research_index) function Elements.container.get_achieved_time(force, research_index)
return assert(Elements.container.data[force][research_index]) return assert(Elements.container.data[force])[research_index]
end end
--- Calculate the starting research index for a force --- Calculate the starting research index for a force
@@ -339,7 +339,7 @@ Gui.toolbar.create_button{
sprite = "item/space-science-pack", sprite = "item/space-science-pack",
tooltip = { "exp-gui_research-milestones.tooltip-main" }, tooltip = { "exp-gui_research-milestones.tooltip-main" },
visible = function(player, element) visible = function(player, element)
return Roles.player_allowed(player, "gui/research") return Roles.player_has_permission(player, "exp_scenario.gui.research")
end end
} }
+2 -2
View File
@@ -4,7 +4,7 @@ Adds a rocket information gui which shows general stats, milestones and build pr
local ExpUtil = require("modules/exp_util") local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_legacy/expcore/roles") local Roles = require("modules/exp_roles")
local config = require("modules/exp_legacy/config/gui/rockets") local config = require("modules/exp_legacy/config/gui/rockets")
--- @class ExpGui_RocketInfo.elements --- @class ExpGui_RocketInfo.elements
@@ -651,7 +651,7 @@ Gui.toolbar.create_button{
sprite = "item/rocket-silo", sprite = "item/rocket-silo",
tooltip = { "exp-gui_rocket-info.tooltip-main" }, tooltip = { "exp-gui_rocket-info.tooltip-main" },
visible = function(player, element) visible = function(player, element)
return Roles.player_allowed(player, "gui/rocket-info") return Roles.player_has_permission(player, "exp_scenario.gui.rocket_info")
end end
} }
@@ -5,7 +5,7 @@ Adds a science info gui that shows production usage and net for the different sc
local ExpUtil = require("modules/exp_util") local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local Colors = require("modules/exp_util/include/color") local Colors = require("modules/exp_util/include/color")
local Roles = require("modules/exp_legacy/expcore/roles") local Roles = require("modules/exp_roles")
local config = require("modules/exp_legacy/config/gui/science") local config = require("modules/exp_legacy/config/gui/science")
local _format_number = require("util").format_number local _format_number = require("util").format_number
@@ -571,7 +571,7 @@ Gui.toolbar.create_button{
sprite = "entity/lab", sprite = "entity/lab",
tooltip = { "exp-gui_science-production.tooltip-main" }, tooltip = { "exp-gui_science-production.tooltip-main" },
visible = function(player, element) visible = function(player, element)
return Roles.player_allowed(player, "gui/science-info") return Roles.player_has_permission(player, "exp_scenario.gui.science_info")
end end
} }
+2 -2
View File
@@ -4,7 +4,7 @@ Adds cameras which can be used to view players and locations
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local ElementsExtra = require("modules/exp_scenario/gui/elements") local ElementsExtra = require("modules/exp_scenario/gui/elements")
local Roles = require("modules/exp_legacy/expcore/roles") local Roles = require("modules/exp_roles")
--- @class ExpGui_Surveillance.elements --- @class ExpGui_Surveillance.elements
local Elements = {} local Elements = {}
@@ -234,7 +234,7 @@ Gui.toolbar.create_button{
sprite = "entity/radar", sprite = "entity/radar",
tooltip = { "exp-gui_surveillance.tooltip-main" }, tooltip = { "exp-gui_surveillance.tooltip-main" },
visible = function(player, element) visible = function(player, element)
return Roles.player_allowed(player, "gui/surveillance") return Roles.player_has_permission(player, "exp_scenario.gui.surveillance")
end end
}:on_click(function(def, player, element, event) }:on_click(function(def, player, element, event)
Elements.container(player.gui.screen) Elements.container(player.gui.screen)
+7 -8
View File
@@ -3,7 +3,7 @@ Adds a task list to the game which players can add, remove and edit items on
]] ]]
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local Roles = require("modules.exp_legacy.expcore.roles") local Roles = require("modules/exp_roles")
local config = require("modules.exp_legacy.config.gui.tasks") local config = require("modules.exp_legacy.config.gui.tasks")
local ExpUtil = require("modules/exp_util") local ExpUtil = require("modules/exp_util")
@@ -43,8 +43,8 @@ local function has_permission_create_task(player)
return true return true
elseif allow_add_task == "admin" then elseif allow_add_task == "admin" then
return player.admin return player.admin
elseif allow_add_task == "expcore.roles" then elseif allow_add_task == "exp_roles" then
return Roles.player_allowed(player, config.expcore_roles_allow_add_task) return Roles.player_has_permission(player, config.exp_roles_allow_add_task)
end end
return false return false
@@ -67,8 +67,8 @@ local function has_permission_edit_task(player, task)
return true return true
elseif allow_edit_task == "admin" then elseif allow_edit_task == "admin" then
return player.admin return player.admin
elseif allow_edit_task == "expcore.roles" then elseif allow_edit_task == "exp_roles" then
return Roles.player_allowed(player, config.expcore_roles_allow_edit_task) return Roles.player_has_permission(player, config.exp_roles_allow_edit_task)
end end
return false return false
@@ -922,7 +922,7 @@ Gui.toolbar.create_button{
sprite = "utility/not_enough_repair_packs_icon", sprite = "utility/not_enough_repair_packs_icon",
tooltip = { "exp-gui_task-list.tooltip-main" }, tooltip = { "exp-gui_task-list.tooltip-main" },
visible = function(player, element) visible = function(player, element)
return Roles.player_allowed(player, "gui/task-list") return Roles.player_has_permission(player, "exp_scenario.gui.task_list")
end end
} }
@@ -950,7 +950,6 @@ return {
events = { events = {
[e.on_player_joined_game] = refresh_player_tasks, [e.on_player_joined_game] = refresh_player_tasks,
[e.on_player_changed_force] = refresh_player_tasks, [e.on_player_changed_force] = refresh_player_tasks,
[Roles.events.on_role_assigned] = refresh_player_permissions, [Roles.events.on_player_roles_changed] = refresh_player_permissions,
[Roles.events.on_role_unassigned] = refresh_player_permissions,
} }
} }
+1
View File
@@ -8,6 +8,7 @@
"dependencies": { "dependencies": {
"clusterio": "*", "clusterio": "*",
"exp_util": "*", "exp_util": "*",
"exp_roles": "*",
"exp_gui": "*", "exp_gui": "*",
"exp_commands": "*" "exp_commands": "*"
} }
+121 -151
View File
@@ -1,160 +1,130 @@
import * as lib from "@clusterio/lib"; import * as lib from "@clusterio/lib";
/** /**
* Convert a legacy role action into its clusterio permission name. * Permissions checked by the scenario in game through exp_roles.
* *
* The exp_roles lua module applies the same transform, so existing call sites such as * Commands are checked as `exp_scenario.command.<name>` with hyphens replaced by
* `Roles.player_allowed(player, "gui/warp-list/add")` keep working unchanged. * underscores, see module/commands/_authorities.lua. Everything else is checked
* * by name at its call site.
* @param action - Legacy action such as `gui/warp-list/add` or `fast-tree-decon`.
* @returns Permission name such as `exp_scenario.gui.warp_list.add`.
*/ */
export function permissionFromAction(action: string) { type Definition = [name: string, title: string, description: string, grantByDefault?: boolean];
const body = action.replace(/-/g, "_").replace(/\//g, ".");
return `exp_scenario.${body.includes(".") ? body : `action.${body}`}`;
}
/** const definitions: Definition[] = [
* Convert a legacy role flag into its clusterio permission name. ["exp_scenario.bypass.deconstruction_log", "Deconstruction log bypass", "Be excluded from the deconstruction log."],
* ["exp_scenario.bypass.entity_protection", "Bypass entity protection", "Remove entities that the protection filter would block."],
* @param flag - Legacy flag such as `report-immune`. ["exp_scenario.bypass.nuke_protection", "Bypass nuke protection", "Use nukes without the nuke protection restrictions."],
* @returns Permission name such as `exp_scenario.flag.report_immune`. ["exp_scenario.bypass.reports", "Report immune", "Be immune to being reported by other players."],
*/ ["exp_scenario.chat.commands", "Chat commands", "Use the chat command prefix to trigger auto replies."],
export function permissionFromFlag(flag: string) { ["exp_scenario.command._ipc", "/_ipc", "Send an IPC message on the selected channel."],
return `exp_scenario.flag.${flag.replace(/-/g, "_")}`; ["exp_scenario.command._rcon", "/_rcon", "Execute arbitrary code within a custom environment."],
} ["exp_scenario.command._sudo", "/_sudo", "Run a command as another player."],
["exp_scenario.command.admin_chat", "/admin-chat", "Sends a message in chat that only admins can see."],
/** Legacy actions which map onto core clusterio permissions rather than scenario ones. */ ["exp_scenario.command.artillery", "/artillery", "Automaticly select enemy target with artillery."],
export const coreActionPermissions: Record<string, string> = { ["exp_scenario.command.assign_role", "/assign-role", "Assigns a role lower than your own to a player."],
"command/assign-role": "core.user.update_roles", ["exp_scenario.command.bring", "/bring", "Teleports a player to you."],
"command/unassign-role": "core.user.update_roles", ["exp_scenario.command.clear_blueprints", "/clear-blueprints", "Clear all blueprints."],
"command/get-roles": "core.role.list", ["exp_scenario.command.clear_blueprints_surface", "/clear-blueprints-surface", "Clear all blueprints on the current surface."],
}; ["exp_scenario.command.clear_ground_items", "/clear-ground-items", "Clear all items on the ground."],
["exp_scenario.command.clear_inventory", "/clear-inventory", "Clear a player's inventory, moving all items to spawn."],
/** Legacy action, user facing title, description, and whether every player gets it. */ ["exp_scenario.command.clear_last_warnings", "/clear-last-warnings", "Clears the last warning from a player."],
type ActionDefinition = [action: string, title: string, description: string, grantByDefault?: boolean]; ["exp_scenario.command.clear_pollution", "/clear-pollution", "Clear pollution from your current surface, or another surface."],
["exp_scenario.command.clear_reports", "/clear-reports", "Clears all reports from a player or just the report from one player."],
const actions: ActionDefinition[] = [ ["exp_scenario.command.clear_script_warnings", "/clear-script-warnings", "Clears all script warnings from a player."],
["bypass-entity-protection", "Bypass entity protection", "Remove entities that the protection filter would block."], ["exp_scenario.command.clear_warnings", "/clear-warnings", "Clears all warnings (and script warnings) from a player."],
["bypass-nukeprotect", "Bypass nuke protection", "Use nukes without the nuke protection restrictions."], ["exp_scenario.command.collectdata", "/collectdata", "Collect data for RCON usage."],
["command/_rcon", "/_rcon", "Execute arbitrary code within a custom environment."], ["exp_scenario.command.commands", "/commands", "List and search all commands for a keyword.", true],
["command/admin-chat", "/admin-chat", "Sends a message in chat that only admins can see."], ["exp_scenario.command.connect", "/connect", "Connect to another server.", true],
["command/artillery", "/artillery", "Automaticly select enemy target with artillery."], ["exp_scenario.command.connect_all", "/connect-all", "Connect all players to another server."],
["command/ban", "/ban", "Ban a player from the cluster via the player list."], ["exp_scenario.command.connect_player", "/connect-player", "Connect a player to a different server."],
["command/bring", "/bring", "Teleports a player to you."], ["exp_scenario.command.create_report", "/create-report", "Reports a player and notifies moderators.", true],
["command/chat-commands", "Chat commands", "Use the chat command prefix to trigger auto replies."], ["exp_scenario.command.create_warning", "/create-warning", "Gives a warning to a player; may lead to automatic script action."],
["command/clear-blueprints", "/clear-blueprints", "Clear all blueprints."], ["exp_scenario.command.data_preference", "/data-preference", "Allows you to set/get your data saving preference.", true],
["command/clear-blueprints-surface", "/clear-blueprints-surface", "Clear all blueprints on the current surface."], ["exp_scenario.command.debug", "/debug", "Opens the debug gui."],
["command/clear-ground-items", "/clear-ground-items", "Clear all items on the ground."], ["exp_scenario.command.follow", "/follow", "Start following a player in spectator."],
["command/clear-inventory", "/clear-inventory", "Clear a player's inventory, moving all items to spawn."], ["exp_scenario.command.get_home", "/get-home", "Returns your current home location."],
["command/clear-last-warnings", "/clear-last-warnings", "Clears the last warning from a player."], ["exp_scenario.command.get_reports", "/get-reports", "List the reports against a player, or against every player."],
["command/clear-pollution", "/clear-pollution", "Clear pollution from your current surface, or another surface."], ["exp_scenario.command.get_roles", "/get-roles", "Get all roles that a player has, if no player provided it lists all roles.", true],
["command/clear-reports", "/clear-reports", "Clears all reports from a player or just the report from one player."], ["exp_scenario.command.get_warnings", "/get-warnings", "List the warnings against a player, or against every player."],
["command/clear-script-warnings", "/clear-script-warnings", "Clears all script warnings from a player."], ["exp_scenario.command.goto", "/goto", "Teleports you to a player."],
["command/clear-tag/always", "/clear-tag (any player)", "Clear the tag of any player, not just your own."], ["exp_scenario.command.home", "/home", "Teleports you to your home location."],
["command/clear-warnings", "/clear-warnings", "Clears all warnings (and script warnings) from a player."], ["exp_scenario.command.jail", "/jail", "Puts a player into jail, which suppresses all of their other roles."],
["command/collectdata", "/collectdata", "Collect data for RCON usage."], ["exp_scenario.command.kill", "/kill", "Kills yourself or another player."],
["command/commands", "/commands", "List and search all commands for a keyword.", true], ["exp_scenario.command.kill.always", "/kill (any player)", "Kill any player, not just yourself."],
["command/connect", "/connect", "Connect to another server.", true], ["exp_scenario.command.kill_enemies", "/kill-enemies", "Kill all enemy units."],
["command/connect-all", "/connect-all", "Connect all players to another server."], ["exp_scenario.command.lawnmower", "/lawnmower", "Clean up biter corpse, decoratives and nuclear hole."],
["command/connect-player", "/connect-player", "Connect a player to a different server."], ["exp_scenario.command.locate", "/locate", "Opens remote view at the location of the player's last location.", true],
["command/create-report", "/create-report", "Reports a player and notifies moderators.", true], ["exp_scenario.command.me", "/me", "Sends an action message in the chat."],
["command/create-warning", "/create-warning", "Gives a warning to a player; may lead to automatic script action."], ["exp_scenario.command.protect_area", "/protect-area", "Toggles area protection selection, hold shift to remove protection."],
["command/data-preference", "/data-preference", "Allows you to set/get your data saving preference.", true], ["exp_scenario.command.protect_entity", "/protect-entity", "Toggles entity protection selection, hold shift to remove protection."],
["command/debug", "/debug", "Opens the debug gui."], ["exp_scenario.command.protect_tag", "/protect-tag", "Toggles protected tag mode, edit and create protected map tags."],
["command/follow", "/follow", "Start following a player in spectator."], ["exp_scenario.command.rainbow", "/rainbow", "Sends an rainbow message in the chat."],
["command/get-home", "/get-home", "Returns your current home location."], ["exp_scenario.command.ratio", "/ratio", "Get the input and output ratios of the selected machine.", true],
["command/get-reports", "/get-reports", "List the reports against a player, or against every player."], ["exp_scenario.command.remove_enemies", "/remove-enemies", "Remove all enemy spawners."],
["command/get-warnings", "/get-warnings", "List the warnings against a player, or against every player."], ["exp_scenario.command.remove_join_message", "/remove-join-message", "Removes you custom join message."],
["command/give-warning", "/give-warning", "Give a player a warning via the player list."], ["exp_scenario.command.repair", "/repair", "Repairs entities on your force around you."],
["command/goto", "/goto", "Teleports you to a player."], ["exp_scenario.command.research_all", "/research-all", "Research all technology for your force, or another force."],
["command/home", "/home", "Teleports you to your home location."], ["exp_scenario.command.return", "/return", "Teleports you to previous location."],
["command/jail", "/jail", "Puts a player into jail and removes all other roles."], ["exp_scenario.command.save_data", "/save-data", "Writes all your player data to a file on your computer.", true],
["command/kick", "/kick", "Kick a player from the server via the player list."], ["exp_scenario.command.save_quickbar", "/save-quickbar", "Saves your Quickbar preset items to file."],
["command/kill", "/kill", "Kills yourself or another player."], ["exp_scenario.command.search", "/search", "Display players sorted by the quantity of an item held and playtime."],
["command/kill-enemies", "/kill-enemies", "Kill all enemy units."], ["exp_scenario.command.search_amount", "/search-amount", "Display players sorted by the quantity of an item held."],
["command/kill/always", "/kill (any player)", "Kill any player, not just yourself."], ["exp_scenario.command.search_online", "/search-online", "Display online players sorted by item count and playtime."],
["command/lawnmower", "/lawnmower", "Clean up biter corpse, decoratives and nuclear hole."], ["exp_scenario.command.search_recent", "/search-recent", "Display players who hold an item sorted by join time."],
["command/locate", "/locate", "Opens remote view at the location of the player's last location.", true], ["exp_scenario.command.server_ups", "/server-ups", "Toggle the server UPS display.", true],
["command/me", "/me", "Sends an action message in the chat."], ["exp_scenario.command.set_always_day", "/set-always-day", "Set always day for your current surface, or another surface."],
["command/protect-area", "/protect-area", "Toggles area protection selection, hold shift to remove protection."], ["exp_scenario.command.set_bot_queue", "/set-bot-queue", "Get / Set the construction bot queue limits."],
["command/protect-entity", "/protect-entity", "Toggles entity protection selection, hold shift to remove protection."], ["exp_scenario.command.set_cheat_mode", "/set-cheat-mode", "Set cheat mode for your player, or another player."],
["command/protect-tag", "/protect-tag", "Toggles protected tag mode, edit and create protected map tags."], ["exp_scenario.command.set_friendly_fire", "/set-friendly-fire", "Set friendly fire for your force, or another force."],
["command/rainbow", "/rainbow", "Sends an rainbow message in the chat."], ["exp_scenario.command.set_game_speed", "/set-game-speed", "Set or get the current game speed."],
["command/ratio", "/ratio", "Get the input and output ratios of the selected machine.", true], ["exp_scenario.command.set_home", "/set-home", "Sets your home location to your current position."],
["command/remove-enemies", "/remove-enemies", "Remove all enemy spawners."], ["exp_scenario.command.set_join_message", "/set-join-message", "Sets / Gets your custom join message."],
["command/remove-join-message", "/remove-join-message", "Removes you custom join message."], ["exp_scenario.command.set_pollution_enabled", "/set-pollution-enabled", "Set polution enabled state for this game."],
["command/repair", "/repair", "Repairs entities on your force around you."], ["exp_scenario.command.set_trains_to_automatic", "/set-trains-to-automatic", "Set all trains without passengers to automatic."],
["command/research-all", "/research-all", "Research all technology for your force, or another force."], ["exp_scenario.command.spawn", "/spawn", "Teleport to spawn."],
["command/return", "/return", "Teleports you to previous location."], ["exp_scenario.command.spawn.always", "/spawn (any player)", "Teleport any player to spawn, not just yourself."],
["command/save-data", "/save-data", "Writes all your player data to a file on your computer.", true], ["exp_scenario.command.spectate", "/spectate", "Toggles spectator mode."],
["command/save-quickbar", "/save-quickbar", "Saves your Quickbar preset items to file."], ["exp_scenario.command.tag", "/tag", "Sets your player tag.", true],
["command/search", "/search", "Display players sorted by the quantity of an item held and playtime."], ["exp_scenario.command.tag_clear", "/tag-clear", "Clears your tag. Or another player if you are admin.", true],
["command/search-amount", "/search-amount", "Display players sorted by the quantity of an item held."], ["exp_scenario.command.tag_clear.always", "/tag-clear (any player)", "Clear the tag of any player, not just your own."],
["command/search-online", "/search-online", "Display online players sorted by item count and playtime."], ["exp_scenario.command.tag_color", "/tag-color", "Sets your player tag color."],
["command/search-recent", "/search-recent", "Display players who hold an item sorted by join time."], ["exp_scenario.command.teleport", "/teleport", "Teleports a player to another player."],
["command/server-ups", "/server-ups", "Toggle the server UPS display.", true], ["exp_scenario.command.unassign_role", "/unassign-role", "Unassigns a role lower than your own from a player."],
["command/set-always-day", "/set-always-day", "Set always day for your current surface, or another surface."], ["exp_scenario.command.unjail", "/unjail", "Removes a player from jail, restoring their other roles."],
["command/set-bot-queue", "/set-bot-queue", "Get / Set the construction bot queue limits."], ["exp_scenario.command.vlayer_info", "/vlayer-info", "Print all vlayer information."],
["command/set-cheat-mode", "/set-cheat-mode", "Set cheat mode for your player, or another player."], ["exp_scenario.command.waterfill", "/waterfill", "Replace tiles with shallow water."],
["command/set-friendly-fire", "/set-friendly-fire", "Set friendly fire for your force, or another force."], ["exp_scenario.decon.fast_trees", "Fast tree deconstruction", "Trees and rocks marked for deconstruction are removed instantly, when enabled from the toolbar."],
["command/set-game-speed", "/set-game-speed", "Set or get the current game speed."], ["exp_scenario.decon.standard", "Standard deconstruction", "Mark entities placed by other players for deconstruction."],
["command/set-home", "/set-home", "Sets your home location to your current position."], ["exp_scenario.gui.autofill", "Autofill", "Open the autofill GUI, which fills placed entities from your inventory.", true],
["command/set-join-message", "/set-join-message", "Sets / Gets your custom join message."], ["exp_scenario.gui.bonus", "Bonus", "Open the bonus GUI to adjust your personal bonuses."],
["command/set-pollution-enabled", "/set-pollution-enabled", "Set polution enabled state for this game."], ["exp_scenario.gui.module", "Module inserter", "Open the module inserter GUI.", true],
["command/set-trains-to-automatic", "/set-trains-to-automatic", "Set all trains without passengers to automatic."], ["exp_scenario.gui.player_list", "Player list", "Open the player list GUI.", true],
["command/spawn", "/spawn", "Teleport to spawn."], ["exp_scenario.gui.player_list.ban", "Player list: ban", "Ban a player from the cluster via the player list."],
["command/spawn/always", "/spawn (any player)", "Teleport any player to spawn, not just yourself."], ["exp_scenario.gui.player_list.kick", "Player list: kick", "Kick a player from the server via the player list."],
["command/spectate", "/spectate", "Toggles spectator mode."], ["exp_scenario.gui.playerdata", "Player statistics", "Open the player statistics GUI."],
["command/tag", "/tag", "Sets your player tag.", true], ["exp_scenario.gui.production", "Production statistics", "Open the production statistics GUI.", true],
["command/tag-clear", "/tag-clear", "Clears your tag. Or another player if you are admin.", true], ["exp_scenario.gui.readme", "Readme", "Open the server readme GUI.", true],
["command/tag-color", "/tag-color", "Sets your player tag color."], ["exp_scenario.gui.research", "Research milestones", "Open the research milestones GUI.", true],
["command/teleport", "/teleport", "Teleports a player to another player."], ["exp_scenario.gui.rocket_info", "Rocket info", "Open the rocket info GUI.", true],
["command/unjail", "/unjail", "Removes a player from jail and restores their previous roles."], ["exp_scenario.gui.rocket_info.remote_launch", "Rocket info: remote launch", "Launch rockets remotely from the rocket info GUI."],
["command/vlayer-info", "/vlayer-info", "Print all vlayer information."], ["exp_scenario.gui.rocket_info.toggle_active", "Rocket info: toggle active", "Toggle whether a silo launches automatically."],
["command/waterfill", "/waterfill", "Replace tiles with shallow water."], ["exp_scenario.gui.science_info", "Science production", "Open the science production GUI.", true],
["fast-tree-decon", "Fast tree deconstruction", "Deconstruct trees and rocks instantly over a large area."], ["exp_scenario.gui.surveillance", "Surveillance", "Open the surveillance GUI showing remote camera views."],
["gui/autofill", "Autofill", "Open the autofill GUI, which fills placed entities from your inventory.", true], ["exp_scenario.gui.task_list", "Task list", "Open the task list GUI.", true],
["gui/bonus", "Bonus", "Open the bonus GUI to adjust your personal bonuses."], ["exp_scenario.gui.task_list.add", "Task list: add", "Add new tasks to the task list."],
["gui/module", "Module inserter", "Open the module inserter GUI.", true], ["exp_scenario.gui.task_list.edit", "Task list: edit", "Edit and remove existing tasks on the task list."],
["gui/player-list", "Player list", "Open the player list GUI.", true], ["exp_scenario.gui.tool", "Quick actions", "Open the quick actions GUI."],
["gui/playerdata", "Player statistics", "Open the player statistics GUI."], ["exp_scenario.gui.vlayer", "Virtual layer", "Open the virtual layer GUI.", true],
["gui/production", "Production statistics", "Open the production statistics GUI.", true], ["exp_scenario.gui.vlayer_edit", "Virtual layer: edit", "Use the edit controls in the virtual layer GUI."],
["gui/readme", "Readme", "Open the server readme GUI.", true], ["exp_scenario.gui.warp_list", "Warp list", "Open the warp list GUI and warp between locations.", true],
["gui/research", "Research milestones", "Open the research milestones GUI.", true], ["exp_scenario.gui.warp_list.add", "Warp list: add", "Add new warp points to the warp list."],
["gui/rocket-info", "Rocket info", "Open the rocket info GUI.", true], ["exp_scenario.gui.warp_list.bypass_cooldown", "Warp list: bypass cooldown", "Warp without waiting for the warp cooldown."],
["gui/rocket-info/remote_launch", "Rocket info: remote launch", "Launch rockets remotely from the rocket info GUI."], ["exp_scenario.gui.warp_list.bypass_proximity", "Warp list: bypass proximity", "Warp without standing near a warp point."],
["gui/rocket-info/toggle-active", "Rocket info: toggle active", "Toggle whether a silo launches automatically."], ["exp_scenario.gui.warp_list.edit", "Warp list: edit", "Edit and remove existing warp points."],
["gui/science-info", "Science production", "Open the science production GUI.", true], ["exp_scenario.player.admin", "Factorio admin", "Be promoted to Factorio admin while holding a role with this permission."],
["gui/surveillance", "Surveillance", "Open the surveillance GUI showing remote camera views."], ["exp_scenario.player.instant_respawn", "Instant respawn", "Respawn after two seconds instead of the default delay."],
["gui/task-list", "Task list", "Open the task list GUI.", true], ["exp_scenario.player.spectator", "Spectator", "Remove the zoom to world noise effect, as Factorio does for spectators."],
["gui/task-list/add", "Task list: add", "Add new tasks to the task list."],
["gui/task-list/edit", "Task list: edit", "Edit and remove existing tasks on the task list."],
["gui/tool", "Quick actions", "Open the quick actions GUI."],
["gui/vlayer", "Virtual layer", "Open the virtual layer GUI.", true],
["gui/vlayer-edit", "Virtual layer: edit", "Use the edit controls in the virtual layer GUI."],
["gui/warp-list", "Warp list", "Open the warp list GUI and warp between locations.", true],
["gui/warp-list/add", "Warp list: add", "Add new warp points to the warp list."],
["gui/warp-list/bypass-cooldown", "Warp list: bypass cooldown", "Warp without waiting for the warp cooldown."],
["gui/warp-list/bypass-proximity", "Warp list: bypass proximity", "Warp without standing near a warp point."],
["gui/warp-list/edit", "Warp list: edit", "Edit and remove existing warp points."],
["standard-decon", "Standard deconstruction", "Deconstruct trees and rocks."],
]; ];
const flags: ActionDefinition[] = [ for (const [name, title, description, grantByDefault] of definitions) {
["deconlog-bypass", "Deconstruction log bypass", "Be excluded from the deconstruction log."], lib.definePermission({ name, title, description, grantByDefault });
["defer_role_changes", "Defer role changes", "Hold back role changes until this role is removed; used by Jail."],
["instant-respawn", "Instant respawn", "Respawn after two seconds instead of the default delay."],
["is_admin", "Factorio admin", "Be promoted to Factorio admin while holding a role with this flag."],
["is_spectator", "Spectator", "Be placed into Factorio spectator mode."],
["is_system", "System commands", "Unlock system commands such as /_rcon and /_sudo."],
["report-immune", "Report immune", "Be immune to being reported by other players."],
];
for (const [action, title, description, grantByDefault] of actions) {
lib.definePermission({ name: permissionFromAction(action), title, description, grantByDefault });
}
for (const [flag, title, description] of flags) {
lib.definePermission({ name: permissionFromFlag(flag), title, description });
} }
+1 -1
View File
@@ -14,7 +14,7 @@ local PlayerData = require("modules/exp_legacy/expcore/player_data")
local UsesServerUps = PlayerData.Settings:combine("UsesServerUps") local UsesServerUps = PlayerData.Settings:combine("UsesServerUps")
UsesServerUps:set_default(false) UsesServerUps:set_default(false)
UsesServerUps:set_metadata{ UsesServerUps:set_metadata{
permission = "command/server-ups", permission = "exp_scenario.command.server_ups",
stringify = function(value) return value and "Visible" or "Hidden" end, stringify = function(value) return value and "Visible" or "Hidden" end,
} }
+55
View File
@@ -0,0 +1,55 @@
"use strict";
const { compile } = require("@clusterio/lib");
/**
* Generate a flat array of tests from a matrix of inputs.
*
* @template {any[][]} T
* @param {T} arrays - A list of arrays representing the different values for each argument
* @returns {Array<{ [K in keyof T]: T[K][number] }>}
* An array of tuples, each containing one value from each input array.
*/
function testMatrix(...arrays) {
return arrays.reduce((acc, curr) => acc.flatMap(a => curr.map(b => [...a, b])), [[]]);
}
/**
* Test that a class is round trip JSON serialisable across multiple test cases.
*
* @template {any[]} T - Constructor arguments
* @param {import("tap").Test} t - Parent test.
* @param {{new(...args: T): object}} Class - The class which has toJSON and fromJSON methods.
* @param {T[]} tests - The test inputs to pass to the class constructor.
*/
function testRoundTripJsonSerialisable(t, Class, tests) {
const validate = compile(Class.jsonSchema);
for (const test of tests) {
const name = JSON.stringify(test);
try {
const original = new Class(...test);
const jsonObject = JSON.parse(JSON.stringify(original));
const reconstructed = Class.fromJSON(jsonObject);
if (!validate(jsonObject)) {
t.fail(`schema validation: ${name}`, {
diagnostic: {
json: jsonObject,
errors: validate.errors,
},
});
continue;
}
t.strictSame(reconstructed, original, `round trip: ${name}`);
} catch (error) {
t.fail(`round trip: ${name}`, { error });
}
}
}
module.exports = {
testMatrix,
testRoundTripJsonSerialisable,
};
+168
View File
@@ -0,0 +1,168 @@
--[[-- Test framework for plugin module tests
A suite is a collection of named tests. Test files declare them with
`Suite.test(name, function(env) ... end)`, naming each test after the function
or method it covers. Every test function receives a fresh environment: the
stubs extended by the function the suite was created with, so tests are
independent and can not leak state into each other.
]]
local source = assert(debug.getinfo(1, "S")).source
local shared_root = assert(source:match("^@(.*)/framework%.lua$"))
local Stubs = assert(loadfile(shared_root .. "/stubs.lua"))()
--- @class Framework
local Framework = {}
--- Turn a value into a string for failure messages
local function repr(value, depth)
if type(value) ~= "table" then return tostring(value) end
if depth > 3 then return "{...}" end
local parts = {}
for key, entry in pairs(value) do
parts[#parts + 1] = tostring(key) .. " = " .. repr(entry, depth + 1)
end
return "{ " .. table.concat(parts, ", ") .. " }"
end
--- Create a suite of tests, one is made per test file
--- @generic T : Stubs
--- @param extend_env fun(env: Stubs): T Extends the stubs given to each test
--- @return Suite<T>
function Framework.suite(extend_env)
--- @class Suite<T>
local Suite = {
tests = {}, --- @type { name: string, fn: fun(env: T) }[] In declaration order
results = {}, --- @type { name: string, ok: boolean, detail: string? }[] Accumulated across tests
}
local current_test --- @type string?
--- Declare a named test, its function receives a fresh environment
--- @param name string
--- @param fn fun(env: T)
function Suite.test(name, fn)
Suite.tests[#Suite.tests + 1] = { name = name, fn = fn }
end
--- Record a passing check within the current test
--- @param name string
function Suite.pass(name)
Suite.results[#Suite.results + 1] = {
name = current_test and (current_test .. ": " .. name) or name,
ok = true,
}
end
--- Record a failing check within the current test
--- @param name string
--- @param detail string?
function Suite.fail(name, detail)
Suite.results[#Suite.results + 1] = {
name = current_test and (current_test .. ": " .. name) or name,
ok = false,
detail = detail,
}
end
--- Check a condition
--- @param ok any Truthy when the check passed
--- @param name string
--- @param detail string?
function Suite.check(ok, name, detail)
if ok then
Suite.pass(name)
else
Suite.fail(name, detail)
end
end
--- Recursive table equality, keys checked from both sides
local function deep_equal(a, b)
if type(a) ~= "table" or type(b) ~= "table" then return a == b end
for key, value in pairs(a) do
if not deep_equal(value, b[key]) then return false end
end
for key in pairs(b) do
if a[key] == nil then return false end
end
return true
end
--- Check two values are equal, comparing tables recursively
function Suite.eq(a, b, name)
Suite.check(deep_equal(a, b), name, ("%s ~= %s"):format(repr(a, 1), repr(b, 1)))
end
--- Check a table has no entries
function Suite.empty(value, name)
Suite.check(next(value) == nil, name, repr(value, 1))
end
--- Check a function raises an error containing the message
--- @param fn fun()
--- @param message string
--- @param name string
function Suite.throws(fn, message, name)
local ok, err = pcall(fn)
if ok then
Suite.fail(name, "did not error")
elseif not tostring(err):find(message, 1, true) then
Suite.fail(name, ("%s does not contain %s"):format(tostring(err), message))
else
Suite.pass(name)
end
end
--- Names of an array of values with a name property, such as roles,
--- players, forces, or events
--- @param values { name: string }[]
--- @return string[]
function Suite.names(values)
local rtn = {}
for index, value in ipairs(values) do
rtn[index] = value.name
end
return rtn
end
--- Sorted copy of an array of strings
function Suite.sorted(list)
local rtn = {}
for index, value in ipairs(list) do rtn[index] = value end
table.sort(rtn)
return rtn
end
--- Run every declared test, each against a fresh environment, and return
--- the results as JSON for the javascript side
function Suite.run()
for _, test in ipairs(Suite.tests) do
current_test = test.name
local ok, err = pcall(function()
test.fn(extend_env(Stubs.new()))
end)
if not ok then
Suite.fail("did not error", tostring(err))
end
end
current_test = nil
local function escape(value)
return (value:gsub('[%c"\\]', function(c)
return string.format("\\u%04x", c:byte())
end))
end
local parts = {}
for index, result in ipairs(Suite.results) do
local detail = result.detail and string.format(',"detail":"%s"', escape(result.detail)) or ""
parts[index] = string.format('{"name":"%s","ok":%s%s}', escape(result.name), tostring(result.ok), detail)
end
return "[" .. table.concat(parts, ",") .. "]"
end
return Suite
end
return Framework
+63
View File
@@ -0,0 +1,63 @@
"use strict";
const path = require("node:path");
const fs = require("node:fs");
const { lua, lauxlib, lualib, to_luastring, to_jsstring } = require("fengari");
/**
* Run one lua test file in its own lua state and return its results.
*
* The state is created here, on first use by the caller, so every test file
* gets an independent set of stubs. Three chunks run in order, each taking one
* argument and returning one value:
*
* 1. The plugin's env.lua, receiving this directory so it can load the shared
* stubs and framework, returning its environment module.
* 2. The test file, receiving the environment module, declaring its tests.
* 3. The tests then run, each against a fresh environment, and the results
* are returned as JSON.
*
* @param {string} envFile - Absolute path of the plugin's test/lua/env.lua.
* @param {string} testFile - Absolute path of the lua test file to run.
* @returns {{ name: string, ok: boolean, detail?: string }[]}
*/
function runLuaTests(envFile, testFile) {
const L = lauxlib.luaL_newstate();
lualib.luaL_openlibs(L);
const load = (file) => {
const code = fs.readFileSync(file);
const status = lauxlib.luaL_loadbuffer(L, code, code.length, to_luastring(`@${file}`));
if (status !== lua.LUA_OK) {
throw new Error(to_jsstring(lua.lua_tostring(L, -1)));
}
};
const call = () => {
if (lua.lua_pcall(L, 1, 1, 0) !== lua.LUA_OK) {
throw new Error(to_jsstring(lua.lua_tostring(L, -1)));
}
};
load(envFile);
lua.lua_pushstring(L, to_luastring(__dirname));
call();
// The environment stays on the stack and is passed to the test chunk
load(testFile);
lua.lua_insert(L, -2);
call();
const json = to_jsstring(lua.lua_tostring(L, -1));
return JSON.parse(json);
}
/** Report the results of a lua test file through a tap test object. */
function reportLuaTests(t, envFile, testFile) {
const results = runLuaTests(envFile, testFile);
t.ok(results.length > 0, `${path.basename(testFile)} produced results`);
for (const result of results) {
t.ok(result.ok, result.name, result.detail ? { detail: result.detail } : {});
}
t.end();
}
module.exports = { runLuaTests, reportLuaTests };
+195
View File
@@ -0,0 +1,195 @@
--[[-- Generic factorio stubs for plugin module tests
Provides the surface a clusterio module touches, recording what the module does
to it. Every stub raises an error when an unimplemented property is read, which
mirrors the game api and catches mistakes in tests early.
Plugins compose these from their own test/lua/env.lua, adding their own stubs
with the extend methods rather than by mutating the tables directly.
]]
local Stubs = {}
--- Give a table an index metamethod which raises on unknown properties
--- @generic T : table
--- @param name string Shown in the error message
--- @param tbl T
--- @param allowed_nil string[]? Properties which may be read while unset
--- @return T
function Stubs.strict(name, tbl, allowed_nil)
local allowed = {}
for _, key in pairs(allowed_nil or {}) do
allowed[key] = true
end
return setmetatable(tbl, {
__index = function(_, key)
if allowed[key] then return nil end
error(name .. " does not implement: " .. tostring(key), 2)
end,
})
end
--- Merge extra into target recursively, raising when a value already exists
--- @generic T : table
--- @param target T
--- @param extra table
--- @return T
function Stubs.extend(target, extra)
for key, value in pairs(extra) do
local existing = rawget(target, key)
if existing == nil then
rawset(target, key, value)
elseif type(existing) == "table" and type(value) == "table" then
Stubs.extend(existing, value)
else
error("Stub already implements: " .. tostring(key), 2)
end
end
return target
end
--- Create an independent set of stubs, installed as the lua globals
function Stubs.new()
--- @class Stubs
--- @field events table[] Events raised through script.raise_event, name and tick filled in
--- @field printed (LocalisedString | { to: string, [1]: LocalisedString })[] Messages from game.print and player.print
--- @field sent { channel: string, data: table }[] Payloads sent through the clusterio api
--- @field sounds string[] Sounds played to players, as "player:path"
local stubs = {
events = {},
printed = {},
sent = {},
sounds = {},
}
local next_event_id = 100
local registered_metatables = {} --- @type table<table, true>
script = Stubs.strict("LuaBootstrap", {
generate_event_name = function()
next_event_id = next_event_id + 1
return next_event_id
end,
raise_event = function(id, data)
-- Factorio fills in the name and tick of the event
data.name = id
data.tick = game.tick
stubs.events[#stubs.events + 1] = data
end,
register_metatable = function(_, metatable)
registered_metatables[metatable] = true
end,
})
defines = Stubs.strict("defines", {
events = Stubs.strict("defines.events", {
on_player_joined_game = 1,
on_multiplayer_init = 2,
}),
})
-- Stored by name, with an index metamethod so players are also found by
-- their player index, the same way game.players works
local players_by_index = {}
local players = setmetatable({}, { __index = players_by_index })
local connected = {}
game = Stubs.strict("LuaGameScript", {
tick = 1,
players = players,
connected_players = connected,
print = function(message) stubs.printed[#stubs.printed + 1] = message end,
get_player = function(key) return players[key] end,
}, { "player" })
--- Add a player to the stubbed game, indexes are assigned in join order
--- @param name string
--- @param is_connected boolean? Defaults to connected
--- @return Stubs.Player
function stubs.add_player(name, is_connected)
local index = #players_by_index + 1
--- @class Stubs.Player
local player = Stubs.strict("LuaPlayer " .. name, {
name = name,
index = index,
connected = is_connected ~= false,
valid = true,
play_sound = function(opts)
stubs.sounds[#stubs.sounds + 1] = name .. ":" .. opts.path
end,
print = function(message)
stubs.printed[#stubs.printed + 1] = { to = name, message }
end,
})
rawset(players, name, player)
players_by_index[index] = player
if player.connected then connected[#connected + 1] = player end
return player
end
--- A player object which represents the server
--- @type Stubs.Player
stubs.server = Stubs.strict("LuaPlayer <server>", { index = 0, name = "<server>" })
--- Modules resolved by the stubbed require, add to them with extend_requires
local requires = {
["modules/clusterio/api"] = Stubs.strict("clusterio api", {
send_json = function(channel, data)
stubs.sent[#stubs.sent + 1] = { channel = channel, data = data }
end,
}),
["modules/clusterio/compat"] = Stubs.strict("clusterio compat", { script_data = {} }),
["modules/exp_util/async"] = Stubs.strict("exp_util async", {
register = function(callback)
return function(...) return callback(...) end
end,
}),
}
require = function(name)
return assert(rawget(requires, name), "Unexpected require: " .. name)
end
--- Add modules or properties to the stubbed require
function stubs.extend_requires(extra) return Stubs.extend(requires, extra) end
--- Add properties to the script, game, and defines globals
function stubs.extend_script(extra) return Stubs.extend(script, extra) end
function stubs.extend_game(extra) return Stubs.extend(game, extra) end
function stubs.extend_defines(extra) return Stubs.extend(defines, extra) end
--- Copy a value the way factorio saves script data: functions are refused
--- and only metatables registered with script.register_metatable survive
local function save_load_copy(value, copies)
if type(value) == "function" then
error("Functions can not be stored in script data", 0)
end
if type(value) ~= "table" then return value end
if copies[value] then return copies[value] end
local copy = {}
copies[value] = copy
for key, entry in pairs(value) do
copy[save_load_copy(key, copies)] = save_load_copy(entry, copies)
end
local metatable = getmetatable(value)
if metatable ~= nil and registered_metatables[metatable] then
setmetatable(copy, metatable)
end
return copy
end
--- Replace the script data with a copy of itself as if the map was saved
--- and loaded, the module's on_load handler should be called afterwards
function stubs.save_load()
local compat = requires["modules/clusterio/compat"]
compat.script_data = save_load_copy(compat.script_data, {})
end
--- Forget everything recorded so far
function stubs.reset_log()
stubs.events, stubs.printed, stubs.sent, stubs.sounds = {}, {}, {}, {}
end
return stubs
end
return Stubs