From e2b22a4cd8eebadfce6d80c3abeb48fcff235b2a Mon Sep 17 00:00:00 2001 From: Moony Date: Wed, 2 Aug 2023 16:09:08 -0500 Subject: [PATCH] Toolshed (#17895) * ogh * i should save my work * ogh * hhcdfhjbghshbxdfhghshc - lots of bugs in parsing still - invocation is a stub * expr parsing works * awawa * Saving work * Improve APIs a bit all around, add shortcuts. * awa * awa * AAAAAA * save work * Move shit to engine * lord * bql is kill * forgot the fucking bike rack * bql is kill for real * pjb will kill me * aughfhbdj * adgddf * gdsgvfvxshngfgh * b * hfsjhghj * a * tf you mean i have to document it * follow C# standards * Assorted cleanup and documentation pass, minor bugfix in ValueRefParser. * Start porting old commands, remove that pesky prefix in favor of integrating with the shell. * bw * Fix valueref up a bit, improve autocomplete for it. * awa * fix tests * git shut up * Arithmetic commands. * parse improvements * Update engine. --------- Co-authored-by: moonheart08 --- ...lResultsEui.cs => ToolshedVisualizeEui.cs} | 10 +- ...ndow.xaml => ToolshedVisualizeWindow.xaml} | 0 ...aml.cs => ToolshedVisualizeWindow.xaml.cs} | 4 +- Content.Client/Content.Client.csproj | 3 + Content.Client/Stylesheets/StyleNano.cs | 3 + .../Tests/Commands/RejuvenateTest.cs | 6 +- .../Tests/Toolshed/AdminTest.cs | 20 +++ .../Tests/Toolshed/CommandParseTest.cs | 84 ++++++++++ .../Tests/Toolshed/LocTest.cs | 19 +++ .../Tests/Toolshed/ToolshedTest.cs | 153 ++++++++++++++++++ .../Administration/AdminCommandAttribute.cs | 1 - .../Commands/DeleteEntitiesWithComponent.cs | 47 ------ .../Commands/DeleteEntitiesWithId.cs | 36 ----- .../Commands/DeleteEntityCommand.cs | 39 ----- .../Commands/FindEntitiesWithComponents.cs | 68 -------- .../Commands/RejuvenateCommand.cs | 48 ------ .../Station/AdjustStationJobCommand.cs | 84 ---------- .../Station/ListStationJobsCommand.cs | 55 ------- .../Commands/Station/ListStationsCommand.cs | 29 ---- .../Commands/Station/RenameStationCommand.cs | 55 ------- .../Administration/Commands/TagCommands.cs | 113 ------------- .../Administration/Managers/AdminManager.cs | 124 +++++++++++++- .../Administration/Managers/IAdminManager.cs | 3 + .../Systems/AdminVerbSystem.Tools.cs | 2 +- .../Administration/Systems/AdminVerbSystem.cs | 20 ++- .../Systems/RejuvenateSystem.cs | 11 ++ .../Administration/Toolshed/AdminsCommand.cs | 24 +++ .../Administration/Toolshed/MarkedCommand.cs | 16 ++ .../Toolshed/RejuvenateCommand.cs | 22 +++ .../Toolshed/SolutionCommand.cs | 49 ++++++ .../Administration/Toolshed/TagCommand.cs | 106 ++++++++++++ Content.Server/Bql/BqlSelectCommand.cs | 56 ------- Content.Server/Bql/QuerySelectors.cs | 137 ---------------- Content.Server/Entry/EntryPoint.cs | 4 - Content.Server/IoC/ServerContentIoC.cs | 2 + .../NewCon/Commands/AdminDebug/ACmdCommand.cs | 34 ++++ .../NewCon/Commands/Verbs/RunVerbAsCommand.cs | 62 +++++++ .../NewCon/Commands/VisualizeCommand.cs | 51 ++++++ .../Station/Commands/JobsCommand.cs | 129 +++++++++++++++ .../Station/Commands/StationCommand.cs | 126 +++++++++++++++ .../Administration/AnyCommandAttribute.cs | 2 +- ...iState.cs => ToolshedVisualizeEuiState.cs} | 4 +- .../en-US/administration/admin-verbs.ftl | 2 + .../en-US/commands/toolshed-commands.ftl | 56 +++++++ Resources/engineCommandPerms.yml | 4 +- Resources/toolshedEngineCommandPerms.yml | 70 ++++++++ RobustToolbox | 2 +- SpaceStation14.sln.DotSettings | 2 + 48 files changed, 1204 insertions(+), 793 deletions(-) rename Content.Client/Bql/{BqlResultsEui.cs => ToolshedVisualizeEui.cs} (74%) rename Content.Client/Bql/{BqlResultsWindow.xaml => ToolshedVisualizeWindow.xaml} (100%) rename Content.Client/Bql/{BqlResultsWindow.xaml.cs => ToolshedVisualizeWindow.xaml.cs} (91%) create mode 100644 Content.IntegrationTests/Tests/Toolshed/AdminTest.cs create mode 100644 Content.IntegrationTests/Tests/Toolshed/CommandParseTest.cs create mode 100644 Content.IntegrationTests/Tests/Toolshed/LocTest.cs create mode 100644 Content.IntegrationTests/Tests/Toolshed/ToolshedTest.cs delete mode 100644 Content.Server/Administration/Commands/DeleteEntitiesWithComponent.cs delete mode 100644 Content.Server/Administration/Commands/DeleteEntitiesWithId.cs delete mode 100644 Content.Server/Administration/Commands/DeleteEntityCommand.cs delete mode 100644 Content.Server/Administration/Commands/FindEntitiesWithComponents.cs delete mode 100644 Content.Server/Administration/Commands/RejuvenateCommand.cs delete mode 100644 Content.Server/Administration/Commands/Station/AdjustStationJobCommand.cs delete mode 100644 Content.Server/Administration/Commands/Station/ListStationJobsCommand.cs delete mode 100644 Content.Server/Administration/Commands/Station/ListStationsCommand.cs delete mode 100644 Content.Server/Administration/Commands/Station/RenameStationCommand.cs delete mode 100644 Content.Server/Administration/Commands/TagCommands.cs create mode 100644 Content.Server/Administration/Systems/RejuvenateSystem.cs create mode 100644 Content.Server/Administration/Toolshed/AdminsCommand.cs create mode 100644 Content.Server/Administration/Toolshed/MarkedCommand.cs create mode 100644 Content.Server/Administration/Toolshed/RejuvenateCommand.cs create mode 100644 Content.Server/Administration/Toolshed/SolutionCommand.cs create mode 100644 Content.Server/Administration/Toolshed/TagCommand.cs delete mode 100644 Content.Server/Bql/BqlSelectCommand.cs delete mode 100644 Content.Server/Bql/QuerySelectors.cs create mode 100644 Content.Server/NewCon/Commands/AdminDebug/ACmdCommand.cs create mode 100644 Content.Server/NewCon/Commands/Verbs/RunVerbAsCommand.cs create mode 100644 Content.Server/NewCon/Commands/VisualizeCommand.cs create mode 100644 Content.Server/Station/Commands/JobsCommand.cs create mode 100644 Content.Server/Station/Commands/StationCommand.cs rename Content.Shared/Bql/{BqlResultsEuiState.cs => ToolshedVisualizeEuiState.cs} (62%) create mode 100644 Resources/Locale/en-US/commands/toolshed-commands.ftl create mode 100644 Resources/toolshedEngineCommandPerms.yml diff --git a/Content.Client/Bql/BqlResultsEui.cs b/Content.Client/Bql/ToolshedVisualizeEui.cs similarity index 74% rename from Content.Client/Bql/BqlResultsEui.cs rename to Content.Client/Bql/ToolshedVisualizeEui.cs index 3e9bda57ff..ccefc1228d 100644 --- a/Content.Client/Bql/BqlResultsEui.cs +++ b/Content.Client/Bql/ToolshedVisualizeEui.cs @@ -7,13 +7,13 @@ using Robust.Client.Console; namespace Content.Client.Bql; [UsedImplicitly] -public sealed class BqlResultsEui : BaseEui +public sealed class ToolshedVisualizeEui : BaseEui { - private readonly BqlResultsWindow _window; + private readonly ToolshedVisualizeWindow _window; - public BqlResultsEui() + public ToolshedVisualizeEui() { - _window = new BqlResultsWindow( + _window = new ToolshedVisualizeWindow( IoCManager.Resolve(), IoCManager.Resolve() ); @@ -23,7 +23,7 @@ public sealed class BqlResultsEui : BaseEui public override void HandleState(EuiStateBase state) { - if (state is not BqlResultsEuiState castState) + if (state is not ToolshedVisualizeEuiState castState) return; _window.Update(castState.Entities); diff --git a/Content.Client/Bql/BqlResultsWindow.xaml b/Content.Client/Bql/ToolshedVisualizeWindow.xaml similarity index 100% rename from Content.Client/Bql/BqlResultsWindow.xaml rename to Content.Client/Bql/ToolshedVisualizeWindow.xaml diff --git a/Content.Client/Bql/BqlResultsWindow.xaml.cs b/Content.Client/Bql/ToolshedVisualizeWindow.xaml.cs similarity index 91% rename from Content.Client/Bql/BqlResultsWindow.xaml.cs rename to Content.Client/Bql/ToolshedVisualizeWindow.xaml.cs index 4a9fde855a..0265e3343e 100644 --- a/Content.Client/Bql/BqlResultsWindow.xaml.cs +++ b/Content.Client/Bql/ToolshedVisualizeWindow.xaml.cs @@ -8,12 +8,12 @@ using Robust.Client.UserInterface.XAML; namespace Content.Client.Bql; [GenerateTypedNameReferences] -internal sealed partial class BqlResultsWindow : DefaultWindow +internal sealed partial class ToolshedVisualizeWindow : DefaultWindow { private readonly IClientConsoleHost _console; private readonly ILocalizationManager _loc; - public BqlResultsWindow(IClientConsoleHost console, ILocalizationManager loc) + public ToolshedVisualizeWindow(IClientConsoleHost console, ILocalizationManager loc) { _console = console; _loc = loc; diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj index 636233395c..29488a7ae1 100644 --- a/Content.Client/Content.Client.csproj +++ b/Content.Client/Content.Client.csproj @@ -33,6 +33,9 @@ StrippingWindow.xaml + + ToolshedVisualizeWindow.xaml + diff --git a/Content.Client/Stylesheets/StyleNano.cs b/Content.Client/Stylesheets/StyleNano.cs index 5dcbc9b3ae..cb90e5c481 100644 --- a/Content.Client/Stylesheets/StyleNano.cs +++ b/Content.Client/Stylesheets/StyleNano.cs @@ -162,6 +162,7 @@ namespace Content.Client.Stylesheets var notoSansBold16 = resCache.NotoStack(variation: "Bold", size: 16); var notoSansBold18 = resCache.NotoStack(variation: "Bold", size: 18); var notoSansBold20 = resCache.NotoStack(variation: "Bold", size: 20); + var notoSansMono = resCache.GetFont("/EngineFonts/NotoSans/NotoSansMono-Regular.ttf", size: 12); var windowHeaderTex = resCache.GetTexture("/Textures/Interface/Nano/window_header.png"); var windowHeader = new StyleBoxTexture { @@ -512,6 +513,8 @@ namespace Content.Client.Stylesheets Stylesheet = new Stylesheet(BaseRules.Concat(new[] { + Element().Class("monospace") + .Prop("font", notoSansMono), // Window title. new StyleRule( new SelectorElement(typeof(Label), new[] {DefaultWindow.StyleClassWindowTitle}, null, null), diff --git a/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs b/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs index ddd5e97de7..1fbc6c7532 100644 --- a/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs +++ b/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs @@ -1,4 +1,5 @@ using Content.Server.Administration.Commands; +using Content.Server.Administration.Systems; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; using Content.Shared.FixedPoint; @@ -11,7 +12,7 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Commands { [TestFixture] - [TestOf(typeof(RejuvenateCommand))] + [TestOf(typeof(RejuvenateSystem))] public sealed class RejuvenateTest { private const string Prototypes = @" @@ -42,6 +43,7 @@ namespace Content.IntegrationTests.Tests.Commands var prototypeManager = server.ResolveDependency(); var mobStateSystem = entManager.EntitySysManager.GetEntitySystem(); var damSystem = entManager.EntitySysManager.GetEntitySystem(); + var rejuvenateSystem = entManager.EntitySysManager.GetEntitySystem(); await server.WaitAssertion(() => { @@ -78,7 +80,7 @@ namespace Content.IntegrationTests.Tests.Commands }); // Rejuvenate them - RejuvenateCommand.PerformRejuvenate(human); + rejuvenateSystem.PerformRejuvenate(human); // Check that it is alive and with no damage Assert.Multiple(() => diff --git a/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs b/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs new file mode 100644 index 0000000000..2c52d5d5c2 --- /dev/null +++ b/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; +using Robust.Shared.Toolshed; + +namespace Content.IntegrationTests.Tests.Toolshed; + +[TestFixture] +public sealed class AdminTest : ToolshedTest +{ + [Test] + public async Task AllCommandsHavePermissions() + { + await Server.WaitAssertion(() => + { + Assert.That(InvokeCommand("cmd:list where { acmd:perms isnull }", out var res)); + var list = ((IEnumerable) res).ToList(); + Assert.That(list, Is.Empty, "All commands must have admin permissions set up."); + }); + } +} diff --git a/Content.IntegrationTests/Tests/Toolshed/CommandParseTest.cs b/Content.IntegrationTests/Tests/Toolshed/CommandParseTest.cs new file mode 100644 index 0000000000..f997db1c48 --- /dev/null +++ b/Content.IntegrationTests/Tests/Toolshed/CommandParseTest.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using Robust.Shared.GameObjects; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; + +namespace Content.IntegrationTests.Tests.Toolshed; + +[TestFixture] +public sealed class CommandRunTest : ToolshedTest +{ + [Test] + public async Task SimpleCommandRun() + { + await Server.WaitAssertion(() => + { + ParseCommand("entities"); + ParseCommand("entities select 1"); + ParseCommand("entities with Item select 1"); + + ExpectError(); + ParseCommand("entities with"); + + ExpectError(); + ParseCommand("player:list with MetaData"); + + ExpectError(); + ParseCommand("player:list", expectedType: typeof(IEnumerable)); + + ParseCommand("entities not with MetaData"); + ParseCommand("with MetaData select 2 any", inputType: typeof(List)); + + ParseCommand("entities not with MetaData => $myEntities"); + ParseCommand("=> $fooBar with MetaData", inputType: typeof(List)); + }); + } + + [Test] + public async Task EntityUidTypeParser() + { + await Server.WaitAssertion(() => + { + ParseCommand("ent 1"); + ParseCommand("ent c1"); + + ExpectError(); + ParseCommand("ent foodigity"); + }); + } + + [Test] + public async Task QuantityTypeParser() + { + await Server.WaitAssertion(() => + { + ParseCommand("entities select 100"); + ParseCommand("entities select 50%"); + + ExpectError(); + ParseCommand("entities select -1"); + + ExpectError(); + ParseCommand("entities select 200%"); + + ExpectError(); + ParseCommand("entities select hotdog"); + }); + } + + [Test] + public async Task ComponentTypeParser() + { + await Server.WaitAssertion(() => + { + ParseCommand("entities with MetaData"); + + ExpectError(); + ParseCommand("entities with Foodiddy"); + + ExpectError(); + ParseCommand("entities with MetaDataComponent"); + }); + } +} diff --git a/Content.IntegrationTests/Tests/Toolshed/LocTest.cs b/Content.IntegrationTests/Tests/Toolshed/LocTest.cs new file mode 100644 index 0000000000..db8d451dbc --- /dev/null +++ b/Content.IntegrationTests/Tests/Toolshed/LocTest.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; +using Robust.Shared.Toolshed; + +namespace Content.IntegrationTests.Tests.Toolshed; + +[TestFixture] +public sealed class LocTest : ToolshedTest +{ + [Test] + public async Task AllCommandsHaveDescriptions() + { + await Server.WaitAssertion(() => + { + Assert.That(InvokeCommand("cmd:list where { cmd:descloc loc:tryloc isnull }", out var res)); + Assert.That((IEnumerable)res, Is.Empty, "All commands must have localized descriptions."); + }); + } +} diff --git a/Content.IntegrationTests/Tests/Toolshed/ToolshedTest.cs b/Content.IntegrationTests/Tests/Toolshed/ToolshedTest.cs new file mode 100644 index 0000000000..54df7b7335 --- /dev/null +++ b/Content.IntegrationTests/Tests/Toolshed/ToolshedTest.cs @@ -0,0 +1,153 @@ +#nullable enable +using System.Collections.Generic; +using Content.Server.Administration.Managers; +using Robust.Server.Player; +using Robust.Shared.Players; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Errors; +using Robust.Shared.Toolshed.Syntax; +using Robust.UnitTesting; + +namespace Content.IntegrationTests.Tests.Toolshed; + +[TestFixture] +[FixtureLifeCycle(LifeCycle.SingleInstance)] +public abstract class ToolshedTest : IInvocationContext +{ + protected PairTracker PairTracker = default!; + + protected virtual bool NoClient => true; + protected virtual bool AssertOnUnexpectedError => true; + + protected RobustIntegrationTest.ServerIntegrationInstance Server = default!; + protected RobustIntegrationTest.ClientIntegrationInstance? Client = null; + protected ToolshedManager Toolshed = default!; + protected IAdminManager AdminManager = default!; + + protected IInvocationContext? Context = null; + + [TearDown] + public virtual async Task TearDown() + { + Assert.IsEmpty(_expectedErrors); + ClearErrors(); + } + + [OneTimeSetUp] + public virtual async Task Setup() + { + PairTracker = await PoolManager.GetServerClient(new PoolSettings {NoClient = NoClient}); + Server = PairTracker.Pair.Server; + + if (!NoClient) + { + Client = PairTracker.Pair.Client; + await Client.WaitIdleAsync(); + } + + await Server.WaitIdleAsync(); + + Toolshed = Server.ResolveDependency(); + AdminManager = Server.ResolveDependency(); + } + + protected bool InvokeCommand(string command, out object? result, IPlayerSession? session = null) + { + return Toolshed.InvokeCommand(this, command, null, out result); + } + + protected void ParseCommand(string command, Type? inputType = null, Type? expectedType = null, bool once = false) + { + var parser = new ForwardParser(command, Toolshed); + var success = CommandRun.TryParse(false, false, parser, inputType, expectedType, once, out _, out _, out var error); + + if (error is not null) + ReportError(error); + + if (error is null) + Assert.That(success, $"Parse failed despite no error being reported. Parsed {command}"); + } + + public bool CheckInvokable(CommandSpec command, out IConError? error) + { + if (Context is not null) + { + return Context.CheckInvokable(command, out error); + } + + error = null; + return true; + } + + protected IPlayerSession? InvocationSession { get; set; } + + public ICommonSession? Session + { + get + { + if (Context is not null) + { + return Context.Session; + } + + return InvocationSession; + } + } + + public void WriteLine(string line) + { + return; + } + + private Queue _expectedErrors = new(); + + private List _errors = new(); + + public void ReportError(IConError err) + { + if (_expectedErrors.Count == 0) + { + if (AssertOnUnexpectedError) + { + Assert.Fail($"Got an error, {err.GetType()}, when none was expected.\n{err.Describe()}"); + } + + goto done; + } + + var ty = _expectedErrors.Dequeue(); + + if (AssertOnUnexpectedError) + { + Assert.That( + err.GetType().IsAssignableTo(ty), + $"The error {err.GetType()} wasn't assignable to the expected type {ty}.\n{err.Describe()}" + ); + } + + done: + _errors.Add(err); + } + + public IEnumerable GetErrors() + { + return _errors; + } + + public void ClearErrors() + { + _errors.Clear(); + } + + public Dictionary Variables { get; } = new(); + + protected void ExpectError(Type err) + { + _expectedErrors.Enqueue(err); + } + + protected void ExpectError() + { + _expectedErrors.Enqueue(typeof(T)); + } +} diff --git a/Content.Server/Administration/AdminCommandAttribute.cs b/Content.Server/Administration/AdminCommandAttribute.cs index e115be2ef8..409d9fe984 100644 --- a/Content.Server/Administration/AdminCommandAttribute.cs +++ b/Content.Server/Administration/AdminCommandAttribute.cs @@ -12,7 +12,6 @@ namespace Content.Server.Administration /// /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] - [BaseTypeRequired(typeof(IConsoleCommand))] [MeansImplicitUse] public sealed class AdminCommandAttribute : Attribute { diff --git a/Content.Server/Administration/Commands/DeleteEntitiesWithComponent.cs b/Content.Server/Administration/Commands/DeleteEntitiesWithComponent.cs deleted file mode 100644 index 578ef2e414..0000000000 --- a/Content.Server/Administration/Commands/DeleteEntitiesWithComponent.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Linq; -using Content.Shared.Administration; -using Robust.Shared.Console; - -namespace Content.Server.Administration.Commands -{ - [AdminCommand(AdminFlags.Spawn)] - sealed class DeleteEntitiesWithComponent : IConsoleCommand - { - public string Command => "deleteewc"; - - public string Description => Loc.GetString("delete-entities-with-component-command-description"); - - public string Help => Loc.GetString("delete-entities-with-component-command-help-text"); - - public void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length < 1) - { - shell.WriteLine(Help); - return; - } - - var factory = IoCManager.Resolve(); - - var components = new List(); - foreach (var arg in args) - { - components.Add(factory.GetRegistration(arg).Type); - } - - var entityManager = IoCManager.Resolve(); - - var entitiesWithComponents = components.Select(c => entityManager.GetAllComponents(c).Select(x => x.Uid)); - var entitiesWithAllComponents = entitiesWithComponents.Skip(1).Aggregate(new HashSet(entitiesWithComponents.First()), (h, e) => { h.IntersectWith(e); return h; }); - - var count = 0; - foreach (var entity in entitiesWithAllComponents) - { - entityManager.DeleteEntity(entity); - count += 1; - } - - shell.WriteLine(Loc.GetString("delete-entities-with-component-command-deleted-components",("count", count))); - } - } -} diff --git a/Content.Server/Administration/Commands/DeleteEntitiesWithId.cs b/Content.Server/Administration/Commands/DeleteEntitiesWithId.cs deleted file mode 100644 index 82be56ee36..0000000000 --- a/Content.Server/Administration/Commands/DeleteEntitiesWithId.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Linq; -using Content.Shared.Administration; -using Robust.Shared.Console; - -namespace Content.Server.Administration.Commands -{ - [AdminCommand(AdminFlags.Spawn)] - public sealed class DeleteEntitiesWithId : IConsoleCommand - { - public string Command => "deleteewi"; - public string Description => "Deletes entities with the specified prototype ID."; - public string Help => $"Usage: {Command} "; - - public void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length != 1) - { - shell.WriteLine(Help); - return; - } - - var id = args[0].ToLower(); - var entityManager = IoCManager.Resolve(); - var entities = entityManager.GetEntities().Where(e => entityManager.GetComponent(e).EntityPrototype?.ID.ToLower() == id); - var i = 0; - - foreach (var entity in entities) - { - entityManager.DeleteEntity(entity); - i++; - } - - shell.WriteLine($"Deleted all entities with id {id}. Occurrences: {i}"); - } - } -} diff --git a/Content.Server/Administration/Commands/DeleteEntityCommand.cs b/Content.Server/Administration/Commands/DeleteEntityCommand.cs deleted file mode 100644 index 5a7df2ac7f..0000000000 --- a/Content.Server/Administration/Commands/DeleteEntityCommand.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Content.Shared.Administration; -using Robust.Shared.Console; - -namespace Content.Server.Administration.Commands -{ - [AdminCommand(AdminFlags.Spawn)] - public sealed class DeleteEntityCommand : IConsoleCommand - { - public string Command => "deleteentity"; - public string Description => "Deletes an entity with the given id."; - public string Help => $"Usage: {Command} "; - - public void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length != 1) - { - shell.WriteLine($"Invalid amount of arguments.\n{Help}"); - return; - } - - if (!EntityUid.TryParse(args[0], out var id)) - { - shell.WriteLine($"{args[0]} is not a valid entity id."); - return; - } - - var entityManager = IoCManager.Resolve(); - - if (!entityManager.EntityExists(id)) - { - shell.WriteLine($"No entity found with id {id}."); - return; - } - - entityManager.DeleteEntity(id); - shell.WriteLine($"Deleted entity with id {id}."); - } - } -} diff --git a/Content.Server/Administration/Commands/FindEntitiesWithComponents.cs b/Content.Server/Administration/Commands/FindEntitiesWithComponents.cs deleted file mode 100644 index b583b56db6..0000000000 --- a/Content.Server/Administration/Commands/FindEntitiesWithComponents.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Linq; -using Content.Shared.Administration; -using Robust.Shared.Console; - -namespace Content.Server.Administration.Commands -{ - [AdminCommand(AdminFlags.Mapping)] - public sealed class FindEntitiesWithComponents : IConsoleCommand - { - public string Command => "findentitieswithcomponents"; - public string Description => "Finds entities with all of the specified components."; - public string Help => $"{Command} ..."; - - public void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length == 0) - { - shell.WriteLine($"Invalid amount of arguments: {args.Length}.\n{Help}"); - return; - } - - var components = new List(); - var componentFactory = IoCManager.Resolve(); - var invalidArgs = new List(); - - foreach (var arg in args) - { - if (!componentFactory.TryGetRegistration(arg, out var registration)) - { - invalidArgs.Add(arg); - continue; - } - - components.Add(registration.Type); - } - - if (invalidArgs.Count > 0) - { - shell.WriteLine($"No component found for component names: {string.Join(", ", invalidArgs)}"); - return; - } - - var entityManager = IoCManager.Resolve(); - var entityIds = new HashSet(); - - var entitiesWithComponents = components.Select(c => entityManager.GetAllComponents(c).Select(x => x.Uid)).ToArray(); - var entitiesWithAllComponents = entitiesWithComponents.Skip(1).Aggregate(new HashSet(entitiesWithComponents.First()), (h, e) => { h.IntersectWith(e); return h; }); - - foreach (var entity in entitiesWithAllComponents) - { - if (entityManager.GetComponent(entity).EntityPrototype is not { } prototypeId) - { - continue; - } - - entityIds.Add(prototypeId.ID); - } - - if (entityIds.Count == 0) - { - shell.WriteLine($"No entities found with components {string.Join(", ", args)}."); - return; - } - - shell.WriteLine($"{entityIds.Count} entities found:\n{string.Join("\n", entityIds)}"); - } - } -} diff --git a/Content.Server/Administration/Commands/RejuvenateCommand.cs b/Content.Server/Administration/Commands/RejuvenateCommand.cs deleted file mode 100644 index 0e2e39a9ee..0000000000 --- a/Content.Server/Administration/Commands/RejuvenateCommand.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Content.Shared.Administration; -using Content.Shared.Rejuvenate; -using Robust.Server.Player; -using Robust.Shared.Console; - -namespace Content.Server.Administration.Commands -{ - [AdminCommand(AdminFlags.Admin)] - public sealed class RejuvenateCommand : IConsoleCommand - { - public string Command => "rejuvenate"; - - public string Description => Loc.GetString("rejuvenate-command-description"); - - public string Help => Loc.GetString("rejuvenate-command-help-text"); - - public void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length < 1 && shell.Player is IPlayerSession player) //Try to heal the users mob if applicable - { - shell.WriteLine(Loc.GetString("rejuvenate-command-self-heal-message")); - if (player.AttachedEntity == null) - { - shell.WriteLine(Loc.GetString("rejuvenate-command-no-entity-attached-message")); - return; - } - PerformRejuvenate(player.AttachedEntity.Value); - } - - var entityManager = IoCManager.Resolve(); - foreach (var arg in args) - { - if (!EntityUid.TryParse(arg, out var entity) || !entityManager.EntityExists(entity)) - { - shell.WriteLine(Loc.GetString("shell-could-not-find-entity",("entity", arg))); - continue; - } - PerformRejuvenate(entity); - } - } - - public static void PerformRejuvenate(EntityUid target) - { - var entityManager = IoCManager.Resolve(); - entityManager.EventBus.RaiseLocalEvent(target, new RejuvenateEvent()); - } - } -} diff --git a/Content.Server/Administration/Commands/Station/AdjustStationJobCommand.cs b/Content.Server/Administration/Commands/Station/AdjustStationJobCommand.cs deleted file mode 100644 index 3af9ed6b26..0000000000 --- a/Content.Server/Administration/Commands/Station/AdjustStationJobCommand.cs +++ /dev/null @@ -1,84 +0,0 @@ -using Content.Server.Commands; -using Content.Server.Station.Components; -using Content.Server.Station.Systems; -using Content.Shared.Administration; -using Content.Shared.Roles; -using Robust.Shared.Console; -using Robust.Shared.Prototypes; - -namespace Content.Server.Administration.Commands.Station; - -[AdminCommand(AdminFlags.Round)] -public sealed class AdjustStationJobCommand : IConsoleCommand -{ - [Dependency] private readonly IEntityManager _entityManager = default!; - [Dependency] private readonly IEntitySystemManager _entSysManager = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - - public string Command => "adjstationjob"; - - public string Description => "Adjust the job manifest on a station."; - - public string Help => "adjstationjob "; - - public void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length != 3) - { - shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); - return; - } - - var stationJobs = _entSysManager.GetEntitySystem(); - - if (!EntityUid.TryParse(args[0], out var station) || !_entityManager.HasComponent(station)) - { - shell.WriteError(Loc.GetString("shell-argument-station-id-invalid", ("index", 1))); - return; - } - - if (!_prototypeManager.TryIndex(args[1], out var job)) - { - shell.WriteError(Loc.GetString("shell-argument-must-be-prototype", - ("index", 2), ("prototypeName", nameof(JobPrototype)))); - return; - } - - if (!int.TryParse(args[2], out var amount) || amount < -1) - { - shell.WriteError(Loc.GetString("shell-argument-number-must-be-between", - ("index", 3), ("lower", -1), ("upper", int.MaxValue))); - return; - } - - if (amount == -1) - { - stationJobs.MakeJobUnlimited(station, job); - return; - } - - stationJobs.TrySetJobSlot(station, job, amount, true); - } - - public CompletionResult GetCompletion(IConsoleShell shell, string[] args) - { - if (args.Length == 1) - { - var options = ContentCompletionHelper.StationIds(_entityManager); - return CompletionResult.FromHintOptions(options, ""); - } - - if (args.Length == 2) - { - var options = CompletionHelper.PrototypeIDs(); - return CompletionResult.FromHintOptions(options, ""); - } - - if (args.Length == 3) - { - return CompletionResult.FromHint(""); - } - - return CompletionResult.Empty; - } -} diff --git a/Content.Server/Administration/Commands/Station/ListStationJobsCommand.cs b/Content.Server/Administration/Commands/Station/ListStationJobsCommand.cs deleted file mode 100644 index 53a902ec29..0000000000 --- a/Content.Server/Administration/Commands/Station/ListStationJobsCommand.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Content.Server.Commands; -using Content.Server.Station.Components; -using Content.Server.Station.Systems; -using Content.Shared.Administration; -using Robust.Shared.Console; - -namespace Content.Server.Administration.Commands.Station; - -[AdminCommand(AdminFlags.Admin)] -public sealed class ListStationJobsCommand : IConsoleCommand -{ - [Dependency] private readonly IEntityManager _entityManager = default!; - [Dependency] private readonly IEntitySystemManager _entSysManager = default!; - - public string Command => "lsstationjobs"; - - public string Description => "Lists all jobs on the given station."; - - public string Help => "lsstationjobs "; - - public void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length != 1) - { - shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); - return; - } - - var stationSystem = _entSysManager.GetEntitySystem(); - var stationJobs = _entSysManager.GetEntitySystem(); - - if (!EntityUid.TryParse(args[0], out var station) || !_entityManager.HasComponent(station)) - { - shell.WriteError(Loc.GetString("shell-argument-station-id-invalid", ("index", 1))); - return; - } - - foreach (var (job, amount) in stationJobs.GetJobs(station)) - { - var amountText = amount is null ? "Infinite" : amount.ToString(); - shell.WriteLine($"{job}: {amountText}"); - } - } - - public CompletionResult GetCompletion(IConsoleShell shell, string[] args) - { - if (args.Length == 1) - { - var options = ContentCompletionHelper.StationIds(_entityManager); - return CompletionResult.FromHintOptions(options, ""); - } - - return CompletionResult.Empty; - } -} diff --git a/Content.Server/Administration/Commands/Station/ListStationsCommand.cs b/Content.Server/Administration/Commands/Station/ListStationsCommand.cs deleted file mode 100644 index 0877febe4d..0000000000 --- a/Content.Server/Administration/Commands/Station/ListStationsCommand.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Content.Server.Station.Components; -using Content.Server.Station.Systems; -using Content.Shared.Administration; -using Robust.Shared.Console; - -namespace Content.Server.Administration.Commands.Station; - -[AdminCommand(AdminFlags.Admin)] -public sealed class ListStationsCommand : IConsoleCommand -{ - [Dependency] private readonly IEntityManager _entityManager = default!; - - public string Command => "lsstations"; - - public string Description => "List all active stations"; - - public string Help => "lsstations"; - - public void Execute(IConsoleShell shell, string argStr, string[] args) - { - var query = _entityManager.EntityQueryEnumerator(); - - while (query.MoveNext(out var station, out _)) - { - var name = _entityManager.GetComponent(station).EntityName; - shell.WriteLine($"{station, -10} | {name}"); - } - } -} diff --git a/Content.Server/Administration/Commands/Station/RenameStationCommand.cs b/Content.Server/Administration/Commands/Station/RenameStationCommand.cs deleted file mode 100644 index 10df9049fd..0000000000 --- a/Content.Server/Administration/Commands/Station/RenameStationCommand.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Content.Server.Commands; -using Content.Server.Station.Components; -using Content.Server.Station.Systems; -using Content.Shared.Administration; -using Robust.Shared.Console; - -namespace Content.Server.Administration.Commands.Station; - -[AdminCommand(AdminFlags.Admin)] -public sealed class RenameStationCommand : IConsoleCommand -{ - [Dependency] private readonly IEntityManager _entityManager = default!; - [Dependency] private readonly IEntitySystemManager _entSysManager = default!; - - public string Command => "renamestation"; - - public string Description => "Renames the given station"; - - public string Help => "renamestation "; - - public void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length != 2) - { - shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); - return; - } - - var stationSystem = _entSysManager.GetEntitySystem(); - - if (!EntityUid.TryParse(args[0], out var station) || !_entityManager.HasComponent(station)) - { - shell.WriteError(Loc.GetString("shell-argument-station-id-invalid", ("index", 1))); - return; - } - - stationSystem.RenameStation(station, args[1]); - } - - public CompletionResult GetCompletion(IConsoleShell shell, string[] args) - { - if (args.Length == 1) - { - var options = ContentCompletionHelper.StationIds(_entityManager); - return CompletionResult.FromHintOptions(options, ""); - } - - if (args.Length == 2) - { - return CompletionResult.FromHint(""); - } - - return CompletionResult.Empty; - } -} diff --git a/Content.Server/Administration/Commands/TagCommands.cs b/Content.Server/Administration/Commands/TagCommands.cs deleted file mode 100644 index 790768be92..0000000000 --- a/Content.Server/Administration/Commands/TagCommands.cs +++ /dev/null @@ -1,113 +0,0 @@ -using Content.Shared.Administration; -using Content.Shared.Tag; -using Robust.Shared.Console; - -namespace Content.Server.Administration.Commands -{ - [AdminCommand(AdminFlags.Debug)] - public sealed class AddTagCommand : LocalizedCommands - { - [Dependency] private readonly IEntityManager _entityManager = default!; - - public override string Command => "addtag"; - public override string Description => Loc.GetString("addtag-command-description"); - public override string Help => Loc.GetString("addtag-command-help"); - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length != 2) - { - shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); - return; - } - - if (!EntityUid.TryParse(args[0], out var entityUid)) - { - shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); - return; - } - - if (!_entityManager.TrySystem(out TagSystem? tagSystem)) - return; - _entityManager.EnsureComponent(entityUid); - - if (tagSystem.TryAddTag(entityUid, args[1])) - { - shell.WriteLine(Loc.GetString("addtag-command-success", ("tag", args[1]), ("target", entityUid))); - } - else - { - shell.WriteError(Loc.GetString("addtag-command-fail", ("tag", args[1]), ("target", entityUid))); - } - } - - public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) - { - if (args.Length == 1) - { - return CompletionResult.FromHint(Loc.GetString("shell-argument-uid")); - } - - if (args.Length == 2) - { - return CompletionResult.FromHintOptions(CompletionHelper.PrototypeIDs(), - Loc.GetString("tag-command-arg-tag")); - } - - return CompletionResult.Empty; - } - } - - [AdminCommand(AdminFlags.Debug)] - public sealed class RemoveTagCommand : LocalizedCommands - { - [Dependency] private readonly IEntityManager _entityManager = default!; - - public override string Command => "removetag"; - public override string Description => Loc.GetString("removetag-command-description"); - public override string Help => Loc.GetString("removetag-command-help"); - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length != 2) - { - shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); - return; - } - - if (!EntityUid.TryParse(args[0], out var entityUid)) - { - shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); - return; - } - - if (!_entityManager.TrySystem(out TagSystem? tagSystem)) - return; - - if (tagSystem.RemoveTag(entityUid, args[1])) - { - shell.WriteLine(Loc.GetString("removetag-command-success", ("tag", args[1]), ("target", entityUid))); - } - else - { - shell.WriteError(Loc.GetString("removetag-command-fail", ("tag", args[1]), ("target", entityUid))); - } - } - - public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) - { - if (args.Length == 1) - { - return CompletionResult.FromHint(Loc.GetString("shell-argument-uid")); - } - - if (args.Length == 2&& EntityUid.TryParse(args[0], out var entityUid) && _entityManager.TryGetComponent(entityUid, out TagComponent? tagComponent)) - { - return CompletionResult.FromHintOptions(tagComponent.Tags, - Loc.GetString("tag-command-arg-tag")); - } - - return CompletionResult.Empty; - } - } -} diff --git a/Content.Server/Administration/Managers/AdminManager.cs b/Content.Server/Administration/Managers/AdminManager.cs index 950dffd1c8..0265e51ae7 100644 --- a/Content.Server/Administration/Managers/AdminManager.cs +++ b/Content.Server/Administration/Managers/AdminManager.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -12,7 +13,11 @@ using Robust.Shared.Configuration; using Robust.Shared.Console; using Robust.Shared.ContentPack; using Robust.Shared.Enums; +using Robust.Shared.Map; using Robust.Shared.Network; +using Robust.Shared.Players; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Errors; using Robust.Shared.Utility; @@ -28,6 +33,7 @@ namespace Content.Server.Administration.Managers [Dependency] private readonly IResourceManager _res = default!; [Dependency] private readonly IServerConsoleHost _consoleHost = default!; [Dependency] private readonly IChatManager _chat = default!; + [Dependency] private readonly ToolshedManager _toolshed = default!; private readonly Dictionary _admins = new(); private readonly HashSet _promotedPlayers = new(); @@ -41,6 +47,7 @@ namespace Content.Server.Administration.Managers public IEnumerable AllAdmins => _admins.Select(p => p.Key); private readonly AdminCommandPermissions _commandPermissions = new(); + private readonly AdminCommandPermissions _toolshedCommandPermissions = new(); public bool IsAdmin(IPlayerSession session, bool includeDeAdmin = false) { @@ -196,11 +203,37 @@ namespace Content.Server.Administration.Managers } } + foreach (var spec in _toolshed.AllCommands()) + { + var (isAvail, flagsReq) = GetRequiredFlag(spec.Cmd); + + if (!isAvail) + { + continue; + } + + if (flagsReq.Length != 0) + { + _toolshedCommandPermissions.AdminCommands.TryAdd(spec.Cmd.Name, flagsReq); + } + else + { + _toolshedCommandPermissions.AnyCommands.Add(spec.Cmd.Name); + } + } + // Load flags for engine commands, since those don't have the attributes. if (_res.TryContentFileRead(new ResPath("/engineCommandPerms.yml"), out var efs)) { _commandPermissions.LoadPermissionsFromStream(efs); } + + if (_res.TryContentFileRead(new ResPath("/toolshedEngineCommandPerms.yml"), out var toolshedPerms)) + { + _toolshedCommandPermissions.LoadPermissionsFromStream(toolshedPerms); + } + + _toolshed.ActivePermissionController = this; } public void PromoteHost(IPlayerSession player) @@ -366,6 +399,26 @@ namespace Content.Server.Administration.Managers return Equals(addr, System.Net.IPAddress.Loopback) || Equals(addr, System.Net.IPAddress.IPv6Loopback); } + public bool TryGetCommandFlags(CommandSpec command, out AdminFlags[]? flags) + { + var cmdName = command.Cmd.Name; + + if (_toolshedCommandPermissions.AnyCommands.Contains(cmdName)) + { + // Anybody can use this command. + flags = null; + return true; + } + + if (_toolshedCommandPermissions.AdminCommands.TryGetValue(cmdName, out flags)) + { + return true; + } + + flags = null; + return false; + } + public bool CanCommand(IPlayerSession session, string cmdName) { if (_commandPermissions.AnyCommands.Contains(cmdName)) @@ -398,7 +451,51 @@ namespace Content.Server.Administration.Managers return false; } - private static (bool isAvail, AdminFlags[] flagsReq) GetRequiredFlag(IConsoleCommand cmd) + public bool CheckInvokable(CommandSpec command, ICommonSession? user, out IConError? error) + { + if (user is null) + { + error = null; + return true; // Server console. + } + + var name = command.Cmd.Name; + if (!TryGetCommandFlags(command, out var flags)) + { + // Command is missing permissions. + error = new CommandPermissionsUnassignedError(command); + return false; + } + + if (flags is null) + { + // Anyone can execute this. + error = null; + return true; + } + + var data = GetAdminData((IPlayerSession)user); + if (data == null) + { + // Player isn't an admin. + error = new NoPermissionError(command); + return false; + } + + foreach (var flag in flags) + { + if (data.HasFlag(flag)) + { + error = null; + return true; + } + } + + error = new NoPermissionError(command); + return false; + } + + private static (bool isAvail, AdminFlags[] flagsReq) GetRequiredFlag(object cmd) { MemberInfo type = cmd.GetType(); @@ -472,3 +569,28 @@ namespace Content.Server.Administration.Managers } } } + +public record struct CommandPermissionsUnassignedError(CommandSpec Command) : IConError +{ + public FormattedMessage DescribeInner() + { + return FormattedMessage.FromMarkup($"The command {Command.FullName()} is missing permission flags and cannot be executed."); + } + + public string? Expression { get; set; } + public Vector2i? IssueSpan { get; set; } + public StackTrace? Trace { get; set; } +} + + +public record struct NoPermissionError(CommandSpec Command) : IConError +{ + public FormattedMessage DescribeInner() + { + return FormattedMessage.FromMarkup($"You do not have permission to execute {Command.FullName()}"); + } + + public string? Expression { get; set; } + public Vector2i? IssueSpan { get; set; } + public StackTrace? Trace { get; set; } +} diff --git a/Content.Server/Administration/Managers/IAdminManager.cs b/Content.Server/Administration/Managers/IAdminManager.cs index c5cf126f33..f5aa9da23e 100644 --- a/Content.Server/Administration/Managers/IAdminManager.cs +++ b/Content.Server/Administration/Managers/IAdminManager.cs @@ -1,6 +1,7 @@ using Content.Shared.Administration; using Content.Shared.Administration.Managers; using Robust.Server.Player; +using Robust.Shared.Toolshed; namespace Content.Server.Administration.Managers @@ -87,5 +88,7 @@ namespace Content.Server.Administration.Managers void Initialize(); void PromoteHost(IPlayerSession player); + + bool TryGetCommandFlags(CommandSpec command, out AdminFlags[]? flags); } } diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs index 7ce0112226..77cdee3a9e 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs @@ -117,7 +117,7 @@ public sealed partial class AdminVerbSystem Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/rejuvenate.png")), Act = () => { - RejuvenateCommand.PerformRejuvenate(args.Target); + _rejuvenate.PerformRejuvenate(args.Target); }, Impact = LogImpact.Extreme, Message = Loc.GetString("admin-trick-rejuvenate-description"), diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.cs b/Content.Server/Administration/Systems/AdminVerbSystem.cs index ce5d479851..63a5596020 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Server.Administration.Commands; using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; @@ -31,6 +32,7 @@ using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Prototypes; using Robust.Shared.Timing; +using Robust.Shared.Toolshed; using Robust.Shared.Utility; using static Content.Shared.Configurable.ConfigurationComponent; @@ -55,6 +57,8 @@ namespace Content.Server.Administration.Systems [Dependency] private readonly PrayerSystem _prayerSystem = default!; [Dependency] private readonly EuiManager _eui = default!; [Dependency] private readonly MindSystem _mindSystem = default!; + [Dependency] private readonly ToolshedManager _toolshed = default!; + [Dependency] private readonly RejuvenateSystem _rejuvenate = default!; private readonly Dictionary _openSolutionUis = new(); @@ -78,6 +82,14 @@ namespace Content.Server.Administration.Systems if (_adminManager.IsAdmin(player)) { + Verb mark = new(); + mark.Text = Loc.GetString("toolshed-verb-mark"); + mark.Message = Loc.GetString("toolshed-verb-mark-description"); + mark.Category = VerbCategory.Admin; + mark.Act = () => _toolshed.InvokeCommand(player, "=> $marked", Enumerable.Repeat(args.Target, 1), out _); + mark.Impact = LogImpact.Low; + args.Verbs.Add(mark); + if (TryComp(args.Target, out ActorComponent? targetActor)) { // AdminHelp @@ -188,8 +200,6 @@ namespace Content.Server.Administration.Systems Category = VerbCategory.Admin, Act = () => { - if (!TryComp(args.Target, out var actor)) return; - _console.ExecuteCommand(player, $"respawn {actor.PlayerSession.Name}"); }, ConfirmationPopup = true, @@ -229,7 +239,7 @@ namespace Content.Server.Administration.Systems Text = Loc.GetString("rejuvenate-verb-get-data-text"), Category = VerbCategory.Debug, Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/rejuvenate.svg.192dpi.png")), - Act = () => RejuvenateCommand.PerformRejuvenate(args.Target), + Act = () => _rejuvenate.PerformRejuvenate(args.Target), Impact = LogImpact.Medium }; args.Verbs.Add(verb); @@ -247,11 +257,11 @@ namespace Content.Server.Administration.Systems Act = () => { MakeSentientCommand.MakeSentient(args.Target, EntityManager); - + var mind = player.ContentData()?.Mind; if (mind == null) return; - + _mindSystem.TransferTo(mind, args.Target, ghostCheckOverride: true); }, Impact = LogImpact.High, diff --git a/Content.Server/Administration/Systems/RejuvenateSystem.cs b/Content.Server/Administration/Systems/RejuvenateSystem.cs new file mode 100644 index 0000000000..d25db20bd9 --- /dev/null +++ b/Content.Server/Administration/Systems/RejuvenateSystem.cs @@ -0,0 +1,11 @@ +using Content.Shared.Rejuvenate; + +namespace Content.Server.Administration.Systems; + +public sealed class RejuvenateSystem : EntitySystem +{ + public void PerformRejuvenate(EntityUid target) + { + RaiseLocalEvent(target, new RejuvenateEvent()); + } +} diff --git a/Content.Server/Administration/Toolshed/AdminsCommand.cs b/Content.Server/Administration/Toolshed/AdminsCommand.cs new file mode 100644 index 0000000000..aa82e0f1d9 --- /dev/null +++ b/Content.Server/Administration/Toolshed/AdminsCommand.cs @@ -0,0 +1,24 @@ +using Content.Server.Administration.Managers; +using Content.Shared.Administration; +using Robust.Server.Player; +using Robust.Shared.Toolshed; + +namespace Content.Server.Administration.Toolshed; + +[ToolshedCommand, AdminCommand(AdminFlags.Admin)] +public sealed class AdminsCommand : ToolshedCommand +{ + [Dependency] private readonly IAdminManager _admin = default!; + + [CommandImplementation("active")] + public IEnumerable Active() + { + return _admin.ActiveAdmins; + } + + [CommandImplementation("all")] + public IEnumerable All() + { + return _admin.AllAdmins; + } +} diff --git a/Content.Server/Administration/Toolshed/MarkedCommand.cs b/Content.Server/Administration/Toolshed/MarkedCommand.cs new file mode 100644 index 0000000000..b9e39cb82d --- /dev/null +++ b/Content.Server/Administration/Toolshed/MarkedCommand.cs @@ -0,0 +1,16 @@ +using Content.Shared.Administration; +using Robust.Shared.Toolshed; + +namespace Content.Server.Administration.Toolshed; + +[ToolshedCommand, AnyCommand] +public sealed class MarkedCommand : ToolshedCommand +{ + [CommandImplementation] + public IEnumerable Marked([CommandInvocationContext] IInvocationContext ctx) + { + var res = (IEnumerable?)ctx.ReadVar("marked"); + res ??= Array.Empty(); + return res; + } +} diff --git a/Content.Server/Administration/Toolshed/RejuvenateCommand.cs b/Content.Server/Administration/Toolshed/RejuvenateCommand.cs new file mode 100644 index 0000000000..eb1272d386 --- /dev/null +++ b/Content.Server/Administration/Toolshed/RejuvenateCommand.cs @@ -0,0 +1,22 @@ +using Content.Server.Administration.Systems; +using Content.Shared.Administration; +using Robust.Shared.Toolshed; + +namespace Content.Server.Administration.Toolshed; + +[ToolshedCommand, AdminCommand(AdminFlags.Admin)] +public sealed class RejuvenateCommand : ToolshedCommand +{ + private RejuvenateSystem? _rejuvenate; + [CommandImplementation] + public IEnumerable Rejuvenate([PipedArgument] IEnumerable input) + { + _rejuvenate ??= GetSys(); + + foreach (var i in input) + { + _rejuvenate.PerformRejuvenate(i); + yield return i; + } + } +} diff --git a/Content.Server/Administration/Toolshed/SolutionCommand.cs b/Content.Server/Administration/Toolshed/SolutionCommand.cs new file mode 100644 index 0000000000..b3f562a264 --- /dev/null +++ b/Content.Server/Administration/Toolshed/SolutionCommand.cs @@ -0,0 +1,49 @@ +using System.Linq; +using Content.Server.Chemistry.EntitySystems; +using Content.Shared.Administration; +using Content.Shared.Chemistry.Components; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Syntax; + +namespace Content.Server.Administration.Toolshed; + +[ToolshedCommand, AdminCommand(AdminFlags.Debug)] +public sealed class SolutionCommand : ToolshedCommand +{ + private SolutionContainerSystem? _solutionContainer; + + [CommandImplementation("get")] + public SolutionRef? Get( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] EntityUid input, + [CommandArgument] ValueRef name + ) + { + _solutionContainer ??= GetSys(); + + _solutionContainer.TryGetSolution(input, name.Evaluate(ctx)!, out var solution); + + if (solution is not null) + return new SolutionRef(input, solution); + + return null; + } + + [CommandImplementation("get")] + public IEnumerable Get( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] IEnumerable input, + [CommandArgument] ValueRef name + ) + { + return input.Select(x => Get(ctx, x, name)).Where(x => x is not null).Cast(); + } +} + +public readonly record struct SolutionRef(EntityUid Owner, Solution Solution) +{ + public override string ToString() + { + return $"{Owner} {Solution}"; + } +} diff --git a/Content.Server/Administration/Toolshed/TagCommand.cs b/Content.Server/Administration/Toolshed/TagCommand.cs new file mode 100644 index 0000000000..1af2779766 --- /dev/null +++ b/Content.Server/Administration/Toolshed/TagCommand.cs @@ -0,0 +1,106 @@ +using System.Linq; +using Content.Shared.Administration; +using Content.Shared.Tag; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; + +namespace Content.Server.Administration.Toolshed; + +[ToolshedCommand, AdminCommand(AdminFlags.Debug)] +public sealed class TagCommand : ToolshedCommand +{ + private TagSystem? _tag; + + [CommandImplementation("list")] + public IEnumerable List([PipedArgument] IEnumerable ent) + { + return ent.SelectMany(x => + { + if (TryComp(x, out var tags)) + // Note: Cast is required for C# to figure out the type signature. + return (IEnumerable)tags.Tags; + return Array.Empty(); + }); + } + + [CommandImplementation("add")] + public EntityUid Add( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] EntityUid input, + [CommandArgument] ValueRef> @ref + ) + { + _tag ??= GetSys(); + _tag.AddTag(input, @ref.Evaluate(ctx)!); + return input; + } + + [CommandImplementation("add")] + public IEnumerable Add( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] IEnumerable input, + [CommandArgument] ValueRef> @ref + ) + => input.Select(x => Add(ctx, x, @ref)); + + [CommandImplementation("rm")] + public EntityUid Rm( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] EntityUid input, + [CommandArgument] ValueRef> @ref + ) + { + _tag ??= GetSys(); + _tag.RemoveTag(input, @ref.Evaluate(ctx)!); + return input; + } + + [CommandImplementation("rm")] + public IEnumerable Rm( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] IEnumerable input, + [CommandArgument] ValueRef> @ref + ) + => input.Select(x => Rm(ctx, x, @ref)); + + [CommandImplementation("addmany")] + public EntityUid AddMany( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] EntityUid input, + [CommandArgument] ValueRef, IEnumerable> @ref + ) + { + _tag ??= GetSys(); + _tag.AddTags(input, @ref.Evaluate(ctx)!); + return input; + } + + [CommandImplementation("addmany")] + public IEnumerable AddMany( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] IEnumerable input, + [CommandArgument] ValueRef, IEnumerable> @ref + ) + => input.Select(x => AddMany(ctx, x, @ref)); + + [CommandImplementation("rmmany")] + public EntityUid RmMany( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] EntityUid input, + [CommandArgument] ValueRef, IEnumerable> @ref + ) + { + _tag ??= GetSys(); + _tag.RemoveTags(input, @ref.Evaluate(ctx)!); + return input; + } + + [CommandImplementation("rmmany")] + public IEnumerable RmMany( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] IEnumerable input, + [CommandArgument] ValueRef, IEnumerable> @ref + ) + => input.Select(x => RmMany(ctx, x, @ref)); +} diff --git a/Content.Server/Bql/BqlSelectCommand.cs b/Content.Server/Bql/BqlSelectCommand.cs deleted file mode 100644 index bcb658bfe7..0000000000 --- a/Content.Server/Bql/BqlSelectCommand.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Linq; -using Content.Server.Administration; -using Content.Server.EUI; -using Content.Shared.Administration; -using Content.Shared.Bql; -using Content.Shared.Eui; -using Robust.Server.Bql; -using Robust.Server.Player; -using Robust.Shared.Console; - -namespace Content.Server.Bql; - -[AdminCommand(AdminFlags.Query)] -public sealed class BqlSelectCommand : LocalizedCommands -{ - [Dependency] private readonly IBqlQueryManager _bql = default!; - [Dependency] private readonly EuiManager _euiManager = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; - - public override string Command => "bql_select"; - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (shell.Player == null) - { - shell.WriteError(LocalizationManager.GetString("cmd-bql_select-err-server-shell")); - return; - } - - var (entities, rest) = _bql.SimpleParseAndExecute(argStr["bql_select".Length..]); - - if (!string.IsNullOrWhiteSpace(rest)) - shell.WriteLine(LocalizationManager.GetString("cmd-bql_select-err-rest", ("rest", rest))); - - var ui = new BqlResultsEui( - entities.Select(e => (_entityManager.GetComponent(e).EntityName, e)).ToArray() - ); - _euiManager.OpenEui(ui, (IPlayerSession) shell.Player); - _euiManager.QueueStateUpdate(ui); - } -} - -internal sealed class BqlResultsEui : BaseEui -{ - private readonly (string name, EntityUid entity)[] _entities; - - public BqlResultsEui((string name, EntityUid entity)[] entities) - { - _entities = entities; - } - - public override EuiStateBase GetNewState() - { - return new BqlResultsEuiState(_entities); - } -} diff --git a/Content.Server/Bql/QuerySelectors.cs b/Content.Server/Bql/QuerySelectors.cs deleted file mode 100644 index 829dee7e38..0000000000 --- a/Content.Server/Bql/QuerySelectors.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System.Linq; -using Content.Server.Chemistry.Components.SolutionManager; -using Content.Server.Mind; -using Content.Server.Mind.Components; -using Content.Server.Power.Components; -using Content.Shared.Tag; -using Robust.Server.Bql; - -namespace Content.Server.Bql -{ - public sealed class QuerySelectors - { - [RegisterBqlQuerySelector] - public sealed class MindfulQuerySelector : BqlQuerySelector - { - public override string Token => "mindful"; - - public override QuerySelectorArgument[] Arguments => Array.Empty(); - - public override IEnumerable DoSelection(IEnumerable input, - IReadOnlyList arguments, bool isInverted, IEntityManager entityManager) - { - return input.Where(e => - { - if (entityManager.TryGetComponent(e, out var mind)) - return (mind.Mind?.VisitingEntity == e) ^ isInverted; - - return isInverted; - }); - } - - public override IEnumerable DoInitialSelection(IReadOnlyList arguments, bool isInverted, IEntityManager entityManager) - { - - return DoSelection( - entityManager.EntityQuery().Select(x => x.Owner), - arguments, isInverted, entityManager); - } - } - - [RegisterBqlQuerySelector] - public sealed class TaggedQuerySelector : BqlQuerySelector - { - public override string Token => "tagged"; - - public override QuerySelectorArgument[] Arguments => new [] { QuerySelectorArgument.String }; - - public override IEnumerable DoSelection(IEnumerable input, IReadOnlyList arguments, bool isInverted, IEntityManager entityManager) - { - return input.Where(e => - (entityManager.TryGetComponent(e, out var tag) && - tag.Tags.Contains((string) arguments[0])) ^ isInverted); - } - - public override IEnumerable DoInitialSelection(IReadOnlyList arguments, bool isInverted, IEntityManager entityManager) - { - return DoSelection(entityManager.EntityQuery().Select(x => x.Owner), arguments, - isInverted, entityManager); - - } - } - - [RegisterBqlQuerySelector] - public sealed class AliveQuerySelector : BqlQuerySelector - { - public override string Token => "alive"; - - public override QuerySelectorArgument[] Arguments => Array.Empty(); - - public override IEnumerable DoSelection(IEnumerable input, IReadOnlyList arguments, bool isInverted, IEntityManager entityManager) - { - var mindSystem = entityManager.System(); - return input.Where(e => - entityManager.TryGetComponent(e, out var mind) - && mind.Mind != null - && !mindSystem.IsCharacterDeadPhysically(mind.Mind)); - } - - public override IEnumerable DoInitialSelection(IReadOnlyList arguments, bool isInverted, IEntityManager entityManager) - { - return DoSelection(entityManager.EntityQuery().Select(x => x.Owner), arguments, - isInverted, entityManager); - } - } - - [RegisterBqlQuerySelector] - public sealed class HasReagentQuerySelector : BqlQuerySelector - { - public override string Token => "hasreagent"; - - public override QuerySelectorArgument[] Arguments => new [] { QuerySelectorArgument.String }; - - public override IEnumerable DoSelection(IEnumerable input, IReadOnlyList arguments, bool isInverted, IEntityManager entityManager) - { - var reagent = (string) arguments[0]; - return input.Where(e => - { - if (entityManager.TryGetComponent(e, out var solutionContainerManagerComponent)) - { - return solutionContainerManagerComponent.Solutions - .Any(solution => solution.Value.ContainsReagent(reagent)) ^ isInverted; - } - - return isInverted; - }); - } - - public override IEnumerable DoInitialSelection(IReadOnlyList arguments, bool isInverted, IEntityManager entityManager) - { - return DoSelection(entityManager.EntityQuery().Select(x => x.Owner), arguments, - isInverted, entityManager); - } - } - - [RegisterBqlQuerySelector] - public sealed class ApcPoweredQuerySelector : BqlQuerySelector - { - public override string Token => "apcpowered"; - - public override QuerySelectorArgument[] Arguments => Array.Empty(); - - public override IEnumerable DoSelection(IEnumerable input, IReadOnlyList arguments, bool isInverted, IEntityManager entityManager) - { - return input.Where(e => - entityManager.TryGetComponent(e, out var apcPowerReceiver) - ? apcPowerReceiver.Powered ^ isInverted - : isInverted); - } - - public override IEnumerable DoInitialSelection(IReadOnlyList arguments, bool isInverted, IEntityManager entityManager) - { - return DoSelection(entityManager.EntityQuery().Select(x => x.Owner), arguments, - isInverted, entityManager); - } - } - } -} diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index 46ab3f8bd8..41b6963332 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -19,18 +19,15 @@ using Content.Server.Preferences.Managers; using Content.Server.ServerInfo; using Content.Server.ServerUpdates; using Content.Server.Voting.Managers; -using Content.Shared.Administration; using Content.Shared.CCVar; using Content.Shared.Kitchen; using Robust.Server; -using Robust.Server.Bql; using Robust.Shared.Configuration; using Robust.Server.ServerStatus; using Robust.Shared.ContentPack; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.Utility; -using Content.Server.Station.Systems; using Content.Shared.Localizations; namespace Content.Server.Entry @@ -145,7 +142,6 @@ namespace Content.Server.Entry IoCManager.Resolve().Initialize(); IoCManager.Resolve().GetEntitySystem().PostInitialize(); - IoCManager.Resolve().DoAutoRegistrations(); IoCManager.Resolve().Initialize(); } } diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index 88d43213ba..70796a7239 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -11,6 +11,7 @@ using Content.Server.GhostKick; using Content.Server.Info; using Content.Server.Maps; using Content.Server.MoMMI; +using Content.Server.NewCon; using Content.Server.NodeContainer.NodeGroups; using Content.Server.Objectives; using Content.Server.Objectives.Interfaces; @@ -23,6 +24,7 @@ using Content.Server.Worldgen.Tools; using Content.Shared.Administration.Logs; using Content.Shared.Administration.Managers; using Content.Shared.Kitchen; +using Robust.Shared.Toolshed; namespace Content.Server.IoC { diff --git a/Content.Server/NewCon/Commands/AdminDebug/ACmdCommand.cs b/Content.Server/NewCon/Commands/AdminDebug/ACmdCommand.cs new file mode 100644 index 0000000000..a792196715 --- /dev/null +++ b/Content.Server/NewCon/Commands/AdminDebug/ACmdCommand.cs @@ -0,0 +1,34 @@ +using Content.Server.Administration; +using Content.Server.Administration.Managers; +using Content.Shared.Administration; +using Robust.Server.Player; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Syntax; + +namespace Content.Server.NewCon.Commands.AdminDebug; + +[ToolshedCommand, AdminCommand(AdminFlags.Debug)] +public sealed class ACmdCommand : ToolshedCommand +{ + [Dependency] private readonly IAdminManager _adminManager = default!; + + [CommandImplementation("perms")] + public AdminFlags[]? Perms([PipedArgument] CommandSpec command) + { + var res = _adminManager.TryGetCommandFlags(command, out var flags); + if (res) + flags ??= Array.Empty(); + return flags; + } + + [CommandImplementation("caninvoke")] + public bool CanInvoke( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] CommandSpec command, + [CommandArgument] ValueRef player + ) + { + // Deliberately discard the error. + return ((IPermissionController) _adminManager).CheckInvokable(command, player.Evaluate(ctx), out var err); + } +} diff --git a/Content.Server/NewCon/Commands/Verbs/RunVerbAsCommand.cs b/Content.Server/NewCon/Commands/Verbs/RunVerbAsCommand.cs new file mode 100644 index 0000000000..21f2119dac --- /dev/null +++ b/Content.Server/NewCon/Commands/Verbs/RunVerbAsCommand.cs @@ -0,0 +1,62 @@ +using System.Linq; +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Verbs; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; + +namespace Content.Server.NewCon.Commands.Verbs; + +[ToolshedCommand, AdminCommand(AdminFlags.Admin)] +public sealed class RunVerbAsCommand : ToolshedCommand +{ + private SharedVerbSystem? _verb; + + [CommandImplementation] + public IEnumerable RunVerbAs( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] IEnumerable input, + [CommandArgument] ValueRef runner, + [CommandArgument] string verb + ) + { + _verb ??= GetSys(); + verb = verb.ToLowerInvariant(); + + foreach (var i in input) + { + var runnerEid = runner.Evaluate(ctx); + + + if (EntityManager.Deleted(runnerEid) && runnerEid != default) + ctx.ReportError(new DeadEntity(runnerEid)); + + if (ctx.GetErrors().Any()) + yield break; + + var verbs = _verb.GetLocalVerbs(i, runnerEid, Verb.VerbTypes, true); + + // if the "verb name" is actually a verb-type, try run any verb of that type. + var verbType = Verb.VerbTypes.FirstOrDefault(x => x.Name == verb); + if (verbType != null) + { + var verbTy = verbs.FirstOrDefault(v => v.GetType() == verbType); + if (verbTy != null) + { + _verb.ExecuteVerb(verbTy, runnerEid, i, forced: true); + yield return i; + } + } + + foreach (var verbTy in verbs) + { + if (verbTy.Text.ToLowerInvariant() == verb) + { + _verb.ExecuteVerb(verbTy, runnerEid, i, forced: true); + yield return i; + } + } + } + } +} diff --git a/Content.Server/NewCon/Commands/VisualizeCommand.cs b/Content.Server/NewCon/Commands/VisualizeCommand.cs new file mode 100644 index 0000000000..44e4730959 --- /dev/null +++ b/Content.Server/NewCon/Commands/VisualizeCommand.cs @@ -0,0 +1,51 @@ +using System.Linq; +using Content.Server.Administration; +using Content.Server.EUI; +using Content.Shared.Administration; +using Content.Shared.Bql; +using Content.Shared.Eui; +using Robust.Server.Player; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Errors; + +namespace Content.Server.NewCon.Commands; + +[ToolshedCommand, AdminCommand(AdminFlags.Admin)] +public sealed class VisualizeCommand : ToolshedCommand +{ + [Dependency] private readonly EuiManager _euiManager = default!; + + [CommandImplementation] + public void VisualizeEntities( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] IEnumerable input + ) + { + if (ctx.Session is null) + { + ctx.ReportError(new NotForServerConsoleError()); + return; + } + + var ui = new ToolshedVisualizeEui( + input.Select(e => (EntName(e), e)).ToArray() + ); + _euiManager.OpenEui(ui, (IPlayerSession) ctx.Session); + _euiManager.QueueStateUpdate(ui); + } +} +internal sealed class ToolshedVisualizeEui : BaseEui +{ + private readonly (string name, EntityUid entity)[] _entities; + + public ToolshedVisualizeEui((string name, EntityUid entity)[] entities) + { + _entities = entities; + } + + public override EuiStateBase GetNewState() + { + return new ToolshedVisualizeEuiState(_entities); + } +} + diff --git a/Content.Server/Station/Commands/JobsCommand.cs b/Content.Server/Station/Commands/JobsCommand.cs new file mode 100644 index 0000000000..1d023c4a84 --- /dev/null +++ b/Content.Server/Station/Commands/JobsCommand.cs @@ -0,0 +1,129 @@ +using System.Linq; +using Content.Server.Administration; +using Content.Server.Station.Systems; +using Content.Shared.Administration; +using Content.Shared.Roles; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; + +namespace Content.Server.Station.Commands; + +[ToolshedCommand, AdminCommand(AdminFlags.VarEdit)] +public sealed class JobsCommand : ToolshedCommand +{ + private StationJobsSystem? _jobs; + + [CommandImplementation("jobs")] + public IEnumerable Jobs([PipedArgument] EntityUid station) + { + _jobs ??= GetSys(); + + foreach (var (job, _) in _jobs.GetJobs(station)) + { + yield return new JobSlotRef(job, station, _jobs, EntityManager); + } + } + + [CommandImplementation("jobs")] + public IEnumerable Jobs([PipedArgument] IEnumerable stations) + => stations.SelectMany(Jobs); + + [CommandImplementation("job")] + public JobSlotRef Job([PipedArgument] EntityUid station, [CommandArgument] Prototype job) + { + _jobs ??= GetSys(); + + return new JobSlotRef(job.Value.ID, station, _jobs, EntityManager); + } + + [CommandImplementation("job")] + public IEnumerable Job([PipedArgument] IEnumerable stations, [CommandArgument] Prototype job) + => stations.Select(x => Job(x, job)); + + [CommandImplementation("isinfinite")] + public bool IsInfinite([PipedArgument] JobSlotRef job, [CommandInverted] bool inverted) + => job.Infinite() ^ inverted; + + [CommandImplementation("isinfinite")] + public IEnumerable IsInfinite([PipedArgument] IEnumerable jobs, [CommandInverted] bool inverted) + => jobs.Select(x => IsInfinite(x, inverted)); + + [CommandImplementation("adjust")] + public JobSlotRef Adjust( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] JobSlotRef @ref, + [CommandArgument] ValueRef by + ) + { + _jobs ??= GetSys(); + _jobs.TryAdjustJobSlot(@ref.Station, @ref.Job, by.Evaluate(ctx), true, true); + return @ref; + } + + [CommandImplementation("adjust")] + public IEnumerable Adjust( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] IEnumerable @ref, + [CommandArgument] ValueRef by + ) + => @ref.Select(x => Adjust(ctx, x, by)); + + + [CommandImplementation("set")] + public JobSlotRef Set( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] JobSlotRef @ref, + [CommandArgument] ValueRef by + ) + { + _jobs ??= GetSys(); + _jobs.TrySetJobSlot(@ref.Station, @ref.Job, by.Evaluate(ctx), true); + return @ref; + } + + [CommandImplementation("set")] + public IEnumerable Set( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] IEnumerable @ref, + [CommandArgument] ValueRef by + ) + => @ref.Select(x => Set(ctx, x, by)); + + [CommandImplementation("amount")] + public int Amount( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] JobSlotRef @ref + ) + { + _jobs ??= GetSys(); + _jobs.TryGetJobSlot(@ref.Station, @ref.Job, out var slots); + return (int)(slots ?? 0); + } + + [CommandImplementation("amount")] + public IEnumerable Amount( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] IEnumerable @ref + ) + => @ref.Select(x => Amount(ctx, x)); +} + +// Used for Toolshed queries. +public readonly record struct JobSlotRef(string Job, EntityUid Station, StationJobsSystem Jobs, IEntityManager EntityManager) +{ + public override string ToString() + { + if (!Jobs.TryGetJobSlot(Station, Job, out var slot)) + { + return $"{EntityManager.ToPrettyString(Station)} job {Job} : (not a slot)"; + } + + return $"{EntityManager.ToPrettyString(Station)} job {Job} : {slot?.ToString() ?? "infinite"}"; + } + + public bool Infinite() + { + return Jobs.TryGetJobSlot(Station, Job, out var slot) && slot is null; + } +} diff --git a/Content.Server/Station/Commands/StationCommand.cs b/Content.Server/Station/Commands/StationCommand.cs new file mode 100644 index 0000000000..b2381a0322 --- /dev/null +++ b/Content.Server/Station/Commands/StationCommand.cs @@ -0,0 +1,126 @@ +using System.Diagnostics; +using System.Linq; +using Content.Server.Administration; +using Content.Server.Station.Components; +using Content.Server.Station.Systems; +using Content.Shared.Administration; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Errors; +using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Utility; + +namespace Content.Server.Station.Commands; + +[ToolshedCommand, AdminCommand(AdminFlags.Admin)] +public sealed class StationsCommand : ToolshedCommand +{ + private StationSystem? _station; + + [CommandImplementation("list")] + public IEnumerable List() + { + _station ??= GetSys(); + + return _station.GetStationsSet(); + } + + [CommandImplementation("get")] + public EntityUid Get([CommandInvocationContext] IInvocationContext ctx) + { + _station ??= GetSys(); + + var set = _station.GetStationsSet(); + if (set.Count > 1 || set.Count == 0) + ctx.ReportError(new OnlyOneStationsError()); + + return set.FirstOrDefault(); + } + + [CommandImplementation("getowningstation")] + public IEnumerable GetOwningStation([PipedArgument] IEnumerable input) + => input.Select(GetOwningStation); + + [CommandImplementation("getowningstation")] + public EntityUid? GetOwningStation([PipedArgument] EntityUid input) + { + _station ??= GetSys(); + + return _station.GetOwningStation(input); + } + + [CommandImplementation("largestgrid")] + public EntityUid? LargestGrid([PipedArgument] EntityUid input) + { + _station ??= GetSys(); + + return _station.GetLargestGrid(Comp(input)); + } + + [CommandImplementation("largestgrid")] + public IEnumerable LargestGrid([PipedArgument] IEnumerable input) + => input.Select(LargestGrid); + + + [CommandImplementation("grids")] + public IEnumerable Grids([PipedArgument] EntityUid input) + => Comp(input).Grids; + + [CommandImplementation("grids")] + public IEnumerable Grids([PipedArgument] IEnumerable input) + => input.SelectMany(Grids); + + [CommandImplementation("config")] + public StationConfig? Config([PipedArgument] EntityUid input) + => Comp(input).StationConfig; + + [CommandImplementation("config")] + public IEnumerable Config([PipedArgument] IEnumerable input) + => input.Select(Config); + + [CommandImplementation("addgrid")] + public void AddGrid( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] EntityUid input, + [CommandArgument] ValueRef grid + ) + { + _station ??= GetSys(); + + _station.AddGridToStation(input, grid.Evaluate(ctx)); + } + + [CommandImplementation("rmgrid")] + public void RmGrid( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] EntityUid input, + [CommandArgument] ValueRef grid + ) + { + _station ??= GetSys(); + + _station.RemoveGridFromStation(input, grid.Evaluate(ctx)); + } + + [CommandImplementation("rename")] + public void Rename([CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] EntityUid input, + [CommandArgument] ValueRef name + ) + { + _station ??= GetSys(); + + _station.RenameStation(input, name.Evaluate(ctx)!); + } +} + +public record struct OnlyOneStationsError : IConError +{ + public FormattedMessage DescribeInner() + { + return FormattedMessage.FromMarkup("This command doesn't function if there is more than one or no stations, explicitly specify a station with the ent command or similar."); + } + + public string? Expression { get; set; } + public Vector2i? IssueSpan { get; set; } + public StackTrace? Trace { get; set; } +} diff --git a/Content.Shared/Administration/AnyCommandAttribute.cs b/Content.Shared/Administration/AnyCommandAttribute.cs index 52c8951631..66a1bd0bad 100644 --- a/Content.Shared/Administration/AnyCommandAttribute.cs +++ b/Content.Shared/Administration/AnyCommandAttribute.cs @@ -1,5 +1,6 @@ using JetBrains.Annotations; using Robust.Shared.Console; +using Robust.Shared.Toolshed; namespace Content.Shared.Administration { @@ -7,7 +8,6 @@ namespace Content.Shared.Administration /// Specifies that a command can be executed by any player. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - [BaseTypeRequired(typeof(IConsoleCommand))] [MeansImplicitUse] public sealed class AnyCommandAttribute : Attribute { diff --git a/Content.Shared/Bql/BqlResultsEuiState.cs b/Content.Shared/Bql/ToolshedVisualizeEuiState.cs similarity index 62% rename from Content.Shared/Bql/BqlResultsEuiState.cs rename to Content.Shared/Bql/ToolshedVisualizeEuiState.cs index cd298fa814..9505854614 100644 --- a/Content.Shared/Bql/BqlResultsEuiState.cs +++ b/Content.Shared/Bql/ToolshedVisualizeEuiState.cs @@ -4,11 +4,11 @@ using Robust.Shared.Serialization; namespace Content.Shared.Bql; [Serializable, NetSerializable] -public sealed class BqlResultsEuiState : EuiStateBase +public sealed class ToolshedVisualizeEuiState : EuiStateBase { public readonly (string name, EntityUid entity)[] Entities; - public BqlResultsEuiState((string name, EntityUid entity)[] entities) + public ToolshedVisualizeEuiState((string name, EntityUid entity)[] entities) { Entities = entities; } diff --git a/Resources/Locale/en-US/administration/admin-verbs.ftl b/Resources/Locale/en-US/administration/admin-verbs.ftl index dab68eab4d..6804171f7d 100644 --- a/Resources/Locale/en-US/administration/admin-verbs.ftl +++ b/Resources/Locale/en-US/administration/admin-verbs.ftl @@ -7,3 +7,5 @@ admin-verbs-teleport-to = Teleport To admin-verbs-teleport-here = Teleport Here admin-verbs-freeze = Freeze admin-verbs-unfreeze = Unfreeze +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/commands/toolshed-commands.ftl b/Resources/Locale/en-US/commands/toolshed-commands.ftl new file mode 100644 index 0000000000..a0416d9ecf --- /dev/null +++ b/Resources/Locale/en-US/commands/toolshed-commands.ftl @@ -0,0 +1,56 @@ +command-description-visualize = + Takes the input list of entities and puts them into a UI window for easy browsing. +command-description-runverbas = + Runs a verb over the input entities with the given user. +command-description-acmd-perms = + Returns the admin permissions of the given command, if any. +command-description-acmd-caninvoke = + Check if the given player can invoke the given command. +command-description-jobs-jobs = + Returns all jobs on a station. +command-description-jobs-job = + Returns a given job on a station. +command-description-jobs-isinfinite = + Returns true if the input job is infinite, otherwise false. +command-description-jobs-adjust = + Adjusts the number of slots for the given job. +command-description-jobs-set = + Sets the number of slots for the given job. +command-description-jobs-amount = + Returns the number of slots for the given job. +command-description-stations-list = + Returns a list of all stations. +command-description-stations-get = + Gets the active station, if and only if there is only one. +command-description-stations-getowningstation = + Gets the station that a given entity is "owned by" (within) +command-description-stations-grids = + Returns all grids associated with the input station. +command-description-stations-config = + Returns the config associated with the input station, if any. +command-description-stations-addgrid = + Adds a grid to the given station. +command-description-stations-rmgrid = + Removes a grid from the given station. +command-description-stations-rename = + Renames the given station. +command-description-stations-largestgrid = + Returns the largest grid the given station has, if any. +command-description-admins-active = + Returns a list of active admins. +command-description-admins-all = + Returns a list of ALL admins, including deadmined ones. +command-description-marked = + Returns the value of $marked as a List. +command-description-rejuvenate = + Rejuvenates the given entities, restoring them to full health, clearing status effects, etc. +command-description-tag-list = + Lists tags on the given entities. +command-description-tag-add = + Adds a tag to the given entities. +command-description-tag-rm = + Removes a tag from the given entities. +command-description-tag-addmany = + Adds a list of tags to the given entities. +command-description-tag-rmmany = + Removes a list of tags from the given entities. diff --git a/Resources/engineCommandPerms.yml b/Resources/engineCommandPerms.yml index 6dff0d9b22..ccbacde916 100644 --- a/Resources/engineCommandPerms.yml +++ b/Resources/engineCommandPerms.yml @@ -126,7 +126,9 @@ - Flags: QUERY Commands: - - forall - uploadfile - loadprototype - uploadfolder + +- Commands: + - "|" diff --git a/Resources/toolshedEngineCommandPerms.yml b/Resources/toolshedEngineCommandPerms.yml new file mode 100644 index 0000000000..f090ac4baa --- /dev/null +++ b/Resources/toolshedEngineCommandPerms.yml @@ -0,0 +1,70 @@ +- Flags: QUERY + Commands: + - entities + - nearby + - map + - physics + - player + - splat + - emplace + +- Flags: DEBUG + Commands: + - comp + - delete + - do + - named + - paused + - with + - count + - select + - where + - prototyped + - types + - ecscomp + - actor + +- Flags: HOST + Commands: + - methods + - ioc + +- Commands: + - fuck + - ent + - as + - buildinfo + - help + - explain + - cmd + - stopwatch + - self + - search + - isnull + - help + - isempty + - any + - unique + - cd + - ls + - loc + - vars + - '=>' + - first + - val + - '+' + - '-' + - '*' + - '/' + - 'min' + - 'max' + - '&' + - '|' + - '^' + - 'neg' + - '<' + - '>' + - '<=' + - '>=' + - '==' + - '!=' diff --git a/RobustToolbox b/RobustToolbox index a7315b1c95..cf91369d27 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit a7315b1c95e335ec24f77732021fdedc0bd0cf74 +Subproject commit cf91369d27f5653fc5c597b3a9fca04a29172674 diff --git a/SpaceStation14.sln.DotSettings b/SpaceStation14.sln.DotSettings index 37e8181972..dbb20970b1 100644 --- a/SpaceStation14.sln.DotSettings +++ b/SpaceStation14.sln.DotSettings @@ -604,6 +604,8 @@ public sealed class $CLASS$ : Shared$CLASS$ { True True True + True + True True True True