]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add admin Erase verb, add checkbox to erase from the ban panel (#20985)
authorDrSmugleaf <DrSmugleaf@users.noreply.github.com>
Sat, 14 Oct 2023 09:02:56 +0000 (02:02 -0700)
committerGitHub <noreply@github.com>
Sat, 14 Oct 2023 09:02:56 +0000 (02:02 -0700)
22 files changed:
Content.Client/Administration/UI/BanPanel/BanPanel.xaml
Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
Content.Client/Administration/UI/BanPanel/BanPanelEui.cs
Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs
Content.Server/Administration/BanPanelEui.cs
Content.Server/Administration/Systems/AdminSystem.cs
Content.Server/Administration/Systems/AdminVerbSystem.cs
Content.Server/Chat/Managers/ChatManager.cs
Content.Server/Chat/Managers/IChatManager.cs
Content.Server/Chat/Systems/ChatSystem.cs
Content.Server/CrewManifest/CrewManifestSystem.cs
Content.Server/Radio/EntitySystems/RadioSystem.cs
Content.Server/StationRecords/Systems/GeneralStationRecordConsoleSystem.cs
Content.Server/StationRecords/Systems/StationRecordsSystem.cs
Content.Shared/Administration/BanPanelEuiState.cs
Content.Shared/Chat/MsgChatMessage.cs
Content.Shared/Chat/MsgDeleteChatMessagesBy.cs [new file with mode: 0644]
Resources/Changelog/Admin.yml
Resources/Locale/en-US/administration/admin-verbs.ftl
Resources/Locale/en-US/administration/ui/admin-erase.ftl [new file with mode: 0644]
Resources/Locale/en-US/info/ban.ftl

index b8f91e050ea1f490adff12471ffd566c1a52b30b..333184f1c0e40282c3efd9cc655f748dff6667e2 100644 (file)
@@ -22,6 +22,7 @@
                     <LineEdit Name="HwidLine" MinWidth="100" HorizontalExpand="True" PlaceHolder="{Loc ban-panel-hwid}" ToolTip="{Loc ban-panel-ip-hwid-tooltip}" />
                 </BoxContainer>
                 <CheckBox Name="LastConnCheckbox" Margin="2" Text="{Loc ban-panel-last-conn}" Pressed="True" />
+                <CheckBox Name="EraseCheckbox" Margin="2" Text="{Loc ban-panel-erase}" Pressed="False" />
                 <BoxContainer Orientation="Horizontal" Margin="2">
                     <LineEdit Name="TimeLine" MaxWidth="150" MinWidth="70" PlaceHolder="0" />
                     <OptionButton Name="MultiplierOption" />
@@ -30,7 +31,7 @@
                 </BoxContainer>
                 <BoxContainer Orientation="Horizontal" Margin="4">
                     <OptionButton Name="TypeOption" />
-                    <Control MinWidth="30"></Control>
+                    <Control MinWidth="30" />
                     <Label Text="{Loc ban-panel-severity}" />
                     <OptionButton Name="SeverityOption" />
                 </BoxContainer>
index e6d122766e64547b01ad546998babb92048bed04..1f32640f7ddffae85634eafca0e4445aeb22a1e9 100644 (file)
@@ -1,10 +1,7 @@
-using System.Globalization;
 using System.Linq;
 using System.Net;
 using System.Net.Sockets;
-using System.Text.RegularExpressions;
 using Content.Client.Administration.UI.CustomControls;
-using Content.Client.Stylesheets;
 using Content.Shared.Administration;
 using Content.Shared.Database;
 using Content.Shared.Roles;
@@ -23,7 +20,7 @@ namespace Content.Client.Administration.UI.BanPanel;
 [GenerateTypedNameReferences]
 public sealed partial class BanPanel : DefaultWindow
 {
-    public event Action<string?, (IPAddress, int)?, bool, byte[]?, bool, uint, string, NoteSeverity, string[]?>? BanSubmitted;
+    public event Action<string?, (IPAddress, int)?, bool, byte[]?, bool, uint, string, NoteSeverity, string[]?, bool>? BanSubmitted;
     public event Action<string>? PlayerChanged;
     private string? PlayerUsername { get; set; }
     private (IPAddress, int)? IpAddress { get; set; }
@@ -441,7 +438,8 @@ public sealed partial class BanPanel : DefaultWindow
         var useLastIp = IpCheckbox.Pressed && LastConnCheckbox.Pressed && IpAddress is null;
         var useLastHwid = HwidCheckbox.Pressed && LastConnCheckbox.Pressed && Hwid is null;
         var severity = (NoteSeverity) SeverityOption.SelectedId;
-        BanSubmitted?.Invoke(player, IpAddress, useLastIp, Hwid, useLastHwid, (uint) (TimeEntered * Multiplier), reason, severity, roles);
+        var erase = EraseCheckbox.Pressed;
+        BanSubmitted?.Invoke(player, IpAddress, useLastIp, Hwid, useLastHwid, (uint) (TimeEntered * Multiplier), reason, severity, roles, erase);
     }
 
     protected override void FrameUpdate(FrameEventArgs args)
index 0a7d88f65db3747d9a2eae7e017ed9411448ac35..940a55e010a4372dc64f9e67cb17d36eb684e996 100644 (file)
@@ -1,8 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
 using Content.Client.Eui;
 using Content.Shared.Administration;
 using Content.Shared.Eui;
@@ -19,8 +14,8 @@ public sealed class BanPanelEui : BaseEui
     {
         BanPanel = new BanPanel();
         BanPanel.OnClose += () => SendMessage(new CloseEuiMessage());
-        BanPanel.BanSubmitted += (player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles)
-            => SendMessage(new BanPanelEuiStateMsg.CreateBanRequest(player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles));
+        BanPanel.BanSubmitted += (player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles, erase)
+            => SendMessage(new BanPanelEuiStateMsg.CreateBanRequest(player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles, erase));
         BanPanel.PlayerChanged += player => SendMessage(new BanPanelEuiStateMsg.GetPlayerInfoRequest(player));
     }
 
index 78193b5aec68107b98f9a28331515681348525fa..6d86e458c5c846b24d28decc8db46758ae09ca4d 100644 (file)
@@ -9,7 +9,6 @@ using Content.Client.Chat.UI;
 using Content.Client.Examine;
 using Content.Client.Gameplay;
 using Content.Client.Ghost;
-using Content.Client.Lobby.UI;
 using Content.Client.UserInterface.Screens;
 using Content.Client.UserInterface.Systems.Chat.Widgets;
 using Content.Client.UserInterface.Systems.Gameplay;
@@ -31,7 +30,6 @@ using Robust.Shared.Configuration;
 using Robust.Shared.Input.Binding;
 using Robust.Shared.Map;
 using Robust.Shared.Network;
-using Robust.Shared.Random;
 using Robust.Shared.Replays;
 using Robust.Shared.Timing;
 using Robust.Shared.Utility;
@@ -135,7 +133,8 @@ public sealed class ChatUIController : UIController
     /// </summary>
     private readonly Dictionary<ChatChannel, int> _unreadMessages = new();
 
-    public readonly List<(GameTick, ChatMessage)> History = new();
+    // TODO add a cap for this for non-replays
+    public readonly List<(GameTick Tick, ChatMessage Msg)> History = new();
 
     // Maintains which channels a client should be able to filter (for showing in the chatbox)
     // and select (for attempting to send on).
@@ -166,6 +165,7 @@ public sealed class ChatUIController : UIController
         _player.LocalPlayerChanged += OnLocalPlayerChanged;
         _state.OnStateChanged += StateChanged;
         _net.RegisterNetMessage<MsgChatMessage>(OnChatMessage);
+        _net.RegisterNetMessage<MsgDeleteChatMessagesBy>(OnDeleteChatMessagesBy);
         SubscribeNetworkEvent<DamageForceSayEvent>(OnDamageForceSay);
 
         _speechBubbleRoot = new LayoutContainer();
@@ -867,6 +867,16 @@ public sealed class ChatUIController : UIController
         }
     }
 
