Merge pull request #213 from Cooldude2606/feature/entity-alert

Entity protection
This commit is contained in:
Cooldude2606
2021-04-26 00:22:36 +01:00
committed by GitHub
9 changed files with 644 additions and 1 deletions

View File

@@ -30,6 +30,7 @@ return {
'modules.commands.home',
'modules.commands.connect',
'modules.commands.last-location',
'modules.commands.protection',
'modules.commands.spectate',
'modules.commands.search',

View File

@@ -2,6 +2,7 @@
-- @config Discord-Alerts
return {
entity_protection=true,
player_reports=true,
player_warnings=true,
player_bans=true,

View File

@@ -99,6 +99,8 @@ Roles.new_role('Trainee','TrMod')
'command/give-warning',
'command/get-warnings',
'command/get-reports',
'command/protect-entity',
'command/protect-area',
'command/jail',
'command/unjail',
'command/kick',
@@ -219,7 +221,8 @@ Roles.new_role('Regular','Reg')
'command/rainbow',
'command/go-to-spawn',
'command/me',
'standard-decon'
'standard-decon',
'bypass-entity-protection'
}
:set_auto_assign_condition(function(player)
if player.online_time >= hours3 then

19
config/protection.lua Normal file
View File

@@ -0,0 +1,19 @@
return {
ignore_admins = true, --- @setting ignore_admins If admins are ignored by the protection filter
ignore_permission = 'bypass-entity-protection', --- @setting ignore_permission Players with this permission will be ignored by the protection filter, leave nil if expcore.roles is not used
repeat_count = 5, --- @setting repeat_count Number of protected entities that must be removed within repeat_lifetime in order to trigger repeated removal protection
repeat_lifetime = 3600*20, --- @setting repeat_lifetime The length of time, in ticks, that protected removals will be remembered for
refresh_rate = 3600*5, --- @setting refresh_rate How often the age of protected removals are checked against repeat_lifetime
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', 'power-switch', 'reactor', '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', 'rocket-silo'
}
}

View File

@@ -85,6 +85,16 @@ none-matching=No servers were found with that name, if you used an address pleas
[expcom-lastlocation]
response=Last location of __1__ was [gps=__2__,__3__]
[expcom-protection]
entered-entity-selection=Entered entity selection, select entites to protect, hold shift to remove protection.
entered-area-selection=Entered area selection, select areas to protect, hold shift to remove protection.
protected-entities=__1__ entities have been protected.
unprotected-entities=__1__ entities have been unprotected.
already-protected=This area is already protected.
protected-area=This area is now protected.
unprotected-area=This area is now unprotected.
repeat-offence=__1__ has removed __2__ at [gps=__3__,__4__]
[expcom-spectate]
follow-self=You can not follow yourself

View File

@@ -76,6 +76,21 @@ local function emit_event(args)
})
end
--- Repeated protected entity mining
if config.entity_protection then
local EntityProtection = require 'modules.control.protection' --- @dep modules.control.protection
Event.add(EntityProtection.events.on_repeat_violation, function(event)
local player_name = get_player_name(event)
emit_event{
title='Entity Protection',
description='A player removed protected entities',
color=Colors.yellow,
['Player']='<inline>'..player_name,
['Entity']='<inline>'..event.entity.name
}
end)
end
--- Reports added and removed
if config.player_reports then
local Reports = require 'modules.control.reports' --- @dep modules.control.reports

View File

