diff --git a/exp_legacy/module/config/discord_alerts.lua b/exp_legacy/module/config/discord_alerts.lua index be19c6e2..4d5b7399 100644 --- a/exp_legacy/module/config/discord_alerts.lua +++ b/exp_legacy/module/config/discord_alerts.lua @@ -5,7 +5,6 @@ return { show_playtime = true, entity_protection = true, player_reports = true, - player_warnings = true, player_bans = true, player_mutes = true, player_kicks = true, diff --git a/exp_legacy/module/config/gui/player_list_actions.lua b/exp_legacy/module/config/gui/player_list_actions.lua index 146338aa..fd631006 100644 --- a/exp_legacy/module/config/gui/player_list_actions.lua +++ b/exp_legacy/module/config/gui/player_list_actions.lua @@ -9,8 +9,7 @@ local ExpUtil = require("modules/exp_util") 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 @@ -100,26 +99,12 @@ local function report_player_callback(player, reason) Reports.report_player(selected_player.name, player.name, reason) end ---- Gives the action player a warning, requires a reason --- @element warn_player -local warn_player = new_button("utility/spawn_flag", { "exp-gui_player-list.warn-player" }) - :on_click(function(def, player, element) - set_selected_action(player, "exp_scenario.command.create_warning") - end) - -local function warn_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_warnings.create", selected_player_color, by_player_name_color, reason } - Warnings.add_warning(selected_player.name, player.name, reason) -end - --- Jails the action player, requires a reason -- @element jail_player 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 +115,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 @@ -170,18 +155,11 @@ return { ["exp_scenario.command.create_report"] = { auth = function(player, selected_player) if player == selected_player then return false end - if not Roles.player_has_permission(player, "exp_scenario.command.create_warning") then - return not Roles.player_has_permission(selected_player, "exp_scenario.bypass.reports") - end - end, -- can report any player that isn't immune and you aren't able to give warnings + return not Roles.player_has_permission(selected_player, "exp_scenario.bypass.reports") + end, -- can report any player that isn't immune reason_callback = report_player_callback, report_player, }, - ["exp_scenario.command.create_warning"] = { - auth = Roles.player_outranks, -- warn a lower user, replaces report - reason_callback = warn_player_callback, - warn_player, - }, ["exp_scenario.command.jail"] = { auth = Roles.player_outranks, reason_callback = jail_player_callback, diff --git a/exp_legacy/module/config/warnings.lua b/exp_legacy/module/config/warnings.lua deleted file mode 100644 index b60e003b..00000000 --- a/exp_legacy/module/config/warnings.lua +++ /dev/null @@ -1,38 +0,0 @@ ---- Config file for the warning system, this is very similar to reports but is for the use of moderators rather than normal users. --- @config Warnings - -return { - --- @type (LocalisedString[] | fun(player: LuaPlayer, by_player_name: string, number_of_warnings: number))[] - actions = { --- @setting actions what actions are taking at number of warnings - -- if a localized string is used then __1__ will by_player_name and __2__ will be the current warning count (auto inserted) - { "warnings.received", "" }, - { "warnings.received", "" }, - { "warnings.received", { "warnings.pre-kick" } }, - function(player, by_player_name, number_of_warnings) - local str = { - "You received a warning from ", - by_player_name, - ". You have ", - number_of_warnings, - " warnings. You were kicked for having too many warnings; you may rejoin if you wish.", - } - game.kick_player(player, table.concat(str, "")) -- Does not support locale strings - -- game.kick_player(player, { "warnings.received", by_player_name, number_of_warnings, { "warnings.kick" } }) - end, - { "warnings.received", { "warnings.pre-pre-ban" } }, - { "warnings.received", { "warnings.pre-ban" } }, - function(player, by_player_name, number_of_warnings) - local str = { - "You received a warning from ", - by_player_name, - ". You have ", - number_of_warnings, - " warnings. You were banned for having too many warnings; visit https://www.explosivegaming.nl to request a ban appeal.", - } - game.kick_player(player, table.concat(str, "")) -- Does not support locale strings - -- game.ban_player(player, { "warnings.received", by_player_name, number_of_warnings, { "warnings.ban", { "links.website" } } }) - end, - }, - script_warning_cool_down = 30, --- @setting script_warning_cool_down time for a script warning (given by script) to be removed (in minutes) - script_warning_limit = 5, --- @setting script_warning_limit the number of script warnings (given by script) that are allowed before full warnings are given -} diff --git a/exp_legacy/module/locale/en/addons.cfg b/exp_legacy/module/locale/en/addons.cfg index 7c72a395..a86a4314 100644 --- a/exp_legacy/module/locale/en/addons.cfg +++ b/exp_legacy/module/locale/en/addons.cfg @@ -18,14 +18,3 @@ read-readme=Make sure you have read the information gui (It can be found through softmod=We run a softmod on our servers. A softmod is a custom scenario that runs on this server, an example is the player list. redmew=We don't talk about redmew here; they beat us to 1000 members ;-; lhd=All trains must be LHD! This is a long standing rule on our servers, please respect this. - -[warnings] -received=You received a warning from __1__. You have __2__ warnings. __3__ -pre-kick=This is your last warning before you are kicked. -kick=You were kicked for having too many warnings; you may rejoin if you wish. -pre-pre-ban=You are close to receiving a ban; successful ban appeals are unlikely. -pre-ban=This your LAST warning before you are BANNED! Successful ban appeals are unlikely. -ban=You were banned for having too many warnings; visit __1__ to request a ban appeal. -script-warning=You are receiving script warnings; if you recive too many you will receive a permanent warning (__1__/__2__) -script-warning-removed=A script warning has expired (__1__/__2__) -script-warning-limit=__1__ has received a permanent warning from the script. diff --git a/exp_legacy/module/locale/en/data.cfg b/exp_legacy/module/locale/en/data.cfg index 529e7852..8fc02534 100644 --- a/exp_legacy/module/locale/en/data.cfg +++ b/exp_legacy/module/locale/en/data.cfg @@ -10,11 +10,6 @@ message-cleared=Your join message has been cleared. [quickbar] saved=Your quickbar filters have been saved. -[exp-required] -Warnings=Warnings -Warnings-tooltip=The total number of warnings you have received from staff -Warnings-value-tooltip=The total number of warnings you have received from staff - [exp-settings] Colour=Colour Colour-tooltip=Your player colour diff --git a/exp_legacy/module/locale/en/gui.cfg b/exp_legacy/module/locale/en/gui.cfg index c83514fc..5e6bb78a 100644 --- a/exp_legacy/module/locale/en/gui.cfg +++ b/exp_legacy/module/locale/en/gui.cfg @@ -7,7 +7,6 @@ reason-entry=Enter Reason goto-player=Goto player bring-player=Bring player report-player=Report player -warn-player=Warn player jail-player=Jail player kick-player=Kick player ban-player=Ban player diff --git a/exp_legacy/module/locale/zh-CN/addons.cfg b/exp_legacy/module/locale/zh-CN/addons.cfg index d706e713..366999b2 100644 --- a/exp_legacy/module/locale/zh-CN/addons.cfg +++ b/exp_legacy/module/locale/zh-CN/addons.cfg @@ -18,14 +18,3 @@ read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看 softmod=這裹用了自設情境,是一種軟裝模組。 redmew= lhd=列車必須是左則通行。這是本服務器長久以來的規則。 - -[warnings] -received=你已被 __1__ 警告了,現在共有 __2__ 個警告。 __3__ -pre-kick=這是在被踢離之前的最後警告。 -kick=你已因為被警告太多次而被請離,不過你之後還是可以回來的。 -pre-pre-ban=按照你的行為,你有可能會被封禁。 -pre-ban=這是在被封禁之前的最後警告。 -ban=你已因為被警告太多次而被封禁; 可以到 __1__ 申訴. -script-warning=這是系統發出的自動警告 (__1__/__2__) -script-warning-removed=系統發出的自動警告已失效 (__1__/__2__) -script-warning-limit=__1__ 已被系統發出了一則自動警告。 diff --git a/exp_legacy/module/locale/zh-CN/data.cfg b/exp_legacy/module/locale/zh-CN/data.cfg index 96a1b4b1..d8ce31ea 100644 --- a/exp_legacy/module/locale/zh-CN/data.cfg +++ b/exp_legacy/module/locale/zh-CN/data.cfg @@ -10,11 +10,6 @@ message-cleared=你的加入信息已清除。 [quickbar] saved=你的工具列已儲存。 -[exp-required] -Warnings=警告 -Warnings-tooltip=你所有收到的警告 -Warnings-value-tooltip=你所有收到的警告 - [exp-settings] Colour=顏色 Colour-tooltip=你的人物顏色 diff --git a/exp_legacy/module/locale/zh-CN/gui.cfg b/exp_legacy/module/locale/zh-CN/gui.cfg index fbadf609..fbcdd618 100644 --- a/exp_legacy/module/locale/zh-CN/gui.cfg +++ b/exp_legacy/module/locale/zh-CN/gui.cfg @@ -7,7 +7,6 @@ reason-entry=輸入原因 goto-player=傳送到用戶 bring-player=傳送用戶到自己 report-player=舉報用戶 -warn-player=警告用戶 jail-player=監禁用戶 kick-player=踢除用戶 ban-player=封禁用戶 diff --git a/exp_legacy/module/locale/zh-TW/addons.cfg b/exp_legacy/module/locale/zh-TW/addons.cfg index d706e713..366999b2 100644 --- a/exp_legacy/module/locale/zh-TW/addons.cfg +++ b/exp_legacy/module/locale/zh-TW/addons.cfg @@ -18,14 +18,3 @@ read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看 softmod=這裹用了自設情境,是一種軟裝模組。 redmew= lhd=列車必須是左則通行。這是本服務器長久以來的規則。 - -[warnings] -received=你已被 __1__ 警告了,現在共有 __2__ 個警告。 __3__ -pre-kick=這是在被踢離之前的最後警告。 -kick=你已因為被警告太多次而被請離,不過你之後還是可以回來的。 -pre-pre-ban=按照你的行為,你有可能會被封禁。 -pre-ban=這是在被封禁之前的最後警告。 -ban=你已因為被警告太多次而被封禁; 可以到 __1__ 申訴. -script-warning=這是系統發出的自動警告 (__1__/__2__) -script-warning-removed=系統發出的自動警告已失效 (__1__/__2__) -script-warning-limit=__1__ 已被系統發出了一則自動警告。 diff --git a/exp_legacy/module/locale/zh-TW/data.cfg b/exp_legacy/module/locale/zh-TW/data.cfg index 96a1b4b1..d8ce31ea 100644 --- a/exp_legacy/module/locale/zh-TW/data.cfg +++ b/exp_legacy/module/locale/zh-TW/data.cfg @@ -10,11 +10,6 @@ message-cleared=你的加入信息已清除。 [quickbar] saved=你的工具列已儲存。 -[exp-required] -Warnings=警告 -Warnings-tooltip=你所有收到的警告 -Warnings-value-tooltip=你所有收到的警告 - [exp-settings] Colour=顏色 Colour-tooltip=你的人物顏色 diff --git a/exp_legacy/module/locale/zh-TW/gui.cfg b/exp_legacy/module/locale/zh-TW/gui.cfg index fbadf609..fbcdd618 100644 --- a/exp_legacy/module/locale/zh-TW/gui.cfg +++ b/exp_legacy/module/locale/zh-TW/gui.cfg @@ -7,7 +7,6 @@ reason-entry=輸入原因 goto-player=傳送到用戶 bring-player=傳送用戶到自己 report-player=舉報用戶 -warn-player=警告用戶 jail-player=監禁用戶 kick-player=踢除用戶 ban-player=封禁用戶 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_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_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_legacy/module/modules/control/warnings.lua b/exp_legacy/module/modules/control/warnings.lua deleted file mode 100644 index 6a0fa116..00000000 --- a/exp_legacy/module/modules/control/warnings.lua +++ /dev/null @@ -1,331 +0,0 @@ ---[[-- Control Module - Warnings - - Adds a way to give and remove warnings to players. - @control Warnings - @alias Warnings - - @usage - -- import the module from the control modules - local Warnings = require("modules.exp_legacy.modules.control.warnings") --- @dep modules.control.warnings - - -- This will add a warning to the player - Warnings.add_warning('MrBiter', 'Cooldude2606', 'Killed too many biters') - - -- This will remove a warning from a player, second name is just who is doing the action - Warnings.remove_warning('MrBiter', 'Cooldude2606') - - -- Script warning as similar to normal warning but are designed to have no effect for a short amount of time - -- this is so it can be used for greifer protection without being too agressive - Warnings.add_script_warning('MrBiter', 'Killed too many biters') - - -- Both normal and script warnings can also be cleared, this will remove all warnings - Warnings.clear_warnings('MrBiter', 'Cooldude2606') -]] - -local Event = require("modules/exp_legacy/utils/event") -local Storage = require("modules/exp_util/storage") -local config = require("modules.exp_legacy.config.warnings") - -local valid_player = function(p) return type(p) == "userdata" and p or game.get_player(p) end - ---- Stores the quickbar filters for a player -local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data -local PlayerWarnings = PlayerData.Required:combine("Warnings") -PlayerWarnings:set_metadata{ - stringify = function(value) - if not value then return "You have no warnings" end - local count = 0 - for _ in pairs(value) do count = count + 1 end - - return "You have " .. count .. " warnings" - end, -} - -local Warnings = { - user_warnings = PlayerWarnings, - user_script_warnings = {}, - events = { - --- When a warning is added to a player - -- @event on_warning_added - -- @tparam number player_index the index of the player who recived the warning - -- @tparam string by_player_name the name of the player who gave the warning - -- @tparam string reason the reason that the player was given a warning - -- @tparam number warning_count the new number of warnings that the player has - on_warning_added = script.generate_event_name(), - --- When a warning is removed from a player - -- @event on_warning_removed - -- @tparam number player_index the index of the player who is having the warning removed - -- @tparam string warning_by_name the name of the player who gave the warning - -- @tparam string removed_by_name the name of the player who is removing the warning - -- @tparam number warning_count the new number of warnings that the player has - -- @tparam number batch_count the number of warnings removed in this batch, always one when not a batch - -- @tparam number batch the index of this event in a batch, always one when not a batch - on_warning_removed = script.generate_event_name(), - --- When a warning is added to a player, by the script - -- @event on_script_warning_added - -- @tparam number player_index the index of the player who recived the warning - -- @tparam string reason the reason that the player was given a warning - -- @tparam number warning_count the new number of warnings that the player has - on_script_warning_added = script.generate_event_name(), - --- When a warning is removed from a player, by the script - -- @event on_script_warning_removed - -- @tparam number player_index the index of the player who is having the warning removed - -- @tparam number warning_count the new number of warnings that the player has - on_script_warning_removed = script.generate_event_name(), - }, -} - -local user_script_warnings = Warnings.user_script_warnings -Storage.register(user_script_warnings, function(tbl) - Warnings.user_script_warnings = tbl - user_script_warnings = tbl -end) - ---- Gets an array of warnings that the player has, always returns a list even if empty --- @tparam LuaPlayer player the player to get the warning for --- @treturn table an array of all the warnings on this player, contains tick, by_player_name and reason -function Warnings.get_warnings(player) - return PlayerWarnings:get(player.name, {}) -end - ---- Gets the number of warnings that a player has on them --- @tparam LuaPlayer player the player to count the warnings for --- @treturn number the number of warnings that the player has -function Warnings.count_warnings(player) - local warnings = PlayerWarnings:get(player.name, {}) - return #warnings -end - ---- Adds a warning to a player, when a warning is added a set action is done based on the number of warnings and the config file --- @tparam LuaPlayer player the player to add a warning to --- @tparam string by_player_name the name of the player who is doing the action --- @tparam[opt='Non given.'] string reason the reason that the player is being warned --- @treturn number the number of warnings that the player has -function Warnings.add_warning(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 "None given." - - local warning_count --- @type number - PlayerWarnings:update(player.name, function(_, warnings) - local warning = { - by_player_name = by_player_name, - reason = reason, - } - - if not warnings then - warning_count = 1 - return { warning } - else - table.insert(warnings, warning) - warning_count = #warnings - end - end) - - script.raise_event(Warnings.events.on_warning_added, { - name = Warnings.events.on_warning_added, - tick = game.tick, - player_index = player.index, - warning_count = warning_count, - by_player_name = by_player_name, - reason = reason, - }) - - local action = config.actions[warning_count] - if action then - local _type = type(action) - if _type == "function" then - action(player, by_player_name, warning_count) - elseif _type == "table" then - local current = table.deepcopy(action) - table.insert(current, 2, by_player_name) - table.insert(current, 3, warning_count) - player.print(current) - elseif type(action) == "string" then - player.print(action) - end - end - - return warning_count -end - ---- Event trigger for removing a waring due to it being looped in clear warnings --- @tparam LuaPlayer player the player who is having a warning removed --- @tparam string warning_by_name the name of the player who made the warning --- @tparam string removed_by_name the name of the player who is doing the action --- @tparam number warning_count the number of warnings that the player how has --- @tparam number batch the index of this event in a batch, always one when not a batch --- @tparam number batch_count the number of reports removed in this batch, always one when not a batch -local function warning_removed_event(player, warning_by_name, removed_by_name, warning_count, batch, batch_count) - script.raise_event(Warnings.events.on_warning_removed, { - name = Warnings.events.on_warning_removed, - tick = game.tick, - player_index = player.index, - warning_count = warning_count, - warning_by_name = warning_by_name, - removed_by_name = removed_by_name, - batch_count = batch_count or 1, - batch = batch or 1, - }) -end - ---- Removes a warning from a player, always removes the earliest warning, fifo --- @tparam LuaPlayer player the player to remove a warning from --- @tparam string by_player_name the name of the player who is doing the action --- @treturn number the number of warnings that the player has -function Warnings.remove_warning(player, by_player_name) - player = valid_player(player) - if not player then return end - if not by_player_name then return end - - local warning, warning_count - PlayerWarnings:update(player.name, function(_, warnings) - if not warnings then return end - warning = table.remove(warnings, 1) - warning_count = #warnings - end) - - if not warning then return end - warning_removed_event(player, warning.by_player_name, by_player_name, warning_count) - - return warning_count -end - ---- Removes all warnings from a player, will trigger remove event for each warning --- @tparam LuaPlayer player the player to clear the warnings from --- @tparam string by_player_name the name of the player who is doing the action --- @treturn boolean true when warnings were cleared succesfully -function Warnings.clear_warnings(player, by_player_name) - player = valid_player(player) - if not player then return end - if not by_player_name then return end - - local warnings = PlayerWarnings:get(player) - if not warnings then return end - - local warning_count = #warnings - for n, warning in pairs(warnings) do - warning_removed_event(player, warning.by_player_name, by_player_name, warning_count - n, n, warning_count) - end - - PlayerWarnings:remove(player) - return true -end - ---- Gets an array of all the script warnings that a player has --- @tparam LuaPlayer player the player to get the script warnings of --- @treturn table a table of all the script warnings a player has, contains tick and reason -function Warnings.get_script_warnings(player) - return user_script_warnings[player.name] or {} -end - ---- Gets the number of script warnings that a player has on them --- @tparam LuaPlayer player the player to count the script warnings of --- @treturn number the number of script warnings that the player has -function Warnings.count_script_warnings(player) - local warnings = user_script_warnings[player.name] or {} - return #warnings -end - ---- Adds a script warning to a player, this may add a full warning if max script warnings is met --- @tparam LuaPlayer player the player to add a script warning to --- @tparam[opt='Non given.'] string reason the reason that the player is being warned --- @treturn number the number of script warnings that the player has -function Warnings.add_script_warning(player, reason) - player = valid_player(player) - if not player then return end - - reason = reason or "Non given." - - local warnings = user_script_warnings[player.name] - if not warnings then - warnings = {} - user_script_warnings[player.name] = warnings - end - - table.insert(warnings, { - tick = game.tick, - reason = reason, - }) - - local warning_count = #warnings - - script.raise_event(Warnings.events.on_script_warning_added, { - name = Warnings.events.on_script_warning_added, - tick = game.tick, - player_index = player.index, - warning_count = warning_count, - reason = reason, - }) - - if warning_count > config.script_warning_limit then - Warnings.add_warning(player, "", reason) - end - - return warning_count -end - ---- Script warning removed event trigger due to it being looped in clear script warnings --- @tparam LuaPlayer player the player who is having a script warning removed --- @tparam number warning_count the number of warnings that the player has -local function script_warning_removed_event(player, warning_count) - script.raise_event(Warnings.events.on_script_warning_removed, { - name = Warnings.events.on_script_warning_removed, - tick = game.tick, - player_index = player.index, - warning_count = warning_count, - }) -end - ---- Removes a script warning from a player --- @tparam LuaPlayer player the player to remove a script warning from --- @treturn number the number of script warnings that the player has -function Warnings.remove_script_warning(player) - player = valid_player(player) - if not player then return end - - local warnings = user_script_warnings[player.name] - if not warnings then return end - - table.remove(warnings, 1) - - script_warning_removed_event(player) - - return #warnings -end - ---- Removes all script warnings from a player, emits event for each warning removed --- @tparam LuaPlayer player the player to clear the script warnings from -function Warnings.clear_script_warnings(player) - player = valid_player(player) - if not player then return end - - local warnings = user_script_warnings[player.name] - if not warnings then return end - - local warning_count = #warnings - for n, _ in pairs(warnings) do - script_warning_removed_event(player, warning_count - n) - end - - user_script_warnings[player.name] = nil - return true -end - --- script warnings are removed after a certain amount of time to make them even more lienient -local script_warning_cool_down = config.script_warning_cool_down * 3600 -Event.on_nth_tick(script_warning_cool_down / 4, function() - local cutoff = game.tick - script_warning_cool_down - for player_name, script_warnings in pairs(user_script_warnings) do - if #script_warnings > 0 then - for _, warning in pairs(script_warnings) do - if warning.tick < cutoff then - Warnings.remove_script_warning(player_name) - end - end - end - end -end) - -return Warnings diff --git a/exp_legacy/module/modules/data/quickbar.lua b/exp_legacy/module/modules/data/quickbar.lua index 8550cae9..0db8e846 100644 --- a/exp_legacy/module/modules/data/quickbar.lua +++ b/exp_legacy/module/modules/data/quickbar.lua @@ -20,46 +20,38 @@ PlayerFilters:set_metadata{ end, } +--- Filters are stored by a single index across the ten pages of ten slots +local total_page_count = 10 +local slots_per_page = 10 + --- Loads your quickbar preset PlayerFilters:on_load(function(player_name, filters) if not filters then filters = config[player_name] end if not filters then return end local player = game.players[player_name] - for i, item_name in pairs(filters) do + for index, item_name in pairs(filters) do if item_name ~= nil and item_name ~= "" then - player.set_quick_bar_slot(i, item_name) + local page = math.ceil(index / slots_per_page) + local slot = (index - 1) % slots_per_page + 1 + player.set_quick_bar_slot(page, slot, item_name) end end end) -local ignored_items = { - ["blueprint"] = true, - ["blueprint-book"] = true, - ["deconstruction-planner"] = true, - ["spidertron-remote"] = true, - ["upgrade-planner"] = true, -} - ---- Saves your quickbar preset to the script-output folder +--- Saves your quickbar preset, only plain item filters can be saved Commands.new("save-quickbar", "Saves your Quickbar preset items to file") :add_aliases{ "save-toolbar" } - :add_flags{ "disabled" } :register(function(player) local filters = {} - error("2.1 changes to get_quick_bar_slot beak compatibility with 2.0; waiting for upstream") - -- Upstream may add method to compat, or change inventory sync to have a quickbar only mode - for i = 1, 100 do - --[[ - local slot = player.get_quick_bar_slot(i) - -- Need to filter out blueprint and blueprint books because the slot is a LuaItemPrototype and does not contain a way to export blueprint data - if slot ~= nil then - local ignored = ignored_items[slot.name] - if ignored ~= true then - filters[i] = slot.name + for page = 1, total_page_count do + for slot = 1, slots_per_page do + -- Records, remotes and specific item instances hold data which can not be saved by name + local quick_bar_slot = player.get_quick_bar_slot(page, slot) + if quick_bar_slot and quick_bar_slot.type == "filter" then + filters[(page - 1) * slots_per_page + slot] = assert(quick_bar_slot.filter).name end end - ]] end if next(filters) then diff --git a/exp_legacy/module/modules/data/statistics.lua b/exp_legacy/module/modules/data/statistics.lua index 08bcb7d5..ac844387 100644 --- a/exp_legacy/module/modules/data/statistics.lua +++ b/exp_legacy/module/modules/data/statistics.lua @@ -113,16 +113,47 @@ if config.MachinesRemoved or config.TreesDestroyed or config.OreMined then Event.add(defines.events.on_player_mined_entity, on_event) end +--- Vehicles which credit their driver, or failing that their passenger, with what they hit +local crewed_vehicles = { car = true, ["spider-vehicle"] = true, locomotive = true } + +--- Get the connected player behind the cause of damage or a death, nil for turrets, trains without a crew and the like +--- @param cause LuaEntity? +--- @return LuaPlayer? +local function get_cause_player(cause) + if not cause or not cause.valid then return nil end + + local occupant = cause --- @type LuaEntity | LuaPlayer + if crewed_vehicles[cause.type] then + local crew = cause.get_driver() or cause.get_passenger() + if not crew then return nil end + occupant = crew + end + + local player --- @type LuaPlayer? + if occupant.object_name == "LuaPlayer" then + player = occupant --[[@as LuaPlayer]] + elseif occupant.type == "character" then + player = occupant.player + end + + if not player or not player.valid or not player.connected then return nil end + return player +end + +--- Check that damage or a death counts against an entity, it must be hostile to the player +--- @param entity LuaEntity +--- @param player LuaPlayer +--- @return boolean +local function is_hostile(entity, player) + return entity.valid and entity.force ~= player.force and entity.force.name ~= "neutral" +end + --- Add DamageDealt if it is enabled if config.DamageDealt then local stat = Statistics:combine("DamageDealt") Event.add(defines.events.on_entity_damaged, function(event) - local character = event.cause -- Check character is valid - if not character or not character.valid or character.type ~= "character" then return end - local player = character.player -- Check player is valid - if not player.valid or not player.connected then return end - local entity = event.entity -- Check entity is valid - if not entity.valid or entity.force == player.force or entity.force.name == "neutral" then return end + local player = get_cause_player(event.cause) + if not player or not is_hostile(event.entity, player) then return end stat:increment(player, floor(event.final_damage_amount)) end) end @@ -131,12 +162,8 @@ end if config.Kills then local stat = Statistics:combine("Kills") Event.add(defines.events.on_entity_died, function(event) - local character = event.cause -- Check character is valid - if not character or not character.valid or character.type ~= "character" then return end - local player = character.player -- Check player is valid - if not player or not player.valid or not player.connected then return end - local entity = event.entity -- Check entity is valid - if not entity.valid or entity.force == player.force or entity.force.name == "neutral" then return end + local player = get_cause_player(event.cause) + if not player or not is_hostile(event.entity, player) then return end stat:increment(player) end) end diff --git a/exp_roles/seed.ts b/exp_roles/seed.ts index 97325245..b028314a 100644 --- a/exp_roles/seed.ts +++ b/exp_roles/seed.ts @@ -67,9 +67,6 @@ export const seedRoles: SeedRole[] = [ "exp_scenario.command.tag_clear.always", "exp_scenario.command.spawn.always", "exp_scenario.command.clear_reports", - "exp_scenario.command.clear_warnings", - "exp_scenario.command.clear_script_warnings", - "exp_scenario.command.clear_last_warnings", "exp_scenario.command.clear_inventory", "exp_scenario.command.kill_enemies", "exp_scenario.command.remove_enemies", @@ -103,8 +100,6 @@ export const seedRoles: SeedRole[] = [ "exp_scenario.command.goto", "exp_scenario.command.teleport", "exp_scenario.command.bring", - "exp_scenario.command.create_warning", - "exp_scenario.command.get_warnings", "exp_scenario.command.get_reports", "exp_scenario.command.protect_entity", "exp_scenario.command.protect_area", 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/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/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/commands/warnings.lua b/exp_scenario/module/commands/warnings.lua deleted file mode 100644 index 14be9aad..00000000 --- a/exp_scenario/module/commands/warnings.lua +++ /dev/null @@ -1,96 +0,0 @@ ---[[-- Commands - Warnings -Adds a commands that allow admins to warn other players -]] - -local Commands = require("modules/exp_commands") -local format_player_name = Commands.format_player_name_locale - -local Warnings = require("modules.exp_legacy.modules.control.warnings") --- @dep modules.control.warnings -local config = require("modules.exp_legacy.config.warnings") --- @dep config.warnings - ---- Gives a warning to a player; may lead to automatic script action. -Commands.new("create-warning", { "exp-commands_warnings.description-create" }) - :argument("player", { "exp-commands_warnings.arg-player-create" }, Commands.types.lower_role_player) - :argument("reason", { "exp-commands_warnings.arg-reason" }, Commands.types.string) - :enable_auto_concatenation() - :add_aliases{ "warn" } - :add_flags{ "admin_only" } - :register(function(player, other_player, reason) - --- @cast other_player LuaPlayer - --- @cast reason string - Warnings.add_warning(other_player, player.name, reason) - local player_name = format_player_name(player) - local other_player_name = format_player_name(other_player) - game.print{ "exp-commands_warnings.create", other_player_name, player_name, reason } - end) - ---- Gets a list of all warnings that a player has on them. If no player then lists all players and the number of warnings on them. -Commands.new("get-warnings", { "exp-commands_warnings.description-get" }) - :optional("player", { "exp-commands_warnings.arg-player-get" }, Commands.types.player) - :add_aliases{ "warnings" } - :add_flags{ "admin_only" } - :register(function(player, other_player) - --- @cast other_player LuaPlayer? - if other_player then - local warnings = Warnings.get_warnings(player) - local script_warnings = Warnings.get_script_warnings(player) - local other_player_name = format_player_name(other_player) - Commands.print{ "exp-commands_warnings.player-title", other_player_name, #warnings, #script_warnings, config.script_warning_limit } - for _, warning in pairs(warnings) do - local by_player_name_formatted = format_player_name(warning.by_player_name) - Commands.print{ "exp-commands_warnings.list-element-player", by_player_name_formatted, warning.reason } - end - else - local warnings = Warnings.user_warnings:get_all() - local script_warnings = Warnings.user_script_warnings - Commands.print{ "exp-commands_warnings.warnings-title" } - for player_name, player_warnings in pairs(warnings) do - local player_name_formatted = format_player_name(player_name) - local script_warning_count = script_warnings[player_name] and #script_warnings[player_name] or 0 - Commands.print{ "exp-commands_warnings.list-element", player_name_formatted, #player_warnings, script_warning_count, config.script_warning_limit } - end - for player_name, player_warnings in pairs(script_warnings) do - if not warnings[player_name] then - local player_name_formatted = format_player_name(player_name) - Commands.print{ "exp-commands_warnings.list-element", player_name_formatted, 0, #player_warnings, config.script_warning_limit } - end - end - end - end) - ---- Clears all warnings from a player -Commands.new("clear-warnings", { "exp-commands_warnings.description-clear" }) - :argument("player", { "exp-commands_warnings.arg-player-clear" }, Commands.types.player) - :add_flags{ "admin_only" } - :register(function(player, other_player) - --- @cast other_player LuaPlayer - Warnings.clear_warnings(other_player, player.name) - Warnings.clear_script_warnings(other_player) - local player_name = format_player_name(player) - local other_player_name = format_player_name(other_player) - game.print{ "exp-commands_warnings.cleared", other_player_name, player_name } - end) - ---- Clears all script warnings from a player -Commands.new("clear-script-warnings", { "exp-commands_warnings.description-clear-script" }) - :argument("player", { "exp-commands_warnings.arg-player-clear" }, Commands.types.player) - :add_flags{ "admin_only" } - :register(function(player, other_player) - --- @cast other_player LuaPlayer - Warnings.clear_script_warnings(other_player) - local player_name = format_player_name(player) - local other_player_name = format_player_name(other_player) - game.print{ "exp-commands_warnings.cleared-script", other_player_name, player_name } - end) - ---- Clears the last warning that was given to a player -Commands.new("clear-last-warnings", { "exp-commands_warnings.description-clear-last" }) - :argument("player", { "exp-commands_warnings.arg-player-clear" }, Commands.types.player) - :add_flags{ "admin_only" } - :register(function(player, other_player) - --- @cast other_player LuaPlayer - Warnings.remove_warning(other_player, player.name) - local player_name = format_player_name(player) - local other_player_name = format_player_name(other_player) - game.print{ "exp-commands_warnings.cleared-last", other_player_name, player_name } - end) diff --git a/exp_scenario/module/control.lua b/exp_scenario/module/control.lua index aee9623c..8bfc9107 100644 --- a/exp_scenario/module/control.lua +++ b/exp_scenario/module/control.lua @@ -38,7 +38,6 @@ require("modules/exp_scenario/commands/surface") require("modules/exp_scenario/commands/teleport") require("modules/exp_scenario/commands/trains") require("modules/exp_scenario/commands/vlayer") -require("modules/exp_scenario/commands/warnings") require("modules/exp_scenario/commands/waterfill") --- Control @@ -60,11 +59,13 @@ 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")) 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/deconstruction_log.lua b/exp_scenario/module/control/deconstruction_log.lua index ddfeb979..325bb3f8 100644 --- a/exp_scenario/module/control/deconstruction_log.lua +++ b/exp_scenario/module/control/deconstruction_log.lua @@ -3,6 +3,7 @@ Log certain actions into a file when events are triggered ]] local ExpUtil = require("modules/exp_util") +local Storage = require("modules/exp_util/storage") local Roles = require("modules/exp_roles") local config = require("modules.exp_legacy.config.deconlog") @@ -130,31 +131,79 @@ local function on_player_mined_entity(event) add_log_line(player, "mined_entity", format_entity(event.entity)) end ---- Log when rocket is fired +--- Ammo which is logged when fired +local logged_ammo = { + ["rocket"] = config.fired_rocket, + ["explosive-rocket"] = config.fired_explosive_rocket, + ["atomic-bomb"] = config.fired_nuke, +} + +--- @class ExpScenario_DeconstructionLog.AmmoSlot +--- @field name string +--- @field count number + +--- The last seen contents of each ammo slot, keyed by player index then slot index +local ammo_slots = {} --- @type table> +Storage.register(ammo_slots, function(tbl) + ammo_slots = tbl +end) + +--- Read the ammo slots of a player, empty while they have no character +--- @param player LuaPlayer +--- @return table +local function read_ammo_slots(player) + local slots = {} + if not player.character then return slots end + + local character_ammo = assert(player.get_inventory(defines.inventory.character_ammo)) + for index = 1, #character_ammo do + local stack = character_ammo[index --[[@as uint]]] + if stack.valid_for_read then + slots[index] = { name = stack.name, count = stack.count } + end + end + return slots +end + +--- Log a shot, there is no fired event so a slot losing one of the same ammo is taken as a shot --- @param event EventData.on_player_ammo_inventory_changed local function on_player_ammo_inventory_changed(event) local player = get_log_player(event) - if not player or not player.character then return end + if not player then return end - local character_ammo = assert(player.get_inventory(defines.inventory.character_ammo)) - local gun_index = player.character.selected_gun_index --[[@as uint]] - local item = character_ammo[gun_index] - if not item or not item.valid or not item.valid_for_read then - return + local previous_slots = ammo_slots[player.index] or {} + local slots = read_ammo_slots(player) + ammo_slots[player.index] = slots + + for index, previous in pairs(previous_slots) do + local current = slots[index] + local fired = nil --- @type string? + if current then + if previous.name == current.name and previous.count == current.count + 1 then + fired = current.name + end + elseif previous.count == 1 then + fired = previous.name + end + + if fired and logged_ammo[fired] then + add_log_line(player, "shot-" .. fired, format_position(player.physical_position), format_position(player.shooting_state.position)) + end end - - local action_name = "shot-" .. item.name - if not config.fired_rocket and action_name == "shot-rocket" then - return - elseif not config.fired_explosive_rocket and action_name == "shot-explosive-rocket" then - return - elseif not config.fired_nuke and action_name == "shot-atomic-bomb" then - return - end - - add_log_line(player, action_name, format_position(player.physical_position), format_position(player.shooting_state.position)) end +--- Read the ammo of a player when they join or get a new character, so the first shot afterwards is seen +--- @param event EventData.on_player_joined_game | EventData.on_player_respawned +local function on_player_character_changed(event) + local player = assert(game.get_player(event.player_index)) + ammo_slots[player.index] = read_ammo_slots(player) +end + +--- Forget the ammo of a player who left +--- @param event EventData.on_player_left_game +local function on_player_left_game(event) + ammo_slots[event.player_index] = nil +end local e = defines.events local events = { @@ -175,6 +224,9 @@ end if config.fired_rocket or config.fired_explosive_rocket or config.fired_nuke then events[e.on_player_ammo_inventory_changed] = on_player_ammo_inventory_changed + events[e.on_player_joined_game] = on_player_character_changed + events[e.on_player_respawned] = on_player_character_changed + events[e.on_player_left_game] = on_player_left_game end return { diff --git a/exp_scenario/module/control/discord_alerts.lua b/exp_scenario/module/control/discord_alerts.lua index c4675140..ff1fa5c9 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", @@ -131,44 +131,10 @@ if config.player_reports then end end ---- Warnings added and removed -if config.player_warnings then - local Warnings = require("modules.exp_legacy.modules.control.warnings") - events[Warnings.events.on_warning_added] = function(event) - local player_name, by_player_name = get_player_name(event) - local player = assert(game.get_player(player_name)) - emit_event{ - title = "Warning", - description = "A player has been given a warning", - color = Colors.yellow, - fields = { - { name = "Player", inline = true, value = append_playtime(player_name) }, - { name = "By", inline = true, value = append_playtime(by_player_name) }, - { name = "Report Count", inline = true, value = Warnings.count_warnings(player) }, - { name = "Reason", value = event.reason }, - }, - } - end - events[Warnings.events.on_warning_removed] = function(event) - if event.batch ~= 1 then return end - local player_name = get_player_name(event) - emit_event{ - title = "Warnings Removed", - description = "A player has a warning removed", - color = Colors.green, - fields = { - { name = "Player", inline = true, value = append_playtime(player_name) }, - { name = "By", inline = true, value = append_playtime(event.removed_by_name) }, - { name = "Report Count", inline = true, value = tostring(event.batch_count) }, - }, - } - end -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 +147,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..5bf07c40 --- /dev/null +++ b/exp_scenario/module/control/jail.lua @@ -0,0 +1,85 @@ +--[[-- 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 get_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 get_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 = 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 + 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, { + 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 = 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, { + player_index = player.index, + by_player_name = by_player_name, + }) + + return true +end + +return Jail diff --git a/exp_scenario/module/control/protection.lua b/exp_scenario/module/control/protection.lua new file mode 100644 index 00000000..9839290f --- /dev/null +++ b/exp_scenario/module/control/protection.lua @@ -0,0 +1,237 @@ +--[[-- 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 + + 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 + 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..6b7c2c50 100644 --- a/exp_scenario/module/control/protection_jail.lua +++ b/exp_scenario/module/control/protection_jail.lua @@ -4,8 +4,8 @@ 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 Jail = require("modules/exp_scenario/control/jail") +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, } } 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 diff --git a/exp_scenario/module/control/spectate.lua b/exp_scenario/module/control/spectate.lua new file mode 100644 index 00000000..b8ccb29b --- /dev/null +++ b/exp_scenario/module/control/spectate.lua @@ -0,0 +1,181 @@ +--[[-- 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/gui/surveillance.lua b/exp_scenario/module/gui/surveillance.lua index e9910723..11b1cd58 100644 --- a/exp_scenario/module/gui/surveillance.lua +++ b/exp_scenario/module/gui/surveillance.lua @@ -206,6 +206,78 @@ function Elements.camera.refresh_online() end end +--- Slot in the header showing what the player a camera follows holds in their cursor +--- @class ExpGui_Surveillance.elements.cursor_slot: ExpElement +--- @field data table The camera the slot belongs to +--- @overload fun(parent: LuaGuiElement, camera: LuaGuiElement): LuaGuiElement +Elements.cursor_slot = Gui.define("surveillance/cursor_slot") + :track_all_elements() + :draw{ + type = "sprite-button", + style = "slot_button", + tooltip = { "exp-gui_surveillance.tooltip-cursor-empty" }, + } + :style{ + height = 26, + width = 26, + margin = { -1, -1, -1, -2 }, + } + :element_data( + Gui.from_argument(1) + ) --[[@as any]] + +--- @class ExpGui_Surveillance.elements.cursor_slot.display_data +--- @field sprite SpritePath? Nil when the cursor is empty +--- @field number number? Nil for a ghost, which has no count +--- @field tooltip LocalisedString + +--- Calculate what a slot shows for the cursor of a player +--- @param player LuaPlayer +--- @return ExpGui_Surveillance.elements.cursor_slot.display_data +function Elements.cursor_slot.calculate_display_data(player) + local cursor_stack = player.cursor_stack + if cursor_stack and cursor_stack.valid_for_read then + return { + sprite = "item/" .. cursor_stack.name, + number = cursor_stack.count, + tooltip = { "exp-gui_surveillance.tooltip-cursor", cursor_stack.prototype.localised_name, cursor_stack.count }, + } + end + + local cursor_ghost = player.cursor_ghost --[[@as ItemIDAndQualityIDPair?]] + if cursor_ghost then + local prototype = cursor_ghost.name --[[@as LuaItemPrototype]] + return { + sprite = "item/" .. prototype.name, + tooltip = { "exp-gui_surveillance.tooltip-cursor-ghost", prototype.localised_name }, + } + end + + return { tooltip = { "exp-gui_surveillance.tooltip-cursor-empty" } } +end + +--- Refresh a slot, a ghost is shown greyed out and the slot is hidden when the camera is not following a player +--- @param cursor_slot LuaGuiElement +--- @param target_player LuaPlayer? +function Elements.cursor_slot.refresh(cursor_slot, target_player) + cursor_slot.visible = target_player ~= nil + if not target_player then return end + + local display_data = Elements.cursor_slot.calculate_display_data(target_player) + cursor_slot.sprite = display_data.sprite or "" + cursor_slot.number = display_data.number + cursor_slot.tooltip = display_data.tooltip + cursor_slot.enabled = display_data.sprite == nil or display_data.number ~= nil +end + +--- Refresh the slots of all online cameras +function Elements.cursor_slot.refresh_online() + for _, cursor_slot in Elements.cursor_slot:online_elements() do + local camera = Elements.cursor_slot.data[cursor_slot] + Elements.cursor_slot.refresh(cursor_slot, Elements.camera.data[camera]) + end +end + --- Container added to the screen Elements.container = Gui.define("surveillance/container") :draw(function(def, parent) @@ -214,6 +286,7 @@ Elements.container = Gui.define("surveillance/container") local target_player = Gui.get_player(parent) local camera = Elements.camera(screen_frame, target_player) + Elements.cursor_slot(button_flow, camera) local type_dropdown_data = { camera = camera, @@ -248,6 +321,7 @@ return { [e.on_tick] = Elements.camera.refresh_online, }, on_nth_tick = { + [10] = Elements.cursor_slot.refresh_online, [600] = Elements.type_dropdown.refresh_online, } } diff --git a/exp_scenario/module/locale/en.cfg b/exp_scenario/module/locale/en.cfg index 80e8a1ec..9052704f 100644 --- a/exp_scenario/module/locale/en.cfg +++ b/exp_scenario/module/locale/en.cfg @@ -249,25 +249,6 @@ description=Print all vlayer information. title=VLayer Information: result=__1__: __2__ -[exp-commands_warnings] -description-create=Gives a warning to a player; may lead to automatic script action. -description-get=Gets the number of warnings a player has. If no player then lists all players and the number of warnings they have. -description-clear=Clears all warnings (and script warnings) from a player. -description-clear-script=Clears all script warnings from a player. -description-clear-last=Clears the last warning from a player. -arg-player-create=Player to give the warning to. -arg-player-get=Player to get the warning of, if not given all players are returned. -arg-player-clear=Player to clear the warnings of. -arg-reason=Reason the user is receiving a warning. -create=__1__ received a warning from __2__ for __3__. -player-title=__1__ has __2__ warnings and __3__/__4__ script warnings. -list-element-player=__1__: __2__ -warnings-title=The following players have warnings aginst them (and script warnings): -list-element=__1__: __2__ (__3__/__4__) -cleared=__1__ had all their warnings cleared by __2__. -cleared-script=__1__ had all their script warnings cleared by __2__. -cleared-last=__1__ had their last warning cleared by __2__. - [exp-commands_waterfill] description=Replace tiles with shallow water. requires-explosives=__ITEM__cliff-explosives__ are required to create water. @@ -326,7 +307,6 @@ reason-entry=Enter Reason goto-player=Goto player bring-player=Bring player report-player=Report player -warn-player=Warn player jail-player=Jail player kick-player=Kick player ban-player=Ban player @@ -489,6 +469,9 @@ caption-set-location=Set type-player=Player type-static=Static type-loop=Loop +tooltip-cursor=__1__ x__2__ +tooltip-cursor-ghost=__1__ (ghost) +tooltip-cursor-empty=Empty cursor [exp-gui_task-list] caption-main=Task List [img=info] @@ -577,3 +560,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. diff --git a/exp_scenario/module/locale/zh-CN.cfg b/exp_scenario/module/locale/zh-CN.cfg index bf65db42..888163af 100644 --- a/exp_scenario/module/locale/zh-CN.cfg +++ b/exp_scenario/module/locale/zh-CN.cfg @@ -246,25 +246,6 @@ description=vlayer 資訊 title=vlayer 資訊: result=__1__: __2__ -[exp-commands_warnings] -description-create=給用戶一個警告; 可能會導致系統的自動行動。 -description-get=取得用戶收到的警告次數。如果沒有用戶,則列出所有用戶以及他們受到警告的次數。 -description-clear=清除用戶的所有警告(和系統警告)。 -description-clear-script=清除用戶的系統警告。 -description-clear-last=清除用戶的最後警告。 -arg-player-create=要警告的用戶。 -arg-player-get=要取得的用戶, 若沒有則返回所有用戶。 -arg-player-clear=要清除的用戶。 -arg-reason=原因。 -create=__1__ 被 __2__ 因 __3__ 作出警告。 -player-title=__1__ 有 __2__ 個警告和 __3__/__4__ 的系統警告。 -list-element-player=__1__: __2__ -warnings-title=該用戶警告如下: -list-element=__1__: __2__ (__3__/__4__) -cleared=__1__ 的警告己被 __2__ 清除。 -cleared-script=__1__ 的系統警告己被 __2__ 清除。 -cleared-last=__1__ 的最後警告己被 __2__ 清除。 - [exp-commands_waterfill] description=把地換為淺水。 requires-explosives=沒有足夠的 __ITEM__cliff-explosives__ 。 @@ -474,6 +455,9 @@ caption-set-location=設 type-player=用戶 type-static=靜態 type-loop=循環 +tooltip-cursor=__1__ x__2__ +tooltip-cursor-ghost=__1__ (幻影) +tooltip-cursor-empty=游標為空 [exp-gui_task-list] caption-main=工作流程 [img=info] diff --git a/exp_scenario/module/locale/zh-TW.cfg b/exp_scenario/module/locale/zh-TW.cfg index bf65db42..888163af 100644 --- a/exp_scenario/module/locale/zh-TW.cfg +++ b/exp_scenario/module/locale/zh-TW.cfg @@ -246,25 +246,6 @@ description=vlayer 資訊 title=vlayer 資訊: result=__1__: __2__ -[exp-commands_warnings] -description-create=給用戶一個警告; 可能會導致系統的自動行動。 -description-get=取得用戶收到的警告次數。如果沒有用戶,則列出所有用戶以及他們受到警告的次數。 -description-clear=清除用戶的所有警告(和系統警告)。 -description-clear-script=清除用戶的系統警告。 -description-clear-last=清除用戶的最後警告。 -arg-player-create=要警告的用戶。 -arg-player-get=要取得的用戶, 若沒有則返回所有用戶。 -arg-player-clear=要清除的用戶。 -arg-reason=原因。 -create=__1__ 被 __2__ 因 __3__ 作出警告。 -player-title=__1__ 有 __2__ 個警告和 __3__/__4__ 的系統警告。 -list-element-player=__1__: __2__ -warnings-title=該用戶警告如下: -list-element=__1__: __2__ (__3__/__4__) -cleared=__1__ 的警告己被 __2__ 清除。 -cleared-script=__1__ 的系統警告己被 __2__ 清除。 -cleared-last=__1__ 的最後警告己被 __2__ 清除。 - [exp-commands_waterfill] description=把地換為淺水。 requires-explosives=沒有足夠的 __ITEM__cliff-explosives__ 。 @@ -474,6 +455,9 @@ caption-set-location=設 type-player=用戶 type-static=靜態 type-loop=循環 +tooltip-cursor=__1__ x__2__ +tooltip-cursor-ghost=__1__ (幻影) +tooltip-cursor-empty=游標為空 [exp-gui_task-list] caption-main=工作流程 [img=info] diff --git a/exp_scenario/permissions.ts b/exp_scenario/permissions.ts index 9d604b58..bbaafacd 100644 --- a/exp_scenario/permissions.ts +++ b/exp_scenario/permissions.ts @@ -26,25 +26,20 @@ const definitions: Definition[] = [ ["exp_scenario.command.clear_blueprints_surface", "/clear-blueprints-surface", "Clear all blueprints on the current surface."], ["exp_scenario.command.clear_ground_items", "/clear-ground-items", "Clear all items on the ground."], ["exp_scenario.command.clear_inventory", "/clear-inventory", "Clear a player's inventory, moving all items to spawn."], - ["exp_scenario.command.clear_last_warnings", "/clear-last-warnings", "Clears the last warning from a player."], ["exp_scenario.command.clear_pollution", "/clear-pollution", "Clear pollution from your current surface, or another surface."], ["exp_scenario.command.clear_reports", "/clear-reports", "Clears all reports from a player or just the report from one player."], - ["exp_scenario.command.clear_script_warnings", "/clear-script-warnings", "Clears all script warnings from a player."], - ["exp_scenario.command.clear_warnings", "/clear-warnings", "Clears all warnings (and script warnings) from a player."], ["exp_scenario.command.collectdata", "/collectdata", "Collect data for RCON usage."], ["exp_scenario.command.commands", "/commands", "List and search all commands for a keyword.", true], ["exp_scenario.command.connect", "/connect", "Connect to another server.", true], ["exp_scenario.command.connect_all", "/connect-all", "Connect all players to another server."], ["exp_scenario.command.connect_player", "/connect-player", "Connect a player to a different server."], ["exp_scenario.command.create_report", "/create-report", "Reports a player and notifies moderators.", true], - ["exp_scenario.command.create_warning", "/create-warning", "Gives a warning to a player; may lead to automatic script action."], ["exp_scenario.command.data_preference", "/data-preference", "Allows you to set/get your data saving preference.", true], ["exp_scenario.command.debug", "/debug", "Opens the debug gui."], ["exp_scenario.command.follow", "/follow", "Start following a player in spectator."], ["exp_scenario.command.get_home", "/get-home", "Returns your current home location."], ["exp_scenario.command.get_reports", "/get-reports", "List the reports against a player, or against every player."], ["exp_scenario.command.get_roles", "/get-roles", "Get all roles that a player has, if no player provided it lists all roles.", true], - ["exp_scenario.command.get_warnings", "/get-warnings", "List the warnings against a player, or against every player."], ["exp_scenario.command.goto", "/goto", "Teleports you to a player."], ["exp_scenario.command.home", "/home", "Teleports you to your home location."], ["exp_scenario.command.jail", "/jail", "Puts a player into jail, which suppresses all of their other roles."],