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; /// /// This is an abstract helper class that can be used to create simple debug overlays that need to render tile based data. /// [UsedImplicitly] public abstract partial class TileDebugOverlay : Overlay, IPostInjectInit { [Dependency] protected IEntityManager Entity = default!; [Dependency] protected IEyeManager Eye = default!; [Dependency] protected IInputManager Input = default!; [Dependency] protected IUserInterfaceManager Ui = default!; [Dependency] protected 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> Grids = new(); void IPostInjectInit.PostInject() { Transform = Entity.System(); Map = Entity.System(); Lookup = Entity.System(); var font = Cache.GetResource("/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; Map.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 grid) { var handle = args.ScreenHandle; var (_, _, matrix, invMatrix) = Transform.GetWorldPositionRotationMatrixWithInv(grid.Owner); var gridBounds = invMatrix.TransformBox(args.WorldBounds).Enlarged(grid.Comp.TileSize * 2); foreach (var tile in Map.GetLocalTilesIntersecting(grid, grid, gridBounds)) { 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 (!Map.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)); } /// /// Draw a tooltip around the mouse /// /// The mouse's screen coordinates /// The mouse's local grid coordinates /// The mouse's tile indices /// The grid that the mouse is hovering over protected virtual void DrawTooltip(DrawingHandleScreen handle, Vector2 mouseScreen, Vector2 mouseLocal, Vector2i indices, Entity 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 grid) { if (GetText(indices, grid) is {} text) handle.DrawString(Font, tileCentre, text); } protected virtual void DrawWorld(in OverlayDrawArgs args, Entity grid) { var handle = args.WorldHandle; var (_, _, matrix, invMatrix) = Transform.GetWorldPositionRotationMatrixWithInv(grid.Owner); var gridBounds = invMatrix.TransformBox(args.WorldBounds).Enlarged(grid.Comp.TileSize * 2); foreach (var tile in Map.GetLocalTilesIntersecting(grid, grid, gridBounds)) { 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 grid) { if (GetColor(indices, grid) is not { } color) return; handle.DrawRect(tile, color.Border, filled: false); handle.DrawRect(tile, color.Fill, filled: true); } /// /// Get text that will be rendered in a grid tile. /// protected abstract string? GetText(Vector2i indices, Entity grid); /// /// Get tooltip text that will be shown next to the mouse. /// /// The mouse's position relative to the grid. /// The grid indices corresponding to the mouse's position /// The grid that the mouse is over. protected abstract string? GetTooltip(Vector2 mousePos, Vector2i indices, Entity grid); /// /// Get a border & fill color that will be used to draw a grid tile. /// protected abstract (Color Fill, Color Border)? GetColor(Vector2i indices, Entity grid); } /// /// Variant of that exists to draw simple float information for each tile. /// public abstract class TileFloatDebugOverlay : TileDebugOverlay { protected virtual float MinValue => 0; protected virtual float MaxValue => 1; protected abstract float? GetData(Vector2i indices, Entity grid); protected override string? GetText(Vector2i indices, Entity grid) { return GetData(indices, grid)?.ToString("F2"); } protected override string? GetTooltip(Vector2 mousePos, Vector2i indices, Entity grid) { return GetData(indices, grid)?.ToString("F2"); } protected override (Color Fill, Color Border)? GetColor(Vector2i indices, Entity grid) { if (GetData(indices, grid) is not { } value) return null; var color = Gradient(value, MinValue, MaxValue); return (color.WithAlpha(0.2f), color); } /// /// Simple yellow -> orange -> red gradient. /// 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); } }