diff --git a/exp_scenario/features.ts b/exp_scenario/features.ts index 3327bbb8..dc7c24de 100644 --- a/exp_scenario/features.ts +++ b/exp_scenario/features.ts @@ -86,7 +86,6 @@ export const features: Feature[] = [ description: "Reply to key words in chat and run chat commands", fields: [ str("command_prefix", "Command prefix", "Prefix for chat commands, and for key words to reply to everyone", "!"), - bool("command_admin_only", "Commands for admins only", "Only admins can use chat commands", false), ], }, { @@ -142,7 +141,6 @@ export const features: Feature[] = [ title: "Nuke Protection", description: "Remove banned items from players without the nuke protection bypass permission", fields: [ - bool("ignore_admins", "Ignore admins", "Admins can hold banned items", true), list("banned_items", "Banned items", "Items which are moved to a chest at spawn", ["atomic-bomb"]), ], }, @@ -163,7 +161,6 @@ export const features: Feature[] = [ title: "Entity Protection", description: "Stop players removing protected entities placed by others, players with the bypass permission are ignored", fields: [ - bool("ignore_admins", "Ignore admins", "Admins can remove protected entities", true), num("repeat_count", "Repeat count", "Protected entities removed within the repeat time which count as a repeat violation", 5, 1), num("repeat_minutes", "Repeat time", "How long removed protected entities are remembered", 20, 1, "minutes"), list("always_protected_names", "Protected entity names", "Entities which are always protected", []), diff --git a/exp_scenario/module/commands/lawnmower.lua b/exp_scenario/module/commands/lawnmower.lua index ab69d9d5..26cc33fa 100644 --- a/exp_scenario/module/commands/lawnmower.lua +++ b/exp_scenario/module/commands/lawnmower.lua @@ -8,10 +8,9 @@ local Feature = require("modules/exp_scenario/features") local Selection = require("modules/exp_util/selection") local SelectArea = Selection.connect("ExpCommand_Lawnmower") -local feature = Feature.register("lawnmower", { - destroy_decoratives = false, --- @setting destroy_decoratives remove decoratives under entities when they are built +local feature, config = Feature.register("lawnmower", { + destroy_decoratives = false, -- remove decoratives under entities when they are built }) -local config = feature.config --- @class ExpCommand_Lawnmower.commands local commands = {} diff --git a/exp_scenario/module/commands/protected_entities.lua b/exp_scenario/module/commands/protected_entities.lua index b2b14c9a..b35861e2 100644 --- a/exp_scenario/module/commands/protected_entities.lua +++ b/exp_scenario/module/commands/protected_entities.lua @@ -153,6 +153,17 @@ SelectArea:on_alt_selection(function(event) end end) +--- The keys of a set as a list, find_entities_filtered takes a list of names +--- @param set table +--- @return string[] +local function set_keys(set) + local keys = {} + for key in pairs(set) do + keys[#keys + 1] = key + end + return keys +end + --- When selection starts show all protected entities and protected areas local function on_player_selection_start(event) local player = game.players[event.player_index] @@ -166,7 +177,7 @@ local function on_player_selection_start(event) end -- Show always protected entities by name - local protected_entity_names = EntityProtection.get_protected_entity_names() + local protected_entity_names = set_keys(EntityProtection.config.always_protected_names) if #protected_entity_names > 0 then for _, entity in pairs(surface.find_entities_filtered{ name = protected_entity_names, force = player.force }) do show_protected_entity(player, entity) @@ -174,7 +185,7 @@ local function on_player_selection_start(event) end -- Show always protected entities by type - local protected_entity_types = EntityProtection.get_protected_entity_types() + local protected_entity_types = set_keys(EntityProtection.config.always_protected_types) if #protected_entity_types > 0 then for _, entity in pairs(surface.find_entities_filtered{ type = protected_entity_types, force = player.force }) do show_protected_entity(player, entity) diff --git a/exp_scenario/module/config/chat_auto_reply.lua b/exp_scenario/module/config/chat_auto_reply.lua index 718402d6..a9e627b9 100644 --- a/exp_scenario/module/config/chat_auto_reply.lua +++ b/exp_scenario/module/config/chat_auto_reply.lua @@ -27,7 +27,7 @@ local afk_time_units = { return Feature.register("chat_auto_reply", { --- @type table - messages = { --- @setting messages will trigger when ever the word is said + messages = { -- will trigger when ever the word is said ["discord"] = { "info.discord", config_server_detail["discord"] }, ["expgaming"] = { "info.website", config_server_detail["website"] }, ["website"] = { "info.website", config_server_detail["website"] }, @@ -65,11 +65,10 @@ return Feature.register("chat_auto_reply", { return { "exp_chat-auto-reply.reply-afk", max.name, ExpUtil.format_time_locale(max.afk_time, "long", afk_time_units) } end, }, - allow_command_prefix_for_messages = true, --- @setting allow_command_prefix_for_messages when true any message trigger will print to all player when prefixed - command_admin_only = false, --- @setting command_admin_only when true will only allow chat commands for admins - command_prefix = "!", --- @setting command_prefix prefix used for commands below and to print to all players (if enabled above) + allow_command_prefix_for_messages = true, -- when true any message trigger will print to all player when prefixed + command_prefix = "!", -- prefix used for commands below and to print to all players (if enabled above) --- @type table - commands = { --- @setting commands will trigger only when command prefix is given + commands = { -- will trigger only when command prefix is given ["dev"] = { "exp_chat-auto-reply.reply-dev" }, ["magic"] = { "exp_chat-auto-reply.reply-magic" }, ["aids"] = { "exp_chat-auto-reply.reply-aids" }, diff --git a/exp_scenario/module/config/popups.lua b/exp_scenario/module/config/popups.lua deleted file mode 100644 index 1ad77145..00000000 --- a/exp_scenario/module/config/popups.lua +++ /dev/null @@ -1,12 +0,0 @@ ---- Popups for chat and damage, shared by control/chat_popup.lua and control/damage_popups.lua --- @config Popups - -local Feature = require("modules/exp_scenario/features") - -return Feature.register("popups", { - show_player_messages = true, --- @setting show_player_messages weather a message in chat will make a popup above them - show_player_mentions = true, --- @setting show_player_mentions weather a mentioned player will have a popup when mentioned in chat - show_player_damage = true, --- @setting show_player_damage weather to show damage done by players - show_player_health = true, --- @setting show_player_health weather to show player health when attacked - damage_location_variance = 0.8, --- @setting damage_location_variance how close to the eade of an entity the popups will appear -}) diff --git a/exp_scenario/module/config/spawn_area.lua b/exp_scenario/module/config/spawn_area.lua index 689af943..605ac94f 100644 --- a/exp_scenario/module/config/spawn_area.lua +++ b/exp_scenario/module/config/spawn_area.lua @@ -4,39 +4,39 @@ local Feature = require("modules/exp_scenario/features") return Feature.register("spawn_area", { - spawn_area = { --- @setting spawn_area Settings relating to the whole spawn area + spawn_area = { -- Settings relating to the whole spawn area -- Enable predefined patches: 128, else: 32 - deconstruction_radius = 20, -- @setting deconstruction_radius All entities within this radius will be removed + deconstruction_radius = 20, -- All entities within this radius will be removed tile_radius = 20, - deconstruction_tile = "concrete", --- @setting deconstruction_tile Tile to be placed in the deconstruction radius, use nil for map gen - landfill_radius = 50, --- @setting pattern_radius All water within this radius will be land filled + deconstruction_tile = "concrete", -- Tile to be placed in the deconstruction radius, use nil for map gen + landfill_radius = 50, -- All water within this radius will be land filled }, - turrets = { --- @setting turrets Settings relating to adding turrets to spawn - enabled = true, --- @setting enabled Whether turrets will be added to spawn - ammo_type = "uranium-rounds-magazine", --- @setting ammo_type The ammo type that will be used during refills - offset = { x = 0, y = 0 }, --- @setting offset The position offset to apply to turrets - locations = { --- @setting locations The locations of all turrets, this list can change during runtime + turrets = { -- Settings relating to adding turrets to spawn + enabled = true, -- Whether turrets will be added to spawn + ammo_type = "uranium-rounds-magazine", -- The ammo type that will be used during refills + offset = { x = 0, y = 0 }, -- The position offset to apply to turrets + locations = { -- The locations of all turrets, this list can change during runtime { -3, -3 }, { 3, -3 }, { -3, 3 }, { 3, 3 }, }, }, - afk_belts = { --- @setting afk_belts Settings relating to adding afk belts to spawn - enabled = true, --- @setting enabled Whether afk belts will be added to spawn - belt_type = "transport-belt", --- @setting belt_type The belt to be used as afk belts - protected = true, --- @setting protected Whether belts will be protected from player interaction - offset = { x = 0, y = 0 }, --- @setting offset The position offset to apply to afk belts - locations = { --- @setting locations The locations to spawn afk belts at, given as the top left position + afk_belts = { -- Settings relating to adding afk belts to spawn + enabled = true, -- Whether afk belts will be added to spawn + belt_type = "transport-belt", -- The belt to be used as afk belts + protected = true, -- Whether belts will be protected from player interaction + offset = { x = 0, y = 0 }, -- The position offset to apply to afk belts + locations = { -- The locations to spawn afk belts at, given as the top left position { -5, -5 }, { 5, -5 }, { -5, 5 }, { 5, 5 }, }, }, - water = { --- @setting water Settings relating to adding water to spawn - enabled = true, --- @setting enabled Whether water tiles will be added to spawn - water_tile = "water-mud", --- @setting water_tile The tile to be used as the water tile - offset = { x = 0, y = 0 }, --- @setting offset The position offset to apply to water tiles - locations = { --- @setting locations The location of the water tiles {x,y} + water = { -- Settings relating to adding water to spawn + enabled = true, -- Whether water tiles will be added to spawn + water_tile = "water-mud", -- The tile to be used as the water tile + offset = { x = 0, y = 0 }, -- The position offset to apply to water tiles + locations = { -- The location of the water tiles {x,y} -- Each is a 3x3 with the closest tile to 0,0 removed { 7, 8 }, { 7, 9 }, { 8, 7 }, { 8, 8 }, { 8, 9 }, { 9, 7 }, { 9, 8 }, { 9, 9 }, -- Bottom Right { 7, -9 }, { 7, -10 }, { 8, -8 }, { 8, -9 }, { 8, -10 }, { 9, -8 }, { 9, -9 }, { 9, -10 }, -- Top Right @@ -44,12 +44,12 @@ return Feature.register("spawn_area", { { -8, 8 }, { -8, 9 }, { -9, 7 }, { -9, 8 }, { -9, 9 }, { -10, 7 }, { -10, 8 }, { -10, 9 }, -- Bottom Left }, }, - entities = { --- @setting entities Settings relating to adding entities to spawn - enabled = true, --- @setting enabled Whether entities will be added to spawn - protected = true, --- @setting protected Whether entities will be protected from player interaction - operable = true, --- @setting operable Whether entities can be opened by players, must be true if chests are used - offset = { x = 0, y = -2 }, --- @setting offset The position offset to apply to entities - locations = { --- @setting locations The location and names of entities {name,x,y} + entities = { -- Settings relating to adding entities to spawn + enabled = true, -- Whether entities will be added to spawn + protected = true, -- Whether entities will be protected from player interaction + operable = true, -- Whether entities can be opened by players, must be true if chests are used + offset = { x = 0, y = -2 }, -- The position offset to apply to entities + locations = { -- The location and names of entities {name,x,y} { "stone-wall", -10, -5 }, { "stone-wall", -10, -4 }, { "stone-wall", -10, -3 }, { "stone-wall", -10, -2 }, { "stone-wall", -10, -1 }, { "stone-wall", -10, 0 }, { "stone-wall", -10, 3 }, { "stone-wall", -10, 4 }, { "stone-wall", -10, 5 }, { "stone-wall", -10, 6 }, { "stone-wall", -10, 7 }, { "stone-wall", -10, 8 }, { "small-lamp", -8, -4 }, { "small-lamp", -8, -1 }, { "iron-chest", -8, 0 }, { "iron-chest", -8, 3 }, { "small-lamp", -8, 4 }, { "small-lamp", -8, 7 }, { "stone-wall", -7, -8 }, { "small-electric-pole", -7, -2 }, { "iron-chest", -7, 0 }, { "iron-chest", -7, 3 }, { "small-electric-pole", -7, 5 }, { "stone-wall", -7, 11 }, { "stone-wall", -6, -8 }, { "small-lamp", -6, -6 }, @@ -67,10 +67,10 @@ return Feature.register("spawn_area", { }, }, pattern = { - enabled = true, --- @setting enabled Whether pattern tiles will be added to spawn - pattern_tile = "stone-path", --- @setting pattern_tile The tile to be used for the pattern - offset = { x = 0, y = -2 }, --- @setting offset The position offset to apply to pattern tiles - locations = { --- @setting locations The location of the pattern tiles {x,y} + enabled = true, -- Whether pattern tiles will be added to spawn + pattern_tile = "stone-path", -- The tile to be used for the pattern + offset = { x = 0, y = -2 }, -- The position offset to apply to pattern tiles + locations = { -- The location of the pattern tiles {x,y} { -49, -3 }, { -49, -2 }, { -49, 1 }, { -49, 2 }, { -49, 5 }, { -49, 6 }, { -48, -4 }, { -48, -3 }, { -48, -2 }, { -48, 1 }, { -48, 2 }, { -48, 5 }, { -48, 6 }, { -48, 7 }, { -47, -7 }, { -47, -6 }, { -47, -5 }, { -47, -4 }, { -47, -3 }, { -47, -2 }, { -47, 5 }, { -47, 6 }, { -47, 7 }, { -47, 8 }, { -47, 9 }, { -47, 10 }, { -46, -8 }, { -46, -7 }, { -46, -6 }, { -46, -5 }, { -46, -4 }, { -46, -3 }, { -46, -2 }, { -46, -1 }, { -46, 4 }, { -46, 5 }, { -46, 6 }, { -46, 7 }, { -46, 8 }, { -46, 9 }, { -46, 10 }, { -46, 11 }, { -45, -17 }, { -45, -16 }, { -45, -15 }, { -45, -14 }, { -45, -13 }, { -45, -12 }, { -45, -9 }, { -45, -8 }, { -45, -7 }, { -45, -2 }, { -45, -1 }, { -45, 0 }, { -45, 1 }, { -45, 2 }, { -45, 3 }, { -45, 4 }, { -45, 5 }, { -45, 10 }, { -45, 11 }, { -45, 12 }, { -45, 15 }, { -45, 16 }, { -45, 17 }, { -45, 18 }, { -45, 19 }, { -45, 20 }, { -44, -18 }, { -44, -17 }, { -44, -16 }, { -44, -15 }, { -44, -14 }, { -44, -13 }, { -44, -12 }, { -44, -9 }, { -44, -8 }, { -44, -1 }, { -44, 0 }, { -44, 1 }, { -44, 2 }, { -44, 3 }, { -44, 4 }, { -44, 11 }, { -44, 12 }, { -44, 15 }, { -44, 16 }, { -44, 17 }, { -44, 18 }, { -44, 19 }, diff --git a/exp_scenario/module/control.lua b/exp_scenario/module/control.lua index 24197c1a..a7702f6d 100644 --- a/exp_scenario/module/control.lua +++ b/exp_scenario/module/control.lua @@ -43,9 +43,7 @@ require("modules/exp_scenario/commands/waterfill") add(require("modules/exp_scenario/control/afk_kick")) add(require("modules/exp_scenario/control/bonus")) add(require("modules/exp_scenario/control/chat_auto_reply")) -add(require("modules/exp_scenario/control/chat_popup")) add(require("modules/exp_scenario/control/custom_start")) -add(require("modules/exp_scenario/control/damage_popups")) add(require("modules/exp_scenario/control/death_markers")) add(require("modules/exp_scenario/control/deconstruction_log")) add(require("modules/exp_scenario/control/degrading_tiles")) @@ -59,6 +57,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/popups")) add(require("modules/exp_scenario/control/protection")) add(require("modules/exp_scenario/control/protection_jail")) add(require("modules/exp_scenario/control/report_jail")) diff --git a/exp_scenario/module/control/afk_kick.lua b/exp_scenario/module/control/afk_kick.lua index c257c420..2f213e71 100644 --- a/exp_scenario/module/control/afk_kick.lua +++ b/exp_scenario/module/control/afk_kick.lua @@ -6,15 +6,14 @@ local Async = require("modules/exp_util/async") local Feature = require("modules/exp_scenario/features") local Roles = require("modules/exp_roles") local Storage = require("modules/exp_util/storage") -local feature = Feature.register("afk_kick", { - admin_as_active = true, --- @setting admin_as_active When true admins will be treated as active regardless of afk time - trust_as_active = true, --- @setting trust_as_active When true trusted players (by playtime) will be treated as active regardless of afk time - active_role_id = Feature.optional("number"), --- @setting active_role_id Players with this role or higher are treated as active regardless of afk time - afk_minutes = 10, --- @setting afk_minutes The time in minutes that must pass for a player to be considered afk - kick_minutes = 30, --- @setting kick_minutes The time in minutes that must pass without any active players for all players to be kicked - trust_minutes = 600, --- @setting trust_minutes The time in minutes that a player must be online for to count as trusted +local feature, config = Feature.register("afk_kick", { + admin_as_active = true, -- When true admins will be treated as active regardless of afk time + trust_as_active = true, -- When true trusted players (by playtime) will be treated as active regardless of afk time + active_role_id = Feature.optional("number"), -- Players with this role or higher are treated as active regardless of afk time + afk_minutes = 10, -- The time in minutes that must pass for a player to be considered afk + kick_minutes = 30, -- The time in minutes that must pass without any active players for all players to be kicked + trust_minutes = 600, -- The time in minutes that a player must be online for to count as trusted }) -local config = feature.config local ticks_per_minute = 3600 diff --git a/exp_scenario/module/control/chat_auto_reply.lua b/exp_scenario/module/control/chat_auto_reply.lua index 20f28571..1e1b6348 100644 --- a/exp_scenario/module/control/chat_auto_reply.lua +++ b/exp_scenario/module/control/chat_auto_reply.lua @@ -21,7 +21,6 @@ local function on_console_chat(event) -- Check if the player can chat commands local commands_allowed = true - if config.command_admin_only and not player.admin then commands_allowed = false end if not Roles.player_has_permission(player, "exp_scenario.chat.commands") then commands_allowed = false end -- Check if a key word appears in the message diff --git a/exp_scenario/module/control/chat_popup.lua b/exp_scenario/module/control/chat_popup.lua deleted file mode 100644 index ec01287b..00000000 --- a/exp_scenario/module/control/chat_popup.lua +++ /dev/null @@ -1,50 +0,0 @@ ---[[-- Control - Chat Popup -Creates flying text entities when a player sends a message in chat -]] - -local FlyingText = require("modules/exp_util/flying_text") -local Feature = require("modules/exp_scenario/features") -local feature = require("modules/exp_scenario/config/popups") -local config = feature.config - -local lower = string.lower -local find = string.find - ---- Create a chat bubble when a player types a message ---- @param event EventData.on_console_chat -local function on_console_chat(event) - if not event.player_index then return end - local player = assert(game.get_player(event.player_index)) - local name = player.name - - -- Sends the message as text above them - if config.show_player_messages then - FlyingText.create_as_player{ - target_player = player, - text = { "exp_chat-popup.flying-text-message", name, event.message }, - } - end - - if not config.show_player_mentions then return end - - -- Loops over online players to see if they name is included - local search_string = lower(event.message) - for _, mentioned_player in ipairs(game.connected_players) do - if mentioned_player.index ~= player.index then - if find(search_string, lower(mentioned_player.name), 1, true) then - FlyingText.create_as_player{ - target_player = mentioned_player, - text = { "exp_chat-popup.flying-text-ping", name }, - } - end - end - end -end - -local e = defines.events - -return feature:guard{ - events = { - [e.on_console_chat] = on_console_chat, - }, -} diff --git a/exp_scenario/module/control/damage_popups.lua b/exp_scenario/module/control/damage_popups.lua deleted file mode 100644 index deb40821..00000000 --- a/exp_scenario/module/control/damage_popups.lua +++ /dev/null @@ -1,53 +0,0 @@ ---[[-- Control - Damage PopUps -Displays the amount of dmg that is done by players to entities; -also shows player health when a player is attacked -]] - -local FlyingText = require("modules/exp_util/flying_text") -local Feature = require("modules/exp_scenario/features") -local feature = require("modules/exp_scenario/config/popups") -local config = feature.config - -local random = math.random -local floor = math.floor -local max = math.max - ---- Called when entity entity is damaged including the player character ---- @param event EventData.on_entity_damaged -local function on_entity_damaged(event) - local message - local cause = event.cause - local entity = event.entity - - -- Check which message to display - if config.show_player_health and entity.name == "character" then - message = { "exp_damage-popup.flying-text-health", floor(entity.health) } - elseif config.show_player_damage and entity.name ~= "character" and cause and cause.name == "character" then - message = { "exp_damage-popup.flying-text-damage", floor(event.original_damage_amount) } - end - - -- Outputs the message as floating text - if message then - local entity_position = entity.position - local entity_radius = max(1, entity.get_radius()) - local offset = (random() - 0.5) * entity_radius * config.damage_location_variance - local position = { x = entity_position.x + offset, y = entity_position.y - entity_radius } - - local health_percentage = assert(entity.get_health_ratio()) - local color = { r = 1 - health_percentage, g = health_percentage, b = 0 } - - FlyingText.create{ - text = message, - position = position, - color = color, - } - end -end - -local e = defines.events - -return feature:guard{ - events = { - [e.on_entity_damaged] = on_entity_damaged, - }, -} diff --git a/exp_scenario/module/control/death_markers.lua b/exp_scenario/module/control/death_markers.lua index 41507326..e293a080 100644 --- a/exp_scenario/module/control/death_markers.lua +++ b/exp_scenario/module/control/death_markers.lua @@ -6,16 +6,15 @@ local ExpUtil = require("modules/exp_util") local Feature = require("modules/exp_scenario/features") local Storage = require("modules/exp_util/storage") -local feature = Feature.register("death_markers", { - collect_corpses = true, --- @setting collect_corpses enables items being returned to the spawn point in chests upon corpse expiring - show_map_markers = true, --- @setting show_map_markers shows markers on the map where bodies are - clean_map_markers = false, --- @setting clean_map_markers removes the map marker once the body is gone - include_time_of_death = true, --- @setting include_time_of_death weather to include the time of death on the map marker - map_icon = nil, --- @setting map_icon the icon that the map marker shows; nil means no icon; format as a SingleID - show_light_at_corpse = true, --- @setting show_light_at_corpse if a light should be rendered at the corpse - show_line_to_corpse = true, --- @setting show_line_to_corpse if a line should be rendered from you to your corpse +local feature, config = Feature.register("death_markers", { + collect_corpses = true, -- enables items being returned to the spawn point in chests upon corpse expiring + show_map_markers = true, -- shows markers on the map where bodies are + clean_map_markers = false, -- removes the map marker once the body is gone + include_time_of_death = true, -- weather to include the time of death on the map marker + map_icon = nil, -- the icon that the map marker shows; nil means no icon; format as a SingleID + show_light_at_corpse = true, -- if a light should be rendered at the corpse + show_line_to_corpse = true, -- if a line should be rendered from you to your corpse }) -local config = feature.config local map_tag_time_format = ExpUtil.format_time_factory{ format = "short", hours = true, minutes = true } diff --git a/exp_scenario/module/control/discord_alerts.lua b/exp_scenario/module/control/discord_alerts.lua index 5690024e..064af60a 100644 --- a/exp_scenario/module/control/discord_alerts.lua +++ b/exp_scenario/module/control/discord_alerts.lua @@ -8,19 +8,18 @@ local Feature = require("modules/exp_scenario/features") local EntityProtection = require("modules/exp_scenario/control/protection") local Jail = require("modules/exp_scenario/control/jail") -local feature = Feature.register("discord_alerts", { - show_playtime = true, --- @setting show_playtime Add the playtime of players to alerts - entity_protection = true, --- @setting entity_protection Alert when a player repeatedly removes protected entities - player_bans = true, --- @setting player_bans Alert when a player is banned or unbanned - player_mutes = true, --- @setting player_mutes Alert when a player is muted or unmuted - player_kicks = true, --- @setting player_kicks Alert when a player is kicked - player_promotes = false, --- @setting player_promotes Alert when a player is promoted or demoted - player_jail = true, --- @setting player_jail Alert when a player is jailed or unjailed - logged_commands = Feature.set{ --- @setting logged_commands Alert when a player uses one of these commands +local feature, config = Feature.register("discord_alerts", { + show_playtime = true, -- Add the playtime of players to alerts + entity_protection = true, -- Alert when a player repeatedly removes protected entities + player_bans = true, -- Alert when a player is banned or unbanned + player_mutes = true, -- Alert when a player is muted or unmuted + player_kicks = true, -- Alert when a player is kicked + player_promotes = false, -- Alert when a player is promoted or demoted + player_jail = true, -- Alert when a player is jailed or unjailed + logged_commands = Feature.set{ -- Alert when a player uses one of these commands "config", "purge", "c", "command", "silent-command", "measured-command", "banlist", "permissions", "editor", "cheat", }, }) -local config = feature.config --- Wrap an event handler so it only runs while the alert is enabled --- @param setting string diff --git a/exp_scenario/module/control/mine_depletion.lua b/exp_scenario/module/control/mine_depletion.lua index 188e6b39..77efb618 100644 --- a/exp_scenario/module/control/mine_depletion.lua +++ b/exp_scenario/module/control/mine_depletion.lua @@ -5,12 +5,11 @@ Marks mining drills for deconstruction when resources deplete local Async = require("modules/exp_util/async") local Feature = require("modules/exp_scenario/features") -local feature = Feature.register("mine_depletion", { - fluid = true, --- @setting fluid When true, checks for for fluid pipes when removing miners - chest = true, --- @setting chest When true, checks for for chest when removing miners - beacon = true, --- @setting beacon When true, checks for for beacon when removing miners +local feature, config = Feature.register("mine_depletion", { + fluid = true, -- When true, checks for for fluid pipes when removing miners + chest = true, -- When true, checks for for chest when removing miners + beacon = true, -- When true, checks for for beacon when removing miners }) -local config = feature.config local floor = math.floor diff --git a/exp_scenario/module/control/nuke_protection.lua b/exp_scenario/module/control/nuke_protection.lua index d661b278..e8638ce4 100644 --- a/exp_scenario/module/control/nuke_protection.lua +++ b/exp_scenario/module/control/nuke_protection.lua @@ -6,11 +6,9 @@ local ExpUtil = require("modules/exp_util") local Feature = require("modules/exp_scenario/features") local Roles = require("modules/exp_roles") -local feature = Feature.register("nuke_protection", { - ignore_admins = true, --- @setting ignore_admins Admins can hold banned items - banned_items = Feature.set{ "atomic-bomb" }, --- @setting banned_items Items which are removed from the inventory of players without the bypass permission +local feature, config = Feature.register("nuke_protection", { + banned_items = Feature.set{ "atomic-bomb" }, -- Items which are removed from the inventory of players without the bypass permission }) -local config = feature.config --- The inventories which are checked and the events which trigger the check local inventories = { @@ -26,7 +24,6 @@ local inventories = { local function check_items(player, type) -- If the player has perms to be ignored, then they should be if Roles.player_has_permission(player, "exp_scenario.bypass.nuke_protection") then return end - if config.ignore_admins and player.admin then return end local banned_items = config.banned_items local items = {} --- @type LuaItemStack[] diff --git a/exp_scenario/module/control/popups.lua b/exp_scenario/module/control/popups.lua new file mode 100644 index 00000000..f25b5fba --- /dev/null +++ b/exp_scenario/module/control/popups.lua @@ -0,0 +1,92 @@ +--[[-- Control - Popups +Creates flying text for chat messages and for damage dealt to and by players +]] + +local FlyingText = require("modules/exp_util/flying_text") +local Feature = require("modules/exp_scenario/features") + +local feature, config = Feature.register("popups", { + show_player_messages = true, -- weather a message in chat will make a popup above them + show_player_mentions = true, -- weather a mentioned player will have a popup when mentioned in chat + show_player_damage = true, -- weather to show damage done by players + show_player_health = true, -- weather to show player health when attacked + damage_location_variance = 0.8, -- how close to the eade of an entity the popups will appear +}) + +local lower = string.lower +local find = string.find +local random = math.random +local floor = math.floor +local max = math.max + +--- Create a chat bubble when a player types a message +--- @param event EventData.on_console_chat +local function on_console_chat(event) + if not event.player_index then return end + local player = assert(game.get_player(event.player_index)) + local name = player.name + + -- Sends the message as text above them + if config.show_player_messages then + FlyingText.create_as_player{ + target_player = player, + text = { "exp_chat-popup.flying-text-message", name, event.message }, + } + end + + if not config.show_player_mentions then return end + + -- Loops over online players to see if they name is included + local search_string = lower(event.message) + for _, mentioned_player in ipairs(game.connected_players) do + if mentioned_player.index ~= player.index then + if find(search_string, lower(mentioned_player.name), 1, true) then + FlyingText.create_as_player{ + target_player = mentioned_player, + text = { "exp_chat-popup.flying-text-ping", name }, + } + end + end + end +end + +--- Called when entity entity is damaged including the player character +--- @param event EventData.on_entity_damaged +local function on_entity_damaged(event) + local message + local cause = event.cause + local entity = event.entity + + -- Check which message to display + if config.show_player_health and entity.name == "character" then + message = { "exp_damage-popup.flying-text-health", floor(entity.health) } + elseif config.show_player_damage and entity.name ~= "character" and cause and cause.name == "character" then + message = { "exp_damage-popup.flying-text-damage", floor(event.original_damage_amount) } + end + + -- Outputs the message as floating text + if message then + local entity_position = entity.position + local entity_radius = max(1, entity.get_radius()) + local offset = (random() - 0.5) * entity_radius * config.damage_location_variance + local position = { x = entity_position.x + offset, y = entity_position.y - entity_radius } + + local health_percentage = assert(entity.get_health_ratio()) + local color = { r = 1 - health_percentage, g = health_percentage, b = 0 } + + FlyingText.create{ + text = message, + position = position, + color = color, + } + end +end + +local e = defines.events + +return feature:guard{ + events = { + [e.on_console_chat] = on_console_chat, + [e.on_entity_damaged] = on_entity_damaged, + }, +} diff --git a/exp_scenario/module/control/protection.lua b/exp_scenario/module/control/protection.lua index 42769268..27b58c84 100644 --- a/exp_scenario/module/control/protection.lua +++ b/exp_scenario/module/control/protection.lua @@ -6,20 +6,18 @@ 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 = Feature.set{}, --- @setting always_protected_names Names of entities which are always protected - always_protected_types = Feature.set{ --- @setting always_protected_types Types of entities which are always protected +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{}, --- @setting always_trigger_repeat_names Names of entities which always trigger repeated removal protection - always_trigger_repeat_types = Feature.set{ --- @setting always_trigger_repeat_types Types of entities which always trigger repeated removal protection + 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 config = feature.config local format_string = string.format local floor = math.floor @@ -32,6 +30,8 @@ local Protection = { --- 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 @@ -45,29 +45,6 @@ local Protection = { --- @field last uint Tick of the last protected removal --- @field count number Protected removals since the last repeat violation ---- Get the keys of a set as a list, for the filters of find_entities_filtered ---- @param set table ---- @return string[] -local function set_to_list(set) - local list = {} - for item in pairs(set) do - list[#list + 1] = item - end - return list -end - ---- Names of entities which are always protected ---- @return string[] -function Protection.get_protected_entity_names() - return set_to_list(config.always_protected_names) -end - ---- Types of entities which are always protected ---- @return string[] -function Protection.get_protected_entity_types() - return set_to_list(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 @@ -116,10 +93,17 @@ function Protection.remove_entity(entity) 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 function Protection.get_entities(surface) - return protected_entities[surface.index] or {} + 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 @@ -129,7 +113,13 @@ 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 - return entities[Protection.get_entity_key(entity)] == entity + 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 @@ -178,12 +168,11 @@ function Protection.is_position_protected(surface, position) return false end ---- Players are never checked against their own entities, and can be excluded by permission or admin status +--- 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 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 @@ -216,10 +205,8 @@ end 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) + 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) @@ -253,4 +240,4 @@ 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 +return feature:guard(Protection) diff --git a/exp_scenario/module/control/station_auto_name.lua b/exp_scenario/module/control/station_auto_name.lua index 142f4a31..061cc7ab 100644 --- a/exp_scenario/module/control/station_auto_name.lua +++ b/exp_scenario/module/control/station_auto_name.lua @@ -4,12 +4,11 @@ Automatically name stations when they are placed based on closest resource and d local Feature = require("modules/exp_scenario/features") -local feature = Feature.register("station_auto_name", { - --- @setting station_name the name given to new stations, placeholders: +local feature, config = Feature.register("station_auto_name", { + -- the name given to new stations, placeholders: -- __icon__ __item_name__ __backer_name__ __direction__ __x__ __y__ station_name = "[L] __icon__", }) -local config = feature.config local get_direction do local directions = { diff --git a/exp_scenario/module/features.lua b/exp_scenario/module/features.lua index dd5748a6..32387e88 100644 --- a/exp_scenario/module/features.lua +++ b/exp_scenario/module/features.lua @@ -12,13 +12,12 @@ features.ts. --- Registering a feature, small configs live at the top of the file which uses them: local Feature = require("modules/exp_scenario/features") -local feature = Feature.register("my_feature", { +local feature, config = Feature.register("my_feature", { show_message = true, role_id = Feature.optional("number"), item_names = Feature.set{ "iron-plate" }, surface_names = Feature.list{ "nauvis" }, }) -local config = feature.config --- Handlers do nothing while the feature is disabled: return feature:guard{ @@ -48,11 +47,7 @@ local Storage = require("modules/exp_util/storage") --- @field default any --- @field type string "boolean", "number", "string", "list" or "set" of strings, or a type which can not be overridden ---- @class ExpScenario.Feature ---- @field name string ---- @field config table Changed in place when the config is updated ---- @field fields table The values in the config by path ---- @field overrides table The overrides from the controller by path, restored from storage on load +--- @class ExpScenario.Features local Feature = { _registered = {}, --- @type table events = { @@ -62,6 +57,14 @@ local Feature = { }, } +--- The methods of every feature, kept apart from the lib so they are not called on it by mistake +--- @class ExpScenario.Feature +--- @field name string +--- @field config table Changed in place when the config is updated +--- @field fields table The values in the config by path +--- @field overrides table The overrides from the controller by path, restored from storage on load +Feature._prototype = {} + --- @class EventData.ExpScenario.on_config_updated : EventData --- @field feature_name string --- @field path string The path of the value within the config, "enabled" when the feature is enabled or disabled @@ -69,7 +72,7 @@ local Feature = { --- @field new_value any local Feature_mt = { - __index = Feature, + __index = Feature._prototype, } --- The overrides of each feature by name, only these are stored because the rest is rebuilt when the file loads @@ -173,7 +176,8 @@ end --- Register a feature and its config, features are enabled unless the config says otherwise --- @param name string --- @param config table? ---- @return ExpScenario.Feature +--- @return ExpScenario.Feature feature +--- @return table config The same table as feature.config function Feature.register(name, config) assert(Feature._registered[name] == nil, "Feature already registered: " .. name) config = config or {} @@ -192,7 +196,7 @@ function Feature.register(name, config) }, Feature_mt) --[[@as ExpScenario.Feature]] Feature._registered[name] = feature - return feature + return feature, config end --- Get a registered feature by name @@ -213,26 +217,36 @@ local function is_valid(field, value) return type(value) == field.type and (field.type == "boolean" or field.type == "number" or field.type == "string") end +--- Set a value to its override, or its default when there is no valid override +--- @package +--- @param path string +--- @param field ExpScenario.FeatureField +--- @return any # The value which was set +function Feature._prototype:_apply_field(path, field) + local value = self.overrides[path] + if value == nil then + value = field.default + elseif not is_valid(field, value) then + log("[WARNING] Invalid override for " .. self.name .. "." .. path .. ", using the default") + value = field.default + elseif field.type == "set" then + value = list_to_set(value) + end + field.parent[field.key] = value + return value +end + --- Set every value to its override, or its default when there is no valid override --- @package -function Feature:_apply() +function Feature._prototype:_apply() for path, field in pairs(self.fields) do - local value = self.overrides[path] - if value == nil then - value = field.default - elseif not is_valid(field, value) then - log("[WARNING] Invalid override for " .. self.name .. "." .. path .. ", using the default") - value = field.default - elseif field.type == "set" then - value = list_to_set(value) - end - field.parent[field.key] = value + self:_apply_field(path, field) end end --- Check if the feature is enabled --- @return boolean -function Feature:is_enabled() +function Feature._prototype:is_enabled() return self.config.enabled end @@ -240,7 +254,7 @@ end --- @generic T : table --- @param lib T --- @return T -function Feature:guard(lib) +function Feature._prototype:guard(lib) local config = self.config for _, handlers in pairs{ lib.events or {}, lib.on_nth_tick or {} } do for key, handler in pairs(handlers) do @@ -258,7 +272,8 @@ end --- Replace the overrides of the feature, then raise on_config_updated for each value which changed --- @param enabled boolean --- @param values table -function Feature:update_config(enabled, values) +function Feature._prototype:update_config(enabled, values) + local old_overrides = self.overrides local overrides = { enabled = enabled } for path, value in pairs(values) do if self.fields[path] == nil then @@ -267,27 +282,34 @@ function Feature:update_config(enabled, values) overrides[path] = value end end - - local old_values = {} - for path, field in pairs(self.fields) do - old_values[path] = field.parent[field.key] - end - self.overrides = overrides stored_overrides[self.name] = overrides - self:_apply() - for path, field in pairs(self.fields) do - local new_value = field.parent[field.key] - if not values_equal(old_values[path], new_value) then - script.raise_event(Feature.events.on_config_updated, { - feature_name = self.name, - path = path, - old_value = old_values[path], - new_value = new_value, - }) + -- Only a value whose override was added, removed, or replaced can change + local paths = {} + for path in pairs(overrides) do + paths[path] = true + end + for path in pairs(old_overrides) do + -- A save can hold overrides for settings which no longer exist + if self.fields[path] then + paths[path] = true end end + + local updates = {} + for path in pairs(paths) do + local field = self.fields[path] + local old_value = field.parent[field.key] + local new_value = self:_apply_field(path, field) + if not values_equal(old_value, new_value) then + updates[#updates + 1] = { feature_name = self.name, path = path, old_value = old_value, new_value = new_value } + end + end + + for _, update in ipairs(updates) do + script.raise_event(Feature.events.on_config_updated, update) + end end --- @class ExpScenario.FeatureUpdate diff --git a/exp_scenario/test/module/env.lua b/exp_scenario/test/module/env.lua index 0121e16f..813868f3 100644 --- a/exp_scenario/test/module/env.lua +++ b/exp_scenario/test/module/env.lua @@ -13,7 +13,7 @@ local plugin_root = assert(source:match("^@(.*)/test/module/env%.lua$")) local Framework = assert(loadfile(shared_root .. "/framework.lua"))() --- @type Framework --- @class ExpScenario.TestEnv : Stubs ---- @field Feature ExpScenario.Feature A fresh copy of the features module +--- @field Feature ExpScenario.Features A fresh copy of the features module --- @field storage table The table registered with storage, as on_init would store it --- @field on_load fun(tbl: table) Act out on_load with the given storage table --- @field load_config fun(name: string): ExpScenario.Feature Load module/config/.lua diff --git a/exp_scenario/test/module/features.lua b/exp_scenario/test/module/features.lua index fb85bdaf..93751e1e 100644 --- a/exp_scenario/test/module/features.lua +++ b/exp_scenario/test/module/features.lua @@ -135,11 +135,32 @@ Suite.test("optional() declares a value without a default", function(env) end) +Suite.test("register() returns the config as well", function(env) + local feature, config = env.Feature.register("test", { flag = true }) + Suite.eq(config == feature.config, true, "the same table") +end) + +Suite.test("update_config() raises when an override is removed", function(env) + local feature = env.Feature.register("test", { flag = true, count = 1 }) + feature:update_config(true, { flag = false }) + env.events = {} + feature:update_config(true, {}) + Suite.eq(updates(env), { { "test", "flag", false, true } }, "the value goes back to its default") +end) + +Suite.test("update_config() skips stored overrides for settings which no longer exist", function(env) + local feature = env.Feature.register("test", { flag = true }) + env.on_load{ test = { enabled = true, gone = 1 } } + feature:update_config(true, { flag = false }) + Suite.eq(updates(env), { { "test", "flag", true, false } }, "only the known value is compared") + Suite.eq(env.storage.test, { enabled = true, flag = false }, "and the stale key is not kept") +end) + Suite.test("config files register their feature", function(env) - local feature = env.load_config("popups") - feature:update_config(true, { show_player_damage = false }) - Suite.eq(feature.config.show_player_damage, false, "popups can be changed") - Suite.eq(feature.config.show_player_health, true, "other values keep their default") + local feature = env.load_config("spawn_area") + feature:update_config(true, { ["turrets.enabled"] = false }) + Suite.eq(feature.config.turrets.enabled, false, "spawn_area can be changed") + Suite.eq(feature.config.turrets.ammo_type, "uranium-rounds-magazine", "other values keep their default") end) return Suite.run()