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