--[[-- 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 Feature = require("modules/exp_scenario/features") local Roles = require("modules/exp_roles") local feature = Feature.register("protection", { ignore_admins = true, --- @setting ignore_admins If admins are ignored by the protection filter repeat_count = 5, --- @setting repeat_count Number of protected entities that must be removed within repeat_minutes in order to trigger repeated removal protection repeat_minutes = 20, --- @setting repeat_minutes The length of time, in minutes, that protected removals will be remembered for always_protected_names = {}, --- @setting always_protected_names Names of entities which are always protected always_protected_types = { --- @setting always_protected_types Types of entities which are always protected "boiler", "generator", "offshore-pump", "reactor", "heat-exchanger", "heat-pipe", "fusion-reactor", "fusion-generator", "power-switch", "rocket-silo", }, always_trigger_repeat_names = {}, --- @setting always_trigger_repeat_names Names of entities which always trigger repeated removal protection always_trigger_repeat_types = { --- @setting always_trigger_repeat_types Types of entities which always trigger repeated removal protection "reactor", "fusion-reactor", "rocket-silo", }, }) local config = feature.config 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(), --- @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 --- Names of entities which are always protected --- @return string[] function Protection.get_protected_entity_names() return config.always_protected_names end --- Types of entities which are always protected --- @return string[] function Protection.get_protected_entity_types() return config.always_protected_types end 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 Feature.to_set(config.always_protected_names)[entity.name] or Feature.to_set(config.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 Roles.player_has_permission(player, "exp_scenario.bypass.entity_protection") 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 script.raise_event(Protection.on_player_mined_protected, event) local entity = event.entity local always_repeat = Feature.to_set(config.always_trigger_repeat_names)[entity.name] or Feature.to_set(config.always_trigger_repeat_types)[entity.type] if always_repeat or player_repeats.count >= config.repeat_count then player_repeats.count = 0 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] -- Protected entities are still forgotten while disabled, only the violations stop if config.enabled and 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_minutes * 3600 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[3600 * 5] = clear_old_repeats return Protection