+    public void OnDeleteChatMessagesBy(MsgDeleteChatMessagesBy msg)
+    {
+        // This will delete messages from an entity even if different players were the author.
+        // Usages of the erase admin verb should be rare enough that this does not matter.
+        // Otherwise the client would need to know that one entity has multiple author players,
+        // or the server would need to track when and which entities a player sent messages as.
+        History.RemoveAll(h => h.Msg.SenderKey == msg.Key || msg.Entities.Contains(h.Msg.SenderEntity));
+        Repopulate();
+    }
+
     public void RegisterChat(ChatBox chat)
     {
         _chats.Add(chat);
index 09647bb5832a04ee6dbd4da0e84bb971fac354e8..56c0c2863482302ce6631501c1d43e4515f525f8 100644 (file)
@@ -1,5 +1,3 @@
-using Content.Client.Chat;
-using Content.Client.Chat.TypingIndicator;
 using Content.Client.UserInterface.Systems.Chat.Controls;
 using Content.Shared.Chat;
 using Content.Shared.Input;
@@ -54,14 +52,12 @@ public partial class ChatBox : UIWidget
             return;
         }
 
-        if (msg is { Read: false, AudioPath: { } })
+        if (msg is { Read: false, AudioPath: not null })
             SoundSystem.Play(msg.AudioPath, Filter.Local(), new AudioParams().WithVolume(msg.AudioVolume));
 
         msg.Read = true;
 
-        var color = msg.MessageColorOverride != null
-            ? msg.MessageColorOverride.Value
-            : msg.Channel.TextColor();
+        var color = msg.MessageColorOverride ?? msg.Channel.TextColor();
 
         AddLine(msg.WrappedMessage, color);
     }
index f4a1a308d0b69919b86288340eaa6e7052bd2ef6..2e6dfab18adce86b0246d0737f8c2f597ffb78a4 100644 (file)
@@ -2,22 +2,29 @@ using System.Collections.Immutable;
 using System.Net;
 using System.Net.Sockets;
 using Content.Server.Administration.Managers;
+using Content.Server.Administration.Systems;
 using Content.Server.Chat.Managers;
 using Content.Server.EUI;
 using Content.Shared.Administration;
 using Content.Shared.Database;
 using Content.Shared.Eui;
+using Robust.Server.Player;
 using Robust.Shared.Network;
 
 namespace Content.Server.Administration;
 
