diff --git a/Content.Server/Administration/Commands/ForceGhostCommand.cs b/Content.Server/Administration/Commands/ForceGhostCommand.cs new file mode 100644 index 0000000000..68a77b7454 --- /dev/null +++ b/Content.Server/Administration/Commands/ForceGhostCommand.cs @@ -0,0 +1,61 @@ +using Content.Server.GameTicking; +using Content.Server.Ghost; +using Content.Shared.Administration; +using Content.Shared.GameTicking; +using Content.Shared.Mind; +using Robust.Server.Player; +using Robust.Shared.Console; + +namespace Content.Server.Administration.Commands; + +[AdminCommand(AdminFlags.Admin)] +public sealed class ForceGhostCommand : LocalizedEntityCommands +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly GhostSystem _ghost = default!; + + public override string Command => "forceghost"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length == 0 || args.Length > 1) + { + shell.WriteError(LocalizationManager.GetString("shell-wrong-arguments-number")); + return; + } + + if (!_playerManager.TryGetSessionByUsername(args[0], out var player)) + { + shell.WriteError(LocalizationManager.GetString("shell-target-player-does-not-exist")); + return; + } + + if (!_gameTicker.PlayerGameStatuses.TryGetValue(player.UserId, out var playerStatus) || + playerStatus is not PlayerGameStatus.JoinedGame) + { + shell.WriteLine(Loc.GetString("cmd-forceghost-error-lobby")); + return; + } + + if (!_mind.TryGetMind(player, out var mindId, out var mind)) + (mindId, mind) = _mind.CreateMind(player.UserId); + + if (!_ghost.OnGhostAttempt(mindId, false, true, true, mind)) + shell.WriteLine(Loc.GetString("cmd-forceghost-denied")); + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + { + return CompletionResult.FromHintOptions( + CompletionHelper.SessionNames(players: _playerManager), + Loc.GetString("cmd-forceghost-hint")); + } + + return CompletionResult.Empty; + } +} diff --git a/Content.Server/Ghost/GhostCommand.cs b/Content.Server/Ghost/GhostCommand.cs index 927f9c8082..f5df115fde 100644 --- a/Content.Server/Ghost/GhostCommand.cs +++ b/Content.Server/Ghost/GhostCommand.cs @@ -50,7 +50,7 @@ namespace Content.Server.Ghost mind = _entities.GetComponent(mindId); } - if (!_entities.System().OnGhostAttempt(mindId, true, true, mind)) + if (!_entities.System().OnGhostAttempt(mindId, true, true, mind: mind)) { shell.WriteLine(Loc.GetString("ghost-command-denied")); } diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs index b5e9894ee3..5bfa208b1c 100644 --- a/Content.Server/Ghost/GhostSystem.cs +++ b/Content.Server/Ghost/GhostSystem.cs @@ -499,7 +499,7 @@ namespace Content.Server.Ghost return ghost; } - public bool OnGhostAttempt(EntityUid mindId, bool canReturnGlobal, bool viaCommand = false, MindComponent? mind = null) + public bool OnGhostAttempt(EntityUid mindId, bool canReturnGlobal, bool viaCommand = false, bool forced = false, MindComponent? mind = null) { if (!Resolve(mindId, ref mind)) return false; @@ -507,7 +507,12 @@ namespace Content.Server.Ghost var playerEntity = mind.CurrentEntity; if (playerEntity != null && viaCommand) - _adminLog.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} is attempting to ghost via command"); + { + if (forced) + _adminLog.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} was forced to ghost via command"); + else + _adminLog.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} is attempting to ghost via command"); + } var handleEv = new GhostAttemptHandleEvent(mind, canReturnGlobal); RaiseLocalEvent(handleEv); @@ -516,7 +521,7 @@ namespace Content.Server.Ghost if (handleEv.Handled) return handleEv.Result; - if (mind.PreventGhosting) + if (mind.PreventGhosting && !forced) { if (mind.Session != null) // Logging is suppressed to prevent spam from ghost attempts caused by movement attempts { diff --git a/Resources/Locale/en-US/administration/commands/forceghost.ftl b/Resources/Locale/en-US/administration/commands/forceghost.ftl new file mode 100644 index 0000000000..9d74409125 --- /dev/null +++ b/Resources/Locale/en-US/administration/commands/forceghost.ftl @@ -0,0 +1,6 @@ +cmd-forceghost-desc = Makes a player an observer. +cmd-forceghost-help = Usage: forceghost + +cmd-forceghost-error-lobby = Target player can't ghost right now. They are not in the game! +cmd-forceghost-denied = Failed to ghost the target player. +cmd-forceghost-hint =