From 2e53e4668d419c02accfcd7b920331e4652a7b4f Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:18:04 +0000 Subject: [PATCH 1/6] Move jail into exp_scenario With the role system finished the jail module has nothing legacy left in it, so it moves to exp_scenario/module/control/jail.lua and the copy in exp_legacy is removed. The module takes LuaPlayer only, requires a reason, returns false rather than nil when there is nothing to do, and exposes its event ids as Jail.on_player_jailed and Jail.on_player_unjailed in the same shape as exp_util's selection events. --- .../module/config/gui/player_list_actions.lua | 6 +- exp_legacy/module/modules/control/jail.lua | 119 ------------------ exp_scenario/module/commands/jail.lua | 2 +- .../module/control/discord_alerts.lua | 6 +- exp_scenario/module/control/jail.lua | 89 +++++++++++++ .../module/control/protection_jail.lua | 2 +- exp_scenario/module/control/report_jail.lua | 2 +- 7 files changed, 98 insertions(+), 128 deletions(-) delete mode 100644 exp_legacy/module/modules/control/jail.lua create mode 100644 exp_scenario/module/control/jail.lua diff --git a/exp_legacy/module/config/gui/player_list_actions.lua b/exp_legacy/module/config/gui/player_list_actions.lua index 146338aa..fe1b3c41 100644 --- a/exp_legacy/module/config/gui/player_list_actions.lua +++ b/exp_legacy/module/config/gui/player_list_actions.lua @@ -10,7 +10,7 @@ local Gui = require("modules/exp_gui") local Roles = require("modules/exp_roles") local Reports = require("modules.exp_legacy.modules.control.reports") --- @dep modules.control.reports local Warnings = require("modules.exp_legacy.modules.control.warnings") --- @dep modules.control.warnings -local Jail = require("modules.exp_legacy.modules.control.jail") --- @dep modules.control.jail +local Jail = require("modules/exp_scenario/control/jail") local Colors = require("modules/exp_util/include/color") local format_player_name = ExpUtil.format_player_name_locale @@ -119,7 +119,7 @@ end local jail_player = new_button("utility/multiplayer_waiting_icon", { "exp-gui_player-list.jail-player" }) :on_click(function(def, player, element) local selected_player, selected_player_color = get_action_player(player) - if Jail.is_jailed(selected_player.name) then + if Jail.is_jailed(selected_player) then player.print({ "exp-commands_jail.already-jailed", selected_player_color }, Colors.orange_red) else set_selected_action(player, "exp_scenario.command.jail") @@ -130,7 +130,7 @@ local function jail_player_callback(player, reason) local selected_player, selected_player_color = get_action_player(player) local by_player_name_color = format_player_name(player) game.print{ "exp-commands_jail.jailed", selected_player_color, by_player_name_color, reason } - Jail.jail_player(selected_player.name, player.name, reason) + Jail.jail_player(selected_player, player.name, reason) end --- Kicks the action player, requires a reason diff --git a/exp_legacy/module/modules/control/jail.lua b/exp_legacy/module/modules/control/jail.lua deleted file mode 100644 index 0d9b6cfe..00000000 --- a/exp_legacy/module/modules/control/jail.lua +++ /dev/null @@ -1,119 +0,0 @@ ---[[-- Control Module - Jail - - Adds a way to jail players. - @control Jail - @alias Jail - - @usage - -- import the module from the control modules - local Jail = require("modules.exp_legacy.modules.control.jail") --- @dep modules.control.jail - - -- This will give 'MrBiter' the jail role, which suppresses all of their other roles - -- the player name and reason are only so they can be included in the event for user feedback - Jail.jail_player('MrBiter', 'Cooldude2606', 'Likes biters too much') - - -- This will remove the jail role from 'MrBiter', restoring their other roles - -- again as above the player name is only used in the event for user feedback - Jail.unjail_player('MrBiter', 'Cooldude2606') -]] - -local Roles = require("modules/exp_roles") - -local valid_player = function(p) return type(p) == "userdata" and p or game.get_player(p) end - ---- The role given to jailed players, it has a higher priority than every other role -local function jail_role() - return assert(Roles.get_role_by_name("Jail"), "The Jail role does not exist") -end - -local Jail = { - events = { - --- When a player is assigned to jail - -- @event on_player_jailed - -- @tparam number player_index the index of the player who was jailed - -- @tparam string by_player_name the name of the player who jailed the other player - -- @tparam string reason the reason that the player was jailed - on_player_jailed = script.generate_event_name(), - --- When a player is unassigned from jail - -- @event on_player_unjailed - -- @tparam number player_index the index of the player who was unjailed - -- @tparam string by_player_name the name of the player who unjailed the other player - on_player_unjailed = script.generate_event_name(), - }, -} - ---- Used to emit the jail related events --- @tparam number event the name of the event that will be emited --- @tparam LuaPlayer player the player who is being acted on --- @tparam string by_player_name the player who is doing the action --- @tparam string reason the reason for the action (jail) -local function event_emit(event, player, by_player_name, reason) - script.raise_event(event, { - name = event, - tick = game.tick, - player_index = player.index, - by_player_name = by_player_name, - reason = reason, - }) -end - ---- Jail. --- Functions related to jail --- @section jail-functions - ---- Checks if the player is currently in jail --- @tparam LuaPlayer player the player to check if they are in jail --- @treturn boolean whether the player is currently in jail -function Jail.is_jailed(player) - local valid = valid_player(player) - return valid ~= nil and jail_role():has_player(valid) -end - ---- Moves a player to jail, which suppresses all of their other roles --- @tparam LuaPlayer player the player who will be jailed --- @tparam string by_player_name the name of the player who is doing the jailing --- @tparam[opt='Non given.'] string reason the reason that the player is being jailed --- @treturn boolean wheather the user was jailed successfully -function Jail.jail_player(player, by_player_name, reason) - player = valid_player(player) - if not player then return end - if not by_player_name then return end - - reason = reason or "Non given." - - local role = jail_role() - if role:has_player(player) then return end - - player.walking_state = { walking = false, direction = player.walking_state.direction } - player.riding_state = { acceleration = defines.riding.acceleration.nothing, direction = player.riding_state.direction } - player.mining_state = { mining = false } - player.shooting_state = { state = defines.shooting.not_shooting, position = player.shooting_state.position } - player.picking_state = false - player.repair_state = { repairing = false, position = player.repair_state.position } - - role:assign(player, { by_player_name = by_player_name, silent = true }) - - event_emit(Jail.events.on_player_jailed, player, by_player_name, reason) - - return true -end - ---- Moves a player out of jail, which restores all of their other roles --- @tparam LuaPlayer player the player that will be unjailed --- @tparam string by_player_name the name of the player that is doing the unjail --- @treturn boolean whether the player was unjailed successfully -function Jail.unjail_player(player, by_player_name) - player = valid_player(player) - if not player then return end - if not by_player_name then return end - - local role = jail_role() - if not role:has_player(player) then return end - - role:unassign(player, { by_player_name = by_player_name, silent = true }) - - event_emit(Jail.events.on_player_unjailed, player, by_player_name) - - return true -end - -return Jail diff --git a/exp_scenario/module/commands/jail.lua b/exp_scenario/module/commands/jail.lua index 95c78aee..70b6b7bb 100644 --- a/exp_scenario/module/commands/jail.lua +++ b/exp_scenario/module/commands/jail.lua @@ -5,7 +5,7 @@ Adds a commands that allow admins to jail and unjail local Commands = require("modules/exp_commands") local format_player_name = Commands.format_player_name_locale -local Jail = require("modules.exp_legacy.modules.control.jail") --- @dep modules.control.jail +local Jail = require("modules/exp_scenario/control/jail") --- Puts a player into jail and removes all other roles. Commands.new("jail", { "exp-commands_jail.description" }) diff --git a/exp_scenario/module/control/discord_alerts.lua b/exp_scenario/module/control/discord_alerts.lua index c4675140..a2b3e5e1 100644 --- a/exp_scenario/module/control/discord_alerts.lua +++ b/exp_scenario/module/control/discord_alerts.lua @@ -167,8 +167,8 @@ end --- When a player is jailed or unjailed if config.player_jail then - local Jail = require("modules.exp_legacy.modules.control.jail") - events[Jail.events.on_player_jailed] = function(event) + local Jail = require("modules/exp_scenario/control/jail") + events[Jail.on_player_jailed] = function(event) local player_name, by_player_name = get_player_name(event) emit_event{ title = "Jail", @@ -181,7 +181,7 @@ if config.player_jail then }, } end - events[Jail.events.on_player_unjailed] = function(event) + events[Jail.on_player_unjailed] = function(event) local player_name, by_player_name = get_player_name(event) emit_event{ title = "Unjail", diff --git a/exp_scenario/module/control/jail.lua b/exp_scenario/module/control/jail.lua new file mode 100644 index 00000000..bc4730fa --- /dev/null +++ b/exp_scenario/module/control/jail.lua @@ -0,0 +1,89 @@ +--[[-- Control - Jail +Adds a way to jail players, the jail role suppresses all of their other roles +]] + +local Roles = require("modules/exp_roles") + +--- @class ExpScenario_Jail +local Jail = { + --- Raised when a player is put into jail + --- @type EventData.ExpScenario_Jail.on_player_jailed + on_player_jailed = script.generate_event_name(), + --- Raised when a player is taken out of jail + --- @type EventData.ExpScenario_Jail.on_player_unjailed + on_player_unjailed = script.generate_event_name(), +} + +--- @class EventData.ExpScenario_Jail.on_player_jailed : EventData +--- @field player_index uint +--- @field by_player_name string +--- @field reason string + +--- @class EventData.ExpScenario_Jail.on_player_unjailed : EventData +--- @field player_index uint +--- @field by_player_name string + +--- The role given to jailed players, it has a higher priority than every other role +--- @return ExpRoles.Role +local function jail_role() + return (assert(Roles.get_role_by_name("Jail"), "The Jail role does not exist")) +end + +--- Check if a player is in jail +--- @param player LuaPlayer +--- @return boolean +function Jail.is_jailed(player) + return jail_role():has_player(player) +end + +--- Put a player into jail, which suppresses all of their other roles +--- @param player LuaPlayer +--- @param by_player_name string +--- @param reason string +--- @return boolean # False when the player was already in jail +function Jail.jail_player(player, by_player_name, reason) + local role = jail_role() + if role:has_player(player) then return false end + + -- Stop whatever the player is doing, the jail permission group stops them from starting again + player.walking_state = { walking = false, direction = player.walking_state.direction } + player.riding_state = { acceleration = defines.riding.acceleration.nothing, direction = player.riding_state.direction } + player.mining_state = { mining = false } + player.shooting_state = { state = defines.shooting.not_shooting, position = player.shooting_state.position } + player.picking_state = false + player.repair_state = { repairing = false, position = player.repair_state.position } + + role:assign(player, { by_player_name = by_player_name, silent = true }) + + script.raise_event(Jail.on_player_jailed, { + name = Jail.on_player_jailed, + tick = game.tick, + player_index = player.index, + by_player_name = by_player_name, + reason = reason, + }) + + return true +end + +--- Take a player out of jail, which restores all of their other roles +--- @param player LuaPlayer +--- @param by_player_name string +--- @return boolean # False when the player was not in jail +function Jail.unjail_player(player, by_player_name) + local role = jail_role() + if not role:has_player(player) then return false end + + role:unassign(player, { by_player_name = by_player_name, silent = true }) + + script.raise_event(Jail.on_player_unjailed, { + name = Jail.on_player_unjailed, + tick = game.tick, + player_index = player.index, + by_player_name = by_player_name, + }) + + return true +end + +return Jail diff --git a/exp_scenario/module/control/protection_jail.lua b/exp_scenario/module/control/protection_jail.lua index c39e649b..987d2c6e 100644 --- a/exp_scenario/module/control/protection_jail.lua +++ b/exp_scenario/module/control/protection_jail.lua @@ -4,7 +4,7 @@ When a player triggers protection multiple times they are automatically jailed local ExpUtil = require("modules/exp_util") local Storage = require("modules/exp_util/storage") -local Jail = require("modules.exp_legacy.modules.control.jail") +local Jail = require("modules/exp_scenario/control/jail") local Protection = require("modules.exp_legacy.modules.control.protection") local format_player_name = ExpUtil.format_player_name_locale diff --git a/exp_scenario/module/control/report_jail.lua b/exp_scenario/module/control/report_jail.lua index e4bbdd72..25ec24f9 100644 --- a/exp_scenario/module/control/report_jail.lua +++ b/exp_scenario/module/control/report_jail.lua @@ -3,7 +3,7 @@ When a player is reported, the player is automatically jailed if the combined pl ]] local ExpUtil = require("modules/exp_util") -local Jail = require("modules.exp_legacy.modules.control.jail") +local Jail = require("modules/exp_scenario/control/jail") local Reports = require("modules.exp_legacy.modules.control.reports") local max = math.max From 0c52532c05487b81151a95bd274631a640b7a4f0 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:19:45 +0000 Subject: [PATCH 2/6] 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. --- .../module/modules/control/protection.lua | 204 --------------- .../module/commands/protected_entities.lua | 24 +- exp_scenario/module/control.lua | 1 + .../module/control/discord_alerts.lua | 4 +- exp_scenario/module/control/protection.lua | 238 ++++++++++++++++++ .../module/control/protection_jail.lua | 4 +- 6 files changed, 247 insertions(+), 228 deletions(-) delete mode 100644 exp_legacy/module/modules/control/protection.lua create mode 100644 exp_scenario/module/control/protection.lua diff --git a/exp_legacy/module/modules/control/protection.lua b/exp_legacy/module/modules/control/protection.lua deleted file mode 100644 index b0c35a0b..00000000 --- a/exp_legacy/module/modules/control/protection.lua +++ /dev/null @@ -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 diff --git a/exp_scenario/module/commands/protected_entities.lua b/exp_scenario/module/commands/protected_entities.lua index 11c24fe8..34b66783 100644 --- a/exp_scenario/module/commands/protected_entities.lua +++ b/exp_scenario/module/commands/protected_entities.lua @@ -12,10 +12,9 @@ local Commands = require("modules/exp_commands") local format_player_name = Commands.format_player_name_locale local Roles = require("modules/exp_roles") -local EntityProtection = require("modules.exp_legacy.modules.control.protection") --- @dep modules.control.protection - -local format_string = string.format -local floor = math.floor +local EntityProtection = require("modules/exp_scenario/control/protection") +local get_entity_key = EntityProtection.get_entity_key +local get_area_key = EntityProtection.get_area_key local Selection = require("modules/exp_util/selection") local SelectEntities = Selection.connect("ExpCommand_ProtectEntity") @@ -28,21 +27,6 @@ Storage.register({ renders = tbl.renders 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 --- @param player LuaPlayer --- @param entity LuaEntity @@ -232,6 +216,6 @@ end return { events = { - [EntityProtection.events.on_repeat_violation] = on_repeat_violation, + [EntityProtection.on_repeat_violation] = on_repeat_violation, } } diff --git a/exp_scenario/module/control.lua b/exp_scenario/module/control.lua index 2e04e552..bf8741e3 100644 --- a/exp_scenario/module/control.lua +++ b/exp_scenario/module/control.lua @@ -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/nuke_protection")) 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/report_jail")) add(require("modules/exp_scenario/control/research")) diff --git a/exp_scenario/module/control/discord_alerts.lua b/exp_scenario/module/control/discord_alerts.lua index c4675140..4af0fd9d 100644 --- a/exp_scenario/module/control/discord_alerts.lua +++ b/exp_scenario/module/control/discord_alerts.lua @@ -81,8 +81,8 @@ end --- Repeated protected entity mining if config.entity_protection then - local EntityProtection = require("modules.exp_legacy.modules.control.protection") - events[EntityProtection.events.on_repeat_violation] = function(event) + local EntityProtection = require("modules/exp_scenario/control/protection") + events[EntityProtection.on_repeat_violation] = function(event) local player_name = get_player_name(event) emit_event{ title = "Entity Protection", diff --git a/exp_scenario/module/control/protection.lua b/exp_scenario/module/control/protection.lua new file mode 100644 index 00000000..c8907e0f --- /dev/null +++ b/exp_scenario/module/control/protection.lua @@ -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 +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> Keyed by surface index then entity key +local protected_areas = {} --- @type table> Keyed by surface index then area key +local repeats = {} --- @type table Keyed by player name + +Storage.register({ + protected_entities = protected_entities, + protected_areas = protected_areas, + repeats = repeats, +}, function(tbl) + protected_entities = tbl.protected_entities + protected_areas = tbl.protected_areas + repeats = tbl.repeats +end) + +--- Get the key an entity is stored under +--- @param entity LuaEntity +--- @return string +function Protection.get_entity_key(entity) + return format_string("%i,%i", floor(entity.position.x), floor(entity.position.y)) +end + +--- Get the key an area is stored under +--- @param area BoundingBox +--- @return string +function Protection.get_area_key(area) + return format_string("%i,%i", floor(area.left_top.x), floor(area.left_top.y)) +end + +--- Protect an entity +--- @param entity LuaEntity +function Protection.add_entity(entity) + local entities = protected_entities[entity.surface.index] + if not entities then + entities = {} + protected_entities[entity.surface.index] = entities + end + entities[Protection.get_entity_key(entity)] = entity +end + +--- Remove the protection from an entity +--- @param entity LuaEntity +function Protection.remove_entity(entity) + local entities = protected_entities[entity.surface.index] + if not entities then return end + entities[Protection.get_entity_key(entity)] = nil +end + +--- Get the protected entities on a surface, always protected entities are not included +--- @param surface LuaSurface +--- @return table +function Protection.get_entities(surface) + return protected_entities[surface.index] or {} +end + +--- Check if an entity is protected, either directly or by its name or type +--- @param entity LuaEntity +--- @return boolean +function Protection.is_entity_protected(entity) + if 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 +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 diff --git a/exp_scenario/module/control/protection_jail.lua b/exp_scenario/module/control/protection_jail.lua index c39e649b..f71469d4 100644 --- a/exp_scenario/module/control/protection_jail.lua +++ b/exp_scenario/module/control/protection_jail.lua @@ -5,7 +5,7 @@ When a player triggers protection multiple times they are automatically jailed local ExpUtil = require("modules/exp_util") local Storage = require("modules/exp_util/storage") 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 @@ -41,7 +41,7 @@ local e = defines.events return { 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, } } From 8014cb1e774bdfee3ad943d8c4f87fc0b1d06629 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:20:55 +0000 Subject: [PATCH 3/6] Move spectate into exp_scenario The spectate and follow module moves to exp_scenario/module/control/spectate.lua in the event_handler shape the other control modules use, and the copy in exp_legacy is removed. Follow state is a typed record rather than a positional table, the follow label caption is localised, and the escape handler marks the record to stop on the next tick instead of poking an invalid position into it. The LuaPlayer.close_map call is gone: it does not exist in Factorio 2.0, where the map is the remote controller and set_controller replaces it. --- .../module/modules/control/spectate.lua | 173 ------------------ exp_scenario/module/commands/spectate.lua | 2 +- exp_scenario/module/control.lua | 1 + exp_scenario/module/control/spectate.lua | 173 ++++++++++++++++++ exp_scenario/module/locale/en.cfg | 3 + 5 files changed, 178 insertions(+), 174 deletions(-) delete mode 100644 exp_legacy/module/modules/control/spectate.lua create mode 100644 exp_scenario/module/control/spectate.lua diff --git a/exp_legacy/module/modules/control/spectate.lua b/exp_legacy/module/modules/control/spectate.lua deleted file mode 100644 index dc2784f8..00000000 --- a/exp_legacy/module/modules/control/spectate.lua +++ /dev/null @@ -1,173 +0,0 @@ -local Storage = require("modules/exp_util/storage") -local Gui = require("modules/exp_gui") - -local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event - ------ Locals ----- -local follow_label --- @type ExpElement Gui constructor -local following = {} -local spectating = {} -local Public = {} - ------ Storage data ----- -Storage.register({ - following = following, - spectating = spectating, -}, function(tbl) - following = tbl.following - spectating = tbl.spectating -end) - ------ Public Functions ----- - ---- Test if a player is in spectator mode --- @tparam LuaPlayer player The player to test the controller type of --- @treturn boolean Returns true if the player is in spectator mode -function Public.is_spectating(player) - assert(player and player.valid, "Invalid player given to follower") - return player.controller_type == defines.controllers.spectator -end - ---- Puts a player into spectator mode while maintaining an association to their character --- @tparam LuaPlayer player The player that will be placed into spectator mode --- @treturn boolean Returns false if the player was already in spectator mode -function Public.start_spectate(player) - assert(player and player.valid, "Invalid player given to follower") - if spectating[player.index] or not player.character then return false end - local character = player.character - local opened = player.opened - player.set_controller{ type = defines.controllers.spectator } - player.associate_character(character) - spectating[player.index] = character - if opened then player.opened = opened end -- Maintain opened after controller change - return true -end - ---- Return a player from spectator mode back to their character, if their character was killed then respawn them --- @tparam LuaPlayer player The player that will leave spectator mode -function Public.stop_spectate(player) - assert(player and player.valid, "Invalid player given to follower") - local character = spectating[player.index] - spectating[player.index] = nil - if character and character.valid then - local opened = player.opened - player.teleport(character.position, character.surface) - player.set_controller{ type = defines.controllers.character, character = character } - if opened then player.opened = opened end -- Maintain opened after controller change - else - player.ticks_to_respawn = 300 - end -end - ---- Test if a player is in follow mode --- @tparam LuaPlayer player The player to test the follow mode of --- @treturn boolean Returns true if the player is in follow mode -function Public.is_following(player) - assert(player and player.valid, "Invalid player given to follower") - return following[player.index] ~= nil -end - ---- Puts a player into spectator mode and follows an entity as it moves --- @tparam LuaPlayer player The player that will follow the entity --- @tparam ?LuaPlayer|LuaEntity entity The player or entity that will be followed -function Public.start_follow(player, entity) - assert(player and player.valid, "Invalid player given to follower") - assert(entity and entity.valid, "Invalid entity given to follower") - local spectate = Public.start_spectate(player) - - player.close_map() - follow_label(player.gui.screen, entity) - player.teleport(entity.position, entity.surface) - following[player.index] = { player, entity, entity.position, spectate } -end - ---- Returns camera control to the player, will return a player to their character if start_follow placed them into spectator mode --- @tparam LuaPlayer player The player that will regain control of their camera -function Public.stop_follow(player) - assert(player and player.valid, "Invalid player given to follower") - if following[player.index] and following[player.index][4] and Public.is_spectating(player) then - Public.stop_spectate(player) - end - - Gui.destroy_if_valid(player.gui.screen.follow_label) - following[player.index] = nil -end - ---- Returns camera control to all players, will return a player to their character if start_follow placed them into spectator mode -function Public.stop_all() - for key, data in pairs(following) do - Public.stop_follow(data[1]) - end -end - ------ Gui ----- - ---- Label used to show that the player is following, also used to allow esc to stop following -follow_label = Gui.define("follow-label") - :draw(function(def, parent, target) - Gui.destroy_if_valid(parent.follow_label) - - local label = parent.add{ - type = "label", - name = "follow_label", - style = "frame_title", - caption = "Following " .. target.name .. ".\nClick here or press esc to stop following.", - } - - local player = Gui.get_player(parent) - local res = player.display_resolution - label.location = { 0, res.height - 150 } - label.style.width = res.width - label.style.horizontal_align = "center" - player.opened = label - - return label - end) - :on_click(function(def, player, element) - Public.stop_follow(player) - end) - :on_closed(function(def, player, element) - -- Don't call set_controller during on_close as it invalidates the controller - -- Setting an invalid position (as to not equal their current) will call stop_follow on the next tick - following[player.index][3] = {} - end) - ------ Events ----- - ---- Updates the location of the player as well as doing some sanity checks --- @tparam LuaPlayer player The player to update the position of --- @tparam ?LuaPlayer|LuaEntity entity The player or entity being followed -local function update_player_location(player, entity, old_position) - if player.character or not entity.valid then - Public.stop_follow(player) - elseif player.position.x ~= old_position.x or player.position.y ~= old_position.y then - Public.stop_follow(player) - else - player.teleport(entity.position, entity.surface) - end -end - ---- Updates the locations of all players currently following something -local function update_all() - for _, data in pairs(following) do - update_player_location(data[1], data[2], data[3]) - data[3] = data[1].position - end -end - --- Update the location of all players each tick -Event.add(defines.events.on_tick, update_all) - --- Check for player leaving -Event.add(defines.events.on_pre_player_left_game, function(event) - local player = game.players[event.player_index] - Public.stop_follow(player) - for _, data in pairs(following) do - if data[2] == player then - Public.stop_follow(data[1]) - end - end -end) - ------ Module Return ----- -return Public diff --git a/exp_scenario/module/commands/spectate.lua b/exp_scenario/module/commands/spectate.lua index 13907289..816d3152 100644 --- a/exp_scenario/module/commands/spectate.lua +++ b/exp_scenario/module/commands/spectate.lua @@ -3,7 +3,7 @@ Adds commands relating to spectate and follow ]] local Commands = require("modules/exp_commands") -local Spectate = require("modules.exp_legacy.modules.control.spectate") --- @dep modules.control.spectate +local Spectate = require("modules/exp_scenario/control/spectate") --- Toggles spectator mode for the caller Commands.new("spectate", { "exp-commands_spectate.description-spectate" }) diff --git a/exp_scenario/module/control.lua b/exp_scenario/module/control.lua index 2e04e552..08f8a3f2 100644 --- a/exp_scenario/module/control.lua +++ b/exp_scenario/module/control.lua @@ -65,6 +65,7 @@ add(require("modules/exp_scenario/control/report_jail")) add(require("modules/exp_scenario/control/research")) add(require("modules/exp_scenario/control/roles")) add(require("modules/exp_scenario/control/spawn_area")) +add(require("modules/exp_scenario/control/spectate")) add(require("modules/exp_scenario/control/station_auto_name")) --- Guis diff --git a/exp_scenario/module/control/spectate.lua b/exp_scenario/module/control/spectate.lua new file mode 100644 index 00000000..b8cac7be --- /dev/null +++ b/exp_scenario/module/control/spectate.lua @@ -0,0 +1,173 @@ +--[[-- Control - Spectate +Lets players spectate without losing their character, and follow other players or entities +]] + +local Storage = require("modules/exp_util/storage") +local Gui = require("modules/exp_gui") + +--- @class ExpScenario_Spectate +local Spectate = { + --- @package + events = {}, +} + +--- @class ExpScenario_Spectate.Following +--- @field player LuaPlayer +--- @field target LuaPlayer | LuaEntity +--- @field position MapPosition Where the player was after the last update, moving away stops following +--- @field started_spectate boolean True when start_follow put the player into spectator mode +--- @field stop boolean? Set when following must end on the next tick + +local following = {} --- @type table Keyed by player index +local spectating = {} --- @type table The character a player returns to, keyed by player index + +Storage.register({ + following = following, + spectating = spectating, +}, function(tbl) + following = tbl.following + spectating = tbl.spectating +end) + +--- Label shown while following, clicking it or pressing escape stops following +--- @class ExpScenario_Spectate.follow_label: ExpElement +--- @overload fun(parent: LuaGuiElement, target: LuaPlayer | LuaEntity): LuaGuiElement +local follow_label = Gui.define("spectate/follow_label") + :draw(function(_, parent, target) + Gui.destroy_if_valid(parent.follow_label) + + local label = parent.add{ + type = "label", + name = "follow_label", + style = "frame_title", + caption = { "exp_spectate.follow-label", target.name }, + } + + local player = Gui.get_player(parent) + local res = player.display_resolution + label.location = { 0, res.height - 150 } + label.style.width = res.width + label.style.horizontal_align = "center" + player.opened = label + + return label + end) + :on_click(function(_, player) + Spectate.stop_follow(player) + end) + :on_closed(function(_, player) + -- set_controller can not be called during on_gui_closed, so the next update stops following + local data = following[player.index] + if data then data.stop = true end + end) --[[@as any]] + +--- Check if a player is in spectator mode +--- @param player LuaPlayer +--- @return boolean +function Spectate.is_spectating(player) + return player.controller_type == defines.controllers.spectator +end + +--- Put a player into spectator mode while keeping their character +--- @param player LuaPlayer +--- @return boolean # False when the player was already spectating or has no character +function Spectate.start_spectate(player) + if spectating[player.index] or not player.character then return false end + local character = player.character + local opened = player.opened + player.set_controller{ type = defines.controllers.spectator } + player.associate_character(character) + spectating[player.index] = character + if opened then player.opened = opened end -- Changing controller closes the opened gui + return true +end + +--- Return a player to their character, or respawn them if it was killed +--- @param player LuaPlayer +function Spectate.stop_spectate(player) + local character = spectating[player.index] + spectating[player.index] = nil + if character and character.valid then + local opened = player.opened + player.teleport(character.position, character.surface) + player.set_controller{ type = defines.controllers.character, character = character } + if opened then player.opened = opened end -- Changing controller closes the opened gui + else + player.ticks_to_respawn = 300 + end +end + +--- Check if a player is following something +--- @param player LuaPlayer +--- @return boolean +function Spectate.is_following(player) + return following[player.index] ~= nil +end + +--- Put a player into spectator mode and keep their camera on a target as it moves +--- @param player LuaPlayer +--- @param target LuaPlayer | LuaEntity +function Spectate.start_follow(player, target) + local started_spectate = Spectate.start_spectate(player) + + follow_label(player.gui.screen, target) + player.teleport(target.position, target.surface) + following[player.index] = { + player = player, + target = target, + position = player.position, + started_spectate = started_spectate, + } +end + +--- Give a player their camera back, returning them to their character if start_follow took it +--- @param player LuaPlayer +function Spectate.stop_follow(player) + local data = following[player.index] + if data and data.started_spectate and Spectate.is_spectating(player) then + Spectate.stop_spectate(player) + end + + Gui.destroy_if_valid(player.gui.screen.follow_label) + following[player.index] = nil +end + +--- Move a following player onto their target, or stop following when they have moved away or regained a character +--- @param data ExpScenario_Spectate.Following +local function update_following(data) + local player, target = data.player, data.target + local position = player.position + if data.stop or player.character or not target.valid + or position.x ~= data.position.x or position.y ~= data.position.y + then + Spectate.stop_follow(player) + else + player.teleport(target.position, target.surface) + data.position = player.position + end +end + +local function on_tick() + for _, data in pairs(following) do + update_following(data) + end +end + +--- Stop following when a player leaves, and stop anyone who was following them +--- @param event EventData.on_pre_player_left_game +local function on_pre_player_left_game(event) + local player = game.players[event.player_index] + Spectate.stop_follow(player) + for _, data in pairs(following) do + if data.target == player then + Spectate.stop_follow(data.player) + end + end +end + +local e = defines.events + +Spectate.events[e.on_tick] = on_tick +Spectate.events[e.on_pre_player_left_game] = on_pre_player_left_game + +return Spectate diff --git a/exp_scenario/module/locale/en.cfg b/exp_scenario/module/locale/en.cfg index 80e8a1ec..ff47c27c 100644 --- a/exp_scenario/module/locale/en.cfg +++ b/exp_scenario/module/locale/en.cfg @@ -577,3 +577,6 @@ chat-jailed=__1__ was jailed because they removed too many protected entities. P [exp_report-jail] chat-jailed=__1__ was jailed because they were reported too many times. Please wait for a moderator. + +[exp_spectate] +follow-label=Following __1__.\nClick here or press escape to stop following. From c12f0a8f8960afad67767a1df3e70d4671af18c3 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:03:26 +0100 Subject: [PATCH 4/6] Remove redundant event properties --- exp_scenario/module/control/jail.lua | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/exp_scenario/module/control/jail.lua b/exp_scenario/module/control/jail.lua index bc4730fa..5bf07c40 100644 --- a/exp_scenario/module/control/jail.lua +++ b/exp_scenario/module/control/jail.lua @@ -25,7 +25,7 @@ local Jail = { --- The role given to jailed players, it has a higher priority than every other role --- @return ExpRoles.Role -local function jail_role() +local function get_jail_role() return (assert(Roles.get_role_by_name("Jail"), "The Jail role does not exist")) end @@ -33,7 +33,7 @@ end --- @param player LuaPlayer --- @return boolean function Jail.is_jailed(player) - return jail_role():has_player(player) + return get_jail_role():has_player(player) end --- Put a player into jail, which suppresses all of their other roles @@ -42,7 +42,7 @@ end --- @param reason string --- @return boolean # False when the player was already in jail function Jail.jail_player(player, by_player_name, reason) - local role = jail_role() + local role = get_jail_role() if role:has_player(player) then return false end -- Stop whatever the player is doing, the jail permission group stops them from starting again @@ -56,8 +56,6 @@ function Jail.jail_player(player, by_player_name, reason) role:assign(player, { by_player_name = by_player_name, silent = true }) script.raise_event(Jail.on_player_jailed, { - name = Jail.on_player_jailed, - tick = game.tick, player_index = player.index, by_player_name = by_player_name, reason = reason, @@ -71,14 +69,12 @@ end --- @param by_player_name string --- @return boolean # False when the player was not in jail function Jail.unjail_player(player, by_player_name) - local role = jail_role() + local role = get_jail_role() if not role:has_player(player) then return false end role:unassign(player, { by_player_name = by_player_name, silent = true }) script.raise_event(Jail.on_player_unjailed, { - name = Jail.on_player_unjailed, - tick = game.tick, player_index = player.index, by_player_name = by_player_name, }) From 702016b3e2bdeee486df90662f3d6c40bf97e2b6 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:15:37 +0100 Subject: [PATCH 5/6] Remove redundant event properties --- exp_scenario/module/control/protection.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/exp_scenario/module/control/protection.lua b/exp_scenario/module/control/protection.lua index c8907e0f..9839290f 100644 --- a/exp_scenario/module/control/protection.lua +++ b/exp_scenario/module/control/protection.lua @@ -184,14 +184,12 @@ local function raise_violation(event, player) 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 @@ -201,8 +199,9 @@ end 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)) + 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 From e1966dec9cc140d3f46f5a8c1b65ea0bd754865b Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:27:14 +0100 Subject: [PATCH 6/6] Small format changes --- exp_scenario/module/control/spectate.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/exp_scenario/module/control/spectate.lua b/exp_scenario/module/control/spectate.lua index b8cac7be..b8ccb29b 100644 --- a/exp_scenario/module/control/spectate.lua +++ b/exp_scenario/module/control/spectate.lua @@ -75,10 +75,13 @@ function Spectate.start_spectate(player) if spectating[player.index] or not player.character then return false end local character = player.character local opened = player.opened + player.set_controller{ type = defines.controllers.spectator } player.associate_character(character) spectating[player.index] = character + if opened then player.opened = opened end -- Changing controller closes the opened gui + return true end @@ -87,6 +90,7 @@ end function Spectate.stop_spectate(player) local character = spectating[player.index] spectating[player.index] = nil + if character and character.valid then local opened = player.opened player.teleport(character.position, character.surface) @@ -137,8 +141,12 @@ end local function update_following(data) local player, target = data.player, data.target local position = player.position - if data.stop or player.character or not target.valid - or position.x ~= data.position.x or position.y ~= data.position.y + if + data.stop + or player.character + or not target.valid + or position.x ~= data.position.x + or position.y ~= data.position.y then Spectate.stop_follow(player) else