mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-11-13 08:04:53 +09:00
- Feature.register returns the config as a second value, so files write `local feature, config = Feature.register(...)`. - The lib is ExpScenario.Features and the instance class with its fields sits on Feature._prototype, which is the __index, the same shape as exp_commands. - update_config only looks at values whose override was added, removed, or replaced. _apply_field sets one value and returns it, so the comparison happens while applying and the second loop only raises. Overrides dropped from the previous set count as candidates because the value reverts to its default. - The `@setting name` tags are gone from the config comments. - Protection is guarded as a whole instead of checking config.enabled in one handler. The handlers also forget mined entities, so get_entities and is_entity_protected now drop invalid references, which also covers entities destroyed without an event. The list getters are gone, the selection overlay builds its lists from Protection.config. - Chat and damage popups are one feature in control/popups.lua. - ignore_admins and command_admin_only are removed, roles cover them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
244 lines
9.0 KiB
Lua
244 lines
9.0 KiB
Lua
--[[-- 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, config = Feature.register("protection", {
|
|
repeat_count = 5, -- Number of protected entities that must be removed within repeat_minutes in order to trigger repeated removal protection
|
|
repeat_minutes = 20, -- The length of time, in minutes, that protected removals will be remembered for
|
|
always_protected_names = Feature.set{}, -- Names of entities which are always protected
|
|
always_protected_types = Feature.set{ -- 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 = Feature.set{}, -- Names of entities which always trigger repeated removal protection
|
|
always_trigger_repeat_types = Feature.set{ -- Types of entities which always trigger repeated removal protection
|
|
"reactor", "fusion-reactor", "rocket-silo",
|
|
},
|
|
})
|
|
|
|
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(),
|
|
--- The config of the feature, read only
|
|
config = config,
|
|
--- @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
|
|
|
|
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
|
|
-- Entities removed while the feature was disabled, or without an event, are forgotten here
|
|
--- @param surface LuaSurface
|
|
--- @return table<string, LuaEntity>
|
|
function Protection.get_entities(surface)
|
|
local entities = protected_entities[surface.index] or {}
|
|
for key, entity in pairs(entities) do
|
|
if not entity.valid then
|
|
entities[key] = nil
|
|
end
|
|
end
|
|
return entities
|
|
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 config.always_protected_names[entity.name] or config.always_protected_types[entity.type] then return true end
|
|
local entities = protected_entities[entity.surface.index]
|
|
if not entities then return false end
|
|
local key = Protection.get_entity_key(entity)
|
|
local protected = entities[key]
|
|
if protected and not protected.valid then
|
|
entities[key] = nil
|
|
return false
|
|
end
|
|
return protected == 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
|
|
--- @param player LuaPlayer
|
|
--- @param entity LuaEntity
|
|
--- @return boolean
|
|
local function is_ignored(player, entity)
|
|
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 = config.always_trigger_repeat_names[entity.name] or 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]
|
|
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_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 feature:guard(Protection)
|