@@ -0,0 +1,216 @@
--[[-- Commands Module - Protection
- Adds commands that can add and remove protection
@commands Protection
]]
local Event = require 'utils.event' --- @dep utils.event
local Global = require 'utils.global' --- @dep utils.global
local Roles = require 'expcore.roles' --- @dep expcore.roles
local Commands = require 'expcore.commands' --- @dep expcore.commands
local format_chat_player_name = _C.format_chat_player_name --- @dep expcore.common
local EntityProtection = require 'modules.control.protection' --- @dep modules.control.protection
local Selection = require 'modules.control.selection' --- @dep modules.control.selection
local SelectionProtectEntity = 'ProtectEntity'
local SelectionProtectArea = 'ProtectArea'
local renders = {} -- Stores all renders for a player
Global.register({
renders = renders
}, function(tbl)
renders = tbl.renders
end)
--- Test if a point is inside an aabb
local function aabb_point_enclosed(point, aabb)
return point.x >= aabb.left_top.x and point.y >= aabb.left_top.y
and point.x <= aabb.right_bottom.x and point.y <= aabb.right_bottom.y
end
--- Test if an aabb is inside another aabb
local function aabb_area_enclosed(aabbOne, aabbTwo)
return aabb_point_enclosed(aabbOne.left_top, aabbTwo)
and aabb_point_enclosed(aabbOne.right_bottom, aabbTwo)
end
--- Align an aabb to the grid by expanding it
local function aabb_align_expand(aabb)
return {
left_top = { x = math.floor(aabb.left_top.x), y = math.floor(aabb.left_top.y) },
right_bottom = { x = math.ceil(aabb.right_bottom.x), y = math.ceil(aabb.right_bottom.y) }
}
end
--- 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
--- Show a protected entity to a player
local function show_protected_entity(player, entity)
local key = get_entity_key(entity)
if renders[player.index][key] then return end
local rb = entity.selection_box.right_bottom
local render_id = rendering.draw_sprite{
sprite = 'utility/notification',
target = entity,
target_offset = {
(rb.x-entity.position.x)*0.75,
(rb.y-entity.position.y)*0.75
},
x_scale = 2,
y_scale = 2,
surface = entity.surface,
players = { player }
}
renders[player.index][key] = render_id
end
--- Show a protected area to a player
local function show_protected_area(player, surface, area)
local key = get_area_key(area)
if renders[player.index][key] then return end
local render_id = rendering.draw_rectangle{
color = {1, 1, 0, 0.5},
filled = false,
width = 3,
left_top = area.left_top,
right_bottom = area.right_bottom,
surface = surface,
players = { player }
}
renders[player.index][key] = render_id
end
--- Remove a render object for a player
local function remove_render(player, key)
local render = renders[player.index][key]
if render and rendering.is_valid(render) then rendering.destroy(render) end
renders[player.index][key] = nil
end
--- Toggles entity protection selection
-- @command protect-entity
Commands.new_command('protect-entity', 'Toggles entity protection selection, hold shift to remove protection')
:add_alias('pe')
:register(function(player)
if Selection.is_selecting(player, SelectionProtectEntity) then
Selection.stop(player)
else
Selection.start(player, SelectionProtectEntity)
return Commands.success{'expcom-protection.entered-entity-selection'}
end
end)
--- Toggles area protection selection
-- @command protect-area
Commands.new_command('protect-area', 'Toggles area protection selection, hold shift to remove protection')
:add_alias('pa')
:register(function(player)
if Selection.is_selecting(player, SelectionProtectArea) then
Selection.stop(player)
else
Selection.start(player, SelectionProtectArea)
return Commands.success{'expcom-protection.entered-area-selection'}
end
end)
--- When an area is selected to add protection to entities
Selection.on_selection(SelectionProtectEntity, function(event)
local player = game.get_player(event.player_index)
for _, entity in ipairs(event.entities) do
EntityProtection.add_entity(entity)
show_protected_entity(player, entity)
end
player.print{'expcom-protection.protected-entities', #event.entities}
end)
--- When an area is selected to remove protection from entities
Selection.on_alt_selection(SelectionProtectEntity, function(event)
local player = game.get_player(event.player_index)
for _, entity in ipairs(event.entities) do
EntityProtection.remove_entity(entity)
remove_render(player, get_entity_key(entity))
end
player.print{'expcom-protection.unprotected-entities', #event.entities}
end)
--- When an area is selected to add protection to the area
Selection.on_selection(SelectionProtectArea, function(event)
local area = aabb_align_expand(event.area)
local areas = EntityProtection.get_areas(event.surface)
local player = game.get_player(event.player_index)
for _, next_area in pairs(areas) do
if aabb_area_enclosed(area, next_area) then
return player.print{'expcom-protection.already-protected'}
end
end
EntityProtection.add_area(event.surface, area)
show_protected_area(player, event.surface, area)
player.print{'expcom-protection.protected-area'}
end)
--- When an area is selected to remove protection from the area
Selection.on_alt_selection(SelectionProtectArea, function(event)
local area = aabb_align_expand(event.area)
local areas = EntityProtection.get_areas(event.surface)
local player = game.get_player(event.player_index)
for _, next_area in pairs(areas) do
if aabb_area_enclosed(next_area, area) then
EntityProtection.remove_area(event.surface, next_area)
player.print{'expcom-protection.unprotected-area'}
remove_render(player, get_area_key(next_area))
end
end
end)
--- When selection starts show all protected entities and protected areas
Event.add(Selection.events.on_player_selection_start, function(event)
if event.selection ~= SelectionProtectEntity and event.selection ~= SelectionProtectArea then return end
local player = game.get_player(event.player_index)
local surface = player.surface
renders[player.index] = {}
-- Show protected entities
local entities = EntityProtection.get_entities(surface)
for _, entity in pairs(entities) do
show_protected_entity(player, entity)
end
-- Show always protected entities by name
if #EntityProtection.protected_entity_names > 0 then
for _, entity in pairs(surface.find_entities_filtered{ name = EntityProtection.protected_entity_names, force = player.force }) do
show_protected_entity(player, entity)
end
end
-- Show always protected entities by type
if #EntityProtection.protected_entity_types > 0 then
for _, entity in pairs(surface.find_entities_filtered{ type = EntityProtection.protected_entity_types, force = player.force }) do
show_protected_entity(player, entity)
end
end
-- Show protected areas
local areas = EntityProtection.get_areas(surface)
for _, area in pairs(areas) do
show_protected_area(player, surface, area)
end
end)
--- When selection ends hide protected entities and protected areas
Event.add(Selection.events.on_player_selection_end, function(event)
if event.selection ~= SelectionProtectEntity and event.selection ~= SelectionProtectArea then return end
for _, id in pairs(renders[event.player_index]) do
if rendering.is_valid(id) then rendering.destroy(id) end
end
renders[event.player_index] = nil
end)
--- When there is a repeat offence print it in chat
Event.add(EntityProtection.events.on_repeat_violation, function(event)
local player_name = format_chat_player_name(event.player_index)
Roles.print_to_roles_higher('Regular', {'expcom-protection.repeat-offence', player_name, event.entity.localised_name, event.entity.position.x, event.entity.position.y})
end)

View File

@@ -0,0 +1,206 @@
--[[-- Control Module - Protection
- Controls protected entities
@control Protection
@alias Protection
]]
local Global = require 'utils.global' --- @dep utils.global
local Event = require 'utils.event' --- @dep utils.event
local config = require '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
-- Require roles if a permission is assigned in the config
local Roles
if config.ignore_permission then
Roles = require 'expcore.roles' --- @dep expcore.roles
end
----- Global 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
Global.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.get_player(event.player_index)
-- Check if the player should be ignored
if config.ignore_admins and player.admin then return end
if entity.last_user.index == player.index then return end
if config.ignore_permission and Roles.player_allowed(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_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

View File

@@ -0,0 +1,172 @@
--[[-- Control Module - Selection
- Controls players who have a selection planner, mostly event handlers
@control Selection
@alias Selection
]]
local Event = require 'utils.event' --- @dep utils.event
local Global = require 'utils.global' --- @dep utils.global
local Selection = {
events = {
--- When a player enters selection mode
-- @event on_player_selection_start
-- @tparam number player_index the player index of the player who entered selection mode
-- @tparam string selection the name of the selection being made
on_player_selection_start = script.generate_event_name(),
--- When a player leaves selection mode
-- @event on_player_selection_end
-- @tparam number player_index the player index of the player who left selection mode
-- @tparam string selection the name of the selection which ended
on_player_selection_end = script.generate_event_name(),
}
}
local selection_tool = { name='selection-tool' }
local selections = {}
Global.register({
selections = selections
}, function(tbl)
selections = tbl.selections
end)
--- Let a player select an area by providing a selection planner
-- @tparam LuaPlayer player The player to place into selection mode
-- @tparam string selection_name The name of the selection to start, used with on_selection
-- @tparam[opt=false] boolean single_use When true the selection will stop after first use
function Selection.start(player, selection_name, single_use, ...)
if not player or not player.valid then return end
if selections[player.index] then
-- Raise the end event if a selection was already in progress
script.raise_event(Selection.events.on_player_selection_end, {
name = Selection.events.on_player_selection_end,
tick = game.tick,
player_index = player.index,
selection = selections[player.index].name
})
end
-- Set the selection data
selections[player.index] = {
name = selection_name,
arguments = { ... },
single_use = single_use == true,
character = player.character
}
-- Raise the event
script.raise_event(Selection.events.on_player_selection_start, {
name = Selection.events.on_player_selection_start,
tick = game.tick,
player_index = player.index,
selection = selection_name
})
-- Give a selection tool if one is not in use
if player.cursor_stack.is_selection_tool then return end
player.clear_cursor() -- Clear the current item
player.cursor_stack.set_stack(selection_tool)
-- Make a slot to place the selection tool even if inventory is full
if not player.character then return end
player.character_inventory_slots_bonus = player.character_inventory_slots_bonus + 1
player.hand_location = { inventory = defines.inventory.character_main, slot = #player.get_main_inventory() }
end
--- Stop a player selection by removing the selection planner
-- @tparam LuaPlayer player The player to exit out of selection mode
function Selection.stop(player)
if not selections[player.index] then return end
local character = selections[player.index].character
local selection = selections[player.index].name
selections[player.index] = nil
-- Raise the event
script.raise_event(Selection.events.on_player_selection_end, {
name = Selection.events.on_player_selection_end,
tick = game.tick,
player_index = player.index,
selection = selection
})
-- Remove the selection tool
if player.cursor_stack.is_selection_tool then
player.cursor_stack.clear()
else
player.remove_item(selection_tool)
end
-- Remove the extra slot
if character and character == player.character then
player.character_inventory_slots_bonus = player.character_inventory_slots_bonus - 1
player.hand_location = nil
end
end
--- Get the selection arguments for a player
-- @tparam LuaPlayer player The player to get the selection arguments for
function Selection.get_arguments(player)
if not selections[player.index] then return end
return selections[player.index].arguments
end
--- Test if a player is selecting something
-- @tparam LuaPlayer player The player to test
-- @tparam[opt] string selection_name If given will only return true if the selection is this selection
function Selection.is_selecting(player, selection_name)
if selection_name ~= nil then
if not selections[player.index] then return false end
return selections[player.index].name == selection_name
else
return player.cursor_stack.is_selection_tool
end
end
--- Filter on_player_selected_area to this custom selection, appends the selection arguments
-- @tparam string selection_name The name of the selection to listen for
-- @tparam function handler The event handler
function Selection.on_selection(selection_name, handler)
Event.add(defines.events.on_player_selected_area, function(event)
local selection = selections[event.player_index]
if not selection or selection.name ~= selection_name then return end
handler(event, unpack(selection.arguments))
end)
end
--- Filter on_player_alt_selected_area to this custom selection, appends the selection arguments
-- @param string selection_name The name of the selection to listen for
-- @param function handler The event handler
function Selection.on_alt_selection(selection_name, handler)
Event.add(defines.events.on_player_alt_selected_area, function(event)
local selection = selections[event.player_index]
if not selection or selection.name ~= selection_name then return end
handler(event, unpack(selection.arguments))
end)
end
--- Stop selection if the selection tool is removed from the cursor
Event.add(defines.events.on_player_cursor_stack_changed, function(event)
local player = game.get_player(event.player_index)
if player.cursor_stack.is_selection_tool then return end
Selection.stop(player)
end)
--- Stop selection after an event such as death or leaving the game
local function stop_after_event(event)
local player = game.get_player(event.player_index)
Selection.stop(player)
end
Event.add(defines.events.on_pre_player_left_game, stop_after_event)
Event.add(defines.events.on_pre_player_died, stop_after_event)
--- Stop selection after a single use if single_use was true during Selection.start
local function stop_after_use(event)
if not selections[event.player_index] then return end
if not selections[event.player_index].single_use then return end
stop_after_event(event)
end
Event.add(defines.events.on_player_selected_area, stop_after_use)
Event.add(defines.events.on_player_alt_selected_area, stop_after_use)
return Selection