diff --git a/Content.Server/Administration/BanPanelEui.cs b/Content.Server/Administration/BanPanelEui.cs index aa6bd8d4bf..b3253f0d0c 100644 --- a/Content.Server/Administration/BanPanelEui.cs +++ b/Content.Server/Administration/BanPanelEui.cs @@ -132,13 +132,12 @@ public sealed class BanPanelEui : BaseEui } if (erase && - targetUid != null && - _playerManager.TryGetSessionById(targetUid.Value, out var targetPlayer)) + targetUid != null) { try { if (_entities.TrySystem(out AdminSystem? adminSystem)) - adminSystem.Erase(targetPlayer); + adminSystem.Erase(targetUid.Value); } catch (Exception e) { diff --git a/Content.Server/Administration/Commands/EraseCommand.cs b/Content.Server/Administration/Commands/EraseCommand.cs new file mode 100644 index 0000000000..cb01d742a0 --- /dev/null +++ b/Content.Server/Administration/Commands/EraseCommand.cs @@ -0,0 +1,47 @@ +using System.Linq; +using Content.Server.Administration.Systems; +using Content.Shared.Administration; +using Robust.Server.Player; +using Robust.Shared.Console; + +namespace Content.Server.Administration.Commands; + +[AdminCommand(AdminFlags.Admin)] +public sealed class EraseCommand : LocalizedEntityCommands +{ + [Dependency] private readonly IPlayerLocator _locator = default!; + [Dependency] private readonly IPlayerManager _players = default!; + [Dependency] private readonly AdminSystem _admin = default!; + + public override string Command => "erase"; + + public override async void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length != 1) + { + shell.WriteError(Loc.GetString("cmd-erase-invalid-args")); + shell.WriteLine(Help); + return; + } + + var located = await _locator.LookupIdByNameOrIdAsync(args[0]); + + if (located == null) + { + shell.WriteError(Loc.GetString("cmd-erase-player-not-found")); + return; + } + + _admin.Erase(located.UserId); + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length != 1) + return CompletionResult.Empty; + + var options = _players.Sessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray(); + + return CompletionResult.FromHintOptions(options, Loc.GetString("cmd-erase-player-completion")); + } +} diff --git a/Content.Server/Administration/Systems/AdminSystem.cs b/Content.Server/Administration/Systems/AdminSystem.cs index db22c41520..99551c714c 100644 --- a/Content.Server/Administration/Systems/AdminSystem.cs +++ b/Content.Server/Administration/Systems/AdminSystem.cs @@ -15,7 +15,9 @@ using Content.Shared.GameTicking; using Content.Shared.Hands.Components; using Content.Shared.IdentityManagement; using Content.Shared.Inventory; +using Content.Shared.Mind; using Content.Shared.PDA; +using Content.Shared.Players; using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Popups; using Content.Shared.Roles; @@ -377,30 +379,32 @@ public sealed class AdminSystem : EntitySystem } } - /// - /// 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(ICommonSession player) - { - var entity = player.AttachedEntity; - _chat.DeleteMessagesBy(player); - - if (entity != null && !TerminatingOrDeleted(entity.Value)) + /// + /// 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(NetUserId uid) { - if (TryComp(entity.Value, out TransformComponent? transform)) + _chat.DeleteMessagesBy(uid); + + if (!_minds.TryGetMind(uid, out var mindId, out var mind) || mind.OwnedEntity == null || TerminatingOrDeleted(mind.OwnedEntity.Value)) + return; + + var entity = mind.OwnedEntity.Value; + + if (TryComp(entity, out TransformComponent? transform)) { - var coordinates = _transform.GetMoverCoordinates(entity.Value, transform); - var name = Identity.Entity(entity.Value, EntityManager); + var coordinates = _transform.GetMoverCoordinates(entity, transform); + var name = Identity.Entity(entity, EntityManager); _popup.PopupCoordinates(Loc.GetString("admin-erase-popup", ("user", name)), coordinates, PopupType.LargeCaution); var filter = Filter.Pvs(coordinates, 1, EntityManager, _playerManager); var audioParams = new AudioParams().WithVolume(3); _audio.PlayStatic("/Audio/Effects/pop_high.ogg", filter, coordinates, true, audioParams); } - foreach (var item in _inventory.GetHandOrInventoryEntities(entity.Value)) + foreach (var item in _inventory.GetHandOrInventoryEntities(entity)) { if (TryComp(item, out PdaComponent? pda) && TryComp(pda.ContainedId, out StationRecordKeyStorageComponent? keyStorage) && @@ -424,30 +428,30 @@ public sealed class AdminSystem : EntitySystem } } - if (_inventory.TryGetContainerSlotEnumerator(entity.Value, out var enumerator)) + if (_inventory.TryGetContainerSlotEnumerator(entity, out var enumerator)) { while (enumerator.NextItem(out var item, out var slot)) { - if (_inventory.TryUnequip(entity.Value, entity.Value, slot.Name, true, true)) + if (_inventory.TryUnequip(entity, entity, slot.Name, true, true)) _physics.ApplyAngularImpulse(item, ThrowingSystem.ThrowAngularImpulse); } } - if (TryComp(entity.Value, out HandsComponent? hands)) + if (TryComp(entity, out HandsComponent? hands)) { - foreach (var hand in _hands.EnumerateHands(entity.Value, hands)) + foreach (var hand in _hands.EnumerateHands(entity, hands)) { - _hands.TryDrop(entity.Value, hand, checkActionBlocker: false, doDropInteraction: false, handsComp: hands); + _hands.TryDrop(entity, hand, checkActionBlocker: false, doDropInteraction: false, handsComp: hands); } } + + _minds.WipeMind(mindId, mind); + QueueDel(entity); + + if (_playerManager.TryGetSessionById(uid, out var session)) + _gameTicker.SpawnObserver(session); } - _minds.WipeMind(player); - QueueDel(entity); - - _gameTicker.SpawnObserver(player); - } - private void OnSessionPlayTimeUpdated(ICommonSession session) { UpdatePlayerList(session); diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.cs b/Content.Server/Administration/Systems/AdminVerbSystem.cs index 308a679846..5aa05ce28b 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.cs @@ -34,11 +34,11 @@ using Robust.Shared.Timing; using Robust.Shared.Toolshed; using Robust.Shared.Utility; using System.Linq; -using System.Numerics; using Content.Server.Silicons.Laws; using Content.Shared.Silicons.Laws; using Content.Shared.Silicons.Laws.Components; using Robust.Server.Player; +using Content.Shared.Mind; using Robust.Shared.Physics.Components; using static Content.Shared.Configurable.ConfigurationComponent; @@ -137,34 +137,6 @@ namespace Content.Server.Administration.Systems prayerVerb.Impact = LogImpact.Low; args.Verbs.Add(prayerVerb); - // Erase - args.Verbs.Add(new Verb - { - Text = Loc.GetString("admin-verbs-erase"), - Message = Loc.GetString("admin-verbs-erase-description"), - 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 - }); - - // Respawn - args.Verbs.Add(new Verb() - { - Text = Loc.GetString("admin-player-actions-respawn"), - Category = VerbCategory.Admin, - Act = () => - { - _console.ExecuteCommand(player, $"respawn {targetActor.PlayerSession.Name}"); - }, - ConfirmationPopup = true, - // No logimpact as the command does it internally. - }); - // Spawn - Like respawn but on the spot. args.Verbs.Add(new Verb() { @@ -225,6 +197,38 @@ namespace Content.Server.Administration.Systems }); } + if (_mindSystem.TryGetMind(args.Target, out _, out var mind) && mind.UserId != null) + { + // Erase + args.Verbs.Add(new Verb + { + Text = Loc.GetString("admin-verbs-erase"), + Message = Loc.GetString("admin-verbs-erase-description"), + Category = VerbCategory.Admin, + Icon = new SpriteSpecifier.Texture( + new("/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png")), + Act = () => + { + _adminSystem.Erase(mind.UserId.Value); + }, + Impact = LogImpact.Extreme, + ConfirmationPopup = true + }); + + // Respawn + args.Verbs.Add(new Verb + { + Text = Loc.GetString("admin-player-actions-respawn"), + Category = VerbCategory.Admin, + Act = () => + { + _console.ExecuteCommand(player, $"respawn \"{mind.UserId}\""); + }, + ConfirmationPopup = true, + // No logimpact as the command does it internally. + }); + } + // Freeze var frozen = TryComp(args.Target, out var frozenComp); var frozenAndMuted = frozenComp?.Muted ?? false; diff --git a/Content.Server/Chat/Managers/ChatManager.cs b/Content.Server/Chat/Managers/ChatManager.cs index 6fb7bbba8e..02f718daef 100644 --- a/Content.Server/Chat/Managers/ChatManager.cs +++ b/Content.Server/Chat/Managers/ChatManager.cs @@ -81,10 +81,10 @@ internal sealed partial class ChatManager : IChatManager DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-admin-ooc-chat-enabled-message" : "chat-manager-admin-ooc-chat-disabled-message")); } - public void DeleteMessagesBy(ICommonSession player) - { - if (!_players.TryGetValue(player.UserId, out var user)) - return; + public void DeleteMessagesBy(NetUserId uid) + { + if (!_players.TryGetValue(uid, out var user)) + return; var msg = new MsgDeleteChatMessagesBy { Key = user.Key, Entities = user.Entities }; _netManager.ServerSendToAll(msg); diff --git a/Content.Server/Chat/Managers/IChatManager.cs b/Content.Server/Chat/Managers/IChatManager.cs index 15d1288ee2..76fa91d847 100644 --- a/Content.Server/Chat/Managers/IChatManager.cs +++ b/Content.Server/Chat/Managers/IChatManager.cs @@ -41,7 +41,7 @@ namespace Content.Server.Chat.Managers bool MessageCharacterLimit(ICommonSession player, string message); - void DeleteMessagesBy(ICommonSession player); + void DeleteMessagesBy(NetUserId uid); [return: NotNullIfNotNull(nameof(author))] ChatUser? EnsurePlayer(NetUserId? author); diff --git a/Content.Server/GameTicking/Commands/RespawnCommand.cs b/Content.Server/GameTicking/Commands/RespawnCommand.cs index 4f101d0939..f7ea11baf1 100644 --- a/Content.Server/GameTicking/Commands/RespawnCommand.cs +++ b/Content.Server/GameTicking/Commands/RespawnCommand.cs @@ -1,4 +1,6 @@ -using Content.Shared.Mind; +using System.Linq; +using Content.Server.Administration; +using Content.Server.Mind; using Content.Shared.Players; using Robust.Server.Player; using Robust.Shared.Console; @@ -6,57 +8,72 @@ using Robust.Shared.Network; namespace Content.Server.GameTicking.Commands { - sealed class RespawnCommand : IConsoleCommand + sealed class RespawnCommand : LocalizedEntityCommands { - public string Command => "respawn"; - public string Description => "Respawns a player, kicking them back to the lobby."; - public string Help => "respawn [player]"; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IPlayerLocator _locator = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly MindSystem _mind = default!; - public void Execute(IConsoleShell shell, string argStr, string[] args) + public override string Command => "respawn"; + + public override async void Execute(IConsoleShell shell, string argStr, string[] args) { var player = shell.Player; if (args.Length > 1) { - shell.WriteLine("Must provide <= 1 argument."); + shell.WriteError(Loc.GetString("cmd-respawn-invalid-args")); return; } - var playerMgr = IoCManager.Resolve(); - var sysMan = IoCManager.Resolve(); - var ticker = sysMan.GetEntitySystem(); - var mind = sysMan.GetEntitySystem(); - NetUserId userId; if (args.Length == 0) { if (player == null) { - shell.WriteLine("If not a player, an argument must be given."); + shell.WriteError(Loc.GetString("cmd-respawn-no-player")); return; } userId = player.UserId; } - else if (!playerMgr.TryGetUserId(args[0], out userId)) + else { - shell.WriteLine("Unknown player"); - return; - } + var located = await _locator.LookupIdByNameOrIdAsync(args[0]); - if (!playerMgr.TryGetSessionById(userId, out var targetPlayer)) - { - if (!playerMgr.TryGetPlayerData(userId, out var data)) + if (located == null) { - shell.WriteLine("Unknown player"); + shell.WriteError(Loc.GetString("cmd-respawn-unknown-player")); return; } - mind.WipeMind(data.ContentData()?.Mind); - shell.WriteLine("Player is not currently online, but they will respawn if they come back online"); + userId = located.UserId; + } + + if (!_player.TryGetSessionById(userId, out var targetPlayer)) + { + if (!_player.TryGetPlayerData(userId, out var data)) + { + shell.WriteError(Loc.GetString("cmd-respawn-unknown-player")); + return; + } + + _mind.WipeMind(data.ContentData()?.Mind); + shell.WriteError(Loc.GetString("cmd-respawn-player-not-online")); return; } - ticker.Respawn(targetPlayer); + _gameTicker.Respawn(targetPlayer); + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length != 1) + return CompletionResult.Empty; + + var options = _player.Sessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray(); + + return CompletionResult.FromHintOptions(options, Loc.GetString("cmd-respawn-player-completion")); } } } diff --git a/Resources/Locale/en-US/administration/commands/erase.ftl b/Resources/Locale/en-US/administration/commands/erase.ftl new file mode 100644 index 0000000000..e9f995f03e --- /dev/null +++ b/Resources/Locale/en-US/administration/commands/erase.ftl @@ -0,0 +1,7 @@ +# erase +cmd-erase-desc = Erase a player's entity if it exists and all their chat messages +cmd-erase-help = erase +cmd-erase-invalid-args = Invalid number of arguments +cmd-erase-player-not-found = Player not found + +cmd-erase-player-completion = diff --git a/Resources/Locale/en-US/administration/commands/respawn.ftl b/Resources/Locale/en-US/administration/commands/respawn.ftl new file mode 100644 index 0000000000..6aab854ee4 --- /dev/null +++ b/Resources/Locale/en-US/administration/commands/respawn.ftl @@ -0,0 +1,9 @@ +cmd-respawn-desc = Respawns a player, kicking them back to the lobby. +cmd-respawn-help = respawn [player or UserId] + +cmd-respawn-invalid-args = Must provide <= 1 argument. +cmd-respawn-no-player = If not a player, an argument must be given. +cmd-respawn-unknown-player = Unknown player +cmd-respawn-player-not-online = Player is not currently online, but they will respawn if they come back online + +cmd-respawn-player-completion =