From 08970e745b0a02937901bbd40031e67d2052a0c6 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Fri, 28 Jun 2024 09:29:24 +0200 Subject: [PATCH] Entity console commands system. (#5267) * Entity console commands system. This adds a new base type, LocalizedEntityCommands, which is able to import entity systems as dependencies. This is done by only registering these while the entity system is active. Handling registration separately like this required a bit of changes around ConsoleHost to make it more suitable for this purpose: You can now directly register command instances, and also have a system to suppress `UpdateAvailableCommands` on the client so there's no bad O(N*M) behavior. * Convert TeleportCommands.cs to new entity commands. Removes some obsoletion warnings without pain from having to manually import transform system. * Fix RobustServerSimulation dependency issue. --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> --- RELEASE-NOTES.md | 3 ++ .../Console/Commands/TeleportCommands.cs | 42 +++++++-------- Robust.Shared/Console/ConsoleHost.cs | 50 ++++++++++++++--- Robust.Shared/Console/EntityConsoleHost.cs | 54 +++++++++++++++++++ Robust.Shared/Console/IConsoleCommand.cs | 7 +++ Robust.Shared/Console/IConsoleHost.cs | 28 ++++++++++ Robust.Shared/Console/LocalizedCommands.cs | 14 +++++ Robust.Shared/GameObjects/EntityManager.cs | 4 ++ Robust.Shared/SharedIoC.cs | 2 + .../Server/RobustServerSimulation.cs | 1 + 10 files changed, 176 insertions(+), 29 deletions(-) create mode 100644 Robust.Shared/Console/EntityConsoleHost.cs diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 3f5c24b04..1a5a43272 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,6 +39,9 @@ END TEMPLATE--> ### New features +* Added `LocalizedEntityCommands`, which are console commands that have the ability to take entity system dependencies. +* Added `BeginRegistrationRegion` to `IConsoleHost` to allow efficient bulk-registration of console commands. +* Added `IConsoleHost.RegisterCommand` overload that takes an `IConsoleCommand`. * Added a `Finished` boolean to `AnimationCompletedEvent` which allows distinguishing if an animation was removed prematurely or completed naturally. ### Bugfixes diff --git a/Robust.Shared/Console/Commands/TeleportCommands.cs b/Robust.Shared/Console/Commands/TeleportCommands.cs index 35724df37..24ec31651 100644 --- a/Robust.Shared/Console/Commands/TeleportCommands.cs +++ b/Robust.Shared/Console/Commands/TeleportCommands.cs @@ -9,18 +9,17 @@ using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Map; using Robust.Shared.Map.Components; -using Robust.Shared.Maths; using Robust.Shared.Physics.Components; using Robust.Shared.Player; using Robust.Shared.Utility; namespace Robust.Shared.Console.Commands; -internal sealed class TeleportCommand : LocalizedCommands +internal sealed class TeleportCommand : LocalizedEntityCommands { [Dependency] private readonly IMapManager _map = default!; - [Dependency] private readonly IEntitySystemManager _entitySystem = default!; [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; public override string Command => "tp"; public override bool RequireServerOrSingleplayer => true; @@ -36,11 +35,10 @@ internal sealed class TeleportCommand : LocalizedCommands return; } - var xformSystem = _entitySystem.GetEntitySystem(); var transform = _entityManager.GetComponent(entity); var position = new Vector2(posX, posY); - xformSystem.AttachToGridOrMap(entity, transform); + _transform.AttachToGridOrMap(entity, transform); MapId mapId; if (args.Length == 3 && int.TryParse(args[2], out var intMapId)) @@ -56,25 +54,26 @@ internal sealed class TeleportCommand : LocalizedCommands if (_map.TryFindGridAt(mapId, position, out var gridUid, out var grid)) { - var gridPos = Vector2.Transform(position, xformSystem.GetInvWorldMatrix(gridUid)); + var gridPos = Vector2.Transform(position, _transform.GetInvWorldMatrix(gridUid)); - xformSystem.SetCoordinates(entity, transform, new EntityCoordinates(gridUid, gridPos)); + _transform.SetCoordinates(entity, transform, new EntityCoordinates(gridUid, gridPos)); } else { var mapEnt = _map.GetMapEntityIdOrThrow(mapId); - xformSystem.SetWorldPosition(transform, position); - xformSystem.SetParent(entity, transform, mapEnt); + _transform.SetWorldPosition(transform, position); + _transform.SetParent(entity, transform, mapEnt); } shell.WriteLine($"Teleported {shell.Player} to {mapId}:{posX},{posY}."); } } -public sealed class TeleportToCommand : LocalizedCommands +public sealed class TeleportToCommand : LocalizedEntityCommands { [Dependency] private readonly ISharedPlayerManager _players = default!; [Dependency] private readonly IEntityManager _entities = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; public override string Command => "tpto"; public override bool RequireServerOrSingleplayer => true; @@ -89,7 +88,6 @@ public sealed class TeleportToCommand : LocalizedCommands if (!TryGetTransformFromUidOrUsername(target, shell, out var targetUid, out _)) return; - var transformSystem = _entities.System(); var targetCoords = new EntityCoordinates(targetUid.Value, Vector2.Zero); if (_entities.TryGetComponent(targetUid, out PhysicsComponent? targetPhysics)) @@ -127,8 +125,8 @@ public sealed class TeleportToCommand : LocalizedCommands foreach (var victim in victims) { - transformSystem.SetCoordinates(victim.Entity, targetCoords); - transformSystem.AttachToGridOrMap(victim.Entity, victim.Transform); + _transform.SetCoordinates(victim.Entity, targetCoords); + _transform.AttachToGridOrMap(victim.Entity, victim.Transform); } } @@ -178,9 +176,10 @@ public sealed class TeleportToCommand : LocalizedCommands } } -sealed class LocationCommand : LocalizedCommands +sealed class LocationCommand : LocalizedEntityCommands { [Dependency] private readonly IEntityManager _ent = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; public override string Command => "loc"; @@ -192,18 +191,19 @@ sealed class LocationCommand : LocalizedCommands var pt = _ent.GetComponent(entity); var pos = pt.Coordinates; - shell.WriteLine($"MapID:{pos.GetMapId(_ent)} GridUid:{pos.GetGridUid(_ent)} X:{pos.X:N2} Y:{pos.Y:N2}"); + var mapId = _transform.GetMapId(pos); + var gridUid = _transform.GetGrid(pos); + + shell.WriteLine($"MapID:{mapId} GridUid:{gridUid} X:{pos.X:N2} Y:{pos.Y:N2}"); } } -sealed class TpGridCommand : LocalizedCommands +sealed class TpGridCommand : LocalizedEntityCommands { [Dependency] private readonly IEntityManager _ent = default!; - [Dependency] private readonly IMapManager _map = default!; + [Dependency] private readonly SharedMapSystem _map = default!; public override string Command => "tpgrid"; - public override string Description => Loc.GetString("cmd-tpgrid-desc"); - public override string Help => Loc.GetString("cmd-tpgrid-help"); public override bool RequireServerOrSingleplayer => true; public override void Execute(IConsoleShell shell, string argStr, string[] args) @@ -246,14 +246,14 @@ sealed class TpGridCommand : LocalizedCommands mapId = new MapId(map); } - var id = _map.GetMapEntityId(mapId); + var id = _map.GetMap(mapId); if (id == EntityUid.Invalid) { shell.WriteError(Loc.GetString("cmd-parse-failure-mapid", ("arg", mapId.Value))); return; } - var pos = new EntityCoordinates(_map.GetMapEntityId(mapId), new Vector2(xPos, yPos)); + var pos = new EntityCoordinates(id, new Vector2(xPos, yPos)); _ent.System().SetCoordinates(uid.Value, pos); } diff --git a/Robust.Shared/Console/ConsoleHost.cs b/Robust.Shared/Console/ConsoleHost.cs index 82d68c830..b26f24660 100644 --- a/Robust.Shared/Console/ConsoleHost.cs +++ b/Robust.Shared/Console/ConsoleHost.cs @@ -29,6 +29,9 @@ namespace Robust.Shared.Console [Dependency] protected readonly ILocalizationManager LocalizationManager = default!; [ViewVariables] protected readonly Dictionary RegisteredCommands = new(); + [ViewVariables] private readonly HashSet _autoRegisteredCommands = []; + + private bool _isInRegistrationRegion; private readonly CommandBuffer _commandBuffer = new CommandBuffer(); @@ -61,6 +64,11 @@ namespace Robust.Shared.Console // search for all client commands in all assemblies, and register them foreach (var type in ReflectionManager.GetAllChildren()) { + // This sucks but I can't come up with anything better + // that won't just be 10x worse complexity for no gain. + if (type.IsAssignableTo(typeof(IEntityConsoleCommand))) + continue; + var instance = (IConsoleCommand)_typeFactory.CreateInstanceUnchecked(type, true); if (AvailableCommands.TryGetValue(instance.Command, out var duplicate)) { @@ -69,6 +77,7 @@ namespace Robust.Shared.Console } RegisteredCommands[instance.Command] = instance; + _autoRegisteredCommands.Add(instance.Command); } } @@ -76,6 +85,23 @@ namespace Robust.Shared.Console { } + public void BeginRegistrationRegion() + { + if (_isInRegistrationRegion) + throw new InvalidOperationException("Cannot enter registration region twice!"); + + _isInRegistrationRegion = true; + } + + public void EndRegistrationRegion() + { + if (!_isInRegistrationRegion) + throw new InvalidOperationException("Was not in registration region."); + + _isInRegistrationRegion = false; + UpdateAvailableCommands(); + } + #region RegisterCommand public void RegisterCommand( string command, @@ -88,8 +114,7 @@ namespace Robust.Shared.Console throw new InvalidOperationException($"Command already registered: {command}"); var newCmd = new RegisteredCommand(command, description, help, callback, requireServerOrSingleplayer); - RegisteredCommands.Add(command, newCmd); - UpdateAvailableCommands(); + RegisterCommand(newCmd); } public void RegisterCommand( @@ -104,8 +129,7 @@ namespace Robust.Shared.Console throw new InvalidOperationException($"Command already registered: {command}"); var newCmd = new RegisteredCommand(command, description, help, callback, completionCallback, requireServerOrSingleplayer); - RegisteredCommands.Add(command, newCmd); - UpdateAvailableCommands(); + RegisterCommand(newCmd); } public void RegisterCommand( @@ -120,8 +144,7 @@ namespace Robust.Shared.Console throw new InvalidOperationException($"Command already registered: {command}"); var newCmd = new RegisteredCommand(command, description, help, callback, completionCallback, requireServerOrSingleplayer); - RegisteredCommands.Add(command, newCmd); - UpdateAvailableCommands(); + RegisterCommand(newCmd); } public void RegisterCommand(string command, ConCommandCallback callback, @@ -153,6 +176,15 @@ namespace Robust.Shared.Console var help = LocalizationManager.TryGetString($"cmd-{command}-help", out var val) ? val : ""; RegisterCommand(command, description, help, callback, completionCallback, requireServerOrSingleplayer); } + + public void RegisterCommand(IConsoleCommand command) + { + RegisteredCommands.Add(command.Command, command); + + if (!_isInRegistrationRegion) + UpdateAvailableCommands(); + } + #endregion /// @@ -161,12 +193,14 @@ namespace Robust.Shared.Console if (!RegisteredCommands.TryGetValue(command, out var cmd)) throw new KeyNotFoundException($"Command {command} is not registered."); - if (cmd is not RegisteredCommand) + if (_autoRegisteredCommands.Contains(command)) throw new InvalidOperationException( "You cannot unregister commands that have been registered automatically."); RegisteredCommands.Remove(command); - UpdateAvailableCommands(); + + if (!_isInRegistrationRegion) + UpdateAvailableCommands(); } //TODO: Pull up diff --git a/Robust.Shared/Console/EntityConsoleHost.cs b/Robust.Shared/Console/EntityConsoleHost.cs new file mode 100644 index 000000000..ede5fd364 --- /dev/null +++ b/Robust.Shared/Console/EntityConsoleHost.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Reflection; +using Robust.Shared.Utility; + +namespace Robust.Shared.Console; + +/// +/// Manages registration for "entity" console commands. +/// +/// +/// See for details on what "entity" console commands are. +/// +internal sealed class EntityConsoleHost +{ + [Dependency] private readonly IConsoleHost _consoleHost = default!; + [Dependency] private readonly IReflectionManager _reflectionManager = default!; + [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; + + private readonly HashSet _entityCommands = []; + + public void Startup() + { + DebugTools.Assert(_entityCommands.Count == 0); + + var deps = ((EntitySystemManager)_entitySystemManager).SystemDependencyCollection; + + _consoleHost.BeginRegistrationRegion(); + + // search for all client commands in all assemblies, and register them + foreach (var type in _reflectionManager.GetAllChildren()) + { + var instance = (IConsoleCommand)Activator.CreateInstance(type)!; + deps.InjectDependencies(instance, oneOff: true); + + _entityCommands.Add(instance.Command); + _consoleHost.RegisterCommand(instance); + } + + _consoleHost.EndRegistrationRegion(); + } + + public void Shutdown() + { + foreach (var command in _entityCommands) + { + _consoleHost.UnregisterCommand(command); + } + + _entityCommands.Clear(); + } +} diff --git a/Robust.Shared/Console/IConsoleCommand.cs b/Robust.Shared/Console/IConsoleCommand.cs index 46c705aca..1ab9785bd 100644 --- a/Robust.Shared/Console/IConsoleCommand.cs +++ b/Robust.Shared/Console/IConsoleCommand.cs @@ -83,4 +83,11 @@ namespace Robust.Shared.Console return ValueTask.FromResult(GetCompletion(shell, args)); } } + + /// + /// Special marker interface used to indicate "entity" commands. + /// See for an overview. + /// + /// + internal interface IEntityConsoleCommand : IConsoleCommand; } diff --git a/Robust.Shared/Console/IConsoleHost.cs b/Robust.Shared/Console/IConsoleHost.cs index 223b8b51d..de28453aa 100644 --- a/Robust.Shared/Console/IConsoleHost.cs +++ b/Robust.Shared/Console/IConsoleHost.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Robust.Shared.Player; +using Robust.Shared.Reflection; using Robust.Shared.Utility; namespace Robust.Shared.Console @@ -173,6 +174,33 @@ namespace Robust.Shared.Console ConCommandCallback callback, ConCommandCompletionAsyncCallback completionCallback, bool requireServerOrSingleplayer = false); + + /// + /// Register an existing console command instance directly. + /// + /// + /// For this to be useful, the command has to be somehow excluded from automatic registration, + /// such as by using the . + /// + /// The command to register. + /// + void RegisterCommand(IConsoleCommand command); + + /// + /// Begin a region for registering many console commands in one go. + /// The region can be ended with . + /// + /// + /// Commands registered inside this region temporarily suppress some updating + /// logic that would cause significant wasted work. This logic runs when the region is ended instead. + /// + void BeginRegistrationRegion(); + + /// + /// End a registration region started with . + /// + void EndRegistrationRegion(); + #endregion /// diff --git a/Robust.Shared/Console/LocalizedCommands.cs b/Robust.Shared/Console/LocalizedCommands.cs index f8266971f..13672f357 100644 --- a/Robust.Shared/Console/LocalizedCommands.cs +++ b/Robust.Shared/Console/LocalizedCommands.cs @@ -34,3 +34,17 @@ public abstract class LocalizedCommands : IConsoleCommand return ValueTask.FromResult(GetCompletion(shell, args)); } } + +/// +/// Base class for localized console commands that run in "entity space". +/// +/// +/// +/// This type of command is registered only while the entity system is active. +/// On the client this means that the commands are only available while connected to a server or in single player. +/// +/// +/// These commands are allowed to take dependencies on entity systems, reducing boilerplate for many usages. +/// +/// +public abstract class LocalizedEntityCommands : LocalizedCommands, IEntityConsoleCommand; diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index 79f5f7ab2..0b8e7d224 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using Prometheus; +using Robust.Shared.Console; using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Log; @@ -42,6 +43,7 @@ namespace Robust.Shared.GameObjects [IoC.Dependency] private readonly ProfManager _prof = default!; [IoC.Dependency] private readonly INetManager _netMan = default!; [IoC.Dependency] private readonly IReflectionManager _reflection = default!; + [IoC.Dependency] private readonly EntityConsoleHost _entityConsoleHost = default!; // I feel like PJB might shed me for putting a system dependency here, but its required for setting entity // positions on spawn.... @@ -216,6 +218,7 @@ namespace Robust.Shared.GameObjects TransformQuery = GetEntityQuery(); _physicsQuery = GetEntityQuery(); _actorQuery = GetEntityQuery(); + _entityConsoleHost.Startup(); } public virtual void Shutdown() @@ -227,6 +230,7 @@ namespace Robust.Shared.GameObjects ClearComponents(); ShuttingDown = false; Started = false; + _entityConsoleHost.Shutdown(); } public virtual void Cleanup() diff --git a/Robust.Shared/SharedIoC.cs b/Robust.Shared/SharedIoC.cs index 352a499da..df176b113 100644 --- a/Robust.Shared/SharedIoC.cs +++ b/Robust.Shared/SharedIoC.cs @@ -1,5 +1,6 @@ using Robust.Shared.Asynchronous; using Robust.Shared.Configuration; +using Robust.Shared.Console; using Robust.Shared.ContentPack; using Robust.Shared.Exceptions; using Robust.Shared.GameObjects; @@ -50,6 +51,7 @@ namespace Robust.Shared deps.Register(); deps.Register(); deps.Register(); + deps.Register(); } } } diff --git a/Robust.UnitTesting/Server/RobustServerSimulation.cs b/Robust.UnitTesting/Server/RobustServerSimulation.cs index 2c1426feb..d8c71cbff 100644 --- a/Robust.UnitTesting/Server/RobustServerSimulation.cs +++ b/Robust.UnitTesting/Server/RobustServerSimulation.cs @@ -232,6 +232,7 @@ namespace Robust.UnitTesting.Server container.Register(); // Needed for grid fixture debugging. container.Register(); + container.Register(); // I just wanted to load pvs system container.Register();