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.
This commit is contained in:
bbassie
2026-09-05 19:19:45 +00:00
parent 810269971b
commit 0c52532c05
6 changed files with 247 additions and 228 deletions
@@ -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
@@ -12,10 +12,9 @@ local Commands = require("modules/exp_commands")
local format_player_name = Commands.format_player_name_locale local format_player_name = Commands.format_player_name_locale
local Roles = require("modules/exp_roles") local Roles = require("modules/exp_roles")
local EntityProtection = require("modules.exp_legacy.modules.control.protection") --- @dep modules.control.protection local EntityProtection = require("modules/exp_scenario/control/protection")
local get_entity_key = EntityProtection.get_entity_key
local format_string = string.format local get_area_key = EntityProtection.get_area_key
local floor = math.floor
local Selection = require("modules/exp_util/selection") local Selection = require("modules/exp_util/selection")
local SelectEntities = Selection.connect("ExpCommand_ProtectEntity") local SelectEntities = Selection.connect("ExpCommand_ProtectEntity")
@@ -28,21 +27,6 @@ Storage.register({
renders = tbl.renders renders = tbl.renders
end) 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 --- Show a protected entity to a player
--- @param player LuaPlayer --- @param player LuaPlayer
--- @param entity LuaEntity --- @param entity LuaEntity
@@ -232,6 +216,6 @@ end
return { return {
events = { events = {
[EntityProtection.events.on_repeat_violation] = on_repeat_violation, [EntityProtection.on_repeat_violation] = on_repeat_violation,
} }
} }
+1
View File
@@ -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/mine_depletion"))
add(require("modules/exp_scenario/control/nuke_protection")) add(require("modules/exp_scenario/control/nuke_protection"))
add(require("modules/exp_scenario/control/pollution_grading")) 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/protection_jail"))
add(require("modules/exp_scenario/control/report_jail")) add(require("modules/exp_scenario/control/report_jail"))
add(require("modules/exp_scenario/control/research")) add(require("modules/exp_scenario/control/research"))
@@ -81,8 +81,8 @@ end
--- Repeated protected entity mining --- Repeated protected entity mining
if config.entity_protection then if config.entity_protection then
local EntityProtection = require("modules.exp_legacy.modules.control.protection") local EntityProtection = require("modules/exp_scenario/control/protection")
events[EntityProtection.events.on_repeat_violation] = function(event) events[EntityProtection.on_repeat_violation] = function(event)
local player_name = get_player_name(event) local player_name = get_player_name(event)
emit_event{ emit_event{
title = "Entity Protection", title = "Entity Protection",
+238
View File
@@ -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<string, true>
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<uint, table<string, LuaEntity>> Keyed by surface index then entity key
local protected_areas = {} --- @type table<uint, table<string, BoundingBox>> Keyed by surface index then area key
local repeats = {} --- @type table<string, ExpScenario_Protection.Repeat> 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<string, LuaEntity>
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<string, BoundingBox>
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
@@ -5,7 +5,7 @@ When a player triggers protection multiple times they are automatically jailed
local ExpUtil = require("modules/exp_util") local ExpUtil = require("modules/exp_util")
local Storage = require("modules/exp_util/storage") local Storage = require("modules/exp_util/storage")
local Jail = require("modules.exp_legacy.modules.control.jail") 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 local format_player_name = ExpUtil.format_player_name_locale
@@ -41,7 +41,7 @@ local e = defines.events
return { return {
events = { 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, [e.on_player_left_game] = on_player_left_game,
} }
} }