Skip to content

Commit

Permalink
Entity console commands system. (space-wizards#5267)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
PJB3005 and metalgearsloth authored Jun 28, 2024
1 parent 0ba4a66 commit 08970e7
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 29 deletions.
3 changes: 3 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 21 additions & 21 deletions Robust.Shared/Console/Commands/TeleportCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,17 @@
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;
Expand All @@ -36,11 +35,10 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
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))
Expand All @@ -56,25 +54,26 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)

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;
Expand All @@ -89,7 +88,6 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
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))
Expand Down Expand Up @@ -127,8 +125,8 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)

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);
}
}

Expand Down Expand Up @@ -178,9 +176,10 @@ public override CompletionResult GetCompletion(IConsoleShell shell, string[] arg
}
}

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";

Expand All @@ -192,18 +191,19 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
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)
Expand Down Expand Up @@ -246,14 +246,14 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
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);
}

Expand Down
50 changes: 42 additions & 8 deletions Robust.Shared/Console/ConsoleHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public abstract class ConsoleHost : IConsoleHost
[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();

Expand Down Expand Up @@ -61,6 +64,11 @@ public void LoadConsoleCommands()
// 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))
{
Expand All @@ -69,13 +77,31 @@ public void LoadConsoleCommands()
}

RegisteredCommands[instance.Command] = instance;
_autoRegisteredCommands.Add(instance.Command);
}
}

protected virtual void UpdateAvailableCommands()
{
}

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,
Expand All @@ -88,8 +114,7 @@ public void RegisterCommand(
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(
Expand All @@ -104,8 +129,7 @@ public void RegisterCommand(
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(
Expand All @@ -120,8 +144,7 @@ public void RegisterCommand(
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,
Expand Down Expand Up @@ -153,6 +176,15 @@ public void RegisterCommand(
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 />
Expand All @@ -161,12 +193,14 @@ public void UnregisterCommand(string command)
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
Expand Down
54 changes: 54 additions & 0 deletions Robust.Shared/Console/EntityConsoleHost.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
7 changes: 7 additions & 0 deletions Robust.Shared/Console/IConsoleCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,11 @@ ValueTask<CompletionResult> GetCompletionAsync(IConsoleShell shell, string[] arg
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;
}
28 changes: 28 additions & 0 deletions Robust.Shared/Console/IConsoleHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Robust.Shared.Player;
using Robust.Shared.Reflection;
using Robust.Shared.Utility;

namespace Robust.Shared.Console
Expand Down Expand Up @@ -173,6 +174,33 @@ void RegisterCommand(
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>
Expand Down
14 changes: 14 additions & 0 deletions Robust.Shared/Console/LocalizedCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,17 @@ public virtual ValueTask<CompletionResult> GetCompletionAsync(IConsoleShell shel
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;
Loading

0 comments on commit 08970e7

Please sign in to comment.