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] 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, } }