-public sealed class BanPanelEui : BaseEui
+public sealed class BanPanelEui : BaseEui, IPostInjectInit
 {
     [Dependency] private readonly IBanManager _banManager = default!;
+    [Dependency] private readonly IEntityManager _entities = default!;
+    [Dependency] private readonly ILogManager _log = default!;
     [Dependency] private readonly IPlayerLocator _playerLocator = default!;
+    [Dependency] private readonly IPlayerManager _playerManager = default!;
     [Dependency] private readonly IChatManager _chat = default!;
     [Dependency] private readonly IAdminManager _admins = default!;
 
+    private ISawmill _sawmill = default!;
+
     private NetUserId? PlayerId { get; set; }
     private string PlayerName { get; set; } = string.Empty;
     private IPAddress? LastAddress { get; set; }
@@ -41,7 +48,7 @@ public sealed class BanPanelEui : BaseEui
         switch (msg)
         {
             case BanPanelEuiStateMsg.CreateBanRequest r:
-                BanPlayer(r.Player, r.IpAddress, r.UseLastIp, r.Hwid?.ToImmutableArray(), r.UseLastHwid, r.Minutes, r.Severity, r.Reason, r.Roles);
+                BanPlayer(r.Player, r.IpAddress, r.UseLastIp, r.Hwid?.ToImmutableArray(), r.UseLastHwid, r.Minutes, r.Severity, r.Reason, r.Roles, r.Erase);
                 break;
             case BanPanelEuiStateMsg.GetPlayerInfoRequest r:
                 ChangePlayer(r.PlayerUsername);
@@ -49,11 +56,11 @@ public sealed class BanPanelEui : BaseEui
         }
     }
 
-    private async void BanPlayer(string? target, string? ipAddressString, bool useLastIp, ImmutableArray<byte>? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection<string>? roles)
+    private async void BanPlayer(string? target, string? ipAddressString, bool useLastIp, ImmutableArray<byte>? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection<string>? roles, bool erase)
     {
         if (!_admins.HasAdminFlag(Player, AdminFlags.Ban))
         {
-            Logger.WarningS("admin.bans_eui", $"{Player.Name} ({Player.UserId}) tried to create a ban with no ban flag");
+            _sawmill.Warning($"{Player.Name} ({Player.UserId}) tried to create a ban with no ban flag");
             return;
         }
         if (target == null && string.IsNullOrWhiteSpace(ipAddressString) && hwid == null)
@@ -120,7 +127,23 @@ public sealed class BanPanelEui : BaseEui
             return;
         }
 
+        if (erase &&
+            targetUid != null &&
+            _playerManager.TryGetSessionById(targetUid.Value, out var targetPlayer))
+        {
+            try
+            {
+                if (_entities.TrySystem(out AdminSystem? adminSystem))
+                    adminSystem.Erase(targetPlayer);
+            }
+            catch (Exception e)
+            {
+                _sawmill.Error($"Error while erasing banned player:\n{e}");
+            }
+        }
+
         _banManager.CreateServerBan(targetUid, target, Player.UserId, addressRange, targetHWid, minutes, severity, reason);
+
         Close();
     }
 
@@ -160,4 +183,9 @@ public sealed class BanPanelEui : BaseEui
 
         StateDirty();
     }
+
+    public void PostInject()
+    {
+        _sawmill = _log.GetSawmill("admin.bans_eui");
+    }
 }
index e1d769b1b2471c3d21cdfb6d317911efc94af761..8a2b6d5119417e4d14d16394ea36377f49529f7f 100644 (file)
@@ -1,17 +1,28 @@
 using System.Linq;
 using Content.Server.Administration.Managers;
 using Content.Server.Chat.Managers;
+using Content.Server.Forensics;
+using Content.Server.GameTicking;
+using Content.Server.Hands.Systems;
 using Content.Server.IdentityManagement;
 using Content.Server.Mind;
 using Content.Server.Players.PlayTimeTracking;
+using Content.Server.Popups;
+using Content.Server.StationRecords.Systems;
 using Content.Shared.Administration;
 using Content.Shared.Administration.Events;
 using Content.Shared.CCVar;
 using Content.Shared.GameTicking;
+using Content.Shared.Hands.Components;
 using Content.Shared.IdentityManagement;
+using Content.Shared.Inventory;
+using Content.Shared.PDA;
 using Content.Shared.Players.PlayTimeTracking;
+using Content.Shared.Popups;
 using Content.Shared.Roles;
 using Content.Shared.Roles.Jobs;
+using Content.Shared.StationRecords;
+using Content.Shared.Throwing;
 using Robust.Server.GameObjects;
 using Robust.Server.Player;
 using Robust.Shared.Configuration;
@@ -27,10 +38,17 @@ namespace Content.Server.Administration.Systems
         [Dependency] private readonly IChatManager _chat = default!;
         [Dependency] private readonly IConfigurationManager _config = default!;
         [Dependency] private readonly IPlayerManager _playerManager = default!;
-        [Dependency] private readonly PlayTimeTrackingManager _playTime = default!;
+        [Dependency] private readonly HandsSystem _hands = default!;
         [Dependency] private readonly SharedJobSystem _jobs = default!;
+        [Dependency] private readonly InventorySystem _inventory = default!;
         [Dependency] private readonly MindSystem _minds = default!;
+        [Dependency] private readonly PopupSystem _popup = default!;
+        [Dependency] private readonly PhysicsSystem _physics = default!;
+        [Dependency] private readonly PlayTimeTrackingManager _playTime = default!;
         [Dependency] private readonly SharedRoleSystem _role = default!;
