From 82ab14da3ac73e5edd61a4cc8090a96174ddaa89 Mon Sep 17 00:00:00 2001 From: beck-thompson <107373427+beck-thompson@users.noreply.github.com> Date: Sun, 26 Oct 2025 19:40:07 -0700 Subject: [PATCH] Admin alerts now link players with tpto (#40472) * Admin alerts now link players with tpto * Add coords * Slarti tweaks! * He saw my minor spelling mistake - its over... --- Content.Client/Chat/Managers/ChatManager.cs | 5 + .../Administration/Logs/AdminLogManager.cs | 104 +++++++++++++++++- Content.Server/Chat/Managers/ChatManager.cs | 12 +- .../EntitySystems/ExplosionSystem.cs | 7 +- Content.Shared/Chat/ISharedChatManager.cs | 22 ++++ .../en-US/administration/admin-alerts.ftl | 2 + 6 files changed, 145 insertions(+), 7 deletions(-) diff --git a/Content.Client/Chat/Managers/ChatManager.cs b/Content.Client/Chat/Managers/ChatManager.cs index 68707e021c..1b66bf8732 100644 --- a/Content.Client/Chat/Managers/ChatManager.cs +++ b/Content.Client/Chat/Managers/ChatManager.cs @@ -31,6 +31,11 @@ internal sealed class ChatManager : IChatManager // See server-side manager. This just exists for shared code. } + public void SendAdminAlertNoFormatOrEscape(string message) + { + // See server-side manager. This just exists for shared code. + } + public void SendMessage(string text, ChatSelectChannel channel) { var str = text.ToString(); diff --git a/Content.Server/Administration/Logs/AdminLogManager.cs b/Content.Server/Administration/Logs/AdminLogManager.cs index e7682cf559..2587d4b8f9 100644 --- a/Content.Server/Administration/Logs/AdminLogManager.cs +++ b/Content.Server/Administration/Logs/AdminLogManager.cs @@ -12,13 +12,16 @@ using Content.Shared.Database; using Content.Shared.Mind; using Content.Shared.Players.PlayTimeTracking; using Prometheus; +using Robust.Server.GameObjects; using Robust.Shared; using Robust.Shared.Configuration; +using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Content.Server.Administration.Logs; @@ -338,7 +341,7 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa Players = players, }; - DoAdminAlerts(players, message, impact); + DoAdminAlerts(players, message, impact, handler); if (preRound) { @@ -380,6 +383,34 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa return players; } + /// + /// Get a list of coordinates from the s values. Will transform all coordinate types + /// to map coordinates! + /// + /// A list of map coordinates that were found in the value input, can return an empty list. + private List GetCoordinates(Dictionary values) + { + List coordList = new(); + EntityManager.TrySystem(out TransformSystem? transform); + + foreach (var value in values.Values) + { + switch (value) + { + case EntityCoordinates entCords: + if (transform != null) + coordList.Add(transform.ToMapCoordinates(entCords)); + continue; + + case MapCoordinates mapCord: + coordList.Add(mapCord); + continue; + } + } + + return coordList; + } + private void AddPlayer(List players, Guid user, int logId) { // The majority of logs have a single player, or maybe two. Instead of allocating a List and @@ -397,10 +428,11 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa }); } - private void DoAdminAlerts(List players, string message, LogImpact impact) + private void DoAdminAlerts(List players, string message, LogImpact impact, LogStringHandler handler) { var adminLog = false; var logMessage = message; + var playerNetEnts = new List<(NetEntity, string)>(); foreach (var player in players) { @@ -419,6 +451,8 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa ("name", cachedInfo.CharacterName), ("subtype", subtype)); } + if (cachedInfo != null && cachedInfo.NetEntity != null) + playerNetEnts.Add((cachedInfo.NetEntity.Value, cachedInfo.CharacterName)); } if (adminLog) @@ -442,7 +476,73 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa } if (adminLog) + { _chat.SendAdminAlert(logMessage); + + if (CreateTpLinks(playerNetEnts, out var tpLinks)) + _chat.SendAdminAlertNoFormatOrEscape(tpLinks); + + var coords = GetCoordinates(handler.Values); + + if (CreateCordLinks(coords, out var cordLinks)) + _chat.SendAdminAlertNoFormatOrEscape(cordLinks); + } + } + + /// + /// Creates a list of tpto command links of the given players + /// + private bool CreateTpLinks(List<(NetEntity NetEnt, string CharacterName)> players, out string outString) + { + outString = string.Empty; + + if (players.Count == 0) + return false; + + outString = Loc.GetString("admin-alert-tp-to-players-header"); + + for (var i = 0; i < players.Count; i++) + { + var player = players[i]; + outString += $"[cmdlink=\"{EscapeText(player.CharacterName)}\" command=\"tpto {player.NetEnt}\"/]"; + + if (i < players.Count - 1) + outString += ", "; + } + + return true; + } + + /// + /// Creates a list of toto command links for the given map coordinates. + /// + private bool CreateCordLinks(List cords, out string outString) + { + outString = string.Empty; + + if (cords.Count == 0) + return false; + + outString = Loc.GetString("admin-alert-tp-to-coords-header"); + + for (var i = 0; i < cords.Count; i++) + { + var cord = cords[i]; + outString += $"[cmdlink=\"{cord.ToString()}\" command=\"tp {cord.X} {cord.Y} {cord.MapId}\"/]"; + + if (i < cords.Count - 1) + outString += ", "; + } + + return true; + } + + /// + /// Escape the given text to not allow breakouts of the cmdlink tags. + /// + private string EscapeText(string text) + { + return FormattedMessage.EscapeText(text).Replace("\"", "\\\"").Replace("'", "\\'"); } public async Task> All(LogFilter? filter = null, Func>? listProvider = null) diff --git a/Content.Server/Chat/Managers/ChatManager.cs b/Content.Server/Chat/Managers/ChatManager.cs index c62a10ada3..f90e286d9e 100644 --- a/Content.Server/Chat/Managers/ChatManager.cs +++ b/Content.Server/Chat/Managers/ChatManager.cs @@ -160,14 +160,20 @@ internal sealed partial class ChatManager : IChatManager public void SendAdminAlert(string message) { - var clients = _adminManager.ActiveAdmins.Select(p => p.Channel); - var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message", ("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message))); - ChatMessageToMany(ChatChannel.AdminAlert, message, wrappedMessage, default, false, true, clients); + SendAdminAlertNoFormatOrEscape(wrappedMessage); } + public void SendAdminAlertNoFormatOrEscape(string message) + { + var clients = _adminManager.ActiveAdmins.Select(p => p.Channel); + + ChatMessageToMany(ChatChannel.AdminAlert, message, message, default, false, true, clients); + } + + public void SendAdminAlert(EntityUid player, string message) { var mindSystem = _entityManager.System(); diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs index 67dbe97b29..198db3eca1 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs @@ -256,11 +256,14 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem var logImpact = (alertMinExplosionIntensity > -1 && totalIntensity >= alertMinExplosionIntensity) ? LogImpact.Extreme : LogImpact.High; - _adminLogger.Add(LogType.Explosion, logImpact, - $"{ToPrettyString(user.Value):user} caused {ToPrettyString(uid):entity} to explode ({typeId}) at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not found]")} with intensity {totalIntensity} slope {slope}"); + if (posFound) + _adminLogger.Add(LogType.Explosion, logImpact, $"{ToPrettyString(user.Value):user} caused {ToPrettyString(uid):entity} to explode ({typeId}) at Pos:{gridPos:coordinates} with intensity {totalIntensity} slope {slope}"); + else + _adminLogger.Add(LogType.Explosion, logImpact, $"{ToPrettyString(user.Value):user} caused {ToPrettyString(uid):entity} to explode ({typeId}) at Pos:[Grid or Map not found] with intensity {totalIntensity} slope {slope}"); } } + /// /// Queue an explosion, with a specified epicenter and set of starting tiles. /// diff --git a/Content.Shared/Chat/ISharedChatManager.cs b/Content.Shared/Chat/ISharedChatManager.cs index 39c1d85dd2..76fb4fbea8 100644 --- a/Content.Shared/Chat/ISharedChatManager.cs +++ b/Content.Shared/Chat/ISharedChatManager.cs @@ -3,6 +3,28 @@ namespace Content.Shared.Chat; public interface ISharedChatManager { void Initialize(); + + /// + /// Send an admin alert to the admin chat channel. + /// + /// The message to send. void SendAdminAlert(string message); + + /// + /// Send an admin alert to the admin chat channel specifically about the given player. + /// Will include info extra like their antag status and name. + /// + /// The player that the message is about. + /// The message to send. void SendAdminAlert(EntityUid player, string message); + + /// + /// This is a dangerous function! Only pass in property escaped text. + /// See: + ///

+ /// Use this for things that need to be unformatted (like tpto links) but ensure that everything else + /// is formated properly. If it's not, players could sneak in ban links or other nasty commands that the admins + /// could clink on. + ///
+ void SendAdminAlertNoFormatOrEscape(string message); } diff --git a/Resources/Locale/en-US/administration/admin-alerts.ftl b/Resources/Locale/en-US/administration/admin-alerts.ftl index 931c3766a7..512c654650 100644 --- a/Resources/Locale/en-US/administration/admin-alerts.ftl +++ b/Resources/Locale/en-US/administration/admin-alerts.ftl @@ -2,3 +2,5 @@ admin-alert-ipintel-blocked = {$player} was rejected from joining due to their IP having a {TOSTRING($percent, "P2")} confidence of being a VPN/Datacenter. admin-alert-ipintel-warning = {$player} IP has a {TOSTRING($percent, "P2")} confidence of being a VPN/Datacenter. Please watch them. admin-alert-antag-label = {$message} [ANTAG: {$name}, {$subtype}] +admin-alert-tp-to-players-header = Players:{" "} +admin-alert-tp-to-coords-header = Coords:{" "}