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>
This commit is contained in:
Pieter-Jan Briers
2024-06-28 09:29:24 +02:00
committed by GitHub
parent 0ba4a66787
commit 08970e745b
10 changed files with 176 additions and 29 deletions

View File

@@ -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

View File

@@ -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<SharedTransformSystem>();
var transform = _entityManager.GetComponent<TransformComponent>(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<SharedTransformSystem>();
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<TransformComponent>(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<SharedTransformSystem>().SetCoordinates(uid.Value, pos);
}

View File

@@ -29,6 +29,9 @@ namespace Robust.Shared.Console
[Dependency] protected readonly ILocalizationManager LocalizationManager = default!;
[ViewVariables] protected readonly Dictionary<string, IConsoleCommand> RegisteredCommands = new();
[ViewVariables] private readonly HashSet<string> _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<IConsoleCommand>())
{
// 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
/// <inheritdoc />
@@ -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

View File

@@ -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;
/// <summary>
/// Manages registration for "entity" console commands.
/// </summary>
/// <remarks>
/// See <see cref="LocalizedEntityCommands"/> for details on what "entity" console commands are.
/// </remarks>
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<string> _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<IEntityConsoleCommand>())
{
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();
}
}

View File

@@ -83,4 +83,11 @@ namespace Robust.Shared.Console
return ValueTask.FromResult(GetCompletion(shell, args));
}
}
/// <summary>
/// Special marker interface used to indicate "entity" commands.
/// See <see cref="LocalizedEntityCommands"/> for an overview.
/// </summary>
/// <seealso cref="EntityConsoleHost"/>
internal interface IEntityConsoleCommand : IConsoleCommand;
}

View File

@@ -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);
/// <summary>
/// Register an existing console command instance directly.
/// </summary>
/// <remarks>
/// For this to be useful, the command has to be somehow excluded from automatic registration,
/// such as by using the <see cref="ReflectAttribute"/>.
/// </remarks>
/// <param name="command">The command to register.</param>
/// <seealso cref="BeginRegistrationRegion"/>
void RegisterCommand(IConsoleCommand command);
/// <summary>
/// Begin a region for registering many console commands in one go.
/// The region can be ended with <see cref="EndRegistrationRegion"/>.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
void BeginRegistrationRegion();
/// <summary>
/// End a registration region started with <see cref="BeginRegistrationRegion"/>.
/// </summary>
void EndRegistrationRegion();
#endregion
/// <summary>

View File

@@ -34,3 +34,17 @@ public abstract class LocalizedCommands : IConsoleCommand
return ValueTask.FromResult(GetCompletion(shell, args));
}
}
/// <summary>
/// Base class for localized console commands that run in "entity space".
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// These commands are allowed to take dependencies on entity systems, reducing boilerplate for many usages.
/// </para>
/// </remarks>
public abstract class LocalizedEntityCommands : LocalizedCommands, IEntityConsoleCommand;

View File

@@ -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<TransformComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_actorQuery = GetEntityQuery<ActorComponent>();
_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()

View File

@@ -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<ToolshedManager>();
deps.Register<HttpClientHolder>();
deps.Register<RobustMemoryManager>();
deps.Register<EntityConsoleHost>();
}
}
}

View File

@@ -232,6 +232,7 @@ namespace Robust.UnitTesting.Server
container.Register<IParallelManagerInternal, TestingParallelManager>();
// Needed for grid fixture debugging.
container.Register<IConGroupController, ConGroupController>();
container.Register<EntityConsoleHost>();
// I just wanted to load pvs system
container.Register<IServerEntityManager, ServerEntityManager>();