+        [Dependency] private readonly GameTicker _gameTicker = default!;
+        [Dependency] private readonly StationRecordsSystem _stationRecords = default!;
+        [Dependency] private readonly TransformSystem _transform = default!;
 
         private readonly Dictionary<NetUserId, PlayerInfo> _playerList = new();
 
@@ -299,5 +317,76 @@ namespace Content.Server.Administration.Systems
                 RaiseNetworkEvent(ev, admin);
             }
         }
+
+        /// <summary>
+        ///     Erases a player from the round.
+        ///     This removes them and any trace of them from the round, deleting their
+        ///     chat messages and showing a popup to other players.
+        ///     Their items are dropped on the ground.
+        /// </summary>
+        public void Erase(IPlayerSession player)
+        {
+            var entity = player.AttachedEntity;
+            _chat.DeleteMessagesBy(player);
+
+            if (entity != null && !TerminatingOrDeleted(entity.Value))
+            {
+                if (TryComp(entity.Value, out TransformComponent? transform))
+                {
+                    var coordinates = _transform.GetMoverCoordinates(entity.Value, transform);
+                    var name = Identity.Entity(entity.Value, EntityManager);
+                    _popup.PopupCoordinates(Loc.GetString("admin-erase-popup", ("user", name)), coordinates, PopupType.LargeCaution);
+                }
+
+                foreach (var item in _inventory.GetHandOrInventoryEntities(entity.Value))
+                {
+                    if (TryComp(item, out PdaComponent? pda) &&
+                        TryComp(pda.ContainedId, out StationRecordKeyStorageComponent? keyStorage) &&
+                        keyStorage.Key is { } key &&
+                        _stationRecords.TryGetRecord(key.OriginStation, key, out GeneralStationRecord? record))
+                    {
+                        if (TryComp(entity, out DnaComponent? dna) &&
+                            dna.DNA != record.DNA)
+                        {
+                            continue;
+                        }
+
+                        if (TryComp(entity, out FingerprintComponent? fingerPrint) &&
+                            fingerPrint.Fingerprint != record.Fingerprint)
+                        {
+                            continue;
+                        }
+
+                        _stationRecords.RemoveRecord(key.OriginStation, key);
+                        Del(item);
+                    }
+                }
+
+                if (TryComp(entity.Value, out InventoryComponent? inventory) &&
+                    _inventory.TryGetSlots(entity.Value, out var slots, inventory))
+                {
+                    foreach (var slot in slots)
+                    {
+                        if (_inventory.TryUnequip(entity.Value, entity.Value, slot.Name, out var item, true, true))
+                        {
+                            _physics.ApplyAngularImpulse(item.Value, ThrowingSystem.ThrowAngularImpulse);
+                        }
+                    }
+                }
+
+                if (TryComp(entity.Value, out HandsComponent? hands))
+                {
+                    foreach (var hand in _hands.EnumerateHands(entity.Value, hands))
+                    {
+                        _hands.TryDrop(entity.Value, hand, checkActionBlocker: false, doDropInteraction: false, handsComp: hands);
+                    }
+                }
+            }
+
+            _minds.WipeMind(player);
+            QueueDel(entity);
+
+            _gameTicker.SpawnObserver(player);
+        }
     }
 }
index 8b792b0892642715f951b039f11298f4d45a91c8..e493cea6a7b9f9d2da2c9ab0f4b35cc4eda92ff3 100644 (file)
@@ -48,6 +48,7 @@ namespace Content.Server.Administration.Systems
         [Dependency] private readonly IGameTiming _gameTiming = default!;
         [Dependency] private readonly IMapManager _mapManager = default!;
         [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+        [Dependency] private readonly AdminSystem _adminSystem = default!;
         [Dependency] private readonly DisposalTubeSystem _disposalTubes = default!;
         [Dependency] private readonly EuiManager _euiManager = default!;
         [Dependency] private readonly GhostRoleSystem _ghostRoleSystem = default!;
@@ -140,6 +141,20 @@ namespace Content.Server.Administration.Systems
                         },
                         Impact = LogImpact.Medium,
                     });
+
+                    // Erase
+                    args.Verbs.Add(new Verb
+                    {
+                        Text = Loc.GetString("admin-verbs-erase"),
+                        Category = VerbCategory.Admin,
+                        Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png")),
+                        Act = () =>
+                        {
+                            _adminSystem.Erase(targetActor.PlayerSession);
+                        },
+                        Impact = LogImpact.Extreme,
+                        ConfirmationPopup = true
+                    });
                 }
 
                 // Admin Logs
index aa055e44914f0b4662345971151b14894728f5ad..88e143d24e37eb6c5998b25fa9e73f41099343b7 100644 (file)
@@ -1,4 +1,5 @@
 using System.Linq;
+using System.Runtime.InteropServices;
 using Content.Server.Administration.Logs;
 using Content.Server.Administration.Managers;
 using Content.Server.Administration.Systems;
@@ -50,9 +51,13 @@ namespace Content.Server.Chat.Managers
         private bool _oocEnabled = true;
         private bool _adminOocEnabled = true;
 
+        public Dictionary<IPlayerSession, int> SenderKeys { get; } = new();
+        public Dictionary<IPlayerSession, HashSet<NetEntity>> SenderEntities { get; } = new();
+
         public void Initialize()
         {
             _netManager.RegisterNetMessage<MsgChatMessage>();
+            _netManager.RegisterNetMessage<MsgDeleteChatMessagesBy>();
 
             _configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
             _configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
@@ -74,6 +79,15 @@ namespace Content.Server.Chat.Managers
             DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-admin-ooc-chat-enabled-message" : "chat-manager-admin-ooc-chat-disabled-message"));
         }
 
