diff --git a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml
index b8f91e050e..333184f1c0 100644
--- a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml
+++ b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml
@@ -22,6 +22,7 @@
+
@@ -30,7 +31,7 @@
-
+
diff --git a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
index e6d122766e..1f32640f7d 100644
--- a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
+++ b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
@@ -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? BanSubmitted;
+ public event Action? BanSubmitted;
public event Action? 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)
diff --git a/Content.Client/Administration/UI/BanPanel/BanPanelEui.cs b/Content.Client/Administration/UI/BanPanel/BanPanelEui.cs
index 0a7d88f65d..940a55e010 100644
--- a/Content.Client/Administration/UI/BanPanel/BanPanelEui.cs
+++ b/Content.Client/Administration/UI/BanPanel/BanPanelEui.cs
@@ -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));
}
diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
index 78193b5aec..6d86e458c5 100644
--- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
+++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
@@ -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
///
private readonly Dictionary _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(OnChatMessage);
+ _net.RegisterNetMessage(OnDeleteChatMessagesBy);
SubscribeNetworkEvent(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);
diff --git a/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs b/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs
index 09647bb583..56c0c28634 100644
--- a/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs
@@ -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);
}
diff --git a/Content.Server/Administration/BanPanelEui.cs b/Content.Server/Administration/BanPanelEui.cs
index f4a1a308d0..2e6dfab18a 100644
--- a/Content.Server/Administration/BanPanelEui.cs
+++ b/Content.Server/Administration/BanPanelEui.cs
@@ -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? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection? roles)
+ private async void BanPlayer(string? target, string? ipAddressString, bool useLastIp, ImmutableArray? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection? 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");
+ }
}
diff --git a/Content.Server/Administration/Systems/AdminSystem.cs b/Content.Server/Administration/Systems/AdminSystem.cs
index e1d769b1b2..8a2b6d5119 100644
--- a/Content.Server/Administration/Systems/AdminSystem.cs
+++ b/Content.Server/Administration/Systems/AdminSystem.cs
@@ -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 _playerList = new();
@@ -299,5 +317,76 @@ namespace Content.Server.Administration.Systems
RaiseNetworkEvent(ev, admin);
}
}
+
+ ///
+ /// 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.
+ ///
+ 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);
+ }
}
}
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.cs b/Content.Server/Administration/Systems/AdminVerbSystem.cs
index 8b792b0892..e493cea6a7 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.cs
@@ -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
diff --git a/Content.Server/Chat/Managers/ChatManager.cs b/Content.Server/Chat/Managers/ChatManager.cs
index aa055e4491..88e143d24e 100644
--- a/Content.Server/Chat/Managers/ChatManager.cs
+++ b/Content.Server/Chat/Managers/ChatManager.cs
@@ -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 SenderKeys { get; } = new();
+ public Dictionary> SenderEntities { get; } = new();
+
public void Initialize()
{
_netManager.RegisterNetMessage();
+ _netManager.RegisterNetMessage();
_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();
+ 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 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)
diff --git a/Content.Server/Chat/Managers/IChatManager.cs b/Content.Server/Chat/Managers/IChatManager.cs
index a398be74fd..10103f011d 100644
--- a/Content.Server/Chat/Managers/IChatManager.cs
+++ b/Content.Server/Chat/Managers/IChatManager.cs
@@ -8,6 +8,17 @@ namespace Content.Server.Chat.Managers
{
public interface IChatManager
{
+ ///
+ /// Keys identifying messages sent by a specific player, used when sending
+ ///
+ ///
+ Dictionary SenderKeys { get; }
+
+ ///
+ /// Tracks which entities a player was attached to while sending messages.
+ ///
+ Dictionary> SenderEntities { get; }
+
void Initialize();
///
@@ -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 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);
}
}
diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs
index b8f4e116a4..24e13bcde2 100644
--- a/Content.Server/Chat/Systems/ChatSystem.cs
+++ b/Content.Server/Chat/Systems/ChatSystem.cs
@@ -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
///
/// 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)));
}
///
@@ -893,4 +899,3 @@ public enum ChatTransmitRange : byte
/// Ghosts can't hear or see it at all. Regular players can if in-range.
NoGhosts
}
-
diff --git a/Content.Server/CrewManifest/CrewManifestSystem.cs b/Content.Server/CrewManifest/CrewManifestSystem.cs
index 9471c88c9e..fa00648c55 100644
--- a/Content.Server/CrewManifest/CrewManifestSystem.cs
+++ b/Content.Server/CrewManifest/CrewManifestSystem.cs
@@ -37,6 +37,7 @@ public sealed class CrewManifestSystem : EntitySystem
{
SubscribeLocalEvent(AfterGeneralRecordCreated);
SubscribeLocalEvent(OnRecordModified);
+ SubscribeLocalEvent(OnRecordRemoved);
SubscribeLocalEvent(OnBoundUiClose);
SubscribeLocalEvent(OpenEuiFromBui);
SubscribeLocalEvent(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);
diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs
index c4f66a0cd9..4f9099d0af 100644
--- a/Content.Server/Radio/EntitySystems/RadioSystem.cs
+++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs
@@ -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);
diff --git a/Content.Server/StationRecords/Systems/GeneralStationRecordConsoleSystem.cs b/Content.Server/StationRecords/Systems/GeneralStationRecordConsoleSystem.cs
index ea8eed8445..f69caaa9a7 100644
--- a/Content.Server/StationRecords/Systems/GeneralStationRecordConsoleSystem.cs
+++ b/Content.Server/StationRecords/Systems/GeneralStationRecordConsoleSystem.cs
@@ -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(OnFiltersChanged);
SubscribeLocalEvent(UpdateUserInterface);
SubscribeLocalEvent(UpdateUserInterface);
+ SubscribeLocalEvent(UpdateUserInterface);
}
private void UpdateUserInterface(EntityUid uid, GeneralStationRecordConsoleComponent component, T ev)
diff --git a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
index c70e1d0d9a..fd5094d533 100644
--- a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
+++ b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
@@ -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;
}
///
diff --git a/Content.Shared/Administration/BanPanelEuiState.cs b/Content.Shared/Administration/BanPanelEuiState.cs
index 545c9c0071..dd10068e5d 100644
--- a/Content.Shared/Administration/BanPanelEuiState.cs
+++ b/Content.Shared/Administration/BanPanelEuiState.cs
@@ -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;
}
}
diff --git a/Content.Shared/Chat/MsgChatMessage.cs b/Content.Shared/Chat/MsgChatMessage.cs
index 27ab203d6e..55a3a7342b 100644
--- a/Content.Shared/Chat/MsgChatMessage.cs
+++ b/Content.Shared/Chat/MsgChatMessage.cs
@@ -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;
+
+ ///
+ /// Identifier sent when is
+ /// if this was sent by a player to assign a key to the sender of this message.
+ /// This is unique per sender.
+ ///
+ 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
index 0000000000..55d27518d8
--- /dev/null
+++ b/Content.Shared/Chat/MsgDeleteChatMessagesBy.cs
@@ -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 Entities = default!;
+
+ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
+ {
+ Key = buffer.ReadInt32();
+
+ var entities = buffer.ReadInt32();
+ Entities = new HashSet(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);
+ }
+ }
+}
diff --git a/Resources/Changelog/Admin.yml b/Resources/Changelog/Admin.yml
index 4a1905d110..93db39b973 100644
--- a/Resources/Changelog/Admin.yml
+++ b/Resources/Changelog/Admin.yml
@@ -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'
diff --git a/Resources/Locale/en-US/administration/admin-verbs.ftl b/Resources/Locale/en-US/administration/admin-verbs.ftl
index 6804171f7d..224ada4b63 100644
--- a/Resources/Locale/en-US/administration/admin-verbs.ftl
+++ b/Resources/Locale/en-US/administration/admin-verbs.ftl
@@ -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
index 0000000000..86b75196ac
--- /dev/null
+++ b/Resources/Locale/en-US/administration/ui/admin-erase.ftl
@@ -0,0 +1 @@
+admin-erase-popup = {$user} disappears without a trace. You should keep playing as if they never existed.
diff --git a/Resources/Locale/en-US/info/ban.ftl b/Resources/Locale/en-US/info/ban.ftl
index f1e67c66cd..2804690fc5 100644
--- a/Resources/Locale/en-US/info/ban.ftl
+++ b/Resources/Locale/en-US/info/ban.ftl
@@ -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}