Add abstract tile debug overlay & command (#6213)

* Add generic debug overlay & command

* fix

* Fix overlays

* a

* comments

* comments

* comment 2
This commit is contained in:
Leon Friedrich
2025-10-09 17:49:17 +13:00
committed by GitHub
parent 8ae35e12ee
commit 657455dae0
4 changed files with 346 additions and 0 deletions

View File

@@ -428,3 +428,7 @@ command-description-cmd-info =
On its own, this means it'll print the command's help message.
command-description-comp-rm =
Removes the given component from the entity.
command-description-overlay-toggle = Toggle an overlay on or off
command-description-overlay-add = Add an overlay (if it does not already exist)
command-description-overlay-remove = Remove an overlay

View File

@@ -0,0 +1,53 @@
using System;
using Robust.Client.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.TypeParsers;
using Robust.Shared.Utility;
namespace Robust.Client.Debugging;
[ToolshedCommand]
internal sealed class OverlayCommand : ToolshedCommand
{
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IDynamicTypeFactoryInternal _factory = default!;
[CommandImplementation("toggle")]
internal void Toggle([CommandArgument(customParser:typeof(ReflectionTypeParser<Overlay>))] Type overlay)
{
if (!overlay.IsSubclassOf(typeof(Overlay)))
throw new ArgumentException("Type must be a subclass of overlay");
if (_overlay.HasOverlay(overlay))
Remove(overlay);
else
Add(overlay);
}
[CommandImplementation("add")]
internal void Add([CommandArgument(customParser: typeof(ReflectionTypeParser<Overlay>))] Type overlay)
{
if (!overlay.IsSubclassOf(typeof(Overlay)))
throw new ArgumentException("Type must be a subclass of overlay");
if (!overlay.HasParameterlessConstructor())
throw new ArgumentException("Type must have parameterless constructor");
if (_overlay.HasOverlay(overlay))
return;
// TODO OVERLAYS Give overlays the ContentAccessAllowedAttribute?
var instance = (Overlay) _factory.CreateInstanceUnchecked(overlay, oneOff: true);
if (instance is IPostInjectInit init)
init.PostInject();
_overlay.AddOverlay(instance);
}
[CommandImplementation("remove")]
public void Remove([CommandArgument(customParser: typeof(ReflectionTypeParser<Overlay>))] Type overlay)
{
_overlay.RemoveOverlay(overlay);
}
}

View File

@@ -0,0 +1,229 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
namespace Robust.Client.Debugging.Overlays;
/// <summary>
/// This is an abstract helper class that can be used to create simple debug overlays that need to render tile based data.
/// </summary>
[UsedImplicitly]
public abstract class TileDebugOverlay : Overlay, IPostInjectInit
{
[Dependency] protected readonly IEntityManager Entity = default!;
[Dependency] protected readonly IEyeManager Eye = default!;
[Dependency] protected readonly IMapManager MapMan = default!;
[Dependency] protected readonly IInputManager Input = default!;
[Dependency] protected readonly IUserInterfaceManager Ui = default!;
[Dependency] protected readonly IResourceCache Cache = default!;
protected SharedTransformSystem Transform = default!;
protected MapSystem Map = default!;
protected EntityLookupSystem Lookup = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace | OverlaySpace.ScreenSpace;
protected Font Font = default!;
protected List<Entity<MapGridComponent>> Grids = new();
public void PostInject()
{
Transform = Entity.System<SharedTransformSystem>();
Map = Entity.System<MapSystem>();
Lookup = Entity.System<EntityLookupSystem>();
var font = Cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf");
Font = new VectorFont(font, 8);
Init();
}
protected virtual void Init()
{
}
protected internal override void Draw(in OverlayDrawArgs args)
{
Grids.Clear();
if (args.Viewport.Eye?.Position.MapId is not {} map || map == MapId.Nullspace)
return;
MapMan.FindGridsIntersecting(map, args.WorldBounds, ref Grids);
foreach (var grid in Grids)
{
switch (args.Space)
{
case OverlaySpace.ScreenSpace:
DrawScreen(args, grid);
break;
case OverlaySpace.WorldSpace:
DrawWorld(args, grid);
break;
}
}
Grids.Clear();
}
protected virtual void DrawScreen(in OverlayDrawArgs args, Entity<MapGridComponent> grid)
{
var handle = args.ScreenHandle;
var (_, _, matrix, invMatrix) = Transform.GetWorldPositionRotationMatrixWithInv(grid.Owner);
var gridBounds = invMatrix.TransformBox(args.WorldBounds).Enlarged(grid.Comp.TileSize * 2);
var tilesEnumerator = Map.GetLocalTilesEnumerator(grid, grid, gridBounds);
while (tilesEnumerator.MoveNext(out var tile))
{
var tileBounds = Lookup.GetLocalBounds(tile, grid.Comp.TileSize);
if (!gridBounds.Intersects(tileBounds))
continue;
var screenTileCentre = Eye.WorldToScreen(Vector2.Transform(tileBounds.Center, matrix));
DrawTileText(handle, screenTileCentre, tile.GridIndices, grid);
}
// Draw mouse tooltip
DrawTooltip(handle);
}
protected virtual void DrawTooltip(DrawingHandleScreen handle)
{
var mousePos = Input.MouseScreenPosition;
if (!mousePos.IsValid)
return;
if (Ui.MouseGetControl(mousePos) is not IViewportControl viewport)
return;
var coords = viewport.PixelToMap(mousePos.Position);
if (!MapMan.TryFindGridAt(coords, out var grid, out var comp))
return;
var local = Map.WorldToLocal(grid, comp, coords.Position);
var x = (int) Math.Floor(local.X / comp.TileSize);
var y = (int) Math.Floor(local.Y / comp.TileSize);
var indices = new Vector2i(x, y);
DrawTooltip(handle, mousePos.Position, local, indices, (grid, comp));
}
/// <summary>
/// Draw a tooltip around the mouse
/// </summary>
/// <param name="mouseScreen">The mouse's screen coordinates</param>
/// <param name="mouseLocal">The mouse's local grid coordinates</param>
/// <param name="indices">The mouse's tile indices</param>
/// <param name="grid">The grid that the mouse is hovering over</param>
protected virtual void DrawTooltip(DrawingHandleScreen handle, Vector2 mouseScreen, Vector2 mouseLocal, Vector2i indices, Entity<MapGridComponent> grid)
{
if (GetTooltip(mouseLocal, indices, grid) is not { } text)
return;
var lineHeight = Font.GetLineHeight(1f);
var offset = new Vector2(0, lineHeight);
handle.DrawString(Font, mouseScreen - offset, text);
}
protected virtual void DrawTileText(DrawingHandleScreen handle, Vector2 tileCentre, Vector2i indices, Entity<MapGridComponent> grid)
{
if (GetText(indices, grid) is {} text)
handle.DrawString(Font, tileCentre, text);
}
protected virtual void DrawWorld(in OverlayDrawArgs args, Entity<MapGridComponent> grid)
{
var handle = args.WorldHandle;
var (_, _, matrix, invMatrix) = Transform.GetWorldPositionRotationMatrixWithInv(grid.Owner);
var gridBounds = invMatrix.TransformBox(args.WorldBounds).Enlarged(grid.Comp.TileSize * 2);
var tilesEnumerator = Map.GetLocalTilesEnumerator(grid, grid, gridBounds);
while (tilesEnumerator.MoveNext(out var tile))
{
handle.SetTransform(matrix);
var tileBounds = Lookup.GetLocalBounds(tile, grid.Comp.TileSize);
if (gridBounds.Intersects(tileBounds))
DrawTile(handle, tileBounds, tile.GridIndices, grid);
}
handle.SetTransform(Matrix3x2.Identity);
}
protected virtual void DrawTile(DrawingHandleWorld handle, Box2 tile, Vector2i indices, Entity<MapGridComponent> grid)
{
if (GetColor(indices, grid) is not { } color)
return;
handle.DrawRect(tile, color.Border, filled: false);
handle.DrawRect(tile, color.Fill, filled: true);
}
/// <summary>
/// Get text that will be rendered in a grid tile.
/// </summary>
protected abstract string? GetText(Vector2i indices, Entity<MapGridComponent> grid);
/// <summary>
/// Get tooltip text that will be shown next to the mouse.
/// </summary>
/// <param name="mousePos">The mouse's position relative to the grid.</param>
/// <param name="gridIndices">The grid indices corresponding to the mouse's position</param>
/// <param name="grid">The grid that the mouse is over.</param>
protected abstract string? GetTooltip(Vector2 mousePos, Vector2i indices, Entity<MapGridComponent> grid);
/// <summary>
/// Get a border & fill color that will be used to draw a grid tile.
/// </summary>
protected abstract (Color Fill, Color Border)? GetColor(Vector2i indices, Entity<MapGridComponent> grid);
}
/// <summary>
/// Variant of <see cref="TileDebugOverlay"/> that exists to draw simple float information for each tile.
/// </summary>
public abstract class TileFloatDebugOverlay : TileDebugOverlay
{
protected virtual float MinValue => 0;
protected virtual float MaxValue => 1;
protected abstract float? GetData(Vector2i indices, Entity<MapGridComponent> grid);
protected override string? GetText(Vector2i indices, Entity<MapGridComponent> grid)
{
return GetData(indices, grid)?.ToString("F2");
}
protected override string? GetTooltip(Vector2 mousePos, Vector2i indices, Entity<MapGridComponent> grid)
{
return GetData(indices, grid)?.ToString("F2");
}
protected override (Color Fill, Color Border)? GetColor(Vector2i indices, Entity<MapGridComponent> grid)
{
if (GetData(indices, grid) is not { } value)
return null;
var color = Gradient(value, MinValue, MaxValue);
return (color.WithAlpha(0.2f), color);
}
/// <summary>
/// Simple yellow -> orange -> red gradient.
/// </summary>
public Color Gradient(float value, float min, float max)
{
// map min to 1, max to 0
value = (value - min) / (max - min);
return value < 0.5f
? Color.InterpolateBetween(Color.Yellow, Color.Orange, value * 2)
: Color.InterpolateBetween(Color.Orange, Color.Red, (value - 0.5f) * 2);
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using Robust.Shared.Toolshed.Syntax;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.TypeParsers;
/// <summary>
/// This is custom type parser that uses reflection to search for constructible types that are the children of some base type.
/// </summary>
internal sealed class ReflectionTypeParser<TBase> : CustomTypeParser<Type> where TBase : class
{
[Dependency] private readonly IReflectionManager _reflection = default!;
private Dictionary<string, Type>? _cache;
private CompletionOption[]? _options;
[MemberNotNull(nameof(_cache))]
[MemberNotNull(nameof(_options))]
private void InitCache()
{
if (_cache != null && _options != null)
return;
_cache = _reflection.GetAllChildren(typeof(TBase))
.Where(x => x.HasParameterlessConstructor())
.ToDictionary(x => x.Name, x => x);
_options = _cache.Keys.Select(x => new CompletionOption(x)).ToArray();
}
public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? result)
{
InitCache();
var name = ctx.GetWord();
if (name is null)
{
ctx.Error = new OutOfInputError();
result = null;
return false;
}
if (_cache.TryGetValue(name, out result))
return true;
ctx.Error = new UnknownType(name);
result = null;
return false;
}
public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
{
InitCache();
return CompletionResult.FromHintOptions(_options, GetArgHint(arg));
}
}