+        public void DeleteMessagesBy(IPlayerSession player)
+        {
+            var key = SenderKeys.GetValueOrDefault(player);
+            var entities = SenderEntities.GetValueOrDefault(player) ?? new HashSet<NetEntity>();
+            var msg = new MsgDeleteChatMessagesBy { Key = key, Entities = entities };
+
+            _netManager.ServerSendToAll(msg);
+        }
+
         #region Server Announcements
 
         public void DispatchServerAnnouncement(string message, Color? colorOverride = null)
@@ -202,8 +216,12 @@ namespace Content.Server.Chat.Managers
                 wrappedMessage = Loc.GetString("chat-manager-send-ooc-patron-wrap-message", ("patronColor", patronColor),("playerName", player.Name), ("message", FormattedMessage.EscapeText(message)));
             }
 
+            ref var key = ref CollectionsMarshal.GetValueRefOrAddDefault(SenderKeys, player, out var exists);
+            if (!exists)
+                key = SenderKeys.Count;
+
             //TODO: player.Name color, this will need to change the structure of the MsgChatMessage
-            ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride);
+            ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride, senderKey: key);
             _mommiLink.SendOOCMessage(player.Name, message);
             _adminLogger.Add(LogType.Chat, LogImpact.Low, $"OOC from {player:Player}: {message}");
         }
@@ -220,6 +238,11 @@ namespace Content.Server.Chat.Managers
             var wrappedMessage = Loc.GetString("chat-manager-send-admin-chat-wrap-message",
                                             ("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
                                             ("playerName", player.Name), ("message", FormattedMessage.EscapeText(message)));
+
+            ref var key = ref CollectionsMarshal.GetValueRefOrAddDefault(SenderKeys, player, out var exists);
+            if (!exists)
+                key = SenderKeys.Count;
+
             foreach (var client in clients)
             {
                 var isSource = client != player.ConnectedClient;
@@ -230,7 +253,7 @@ namespace Content.Server.Chat.Managers
                     false,
                     client,
                     audioPath: isSource ? _netConfigManager.GetClientCVar(client, CCVars.AdminChatSoundPath) : default,
-                    audioVolume: isSource ? _netConfigManager.GetClientCVar(client, CCVars.AdminChatSoundVolume) : default);
+                    audioVolume: isSource ? _netConfigManager.GetClientCVar(client, CCVars.AdminChatSoundVolume) : default, senderKey: key);
             }
 
             _adminLogger.Add(LogType.Chat, $"Admin chat from {player:Player}: {message}");
@@ -240,9 +263,9 @@ namespace Content.Server.Chat.Managers
 
         #region Utility
 
-        public void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, INetChannel client, Color? colorOverride = null, bool recordReplay = false, string? audioPath = null, float audioVolume = 0)
+        public void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, INetChannel client, Color? colorOverride = null, bool recordReplay = false, string? audioPath = null, float audioVolume = 0, int? senderKey = null)
         {
-            var msg = new ChatMessage(channel, message, wrappedMessage, _entityManager.GetNetEntity(source), hideChat, colorOverride, audioPath, audioVolume);
+            var msg = new ChatMessage(channel, message, wrappedMessage, _entityManager.GetNetEntity(source), senderKey, hideChat, colorOverride, audioPath, audioVolume);
             _netManager.ServerSendMessage(new MsgChatMessage() { Message = msg }, client);
 
             if (!recordReplay)
@@ -260,7 +283,7 @@ namespace Content.Server.Chat.Managers
 
         public void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, List<INetChannel> clients, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0)
         {
-            var msg = new ChatMessage(channel, message, wrappedMessage, _entityManager.GetNetEntity(source), hideChat, colorOverride, audioPath, audioVolume);
+            var msg = new ChatMessage(channel, message, wrappedMessage, _entityManager.GetNetEntity(source), null, hideChat, colorOverride, audioPath, audioVolume);
             _netManager.ServerSendToMany(new MsgChatMessage() { Message = msg }, clients);
 
             if (!recordReplay)
@@ -288,9 +311,9 @@ namespace Content.Server.Chat.Managers
             ChatMessageToMany(channel, message, wrappedMessage, source, hideChat, recordReplay, clients, colorOverride, audioPath, audioVolume);
         }
 
