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:{" "}