using System.Numerics; using Content.Client.Shuttles.Systems; using Content.Shared.Shuttles.BUIStates; using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Systems; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Timing; namespace Content.Client.Shuttles.UI; [GenerateTypedNameReferences] public sealed partial class ShuttleDockControl : BaseShuttleControl { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IMapManager _mapManager = default!; private readonly DockingSystem _dockSystem; private readonly SharedShuttleSystem _shuttles; private readonly SharedTransformSystem _xformSystem; public NetEntity? HighlightedDock; public NetEntity? ViewedDock => _viewedState?.Entity; private DockingPortState? _viewedState; public EntityUid? GridEntity; private EntityCoordinates? _coordinates; private Angle? _angle; public DockingInterfaceState? DockState = null; private List> _grids = new(); private readonly HashSet _drawnDocks = new(); private readonly Dictionary _dockButtons = new(); private readonly Color _fallbackHighlightedColor = Color.Magenta; /// /// Store buttons for every other dock /// private readonly Dictionary _dockContainers = new(); private static readonly TimeSpan DockChangeCooldown = TimeSpan.FromSeconds(0.5); /// /// Rate-limiting for docking changes /// private TimeSpan _nextDockChange; public event Action? OnViewDock; public event Action? DockRequest; public event Action? UndockRequest; public ShuttleDockControl() : base(2f, 32f, 8f) { RobustXamlLoader.Load(this); _dockSystem = EntManager.System(); _shuttles = EntManager.System(); _xformSystem = EntManager.System(); MinSize = new Vector2(SizeFull, SizeFull); } public void SetViewedDock(DockingPortState? dockState) { _viewedState = dockState; if (dockState != null) { _coordinates = EntManager.GetCoordinates(dockState.Coordinates); _angle = dockState.Angle; OnViewDock?.Invoke(dockState.Entity); } else { _coordinates = null; _angle = null; } } protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); HideDocks(); _drawnDocks.Clear(); } protected override void Draw(DrawingHandleScreen handle) { base.Draw(handle); DrawBacking(handle); if (_coordinates == null || _angle == null || DockState == null || !EntManager.TryGetComponent(GridEntity, out var gridXform)) { DrawNoSignal(handle); return; } DrawCircles(handle); var gridNent = EntManager.GetNetEntity(GridEntity); var mapPos = _xformSystem.ToMapCoordinates(_coordinates.Value); var ourGridToWorld = _xformSystem.GetWorldMatrix(GridEntity.Value); var selectedDockToOurGrid = Matrix3Helpers.CreateTransform(_coordinates.Value.Position, Angle.Zero); var selectedDockToWorld = Matrix3x2.Multiply(selectedDockToOurGrid, ourGridToWorld); Box2 viewBoundsWorld = Matrix3Helpers.TransformBox(selectedDockToWorld, new Box2(-WorldRangeVector, WorldRangeVector)); Matrix3x2.Invert(selectedDockToWorld, out var worldToSelectedDock); var selectedDockToView = Matrix3x2.CreateScale(new Vector2(MinimapScale, -MinimapScale)) * Matrix3x2.CreateTranslation(MidPointVector); // Draw nearby grids var controlBounds = PixelSizeBox; _grids.Clear(); _mapManager.FindGridsIntersecting(gridXform.MapID, viewBoundsWorld, ref _grids); // offset the dotted-line position to the bounds. Vector2? viewedDockPos = _viewedState != null ? MidPointVector : null; if (viewedDockPos != null) { viewedDockPos = viewedDockPos.Value + _angle.Value.RotateVec(new Vector2(0f,-0.6f) * MinimapScale); } var canDockChange = _timing.CurTime > _nextDockChange; var lineOffset = (float) _timing.RealTime.TotalSeconds * 30f; foreach (var grid in _grids) { EntManager.TryGetComponent(grid.Owner, out IFFComponent? iffComp); if (grid.Owner != GridEntity && !_shuttles.CanDraw(grid.Owner, iffComp: iffComp)) continue; var curGridToWorld = _xformSystem.GetWorldMatrix(grid.Owner); var curGridToView = curGridToWorld * worldToSelectedDock * selectedDockToView; var color = _shuttles.GetIFFColor(grid.Owner, grid.Owner == GridEntity, component: iffComp); DrawGrid(handle, curGridToView, grid, color); // Draw any docks on that grid if (!DockState.Docks.TryGetValue(EntManager.GetNetEntity(grid), out var gridDocks)) continue; foreach (var dock in gridDocks) { if (ViewedDock == dock.Entity) continue; var otherDockRotation = Matrix3Helpers.CreateRotation(dock.Angle); // This box is the AABB of all the vertices we draw below. var dockRenderBoundsLocal = new Box2(-0.5f, -0.7f, 0.5f, 0.5f); var currentDockToCurGrid = Matrix3Helpers.CreateTransform(dock.Coordinates.Position, dock.Angle); var currentDockToWorld = Matrix3x2.Multiply(currentDockToCurGrid, curGridToWorld); var dockRenderBoundsWorld = Matrix3Helpers.TransformBox(currentDockToWorld, dockRenderBoundsLocal); if (!viewBoundsWorld.Intersects(dockRenderBoundsWorld)) continue; var collisionBL = Vector2.Transform(dock.Coordinates.Position + Vector2.Transform(new Vector2(-0.2f, -0.7f), otherDockRotation), curGridToView); var collisionBR = Vector2.Transform(dock.Coordinates.Position + Vector2.Transform(new Vector2(0.2f, -0.7f), otherDockRotation), curGridToView); var collisionTR = Vector2.Transform(dock.Coordinates.Position + Vector2.Transform(new Vector2(0.2f, -0.5f), otherDockRotation), curGridToView); var collisionTL = Vector2.Transform(dock.Coordinates.Position + Vector2.Transform(new Vector2(-0.2f, -0.5f), otherDockRotation), curGridToView); var verts = new[] { collisionBL, collisionBR, collisionBR, collisionTR, collisionTR, collisionTL, collisionTL, collisionBL, }; var collisionCenter = verts[0] + verts[1] + verts[3] + verts[5]; var otherDockConnection = Color.ToSrgb(Color.Pink); handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, otherDockConnection.WithAlpha(0.2f)); handle.DrawPrimitives(DrawPrimitiveTopology.LineList, verts, otherDockConnection); // Draw the dock itself var dockBL = Vector2.Transform(dock.Coordinates.Position + new Vector2(-0.5f, -0.5f), curGridToView); var dockBR = Vector2.Transform(dock.Coordinates.Position + new Vector2(0.5f, -0.5f), curGridToView); var dockTR = Vector2.Transform(dock.Coordinates.Position + new Vector2(0.5f, 0.5f), curGridToView); var dockTL = Vector2.Transform(dock.Coordinates.Position + new Vector2(-0.5f, 0.5f), curGridToView); verts = new[] { dockBL, dockBR, dockBR, dockTR, dockTR, dockTL, dockTL, dockBL }; Color otherDockColor; if (HighlightedDock == dock.Entity) { otherDockColor = Color.ToSrgb(dock.HighlightedColor); } else { otherDockColor = Color.ToSrgb(dock.Color); } /* * Can draw in these conditions: * 1. Same grid * 2. It's in range * * We don't want to draw stuff far away that's docked because it will just overlap our buttons */ var canDraw = grid.Owner == GridEntity; _dockButtons.TryGetValue(dock, out var dockButton); // Rate limit if (dockButton != null && dock.GridDockedWith != null) { dockButton.Disabled = !canDockChange; } // If the dock is in range then also do highlighting if (viewedDockPos != null && dock.Coordinates.NetEntity != gridNent) { collisionCenter /= 4; var range = viewedDockPos.Value - collisionCenter; var maxRange = SharedDockingSystem.DockingHiglightRange * MinimapScale; var maxRangeSq = maxRange * maxRange; if (range.LengthSquared() < maxRangeSq) { if (dock.GridDockedWith == null) { var coordsOne = EntManager.GetCoordinates(_viewedState!.Coordinates); var coordsTwo = EntManager.GetCoordinates(dock.Coordinates); var mapOne = _xformSystem.ToMapCoordinates(coordsOne); var mapTwo = _xformSystem.ToMapCoordinates(coordsTwo); var rotA = _xformSystem.GetWorldRotation(coordsOne.EntityId) + _viewedState!.Angle; var rotB = _xformSystem.GetWorldRotation(coordsTwo.EntityId) + dock.Angle; var distanceSq = (mapOne.Position - mapTwo.Position).LengthSquared(); var inAlignment = _dockSystem.InAlignment(mapOne, rotA, mapTwo, rotB); var maxDockDistSq = SharedDockingSystem.DockRange * SharedDockingSystem.DockRange; var canDock = distanceSq < maxDockDistSq && inAlignment; if (dockButton != null) dockButton.Disabled = !canDock || !canDockChange; var lineColor = inAlignment ? Color.Lime : Color.Red; handle.DrawDottedLine(viewedDockPos.Value, collisionCenter, lineColor, offset: lineOffset); } canDraw = true; } else { if (dockButton != null) dockButton.Disabled = true; } } handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, otherDockColor.WithAlpha(0.2f)); handle.DrawPrimitives(DrawPrimitiveTopology.LineList, verts, otherDockColor); // Position the dock control above it var container = _dockContainers[dock]; container.Visible = canDraw; if (canDraw) { // Because it's being layed out top-down we have to arrange for first frame. container.Arrange(PixelRect); var dockPositionInView = Vector2.Transform(dock.Coordinates.Position, curGridToView); var containerPos = dockPositionInView / UIScale - container.DesiredSize / 2 - new Vector2(0f, 0.75f) * MinimapScale; SetPosition(container, containerPos); } _drawnDocks.Add(dock); } } // Draw the dock's collision var invertedPosition = Vector2.Zero; invertedPosition.Y = -invertedPosition.Y; var rotation = Matrix3Helpers.CreateRotation(-_angle.Value + MathF.PI); var ourDockConnection = new UIBox2( ScalePosition(Vector2.Transform(new Vector2(-0.2f, -0.7f), rotation)), ScalePosition(Vector2.Transform(new Vector2(0.2f, -0.5f), rotation))); var ourDock = new UIBox2( ScalePosition(Vector2.Transform(new Vector2(-0.5f, 0.5f), rotation)), ScalePosition(Vector2.Transform(new Vector2(0.5f, -0.5f), rotation))); var dockColor = _viewedState?.HighlightedColor ?? _fallbackHighlightedColor; var connectionColor = Color.Pink; handle.DrawRect(ourDockConnection, connectionColor.WithAlpha(0.2f)); handle.DrawRect(ourDockConnection, connectionColor, filled: false); // Draw the dock itself handle.DrawRect(ourDock, dockColor.WithAlpha(0.2f)); handle.DrawRect(ourDock, dockColor, filled: false); } private void HideDocks() { foreach (var (dock, control) in _dockContainers) { if (_drawnDocks.Contains(dock)) continue; control.Visible = false; } } public void BuildDocks(EntityUid? shuttle) { var viewedEnt = ViewedDock; _viewedState = null; foreach (var btn in _dockButtons.Values) { btn.Dispose(); } foreach (var container in _dockContainers.Values) { container.Dispose(); } _dockButtons.Clear(); _dockContainers.Clear(); if (DockState == null) return; var gridNent = EntManager.GetNetEntity(GridEntity); foreach (var (otherShuttle, docks) in DockState.Docks) { // If it's our shuttle we add a view button foreach (var dock in docks) { if (dock.Entity == viewedEnt) { _viewedState = dock; } var container = new BoxContainer() { Orientation = BoxContainer.LayoutOrientation.Vertical, Margin = new Thickness(3), }; var panel = new PanelContainer() { HorizontalAlignment = HAlignment.Center, VerticalAlignment = VAlignment.Center, PanelOverride = new StyleBoxFlat(new Color(30, 30, 34, 200)), Children = { container, } }; Button button; if (otherShuttle == gridNent) { button = new Button() { Text = Loc.GetString("shuttle-console-view"), }; button.OnPressed += args => { SetViewedDock(dock); }; } else { if (dock.Connected) { button = new Button() { Text = Loc.GetString("shuttle-console-undock"), }; button.OnPressed += args => { _nextDockChange = _timing.CurTime + DockChangeCooldown; UndockRequest?.Invoke(dock.Entity); }; } else { button = new Button() { Text = Loc.GetString("shuttle-console-dock"), Disabled = true, }; button.OnPressed += args => { if (ViewedDock == null) return; _nextDockChange = _timing.CurTime + DockChangeCooldown; DockRequest?.Invoke(ViewedDock.Value, dock.Entity); }; } _dockButtons.Add(dock, button); } container.AddChild(new Label() { Text = dock.Name, HorizontalAlignment = HAlignment.Center, }); button.HorizontalAlignment = HAlignment.Center; container.AddChild(button); AddChild(panel); panel.Measure(Vector2Helpers.Infinity); _dockContainers[dock] = panel; } } } }