-        public void ChatMessageToAll(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, Color? colorOverride = null,  string? audioPath = null, float audioVolume = 0)
+        public void ChatMessageToAll(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, int? senderKey = null)
         {
-            var msg = new ChatMessage(channel, message, wrappedMessage, _entityManager.GetNetEntity(source), hideChat, colorOverride, audioPath, audioVolume);
+            var msg = new ChatMessage(channel, message, wrappedMessage, _entityManager.GetNetEntity(source), senderKey, hideChat, colorOverride, audioPath, audioVolume);
             _netManager.ServerSendToAll(new MsgChatMessage() { Message = msg });
 
             if (!recordReplay)
index a398be74fd263ce3422be4dd72bc12826060dfc2..10103f011df2f7b2fd4ec6fb50df58db47e7bc27 100644 (file)
@@ -8,6 +8,17 @@ namespace Content.Server.Chat.Managers
 {
     public interface IChatManager
     {
+        /// <summary>
+        ///     Keys identifying messages sent by a specific player, used when sending
+        ///     <see cref="MsgChatMessage"/>
+        /// </summary>
+        Dictionary<IPlayerSession, int> SenderKeys { get; }
+
+        /// <summary>
+        ///     Tracks which entities a player was attached to while sending messages.
+        /// </summary>
+        Dictionary<IPlayerSession, HashSet<NetEntity>> SenderEntities { get; }
+
         void Initialize();
 
         /// <summary>
@@ -27,15 +38,17 @@ namespace Content.Server.Chat.Managers
         void SendAdminAlert(EntityUid player, string message);
 
         void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat,
-            INetChannel client, Color? colorOverride = null, bool recordReplay = false, string? audioPath = null, float audioVolume = 0);
+            INetChannel client, Color? colorOverride = null, bool recordReplay = false, string? audioPath = null, float audioVolume = 0, int? senderKey = null);
 
         void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay,
             IEnumerable<INetChannel> clients, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0);
 
         void ChatMessageToManyFiltered(Filter filter, ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, Color? colorOverride, string? audioPath = null, float audioVolume = 0);
 
-        void ChatMessageToAll(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0);
+        void ChatMessageToAll(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, int? senderKey = null);
 
         bool MessageCharacterLimit(IPlayerSession player, string message);
+
+        void DeleteMessagesBy(IPlayerSession player);
     }
 }
index b8f4e116a45d6ab73f8fa055c97986750156ff5f..24e13bcde2830a8b2197f8f0b65b9eb6ee4e17bd 100644 (file)
@@ -15,7 +15,6 @@ using Content.Shared.Database;
 using Content.Shared.Ghost;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Interaction;
-using Content.Shared.Inventory;
 using Content.Shared.Mobs.Systems;
 using Content.Shared.Radio;
 using Robust.Server.GameObjects;
@@ -33,6 +32,7 @@ using Robust.Shared.Utility;
 
 namespace Content.Server.Chat.Systems;
 
+// TODO refactor whatever active warzone this class and chatmanager have become
 /// <summary>
 ///     ChatSystem is responsible for in-simulation chat handling, such as whispering, speaking, emoting, etc.
 ///     ChatSystem depends on ChatManager to actually send the messages.
@@ -191,6 +191,9 @@ public sealed partial class ChatSystem : SharedChatSystem
         if (!CanSendInGame(message, shell, player))
             return;
 
+        if (player != null)
+            _chatManager.SenderEntities.GetOrNew(player).Add(GetNetEntity(source));
+
         if (desiredType == InGameICChatType.Speak && message.StartsWith(LocalPrefix))
         {
             // prevent radios and remove prefix.
@@ -484,7 +487,7 @@ public sealed partial class ChatSystem : SharedChatSystem
                 _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedUnknownMessage, source, false, session.ConnectedClient);
         }
 
-        _replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, GetNetEntity(source), MessageRangeHideChatForReplay(range)));
+        _replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range)));
 
         var ev = new EntitySpokeEvent(source, message, channel, obfuscatedMessage);
         RaiseLocalEvent(source, ev, true);
@@ -559,6 +562,8 @@ public sealed partial class ChatSystem : SharedChatSystem
             ("entityName", name),
             ("message", FormattedMessage.EscapeText(message)));
 
+        _chatManager.SenderEntities.GetOrNew(player).Add(GetNetEntity(source));
+
         SendInVoiceRange(ChatChannel.LOOC, message, wrappedMessage, source, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal);
         _adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {player:Player}: {message}");
     }
@@ -585,8 +590,9 @@ public sealed partial class ChatSystem : SharedChatSystem
             _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Dead chat from {player:Player}: {message}");
         }
 
-        _chatManager.ChatMessageToMany(ChatChannel.Dead, message, wrappedMessage, source, hideChat, true, clients.ToList());
+        _chatManager.SenderEntities.GetOrNew(player).Add(GetNetEntity(source));
 
+        _chatManager.ChatMessageToMany(ChatChannel.Dead, message, wrappedMessage, source, hideChat, true, clients.ToList());
     }
     #endregion
 
@@ -651,7 +657,7 @@ public sealed partial class ChatSystem : SharedChatSystem
             _chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.ConnectedClient);
         }
 
-        _replay.RecordServerMessage(new ChatMessage(channel, message, wrappedMessage, GetNetEntity(source), MessageRangeHideChatForReplay(range)));
+        _replay.RecordServerMessage(new ChatMessage(channel, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range)));
     }
 
     /// <summary>
@@ -893,4 +899,3 @@ public enum ChatTransmitRange : byte
     /// Ghosts can't hear or see it at all. Regular players can if in-range.
     NoGhosts
 }
-
index 9471c88c9eaa146617ee50ffffc020f8ca4cbfbe..fa00648c55537129ee043ab7d4d0eca68fdf46f5 100644 (file)
@@ -37,6 +37,7 @@ public sealed class CrewManifestSystem : EntitySystem
     {
         SubscribeLocalEvent<AfterGeneralRecordCreatedEvent>(AfterGeneralRecordCreated);
         SubscribeLocalEvent<RecordModifiedEvent>(OnRecordModified);
+        SubscribeLocalEvent<RecordRemovedEvent>(OnRecordRemoved);
         SubscribeLocalEvent<CrewManifestViewerComponent, BoundUIClosedEvent>(OnBoundUiClose);
         SubscribeLocalEvent<CrewManifestViewerComponent, CrewManifestOpenUiMessage>(OpenEuiFromBui);
         SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
@@ -83,6 +84,12 @@ public sealed class CrewManifestSystem : EntitySystem
         UpdateEuis(ev.Key.OriginStation);
     }
 
