From 2e53e4668d419c02accfcd7b920331e4652a7b4f Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:18:04 +0000 Subject: [PATCH 01/15] Move jail into exp_scenario With the role system finished the jail module has nothing legacy left in it, so it moves to exp_scenario/module/control/jail.lua and the copy in exp_legacy is removed. The module takes LuaPlayer only, requires a reason, returns false rather than nil when there is nothing to do, and exposes its event ids as Jail.on_player_jailed and Jail.on_player_unjailed in the same shape as exp_util's selection events. --- .../module/config/gui/player_list_actions.lua | 6 +- exp_legacy/module/modules/control/jail.lua | 119 ------------------ exp_scenario/module/commands/jail.lua | 2 +- .../module/control/discord_alerts.lua | 6 +- exp_scenario/module/control/jail.lua | 89 +++++++++++++ .../module/control/protection_jail.lua | 2 +- exp_scenario/module/control/report_jail.lua | 2 +- 7 files changed, 98 insertions(+), 128 deletions(-) delete mode 100644 exp_legacy/module/modules/control/jail.lua create mode 100644 exp_scenario/module/control/jail.lua diff --git a/exp_legacy/module/config/gui/player_list_actions.lua b/exp_legacy/module/config/gui/player_list_actions.lua index 146338aa..fe1b3c41 100644 --- a/exp_legacy/module/config/gui/player_list_actions.lua +++ b/exp_legacy/module/config/gui/player_list_actions.lua @@ -10,7 +10,7 @@ local Gui = require("modules/exp_gui") local Roles = require("modules/exp_roles") 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 Jail = require("modules.exp_legacy.modules.control.jail") --- @dep modules.control.jail +local Jail = require("modules/exp_scenario/control/jail") local Colors = require("modules/exp_util/include/color") local format_player_name = ExpUtil.format_player_name_locale @@ -119,7 +119,7 @@ end local jail_player = new_button("utility/multiplayer_waiting_icon", { "exp-gui_player-list.jail-player" }) :on_click(function(def, player, element) local selected_player, selected_player_color = get_action_player(player) - if Jail.is_jailed(selected_player.name) then + if Jail.is_jailed(selected_player) then player.print({ "exp-commands_jail.already-jailed", selected_player_color }, Colors.orange_red) else set_selected_action(player, "exp_scenario.command.jail") @@ -130,7 +130,7 @@ local function jail_player_callback(player, reason) local selected_player, selected_player_color = get_action_player(player) local by_player_name_color = format_player_name(player) game.print{ "exp-commands_jail.jailed", selected_player_color, by_player_name_color, reason } - Jail.jail_player(selected_player.name, player.name, reason) + Jail.jail_player(selected_player, player.name, reason) end --- Kicks the action player, requires a reason diff --git a/exp_legacy/module/modules/control/jail.lua b/exp_legacy/module/modules/control/jail.lua deleted file mode 100644 index 0d9b6cfe..00000000 --- a/exp_legacy/module/modules/control/jail.lua +++ /dev/null @@ -1,119 +0,0 @@ ---[[-- Control Module - Jail - - Adds a way to jail players. - @control Jail - @alias Jail - - @usage - -- import the module from the control modules - local Jail = require("modules.exp_legacy.modules.control.jail") --- @dep modules.control.jail - - -- 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 - Jail.jail_player('MrBiter', 'Cooldude2606', 'Likes biters too much') - - -- 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 - Jail.unjail_player('MrBiter', 'Cooldude2606') -]] - -local Roles = require("modules/exp_roles") - -local valid_player = function(p) return type(p) == "userdata" and p or game.get_player(p) end - ---- The role given to jailed players, it has a higher priority than every other role -local function jail_role() - return assert(Roles.get_role_by_name("Jail"), "The Jail role does not exist") -end - -local Jail = { - events = { - --- When a player is assigned to jail - -- @event on_player_jailed - -- @tparam number player_index the index of the player who was jailed - -- @tparam string by_player_name the name of the player who jailed the other player - -- @tparam string reason the reason that the player was jailed - on_player_jailed = script.generate_event_name(), - --- When a player is unassigned from jail - -- @event on_player_unjailed - -- @tparam number player_index the index of the player who was unjailed - -- @tparam string by_player_name the name of the player who unjailed the other player - on_player_unjailed = script.generate_event_name(), - }, -} - ---- Used to emit the jail related events --- @tparam number event the name of the event that will be emited --- @tparam LuaPlayer player the player who is being acted on --- @tparam string by_player_name the player who is doing the action --- @tparam string reason the reason for the action (jail) -local function event_emit(event, player, by_player_name, reason) - script.raise_event(event, { - name = event, - tick = game.tick, - player_index = player.index, - by_player_name = by_player_name, - reason = reason, - }) -end - ---- Jail. --- Functions related to jail --- @section jail-functions - ---- Checks if the player is currently in jail --- @tparam LuaPlayer player the player to check if they are in jail --- @treturn boolean whether the player is currently in jail -function Jail.is_jailed(player) - local valid = valid_player(player) - return valid ~= nil and jail_role():has_player(valid) -end - ---- Moves a player to jail, which suppresses all of their other roles --- @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[opt='Non given.'] string reason the reason that the player is being jailed --- @treturn boolean wheather the user was jailed successfully -function Jail.jail_player(player, by_player_name, reason) - player = valid_player(player) - if not player then return end - if not by_player_name then return end - - reason = reason or "Non given." - - local role = jail_role() - if role:has_player(player) then return end - - player.walking_state = { walking = false, direction = player.walking_state.direction } - player.riding_state = { acceleration = defines.riding.acceleration.nothing, direction = player.riding_state.direction } - player.mining_state = { mining = false } - player.shooting_state = { state = defines.shooting.not_shooting, position = player.shooting_state.position } - player.picking_state = false - player.repair_state = { repairing = false, position = player.repair_state.position } - - role:assign(player, { by_player_name = by_player_name, silent = true }) - - event_emit(Jail.events.on_player_jailed, player, by_player_name, reason) - - return true -end - ---- Moves a player out of jail, which restores all of their other roles --- @tparam LuaPlayer player the player that will be unjailed --- @tparam string by_player_name the name of the player that is doing the unjail --- @treturn boolean whether the player was unjailed successfully -function Jail.unjail_player(player, by_player_name) - player = valid_player(player) - if not player then return end - if not by_player_name then return end - - local role = jail_role() - if not role:has_player(player) then return end - - role:unassign(player, { by_player_name = by_player_name, silent = true }) - - event_emit(Jail.events.on_player_unjailed, player, by_player_name) - - return true -end - -return Jail diff --git a/exp_scenario/module/commands/jail.lua b/exp_scenario/module/commands/jail.lua index 95c78aee..70b6b7bb 100644 --- a/exp_scenario/module/commands/jail.lua +++ b/exp_scenario/module/commands/jail.lua @@ -5,7 +5,7 @@ Adds a commands that allow admins to jail and unjail local Commands = require("modules/exp_commands") local format_player_name = Commands.format_player_name_locale -local Jail = require("modules.exp_legacy.modules.control.jail") --- @dep modules.control.jail +local Jail = require("modules/exp_scenario/control/jail") --- Puts a player into jail and removes all other roles. Commands.new("jail", { "exp-commands_jail.description" }) diff --git a/exp_scenario/module/control/discord_alerts.lua b/exp_scenario/module/control/discord_alerts.lua index c4675140..a2b3e5e1 100644 --- a/exp_scenario/module/control/discord_alerts.lua +++ b/exp_scenario/module/control/discord_alerts.lua @@ -167,8 +167,8 @@ end --- When a player is jailed or unjailed if config.player_jail then - local Jail = require("modules.exp_legacy.modules.control.jail") - events[Jail.events.on_player_jailed] = function(event) + local Jail = require("modules/exp_scenario/control/jail") + events[Jail.on_player_jailed] = function(event) local player_name, by_player_name = get_player_name(event) emit_event{ title = "Jail", @@ -181,7 +181,7 @@ if config.player_jail then }, } end - events[Jail.events.on_player_unjailed] = function(event) + events[Jail.on_player_unjailed] = function(event) local player_name, by_player_name = get_player_name(event) emit_event{ title = "Unjail", diff --git a/exp_scenario/module/control/jail.lua b/exp_scenario/module/control/jail.lua new file mode 100644 index 00000000..bc4730fa --- /dev/null +++ b/exp_scenario/module/control/jail.lua @@ -0,0 +1,89 @@ +--[[-- Control - Jail +Adds a way to jail players, the jail role suppresses all of their other roles +]] + +local Roles = require("modules/exp_roles") + +--- @class ExpScenario_Jail +local Jail = { + --- Raised when a player is put into jail + --- @type EventData.ExpScenario_Jail.on_player_jailed + on_player_jailed = script.generate_event_name(), + --- Raised when a player is taken out of jail + --- @type EventData.ExpScenario_Jail.on_player_unjailed + on_player_unjailed = script.generate_event_name(), +} + +--- @class EventData.ExpScenario_Jail.on_player_jailed : EventData +--- @field player_index uint +--- @field by_player_name string +--- @field reason string + +--- @class EventData.ExpScenario_Jail.on_player_unjailed : EventData +--- @field player_index uint +--- @field by_player_name string + +--- The role given to jailed players, it has a higher priority than every other role +--- @return ExpRoles.Role +local function jail_role() + return (assert(Roles.get_role_by_name("Jail"), "The Jail role does not exist")) +end + +--- Check if a player is in jail +--- @param player LuaPlayer +--- @return boolean +function Jail.is_jailed(player) + return jail_role():has_player(player) +end + +--- Put a player into jail, which suppresses all of their other roles +--- @param player LuaPlayer +--- @param by_player_name string +--- @param reason string +--- @return boolean # False when the player was already in jail +function Jail.jail_player(player, by_player_name, reason) + local role = jail_role() + if role:has_player(player) then return false end + + -- Stop whatever the player is doing, the jail permission group stops them from starting again + player.walking_state = { walking = false, direction = player.walking_state.direction } + player.riding_state = { acceleration = defines.riding.acceleration.nothing, direction = player.riding_state.direction } + player.mining_state = { mining = false } + player.shooting_state = { state = defines.shooting.not_shooting, position = player.shooting_state.position } + player.picking_state = false + player.repair_state = { repairing = false, position = player.repair_state.position } + + role:assign(player, { by_player_name = by_player_name, silent = true }) + + script.raise_event(Jail.on_player_jailed, { + name = Jail.on_player_jailed, + tick = game.tick, + player_index = player.index, + by_player_name = by_player_name, + reason = reason, + }) + + return true +end + +--- Take a player out of jail, which restores all of their other roles +--- @param player LuaPlayer +--- @param by_player_name string +--- @return boolean # False when the player was not in jail +function Jail.unjail_player(player, by_player_name) + local role = jail_role() + if not role:has_player(player) then return false end + + role:unassign(player, { by_player_name = by_player_name, silent = true }) + + script.raise_event(Jail.on_player_unjailed, { + name = Jail.on_player_unjailed, + tick = game.tick, + player_index = player.index, + by_player_name = by_player_name, + }) + + return true +end + +return Jail diff --git a/exp_scenario/module/control/protection_jail.lua b/exp_scenario/module/control/protection_jail.lua index c39e649b..987d2c6e 100644 --- a/exp_scenario/module/control/protection_jail.lua +++ b/exp_scenario/module/control/protection_jail.lua @@ -4,7 +4,7 @@ When a player triggers protection multiple times they are automatically jailed local ExpUtil = require("modules/exp_util") local Storage = require("modules/exp_util/storage") -local Jail = require("modules.exp_legacy.modules.control.jail") +local Jail = require("modules/exp_scenario/control/jail") local Protection = require("modules.exp_legacy.modules.control.protection") local format_player_name = ExpUtil.format_player_name_locale diff --git a/exp_scenario/module/control/report_jail.lua b/exp_scenario/module/control/report_jail.lua index e4bbdd72..25ec24f9 100644 --- a/exp_scenario/module/control/report_jail.lua +++ b/exp_scenario/module/control/report_jail.lua @@ -3,7 +3,7 @@ When a player is reported, the player is automatically jailed if the combined pl ]] local ExpUtil = require("modules/exp_util") -local Jail = require("modules.exp_legacy.modules.control.jail") +local Jail = require("modules/exp_scenario/control/jail") local Reports = require("modules.exp_legacy.modules.control.reports") local max = math.max From 0c52532c05487b81151a95bd274631a640b7a4f0 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:19:45 +0000 Subject: [PATCH 02/15] Move protection into exp_scenario The entity protection module moves to exp_scenario/module/control/protection.lua in the event_handler shape the other control modules use, and the copy in exp_legacy is removed. The config lookups are built as local sets instead of rewriting the config tables in place, the two mined handlers become one, and the entity and area key helpers are exposed so the protection command no longer keeps its own copies. Event ids are exposed as Protection.on_player_mined_protected and Protection.on_repeat_violation. --- .../module/modules/control/protection.lua | 204 --------------- .../module/commands/protected_entities.lua | 24 +- exp_scenario/module/control.lua | 1 + .../module/control/discord_alerts.lua | 4 +- exp_scenario/module/control/protection.lua | 238 ++++++++++++++++++ .../module/control/protection_jail.lua | 4 +- 6 files changed, 247 insertions(+), 228 deletions(-) delete mode 100644 exp_legacy/module/modules/control/protection.lua create mode 100644 exp_scenario/module/control/protection.lua diff --git a/exp_legacy/module/modules/control/protection.lua b/exp_legacy/module/modules/control/protection.lua deleted file mode 100644 index b0c35a0b..00000000 --- a/exp_legacy/module/modules/control/protection.lua +++ /dev/null @@ -1,204 +0,0 @@ ---[[-- Control Module - Protection - - Controls protected entities - @control Protection - @alias Protection -]] - -local Storage = require("modules/exp_util/storage") -local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event -local config = require("modules.exp_legacy.config.protection") --- @dep config.protection -local EntityProtection = { - protected_entity_names = table.deep_copy(config.always_protected_names), - protected_entity_types = table.deep_copy(config.always_protected_types), - events = { - --- When a player mines a protected entity - -- @event on_player_mined_protected - -- @tparam number player_index the player index of the player who got mined the entity - -- @tparam LuaEntity entity the entity which was mined - on_player_mined_protected = script.generate_event_name(), - --- When a player repeatedly mines protected entities - -- @event on_repeat_violation - -- @tparam number player_index the player index of the player who got mined the entities - -- @tparam LuaEntity entity the last entity which was mined - on_repeat_violation = script.generate_event_name(), - }, -} - --- Convert config tables into lookup tables -for _, config_key in ipairs{ "always_protected_names", "always_protected_types", "always_trigger_repeat_names", "always_trigger_repeat_types" } do - local tbl = config[config_key] - for key, value in ipairs(tbl) do - tbl[key] = nil - tbl[value] = true - end -end - -local Roles = require("modules/exp_roles") - ------ Storage Variables ----- ---- Variables stored in the global table - -local protected_entities = {} -- All entities which are protected -local protected_areas = {} -- All areas which are protected -local repeats = {} -- Stores repeat removals by players - -Storage.register({ - protected_entities = protected_entities, - protected_areas = protected_areas, - repeats = repeats, -}, function(tbl) - protected_entities = tbl.protected_entities - protected_areas = tbl.protected_areas - repeats = tbl.repeats -end) - ------ Local Functions ----- ---- Functions used internally to search and add to the protected array - ---- Get the key used in protected_entities -local function get_entity_key(entity) - return string.format("%i,%i", math.floor(entity.position.x), math.floor(entity.position.y)) -end - ---- Get the key used in protected_areas -local function get_area_key(area) - return string.format("%i,%i", math.floor(area.left_top.x), math.floor(area.left_top.y)) -end - ---- Check if an entity is always protected -local function check_always_protected(entity) - return config.always_protected_names[entity.name] or config.always_protected_types[entity.type] or false -end - ---- Check if an entity always triggers repeat protection -local function check_always_trigger_repeat(entity) - return config.always_trigger_repeat_names[entity.name] or config.always_trigger_repeat_types[entity.type] or false -end - ------ Public Functions ----- ---- Functions used to add and remove protected entities - ---- Add an entity to the protected list -function EntityProtection.add_entity(entity) - local entities = protected_entities[entity.surface.index] - if not entities then - entities = {} - protected_entities[entity.surface.index] = entities - end - entities[get_entity_key(entity)] = entity -end - ---- Remove an entity from the protected list -function EntityProtection.remove_entity(entity) - local entities = protected_entities[entity.surface.index] - if not entities then return end - entities[get_entity_key(entity)] = nil -end - ---- Get all protected entities on a surface -function EntityProtection.get_entities(surface) - return protected_entities[surface.index] or {} -end - ---- Check if an entity is protected -function EntityProtection.is_entity_protected(entity) - if check_always_protected(entity) then return true end - local entities = protected_entities[entity.surface.index] - if not entities then return false end - return entities[get_entity_key(entity)] == entity -end - ---- Add an area to the protected list -function EntityProtection.add_area(surface, area) - local areas = protected_areas[surface.index] - if not areas then - areas = {} - protected_areas[surface.index] = areas - end - areas[get_area_key(area)] = area -end - ---- Remove an area from the protected list -function EntityProtection.remove_area(surface, area) - local areas = protected_areas[surface.index] - if not areas then return end - areas[get_area_key(area)] = nil -end - ---- Get all protected areas on a surface -function EntityProtection.get_areas(surface) - return protected_areas[surface.index] or {} -end - ---- Check if an entity is protected -function EntityProtection.is_position_protected(surface, position) - local areas = protected_areas[surface.index] - if not areas then return false end - for _, area in pairs(areas) do - if area.left_top.x <= position.x and area.left_top.y <= position.y - and area.right_bottom.x >= position.x and area.right_bottom.y >= position.y - then - return true - end - end - - return false -end - ------ Events ----- ---- All events registered by this module - ---- Raise events for protected entities -Event.add(defines.events.on_pre_player_mined_item, function(event) - local entity = event.entity - local player = game.players[event.player_index] - -- Check if the player should be ignored - 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 config.ignore_permission and Roles.player_has_permission(player, config.ignore_permission) then return end - - -- Check if the entity is protected - if EntityProtection.is_entity_protected(entity) - or EntityProtection.is_position_protected(entity.surface, entity.position) - then - -- Update repeats - local player_repeats = repeats[player.name] - if not player_repeats then - player_repeats = { last = game.tick, count = 0 } - repeats[player.name] = player_repeats - end - player_repeats.last = game.tick - player_repeats.count = player_repeats.count + 1 - -- Send events - event.name = EntityProtection.events.on_player_mined_protected - script.raise_event(EntityProtection.events.on_player_mined_protected, event) - if check_always_trigger_repeat(entity) or player_repeats.count >= config.repeat_count then - player_repeats.count = 0 -- Reset to avoid spamming of events - event.name = EntityProtection.events.on_repeat_violation - script.raise_event(EntityProtection.events.on_repeat_violation, event) - end - end -end) - ---- Remove old repeats -Event.on_nth_tick(config.refresh_rate, function() - local old = game.tick - config.repeat_lifetime - for player_name, player_repeats in pairs(repeats) do - if player_repeats.last <= old then - repeats[player_name] = nil - end - end -end) - ---- When an entity is removed remove it from the protection list -local function event_remove_entity(event) - EntityProtection.remove_entity(event.entity) -end - -Event.add(defines.events.on_space_platform_pre_mined, event_remove_entity) -Event.add(defines.events.on_pre_player_mined_item, event_remove_entity) -Event.add(defines.events.on_robot_pre_mined, event_remove_entity) -Event.add(defines.events.on_entity_died, event_remove_entity) -Event.add(defines.events.script_raised_destroy, event_remove_entity) - -return EntityProtection diff --git a/exp_scenario/module/commands/protected_entities.lua b/exp_scenario/module/commands/protected_entities.lua index 11c24fe8..34b66783 100644 --- a/exp_scenario/module/commands/protected_entities.lua +++ b/exp_scenario/module/commands/protected_entities.lua @@ -12,10 +12,9 @@ local Commands = require("modules/exp_commands") local format_player_name = Commands.format_player_name_locale local Roles = require("modules/exp_roles") -local EntityProtection = require("modules.exp_legacy.modules.control.protection") --- @dep modules.control.protection - -local format_string = string.format -local floor = math.floor +local EntityProtection = require("modules/exp_scenario/control/protection") +local get_entity_key = EntityProtection.get_entity_key +local get_area_key = EntityProtection.get_area_key local Selection = require("modules/exp_util/selection") local SelectEntities = Selection.connect("ExpCommand_ProtectEntity") @@ -28,21 +27,6 @@ Storage.register({ renders = tbl.renders end) ---- Get the key used in protected_entities ---- @param entity LuaEntity ---- @return string -local function get_entity_key(entity) - return format_string("%i,%i", floor(entity.position.x), floor(entity.position.y)) -end - ---- Get the key used in protected_areas ---- TODO expose this from EntityProtection ---- @param area BoundingBox.struct ---- @return string -local function get_area_key(area) - return format_string("%i,%i", floor(area.left_top.x), floor(area.left_top.y)) -end - --- Show a protected entity to a player --- @param player LuaPlayer --- @param entity LuaEntity @@ -232,6 +216,6 @@ end return { events = { - [EntityProtection.events.on_repeat_violation] = on_repeat_violation, + [EntityProtection.on_repeat_violation] = on_repeat_violation, } } diff --git a/exp_scenario/module/control.lua b/exp_scenario/module/control.lua index 2e04e552..bf8741e3 100644 --- a/exp_scenario/module/control.lua +++ b/exp_scenario/module/control.lua @@ -60,6 +60,7 @@ add(require("modules/exp_scenario/control/inventory_clear")) add(require("modules/exp_scenario/control/mine_depletion")) add(require("modules/exp_scenario/control/nuke_protection")) add(require("modules/exp_scenario/control/pollution_grading")) +add(require("modules/exp_scenario/control/protection")) add(require("modules/exp_scenario/control/protection_jail")) add(require("modules/exp_scenario/control/report_jail")) add(require("modules/exp_scenario/control/research")) diff --git a/exp_scenario/module/control/discord_alerts.lua b/exp_scenario/module/control/discord_alerts.lua index c4675140..4af0fd9d 100644 --- a/exp_scenario/module/control/discord_alerts.lua +++ b/exp_scenario/module/control/discord_alerts.lua @@ -81,8 +81,8 @@ end --- Repeated protected entity mining if config.entity_protection then - local EntityProtection = require("modules.exp_legacy.modules.control.protection") - events[EntityProtection.events.on_repeat_violation] = function(event) + local EntityProtection = require("modules/exp_scenario/control/protection") + events[EntityProtection.on_repeat_violation] = function(event) local player_name = get_player_name(event) emit_event{ title = "Entity Protection", diff --git a/exp_scenario/module/control/protection.lua b/exp_scenario/module/control/protection.lua new file mode 100644 index 00000000..c8907e0f --- /dev/null +++ b/exp_scenario/module/control/protection.lua @@ -0,0 +1,238 @@ +--[[-- Control - Protection +Protects entities and areas from being mined by players other than the one who placed them +]] + +local Storage = require("modules/exp_util/storage") +local Roles = require("modules/exp_roles") +local config = require("modules.exp_legacy.config.protection") + +local format_string = string.format +local floor = math.floor + +--- @class ExpScenario_Protection +local Protection = { + --- Raised when a player mines a protected entity + --- @type EventData.ExpScenario_Protection.on_player_mined_protected + on_player_mined_protected = script.generate_event_name(), + --- Raised when a player mines protected entities repeatedly, or one which always counts as repeated + --- @type EventData.ExpScenario_Protection.on_repeat_violation + on_repeat_violation = script.generate_event_name(), + --- Names of entities which are always protected + --- @type string[] + protected_entity_names = config.always_protected_names, + --- Types of entities which are always protected + --- @type string[] + protected_entity_types = config.always_protected_types, + --- @package + events = {}, + --- @package + on_nth_tick = {}, +} + +--- @class EventData.ExpScenario_Protection.on_player_mined_protected : EventData.on_pre_player_mined_item +--- @class EventData.ExpScenario_Protection.on_repeat_violation : EventData.on_pre_player_mined_item + +--- @class ExpScenario_Protection.Repeat +--- @field last uint Tick of the last protected removal +--- @field count number Protected removals since the last repeat violation + +--- @param values string[] +--- @return table +local function to_set(values) + local set = {} + for _, value in ipairs(values) do + set[value] = true + end + return set +end + +local always_protected_names = to_set(config.always_protected_names) +local always_protected_types = to_set(config.always_protected_types) +local always_trigger_repeat_names = to_set(config.always_trigger_repeat_names) +local always_trigger_repeat_types = to_set(config.always_trigger_repeat_types) + +local protected_entities = {} --- @type table> Keyed by surface index then entity key +local protected_areas = {} --- @type table> Keyed by surface index then area key +local repeats = {} --- @type table Keyed by player name + +Storage.register({ + protected_entities = protected_entities, + protected_areas = protected_areas, + repeats = repeats, +}, function(tbl) + protected_entities = tbl.protected_entities + protected_areas = tbl.protected_areas + repeats = tbl.repeats +end) + +--- Get the key an entity is stored under +--- @param entity LuaEntity +--- @return string +function Protection.get_entity_key(entity) + return format_string("%i,%i", floor(entity.position.x), floor(entity.position.y)) +end + +--- Get the key an area is stored under +--- @param area BoundingBox +--- @return string +function Protection.get_area_key(area) + return format_string("%i,%i", floor(area.left_top.x), floor(area.left_top.y)) +end + +--- Protect an entity +--- @param entity LuaEntity +function Protection.add_entity(entity) + local entities = protected_entities[entity.surface.index] + if not entities then + entities = {} + protected_entities[entity.surface.index] = entities + end + entities[Protection.get_entity_key(entity)] = entity +end + +--- Remove the protection from an entity +--- @param entity LuaEntity +function Protection.remove_entity(entity) + local entities = protected_entities[entity.surface.index] + if not entities then return end + entities[Protection.get_entity_key(entity)] = nil +end + +--- Get the protected entities on a surface, always protected entities are not included +--- @param surface LuaSurface +--- @return table +function Protection.get_entities(surface) + return protected_entities[surface.index] or {} +end + +--- Check if an entity is protected, either directly or by its name or type +--- @param entity LuaEntity +--- @return boolean +function Protection.is_entity_protected(entity) + if always_protected_names[entity.name] or always_protected_types[entity.type] then return true end + local entities = protected_entities[entity.surface.index] + if not entities then return false end + return entities[Protection.get_entity_key(entity)] == entity +end + +--- Protect every position within an area +--- @param surface LuaSurface +--- @param area BoundingBox +function Protection.add_area(surface, area) + local areas = protected_areas[surface.index] + if not areas then + areas = {} + protected_areas[surface.index] = areas + end + areas[Protection.get_area_key(area)] = area +end + +--- Remove the protection from an area +--- @param surface LuaSurface +--- @param area BoundingBox +function Protection.remove_area(surface, area) + local areas = protected_areas[surface.index] + if not areas then return end + areas[Protection.get_area_key(area)] = nil +end + +--- Get the protected areas on a surface +--- @param surface LuaSurface +--- @return table +function Protection.get_areas(surface) + return protected_areas[surface.index] or {} +end + +--- Check if a position is within a protected area +--- @param surface LuaSurface +--- @param position MapPosition +--- @return boolean +function Protection.is_position_protected(surface, position) + local areas = protected_areas[surface.index] + if not areas then return false end + for _, area in pairs(areas) do + if area.left_top.x <= position.x and area.left_top.y <= position.y + and area.right_bottom.x >= position.x and area.right_bottom.y >= position.y + then + return true + end + end + + return false +end + +--- Players are never checked against their own entities, and can be excluded by permission or admin status +--- @param player LuaPlayer +--- @param entity LuaEntity +--- @return boolean +local function is_ignored(player, entity) + if config.ignore_admins and player.admin then return true end + if entity.last_user == nil or entity.last_user.index == player.index then return true end + if config.ignore_permission and Roles.player_has_permission(player, config.ignore_permission) then return true end + return false +end + +--- Raise the protection events, the event data is reused with the name replaced +--- @param event EventData.on_pre_player_mined_item +--- @param player LuaPlayer +local function raise_violation(event, player) + local player_repeats = repeats[player.name] + if not player_repeats then + player_repeats = { last = game.tick, count = 0 } + repeats[player.name] = player_repeats + end + player_repeats.last = game.tick + player_repeats.count = player_repeats.count + 1 + + event.name = Protection.on_player_mined_protected + script.raise_event(Protection.on_player_mined_protected, event) + + local entity = event.entity + local always_repeat = always_trigger_repeat_names[entity.name] or always_trigger_repeat_types[entity.type] + if always_repeat or player_repeats.count >= config.repeat_count then + player_repeats.count = 0 + event.name = Protection.on_repeat_violation + script.raise_event(Protection.on_repeat_violation, event) + end +end + +--- Raise the protection events when a protected entity is mined, then forget the entity +--- @param event EventData.on_pre_player_mined_item +local function on_pre_player_mined_item(event) + local entity = event.entity + local player = game.players[event.player_index] + if not is_ignored(player, entity) + and (Protection.is_entity_protected(entity) or Protection.is_position_protected(entity.surface, entity.position)) + then + raise_violation(event, player) + end + + Protection.remove_entity(entity) +end + +--- Forget an entity once it no longer exists +--- @param event { entity: LuaEntity } +local function on_entity_removed(event) + Protection.remove_entity(event.entity) +end + +--- Forget protected removals older than the repeat lifetime +local function clear_old_repeats() + local old = game.tick - config.repeat_lifetime + for player_name, player_repeats in pairs(repeats) do + if player_repeats.last <= old then + repeats[player_name] = nil + end + end +end + +local e = defines.events + +Protection.events[e.on_pre_player_mined_item] = on_pre_player_mined_item +Protection.events[e.on_space_platform_pre_mined] = on_entity_removed +Protection.events[e.on_robot_pre_mined] = on_entity_removed +Protection.events[e.on_entity_died] = on_entity_removed +Protection.events[e.script_raised_destroy] = on_entity_removed +Protection.on_nth_tick[config.refresh_rate] = clear_old_repeats + +return Protection diff --git a/exp_scenario/module/control/protection_jail.lua b/exp_scenario/module/control/protection_jail.lua index c39e649b..f71469d4 100644 --- a/exp_scenario/module/control/protection_jail.lua +++ b/exp_scenario/module/control/protection_jail.lua @@ -5,7 +5,7 @@ When a player triggers protection multiple times they are automatically jailed local ExpUtil = require("modules/exp_util") local Storage = require("modules/exp_util/storage") local Jail = require("modules.exp_legacy.modules.control.jail") -local Protection = require("modules.exp_legacy.modules.control.protection") +local Protection = require("modules/exp_scenario/control/protection") local format_player_name = ExpUtil.format_player_name_locale @@ -41,7 +41,7 @@ local e = defines.events return { events = { - [Protection.events.on_repeat_violation] = on_repeat_violation, + [Protection.on_repeat_violation] = on_repeat_violation, [e.on_player_left_game] = on_player_left_game, } } From 8014cb1e774bdfee3ad943d8c4f87fc0b1d06629 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:20:55 +0000 Subject: [PATCH 03/15] Move spectate into exp_scenario The spectate and follow module moves to exp_scenario/module/control/spectate.lua in the event_handler shape the other control modules use, and the copy in exp_legacy is removed. Follow state is a typed record rather than a positional table, the follow label caption is localised, and the escape handler marks the record to stop on the next tick instead of poking an invalid position into it. The LuaPlayer.close_map call is gone: it does not exist in Factorio 2.0, where the map is the remote controller and set_controller replaces it. --- .../module/modules/control/spectate.lua | 173 ------------------ exp_scenario/module/commands/spectate.lua | 2 +- exp_scenario/module/control.lua | 1 + exp_scenario/module/control/spectate.lua | 173 ++++++++++++++++++ exp_scenario/module/locale/en.cfg | 3 + 5 files changed, 178 insertions(+), 174 deletions(-) delete mode 100644 exp_legacy/module/modules/control/spectate.lua create mode 100644 exp_scenario/module/control/spectate.lua diff --git a/exp_legacy/module/modules/control/spectate.lua b/exp_legacy/module/modules/control/spectate.lua deleted file mode 100644 index dc2784f8..00000000 --- a/exp_legacy/module/modules/control/spectate.lua +++ /dev/null @@ -1,173 +0,0 @@ -local Storage = require("modules/exp_util/storage") -local Gui = require("modules/exp_gui") - -local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event - ------ Locals ----- -local follow_label --- @type ExpElement Gui constructor -local following = {} -local spectating = {} -local Public = {} - ------ Storage data ----- -Storage.register({ - following = following, - spectating = spectating, -}, function(tbl) - following = tbl.following - spectating = tbl.spectating -end) - ------ Public Functions ----- - ---- Test if a player is in spectator mode --- @tparam LuaPlayer player The player to test the controller type of --- @treturn boolean Returns true if the player is in spectator mode -function Public.is_spectating(player) - assert(player and player.valid, "Invalid player given to follower") - return player.controller_type == defines.controllers.spectator -end - ---- Puts a player into spectator mode while maintaining an association to their character --- @tparam LuaPlayer player The player that will be placed into spectator mode --- @treturn boolean Returns false if the player was already in spectator mode -function Public.start_spectate(player) - assert(player and player.valid, "Invalid player given to follower") - if spectating[player.index] or not player.character then return false end - local character = player.character - local opened = player.opened - player.set_controller{ type = defines.controllers.spectator } - player.associate_character(character) - spectating[player.index] = character - if opened then player.opened = opened end -- Maintain opened after controller change - return true -end - ---- Return a player from spectator mode back to their character, if their character was killed then respawn them --- @tparam LuaPlayer player The player that will leave spectator mode -function Public.stop_spectate(player) - assert(player and player.valid, "Invalid player given to follower") - local character = spectating[player.index] - spectating[player.index] = nil - if character and character.valid then - local opened = player.opened - player.teleport(character.position, character.surface) - player.set_controller{ type = defines.controllers.character, character = character } - if opened then player.opened = opened end -- Maintain opened after controller change - else - player.ticks_to_respawn = 300 - end -end - ---- Test if a player is in follow mode --- @tparam LuaPlayer player The player to test the follow mode of --- @treturn boolean Returns true if the player is in follow mode -function Public.is_following(player) - assert(player and player.valid, "Invalid player given to follower") - return following[player.index] ~= nil -end - ---- Puts a player into spectator mode and follows an entity as it moves --- @tparam LuaPlayer player The player that will follow the entity --- @tparam ?LuaPlayer|LuaEntity entity The player or entity that will be followed -function Public.start_follow(player, entity) - assert(player and player.valid, "Invalid player given to follower") - assert(entity and entity.valid, "Invalid entity given to follower") - local spectate = Public.start_spectate(player) - - player.close_map() - follow_label(player.gui.screen, entity) - player.teleport(entity.position, entity.surface) - following[player.index] = { player, entity, entity.position, spectate } -end - ---- Returns camera control to the player, will return a player to their character if start_follow placed them into spectator mode --- @tparam LuaPlayer player The player that will regain control of their camera -function Public.stop_follow(player) - assert(player and player.valid, "Invalid player given to follower") - if following[player.index] and following[player.index][4] and Public.is_spectating(player) then - Public.stop_spectate(player) - end - - Gui.destroy_if_valid(player.gui.screen.follow_label) - following[player.index] = nil -end - ---- Returns camera control to all players, will return a player to their character if start_follow placed them into spectator mode -function Public.stop_all() - for key, data in pairs(following) do - Public.stop_follow(data[1]) - end -end - ------ Gui ----- - ---- Label used to show that the player is following, also used to allow esc to stop following -follow_label = Gui.define("follow-label") - :draw(function(def, parent, target) - Gui.destroy_if_valid(parent.follow_label) - - local label = parent.add{ - type = "label", - name = "follow_label", - style = "frame_title", - caption = "Following " .. target.name .. ".\nClick here or press esc to stop following.", - } - - local player = Gui.get_player(parent) - local res = player.display_resolution - label.location = { 0, res.height - 150 } - label.style.width = res.width - label.style.horizontal_align = "center" - player.opened = label - - return label - end) - :on_click(function(def, player, element) - Public.stop_follow(player) - end) - :on_closed(function(def, player, element) - -- Don't call set_controller during on_close as it invalidates the controller - -- Setting an invalid position (as to not equal their current) will call stop_follow on the next tick - following[player.index][3] = {} - end) - ------ Events ----- - ---- Updates the location of the player as well as doing some sanity checks --- @tparam LuaPlayer player The player to update the position of --- @tparam ?LuaPlayer|LuaEntity entity The player or entity being followed -local function update_player_location(player, entity, old_position) - if player.character or not entity.valid then - Public.stop_follow(player) - elseif player.position.x ~= old_position.x or player.position.y ~= old_position.y then - Public.stop_follow(player) - else - player.teleport(entity.position, entity.surface) - end -end - ---- Updates the locations of all players currently following something -local function update_all() - for _, data in pairs(following) do - update_player_location(data[1], data[2], data[3]) - data[3] = data[1].position - end -end - --- Update the location of all players each tick -Event.add(defines.events.on_tick, update_all) - --- Check for player leaving -Event.add(defines.events.on_pre_player_left_game, function(event) - local player = game.players[event.player_index] - Public.stop_follow(player) - for _, data in pairs(following) do - if data[2] == player then - Public.stop_follow(data[1]) - end - end -end) - ------ Module Return ----- -return Public diff --git a/exp_scenario/module/commands/spectate.lua b/exp_scenario/module/commands/spectate.lua index 13907289..816d3152 100644 --- a/exp_scenario/module/commands/spectate.lua +++ b/exp_scenario/module/commands/spectate.lua @@ -3,7 +3,7 @@ Adds commands relating to spectate and follow ]] local Commands = require("modules/exp_commands") -local Spectate = require("modules.exp_legacy.modules.control.spectate") --- @dep modules.control.spectate +local Spectate = require("modules/exp_scenario/control/spectate") --- Toggles spectator mode for the caller Commands.new("spectate", { "exp-commands_spectate.description-spectate" }) diff --git a/exp_scenario/module/control.lua b/exp_scenario/module/control.lua index 2e04e552..08f8a3f2 100644 --- a/exp_scenario/module/control.lua +++ b/exp_scenario/module/control.lua @@ -65,6 +65,7 @@ add(require("modules/exp_scenario/control/report_jail")) 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/spectate")) add(require("modules/exp_scenario/control/station_auto_name")) --- Guis diff --git a/exp_scenario/module/control/spectate.lua b/exp_scenario/module/control/spectate.lua new file mode 100644 index 00000000..b8cac7be --- /dev/null +++ b/exp_scenario/module/control/spectate.lua @@ -0,0 +1,173 @@ +--[[-- Control - Spectate +Lets players spectate without losing their character, and follow other players or entities +]] + +local Storage = require("modules/exp_util/storage") +local Gui = require("modules/exp_gui") + +--- @class ExpScenario_Spectate +local Spectate = { + --- @package + events = {}, +} + +--- @class ExpScenario_Spectate.Following +--- @field player LuaPlayer +--- @field target LuaPlayer | LuaEntity +--- @field position MapPosition Where the player was after the last update, moving away stops following +--- @field started_spectate boolean True when start_follow put the player into spectator mode +--- @field stop boolean? Set when following must end on the next tick + +local following = {} --- @type table Keyed by player index +local spectating = {} --- @type table The character a player returns to, keyed by player index + +Storage.register({ + following = following, + spectating = spectating, +}, function(tbl) + following = tbl.following + spectating = tbl.spectating +end) + +--- Label shown while following, clicking it or pressing escape stops following +--- @class ExpScenario_Spectate.follow_label: ExpElement +--- @overload fun(parent: LuaGuiElement, target: LuaPlayer | LuaEntity): LuaGuiElement +local follow_label = Gui.define("spectate/follow_label") + :draw(function(_, parent, target) + Gui.destroy_if_valid(parent.follow_label) + + local label = parent.add{ + type = "label", + name = "follow_label", + style = "frame_title", + caption = { "exp_spectate.follow-label", target.name }, + } + + local player = Gui.get_player(parent) + local res = player.display_resolution + label.location = { 0, res.height - 150 } + label.style.width = res.width + label.style.horizontal_align = "center" + player.opened = label + + return label + end) + :on_click(function(_, player) + Spectate.stop_follow(player) + end) + :on_closed(function(_, player) + -- set_controller can not be called during on_gui_closed, so the next update stops following + local data = following[player.index] + if data then data.stop = true end + end) --[[@as any]] + +--- Check if a player is in spectator mode +--- @param player LuaPlayer +--- @return boolean +function Spectate.is_spectating(player) + return player.controller_type == defines.controllers.spectator +end + +--- Put a player into spectator mode while keeping their character +--- @param player LuaPlayer +--- @return boolean # False when the player was already spectating or has no character +function Spectate.start_spectate(player) + if spectating[player.index] or not player.character then return false end + local character = player.character + local opened = player.opened + player.set_controller{ type = defines.controllers.spectator } + player.associate_character(character) + spectating[player.index] = character + if opened then player.opened = opened end -- Changing controller closes the opened gui + return true +end + +--- Return a player to their character, or respawn them if it was killed +--- @param player LuaPlayer +function Spectate.stop_spectate(player) + local character = spectating[player.index] + spectating[player.index] = nil + if character and character.valid then + local opened = player.opened + player.teleport(character.position, character.surface) + player.set_controller{ type = defines.controllers.character, character = character } + if opened then player.opened = opened end -- Changing controller closes the opened gui + else + player.ticks_to_respawn = 300 + end +end + +--- Check if a player is following something +--- @param player LuaPlayer +--- @return boolean +function Spectate.is_following(player) + return following[player.index] ~= nil +end + +--- Put a player into spectator mode and keep their camera on a target as it moves +--- @param player LuaPlayer +--- @param target LuaPlayer | LuaEntity +function Spectate.start_follow(player, target) + local started_spectate = Spectate.start_spectate(player) + + follow_label(player.gui.screen, target) + player.teleport(target.position, target.surface) + following[player.index] = { + player = player, + target = target, + position = player.position, + started_spectate = started_spectate, + } +end + +--- Give a player their camera back, returning them to their character if start_follow took it +--- @param player LuaPlayer +function Spectate.stop_follow(player) + local data = following[player.index] + if data and data.started_spectate and Spectate.is_spectating(player) then + Spectate.stop_spectate(player) + end + + Gui.destroy_if_valid(player.gui.screen.follow_label) + following[player.index] = nil +end + +--- Move a following player onto their target, or stop following when they have moved away or regained a character +--- @param data ExpScenario_Spectate.Following +local function update_following(data) + local player, target = data.player, data.target + local position = player.position + if data.stop or player.character or not target.valid + or position.x ~= data.position.x or position.y ~= data.position.y + then + Spectate.stop_follow(player) + else + player.teleport(target.position, target.surface) + data.position = player.position + end +end + +local function on_tick() + for _, data in pairs(following) do + update_following(data) + end +end + +--- Stop following when a player leaves, and stop anyone who was following them +--- @param event EventData.on_pre_player_left_game +local function on_pre_player_left_game(event) + local player = game.players[event.player_index] + Spectate.stop_follow(player) + for _, data in pairs(following) do + if data.target == player then + Spectate.stop_follow(data.player) + end + end +end + +local e = defines.events + +Spectate.events[e.on_tick] = on_tick +Spectate.events[e.on_pre_player_left_game] = on_pre_player_left_game + +return Spectate diff --git a/exp_scenario/module/locale/en.cfg b/exp_scenario/module/locale/en.cfg index 80e8a1ec..ff47c27c 100644 --- a/exp_scenario/module/locale/en.cfg +++ b/exp_scenario/module/locale/en.cfg @@ -577,3 +577,6 @@ chat-jailed=__1__ was jailed because they removed too many protected entities. P [exp_report-jail] chat-jailed=__1__ was jailed because they were reported too many times. Please wait for a moderator. + +[exp_spectate] +follow-label=Following __1__.\nClick here or press escape to stop following. From a59b2a8414f712e4e1c71f31e7ed2c09e2e1dfc2 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:22:37 +0000 Subject: [PATCH 04/15] Remove the warning system Warnings were a moderator tool layered on top of reports and had no callers left outside their own command and the player list button. The legacy module, its config and locale, the exp_scenario command and its permissions, the discord alert block and the player list button are all removed. Reporting from the player list no longer depends on whether the player could give warnings, anyone can report a player who is not immune. --- exp_legacy/module/config/discord_alerts.lua | 1 - .../module/config/gui/player_list_actions.lua | 26 +- exp_legacy/module/config/warnings.lua | 38 -- exp_legacy/module/locale/en/addons.cfg | 11 - exp_legacy/module/locale/en/data.cfg | 5 - exp_legacy/module/locale/en/gui.cfg | 1 - exp_legacy/module/locale/zh-CN/addons.cfg | 11 - exp_legacy/module/locale/zh-CN/data.cfg | 5 - exp_legacy/module/locale/zh-CN/gui.cfg | 1 - exp_legacy/module/locale/zh-TW/addons.cfg | 11 - exp_legacy/module/locale/zh-TW/data.cfg | 5 - exp_legacy/module/locale/zh-TW/gui.cfg | 1 - .../module/modules/control/warnings.lua | 331 ------------------ exp_roles/seed.ts | 5 - exp_scenario/module/commands/warnings.lua | 96 ----- exp_scenario/module/control.lua | 1 - .../module/control/discord_alerts.lua | 34 -- exp_scenario/module/locale/en.cfg | 20 -- exp_scenario/module/locale/zh-CN.cfg | 19 - exp_scenario/module/locale/zh-TW.cfg | 19 - exp_scenario/permissions.ts | 5 - 21 files changed, 2 insertions(+), 644 deletions(-) delete mode 100644 exp_legacy/module/config/warnings.lua delete mode 100644 exp_legacy/module/modules/control/warnings.lua delete mode 100644 exp_scenario/module/commands/warnings.lua diff --git a/exp_legacy/module/config/discord_alerts.lua b/exp_legacy/module/config/discord_alerts.lua index 11dfb6a8..bb93294e 100644 --- a/exp_legacy/module/config/discord_alerts.lua +++ b/exp_legacy/module/config/discord_alerts.lua @@ -5,7 +5,6 @@ return { show_playtime = true, entity_protection = true, player_reports = true, - player_warnings = true, player_bans = true, player_mutes = true, player_kicks = true, diff --git a/exp_legacy/module/config/gui/player_list_actions.lua b/exp_legacy/module/config/gui/player_list_actions.lua index 146338aa..736aa2b2 100644 --- a/exp_legacy/module/config/gui/player_list_actions.lua +++ b/exp_legacy/module/config/gui/player_list_actions.lua @@ -9,7 +9,6 @@ local ExpUtil = require("modules/exp_util") local Gui = require("modules/exp_gui") local Roles = require("modules/exp_roles") 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 Jail = require("modules.exp_legacy.modules.control.jail") --- @dep modules.control.jail local Colors = require("modules/exp_util/include/color") local format_player_name = ExpUtil.format_player_name_locale @@ -100,20 +99,6 @@ local function report_player_callback(player, reason) Reports.report_player(selected_player.name, player.name, reason) end ---- Gives the action player a warning, requires a reason --- @element warn_player -local warn_player = new_button("utility/spawn_flag", { "exp-gui_player-list.warn-player" }) - :on_click(function(def, player, element) - set_selected_action(player, "exp_scenario.command.create_warning") - end) - -local function warn_player_callback(player, reason) - local selected_player, selected_player_color = get_action_player(player) - local by_player_name_color = format_player_name(player) - game.print{ "exp-commands_warnings.create", selected_player_color, by_player_name_color, reason } - Warnings.add_warning(selected_player.name, player.name, reason) -end - --- Jails the action player, requires a reason -- @element jail_player local jail_player = new_button("utility/multiplayer_waiting_icon", { "exp-gui_player-list.jail-player" }) @@ -170,18 +155,11 @@ return { ["exp_scenario.command.create_report"] = { auth = function(player, selected_player) if player == selected_player then return false end - if not Roles.player_has_permission(player, "exp_scenario.command.create_warning") then - return not Roles.player_has_permission(selected_player, "exp_scenario.bypass.reports") - end - end, -- can report any player that isn't immune and you aren't able to give warnings + return not Roles.player_has_permission(selected_player, "exp_scenario.bypass.reports") + end, -- can report any player that isn't immune reason_callback = report_player_callback, report_player, }, - ["exp_scenario.command.create_warning"] = { - auth = Roles.player_outranks, -- warn a lower user, replaces report - reason_callback = warn_player_callback, - warn_player, - }, ["exp_scenario.command.jail"] = { auth = Roles.player_outranks, reason_callback = jail_player_callback, diff --git a/exp_legacy/module/config/warnings.lua b/exp_legacy/module/config/warnings.lua deleted file mode 100644 index b60e003b..00000000 --- a/exp_legacy/module/config/warnings.lua +++ /dev/null @@ -1,38 +0,0 @@ ---- Config file for the warning system, this is very similar to reports but is for the use of moderators rather than normal users. --- @config Warnings - -return { - --- @type (LocalisedString[] | fun(player: LuaPlayer, by_player_name: string, number_of_warnings: number))[] - actions = { --- @setting actions what actions are taking at number of warnings - -- if a localized string is used then __1__ will by_player_name and __2__ will be the current warning count (auto inserted) - { "warnings.received", "" }, - { "warnings.received", "" }, - { "warnings.received", { "warnings.pre-kick" } }, - function(player, by_player_name, number_of_warnings) - local str = { - "You received a warning from ", - by_player_name, - ". You have ", - number_of_warnings, - " warnings. You were kicked for having too many warnings; you may rejoin if you wish.", - } - game.kick_player(player, table.concat(str, "")) -- Does not support locale strings - -- game.kick_player(player, { "warnings.received", by_player_name, number_of_warnings, { "warnings.kick" } }) - end, - { "warnings.received", { "warnings.pre-pre-ban" } }, - { "warnings.received", { "warnings.pre-ban" } }, - function(player, by_player_name, number_of_warnings) - local str = { - "You received a warning from ", - by_player_name, - ". You have ", - number_of_warnings, - " warnings. You were banned for having too many warnings; visit https://www.explosivegaming.nl to request a ban appeal.", - } - game.kick_player(player, table.concat(str, "")) -- Does not support locale strings - -- game.ban_player(player, { "warnings.received", by_player_name, number_of_warnings, { "warnings.ban", { "links.website" } } }) - end, - }, - script_warning_cool_down = 30, --- @setting script_warning_cool_down time for a script warning (given by script) to be removed (in minutes) - script_warning_limit = 5, --- @setting script_warning_limit the number of script warnings (given by script) that are allowed before full warnings are given -} diff --git a/exp_legacy/module/locale/en/addons.cfg b/exp_legacy/module/locale/en/addons.cfg index 0cf56506..5807e65b 100644 --- a/exp_legacy/module/locale/en/addons.cfg +++ b/exp_legacy/module/locale/en/addons.cfg @@ -18,14 +18,3 @@ read-readme=Make sure you have read the information gui (It can be found through softmod=We run a softmod on our servers. A softmod is a custom scenario that runs on this server, an example is the player list. redmew=We don't talk about redmew here; they beat us to 1000 members ;-; lhd=All trains must be LHD! This is a long standing rule on our servers, please respect this. - -[warnings] -received=You received a warning from __1__. You have __2__ warnings. __3__ -pre-kick=This is your last warning before you are kicked. -kick=You were kicked for having too many warnings; you may rejoin if you wish. -pre-pre-ban=You are close to receiving a ban; successful ban appeals are unlikely. -pre-ban=This your LAST warning before you are BANNED! Successful ban appeals are unlikely. -ban=You were banned for having too many warnings; visit __1__ to request a ban appeal. -script-warning=You are receiving script warnings; if you recive too many you will receive a permanent warning (__1__/__2__) -script-warning-removed=A script warning has expired (__1__/__2__) -script-warning-limit=__1__ has received a permanent warning from the script. diff --git a/exp_legacy/module/locale/en/data.cfg b/exp_legacy/module/locale/en/data.cfg index 529e7852..8fc02534 100644 --- a/exp_legacy/module/locale/en/data.cfg +++ b/exp_legacy/module/locale/en/data.cfg @@ -10,11 +10,6 @@ message-cleared=Your join message has been cleared. [quickbar] saved=Your quickbar filters have been saved. -[exp-required] -Warnings=Warnings -Warnings-tooltip=The total number of warnings you have received from staff -Warnings-value-tooltip=The total number of warnings you have received from staff - [exp-settings] Colour=Colour Colour-tooltip=Your player colour diff --git a/exp_legacy/module/locale/en/gui.cfg b/exp_legacy/module/locale/en/gui.cfg index c83514fc..5e6bb78a 100644 --- a/exp_legacy/module/locale/en/gui.cfg +++ b/exp_legacy/module/locale/en/gui.cfg @@ -7,7 +7,6 @@ reason-entry=Enter Reason goto-player=Goto player bring-player=Bring player report-player=Report player -warn-player=Warn player jail-player=Jail player kick-player=Kick player ban-player=Ban player diff --git a/exp_legacy/module/locale/zh-CN/addons.cfg b/exp_legacy/module/locale/zh-CN/addons.cfg index f586a2ac..ba29b829 100644 --- a/exp_legacy/module/locale/zh-CN/addons.cfg +++ b/exp_legacy/module/locale/zh-CN/addons.cfg @@ -18,14 +18,3 @@ read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看 softmod=這裹用了自設情境,是一種軟裝模組。 redmew= lhd=列車必須是左則通行。這是本服務器長久以來的規則。 - -[warnings] -received=你已被 __1__ 警告了,現在共有 __2__ 個警告。 __3__ -pre-kick=這是在被踢離之前的最後警告。 -kick=你已因為被警告太多次而被請離,不過你之後還是可以回來的。 -pre-pre-ban=按照你的行為,你有可能會被封禁。 -pre-ban=這是在被封禁之前的最後警告。 -ban=你已因為被警告太多次而被封禁; 可以到 __1__ 申訴. -script-warning=這是系統發出的自動警告 (__1__/__2__) -script-warning-removed=系統發出的自動警告已失效 (__1__/__2__) -script-warning-limit=__1__ 已被系統發出了一則自動警告。 diff --git a/exp_legacy/module/locale/zh-CN/data.cfg b/exp_legacy/module/locale/zh-CN/data.cfg index 96a1b4b1..d8ce31ea 100644 --- a/exp_legacy/module/locale/zh-CN/data.cfg +++ b/exp_legacy/module/locale/zh-CN/data.cfg @@ -10,11 +10,6 @@ message-cleared=你的加入信息已清除。 [quickbar] saved=你的工具列已儲存。 -[exp-required] -Warnings=警告 -Warnings-tooltip=你所有收到的警告 -Warnings-value-tooltip=你所有收到的警告 - [exp-settings] Colour=顏色 Colour-tooltip=你的人物顏色 diff --git a/exp_legacy/module/locale/zh-CN/gui.cfg b/exp_legacy/module/locale/zh-CN/gui.cfg index fbadf609..fbcdd618 100644 --- a/exp_legacy/module/locale/zh-CN/gui.cfg +++ b/exp_legacy/module/locale/zh-CN/gui.cfg @@ -7,7 +7,6 @@ reason-entry=輸入原因 goto-player=傳送到用戶 bring-player=傳送用戶到自己 report-player=舉報用戶 -warn-player=警告用戶 jail-player=監禁用戶 kick-player=踢除用戶 ban-player=封禁用戶 diff --git a/exp_legacy/module/locale/zh-TW/addons.cfg b/exp_legacy/module/locale/zh-TW/addons.cfg index f586a2ac..ba29b829 100644 --- a/exp_legacy/module/locale/zh-TW/addons.cfg +++ b/exp_legacy/module/locale/zh-TW/addons.cfg @@ -18,14 +18,3 @@ read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看 softmod=這裹用了自設情境,是一種軟裝模組。 redmew= lhd=列車必須是左則通行。這是本服務器長久以來的規則。 - -[warnings] -received=你已被 __1__ 警告了,現在共有 __2__ 個警告。 __3__ -pre-kick=這是在被踢離之前的最後警告。 -kick=你已因為被警告太多次而被請離,不過你之後還是可以回來的。 -pre-pre-ban=按照你的行為,你有可能會被封禁。 -pre-ban=這是在被封禁之前的最後警告。 -ban=你已因為被警告太多次而被封禁; 可以到 __1__ 申訴. -script-warning=這是系統發出的自動警告 (__1__/__2__) -script-warning-removed=系統發出的自動警告已失效 (__1__/__2__) -script-warning-limit=__1__ 已被系統發出了一則自動警告。 diff --git a/exp_legacy/module/locale/zh-TW/data.cfg b/exp_legacy/module/locale/zh-TW/data.cfg index 96a1b4b1..d8ce31ea 100644 --- a/exp_legacy/module/locale/zh-TW/data.cfg +++ b/exp_legacy/module/locale/zh-TW/data.cfg @@ -10,11 +10,6 @@ message-cleared=你的加入信息已清除。 [quickbar] saved=你的工具列已儲存。 -[exp-required] -Warnings=警告 -Warnings-tooltip=你所有收到的警告 -Warnings-value-tooltip=你所有收到的警告 - [exp-settings] Colour=顏色 Colour-tooltip=你的人物顏色 diff --git a/exp_legacy/module/locale/zh-TW/gui.cfg b/exp_legacy/module/locale/zh-TW/gui.cfg index fbadf609..fbcdd618 100644 --- a/exp_legacy/module/locale/zh-TW/gui.cfg +++ b/exp_legacy/module/locale/zh-TW/gui.cfg @@ -7,7 +7,6 @@ reason-entry=輸入原因 goto-player=傳送到用戶 bring-player=傳送用戶到自己 report-player=舉報用戶 -warn-player=警告用戶 jail-player=監禁用戶 kick-player=踢除用戶 ban-player=封禁用戶 diff --git a/exp_legacy/module/modules/control/warnings.lua b/exp_legacy/module/modules/control/warnings.lua deleted file mode 100644 index 6a0fa116..00000000 --- a/exp_legacy/module/modules/control/warnings.lua +++ /dev/null @@ -1,331 +0,0 @@ ---[[-- Control Module - Warnings - - Adds a way to give and remove warnings to players. - @control Warnings - @alias Warnings - - @usage - -- import the module from the control modules - local Warnings = require("modules.exp_legacy.modules.control.warnings") --- @dep modules.control.warnings - - -- This will add a warning to the player - Warnings.add_warning('MrBiter', 'Cooldude2606', 'Killed too many biters') - - -- This will remove a warning from a player, second name is just who is doing the action - Warnings.remove_warning('MrBiter', 'Cooldude2606') - - -- Script warning as similar to normal warning but are designed to have no effect for a short amount of time - -- this is so it can be used for greifer protection without being too agressive - Warnings.add_script_warning('MrBiter', 'Killed too many biters') - - -- Both normal and script warnings can also be cleared, this will remove all warnings - Warnings.clear_warnings('MrBiter', 'Cooldude2606') -]] - -local Event = require("modules/exp_legacy/utils/event") -local Storage = require("modules/exp_util/storage") -local config = require("modules.exp_legacy.config.warnings") - -local valid_player = function(p) return type(p) == "userdata" and p or game.get_player(p) end - ---- Stores the quickbar filters for a player -local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data -local PlayerWarnings = PlayerData.Required:combine("Warnings") -PlayerWarnings:set_metadata{ - stringify = function(value) - if not value then return "You have no warnings" end - local count = 0 - for _ in pairs(value) do count = count + 1 end - - return "You have " .. count .. " warnings" - end, -} - -local Warnings = { - user_warnings = PlayerWarnings, - user_script_warnings = {}, - events = { - --- When a warning is added to a player - -- @event on_warning_added - -- @tparam number player_index the index of the player who recived the warning - -- @tparam string by_player_name the name of the player who gave the warning - -- @tparam string reason the reason that the player was given a warning - -- @tparam number warning_count the new number of warnings that the player has - on_warning_added = script.generate_event_name(), - --- When a warning is removed from a player - -- @event on_warning_removed - -- @tparam number player_index the index of the player who is having the warning removed - -- @tparam string warning_by_name the name of the player who gave the warning - -- @tparam string removed_by_name the name of the player who is removing the warning - -- @tparam number warning_count the new number of warnings that the player has - -- @tparam number batch_count the number of warnings removed in this batch, always one when not a batch - -- @tparam number batch the index of this event in a batch, always one when not a batch - on_warning_removed = script.generate_event_name(), - --- When a warning is added to a player, by the script - -- @event on_script_warning_added - -- @tparam number player_index the index of the player who recived the warning - -- @tparam string reason the reason that the player was given a warning - -- @tparam number warning_count the new number of warnings that the player has - on_script_warning_added = script.generate_event_name(), - --- When a warning is removed from a player, by the script - -- @event on_script_warning_removed - -- @tparam number player_index the index of the player who is having the warning removed - -- @tparam number warning_count the new number of warnings that the player has - on_script_warning_removed = script.generate_event_name(), - }, -} - -local user_script_warnings = Warnings.user_script_warnings -Storage.register(user_script_warnings, function(tbl) - Warnings.user_script_warnings = tbl - user_script_warnings = tbl -end) - ---- Gets an array of warnings that the player has, always returns a list even if empty --- @tparam LuaPlayer player the player to get the warning for --- @treturn table an array of all the warnings on this player, contains tick, by_player_name and reason -function Warnings.get_warnings(player) - return PlayerWarnings:get(player.name, {}) -end - ---- Gets the number of warnings that a player has on them --- @tparam LuaPlayer player the player to count the warnings for --- @treturn number the number of warnings that the player has -function Warnings.count_warnings(player) - local warnings = PlayerWarnings:get(player.name, {}) - return #warnings -end - ---- Adds a warning to a player, when a warning is added a set action is done based on the number of warnings and the config file --- @tparam LuaPlayer player the player to add a warning to --- @tparam string by_player_name the name of the player who is doing the action --- @tparam[opt='Non given.'] string reason the reason that the player is being warned --- @treturn number the number of warnings that the player has -function Warnings.add_warning(player, by_player_name, reason) - player = valid_player(player) - if not player then return end - if not by_player_name then return end - - reason = reason or "None given." - - local warning_count --- @type number - PlayerWarnings:update(player.name, function(_, warnings) - local warning = { - by_player_name = by_player_name, - reason = reason, - } - - if not warnings then - warning_count = 1 - return { warning } - else - table.insert(warnings, warning) - warning_count = #warnings - end - end) - - script.raise_event(Warnings.events.on_warning_added, { - name = Warnings.events.on_warning_added, - tick = game.tick, - player_index = player.index, - warning_count = warning_count, - by_player_name = by_player_name, - reason = reason, - }) - - local action = config.actions[warning_count] - if action then - local _type = type(action) - if _type == "function" then - action(player, by_player_name, warning_count) - elseif _type == "table" then - local current = table.deepcopy(action) - table.insert(current, 2, by_player_name) - table.insert(current, 3, warning_count) - player.print(current) - elseif type(action) == "string" then - player.print(action) - end - end - - return warning_count -end - ---- Event trigger for removing a waring due to it being looped in clear warnings --- @tparam LuaPlayer player the player who is having a warning removed --- @tparam string warning_by_name the name of the player who made the warning --- @tparam string removed_by_name the name of the player who is doing the action --- @tparam number warning_count the number of warnings that the player how has --- @tparam number batch the index of this event in a batch, always one when not a batch --- @tparam number batch_count the number of reports removed in this batch, always one when not a batch -local function warning_removed_event(player, warning_by_name, removed_by_name, warning_count, batch, batch_count) - script.raise_event(Warnings.events.on_warning_removed, { - name = Warnings.events.on_warning_removed, - tick = game.tick, - player_index = player.index, - warning_count = warning_count, - warning_by_name = warning_by_name, - removed_by_name = removed_by_name, - batch_count = batch_count or 1, - batch = batch or 1, - }) -end - ---- Removes a warning from a player, always removes the earliest warning, fifo --- @tparam LuaPlayer player the player to remove a warning from --- @tparam string by_player_name the name of the player who is doing the action --- @treturn number the number of warnings that the player has -function Warnings.remove_warning(player, by_player_name) - player = valid_player(player) - if not player then return end - if not by_player_name then return end - - local warning, warning_count - PlayerWarnings:update(player.name, function(_, warnings) - if not warnings then return end - warning = table.remove(warnings, 1) - warning_count = #warnings - end) - - if not warning then return end - warning_removed_event(player, warning.by_player_name, by_player_name, warning_count) - - return warning_count -end - ---- Removes all warnings from a player, will trigger remove event for each warning --- @tparam LuaPlayer player the player to clear the warnings from --- @tparam string by_player_name the name of the player who is doing the action --- @treturn boolean true when warnings were cleared succesfully -function Warnings.clear_warnings(player, by_player_name) - player = valid_player(player) - if not player then return end - if not by_player_name then return end - - local warnings = PlayerWarnings:get(player) - if not warnings then return end - - local warning_count = #warnings - for n, warning in pairs(warnings) do - warning_removed_event(player, warning.by_player_name, by_player_name, warning_count - n, n, warning_count) - end - - PlayerWarnings:remove(player) - return true -end - ---- Gets an array of all the script warnings that a player has --- @tparam LuaPlayer player the player to get the script warnings of --- @treturn table a table of all the script warnings a player has, contains tick and reason -function Warnings.get_script_warnings(player) - return user_script_warnings[player.name] or {} -end - ---- Gets the number of script warnings that a player has on them --- @tparam LuaPlayer player the player to count the script warnings of --- @treturn number the number of script warnings that the player has -function Warnings.count_script_warnings(player) - local warnings = user_script_warnings[player.name] or {} - return #warnings -end - ---- Adds a script warning to a player, this may add a full warning if max script warnings is met --- @tparam LuaPlayer player the player to add a script warning to --- @tparam[opt='Non given.'] string reason the reason that the player is being warned --- @treturn number the number of script warnings that the player has -function Warnings.add_script_warning(player, reason) - player = valid_player(player) - if not player then return end - - reason = reason or "Non given." - - local warnings = user_script_warnings[player.name] - if not warnings then - warnings = {} - user_script_warnings[player.name] = warnings - end - - table.insert(warnings, { - tick = game.tick, - reason = reason, - }) - - local warning_count = #warnings - - script.raise_event(Warnings.events.on_script_warning_added, { - name = Warnings.events.on_script_warning_added, - tick = game.tick, - player_index = player.index, - warning_count = warning_count, - reason = reason, - }) - - if warning_count > config.script_warning_limit then - Warnings.add_warning(player, "", reason) - end - - return warning_count -end - ---- Script warning removed event trigger due to it being looped in clear script warnings --- @tparam LuaPlayer player the player who is having a script warning removed --- @tparam number warning_count the number of warnings that the player has -local function script_warning_removed_event(player, warning_count) - script.raise_event(Warnings.events.on_script_warning_removed, { - name = Warnings.events.on_script_warning_removed, - tick = game.tick, - player_index = player.index, - warning_count = warning_count, - }) -end - ---- Removes a script warning from a player --- @tparam LuaPlayer player the player to remove a script warning from --- @treturn number the number of script warnings that the player has -function Warnings.remove_script_warning(player) - player = valid_player(player) - if not player then return end - - local warnings = user_script_warnings[player.name] - if not warnings then return end - - table.remove(warnings, 1) - - script_warning_removed_event(player) - - return #warnings -end - ---- Removes all script warnings from a player, emits event for each warning removed --- @tparam LuaPlayer player the player to clear the script warnings from -function Warnings.clear_script_warnings(player) - player = valid_player(player) - if not player then return end - - local warnings = user_script_warnings[player.name] - if not warnings then return end - - local warning_count = #warnings - for n, _ in pairs(warnings) do - script_warning_removed_event(player, warning_count - n) - end - - user_script_warnings[player.name] = nil - return true -end - --- script warnings are removed after a certain amount of time to make them even more lienient -local script_warning_cool_down = config.script_warning_cool_down * 3600 -Event.on_nth_tick(script_warning_cool_down / 4, function() - local cutoff = game.tick - script_warning_cool_down - for player_name, script_warnings in pairs(user_script_warnings) do - if #script_warnings > 0 then - for _, warning in pairs(script_warnings) do - if warning.tick < cutoff then - Warnings.remove_script_warning(player_name) - end - end - end - end -end) - -return Warnings diff --git a/exp_roles/seed.ts b/exp_roles/seed.ts index 97325245..b028314a 100644 --- a/exp_roles/seed.ts +++ b/exp_roles/seed.ts @@ -67,9 +67,6 @@ export const seedRoles: SeedRole[] = [ "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", @@ -103,8 +100,6 @@ export const seedRoles: SeedRole[] = [ "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", diff --git a/exp_scenario/module/commands/warnings.lua b/exp_scenario/module/commands/warnings.lua deleted file mode 100644 index 14be9aad..00000000 --- a/exp_scenario/module/commands/warnings.lua +++ /dev/null @@ -1,96 +0,0 @@ ---[[-- Commands - Warnings -Adds a commands that allow admins to warn other players -]] - -local Commands = require("modules/exp_commands") -local format_player_name = Commands.format_player_name_locale - -local Warnings = require("modules.exp_legacy.modules.control.warnings") --- @dep modules.control.warnings -local config = require("modules.exp_legacy.config.warnings") --- @dep config.warnings - ---- Gives a warning to a player; may lead to automatic script action. -Commands.new("create-warning", { "exp-commands_warnings.description-create" }) - :argument("player", { "exp-commands_warnings.arg-player-create" }, Commands.types.lower_role_player) - :argument("reason", { "exp-commands_warnings.arg-reason" }, Commands.types.string) - :enable_auto_concatenation() - :add_aliases{ "warn" } - :add_flags{ "admin_only" } - :register(function(player, other_player, reason) - --- @cast other_player LuaPlayer - --- @cast reason string - Warnings.add_warning(other_player, player.name, reason) - local player_name = format_player_name(player) - local other_player_name = format_player_name(other_player) - game.print{ "exp-commands_warnings.create", other_player_name, player_name, reason } - end) - ---- Gets a list of all warnings that a player has on them. If no player then lists all players and the number of warnings on them. -Commands.new("get-warnings", { "exp-commands_warnings.description-get" }) - :optional("player", { "exp-commands_warnings.arg-player-get" }, Commands.types.player) - :add_aliases{ "warnings" } - :add_flags{ "admin_only" } - :register(function(player, other_player) - --- @cast other_player LuaPlayer? - if other_player then - local warnings = Warnings.get_warnings(player) - local script_warnings = Warnings.get_script_warnings(player) - local other_player_name = format_player_name(other_player) - Commands.print{ "exp-commands_warnings.player-title", other_player_name, #warnings, #script_warnings, config.script_warning_limit } - for _, warning in pairs(warnings) do - local by_player_name_formatted = format_player_name(warning.by_player_name) - Commands.print{ "exp-commands_warnings.list-element-player", by_player_name_formatted, warning.reason } - end - else - local warnings = Warnings.user_warnings:get_all() - local script_warnings = Warnings.user_script_warnings - Commands.print{ "exp-commands_warnings.warnings-title" } - for player_name, player_warnings in pairs(warnings) do - local player_name_formatted = format_player_name(player_name) - local script_warning_count = script_warnings[player_name] and #script_warnings[player_name] or 0 - Commands.print{ "exp-commands_warnings.list-element", player_name_formatted, #player_warnings, script_warning_count, config.script_warning_limit } - end - for player_name, player_warnings in pairs(script_warnings) do - if not warnings[player_name] then - local player_name_formatted = format_player_name(player_name) - Commands.print{ "exp-commands_warnings.list-element", player_name_formatted, 0, #player_warnings, config.script_warning_limit } - end - end - end - end) - ---- Clears all warnings from a player -Commands.new("clear-warnings", { "exp-commands_warnings.description-clear" }) - :argument("player", { "exp-commands_warnings.arg-player-clear" }, Commands.types.player) - :add_flags{ "admin_only" } - :register(function(player, other_player) - --- @cast other_player LuaPlayer - Warnings.clear_warnings(other_player, player.name) - Warnings.clear_script_warnings(other_player) - local player_name = format_player_name(player) - local other_player_name = format_player_name(other_player) - game.print{ "exp-commands_warnings.cleared", other_player_name, player_name } - end) - ---- Clears all script warnings from a player -Commands.new("clear-script-warnings", { "exp-commands_warnings.description-clear-script" }) - :argument("player", { "exp-commands_warnings.arg-player-clear" }, Commands.types.player) - :add_flags{ "admin_only" } - :register(function(player, other_player) - --- @cast other_player LuaPlayer - Warnings.clear_script_warnings(other_player) - local player_name = format_player_name(player) - local other_player_name = format_player_name(other_player) - game.print{ "exp-commands_warnings.cleared-script", other_player_name, player_name } - end) - ---- Clears the last warning that was given to a player -Commands.new("clear-last-warnings", { "exp-commands_warnings.description-clear-last" }) - :argument("player", { "exp-commands_warnings.arg-player-clear" }, Commands.types.player) - :add_flags{ "admin_only" } - :register(function(player, other_player) - --- @cast other_player LuaPlayer - Warnings.remove_warning(other_player, player.name) - local player_name = format_player_name(player) - local other_player_name = format_player_name(other_player) - game.print{ "exp-commands_warnings.cleared-last", other_player_name, player_name } - end) diff --git a/exp_scenario/module/control.lua b/exp_scenario/module/control.lua index 2e04e552..31e34fa7 100644 --- a/exp_scenario/module/control.lua +++ b/exp_scenario/module/control.lua @@ -38,7 +38,6 @@ require("modules/exp_scenario/commands/surface") require("modules/exp_scenario/commands/teleport") require("modules/exp_scenario/commands/trains") require("modules/exp_scenario/commands/vlayer") -require("modules/exp_scenario/commands/warnings") require("modules/exp_scenario/commands/waterfill") --- Control diff --git a/exp_scenario/module/control/discord_alerts.lua b/exp_scenario/module/control/discord_alerts.lua index c4675140..9d55687c 100644 --- a/exp_scenario/module/control/discord_alerts.lua +++ b/exp_scenario/module/control/discord_alerts.lua @@ -131,40 +131,6 @@ if config.player_reports then end end ---- Warnings added and removed -if config.player_warnings then - local Warnings = require("modules.exp_legacy.modules.control.warnings") - events[Warnings.events.on_warning_added] = function(event) - local player_name, by_player_name = get_player_name(event) - local player = assert(game.get_player(player_name)) - emit_event{ - title = "Warning", - description = "A player has been given a warning", - color = Colors.yellow, - fields = { - { name = "Player", inline = true, value = append_playtime(player_name) }, - { name = "By", inline = true, value = append_playtime(by_player_name) }, - { name = "Report Count", inline = true, value = Warnings.count_warnings(player) }, - { name = "Reason", value = event.reason }, - }, - } - end - events[Warnings.events.on_warning_removed] = function(event) - if event.batch ~= 1 then return end - local player_name = get_player_name(event) - emit_event{ - title = "Warnings Removed", - description = "A player has a warning removed", - color = Colors.green, - fields = { - { name = "Player", inline = true, value = append_playtime(player_name) }, - { name = "By", inline = true, value = append_playtime(event.removed_by_name) }, - { name = "Report Count", inline = true, value = tostring(event.batch_count) }, - }, - } - end -end - --- When a player is jailed or unjailed if config.player_jail then local Jail = require("modules.exp_legacy.modules.control.jail") diff --git a/exp_scenario/module/locale/en.cfg b/exp_scenario/module/locale/en.cfg index 80e8a1ec..707ed8c1 100644 --- a/exp_scenario/module/locale/en.cfg +++ b/exp_scenario/module/locale/en.cfg @@ -249,25 +249,6 @@ description=Print all vlayer information. title=VLayer Information: result=__1__: __2__ -[exp-commands_warnings] -description-create=Gives a warning to a player; may lead to automatic script action. -description-get=Gets the number of warnings a player has. If no player then lists all players and the number of warnings they have. -description-clear=Clears all warnings (and script warnings) from a player. -description-clear-script=Clears all script warnings from a player. -description-clear-last=Clears the last warning from a player. -arg-player-create=Player to give the warning to. -arg-player-get=Player to get the warning of, if not given all players are returned. -arg-player-clear=Player to clear the warnings of. -arg-reason=Reason the user is receiving a warning. -create=__1__ received a warning from __2__ for __3__. -player-title=__1__ has __2__ warnings and __3__/__4__ script warnings. -list-element-player=__1__: __2__ -warnings-title=The following players have warnings aginst them (and script warnings): -list-element=__1__: __2__ (__3__/__4__) -cleared=__1__ had all their warnings cleared by __2__. -cleared-script=__1__ had all their script warnings cleared by __2__. -cleared-last=__1__ had their last warning cleared by __2__. - [exp-commands_waterfill] description=Replace tiles with shallow water. requires-explosives=__ITEM__cliff-explosives__ are required to create water. @@ -326,7 +307,6 @@ reason-entry=Enter Reason goto-player=Goto player bring-player=Bring player report-player=Report player -warn-player=Warn player jail-player=Jail player kick-player=Kick player ban-player=Ban player diff --git a/exp_scenario/module/locale/zh-CN.cfg b/exp_scenario/module/locale/zh-CN.cfg index bf65db42..b7399f07 100644 --- a/exp_scenario/module/locale/zh-CN.cfg +++ b/exp_scenario/module/locale/zh-CN.cfg @@ -246,25 +246,6 @@ description=vlayer 資訊 title=vlayer 資訊: result=__1__: __2__ -[exp-commands_warnings] -description-create=給用戶一個警告; 可能會導致系統的自動行動。 -description-get=取得用戶收到的警告次數。如果沒有用戶,則列出所有用戶以及他們受到警告的次數。 -description-clear=清除用戶的所有警告(和系統警告)。 -description-clear-script=清除用戶的系統警告。 -description-clear-last=清除用戶的最後警告。 -arg-player-create=要警告的用戶。 -arg-player-get=要取得的用戶, 若沒有則返回所有用戶。 -arg-player-clear=要清除的用戶。 -arg-reason=原因。 -create=__1__ 被 __2__ 因 __3__ 作出警告。 -player-title=__1__ 有 __2__ 個警告和 __3__/__4__ 的系統警告。 -list-element-player=__1__: __2__ -warnings-title=該用戶警告如下: -list-element=__1__: __2__ (__3__/__4__) -cleared=__1__ 的警告己被 __2__ 清除。 -cleared-script=__1__ 的系統警告己被 __2__ 清除。 -cleared-last=__1__ 的最後警告己被 __2__ 清除。 - [exp-commands_waterfill] description=把地換為淺水。 requires-explosives=沒有足夠的 __ITEM__cliff-explosives__ 。 diff --git a/exp_scenario/module/locale/zh-TW.cfg b/exp_scenario/module/locale/zh-TW.cfg index bf65db42..b7399f07 100644 --- a/exp_scenario/module/locale/zh-TW.cfg +++ b/exp_scenario/module/locale/zh-TW.cfg @@ -246,25 +246,6 @@ description=vlayer 資訊 title=vlayer 資訊: result=__1__: __2__ -[exp-commands_warnings] -description-create=給用戶一個警告; 可能會導致系統的自動行動。 -description-get=取得用戶收到的警告次數。如果沒有用戶,則列出所有用戶以及他們受到警告的次數。 -description-clear=清除用戶的所有警告(和系統警告)。 -description-clear-script=清除用戶的系統警告。 -description-clear-last=清除用戶的最後警告。 -arg-player-create=要警告的用戶。 -arg-player-get=要取得的用戶, 若沒有則返回所有用戶。 -arg-player-clear=要清除的用戶。 -arg-reason=原因。 -create=__1__ 被 __2__ 因 __3__ 作出警告。 -player-title=__1__ 有 __2__ 個警告和 __3__/__4__ 的系統警告。 -list-element-player=__1__: __2__ -warnings-title=該用戶警告如下: -list-element=__1__: __2__ (__3__/__4__) -cleared=__1__ 的警告己被 __2__ 清除。 -cleared-script=__1__ 的系統警告己被 __2__ 清除。 -cleared-last=__1__ 的最後警告己被 __2__ 清除。 - [exp-commands_waterfill] description=把地換為淺水。 requires-explosives=沒有足夠的 __ITEM__cliff-explosives__ 。 diff --git a/exp_scenario/permissions.ts b/exp_scenario/permissions.ts index 9d604b58..bbaafacd 100644 --- a/exp_scenario/permissions.ts +++ b/exp_scenario/permissions.ts @@ -26,25 +26,20 @@ const definitions: Definition[] = [ ["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."], - ["exp_scenario.command.clear_last_warnings", "/clear-last-warnings", "Clears the last warning from a player."], ["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."], - ["exp_scenario.command.clear_script_warnings", "/clear-script-warnings", "Clears all script warnings from a player."], - ["exp_scenario.command.clear_warnings", "/clear-warnings", "Clears all warnings (and script warnings) from a player."], ["exp_scenario.command.collectdata", "/collectdata", "Collect data for RCON usage."], ["exp_scenario.command.commands", "/commands", "List and search all commands for a keyword.", true], ["exp_scenario.command.connect", "/connect", "Connect to another server.", true], ["exp_scenario.command.connect_all", "/connect-all", "Connect all players to another server."], ["exp_scenario.command.connect_player", "/connect-player", "Connect a player to a different server."], ["exp_scenario.command.create_report", "/create-report", "Reports a player and notifies moderators.", true], - ["exp_scenario.command.create_warning", "/create-warning", "Gives a warning to a player; may lead to automatic script action."], ["exp_scenario.command.data_preference", "/data-preference", "Allows you to set/get your data saving preference.", true], ["exp_scenario.command.debug", "/debug", "Opens the debug gui."], ["exp_scenario.command.follow", "/follow", "Start following a player in spectator."], ["exp_scenario.command.get_home", "/get-home", "Returns your current home location."], ["exp_scenario.command.get_reports", "/get-reports", "List the reports against a player, or against every player."], ["exp_scenario.command.get_roles", "/get-roles", "Get all roles that a player has, if no player provided it lists all roles.", true], - ["exp_scenario.command.get_warnings", "/get-warnings", "List the warnings against a player, or against every player."], ["exp_scenario.command.goto", "/goto", "Teleports you to a player."], ["exp_scenario.command.home", "/home", "Teleports you to your home location."], ["exp_scenario.command.jail", "/jail", "Puts a player into jail, which suppresses all of their other roles."], From 37eff4b3e93a51a1dce9249fe4ce87ae11713ade Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sat, 5 Sep 2026 20:53:56 +0000 Subject: [PATCH 05/15] Count kills and damage dealt from vehicles The statistics only credited damage and kills when the cause was a character, so anything done from a car, tank, spidertron or locomotive was lost. The cause is now resolved to a player through the vehicle's driver, or passenger when there is no driver, and turrets, remotes and uncrewed trains still count for nobody. Fixes #219 --- exp_legacy/module/modules/data/statistics.lua | 51 ++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/exp_legacy/module/modules/data/statistics.lua b/exp_legacy/module/modules/data/statistics.lua index 08bcb7d5..ac844387 100644 --- a/exp_legacy/module/modules/data/statistics.lua +++ b/exp_legacy/module/modules/data/statistics.lua @@ -113,16 +113,47 @@ if config.MachinesRemoved or config.TreesDestroyed or config.OreMined then Event.add(defines.events.on_player_mined_entity, on_event) end +--- Vehicles which credit their driver, or failing that their passenger, with what they hit +local crewed_vehicles = { car = true, ["spider-vehicle"] = true, locomotive = true } + +--- Get the connected player behind the cause of damage or a death, nil for turrets, trains without a crew and the like +--- @param cause LuaEntity? +--- @return LuaPlayer? +local function get_cause_player(cause) + if not cause or not cause.valid then return nil end + + local occupant = cause --- @type LuaEntity | LuaPlayer + if crewed_vehicles[cause.type] then + local crew = cause.get_driver() or cause.get_passenger() + if not crew then return nil end + occupant = crew + end + + local player --- @type LuaPlayer? + if occupant.object_name == "LuaPlayer" then + player = occupant --[[@as LuaPlayer]] + elseif occupant.type == "character" then + player = occupant.player + end + + if not player or not player.valid or not player.connected then return nil end + return player +end + +--- Check that damage or a death counts against an entity, it must be hostile to the player +--- @param entity LuaEntity +--- @param player LuaPlayer +--- @return boolean +local function is_hostile(entity, player) + return entity.valid and entity.force ~= player.force and entity.force.name ~= "neutral" +end + --- Add DamageDealt if it is enabled if config.DamageDealt then local stat = Statistics:combine("DamageDealt") Event.add(defines.events.on_entity_damaged, function(event) - local character = event.cause -- Check character is valid - if not character or not character.valid or character.type ~= "character" then return end - local player = character.player -- Check player is valid - if not player.valid or not player.connected then return end - local entity = event.entity -- Check entity is valid - if not entity.valid or entity.force == player.force or entity.force.name == "neutral" then return end + local player = get_cause_player(event.cause) + if not player or not is_hostile(event.entity, player) then return end stat:increment(player, floor(event.final_damage_amount)) end) end @@ -131,12 +162,8 @@ end if config.Kills then local stat = Statistics:combine("Kills") Event.add(defines.events.on_entity_died, function(event) - local character = event.cause -- Check character is valid - if not character or not character.valid or character.type ~= "character" then return end - local player = character.player -- Check player is valid - if not player or not player.valid or not player.connected then return end - local entity = event.entity -- Check entity is valid - if not entity.valid or entity.force == player.force or entity.force.name == "neutral" then return end + local player = get_cause_player(event.cause) + if not player or not is_hostile(event.entity, player) then return end stat:increment(player) end) end From ce7a6ae1bfcef0839e0f9f2e6a642b7b7a1680df Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sat, 5 Sep 2026 20:53:57 +0000 Subject: [PATCH 06/15] Only log a rocket as fired when its ammo count drops Factorio has no fired event, so the log was written on every ammo inventory change, which includes loading rockets for the first time and swapping between rocket types. The last seen name and count of each ammo slot is now kept per player, and a shot is only logged when a slot holds the same ammo with one fewer, or empties from a single round. Only the three configured ammo types are logged, rather than every ammo change. Fixes #242 --- .../module/control/deconstruction_log.lua | 66 ++++++++++++++----- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/exp_scenario/module/control/deconstruction_log.lua b/exp_scenario/module/control/deconstruction_log.lua index ddfeb979..7688e5d5 100644 --- a/exp_scenario/module/control/deconstruction_log.lua +++ b/exp_scenario/module/control/deconstruction_log.lua @@ -3,6 +3,7 @@ Log certain actions into a file when events are triggered ]] local ExpUtil = require("modules/exp_util") +local Storage = require("modules/exp_util/storage") local Roles = require("modules/exp_roles") local config = require("modules.exp_legacy.config.deconlog") @@ -130,29 +131,63 @@ local function on_player_mined_entity(event) add_log_line(player, "mined_entity", format_entity(event.entity)) end ---- Log when rocket is fired +--- Ammo which is logged when fired +local logged_ammo = { + ["rocket"] = config.fired_rocket, + ["explosive-rocket"] = config.fired_explosive_rocket, + ["atomic-bomb"] = config.fired_nuke, +} + +--- @class ExpScenario_DeconstructionLog.AmmoSlot +--- @field name string +--- @field count number + +--- The last seen contents of each ammo slot, keyed by player index then slot index +local ammo_slots = {} --- @type table> +Storage.register(ammo_slots, function(tbl) + ammo_slots = tbl +end) + +--- Log a shot, there is no fired event so a slot losing one of the same ammo is taken as a shot --- @param event EventData.on_player_ammo_inventory_changed local function on_player_ammo_inventory_changed(event) local player = get_log_player(event) if not player or not player.character then return end + local slots = ammo_slots[player.index] + if not slots then + slots = {} + ammo_slots[player.index] = slots + end + local character_ammo = assert(player.get_inventory(defines.inventory.character_ammo)) - local gun_index = player.character.selected_gun_index --[[@as uint]] - local item = character_ammo[gun_index] - if not item or not item.valid or not item.valid_for_read then - return - end + for index = 1, #character_ammo do + local stack = character_ammo[index --[[@as uint]]] + local previous = slots[index] + local fired = nil --- @type string? - local action_name = "shot-" .. item.name - if not config.fired_rocket and action_name == "shot-rocket" then - return - elseif not config.fired_explosive_rocket and action_name == "shot-explosive-rocket" then - return - elseif not config.fired_nuke and action_name == "shot-atomic-bomb" then - return - end + if stack.valid_for_read then + if previous and previous.name == stack.name and previous.count == stack.count + 1 then + fired = stack.name + end + slots[index] = { name = stack.name, count = stack.count } + else + if previous and previous.count == 1 then + fired = previous.name + end + slots[index] = nil + end - add_log_line(player, action_name, format_position(player.physical_position), format_position(player.shooting_state.position)) + if fired and logged_ammo[fired] then + add_log_line(player, "shot-" .. fired, format_position(player.physical_position), format_position(player.shooting_state.position)) + end + end +end + +--- Forget the ammo of a player who left, their slots are read again on the next change +--- @param event EventData.on_player_left_game +local function on_player_left_game(event) + ammo_slots[event.player_index] = nil end @@ -175,6 +210,7 @@ end if config.fired_rocket or config.fired_explosive_rocket or config.fired_nuke then events[e.on_player_ammo_inventory_changed] = on_player_ammo_inventory_changed + events[e.on_player_left_game] = on_player_left_game end return { From f961a8d82e1b6935e501ad9ae7a4005c843d51f2 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sat, 5 Sep 2026 20:54:21 +0000 Subject: [PATCH 07/15] Use page and slot for quick bar filters Factorio 2.1 changed get_quick_bar_slot and set_quick_bar_slot to take a page and slot rather than a single index, and to describe slots holding records, remotes and specific item instances. Saved filters keep their single index, which is converted on load and save, and only plain item filters are saved since the other slot types hold data which has no name to store. The command is enabled again. Fixes #444 --- exp_legacy/module/modules/data/quickbar.lua | 37 ++++++++------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/exp_legacy/module/modules/data/quickbar.lua b/exp_legacy/module/modules/data/quickbar.lua index 8550cae9..761168dd 100644 --- a/exp_legacy/module/modules/data/quickbar.lua +++ b/exp_legacy/module/modules/data/quickbar.lua @@ -20,46 +20,37 @@ PlayerFilters:set_metadata{ end, } +--- Filters are stored by a single index across the ten pages of ten slots +local slots_per_page = 10 + --- Loads your quickbar preset PlayerFilters:on_load(function(player_name, filters) if not filters then filters = config[player_name] end if not filters then return end local player = game.players[player_name] - for i, item_name in pairs(filters) do + for index, item_name in pairs(filters) do if item_name ~= nil and item_name ~= "" then - player.set_quick_bar_slot(i, item_name) + local page = math.ceil(index / slots_per_page) + local slot = (index - 1) % slots_per_page + 1 + player.set_quick_bar_slot(page, slot, item_name) end end end) -local ignored_items = { - ["blueprint"] = true, - ["blueprint-book"] = true, - ["deconstruction-planner"] = true, - ["spidertron-remote"] = true, - ["upgrade-planner"] = true, -} - ---- Saves your quickbar preset to the script-output folder +--- Saves your quickbar preset, only plain item filters can be saved Commands.new("save-quickbar", "Saves your Quickbar preset items to file") :add_aliases{ "save-toolbar" } - :add_flags{ "disabled" } :register(function(player) local filters = {} - error("2.1 changes to get_quick_bar_slot beak compatibility with 2.0; waiting for upstream") - -- Upstream may add method to compat, or change inventory sync to have a quickbar only mode - for i = 1, 100 do - --[[ - local slot = player.get_quick_bar_slot(i) - -- Need to filter out blueprint and blueprint books because the slot is a LuaItemPrototype and does not contain a way to export blueprint data - if slot ~= nil then - local ignored = ignored_items[slot.name] - if ignored ~= true then - filters[i] = slot.name + for page = 1, slots_per_page do + for slot = 1, slots_per_page do + -- Records, remotes and specific item instances hold data which can not be saved by name + local quick_bar_slot = player.get_quick_bar_slot(page, slot) + if quick_bar_slot and quick_bar_slot.type == "filter" then + filters[(page - 1) * slots_per_page + slot] = assert(quick_bar_slot.filter).name end end - ]] end if next(filters) then From 00253de11f493d7cfad5badb25e75a5c674ec89a Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sat, 5 Sep 2026 20:54:57 +0000 Subject: [PATCH 08/15] Show the cursor of the followed player in surveillance A label under the camera shows what the player being followed holds in their cursor, item and count or the ghost item, so that using a deconstruction planner or similar can be seen. It is hidden while the camera shows a fixed location. Closes #427 --- exp_scenario/module/gui/surveillance.lua | 58 ++++++++++++++++++++++++ exp_scenario/module/locale/en.cfg | 3 ++ exp_scenario/module/locale/zh-CN.cfg | 3 ++ exp_scenario/module/locale/zh-TW.cfg | 3 ++ 4 files changed, 67 insertions(+) diff --git a/exp_scenario/module/gui/surveillance.lua b/exp_scenario/module/gui/surveillance.lua index e9910723..05d9ee21 100644 --- a/exp_scenario/module/gui/surveillance.lua +++ b/exp_scenario/module/gui/surveillance.lua @@ -6,6 +6,8 @@ local Gui = require("modules/exp_gui") local ElementsExtra = require("modules/exp_scenario/gui/elements") local Roles = require("modules/exp_roles") +local format_string = string.format + --- @class ExpGui_Surveillance.elements local Elements = {} @@ -206,6 +208,60 @@ function Elements.camera.refresh_online() end end +--- Label showing what the player a camera follows holds in their cursor +--- @class ExpGui_Surveillance.elements.cursor_label: ExpElement +--- @field data table The camera the label belongs to +--- @overload fun(parent: LuaGuiElement, camera: LuaGuiElement): LuaGuiElement +Elements.cursor_label = Gui.define("surveillance/cursor_label") + :track_all_elements() + :draw{ + type = "label", + caption = { "exp-gui_surveillance.caption-cursor-empty" }, + } + :style{ + width = 480, + } + :element_data( + Gui.from_argument(1) + ) --[[@as any]] + +--- Calculate the caption describing the cursor of a player +--- @param player LuaPlayer +--- @return LocalisedString +function Elements.cursor_label.calculate_caption(player) + local cursor_stack = player.cursor_stack + if cursor_stack and cursor_stack.valid_for_read then + local item = format_string("[item=%s,quality=%s]", cursor_stack.name, cursor_stack.quality.name) + return { "exp-gui_surveillance.caption-cursor", item, cursor_stack.count } + end + + local cursor_ghost = player.cursor_ghost --[[@as ItemIDAndQualityIDPair?]] + if cursor_ghost then + local prototype = cursor_ghost.name --[[@as LuaItemPrototype]] + return { "exp-gui_surveillance.caption-cursor-ghost", "[item=" .. prototype.name .. "]" } + end + + return { "exp-gui_surveillance.caption-cursor-empty" } +end + +--- Refresh a label, hidden when the camera is not following a player +--- @param cursor_label LuaGuiElement +--- @param target_player LuaPlayer? +function Elements.cursor_label.refresh(cursor_label, target_player) + cursor_label.visible = target_player ~= nil + if target_player then + cursor_label.caption = Elements.cursor_label.calculate_caption(target_player) + end +end + +--- Refresh the labels of all online cameras +function Elements.cursor_label.refresh_online() + for _, cursor_label in Elements.cursor_label:online_elements() do + local camera = Elements.cursor_label.data[cursor_label] + Elements.cursor_label.refresh(cursor_label, Elements.camera.data[camera]) + end +end + --- Container added to the screen Elements.container = Gui.define("surveillance/container") :draw(function(def, parent) @@ -214,6 +270,7 @@ Elements.container = Gui.define("surveillance/container") local target_player = Gui.get_player(parent) local camera = Elements.camera(screen_frame, target_player) + Elements.cursor_label(screen_frame, camera) local type_dropdown_data = { camera = camera, @@ -248,6 +305,7 @@ return { [e.on_tick] = Elements.camera.refresh_online, }, on_nth_tick = { + [10] = Elements.cursor_label.refresh_online, [600] = Elements.type_dropdown.refresh_online, } } diff --git a/exp_scenario/module/locale/en.cfg b/exp_scenario/module/locale/en.cfg index 80e8a1ec..eda13142 100644 --- a/exp_scenario/module/locale/en.cfg +++ b/exp_scenario/module/locale/en.cfg @@ -489,6 +489,9 @@ caption-set-location=Set type-player=Player type-static=Static type-loop=Loop +caption-cursor=Cursor: __1__ x__2__ +caption-cursor-ghost=Cursor: __1__ (ghost) +caption-cursor-empty=Cursor: empty [exp-gui_task-list] caption-main=Task List [img=info] diff --git a/exp_scenario/module/locale/zh-CN.cfg b/exp_scenario/module/locale/zh-CN.cfg index bf65db42..29343d58 100644 --- a/exp_scenario/module/locale/zh-CN.cfg +++ b/exp_scenario/module/locale/zh-CN.cfg @@ -474,6 +474,9 @@ caption-set-location=設 type-player=用戶 type-static=靜態 type-loop=循環 +caption-cursor=游標: __1__ x__2__ +caption-cursor-ghost=游標: __1__ (幻影) +caption-cursor-empty=游標: 空 [exp-gui_task-list] caption-main=工作流程 [img=info] diff --git a/exp_scenario/module/locale/zh-TW.cfg b/exp_scenario/module/locale/zh-TW.cfg index bf65db42..29343d58 100644 --- a/exp_scenario/module/locale/zh-TW.cfg +++ b/exp_scenario/module/locale/zh-TW.cfg @@ -474,6 +474,9 @@ caption-set-location=設 type-player=用戶 type-static=靜態 type-loop=循環 +caption-cursor=游標: __1__ x__2__ +caption-cursor-ghost=游標: __1__ (幻影) +caption-cursor-empty=游標: 空 [exp-gui_task-list] caption-main=工作流程 [img=info] From c12f0a8f8960afad67767a1df3e70d4671af18c3 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:03:26 +0100 Subject: [PATCH 09/15] Remove redundant event properties --- exp_scenario/module/control/jail.lua | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/exp_scenario/module/control/jail.lua b/exp_scenario/module/control/jail.lua index bc4730fa..5bf07c40 100644 --- a/exp_scenario/module/control/jail.lua +++ b/exp_scenario/module/control/jail.lua @@ -25,7 +25,7 @@ local Jail = { --- The role given to jailed players, it has a higher priority than every other role --- @return ExpRoles.Role -local function jail_role() +local function get_jail_role() return (assert(Roles.get_role_by_name("Jail"), "The Jail role does not exist")) end @@ -33,7 +33,7 @@ end --- @param player LuaPlayer --- @return boolean function Jail.is_jailed(player) - return jail_role():has_player(player) + return get_jail_role():has_player(player) end --- Put a player into jail, which suppresses all of their other roles @@ -42,7 +42,7 @@ end --- @param reason string --- @return boolean # False when the player was already in jail function Jail.jail_player(player, by_player_name, reason) - local role = jail_role() + local role = get_jail_role() if role:has_player(player) then return false end -- Stop whatever the player is doing, the jail permission group stops them from starting again @@ -56,8 +56,6 @@ function Jail.jail_player(player, by_player_name, reason) role:assign(player, { by_player_name = by_player_name, silent = true }) script.raise_event(Jail.on_player_jailed, { - name = Jail.on_player_jailed, - tick = game.tick, player_index = player.index, by_player_name = by_player_name, reason = reason, @@ -71,14 +69,12 @@ end --- @param by_player_name string --- @return boolean # False when the player was not in jail function Jail.unjail_player(player, by_player_name) - local role = jail_role() + local role = get_jail_role() if not role:has_player(player) then return false end role:unassign(player, { by_player_name = by_player_name, silent = true }) script.raise_event(Jail.on_player_unjailed, { - name = Jail.on_player_unjailed, - tick = game.tick, player_index = player.index, by_player_name = by_player_name, }) From 702016b3e2bdeee486df90662f3d6c40bf97e2b6 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:15:37 +0100 Subject: [PATCH 10/15] Remove redundant event properties --- exp_scenario/module/control/protection.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/exp_scenario/module/control/protection.lua b/exp_scenario/module/control/protection.lua index c8907e0f..9839290f 100644 --- a/exp_scenario/module/control/protection.lua +++ b/exp_scenario/module/control/protection.lua @@ -184,14 +184,12 @@ local function raise_violation(event, player) player_repeats.last = game.tick player_repeats.count = player_repeats.count + 1 - event.name = Protection.on_player_mined_protected script.raise_event(Protection.on_player_mined_protected, event) local entity = event.entity local always_repeat = always_trigger_repeat_names[entity.name] or always_trigger_repeat_types[entity.type] if always_repeat or player_repeats.count >= config.repeat_count then player_repeats.count = 0 - event.name = Protection.on_repeat_violation script.raise_event(Protection.on_repeat_violation, event) end end @@ -201,8 +199,9 @@ end local function on_pre_player_mined_item(event) local entity = event.entity local player = game.players[event.player_index] - if not is_ignored(player, entity) - and (Protection.is_entity_protected(entity) or Protection.is_position_protected(entity.surface, entity.position)) + if + not is_ignored(player, entity) + and (Protection.is_entity_protected(entity) or Protection.is_position_protected(entity.surface, entity.position)) then raise_violation(event, player) end From e1966dec9cc140d3f46f5a8c1b65ea0bd754865b Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:27:14 +0100 Subject: [PATCH 11/15] Small format changes --- exp_scenario/module/control/spectate.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/exp_scenario/module/control/spectate.lua b/exp_scenario/module/control/spectate.lua index b8cac7be..b8ccb29b 100644 --- a/exp_scenario/module/control/spectate.lua +++ b/exp_scenario/module/control/spectate.lua @@ -75,10 +75,13 @@ function Spectate.start_spectate(player) if spectating[player.index] or not player.character then return false end local character = player.character local opened = player.opened + player.set_controller{ type = defines.controllers.spectator } player.associate_character(character) spectating[player.index] = character + if opened then player.opened = opened end -- Changing controller closes the opened gui + return true end @@ -87,6 +90,7 @@ end function Spectate.stop_spectate(player) local character = spectating[player.index] spectating[player.index] = nil + if character and character.valid then local opened = player.opened player.teleport(character.position, character.surface) @@ -137,8 +141,12 @@ end local function update_following(data) local player, target = data.player, data.target local position = player.position - if data.stop or player.character or not target.valid - or position.x ~= data.position.x or position.y ~= data.position.y + if + data.stop + or player.character + or not target.valid + or position.x ~= data.position.x + or position.y ~= data.position.y then Spectate.stop_follow(player) else From 01bc4cd14d629dde9daf00de28e39d42c99ab919 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sun, 6 Sep 2026 01:07:19 +0100 Subject: [PATCH 12/15] Added constant for total page count --- exp_legacy/module/modules/data/quickbar.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/exp_legacy/module/modules/data/quickbar.lua b/exp_legacy/module/modules/data/quickbar.lua index 761168dd..0db8e846 100644 --- a/exp_legacy/module/modules/data/quickbar.lua +++ b/exp_legacy/module/modules/data/quickbar.lua @@ -21,6 +21,7 @@ PlayerFilters:set_metadata{ } --- Filters are stored by a single index across the ten pages of ten slots +local total_page_count = 10 local slots_per_page = 10 --- Loads your quickbar preset @@ -43,7 +44,7 @@ Commands.new("save-quickbar", "Saves your Quickbar preset items to file") :register(function(player) local filters = {} - for page = 1, slots_per_page do + for page = 1, total_page_count do for slot = 1, slots_per_page do -- Records, remotes and specific item instances hold data which can not be saved by name local quick_bar_slot = player.get_quick_bar_slot(page, slot) From 4d976a9e352d40607882ecad9f35e2442d054e17 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:24:28 +0000 Subject: [PATCH 13/15] Show the cursor as a slot in the surveillance header The cursor was a line of text under the camera, which read as an afterthought next to the framed header. It is now a slot button in the header after the zoom buttons: the item sprite with its count, greyed out for a ghost, an empty slot for an empty cursor, and the item name in the tooltip. The window keeps its shape whatever the item is called. --- exp_scenario/module/gui/surveillance.lua | 76 ++++++++++++++---------- exp_scenario/module/locale/en.cfg | 6 +- exp_scenario/module/locale/zh-CN.cfg | 6 +- exp_scenario/module/locale/zh-TW.cfg | 6 +- 4 files changed, 55 insertions(+), 39 deletions(-) diff --git a/exp_scenario/module/gui/surveillance.lua b/exp_scenario/module/gui/surveillance.lua index 05d9ee21..11057e0a 100644 --- a/exp_scenario/module/gui/surveillance.lua +++ b/exp_scenario/module/gui/surveillance.lua @@ -6,8 +6,6 @@ local Gui = require("modules/exp_gui") local ElementsExtra = require("modules/exp_scenario/gui/elements") local Roles = require("modules/exp_roles") -local format_string = string.format - --- @class ExpGui_Surveillance.elements local Elements = {} @@ -208,57 +206,75 @@ function Elements.camera.refresh_online() end end ---- Label showing what the player a camera follows holds in their cursor ---- @class ExpGui_Surveillance.elements.cursor_label: ExpElement ---- @field data table The camera the label belongs to +--- Slot in the header showing what the player a camera follows holds in their cursor +--- @class ExpGui_Surveillance.elements.cursor_slot: ExpElement +--- @field data table The camera the slot belongs to --- @overload fun(parent: LuaGuiElement, camera: LuaGuiElement): LuaGuiElement -Elements.cursor_label = Gui.define("surveillance/cursor_label") +Elements.cursor_slot = Gui.define("surveillance/cursor_slot") :track_all_elements() :draw{ - type = "label", - caption = { "exp-gui_surveillance.caption-cursor-empty" }, + type = "sprite-button", + style = "slot_button", + tooltip = { "exp-gui_surveillance.tooltip-cursor-empty" }, } :style{ - width = 480, + height = 24, + width = 24, + padding = 0, } :element_data( Gui.from_argument(1) ) --[[@as any]] ---- Calculate the caption describing the cursor of a player +--- @class ExpGui_Surveillance.elements.cursor_slot.display_data +--- @field sprite SpritePath? Nil when the cursor is empty +--- @field number number? Nil for a ghost, which has no count +--- @field tooltip LocalisedString + +--- Calculate what a slot shows for the cursor of a player --- @param player LuaPlayer ---- @return LocalisedString -function Elements.cursor_label.calculate_caption(player) +--- @return ExpGui_Surveillance.elements.cursor_slot.display_data +function Elements.cursor_slot.calculate_display_data(player) local cursor_stack = player.cursor_stack if cursor_stack and cursor_stack.valid_for_read then - local item = format_string("[item=%s,quality=%s]", cursor_stack.name, cursor_stack.quality.name) - return { "exp-gui_surveillance.caption-cursor", item, cursor_stack.count } + return { + sprite = "item/" .. cursor_stack.name, + number = cursor_stack.count, + tooltip = { "exp-gui_surveillance.tooltip-cursor", cursor_stack.prototype.localised_name, cursor_stack.count }, + } end local cursor_ghost = player.cursor_ghost --[[@as ItemIDAndQualityIDPair?]] if cursor_ghost then local prototype = cursor_ghost.name --[[@as LuaItemPrototype]] - return { "exp-gui_surveillance.caption-cursor-ghost", "[item=" .. prototype.name .. "]" } + return { + sprite = "item/" .. prototype.name, + tooltip = { "exp-gui_surveillance.tooltip-cursor-ghost", prototype.localised_name }, + } end - return { "exp-gui_surveillance.caption-cursor-empty" } + return { tooltip = { "exp-gui_surveillance.tooltip-cursor-empty" } } end ---- Refresh a label, hidden when the camera is not following a player ---- @param cursor_label LuaGuiElement +--- Refresh a slot, a ghost is shown greyed out and the slot is hidden when the camera is not following a player +--- @param cursor_slot LuaGuiElement --- @param target_player LuaPlayer? -function Elements.cursor_label.refresh(cursor_label, target_player) - cursor_label.visible = target_player ~= nil - if target_player then - cursor_label.caption = Elements.cursor_label.calculate_caption(target_player) - end +function Elements.cursor_slot.refresh(cursor_slot, target_player) + cursor_slot.visible = target_player ~= nil + if not target_player then return end + + local display_data = Elements.cursor_slot.calculate_display_data(target_player) + cursor_slot.sprite = display_data.sprite or "" + cursor_slot.number = display_data.number + cursor_slot.tooltip = display_data.tooltip + cursor_slot.enabled = display_data.sprite == nil or display_data.number ~= nil end ---- Refresh the labels of all online cameras -function Elements.cursor_label.refresh_online() - for _, cursor_label in Elements.cursor_label:online_elements() do - local camera = Elements.cursor_label.data[cursor_label] - Elements.cursor_label.refresh(cursor_label, Elements.camera.data[camera]) +--- Refresh the slots of all online cameras +function Elements.cursor_slot.refresh_online() + for _, cursor_slot in Elements.cursor_slot:online_elements() do + local camera = Elements.cursor_slot.data[cursor_slot] + Elements.cursor_slot.refresh(cursor_slot, Elements.camera.data[camera]) end end @@ -270,7 +286,6 @@ Elements.container = Gui.define("surveillance/container") local target_player = Gui.get_player(parent) local camera = Elements.camera(screen_frame, target_player) - Elements.cursor_label(screen_frame, camera) local type_dropdown_data = { camera = camera, @@ -281,6 +296,7 @@ Elements.container = Gui.define("surveillance/container") Elements.type_dropdown(button_flow, type_dropdown_data) Elements.zoom_out_button(button_flow, camera) Elements.zoom_in_button(button_flow, camera) + Elements.cursor_slot(button_flow, camera) return Gui.elements.screen_frame.get_root_element(screen_frame) end) @@ -305,7 +321,7 @@ return { [e.on_tick] = Elements.camera.refresh_online, }, on_nth_tick = { - [10] = Elements.cursor_label.refresh_online, + [10] = Elements.cursor_slot.refresh_online, [600] = Elements.type_dropdown.refresh_online, } } diff --git a/exp_scenario/module/locale/en.cfg b/exp_scenario/module/locale/en.cfg index eda13142..baaab220 100644 --- a/exp_scenario/module/locale/en.cfg +++ b/exp_scenario/module/locale/en.cfg @@ -489,9 +489,9 @@ caption-set-location=Set type-player=Player type-static=Static type-loop=Loop -caption-cursor=Cursor: __1__ x__2__ -caption-cursor-ghost=Cursor: __1__ (ghost) -caption-cursor-empty=Cursor: empty +tooltip-cursor=__1__ x__2__ +tooltip-cursor-ghost=__1__ (ghost) +tooltip-cursor-empty=Empty cursor [exp-gui_task-list] caption-main=Task List [img=info] diff --git a/exp_scenario/module/locale/zh-CN.cfg b/exp_scenario/module/locale/zh-CN.cfg index 29343d58..8a8a26e8 100644 --- a/exp_scenario/module/locale/zh-CN.cfg +++ b/exp_scenario/module/locale/zh-CN.cfg @@ -474,9 +474,9 @@ caption-set-location=設 type-player=用戶 type-static=靜態 type-loop=循環 -caption-cursor=游標: __1__ x__2__ -caption-cursor-ghost=游標: __1__ (幻影) -caption-cursor-empty=游標: 空 +tooltip-cursor=__1__ x__2__ +tooltip-cursor-ghost=__1__ (幻影) +tooltip-cursor-empty=游標為空 [exp-gui_task-list] caption-main=工作流程 [img=info] diff --git a/exp_scenario/module/locale/zh-TW.cfg b/exp_scenario/module/locale/zh-TW.cfg index 29343d58..8a8a26e8 100644 --- a/exp_scenario/module/locale/zh-TW.cfg +++ b/exp_scenario/module/locale/zh-TW.cfg @@ -474,9 +474,9 @@ caption-set-location=設 type-player=用戶 type-static=靜態 type-loop=循環 -caption-cursor=游標: __1__ x__2__ -caption-cursor-ghost=游標: __1__ (幻影) -caption-cursor-empty=游標: 空 +tooltip-cursor=__1__ x__2__ +tooltip-cursor-ghost=__1__ (幻影) +tooltip-cursor-empty=游標為空 [exp-gui_task-list] caption-main=工作流程 [img=info] From 1de50da0be09758240636ff155ddf60f19773054 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sun, 6 Sep 2026 01:44:40 +0100 Subject: [PATCH 14/15] Adjust button formatting --- exp_scenario/module/gui/surveillance.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/exp_scenario/module/gui/surveillance.lua b/exp_scenario/module/gui/surveillance.lua index 11057e0a..11b1cd58 100644 --- a/exp_scenario/module/gui/surveillance.lua +++ b/exp_scenario/module/gui/surveillance.lua @@ -218,9 +218,9 @@ Elements.cursor_slot = Gui.define("surveillance/cursor_slot") tooltip = { "exp-gui_surveillance.tooltip-cursor-empty" }, } :style{ - height = 24, - width = 24, - padding = 0, + height = 26, + width = 26, + margin = { -1, -1, -1, -2 }, } :element_data( Gui.from_argument(1) @@ -286,6 +286,7 @@ Elements.container = Gui.define("surveillance/container") local target_player = Gui.get_player(parent) local camera = Elements.camera(screen_frame, target_player) + Elements.cursor_slot(button_flow, camera) local type_dropdown_data = { camera = camera, @@ -296,7 +297,6 @@ Elements.container = Gui.define("surveillance/container") Elements.type_dropdown(button_flow, type_dropdown_data) Elements.zoom_out_button(button_flow, camera) Elements.zoom_in_button(button_flow, camera) - Elements.cursor_slot(button_flow, camera) return Gui.elements.screen_frame.get_root_element(screen_frame) end) From 397033e8a6b907b1bd3f2041f173dcfa3c5d2a76 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:45:53 +0000 Subject: [PATCH 15/15] Read ammo slots on join so the first shot is logged Clearing the slots on leave meant nothing was known about a player's ammo until it changed, so the first shot after rejoining was missed. The slots are now read when a player joins and when they respawn, since a new character starts empty and a stale single round would otherwise count as a shot. The change handler reads the slots the same way and compares them with what was stored. --- .../module/control/deconstruction_log.lua | 62 ++++++++++++------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/exp_scenario/module/control/deconstruction_log.lua b/exp_scenario/module/control/deconstruction_log.lua index 7688e5d5..325bb3f8 100644 --- a/exp_scenario/module/control/deconstruction_log.lua +++ b/exp_scenario/module/control/deconstruction_log.lua @@ -148,34 +148,42 @@ Storage.register(ammo_slots, function(tbl) ammo_slots = tbl end) ---- Log a shot, there is no fired event so a slot losing one of the same ammo is taken as a shot ---- @param event EventData.on_player_ammo_inventory_changed -local function on_player_ammo_inventory_changed(event) - local player = get_log_player(event) - if not player or not player.character then return end - - local slots = ammo_slots[player.index] - if not slots then - slots = {} - ammo_slots[player.index] = slots - end +--- Read the ammo slots of a player, empty while they have no character +--- @param player LuaPlayer +--- @return table +local function read_ammo_slots(player) + local slots = {} + if not player.character then return slots end local character_ammo = assert(player.get_inventory(defines.inventory.character_ammo)) for index = 1, #character_ammo do local stack = character_ammo[index --[[@as uint]]] - local previous = slots[index] - local fired = nil --- @type string? - if stack.valid_for_read then - if previous and previous.name == stack.name and previous.count == stack.count + 1 then - fired = stack.name - end slots[index] = { name = stack.name, count = stack.count } - else - if previous and previous.count == 1 then - fired = previous.name + end + end + return slots +end + +--- Log a shot, there is no fired event so a slot losing one of the same ammo is taken as a shot +--- @param event EventData.on_player_ammo_inventory_changed +local function on_player_ammo_inventory_changed(event) + local player = get_log_player(event) + if not player then return end + + local previous_slots = ammo_slots[player.index] or {} + local slots = read_ammo_slots(player) + ammo_slots[player.index] = slots + + for index, previous in pairs(previous_slots) do + local current = slots[index] + local fired = nil --- @type string? + if current then + if previous.name == current.name and previous.count == current.count + 1 then + fired = current.name end - slots[index] = nil + elseif previous.count == 1 then + fired = previous.name end if fired and logged_ammo[fired] then @@ -184,13 +192,19 @@ local function on_player_ammo_inventory_changed(event) end end ---- Forget the ammo of a player who left, their slots are read again on the next change +--- Read the ammo of a player when they join or get a new character, so the first shot afterwards is seen +--- @param event EventData.on_player_joined_game | EventData.on_player_respawned +local function on_player_character_changed(event) + local player = assert(game.get_player(event.player_index)) + ammo_slots[player.index] = read_ammo_slots(player) +end + +--- Forget the ammo of a player who left --- @param event EventData.on_player_left_game local function on_player_left_game(event) ammo_slots[event.player_index] = nil end - local e = defines.events local events = { [e.on_multiplayer_init] = clear_log, @@ -210,6 +224,8 @@ end if config.fired_rocket or config.fired_explosive_rocket or config.fired_nuke then events[e.on_player_ammo_inventory_changed] = on_player_ammo_inventory_changed + events[e.on_player_joined_game] = on_player_character_changed + events[e.on_player_respawned] = on_player_character_changed events[e.on_player_left_game] = on_player_left_game end