diff --git a/Content.Server/DeltaV/Administration/Commands/LoadCharacter.cs b/Content.Server/DeltaV/Administration/Commands/LoadCharacter.cs new file mode 100644 index 00000000000..1782028f71f --- /dev/null +++ b/Content.Server/DeltaV/Administration/Commands/LoadCharacter.cs @@ -0,0 +1,169 @@ +using System.Linq; +using Content.Server.Administration; +using Content.Server.GameTicking; +using Content.Server.Players; +using Content.Server.Preferences.Managers; +using Content.Server.Station.Systems; +using Content.Shared.Administration; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Preferences; +using Robust.Server.Player; +using Robust.Shared.Console; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; + +// This literally only exists because haha felinid oni +namespace Content.Server.DeltaV.Administration.Commands; + +[AdminCommand(AdminFlags.Admin)] +public sealed class LoadCharacter : IConsoleCommand +{ + [Dependency] private readonly IEntitySystemManager _entitySys = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IServerPreferencesManager _prefs = default!; + + public string Command => "loadcharacter"; + public string Description => Loc.GetString("loadcharacter-command-description"); + public string Help => Loc.GetString("loadcharacter-command-help"); + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (shell.Player is not IPlayerSession player) + { + shell.WriteError(Loc.GetString("shell-only-players-can-run-this-command")); + return; + } + + var data = player.ContentData(); + + if (data == null) + { + shell.WriteError(Loc.GetString("shell-entity-is-not-mob")); // No mind specific errors? :( + return; + } + + EntityUid target; + + if (args.Length >= 1) + { + if (!EntityUid.TryParse(args.First(), out var uid)) + { + shell.WriteLine(Loc.GetString("shell-entity-uid-must-be-number")); + return; + } + + target = uid; + } + else + { + if (player.AttachedEntity == null || + !_entityManager.HasComponent(player.AttachedEntity.Value)) + { + shell.WriteError(Loc.GetString("shell-must-be-attached-to-entity")); + return; + } + + target = player.AttachedEntity.Value; + } + + if (!target.IsValid() || !_entityManager.EntityExists(target)) + { + shell.WriteLine(Loc.GetString("shell-invalid-entity-id")); + return; + } + + if (!_entityManager.TryGetComponent(target, out var humanoidAppearance)) + { + shell.WriteError(Loc.GetString("shell-entity-with-uid-lacks-component", ("uid", target.ToString()), + ("componentName", nameof(HumanoidAppearanceComponent)))); + return; + } + + HumanoidCharacterProfile character; + + if (args.Length >= 2) + { + // This seems like a bad way to go about it, but it works so eh? + var name = string.Join(" ", args.Skip(1).ToArray()); + shell.WriteLine(Loc.GetString("loadcharacter-command-fetching", ("name", name))); + + if (!FetchCharacters(data.UserId, out var characters)) + { + shell.WriteError(Loc.GetString("loadcharacter-command-failed-fetching")); + return; + } + + var selectedCharacter = characters.FirstOrDefault(c => c.Name == name); + + if (selectedCharacter == null) + { + shell.WriteError(Loc.GetString("loadcharacter-command-failed-fetching")); + return; + } + + character = selectedCharacter; + } + else + character = (HumanoidCharacterProfile) _prefs.GetPreferences(data.UserId).SelectedCharacter; + + // This shouldn't ever fail considering the previous checks + if (!_prototypeManager.TryIndex(humanoidAppearance.Species, out var speciesPrototype) || + !_prototypeManager.TryIndex(character.Species, out var entPrototype)) + return; + + if (speciesPrototype != entPrototype) + shell.WriteLine(Loc.GetString("loadcharacter-command-mismatch")); + + var coordinates = player.AttachedEntity != null + ? _entityManager.GetComponent(player.AttachedEntity.Value).Coordinates + : _entitySys.GetEntitySystem().GetObserverSpawnPoint(); + + _entityManager.System() + .SpawnPlayerMob(coordinates, profile: character, entity: target, job: null, station: null); + + shell.WriteLine(Loc.GetString("loadcharacter-command-complete")); + } + + public CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + switch (args.Length) + { + case 1: + return CompletionResult.FromHint(Loc.GetString("shell-argument-uid")); + case 2: + { + var player = shell.Player as IPlayerSession; + if (player == null) + return CompletionResult.Empty; + + var data = player.ContentData(); + var mind = data?.Mind; + + if (mind == null || data == null) + return CompletionResult.Empty; + + return FetchCharacters(data.UserId, out var characters) + ? CompletionResult.FromOptions(characters.Select(c => c.Name)) + : CompletionResult.Empty; + } + default: + return CompletionResult.Empty; + } + } + + private bool FetchCharacters(NetUserId player, out HumanoidCharacterProfile[] characters) + { + characters = null!; + if (!_prefs.TryGetCachedPreferences(player, out var prefs)) + return false; + + characters = prefs.Characters + .Where(kv => kv.Value is HumanoidCharacterProfile) + .Select(kv => (HumanoidCharacterProfile) kv.Value) + .ToArray(); + + return true; + } +} diff --git a/Content.Server/DeltaV/Administration/Commands/SpawnCharacter.cs b/Content.Server/DeltaV/Administration/Commands/SpawnCharacter.cs new file mode 100644 index 00000000000..0361f4a946b --- /dev/null +++ b/Content.Server/DeltaV/Administration/Commands/SpawnCharacter.cs @@ -0,0 +1,124 @@ +using System.Linq; +using Content.Server.Administration; +using Content.Server.GameTicking; +using Content.Server.Players; +using Content.Server.Preferences.Managers; +using Content.Server.Station.Systems; +using Content.Shared.Administration; +using Content.Shared.Mind; +using Content.Shared.Preferences; +using Robust.Server.Player; +using Robust.Shared.Console; +using Robust.Shared.Network; + +namespace Content.Server.DeltaV.Administration.Commands; + +[AdminCommand(AdminFlags.Admin)] +public sealed class SpawnCharacter : IConsoleCommand +{ + [Dependency] private readonly IEntitySystemManager _entitySys = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IServerPreferencesManager _prefs = default!; + + public string Command => "spawncharacter"; + public string Description => Loc.GetString("spawncharacter-command-description"); + public string Help => Loc.GetString("spawncharacter-command-help"); + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (shell.Player is not IPlayerSession player) + { + shell.WriteError(Loc.GetString("shell-only-players-can-run-this-command")); + return; + } + + var mindSystem = _entitySys.GetEntitySystem(); + + var data = player.ContentData(); + + if (data?.UserId == null) + { + shell.WriteError(Loc.GetString("shell-entity-is-not-mob")); + return; + } + + + HumanoidCharacterProfile character; + + if (args.Length >= 1) + { + // This seems like a bad way to go about it, but it works so eh? + var name = string.Join(" ", args.ToArray()); + shell.WriteLine(Loc.GetString("loadcharacter-command-fetching", ("name", name))); + + if (!FetchCharacters(data.UserId, out var characters)) + { + shell.WriteError(Loc.GetString("loadcharacter-command-failed-fetching")); + return; + } + + var selectedCharacter = characters.FirstOrDefault(c => c.Name == name); + + if (selectedCharacter == null) + { + shell.WriteError(Loc.GetString("loadcharacter-command-failed-fetching")); + return; + } + + character = selectedCharacter; + } + else + character = (HumanoidCharacterProfile) _prefs.GetPreferences(data.UserId).SelectedCharacter; + + + var coordinates = player.AttachedEntity != null + ? _entityManager.GetComponent(player.AttachedEntity.Value).Coordinates + : _entitySys.GetEntitySystem().GetObserverSpawnPoint(); + + if (player.AttachedEntity == null || + !mindSystem.TryGetMind(player.AttachedEntity.Value, out var mindId, out var mind)) + return; + + + mindSystem.TransferTo(mindId, _entityManager.System() + .SpawnPlayerMob(coordinates, profile: character, entity: null, job: null, station: null)); + + shell.WriteLine(Loc.GetString("spawncharacter-command-complete")); + } + + public CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + { + var player = shell.Player as IPlayerSession; + if (player == null) + return CompletionResult.Empty; + + var data = player.ContentData(); + var mind = data?.Mind; + + if (mind == null || data == null) + return CompletionResult.Empty; + + return FetchCharacters(data.UserId, out var characters) + ? CompletionResult.FromOptions(characters.Select(c => c.Name)) + : CompletionResult.Empty; + } + + return CompletionResult.Empty; + } + + private bool FetchCharacters(NetUserId player, out HumanoidCharacterProfile[] characters) + { + characters = null!; + if (!_prefs.TryGetCachedPreferences(player, out var prefs)) + return false; + + characters = prefs.Characters + .Where(kv => kv.Value is HumanoidCharacterProfile) + .Select(kv => (HumanoidCharacterProfile) kv.Value) + .ToArray(); + + return true; + } +} diff --git a/Resources/Changelog/Admin.yml b/Resources/Changelog/Admin.yml index be5f2621503..57183f6c37f 100644 --- a/Resources/Changelog/Admin.yml +++ b/Resources/Changelog/Admin.yml @@ -36,3 +36,8 @@ Entries: - {message: 'Add pop sound effect when using the erase admin verb.', type: Tweak} id: 5 time: '2023-10-14T09:47:00.0000000+00:00' +- author: DebugOk + changes: + - {message: 'Add back the loadcharacter and spawncharacter commands.', type: Add} + id: 6 + time: '2023-10-19T00:00:00.0000000+00:00' diff --git a/Resources/Locale/en-US/administration/commands/load-character.ftl b/Resources/Locale/en-US/administration/commands/load-character.ftl new file mode 100644 index 00000000000..6e115e36c60 --- /dev/null +++ b/Resources/Locale/en-US/administration/commands/load-character.ftl @@ -0,0 +1,12 @@ +loadcharacter-command-description = Applies your currently selected character to an entity +loadcharacter-command-help = Usage: loadcharacter | loadcharacter | loadcharacter +loadcharacter-command-mismatch = Species mismatch detected between character and selected entity, this may have unexpected results. +loadcharacter-command-complete = Character loaded. +loadcharacter-command-fetching = Fetching character data for {$name}... +loadcharacter-command-fetching-failed = Failed to fetch character data! +loadcharacter-command-failed-fetching = Profile fetching failed??? +loadcharacter-command-hint-select = Select character + +spawncharacter-command-description = Spawns your currently selected/specified character +spawncharacter-command-help = Usage: spawncharacter | spawncharacter +spawncharacter-command-complete = Character spawned.