+    private void OnRecordRemoved(RecordRemovedEvent ev)
+    {
+        BuildCrewManifest(ev.Key.OriginStation);
+        UpdateEuis(ev.Key.OriginStation);
+    }
+
     private void OnBoundUiClose(EntityUid uid, CrewManifestViewerComponent component, BoundUIClosedEvent ev)
     {
         var owningStation = _stationSystem.GetOwningStation(uid);
index c4f66a0cd9c23249623c7f1a76b013f12d3e1be8..4f9099d0af70fe4280f92294d8137c6b84dcc1ef 100644 (file)
@@ -1,20 +1,18 @@
 using Content.Server.Administration.Logs;
 using Content.Server.Chat.Systems;
+using Content.Server.Power.Components;
 using Content.Server.Radio.Components;
 using Content.Server.VoiceMask;
-using Content.Server.Popups;
 using Content.Shared.Chat;
 using Content.Shared.Database;
 using Content.Shared.Radio;
+using Content.Shared.Radio.Components;
 using Robust.Server.GameObjects;
+using Robust.Shared.Map;
 using Robust.Shared.Network;
+using Robust.Shared.Random;
 using Robust.Shared.Replays;
 using Robust.Shared.Utility;
-using Content.Shared.Popups;
-using Robust.Shared.Map;
-using Content.Shared.Radio.Components;
-using Content.Server.Power.Components;
-using Robust.Shared.Random;
 
 namespace Content.Server.Radio.EntitySystems;
 
@@ -87,7 +85,8 @@ public sealed class RadioSystem : EntitySystem
             ChatChannel.Radio,
             message,
             wrappedMessage,
-            NetEntity.Invalid);
+            NetEntity.Invalid,
+            null);
         var chatMsg = new MsgChatMessage { Message = chat };
         var ev = new RadioReceiveEvent(message, messageSource, channel, chatMsg);
 
index ea8eed84459017c293e9dc7441a0e547cfba0cd4..f69caaa9a7e357519289ae9ad39effee5d490b83 100644 (file)
@@ -1,7 +1,7 @@
+using System.Linq;
 using Content.Server.Station.Systems;
 using Content.Shared.StationRecords;
 using Robust.Server.GameObjects;
-using System.Linq;
 
 namespace Content.Server.StationRecords.Systems;
 
@@ -18,6 +18,7 @@ public sealed class GeneralStationRecordConsoleSystem : EntitySystem
         SubscribeLocalEvent<GeneralStationRecordConsoleComponent, GeneralStationRecordsFilterMsg>(OnFiltersChanged);
         SubscribeLocalEvent<GeneralStationRecordConsoleComponent, RecordModifiedEvent>(UpdateUserInterface);
         SubscribeLocalEvent<GeneralStationRecordConsoleComponent, AfterGeneralRecordCreatedEvent>(UpdateUserInterface);
+        SubscribeLocalEvent<GeneralStationRecordConsoleComponent, RecordRemovedEvent>(UpdateUserInterface);
     }
 
     private void UpdateUserInterface<T>(EntityUid uid, GeneralStationRecordConsoleComponent component, T ev)
index c70e1d0d9a9f276d0875e725082c6faffff8f7a7..fd5094d5330a1045d5ab9fa7a0b5c4827235d6d3 100644 (file)
@@ -1,6 +1,6 @@
 using System.Diagnostics.CodeAnalysis;
-using Content.Server.GameTicking;
 using Content.Server.Forensics;
+using Content.Server.GameTicking;
 using Content.Shared.Inventory;
 using Content.Shared.PDA;
 using Content.Shared.Preferences;
@@ -160,8 +160,13 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
         if (!Resolve(station, ref records))
             return false;
 
-        RaiseLocalEvent(new RecordRemovedEvent(station, key));
-        return records.Records.RemoveAllRecords(key);
+        if (records.Records.RemoveAllRecords(key))
+        {
+            RaiseLocalEvent(new RecordRemovedEvent(station, key));
+            return true;
+        }
+
+        return false;
     }
 
     /// <summary>
index 545c9c0071e14854798e0c66e661f2566b7ce52f..dd10068e5da21c14dd530d088b88d4b024577764 100644 (file)
@@ -1,8 +1,7 @@
-using System.Collections.Immutable;
+using System.Net;
 using Content.Shared.Database;
 using Content.Shared.Eui;
 using Robust.Shared.Serialization;
-using System.Net;
 
 namespace Content.Shared.Administration;
 
@@ -33,8 +32,9 @@ public static class BanPanelEuiStateMsg
         public string[]? Roles { get; set; }
         public bool UseLastIp { get; set; }
         public bool UseLastHwid { get; set; }
+        public bool Erase { get; set; }
 
-        public CreateBanRequest(string? player, (IPAddress, int)? ipAddress, bool useLastIp, byte[]? hwid, bool useLastHwid, uint minutes, string reason, NoteSeverity severity, string[]? roles)
+        public CreateBanRequest(string? player, (IPAddress, int)? ipAddress, bool useLastIp, byte[]? hwid, bool useLastHwid, uint minutes, string reason, NoteSeverity severity, string[]? roles, bool erase)
         {
             Player = player;
             IpAddress = ipAddress == null ? null : $"{ipAddress.Value.Item1}/{ipAddress.Value.Item2}";
@@ -45,6 +45,7 @@ public static class BanPanelEuiStateMsg
             Reason = reason;
             Severity = severity;
             Roles = roles;
+            Erase = erase;
         }
     }
 
index 27ab203d6e8e1e97e9bca7db16cb047e306135fe..55a3a7342b7707ae01b1d14bc1fff6e4c846faeb 100644 (file)
@@ -1,9 +1,9 @@
+using System.IO;
 using JetBrains.Annotations;
 using Lidgren.Network;
 using Robust.Shared.Network;
 using Robust.Shared.Serialization;
 using Robust.Shared.Utility;
-using System.IO;
 
 namespace Content.Shared.Chat
 {
@@ -14,6 +14,14 @@ namespace Content.Shared.Chat
         public string Message;
         public string WrappedMessage;
         public NetEntity SenderEntity;
+
+        /// <summary>
+        ///     Identifier sent when <see cref="SenderEntity"/> is <see cref="NetEntity.Invalid"/>
+        ///     if this was sent by a player to assign a key to the sender of this message.
+        ///     This is unique per sender.
+        /// </summary>
+        public int? SenderKey;
+
         public bool HideChat;
         public Color? MessageColorOverride;
         public string? AudioPath;
@@ -22,12 +30,13 @@ namespace Content.Shared.Chat
         [NonSerialized]
         public bool Read;
 
-        public ChatMessage(ChatChannel channel, string message, string wrappedMessage, NetEntity source, bool hideChat = false, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0)
+        public ChatMessage(ChatChannel channel, string message, string wrappedMessage, NetEntity source, int? senderKey, bool hideChat = false, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0)
         {
             Channel = channel;
             Message = message;
             WrappedMessage = wrappedMessage;
             SenderEntity = source;
+            SenderKey = senderKey;
             HideChat = hideChat;
             MessageColorOverride = colorOverride;
             AudioPath = audioPath;
diff --git a/Content.Shared/Chat/MsgDeleteChatMessagesBy.cs b/Content.Shared/Chat/MsgDeleteChatMessagesBy.cs
new file mode 100644 (file)
index 0000000..55d2751
--- /dev/null
@@ -0,0 +1,37 @@
+using Lidgren.Network;
+using Robust.Shared.Network;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Chat;
+
+public sealed class MsgDeleteChatMessagesBy : NetMessage
+{
+    public override MsgGroups MsgGroup => MsgGroups.Command;
+
+    public int Key;
+    public HashSet<NetEntity> Entities = default!;
+
+    public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
+    {
+        Key = buffer.ReadInt32();
+
+        var entities = buffer.ReadInt32();
+        Entities = new HashSet<NetEntity>(entities);
+
+        for (var i = 0; i < entities; i++)
+        {
+            Entities.Add(buffer.ReadNetEntity());
+        }
+    }
+
+    public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
+    {
+        buffer.Write(Key);
+
+        buffer.Write(Entities.Count);
+        foreach (var ent in Entities)
+        {
+            buffer.Write(ent);
+        }
+    }
+}
index 4a1905d110bfc1653682cd355841138aa4b81c66..93db39b973bd449c9af4d252ec315066b49d33c9 100644 (file)
@@ -19,3 +19,8 @@ Entries:
   - {message: 'Added total playtime to the F7 player list and the AHelp window title.', type: Add}
   id: 3
   time: '2023-10-14T08:55:00.0000000+00:00'
+- author: DrSmugleaf
+  changes:
+  - {message: 'Added admin Erase verb, add checkbox to erase from the ban panel.', type: Add}
+  id: 4
+  time: '2023-10-14T09:00:00.0000000+00:00'
index 6804171f7d2ffc9f65e483aa31d821ff21c0c39b..224ada4b63962757d609ec27148ca4b737aa5eaa 100644 (file)
@@ -7,5 +7,6 @@ admin-verbs-teleport-to = Teleport To
 admin-verbs-teleport-here = Teleport Here
 admin-verbs-freeze = Freeze
 admin-verbs-unfreeze = Unfreeze
+admin-verbs-erase = Erase
 toolshed-verb-mark = Mark
 toolshed-verb-mark-description = Places this entity into the $marked variable, a list of entities, replacing it's prior value.
diff --git a/Resources/Locale/en-US/administration/ui/admin-erase.ftl b/Resources/Locale/en-US/administration/ui/admin-erase.ftl
new file mode 100644 (file)
index 0000000..86b7519
--- /dev/null
@@ -0,0 +1 @@
+admin-erase-popup = {$user} disappears without a trace. You should keep playing as if they never existed.
index f1e67c66cdfd0aa7ace18e45be254403a758c944..2804690fc5e8c45c09141669baa7e25fcfa09fde 100644 (file)
@@ -76,6 +76,7 @@ ban-panel-years = Years
 ban-panel-permanent = Permanent
 ban-panel-ip-hwid-tooltip = Leave empty and check the checkbox below to use last connection's details
 ban-panel-severity = Severity:
+ban-panel-erase = Erase chat messages and player from round
 
 # Ban string
 server-ban-string = {$admin} created a {$severity} severity server ban that expires {$expires} for [{$name}, {$ip}, {$hwid}], with reason: {$reason}