]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Shuttle console + FTL rework (#24430)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Sun, 3 Mar 2024 07:39:19 +0000 (18:39 +1100)
committerGitHub <noreply@github.com>
Sun, 3 Mar 2024 07:39:19 +0000 (18:39 +1100)
* Add shuttle interior drawing back

Just do it per-tile she'll be right, at least it's done with 1 draw call.

* Revamp shuttle console

* Bunch of cleanup work

* Lables sortito

* dok

* Pixel alignment and colours

* Fix a bunch of drawing bugs

* Shuttle map drawing

* Drawing fixes

* Map parallax working finally

* weh

* Commit all my stuff

* mic

* deez

* Update everything

* Xamlify everything

* uh

* Rudimentary blocker range

* My enemies have succeeded

* Bunch of changes to FTL

* Heaps of cleanup

* Fix FTL bugs

* FTL

* weewoo

* FTL fallback

* wew

* weh

* Basic FTL working

* FTL working

* FTL destination fixes

* a

* Exclusion zones

* Fix drawing / FTL

* Beacons working

* Coordinates drawing

* Fix unknown map names

* Dorks beginning

* State + docking cleanup start

* Basic dock drawing

* Bunch of drawing fixes

* Batching / color fixes

* Cleanup and beacons support

* weh

* weh

* Begin pings

* First draft at map objects

* Map fixup

* Faster drawing

* Fix perf + FTL

* Cached drawing

* Fix drawing

* Best I got

* strips

* Back to lists but with caching

* Final optimisation

* Fix dock bounds

* Docking work

* stinker

* kobolds

* Btns

* Docking vis working

* Fix docking pre-vis

* canasses

* Helldivers 2

* a

* Array life

* Fix

* Fix TODOs

* liltenhead feature club

* dorking

* Merge artifacts

* Last-minute touchup

99 files changed:
Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml
Content.Client/Atmos/Monitor/UI/Widgets/PumpControl.xaml
Content.Client/Atmos/Monitor/UI/Widgets/ScrubberControl.xaml
Content.Client/Atmos/Monitor/UI/Widgets/SensorInfo.xaml
Content.Client/Atmos/Monitor/UI/Widgets/ThresholdControl.xaml
Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml
Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml
Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml
Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs
Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs
Content.Client/Pinpointer/UI/NavMapControl.cs
Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs
Content.Client/Shuttles/BUI/RadarConsoleBoundUserInterface.cs
Content.Client/Shuttles/BUI/ShuttleConsoleBoundUserInterface.cs
Content.Client/Shuttles/ShuttleConsoleComponent.cs
Content.Client/Shuttles/Systems/DockingSystem.cs
Content.Client/Shuttles/Systems/ShuttleSystem.Console.cs [new file with mode: 0644]
Content.Client/Shuttles/UI/BaseShuttleControl.xaml [new file with mode: 0644]
Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs [new file with mode: 0644]
Content.Client/Shuttles/UI/DockObject.xaml [new file with mode: 0644]
Content.Client/Shuttles/UI/DockObject.xaml.cs [new file with mode: 0644]
Content.Client/Shuttles/UI/DockingControl.cs [deleted file]
Content.Client/Shuttles/UI/DockingScreen.xaml [new file with mode: 0644]
Content.Client/Shuttles/UI/DockingScreen.xaml.cs [new file with mode: 0644]
Content.Client/Shuttles/UI/MapScreen.xaml [new file with mode: 0644]
Content.Client/Shuttles/UI/MapScreen.xaml.cs [new file with mode: 0644]
Content.Client/Shuttles/UI/NavScreen.xaml [new file with mode: 0644]
Content.Client/Shuttles/UI/NavScreen.xaml.cs [new file with mode: 0644]
Content.Client/Shuttles/UI/RadarConsoleWindow.xaml
Content.Client/Shuttles/UI/RadarConsoleWindow.xaml.cs
Content.Client/Shuttles/UI/RadarControl.cs [deleted file]
Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml
Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs
Content.Client/Shuttles/UI/ShuttleDockControl.xaml [new file with mode: 0644]
Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs [new file with mode: 0644]
Content.Client/Shuttles/UI/ShuttleMapControl.xaml [new file with mode: 0644]
Content.Client/Shuttles/UI/ShuttleMapControl.xaml.cs [new file with mode: 0644]
Content.Client/Shuttles/UI/ShuttleNavControl.xaml [new file with mode: 0644]
Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs [new file with mode: 0644]
Content.Client/UserInterface/Controls/MapGridControl.cs [deleted file]
Content.Client/UserInterface/Controls/MapGridControl.xaml [new file with mode: 0644]
Content.Client/UserInterface/Controls/MapGridControl.xaml.cs [new file with mode: 0644]
Content.Server/Salvage/SalvageSystem.Runner.cs
Content.Server/Shuttles/Commands/DockCommand.cs
Content.Server/Shuttles/Components/AutoDockComponent.cs [deleted file]
Content.Server/Shuttles/Components/FTLBeaconComponent.cs [new file with mode: 0644]
Content.Server/Shuttles/Components/FTLComponent.cs
Content.Server/Shuttles/Components/FTLDestinationComponent.cs [deleted file]
Content.Server/Shuttles/Components/FTLExclusionComponent.cs [new file with mode: 0644]
Content.Server/Shuttles/Components/RecentlyDockedComponent.cs [deleted file]
Content.Server/Shuttles/Systems/ArrivalsSystem.cs
Content.Server/Shuttles/Systems/DockingSystem.AutoDock.cs [deleted file]
Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs
Content.Server/Shuttles/Systems/DockingSystem.cs
Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs
Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs
Content.Server/Shuttles/Systems/RadarConsoleSystem.cs
Content.Server/Shuttles/Systems/ShuttleConsoleSystem.Drone.cs
Content.Server/Shuttles/Systems/ShuttleConsoleSystem.FTL.cs [new file with mode: 0644]
Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs
Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs
Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs
Content.Server/Shuttles/Systems/ShuttleSystem.cs
Content.Shared/Parallax/ParallaxComponent.cs
Content.Shared/Shuttles/BUIStates/DockingInterfaceState.cs [new file with mode: 0644]
Content.Shared/Shuttles/BUIStates/DockingPortState.cs [new file with mode: 0644]
Content.Shared/Shuttles/BUIStates/NavBoundUserInterfaceState.cs [new file with mode: 0644]
Content.Shared/Shuttles/BUIStates/NavInterfaceState.cs [moved from Content.Shared/Shuttles/BUIStates/RadarConsoleBoundInterfaceState.cs with 53% similarity]
Content.Shared/Shuttles/BUIStates/ShuttleBoundUserInterfaceState.cs [new file with mode: 0644]
Content.Shared/Shuttles/BUIStates/ShuttleConsoleBoundInterfaceState.cs [deleted file]
Content.Shared/Shuttles/BUIStates/ShuttleMapInterfaceState.cs [new file with mode: 0644]
Content.Shared/Shuttles/Components/FTLDestinationComponent.cs [new file with mode: 0644]
Content.Shared/Shuttles/Components/FTLMapComponent.cs [new file with mode: 0644]
Content.Shared/Shuttles/Components/IFFComponent.cs
Content.Shared/Shuttles/Components/PilotComponent.cs
Content.Shared/Shuttles/Components/ShuttleMapParallaxComponent.cs [new file with mode: 0644]
Content.Shared/Shuttles/Events/DockRequestMessage.cs [moved from Content.Shared/Shuttles/Events/AutodockRequestMessage.cs with 64% similarity]
Content.Shared/Shuttles/Events/ShuttleConsoleFTLBeaconMessage.cs [new file with mode: 0644]
Content.Shared/Shuttles/Events/ShuttleConsoleFTLPositionMessage.cs [moved from Content.Shared/Shuttles/Events/ShuttleConsoleFTLRequestMessage.cs with 54% similarity]
Content.Shared/Shuttles/Events/StopAutodockRequestMessage.cs [deleted file]
Content.Shared/Shuttles/Systems/SharedDockingSystem.cs [new file with mode: 0644]
Content.Shared/Shuttles/Systems/SharedRadarConsoleSystem.cs
Content.Shared/Shuttles/Systems/SharedShuttleConsoleSystem.cs
Content.Shared/Shuttles/Systems/SharedShuttleSystem.IFF.cs
Content.Shared/Shuttles/Systems/SharedShuttleSystem.cs
Content.Shared/Shuttles/UI/MapObjects/GridMapObject.cs [new file with mode: 0644]
Content.Shared/Shuttles/UI/MapObjects/IMapObject.cs [new file with mode: 0644]
Content.Shared/Shuttles/UI/MapObjects/ShuttleBeaconObject.cs [new file with mode: 0644]
Content.Shared/Shuttles/UI/MapObjects/ShuttleExclusionObject.cs [new file with mode: 0644]
Resources/Audio/Effects/Shuttle/attributions.yml
Resources/Audio/Effects/Shuttle/radar_ping.ogg [new file with mode: 0644]
Resources/Locale/en-US/shuttles/console.ftl
Resources/Maps/Nonstations/nukieplanet.yml
Resources/Maps/Shuttles/infiltrator.yml
Resources/Prototypes/Entities/Markers/shuttle.yml
Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
Resources/Prototypes/Procedural/salvage_mods.yml
Resources/Textures/Parallaxes/space_map2.png [new file with mode: 0644]
Resources/Textures/Parallaxes/weh.txt [new file with mode: 0644]

index 9e0f9d182eaad29beef79b810053737dfa357268..01259b60f7ab07fb5e2f5d53de4099ae3f07bc45 100644 (file)
@@ -12,7 +12,7 @@
         <BoxContainer Name="VolumeBox" Orientation="Vertical" HorizontalExpand="True" Margin="0 4"/>
 
         <!-- The temperature / heat capacity / thermal energy of the solution -->
-        <Collapsible Orientation="Vertical">
+        <Collapsible>
             <CollapsibleHeading Name="ThermalHeading" Title="{Loc 'admin-solutions-window-thermals'}" />
             <CollapsibleBody>
                 <BoxContainer Name="ThermalBox" Orientation="Vertical" HorizontalExpand="True" Margin="0 4"/>
@@ -23,7 +23,7 @@
         <ScrollContainer HorizontalExpand="True" VerticalExpand="True" Margin="0 4">
             <BoxContainer Name="ReagentList" Orientation="Vertical"/>
         </ScrollContainer>
-        
+
         <Button Name="AddButton" Text="{Loc 'admin-solutions-window-add-new-button'}" HorizontalExpand="True"  Margin="0 4"/>
     </BoxContainer>
 </DefaultWindow>
index 632e44a4587fbef365821146b95e3e551eed2fe0..5fb4e5f0c8c28632961f134387c8a6b442c1c2c5 100644 (file)
@@ -1,7 +1,7 @@
 <BoxContainer xmlns="https://spacestation14.io"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          Orientation="Vertical" Margin="2 0 2 4">
-    <Collapsible Orientation="Vertical">
+    <Collapsible>
         <CollapsibleHeading Name="CAddress" />
         <!-- Upper row: toggle, direction, checks -->
         <CollapsibleBody Margin="20 0 0 0">
index 1cb9c9ed5b4c3da6af7f50008c2d7f733d6591fc..34c1a9dd1a9ec9e6e9550cf60b7d95f30e971dab 100644 (file)
@@ -1,7 +1,7 @@
 <BoxContainer xmlns="https://spacestation14.io"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          Orientation="Vertical" Margin="2 0 2 4">
-    <Collapsible Orientation="Vertical">
+    <Collapsible>
         <CollapsibleHeading Name="CAddress" />
         <CollapsibleBody Margin="20 0 0 0">
             <BoxContainer Orientation="Vertical">
@@ -26,7 +26,7 @@
                     <Button Name="CCopySettings" Text="{Loc 'air-alarm-ui-widget-copy'}" ToolTip="{Loc 'air-alarm-ui-widget-copy-tooltip'}" />
                 </BoxContainer>
                 <!-- Lower row: every single gas -->
-                <Collapsible Orientation="Vertical" Margin="2 2 2 2">
+                <Collapsible Margin="2 2 2 2">
                     <CollapsibleHeading Title="Gas filters" />
                     <CollapsibleBody Margin="20 0 0 0">
                         <GridContainer HorizontalExpand="True" Name="CGasContainer" Columns="3" />
index b90ca3f1f66755a31f2d9da5378ba6cc47915cf6..005e6807b37df057f41c98f75c77072127c414eb 100644 (file)
@@ -1,5 +1,5 @@
 <BoxContainer xmlns="https://spacestation14.io" HorizontalExpand="True">
-    <Collapsible Orientation="Vertical">
+    <Collapsible>
         <CollapsibleHeading Name="SensorAddress" />
         <CollapsibleBody Margin="20 2 2 2">
             <BoxContainer Orientation="Vertical" HorizontalExpand="True">
@@ -10,7 +10,7 @@
                     <RichTextLabel Name="TemperatureLabel" />
                     <Control Name="TemperatureThresholdContainer" Margin="20 0 2 0" />
                 </BoxContainer>
-                <Collapsible Orientation="Vertical" Margin="2">
+                <Collapsible Margin="2">
                     <CollapsibleHeading Title="{Loc 'air-alarm-ui-sensor-gases'}" />
                     <CollapsibleBody Margin="20 0 0 0">
                         <BoxContainer Name="GasContainer" Orientation="Vertical" Margin="2" />
index 0f53673da10ba44714fa36c91e4e0242602ef05e..635a70f532c5c28377f3a7aa3fc78dd92f05e24e 100644 (file)
@@ -1,7 +1,7 @@
 <BoxContainer xmlns="https://spacestation14.io"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          Orientation="Vertical" Margin="0 0 0 4">
-    <Collapsible Orientation="Vertical">
+    <Collapsible>
         <CollapsibleHeading Name="CName" />
         <CollapsibleBody Margin="20 0 0 0">
             <BoxContainer Orientation="Vertical">
index 176acbf29b5326237bbb0e5427a5391bf10ece62..5acb142a60721c8d3c088036e6098ad8977f5549 100644 (file)
@@ -11,7 +11,7 @@
         <PanelContainer.PanelOverride>
             <graphics:StyleBoxFlat BackgroundColor="{xNamespace:Static style:StyleNano.ButtonColorDisabled}" />
         </PanelContainer.PanelOverride>
-        <Collapsible Orientation="Vertical" Name="Collapsible">
+        <Collapsible Name="Collapsible">
             <CollapsibleHeading Name="Heading" MinHeight="35"/>
             <CollapsibleBody Name="Body">
                 <BoxContainer Name="ItemsContainer" Orientation="Vertical" HorizontalExpand="True"/>
index 752c9cc6c2c4a922282ba2006aa03c5513941200..f46e319abeb5c7ed30a43e9bcef222b09acd1aad 100644 (file)
@@ -11,7 +11,7 @@
                 <RichTextLabel Name="ReagentName" HorizontalAlignment="Center"/>
             </PanelContainer>
             <BoxContainer Name="RecipesContainer" HorizontalExpand="True">
-                <Collapsible Orientation="Vertical" HorizontalExpand="True">
+                <Collapsible HorizontalExpand="True">
                     <CollapsibleHeading Title="{Loc 'guidebook-reagent-recipes-header'}"/>
                     <CollapsibleBody>
                         <GridContainer Name="RecipesDescriptionContainer"
@@ -24,7 +24,7 @@
                 </Collapsible>
             </BoxContainer>
             <BoxContainer Name="SourcesContainer" HorizontalExpand="True">
-                <Collapsible Orientation="Vertical" HorizontalExpand="True">
+                <Collapsible HorizontalExpand="True">
                     <CollapsibleHeading Title="{Loc 'guidebook-reagent-sources-header'}"/>
                     <CollapsibleBody>
                         <GridContainer Name="SourcesDescriptionContainer"
@@ -37,7 +37,7 @@
                 </Collapsible>
             </BoxContainer>
             <BoxContainer Name="EffectsContainer" HorizontalExpand="True">
-                <Collapsible Orientation="Vertical">
+                <Collapsible>
                     <CollapsibleHeading Title="{Loc 'guidebook-reagent-effects-header'}"/>
                     <CollapsibleBody>
                         <BoxContainer Name="EffectsDescriptionContainer"
index d32d3ba2cf8815a5f8d414286e0515a05e85c972..78e65db2a37896676d06d50695879c21a7c66ac6 100644 (file)
@@ -7,7 +7,7 @@
                 <CheckBox Name="MarkingForced" Text="Force" Pressed="True" />
                 <CheckBox Name="MarkingIgnoreSpecies" Text="Ignore Species" Pressed="True" />
             </BoxContainer>
-            <Collapsible Orientation="Vertical" HorizontalExpand="True">
+            <Collapsible HorizontalExpand="True">
                 <CollapsibleHeading Title="Base layers" />
                 <CollapsibleBody HorizontalExpand="True">
                     <BoxContainer Name="BaseLayersContainer" Orientation="Vertical" HorizontalExpand="True" />
index fcecbad465aca3fcfc0c184c83f1f939c4420b63..340cc9af891c76ee0a7d7de8f9fba5c2e213a4ab 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Client.Pinpointer.UI;
 using Robust.Client.Graphics;
 using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Timing;
 
 namespace Content.Client.Medical.CrewMonitoring;
 
@@ -16,7 +17,7 @@ public sealed partial class CrewMonitoringNavMapControl : NavMapControl
     {
         WallColor = new Color(192, 122, 196);
         TileColor = new(71, 42, 72);
-        _backgroundColor = Color.FromSrgb(TileColor.WithAlpha(_backgroundOpacity));
+        BackgroundColor = Color.FromSrgb(TileColor.WithAlpha(BackgroundOpacity));
 
         _trackedEntityLabel = new Label
         {
@@ -30,7 +31,7 @@ public sealed partial class CrewMonitoringNavMapControl : NavMapControl
         {
             PanelOverride = new StyleBoxFlat
             {
-                BackgroundColor = _backgroundColor,
+                BackgroundColor = BackgroundColor,
             },
 
             Margin = new Thickness(5f, 10f),
@@ -43,9 +44,9 @@ public sealed partial class CrewMonitoringNavMapControl : NavMapControl
         this.AddChild(_trackedEntityPanel);
     }
 
-    protected override void Draw(DrawingHandleScreen handle)
+    protected override void FrameUpdate(FrameEventArgs args)
     {
-        base.Draw(handle);
+        base.FrameUpdate(args);
 
         if (Focus == null)
         {
index 645243b0a3a15d69bb0ecf2e6f64cede037b0270..39326c8a99c49626a602388697fe10d78d026658 100644 (file)
@@ -23,7 +23,6 @@ namespace Content.Client.Medical.CrewMonitoring;
 [GenerateTypedNameReferences]
 public sealed partial class CrewMonitoringWindow : FancyWindow
 {
-    private List<Control> _rowsContent = new();
     private readonly IEntityManager _entManager;
     private readonly IPrototypeManager _prototypeManager;
     private readonly SpriteSystem _spriteSystem;
@@ -100,7 +99,6 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
                 };
 
                 SensorsTable.AddChild(spacer);
-                _rowsContent.Add(spacer);
             }
 
             var deparmentLabel = new RichTextLabel()
@@ -113,7 +111,6 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
             deparmentLabel.StyleClasses.Add(StyleNano.StyleClassTooltipActionDescription);
 
             SensorsTable.AddChild(deparmentLabel);
-            _rowsContent.Add(deparmentLabel);
 
             PopulateDepartmentList(departmentSensors);
         }
@@ -129,7 +126,6 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
             };
 
             SensorsTable.AddChild(spacer);
-            _rowsContent.Add(spacer);
 
             var deparmentLabel = new RichTextLabel()
             {
@@ -141,7 +137,6 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
             deparmentLabel.StyleClasses.Add(StyleNano.StyleClassTooltipActionDescription);
 
             SensorsTable.AddChild(deparmentLabel);
-            _rowsContent.Add(deparmentLabel);
 
             PopulateDepartmentList(remainingSensors);
         }
@@ -175,7 +170,6 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
                 sensorButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
 
             SensorsTable.AddChild(sensorButton);
-            _rowsContent.Add(sensorButton);
 
             // Primary container to hold the button UI elements
             var mainContainer = new BoxContainer()
@@ -422,7 +416,6 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
     private void ClearOutDatedData()
     {
         SensorsTable.RemoveAllChildren();
-        _rowsContent.Clear();
         NavMap.TrackedCoordinates.Clear();
         NavMap.TrackedEntities.Clear();
         NavMap.LocalizedNames.Clear();
index 3b426e73d89143b060df541bf7e0e60f6e6e9f0d..a8ec7b37a0b3f7116296bdc7f68375d09688fa86 100644 (file)
@@ -25,12 +25,14 @@ namespace Content.Client.Pinpointer.UI;
 [UsedImplicitly, Virtual]
 public partial class NavMapControl : MapGridControl
 {
-    [Dependency] private readonly IEntityManager _entManager = default!;
+    [Dependency] private IResourceCache _cache = default!;
     private readonly SharedTransformSystem _transformSystem;
 
     public EntityUid? Owner;
     public EntityUid? MapUid;
 
+    protected override bool Draggable => true;
+
     // Actions
     public event Action<NetEntity?>? TrackedEntitySelectedAction;
     public event Action<DrawingHandleScreen>? PostWallDrawingAction;
@@ -47,23 +49,17 @@ public partial class NavMapControl : MapGridControl
     // Constants
     protected float UpdateTime = 1.0f;
     protected float MaxSelectableDistance = 10f;
-    protected float RecenterMinimum = 0.05f;
     protected float MinDragDistance = 5f;
     protected static float MinDisplayedRange = 8f;
     protected static float MaxDisplayedRange = 128f;
     protected static float DefaultDisplayedRange = 48f;
 
     // Local variables
-    private Vector2 _offset;
-    private bool _draggin;
-    private Vector2 _startDragPosition = default!;
-    private bool _recentering = false;
     private float _updateTimer = 0.25f;
-    private Dictionary<Color, Color> _sRGBLookUp = new Dictionary<Color, Color>();
-    public Color _backgroundColor;
-    public float _backgroundOpacity = 0.9f;
+    private Dictionary<Color, Color> _sRGBLookUp = new();
+    protected Color BackgroundColor;
+    protected float BackgroundOpacity = 0.9f;
     private int _targetFontsize = 8;
-    private IResourceCache _cache;
 
     // Components
     private NavMapComponent? _navMap;
@@ -100,10 +96,9 @@ public partial class NavMapControl : MapGridControl
     public NavMapControl() : base(MinDisplayedRange, MaxDisplayedRange, DefaultDisplayedRange)
     {
         IoCManager.InjectDependencies(this);
-        _cache = IoCManager.Resolve<IResourceCache>();
 
-        _transformSystem = _entManager.System<SharedTransformSystem>();
-        _backgroundColor = Color.FromSrgb(TileColor.WithAlpha(_backgroundOpacity));
+        _transformSystem = EntManager.System<SharedTransformSystem>();
+        BackgroundColor = Color.FromSrgb(TileColor.WithAlpha(BackgroundOpacity));
 
         RectClipContent = true;
         HorizontalExpand = true;
@@ -145,21 +140,16 @@ public partial class NavMapControl : MapGridControl
 
         _recenter.OnPressed += args =>
         {
-            _recentering = true;
+            Recentering = true;
         };
 
         ForceNavMapUpdate();
     }
 
-    public void ForceRecenter()
-    {
-        _recentering = true;
-    }
-
     public void ForceNavMapUpdate()
     {
-        _entManager.TryGetComponent(MapUid, out _navMap);
-        _entManager.TryGetComponent(MapUid, out _grid);
+        EntManager.TryGetComponent(MapUid, out _navMap);
+        EntManager.TryGetComponent(MapUid, out _grid);
 
         UpdateNavMap();
     }
@@ -167,29 +157,15 @@ public partial class NavMapControl : MapGridControl
     public void CenterToCoordinates(EntityCoordinates coordinates)
     {
         if (_physics != null)
-            _offset = new Vector2(coordinates.X, coordinates.Y) - _physics.LocalCenter;
+            Offset = new Vector2(coordinates.X, coordinates.Y) - _physics.LocalCenter;
 
         _recenter.Disabled = false;
     }
 
-    protected override void KeyBindDown(GUIBoundKeyEventArgs args)
-    {
-        base.KeyBindDown(args);
-
-        if (args.Function == EngineKeyFunctions.Use)
-        {
-            _startDragPosition = args.PointerLocation.Position;
-            _draggin = true;
-        }
-    }
-
     protected override void KeyBindUp(GUIBoundKeyEventArgs args)
     {
         base.KeyBindUp(args);
 
-        if (args.Function == EngineKeyFunctions.Use)
-            _draggin = false;
-
         if (args.Function == EngineKeyFunctions.UIClick)
         {
             if (TrackedEntitySelectedAction == null)
@@ -199,15 +175,15 @@ public partial class NavMapControl : MapGridControl
                 return;
 
             // If the cursor has moved a significant distance, exit
-            if ((_startDragPosition - args.PointerLocation.Position).Length() > MinDragDistance)
+            if ((StartDragPosition - args.PointerLocation.Position).Length() > MinDragDistance)
                 return;
 
             // Get the clicked position
-            var offset = _offset + _physics.LocalCenter;
+            var offset = Offset + _physics.LocalCenter;
             var localPosition = args.PointerLocation.Position - GlobalPixelPosition;
 
             // Convert to a world position
-            var unscaledPosition = (localPosition - MidpointVector) / MinimapScale;
+            var unscaledPosition = (localPosition - MidPointVector) / MinimapScale;
             var worldPosition = _transformSystem.GetWorldMatrix(_xform).Transform(new Vector2(unscaledPosition.X, -unscaledPosition.Y) + offset);
 
             // Find closest tracked entity in range
@@ -219,7 +195,7 @@ public partial class NavMapControl : MapGridControl
                 if (!blip.Selectable)
                     continue;
 
-                var currentDistance = (blip.Coordinates.ToMapPos(_entManager, _transformSystem) - worldPosition).Length();
+                var currentDistance = (blip.Coordinates.ToMapPos(EntManager, _transformSystem) - worldPosition).Length();
 
                 if (closestDistance < currentDistance || currentDistance * MinimapScale > MaxSelectableDistance)
                     continue;
@@ -251,15 +227,8 @@ public partial class NavMapControl : MapGridControl
     {
         base.MouseMove(args);
 
-        if (!_draggin)
-            return;
-
-        _recentering = false;
-        _offset -= new Vector2(args.Relative.X, -args.Relative.Y) / MidPoint * WorldRange;
-
-        if (_offset != Vector2.Zero)
+        if (Offset != Vector2.Zero)
             _recenter.Disabled = false;
-
         else
             _recenter.Disabled = true;
     }
@@ -269,36 +238,21 @@ public partial class NavMapControl : MapGridControl
         base.Draw(handle);
 
         // Get the components necessary for drawing the navmap
-        _entManager.TryGetComponent(MapUid, out _navMap);
-        _entManager.TryGetComponent(MapUid, out _grid);
-        _entManager.TryGetComponent(MapUid, out _xform);
-        _entManager.TryGetComponent(MapUid, out _physics);
-        _entManager.TryGetComponent(MapUid, out _fixtures);
+        EntManager.TryGetComponent(MapUid, out _navMap);
+        EntManager.TryGetComponent(MapUid, out _grid);
+        EntManager.TryGetComponent(MapUid, out _xform);
+        EntManager.TryGetComponent(MapUid, out _physics);
+        EntManager.TryGetComponent(MapUid, out _fixtures);
 
         // Map re-centering
-        if (_recentering)
-        {
-            var frameTime = Timing.FrameTime;
-            var diff = _offset * (float) frameTime.TotalSeconds;
-
-            if (_offset.LengthSquared() < RecenterMinimum)
-            {
-                _offset = Vector2.Zero;
-                _recentering = false;
-                _recenter.Disabled = true;
-            }
-            else
-            {
-                _offset -= diff * 5f;
-            }
-        }
+        _recenter.Disabled = DrawRecenter();
 
         _zoom.Text = Loc.GetString("navmap-zoom", ("value", $"{(DefaultDisplayedRange / WorldRange ):0.0}"));
 
         if (_navMap == null || _xform == null)
             return;
 
-        var offset = _offset;
+        var offset = Offset;
 
         if (_physics != null)
             offset += _physics.LocalCenter;
@@ -317,7 +271,7 @@ public partial class NavMapControl : MapGridControl
                 {
                     var vert = poly.Vertices[i] - offset;
 
-                    verts[i] = Scale(new Vector2(vert.X, -vert.Y));
+                    verts[i] = ScalePosition(new Vector2(vert.X, -vert.Y));
                 }
 
                 handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts[..poly.VertexCount], TileColor);
@@ -348,8 +302,8 @@ public partial class NavMapControl : MapGridControl
 
                 foreach (var chunkedLine in chunkedLines)
                 {
-                    var start = Scale(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
-                    var end = Scale(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
+                    var start = ScalePosition(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
+                    var end = ScalePosition(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
 
                     walls.Add(start);
                     walls.Add(end);
@@ -375,7 +329,7 @@ public partial class NavMapControl : MapGridControl
         foreach (var airlock in _navMap.Airlocks)
         {
             var position = airlock.Position - offset;
-            position = Scale(position with { Y = -position.Y });
+            position = ScalePosition(position with { Y = -position.Y });
             airlockLines.Add(position + airlockBuffer);
             airlockLines.Add(position - airlockBuffer * foobarVec);
 
@@ -418,10 +372,10 @@ public partial class NavMapControl : MapGridControl
             foreach (var beacon in _navMap.Beacons)
             {
                 var position = beacon.Position - offset;
-                position = Scale(position with { Y = -position.Y });
+                position = ScalePosition(position with { Y = -position.Y });
 
                 var textDimensions = handle.GetDimensions(font, beacon.Text, 1f);
-                handle.DrawRect(new UIBox2(position - textDimensions / 2 - rectBuffer, position + textDimensions / 2 + rectBuffer), _backgroundColor);
+                handle.DrawRect(new UIBox2(position - textDimensions / 2 - rectBuffer, position + textDimensions / 2 + rectBuffer), BackgroundColor);
                 handle.DrawString(font, position - textDimensions / 2, beacon.Text, beacon.Color);
             }
         }
@@ -435,12 +389,12 @@ public partial class NavMapControl : MapGridControl
         {
             if (lit && value.Visible)
             {
-                var mapPos = coord.ToMap(_entManager, _transformSystem);
+                var mapPos = coord.ToMap(EntManager, _transformSystem);
 
                 if (mapPos.MapId != MapId.Nullspace)
                 {
                     var position = _transformSystem.GetInvWorldMatrix(_xform).Transform(mapPos.Position) - offset;
-                    position = Scale(new Vector2(position.X, -position.Y));
+                    position = ScalePosition(new Vector2(position.X, -position.Y));
 
                     handle.DrawCircle(position, float.Sqrt(MinimapScale) * 2f, value.Color);
                 }
@@ -461,12 +415,12 @@ public partial class NavMapControl : MapGridControl
             if (!iconVertexUVs.TryGetValue((blip.Texture, blip.Color), out var vertexUVs))
                 vertexUVs = new();
 
-            var mapPos = blip.Coordinates.ToMap(_entManager, _transformSystem);
+            var mapPos = blip.Coordinates.ToMap(EntManager, _transformSystem);
 
             if (mapPos.MapId != MapId.Nullspace)
             {
                 var position = _transformSystem.GetInvWorldMatrix(_xform).Transform(mapPos.Position) - offset;
-                position = Scale(new Vector2(position.X, -position.Y));
+                position = ScalePosition(new Vector2(position.X, -position.Y));
 
                 var scalingCoefficient = 2.5f;
                 var positionOffset = scalingCoefficient * float.Sqrt(MinimapScale);
@@ -628,14 +582,9 @@ public partial class NavMapControl : MapGridControl
         return decodedOutput;
     }
 
-    protected Vector2 Scale(Vector2 position)
-    {
-        return position * MinimapScale + MidpointVector;
-    }
-
     protected Vector2 GetOffset()
     {
-        return _offset + (_physics != null ? _physics.LocalCenter : new Vector2());
+        return Offset + (_physics?.LocalCenter ?? new Vector2());
     }
 }
 
index 9f537f38587496d8bc9a709d6f21bc8648ba4e4d..902d6bb7e609ebc6cf6dbd5b889e9fcaeeca0040 100644 (file)
@@ -33,7 +33,7 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
         // Set colors
         TileColor = new Color(30, 57, 67);
         WallColor = new Color(102, 164, 217);
-        _backgroundColor = Color.FromSrgb(TileColor.WithAlpha(_backgroundOpacity));
+        BackgroundColor = Color.FromSrgb(TileColor.WithAlpha(BackgroundOpacity));
 
         PostWallDrawingAction += DrawAllCableNetworks;
     }
@@ -93,8 +93,8 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
                     if (HiddenLineGroups.Contains(chunkedLine.Group))
                         continue;
 
-                    var start = Scale(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
-                    var end = Scale(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
+                    var start = ScalePosition(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
+                    var end = ScalePosition(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
 
                     cableNetworks[(int) chunkedLine.Group].Add(start);
                     cableNetworks[(int) chunkedLine.Group].Add(end);
@@ -139,22 +139,22 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
                     if (HiddenLineGroups.Contains(chunkedLine.Group))
                         continue;
 
-                    var leftTop = Scale(new Vector2
+                    var leftTop = ScalePosition(new Vector2
                         (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
                         Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
                         - new Vector2(offset.X, -offset.Y));
 
-                    var rightTop = Scale(new Vector2
+                    var rightTop = ScalePosition(new Vector2
                         (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
                         Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
                         - new Vector2(offset.X, -offset.Y));
 
-                    var leftBottom = Scale(new Vector2
+                    var leftBottom = ScalePosition(new Vector2
                         (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
                         Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
                         - new Vector2(offset.X, -offset.Y));
 
-                    var rightBottom = Scale(new Vector2
+                    var rightBottom = ScalePosition(new Vector2
                         (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
                         Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
                         - new Vector2(offset.X, -offset.Y));
index 41db4bb0b421218ee74fa1ae99c9deccc2fc14ec..4bd44a47a8e61c6b3ce036a69ca9beec4eb84ac4 100644 (file)
@@ -2,6 +2,7 @@ using Content.Client.Shuttles.UI;
 using Content.Shared.Shuttles.BUIStates;
 using JetBrains.Annotations;
 using Robust.Client.GameObjects;
+using RadarConsoleWindow = Content.Client.Shuttles.UI.RadarConsoleWindow;
 
 namespace Content.Client.Shuttles.BUI;
 
@@ -36,9 +37,9 @@ public sealed class RadarConsoleBoundUserInterface : BoundUserInterface
     protected override void UpdateState(BoundUserInterfaceState state)
     {
         base.UpdateState(state);
-        if (state is not RadarConsoleBoundInterfaceState cState) return;
+        if (state is not NavBoundUserInterfaceState cState)
+            return;
 
-        _window?.SetMatrix(EntMan.GetCoordinates(cState.Coordinates), cState.Angle);
-        _window?.UpdateState(cState);
+        _window?.UpdateState(cState.State);
     }
 }
index 1df8416ad7fc6d67e5fd2c60762a2b4dbc2e9b57..af7b6055c809c1e63f3dca56e18dac7742cedf0f 100644 (file)
@@ -2,7 +2,7 @@ using Content.Client.Shuttles.UI;
 using Content.Shared.Shuttles.BUIStates;
 using Content.Shared.Shuttles.Events;
 using JetBrains.Annotations;
-using Robust.Client.GameObjects;
+using Robust.Shared.Map;
 
 namespace Content.Client.Shuttles.BUI;
 
@@ -20,58 +20,66 @@ public sealed class ShuttleConsoleBoundUserInterface : BoundUserInterface
     {
         base.Open();
         _window = new ShuttleConsoleWindow();
-        _window.UndockPressed += OnUndockPressed;
-        _window.StartAutodockPressed += OnAutodockPressed;
-        _window.StopAutodockPressed += OnStopAutodockPressed;
-        _window.DestinationPressed += OnDestinationPressed;
         _window.OpenCentered();
-        _window.OnClose += OnClose;
+        _window.OnClose += Close;
+
+        _window.RequestFTL += OnFTLRequest;
+        _window.RequestBeaconFTL += OnFTLBeaconRequest;
+        _window.DockRequest += OnDockRequest;
+        _window.UndockRequest += OnUndockRequest;
     }
 
-    private void OnDestinationPressed(NetEntity obj)
+    private void OnUndockRequest(NetEntity entity)
     {
-        SendMessage(new ShuttleConsoleFTLRequestMessage()
+        SendMessage(new UndockRequestMessage()
         {
-            Destination = obj,
+            DockEntity = entity,
         });
     }
 
-    private void OnClose()
+    private void OnDockRequest(NetEntity entity, NetEntity target)
     {
-        Close();
+        SendMessage(new DockRequestMessage()
+        {
+            DockEntity = entity,
+            TargetDockEntity = target,
+        });
     }
 
-    protected override void Dispose(bool disposing)
+    private void OnFTLBeaconRequest(NetEntity ent, Angle angle)
     {
-        base.Dispose(disposing);
-
-        if (disposing)
+        SendMessage(new ShuttleConsoleFTLBeaconMessage()
         {
-            _window?.Dispose();
-        }
+            Beacon = ent,
+            Angle = angle,
+        });
     }
 
-    private void OnStopAutodockPressed(NetEntity obj)
+    private void OnFTLRequest(MapCoordinates obj, Angle angle)
     {
-        SendMessage(new StopAutodockRequestMessage() { DockEntity = obj });
+        SendMessage(new ShuttleConsoleFTLPositionMessage()
+        {
+            Coordinates = obj,
+            Angle = angle,
+        });
     }
 
-    private void OnAutodockPressed(NetEntity obj)
+    protected override void Dispose(bool disposing)
     {
-        SendMessage(new AutodockRequestMessage() { DockEntity = obj });
-    }
+        base.Dispose(disposing);
 
-    private void OnUndockPressed(NetEntity obj)
-    {
-        SendMessage(new UndockRequestMessage() { DockEntity = obj });
+        if (disposing)
+        {
+            _window?.Dispose();
+        }
     }
 
     protected override void UpdateState(BoundUserInterfaceState state)
     {
         base.UpdateState(state);
-        if (state is not ShuttleConsoleBoundInterfaceState cState) return;
+        if (state is not ShuttleBoundUserInterfaceState cState)
+            return;
 
-        _window?.SetMatrix(EntMan.GetCoordinates(cState.Coordinates), cState.Angle);
-        _window?.UpdateState(cState);
+        _window?.UpdateState(Owner, cState);
     }
 }
index 53b0dee848a1bbec762b74157538b497fa43c322..f2c297751b788a9013448b528c6dbfca028c8788 100644 (file)
@@ -1,7 +1,6 @@
 using Content.Shared.Shuttles.Components;
 
-namespace Content.Client.Shuttles
-{
-    [RegisterComponent]
-    public sealed partial class ShuttleConsoleComponent : SharedShuttleConsoleComponent {}
-}
+namespace Content.Client.Shuttles;
+
+[RegisterComponent]
+public sealed partial class ShuttleConsoleComponent : SharedShuttleConsoleComponent {}
index 143f0a971bda1349c8a83de48011ab432d8d0d64..d3261693a37c6c2f9fbd1719c53fde9af45ccf2b 100644 (file)
@@ -1,5 +1,7 @@
-using Content.Shared.Shuttles.Events;
+using Content.Shared.Shuttles.Systems;
 
 namespace Content.Client.Shuttles.Systems;
 
-public sealed class DockingSystem : EntitySystem {}
+public sealed class DockingSystem : SharedDockingSystem
+{
+}
diff --git a/Content.Client/Shuttles/Systems/ShuttleSystem.Console.cs b/Content.Client/Shuttles/Systems/ShuttleSystem.Console.cs
new file mode 100644 (file)
index 0000000..2d28867
--- /dev/null
@@ -0,0 +1,48 @@
+using Content.Client.Resources;
+using Content.Client.Shuttles.UI;
+using Content.Shared.Shuttles.Components;
+using Content.Shared.Shuttles.UI.MapObjects;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Shared.Map;
+using Robust.Shared.Physics.Components;
+
+namespace Content.Client.Shuttles.Systems;
+
+public sealed partial class ShuttleSystem
+{
+    [Dependency] private readonly IResourceCache _resource = default!;
+
+    /// <summary>
+    /// Gets the parallax to use for the specified map or uses the fallback if not available.
+    /// </summary>
+    public Texture GetTexture(Entity<ShuttleMapParallaxComponent?> entity)
+    {
+        if (!Resolve(entity, ref entity.Comp, false))
+        {
+            return _resource.GetTexture(ShuttleMapParallaxComponent.FallbackTexture);
+        }
+
+        return _resource.GetTexture(entity.Comp.TexturePath);
+    }
+
+    /// <summary>
+    /// Gets the map coordinates of a map object.
+    /// </summary>
+    public MapCoordinates GetMapCoordinates(IMapObject mapObj)
+    {
+        switch (mapObj)
+        {
+            case ShuttleBeaconObject beacon:
+                return GetCoordinates(beacon.Coordinates).ToMap(EntityManager, XformSystem);
+            case ShuttleExclusionObject exclusion:
+                return GetCoordinates(exclusion.Coordinates).ToMap(EntityManager, XformSystem);
+            case GridMapObject grid:
+                var gridXform = Transform(grid.Entity);
+                Entity<PhysicsComponent?, TransformComponent?> gridEnt = (grid.Entity, null, gridXform);
+                return new MapCoordinates(Maps.GetGridPosition(gridEnt), gridXform.MapID);
+            default:
+                throw new ArgumentOutOfRangeException();
+        }
+    }
+}
diff --git a/Content.Client/Shuttles/UI/BaseShuttleControl.xaml b/Content.Client/Shuttles/UI/BaseShuttleControl.xaml
new file mode 100644 (file)
index 0000000..c3c1802
--- /dev/null
@@ -0,0 +1,2 @@
+<controls1:MapGridControl
+    xmlns:controls1="clr-namespace:Content.Client.UserInterface.Controls" />
diff --git a/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs b/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs
new file mode 100644 (file)
index 0000000..035823a
--- /dev/null
@@ -0,0 +1,324 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Shuttles.Components;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics;
+using Robust.Shared.Threading;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+using Vector2 = System.Numerics.Vector2;
+
+namespace Content.Client.Shuttles.UI;
+
+/// <summary>
+/// Provides common functionality for radar-like displays on shuttle consoles.
+/// </summary>
+[GenerateTypedNameReferences]
+[Virtual]
+public partial class BaseShuttleControl : MapGridControl
+{
+    [Dependency] private readonly IParallelManager _parallel = default!;
+    protected readonly SharedMapSystem Maps;
+
+    protected readonly Font Font;
+
+    private GridDrawJob _drawJob;
+
+    // Cache grid drawing data as it can be expensive to build
+    public readonly Dictionary<EntityUid, GridDrawData> GridData = new();
+
+    // Per-draw caching
+    private readonly List<Vector2i> _gridTileList = new();
+    private readonly HashSet<Vector2i> _gridNeighborSet = new();
+    private readonly List<(Vector2 Start, Vector2 End)> _edges = new();
+
+    private Vector2[] _allVertices = Array.Empty<Vector2>();
+
+    private (DirectionFlag, Vector2i)[] _neighborDirections;
+
+    public BaseShuttleControl() : this(32f, 32f, 32f)
+    {
+    }
+
+    public BaseShuttleControl(float minRange, float maxRange, float range) : base(minRange, maxRange, range)
+    {
+        RobustXamlLoader.Load(this);
+        Maps = EntManager.System<SharedMapSystem>();
+        Font = new VectorFont(IoCManager.Resolve<IResourceCache>().GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 12);
+
+        _drawJob = new GridDrawJob()
+        {
+            ScaledVertices = _allVertices,
+        };
+
+        _neighborDirections = new (DirectionFlag, Vector2i)[4];
+
+        for (var i = 0; i < 4; i++)
+        {
+            var dir = (DirectionFlag) Math.Pow(2, i);
+            var dirVec = dir.AsDir().ToIntVec();
+            _neighborDirections[i] = (dir, dirVec);
+        }
+    }
+
+    protected void DrawData(DrawingHandleScreen handle, string text)
+    {
+        var coordsDimensions = handle.GetDimensions(Font, text, UIScale);
+        const float coordsMargins = 5f;
+
+        handle.DrawString(Font,
+            new Vector2(coordsMargins, Height) - new Vector2(0f, coordsDimensions.Y + coordsMargins),
+            text,
+            Color.FromSrgb(IFFComponent.SelfColor));
+    }
+
+    protected void DrawCircles(DrawingHandleScreen handle)
+    {
+        // Equatorial lines
+        var gridLines = Color.LightGray.WithAlpha(0.01f);
+
+        // Each circle is this x distance of the last one.
+        const float EquatorialMultiplier = 2f;
+
+        var minDistance = MathF.Pow(EquatorialMultiplier, EquatorialMultiplier * 1.5f);
+        var maxDistance = MathF.Pow(2f, EquatorialMultiplier * 6f);
+        var cornerDistance = MathF.Sqrt(WorldRange * WorldRange + WorldRange * WorldRange);
+
+        var origin = ScalePosition(-new Vector2(Offset.X, -Offset.Y));
+        var distOffset = -24f;
+
+        for (var radius = minDistance; radius <= maxDistance; radius *= EquatorialMultiplier)
+        {
+            if (radius > cornerDistance)
+                continue;
+
+            var color = Color.ToSrgb(gridLines).WithAlpha(0.05f);
+            var scaledRadius = MinimapScale * radius;
+            var text = $"{radius:0}m";
+            var textDimensions = handle.GetDimensions(Font, text, UIScale);
+
+            handle.DrawCircle(origin, scaledRadius, color, false);
+            handle.DrawString(Font, ScalePosition(new Vector2(0f, -radius)) - new Vector2(0f, textDimensions.Y), text, color);
+        }
+
+        const int gridLinesRadial = 8;
+
+        for (var i = 0; i < gridLinesRadial; i++)
+        {
+            Angle angle = (Math.PI / gridLinesRadial) * i;
+            // TODO: Handle distance properly.
+            var aExtent = angle.ToVec() * ScaledMinimapRadius * 1.42f;
+            var lineColor = Color.MediumSpringGreen.WithAlpha(0.02f);
+            handle.DrawLine(origin - aExtent, origin + aExtent, lineColor);
+        }
+    }
+
+    protected void DrawGrid(DrawingHandleScreen handle, Matrix3 matrix, Entity<MapGridComponent> grid, Color color, float alpha = 0.01f)
+    {
+        var rator = Maps.GetAllTilesEnumerator(grid.Owner, grid.Comp);
+        var minimapScale = MinimapScale;
+        var midpoint = new Vector2(MidPoint, MidPoint);
+        var tileSize = grid.Comp.TileSize;
+
+        // Check if we even have data
+        // TODO: Need to prune old grid-data if we don't draw it.
+        var gridData = GridData.GetOrNew(grid.Owner);
+
+        if (gridData.LastBuild < grid.Comp.LastTileModifiedTick)
+        {
+            gridData.Vertices.Clear();
+            _gridTileList.Clear();
+            _gridNeighborSet.Clear();
+
+            // Okay so there's 2 steps to this
+            // 1. Is that get we get a set of all tiles. This is used to decompose into triangle-strips
+            // 2. Is that we get a list of all tiles. This is used for edge data to decompose into line-strips.
+            while (rator.MoveNext(out var tileRef))
+            {
+                var index = tileRef.Value.GridIndices;
+                _gridNeighborSet.Add(index);
+                _gridTileList.Add(index);
+
+                var bl = Maps.TileToVector(grid, index);
+                var br = bl + new Vector2(tileSize, 0f);
+                var tr = bl + new Vector2(tileSize, tileSize);
+                var tl = bl + new Vector2(0f, tileSize);
+
+                gridData.Vertices.Add(bl);
+                gridData.Vertices.Add(br);
+                gridData.Vertices.Add(tl);
+
+                gridData.Vertices.Add(br);
+                gridData.Vertices.Add(tl);
+                gridData.Vertices.Add(tr);
+            }
+
+            gridData.EdgeIndex = gridData.Vertices.Count;
+            _edges.Clear();
+
+            foreach (var index in _gridTileList)
+            {
+                // We get all of the raw lines up front
+                // then we decompose them into longer lines in a separate step.
+                foreach (var (dir, dirVec) in _neighborDirections)
+                {
+                    var neighbor = index + dirVec;
+
+                    if (_gridNeighborSet.Contains(neighbor))
+                        continue;
+
+                    var bl = Maps.TileToVector(grid, index);
+                    var br = bl + new Vector2(tileSize, 0f);
+                    var tr = bl + new Vector2(tileSize, tileSize);
+                    var tl = bl + new Vector2(0f, tileSize);
+
+                    // Could probably rotate this but this might be faster?
+                    Vector2 actualStart;
+                    Vector2 actualEnd;
+
+                    switch (dir)
+                    {
+                        case DirectionFlag.South:
+                            actualStart = bl;
+                            actualEnd = br;
+                            break;
+                        case DirectionFlag.East:
+                            actualStart = br;
+                            actualEnd = tr;
+                            break;
+                        case DirectionFlag.North:
+                            actualStart = tr;
+                            actualEnd = tl;
+                            break;
+                        case DirectionFlag.West:
+                            actualStart = tl;
+                            actualEnd = bl;
+                            break;
+                        default:
+                            throw new NotImplementedException();
+                    }
+
+                    _edges.Add((actualStart, actualEnd));
+                }
+            }
+
+            // Decompose the edges into longer lines to save data.
+            // Now we decompose the lines into longer lines (less data to send to the GPU)
+            var decomposed = true;
+
+            while (decomposed)
+            {
+                decomposed = false;
+
+                for (var i = 0; i < _edges.Count; i++)
+                {
+                    var (start, end) = _edges[i];
+                    var neighborFound = false;
+                    var neighborIndex = 0;
+                    Vector2 neighborStart;
+                    Vector2 neighborEnd = Vector2.Zero;
+
+                    // Does our end correspond with another start?
+                    for (var j = i + 1; j < _edges.Count; j++)
+                    {
+                        (neighborStart, neighborEnd) = _edges[j];
+
+                        if (!end.Equals(neighborStart))
+                            continue;
+
+                        neighborFound = true;
+                        neighborIndex = j;
+                        break;
+                    }
+
+                    if (!neighborFound)
+                        continue;
+
+                    // Check if our start and the neighbor's end are collinear
+                    if (!CollinearSimplifier.IsCollinear(start, end, neighborEnd, 10f * float.Epsilon))
+                        continue;
+
+                    decomposed = true;
+                    _edges[i] = (start, neighborEnd);
+                    _edges.RemoveAt(neighborIndex);
+                }
+            }
+
+            gridData.Vertices.EnsureCapacity(_edges.Count * 2);
+
+            foreach (var edge in _edges)
+            {
+                gridData.Vertices.Add(edge.Start);
+                gridData.Vertices.Add(edge.End);
+            }
+
+            gridData.LastBuild = grid.Comp.LastTileModifiedTick;
+        }
+
+        var totalData = gridData.Vertices.Count;
+        var triCount = gridData.EdgeIndex;
+        var edgeCount = totalData - gridData.EdgeIndex;
+        Extensions.EnsureLength(ref _allVertices, totalData);
+
+        _drawJob.MidPoint = midpoint;
+        _drawJob.Matrix = matrix;
+        _drawJob.MinimapScale = minimapScale;
+        _drawJob.Vertices = gridData.Vertices;
+        _drawJob.ScaledVertices = _allVertices;
+
+        _parallel.ProcessNow(_drawJob, totalData);
+
+        const float BatchSize = 3f * 4096;
+
+        for (var i = 0; i < Math.Ceiling(triCount / BatchSize); i++)
+        {
+            var start = (int) (i * BatchSize);
+            var end = (int) Math.Min(triCount, start + BatchSize);
+            var count = end - start;
+            handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, new Span<Vector2>(_allVertices, start, count), color.WithAlpha(alpha));
+        }
+
+        handle.DrawPrimitives(DrawPrimitiveTopology.LineList, new Span<Vector2>(_allVertices, gridData.EdgeIndex, edgeCount), color);
+    }
+
+    private record struct GridDrawJob : IParallelRobustJob
+    {
+        public int BatchSize => 16;
+
+        public float MinimapScale;
+        public Vector2 MidPoint;
+        public Matrix3 Matrix;
+
+        public List<Vector2> Vertices;
+        public Vector2[] ScaledVertices;
+
+        public void Execute(int index)
+        {
+            var vert = Vertices[index];
+            var adjustedVert = Matrix.Transform(vert);
+            adjustedVert = adjustedVert with { Y = -adjustedVert.Y };
+
+            var scaledVert = ScalePosition(adjustedVert, MinimapScale, MidPoint);
+            ScaledVertices[index] = scaledVert;
+        }
+    }
+}
+
+public sealed class GridDrawData
+{
+    /*
+     * List of lists because we use LineStrip and TriangleStrip respectively (less data to pass to the GPU).
+     */
+
+    public List<Vector2> Vertices = new();
+
+    /// <summary>
+    /// Vertices index from when edges start.
+    /// </summary>
+    public int EdgeIndex;
+
+    public GameTick LastBuild;
+}
diff --git a/Content.Client/Shuttles/UI/DockObject.xaml b/Content.Client/Shuttles/UI/DockObject.xaml
new file mode 100644 (file)
index 0000000..bae6256
--- /dev/null
@@ -0,0 +1,11 @@
+<controls:PanelContainer HorizontalExpand="True"
+                         xmlns:controls="https://spacestation14.io"
+                         Margin="0 3">
+    <controls:BoxContainer Name="Contents" Orientation="Vertical"
+                           Margin="3">
+        <controls:Label Name="DockedLabel"
+                        HorizontalAlignment="Center"
+                        Margin="3 5"/>
+        <controls:BoxContainer Orientation="Vertical" Name="DockContainer"/>
+    </controls:BoxContainer>
+</controls:PanelContainer>
diff --git a/Content.Client/Shuttles/UI/DockObject.xaml.cs b/Content.Client/Shuttles/UI/DockObject.xaml.cs
new file mode 100644 (file)
index 0000000..9dae6b7
--- /dev/null
@@ -0,0 +1,61 @@
+using System.Text;
+using Content.Shared.Shuttles.BUIStates;
+using Content.Shared.Shuttles.Systems;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Map;
+
+namespace Content.Client.Shuttles.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class DockObject : PanelContainer
+{
+    public event Action? UndockPressed;
+    public event Action? ViewPressed;
+
+    public BoxContainer ContentsContainer => Contents;
+
+    public DockObject()
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+
+        PanelOverride = new StyleBoxFlat(new Color(30, 30, 34));
+    }
+
+    public void AddDock(DockingPortState state, ShuttleDockControl dockControl)
+    {
+        var viewButton = new Button()
+        {
+            Text = Loc.GetString("shuttle-console-view"),
+        };
+
+        viewButton.OnPressed += args =>
+        {
+            dockControl.SetViewedDock(state);
+        };
+
+        var container = new BoxContainer()
+        {
+            Orientation = BoxContainer.LayoutOrientation.Vertical,
+            Children =
+            {
+                new Label()
+                {
+                    Text = state.Name,
+                    HorizontalAlignment = HAlignment.Center,
+                },
+                viewButton
+            }
+        };
+
+        DockContainer.AddChild(container);
+    }
+
+    public void SetName(string value)
+    {
+        DockedLabel.Text = value;
+    }
+}
diff --git a/Content.Client/Shuttles/UI/DockingControl.cs b/Content.Client/Shuttles/UI/DockingControl.cs
deleted file mode 100644 (file)
index c0ddeff..0000000
+++ /dev/null
@@ -1,272 +0,0 @@
-using System.Numerics;
-using Content.Client.UserInterface.Controls;
-using Content.Shared.Shuttles.BUIStates;
-using Robust.Client.Graphics;
-using Robust.Client.UserInterface;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Physics;
-using Robust.Shared.Physics.Collision.Shapes;
-
-namespace Content.Client.Shuttles.UI;
-
-/// <summary>
-/// Displays the docking view from a specific docking port
-/// </summary>
-[Virtual]
-public class DockingControl : Control
-{
-    private readonly IEntityManager _entManager;
-    private readonly IMapManager _mapManager;
-
-    private float _range = 8f;
-    private float _rangeSquared = 0f;
-
-    private Vector2 RangeVector => new Vector2(_range, _range);
-
-    private const float GridLinesDistance = 32f;
-
-    private int MidPoint => SizeFull / 2;
-    private Vector2 MidPointVector => new Vector2(MidPoint, MidPoint);
-
-    private int SizeFull => (int) (MapGridControl.UIDisplayRadius * 2 * UIScale);
-    private int ScaledMinimapRadius => (int) (MapGridControl.UIDisplayRadius * UIScale);
-    private float MinimapScale => _range != 0 ? ScaledMinimapRadius / _range : 0f;
-
-    public NetEntity? ViewedDock;
-    public EntityUid? GridEntity;
-
-    public EntityCoordinates? Coordinates;
-    public Angle? Angle;
-
-    /// <summary>
-    /// Stored by GridID then by docks
-    /// </summary>
-    public Dictionary<NetEntity, List<DockingInterfaceState>> Docks = new();
-
-    private List<Entity<MapGridComponent>> _grids = new();
-
-    public DockingControl()
-    {
-        _entManager = IoCManager.Resolve<IEntityManager>();
-        _mapManager = IoCManager.Resolve<IMapManager>();
-        _rangeSquared = _range * _range;
-        MinSize = new Vector2(SizeFull, SizeFull);
-    }
-
-    protected override void Draw(DrawingHandleScreen handle)
-    {
-        base.Draw(handle);
-
-        var fakeAA = new Color(0.08f, 0.08f, 0.08f);
-
-        handle.DrawCircle(new Vector2(MidPoint, MidPoint), ScaledMinimapRadius + 1, fakeAA);
-        handle.DrawCircle(new Vector2(MidPoint, MidPoint), ScaledMinimapRadius, Color.Black);
-
-        var gridLines = new Color(0.08f, 0.08f, 0.08f);
-        var gridLinesRadial = 8;
-        var gridLinesEquatorial = (int) Math.Floor(_range / GridLinesDistance);
-
-        for (var i = 1; i < gridLinesEquatorial + 1; i++)
-        {
-            handle.DrawCircle(new Vector2(MidPoint, MidPoint), GridLinesDistance * MinimapScale * i, gridLines, false);
-        }
-
-        for (var i = 0; i < gridLinesRadial; i++)
-        {
-            Angle angle = (Math.PI / gridLinesRadial) * i;
-            var aExtent = angle.ToVec() * ScaledMinimapRadius;
-            handle.DrawLine(new Vector2(MidPoint, MidPoint) - aExtent, new Vector2(MidPoint, MidPoint) + aExtent, gridLines);
-        }
-
-        if (Coordinates == null ||
-            Angle == null ||
-            !_entManager.TryGetComponent<TransformComponent>(GridEntity, out var gridXform)) return;
-
-        var rotation = Matrix3.CreateRotation(-Angle.Value + Math.PI);
-        var matrix = Matrix3.CreateTranslation(-Coordinates.Value.Position);
-
-        // Draw the fixtures around the dock before drawing it
-        if (_entManager.TryGetComponent<FixturesComponent>(GridEntity, out var fixtures))
-        {
-            foreach (var fixture in fixtures.Fixtures.Values)
-            {
-                var poly = (PolygonShape) fixture.Shape;
-
-                for (var i = 0; i < poly.VertexCount; i++)
-                {
-                    var start = matrix.Transform(poly.Vertices[i]);
-                    var end = matrix.Transform(poly.Vertices[(i + 1) % poly.VertexCount]);
-
-                    var startOut = start.LengthSquared() > _rangeSquared;
-                    var endOut = end.LengthSquared() > _rangeSquared;
-
-                    // We need to draw to the radar border so we'll cap the range,
-                    // but if none of the verts are in range then just leave it.
-                    if (startOut && endOut)
-                        continue;
-
-                    start.Y = -start.Y;
-                    end.Y = -end.Y;
-
-                    // If start is outside we draw capped from end to start
-                    if (startOut)
-                    {
-                        // It's called Jobseeker now.
-                        if (!MathHelper.TryGetIntersecting(start, end, _range, out var newStart))
-                            continue;
-
-                        start = newStart.Value;
-                    }
-                    // otherwise vice versa
-                    else if (endOut)
-                    {
-                        if (!MathHelper.TryGetIntersecting(end, start, _range, out var newEnd))
-                            continue;
-
-                        end = newEnd.Value;
-                    }
-
-                    handle.DrawLine(ScalePosition(start), ScalePosition(end), Color.Goldenrod);
-                }
-            }
-        }
-
-        // Draw the dock's collision
-        handle.DrawRect(new UIBox2(
-            ScalePosition(rotation.Transform(new Vector2(-0.2f, -0.7f))),
-            ScalePosition(rotation.Transform(new Vector2(0.2f, -0.5f)))), Color.Aquamarine);
-
-        // Draw the dock itself
-        handle.DrawRect(new UIBox2(
-            ScalePosition(rotation.Transform(new Vector2(-0.5f, 0.5f))),
-            ScalePosition(rotation.Transform(new Vector2(0.5f, -0.5f)))), Color.Green);
-
-        // Draw nearby grids
-        var worldPos = gridXform.WorldMatrix.Transform(Coordinates.Value.Position);
-        var gridInvMatrix = gridXform.InvWorldMatrix;
-        Matrix3.Multiply(in gridInvMatrix, in matrix, out var invMatrix);
-
-        // TODO: Getting some overdraw so need to fix that.
-        var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
-
-        _grids.Clear();
-        _mapManager.FindGridsIntersecting(gridXform.MapID, new Box2(worldPos - RangeVector, worldPos + RangeVector), ref _grids);
-
-        foreach (var grid in _grids)
-        {
-            if (grid.Owner == GridEntity)
-                continue;
-
-            // Draw the fixtures before drawing any docks in range.
-            if (!_entManager.TryGetComponent<FixturesComponent>(grid, out var gridFixtures))
-                continue;
-
-            var gridMatrix = xformQuery.GetComponent(grid).WorldMatrix;
-
-            Matrix3.Multiply(in gridMatrix, in invMatrix, out var matty);
-
-            foreach (var (_, fixture) in gridFixtures.Fixtures)
-            {
-                var poly = (PolygonShape) fixture.Shape;
-
-                for (var i = 0; i < poly.VertexCount; i++)
-                {
-                    // This is because the same line might be on different fixtures so we don't want to draw it twice.
-                    var startPos = poly.Vertices[i];
-                    var endPos = poly.Vertices[(i + 1) % poly.VertexCount];
-
-                    var start = matty.Transform(startPos);
-                    var end = matty.Transform(endPos);
-
-                    var startOut = start.LengthSquared() > _rangeSquared;
-                    var endOut = end.LengthSquared() > _rangeSquared;
-
-                    // We need to draw to the radar border so we'll cap the range,
-                    // but if none of the verts are in range then just leave it.
-                    if (startOut && endOut)
-                        continue;
-
-                    start.Y = -start.Y;
-                    end.Y = -end.Y;
-
-                    // If start is outside we draw capped from end to start
-                    if (startOut)
-                    {
-                        // It's called Jobseeker now.
-                        if (!MathHelper.TryGetIntersecting(start, end, _range, out var newStart)) continue;
-                        start = newStart.Value;
-                    }
-                    // otherwise vice versa
-                    else if (endOut)
-                    {
-                        if (!MathHelper.TryGetIntersecting(end, start, _range, out var newEnd)) continue;
-                        end = newEnd.Value;
-                    }
-
-                    handle.DrawLine(ScalePosition(start), ScalePosition(end), Color.Aquamarine);
-                }
-            }
-
-            // Draw any docks on that grid
-            if (Docks.TryGetValue(_entManager.GetNetEntity(grid), out var gridDocks))
-            {
-                foreach (var dock in gridDocks)
-                {
-                    var position = matty.Transform(dock.Coordinates.Position);
-
-                    if (position.Length() > _range - 0.8f)
-                        continue;
-
-                    var otherDockRotation = Matrix3.CreateRotation(dock.Angle);
-
-                    // Draw the dock's collision
-                    var verts = new[]
-                    {
-                        matty.Transform(dock.Coordinates.Position +
-                                        otherDockRotation.Transform(new Vector2(-0.2f, -0.7f))),
-                        matty.Transform(dock.Coordinates.Position +
-                                        otherDockRotation.Transform(new Vector2(0.2f, -0.7f))),
-                        matty.Transform(dock.Coordinates.Position +
-                                        otherDockRotation.Transform(new Vector2(0.2f, -0.5f))),
-                        matty.Transform(dock.Coordinates.Position +
-                                        otherDockRotation.Transform(new Vector2(-0.2f, -0.5f))),
-                    };
-
-                    for (var i = 0; i < verts.Length; i++)
-                    {
-                        var vert = verts[i];
-                        vert.Y = -vert.Y;
-                        verts[i] = ScalePosition(vert);
-                    }
-
-                    handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, Color.Turquoise);
-
-                    // Draw the dock itself
-                    verts = new[]
-                    {
-                        matty.Transform(dock.Coordinates.Position + new Vector2(-0.5f, -0.5f)),
-                        matty.Transform(dock.Coordinates.Position + new Vector2(0.5f, -0.5f)),
-                        matty.Transform(dock.Coordinates.Position + new Vector2(0.5f, 0.5f)),
-                        matty.Transform(dock.Coordinates.Position + new Vector2(-0.5f, 0.5f)),
-                    };
-
-                    for (var i = 0; i < verts.Length; i++)
-                    {
-                        var vert = verts[i];
-                        vert.Y = -vert.Y;
-                        verts[i] = ScalePosition(vert);
-                    }
-
-                    handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, Color.Green);
-                }
-            }
-        }
-
-    }
-
-    private Vector2 ScalePosition(Vector2 value)
-    {
-        return value * MinimapScale + MidPointVector;
-    }
-}
diff --git a/Content.Client/Shuttles/UI/DockingScreen.xaml b/Content.Client/Shuttles/UI/DockingScreen.xaml
new file mode 100644 (file)
index 0000000..7c112b6
--- /dev/null
@@ -0,0 +1,28 @@
+<controls:BoxContainer Visible="False"
+               HorizontalExpand="True"
+               xmlns:controls="https://spacestation14.io"
+               xmlns:controls1="clr-namespace:Content.Client.UserInterface.Controls"
+               xmlns:ui="clr-namespace:Content.Client.Shuttles.UI">
+    <ui:ShuttleDockControl Name="DockingControl"
+                       MouseFilter="Stop"
+                       VerticalAlignment="Stretch"
+                       VerticalExpand="True"
+                       HorizontalExpand="True"
+                       Margin="5 4 10 5"/>
+    <controls:BoxContainer Name="RightDisplayDock"
+                           VerticalAlignment="Top"
+                           HorizontalAlignment="Right"
+                           MinWidth="256"
+                           MaxWidth="256"
+                           Align="Center"
+                           Margin="5 0 5 5"
+                           Orientation="Vertical">
+        <controls1:StripeBack MinSize="48 48">
+            <controls:Label Name="DockingPortsLabel" Text="{controls:Loc 'shuttle-console-docks-label'}" HorizontalAlignment="Center"/>
+        </controls1:StripeBack>
+        <controls:ScrollContainer VerticalExpand="True" HScrollEnabled="False"
+                                  ReturnMeasure="True">
+            <controls:BoxContainer Name="DockPorts" Orientation="Vertical"/>
+        </controls:ScrollContainer>
+    </controls:BoxContainer>
+</controls:BoxContainer>
diff --git a/Content.Client/Shuttles/UI/DockingScreen.xaml.cs b/Content.Client/Shuttles/UI/DockingScreen.xaml.cs
new file mode 100644 (file)
index 0000000..c0aa794
--- /dev/null
@@ -0,0 +1,183 @@
+using System.Linq;
+using System.Numerics;
+using System.Text;
+using Content.Shared.Shuttles.BUIStates;
+using Content.Shared.Shuttles.Systems;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Shuttles.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class DockingScreen : BoxContainer
+{
+    [Dependency] private readonly IEntityManager _entManager = default!;
+    private readonly SharedShuttleSystem _shuttles;
+
+    /// <summary>
+    /// Stored by GridID then by docks
+    /// </summary>
+    public Dictionary<NetEntity, List<DockingPortState>> Docks = new();
+
+    /// <summary>
+    /// Store the dock buttons for the side buttons.
+    /// </summary>
+    private readonly Dictionary<NetEntity, Button> _ourDockButtons = new();
+
+    public event Action<NetEntity, NetEntity>? DockRequest;
+    public event Action<NetEntity>? UndockRequest;
+
+    public DockingScreen()
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+        _shuttles = _entManager.System<SharedShuttleSystem>();
+
+        DockingControl.OnViewDock += OnView;
+        DockingControl.DockRequest += (entity, netEntity) =>
+        {
+            DockRequest?.Invoke(entity, netEntity);
+        };
+        DockingControl.UndockRequest += entity =>
+        {
+            UndockRequest?.Invoke(entity);
+        };
+    }
+
+    private void OnView(NetEntity obj)
+    {
+        if (_ourDockButtons.TryGetValue(obj, out var viewed))
+        {
+            viewed.Pressed = true;
+        }
+    }
+
+    public void UpdateState(EntityUid? shuttle, DockingInterfaceState state)
+    {
+        Docks = state.Docks;
+        DockingControl.DockState = state;
+        DockingControl.GridEntity = shuttle;
+        BuildDocks(shuttle);
+    }
+
+    private void BuildDocks(EntityUid? shuttle)
+    {
+        DockingControl.BuildDocks(shuttle);
+        var currentDock = DockingControl.ViewedDock;
+        // DockedWith.DisposeAllChildren();
+        DockPorts.DisposeAllChildren();
+        _ourDockButtons.Clear();
+
+        if (shuttle == null)
+        {
+            DockingControl.SetViewedDock(null);
+            return;
+        }
+
+        var shuttleNent = _entManager.GetNetEntity(shuttle.Value);
+
+        if (!Docks.TryGetValue(shuttleNent, out var shuttleDocks) || shuttleDocks.Count <= 0)
+            return;
+
+        var dockText = new StringBuilder();
+        var buttonGroup = new ButtonGroup();
+        var idx = 0;
+        var selected = false;
+
+        // Build the dock buttons for our docks.
+        foreach (var dock in shuttleDocks)
+        {
+            idx++;
+            dockText.Clear();
+            dockText.Append(dock.Name);
+
+            var button = new Button()
+            {
+                Text = dockText.ToString(),
+                ToggleMode = true,
+                Group = buttonGroup,
+                Margin = new Thickness(0f, 3f),
+            };
+
+            button.OnMouseEntered += args =>
+            {
+                DockingControl.HighlightedDock = dock.Entity;
+            };
+
+            button.OnMouseExited += args =>
+            {
+                DockingControl.HighlightedDock = null;
+            };
+
+            button.Label.Margin = new Thickness(3f);
+
+            if (currentDock == dock.Entity)
+            {
+                selected = true;
+                button.Pressed = true;
+            }
+
+            button.OnPressed += args =>
+            {
+                OnDockPress(dock);
+            };
+
+            _ourDockButtons[dock.Entity] = button;
+            DockPorts.AddChild(button);
+        }
+
+        // Button group needs one selected so just show the first one.
+        if (!selected)
+        {
+            var buttonOne = shuttleDocks[0];
+            OnDockPress(buttonOne);
+        }
+
+        var shuttleContainers = new Dictionary<NetEntity, DockObject>();
+
+        foreach (var dock in shuttleDocks.OrderBy(x => x.GridDockedWith))
+        {
+            if (dock.GridDockedWith == null)
+                continue;
+
+            DockObject? dockContainer;
+
+            if (!shuttleContainers.TryGetValue(dock.GridDockedWith.Value, out dockContainer))
+            {
+                dockContainer = new DockObject();
+                shuttleContainers[dock.GridDockedWith.Value] = dockContainer;
+                var dockGrid = _entManager.GetEntity(dock.GridDockedWith);
+                string? iffLabel = null;
+
+                if (_entManager.EntityExists(dockGrid))
+                {
+                    iffLabel = _shuttles.GetIFFLabel(dockGrid.Value);
+                }
+
+                iffLabel ??= Loc.GetString("shuttle-console-unknown");
+                dockContainer.SetName(iffLabel);
+                // DockedWith.AddChild(dockContainer);
+            }
+
+            dockContainer.AddDock(dock, DockingControl);
+
+            dockContainer.ViewPressed += () =>
+            {
+                OnDockPress(dock);
+            };
+
+            dockContainer.UndockPressed += () =>
+            {
+                UndockRequest?.Invoke(dock.Entity);
+            };
+        }
+    }
+
+    private void OnDockPress(DockingPortState state)
+    {
+        DockingControl.SetViewedDock(state);
+    }
+}
diff --git a/Content.Client/Shuttles/UI/MapScreen.xaml b/Content.Client/Shuttles/UI/MapScreen.xaml
new file mode 100644 (file)
index 0000000..7db61b9
--- /dev/null
@@ -0,0 +1,66 @@
+<controls:BoxContainer Visible="False"
+       HorizontalExpand="True"
+       xmlns:controls="https://spacestation14.io"
+       xmlns:controls1="clr-namespace:Content.Client.UserInterface.Controls"
+       xmlns:ui="clr-namespace:Content.Client.Shuttles.UI">
+            <ui:ShuttleMapControl Name="MapRadar"
+                             MouseFilter="Stop"
+                             Margin="5 4 10 5"
+                             HorizontalExpand="True"
+                             VerticalExpand="True"
+                             VerticalAlignment="Stretch"/>
+            <controls:BoxContainer Name="RightDisplayMap"
+                          VerticalAlignment="Top"
+                          HorizontalAlignment="Right"
+                          MinWidth="256"
+                          MaxWidth="256"
+                          Margin="5 0 5 5"
+                          Orientation="Vertical"
+                          VerticalExpand="True">
+                <controls1:StripeBack
+                    MinSize="48 48">
+                    <controls:Label Name="MapDisplayLabel" Text="{controls:Loc 'shuttle-console-ftl-label'}"
+                                    VerticalExpand="True"
+                                    HorizontalAlignment="Center"/>
+                </controls1:StripeBack>
+                <controls:Label Name="MapFTLState"
+                                Text="{controls:Loc 'shuttle-console-ftl-state-Available'}"
+                                VerticalAlignment="Stretch"
+                                HorizontalAlignment="Center"/>
+                <controls:ProgressBar Name="FTLBar" HorizontalExpand="True"
+                                      Margin="5"
+                                      MinValue="0.0"
+                                      MaxValue="1.0"
+                                      Value="1.0"/>
+                <controls:BoxContainer Orientation="Vertical">
+                <!-- Normal buttons -->
+                <controls1:StripeBack MinSize="48 48">
+                    <controls:Label Name="SettingsLabel" Text="{controls:Loc 'shuttle-console-map-settings'}"
+                           HorizontalAlignment="Center"/>
+                </controls1:StripeBack>
+                <controls:Button Name="MapBeaconsButton"
+                                 Text="{controls:Loc 'shuttle-console-map-beacons'}"
+                                 TextAlign="Center"
+                                 ToggleMode="True"
+                                 Pressed="True"/>
+                <controls:Button Name="MapFTLButton"
+                                 ToggleMode="True"
+                                 Text="{controls:Loc 'shuttle-console-ftl-button'}"
+                                 TextAlign="Center"/>
+                <controls:Button Name="MapRebuildButton"
+                        Text="{controls:Loc 'shuttle-console-map-rebuild'}"
+                        TextAlign="Center"/>
+                <!-- Map objects -->
+                <controls1:StripeBack MinSize="48 48">
+                    <controls:Label Name="HyperspaceLabel" Text="{controls:Loc 'shuttle-console-map-objects'}"
+                           HorizontalAlignment="Center"/>
+                </controls1:StripeBack>
+                <controls:ScrollContainer VerticalExpand="True" HScrollEnabled="False"
+                                 ReturnMeasure="True">
+                    <controls:BoxContainer Name="HyperspaceDestinations"
+                                  Orientation="Vertical"
+                                  VerticalExpand="True"/>
+                </controls:ScrollContainer>
+            </controls:BoxContainer>
+        </controls:BoxContainer>
+</controls:BoxContainer>
diff --git a/Content.Client/Shuttles/UI/MapScreen.xaml.cs b/Content.Client/Shuttles/UI/MapScreen.xaml.cs
new file mode 100644 (file)
index 0000000..8287093
--- /dev/null
@@ -0,0 +1,531 @@
+using System.Linq;
+using System.Numerics;
+using Content.Client.Shuttles.Systems;
+using Content.Shared.Shuttles.BUIStates;
+using Content.Shared.Shuttles.Components;
+using Content.Shared.Shuttles.Systems;
+using Content.Shared.Shuttles.UI.MapObjects;
+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.Audio;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Shuttles.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class MapScreen : BoxContainer
+{
+    [Dependency] private readonly IEntityManager _entManager = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly IMapManager _mapManager = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+    private readonly SharedAudioSystem _audio;
+    private readonly SharedMapSystem _maps;
+    private readonly ShuttleSystem _shuttles;
+    private readonly SharedTransformSystem _xformSystem;
+
+    private EntityUid? _console;
+    private EntityUid? _shuttleEntity;
+
+    private FTLState _state;
+    private float _ftlDuration;
+
+    private List<ShuttleBeaconObject> _beacons = new();
+    private List<ShuttleExclusionObject> _exclusions = new();
+
+    /// <summary>
+    /// When the next FTL state change happens.
+    /// </summary>
+    private TimeSpan _nextFtlTime;
+
+    private TimeSpan _nextPing;
+    private TimeSpan _pingCooldown = TimeSpan.FromSeconds(3);
+    private TimeSpan _nextMapDequeue;
+
+    private float _minMapDequeue = 0.05f;
+    private float _maxMapDequeue = 0.25f;
+
+    private StyleBoxFlat _ftlStyle;
+
+    public event Action<MapCoordinates, Angle>? RequestFTL;
+    public event Action<NetEntity, Angle>? RequestBeaconFTL;
+
+    private readonly Dictionary<MapId, BoxContainer> _mapHeadings = new();
+    private readonly Dictionary<MapId, List<IMapObject>> _mapObjects = new();
+    private readonly List<(MapId mapId, IMapObject mapobj)> _pendingMapObjects = new();
+
+    /// <summary>
+    /// Store the names of map object controls for re-sorting later.
+    /// </summary>
+    private Dictionary<Control, string> _mapObjectControls = new();
+
+    private List<Control> _sortChildren = new();
+
+    public MapScreen()
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+
+        _audio = _entManager.System<SharedAudioSystem>();
+        _maps = _entManager.System<SharedMapSystem>();
+        _shuttles = _entManager.System<ShuttleSystem>();
+        _xformSystem = _entManager.System<SharedTransformSystem>();
+
+        MapRebuildButton.OnPressed += MapRebuildPressed;
+
+        OnVisibilityChanged += OnVisChange;
+
+        MapFTLButton.OnToggled += FtlPreviewToggled;
+
+        _ftlStyle = new StyleBoxFlat(Color.LimeGreen);
+        FTLBar.ForegroundStyleBoxOverride = _ftlStyle;
+
+        // Just pass it on up.
+        MapRadar.RequestFTL += (coords, angle) =>
+        {
+            RequestFTL?.Invoke(coords, angle);
+        };
+
+        MapRadar.RequestBeaconFTL += (ent, angle) =>
+        {
+            RequestBeaconFTL?.Invoke(ent, angle);
+        };
+
+        MapBeaconsButton.OnToggled += args =>
+        {
+            MapRadar.ShowBeacons = args.Pressed;
+        };
+    }
+
+    public void UpdateState(ShuttleMapInterfaceState state)
+    {
+        // Only network the accumulator due to ping making the thing fonky.
+        // This should work better with predicting network states as they come in.
+        _beacons = state.Destinations;
+        _exclusions = state.Exclusions;
+        _state = state.FTLState;
+        _ftlDuration = state.FTLDuration;
+        _nextFtlTime = _timing.CurTime + TimeSpan.FromSeconds(_ftlDuration);
+        MapRadar.InFtl = true;
+        MapFTLState.Text = Loc.GetString($"shuttle-console-ftl-state-{_state.ToString()}");
+
+        switch (_state)
+        {
+            case FTLState.Available:
+                SetFTLAllowed(true);
+                _ftlStyle.BackgroundColor = Color.FromHex("#80C71F");
+                MapRadar.InFtl = false;
+                break;
+            case FTLState.Starting:
+                SetFTLAllowed(false);
+                _ftlStyle.BackgroundColor = Color.FromHex("#169C9C");
+                break;
+            case FTLState.Travelling:
+                SetFTLAllowed(false);
+                _ftlStyle.BackgroundColor = Color.FromHex("#8932B8");
+                break;
+            case FTLState.Arriving:
+                SetFTLAllowed(false);
+                _ftlStyle.BackgroundColor = Color.FromHex("#F9801D");
+                break;
+            case FTLState.Cooldown:
+                SetFTLAllowed(false);
+                // Scroll to the FTL spot
+                if (_entManager.TryGetComponent(_shuttleEntity, out TransformComponent? shuttleXform))
+                {
+                    var targetOffset = _maps.GetGridPosition(_shuttleEntity.Value);
+                    MapRadar.SetMap(shuttleXform.MapID, targetOffset, recentering: true);
+                }
+
+                _ftlStyle.BackgroundColor = Color.FromHex("#B02E26");
+                MapRadar.InFtl = false;
+                break;
+            default:
+                throw new NotImplementedException();
+        }
+
+        if (IsFTLBlocked())
+        {
+            MapRebuildButton.Disabled = true;
+            ClearMapObjects();
+        }
+    }
+
+    private void SetFTLAllowed(bool value)
+    {
+        if (value)
+        {
+            MapFTLButton.Disabled = false;
+        }
+        else
+        {
+            // Unselect FTL
+            MapFTLButton.Pressed = false;
+            MapRadar.FtlMode = false;
+            MapFTLButton.Disabled = true;
+        }
+    }
+
+    private void FtlPreviewToggled(BaseButton.ButtonToggledEventArgs obj)
+    {
+        MapRadar.FtlMode = obj.Pressed;
+    }
+
+    public void SetConsole(EntityUid? console)
+    {
+        _console = console;
+    }
+
+    public void SetShuttle(EntityUid? shuttle)
+    {
+        _shuttleEntity = shuttle;
+        MapRadar.SetShuttle(shuttle);
+    }
+
+    private void OnVisChange(Control obj)
+    {
+        if (!obj.Visible)
+            return;
+
+        // Centre map screen to the shuttle.
+        if (_shuttleEntity != null)
+        {
+            var mapPos = _xformSystem.GetMapCoordinates(_shuttleEntity.Value);
+            MapRadar.SetMap(mapPos.MapId, mapPos.Position);
+        }
+    }
+
+    /// <summary>
+    /// Does a sonar-like effect on the map.
+    /// </summary>
+    public void PingMap()
+    {
+        if (_console != null)
+        {
+            _audio.PlayEntity(new SoundPathSpecifier("/Audio/Effects/Shuttle/radar_ping.ogg"), Filter.Local(), _console.Value, true);
+        }
+
+        RebuildMapObjects();
+        BumpMapDequeue();
+
+        _nextPing = _timing.CurTime + _pingCooldown;
+        MapRebuildButton.Disabled = true;
+    }
+
+    private void BumpMapDequeue()
+    {
+        _nextMapDequeue = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(_minMapDequeue, _maxMapDequeue));
+    }
+
+    private void MapRebuildPressed(BaseButton.ButtonEventArgs obj)
+    {
+        PingMap();
+    }
+
+    /// <summary>
+    /// Clears all sector objects across all maps (e.g. if we start FTLing or need to re-ping).
+    /// </summary>
+    private void ClearMapObjects()
+    {
+        _mapObjectControls.Clear();
+        HyperspaceDestinations.DisposeAllChildren();
+        _pendingMapObjects.Clear();
+        _mapObjects.Clear();
+        _mapHeadings.Clear();
+    }
+
+    /// <summary>
+    /// Gets all map objects at time of ping and adds them to pending to be added over time.
+    /// </summary>
+    private void RebuildMapObjects()
+    {
+        ClearMapObjects();
+
+        if (_shuttleEntity == null)
+            return;
+
+        var mapComps = _entManager.AllEntityQueryEnumerator<MapComponent, TransformComponent, MetaDataComponent>();
+        MapId ourMap = MapId.Nullspace;
+
+        if (_entManager.TryGetComponent(_shuttleEntity, out TransformComponent? shuttleXform))
+        {
+            ourMap = shuttleXform.MapID;
+        }
+
+        while (mapComps.MoveNext(out var mapComp, out var mapXform, out var mapMetadata))
+        {
+            if (!_shuttles.CanFTLTo(_shuttleEntity.Value, mapComp.MapId))
+               continue;
+
+            var mapName = mapMetadata.EntityName;
+
+            if (string.IsNullOrEmpty(mapName))
+            {
+                mapName = Loc.GetString("shuttle-console-unknown");
+            }
+
+            var heading = new CollapsibleHeading(mapName);
+
+            heading.MinHeight = 32f;
+            heading.AddStyleClass(ContainerButton.StyleClassButton);
+            heading.HorizontalAlignment = HAlignment.Stretch;
+            heading.Label.HorizontalAlignment = HAlignment.Center;
+            heading.Label.HorizontalExpand = true;
+            heading.HorizontalExpand = true;
+
+            var gridContents = new BoxContainer()
+            {
+                Orientation = LayoutOrientation.Vertical,
+                VerticalExpand = true,
+            };
+
+            var body = new CollapsibleBody()
+            {
+                HorizontalAlignment = HAlignment.Stretch,
+                VerticalAlignment = VAlignment.Top,
+                HorizontalExpand = true,
+                Children =
+                {
+                    gridContents
+                }
+            };
+
+            var mapButton = new Collapsible(heading, body);
+
+            heading.OnToggled += args =>
+            {
+                if (args.Pressed)
+                {
+                    HideOtherCollapsibles(mapButton);
+                }
+            };
+
+            _mapHeadings.Add(mapComp.MapId, gridContents);
+
+            foreach (var grid in _mapManager.GetAllMapGrids(mapComp.MapId))
+            {
+                var gridObj = new GridMapObject()
+                {
+                    Name = _entManager.GetComponent<MetaDataComponent>(grid.Owner).EntityName,
+                    Entity = grid.Owner
+                };
+
+                // Always show our shuttle immediately
+                if (grid.Owner == _shuttleEntity)
+                {
+                    AddMapObject(mapComp.MapId, gridObj);
+                }
+                else
+                {
+                    _pendingMapObjects.Add((mapComp.MapId, gridObj));
+                }
+            }
+
+            foreach (var (beacon, _) in _shuttles.GetExclusions(mapComp.MapId, _exclusions))
+            {
+                _pendingMapObjects.Add((mapComp.MapId, beacon));
+            }
+
+            foreach (var (beacon, _) in _shuttles.GetBeacons(mapComp.MapId, _beacons))
+            {
+                _pendingMapObjects.Add((mapComp.MapId, beacon));
+            }
+
+            HyperspaceDestinations.AddChild(mapButton);
+
+            // Zoom in to our map
+            if (mapComp.MapId == MapRadar.ViewingMap)
+            {
+                mapButton.BodyVisible = true;
+            }
+        }
+
+        // Need to sort from furthest way to nearest (as we will pop from the end of the list first).
+        // Also prioritise those on our map first.
+        var shuttlePos = _xformSystem.GetWorldPosition(_shuttleEntity.Value);
+
+        _pendingMapObjects.Sort((x, y) =>
+        {
+            if (x.mapId == ourMap && y.mapId != ourMap)
+                return 1;
+
+            if (y.mapId == ourMap && x.mapId != ourMap)
+                return -1;
+
+            var yMapPos = _shuttles.GetMapCoordinates(y.mapobj);
+            var xMapPos = _shuttles.GetMapCoordinates(x.mapobj);
+
+            return (yMapPos.Position - shuttlePos).Length().CompareTo((xMapPos.Position - shuttlePos).Length());
+        });
+    }
+
+    /// <summary>
+    /// Hides other maps upon the specified collapsible being selected (AKA hacky collapsible groups).
+    /// </summary>
+    private void HideOtherCollapsibles(Collapsible collapsible)
+    {
+        foreach (var child in HyperspaceDestinations.Children)
+        {
+            if (child is not Collapsible childCollapse || childCollapse == collapsible)
+                continue;
+
+            childCollapse.BodyVisible = false;
+        }
+    }
+
+    /// <summary>
+    /// Returns true if we shouldn't be able to select the FTL button.
+    /// </summary>
+    private bool IsFTLBlocked()
+    {
+        switch (_state)
+        {
+            case FTLState.Available:
+                return false;
+            default:
+                return true;
+        }
+    }
+
+    private void OnMapObjectPress(IMapObject mapObject)
+    {
+        if (IsFTLBlocked())
+            return;
+
+        var coordinates = _shuttles.GetMapCoordinates(mapObject);
+
+        // If it's our map then scroll, otherwise just set position there.
+        MapRadar.SetMap(coordinates.MapId, coordinates.Position, recentering: true);
+    }
+
+    public void SetMap(MapId mapId, Vector2 position)
+    {
+        MapRadar.SetMap(mapId, position);
+        MapRadar.Offset = position;
+    }
+
+    /// <summary>
+    /// Adds a map object to the specified sector map.
+    /// </summary>
+    private void AddMapObject(MapId mapId, IMapObject mapObj)
+    {
+        var gridContents = _mapHeadings[mapId];
+        var existing = _mapObjects.GetOrNew(mapId);
+        existing.Add(mapObj);
+
+        var gridButton = new Button()
+        {
+            Text = mapObj.Name,
+            HorizontalExpand = true,
+        };
+
+        var gridContainer = new BoxContainer()
+        {
+            Children =
+            {
+                new Control()
+                {
+                    MinWidth = 32f,
+                },
+                gridButton
+            }
+        };
+
+        _mapObjectControls.Add(gridContainer, mapObj.Name);
+        gridContents.AddChild(gridContainer);
+
+        gridButton.OnPressed += args =>
+        {
+            OnMapObjectPress(mapObj);
+        };
+
+        if (gridContents.ChildCount > 1)
+        {
+            // Re-sort the children
+            _sortChildren.Clear();
+
+            foreach (var child in gridContents.Children)
+            {
+                DebugTools.Assert(_mapObjectControls.ContainsKey(child));
+                _sortChildren.Add(child);
+            }
+
+            foreach (var child in _sortChildren)
+            {
+                child.Orphan();
+            }
+
+            _sortChildren.Sort((x, y) =>
+            {
+                var xText = _mapObjectControls[x];
+                var yText = _mapObjectControls[y];
+
+                return string.Compare(xText, yText, StringComparison.CurrentCultureIgnoreCase);
+            });
+
+            foreach (var control in _sortChildren)
+            {
+                gridContents.AddChild(control);
+            }
+        }
+    }
+
+    protected override void FrameUpdate(FrameEventArgs args)
+    {
+        base.FrameUpdate(args);
+
+        var curTime = _timing.CurTime;
+
+        if (_nextMapDequeue < curTime && _pendingMapObjects.Count > 0)
+        {
+            var mapObj = _pendingMapObjects[^1];
+            _pendingMapObjects.RemoveAt(_pendingMapObjects.Count - 1);
+            AddMapObject(mapObj.mapId, mapObj.mapobj);
+            BumpMapDequeue();
+        }
+
+        if (!IsFTLBlocked() && _nextPing < curTime)
+        {
+            MapRebuildButton.Disabled = false;
+        }
+
+        var ftlDiff = (float) (_nextFtlTime - _timing.CurTime).TotalSeconds;
+
+        float ftlRatio;
+
+        if (_ftlDuration.Equals(0f))
+        {
+            ftlRatio = 1f;
+        }
+        else
+        {
+            ftlRatio = Math.Clamp(1f - (ftlDiff / _ftlDuration), 0f, 1f);
+        }
+
+        FTLBar.Value = ftlRatio;
+    }
+
+    protected override void Draw(DrawingHandleScreen handle)
+    {
+        MapRadar.SetMapObjects(_mapObjects);
+        base.Draw(handle);
+    }
+
+    public void Startup()
+    {
+        if (_entManager.TryGetComponent(_shuttleEntity, out TransformComponent? shuttleXform))
+        {
+            SetMap(shuttleXform.MapID, _maps.GetGridPosition((_shuttleEntity.Value, null, shuttleXform)));
+        }
+    }
+}
diff --git a/Content.Client/Shuttles/UI/NavScreen.xaml b/Content.Client/Shuttles/UI/NavScreen.xaml
new file mode 100644 (file)
index 0000000..c97aeda
--- /dev/null
@@ -0,0 +1,69 @@
+<controls:BoxContainer Visible="False"
+              HorizontalExpand="True"
+              xmlns:controls="https://spacestation14.io"
+              xmlns:ui="clr-namespace:Content.Client.Shuttles.UI"
+              xmlns:controls1="clr-namespace:Content.Client.UserInterface.Controls">
+                <ui:ShuttleNavControl Name="NavRadar"
+                                 MouseFilter="Stop"
+                                 VerticalAlignment="Stretch"
+                                 VerticalExpand="True"
+                                 HorizontalExpand="True"
+                                 Margin="5 4 10 5"/>
+                <!-- Nav controls -->
+                <controls:BoxContainer Name="RightDisplayNav"
+                      VerticalAlignment="Top"
+                      HorizontalAlignment="Right"
+                      VerticalExpand="True"
+                      MinWidth="256"
+                      MaxWidth="256"
+                      Margin="5 0 5 5"
+                      Orientation="Vertical">
+                    <controls1:StripeBack
+                        MinSize="48 48">
+                        <controls:Label Name="NavDisplayLabel" Text="{controls:Loc 'shuttle-console-display-label'}"
+                               VerticalExpand="True"
+                               HorizontalAlignment="Center"/>
+                    </controls1:StripeBack>
+                    <controls:GridContainer Columns="2"
+                                   HorizontalAlignment="Stretch"
+                                   VerticalAlignment="Top"
+                                   HorizontalExpand="True"
+                                   Margin="3"
+                                   Name="ReadonlyDisplay">
+                        <controls:Label Text="{controls:Loc 'shuttle-console-position'}"/>
+                        <controls:Label Name="GridPosition"
+                               Text="0.0, 0.0"
+                               HorizontalExpand="True"
+                               Align="Right"/>
+                        <controls:Label Text="{controls:Loc 'shuttle-console-orientation'}"/>
+                        <controls:Label Name="GridOrientation"
+                               Text="0.0"
+                               HorizontalExpand="True"
+                               Align="Right"/>
+                        <controls:Label Text="{controls:Loc 'shuttle-console-linear-velocity'}"/>
+                        <controls:Label Name="GridLinearVelocity"
+                               Text="0.0, 0.0"
+                               HorizontalExpand="True"
+                               Align="Right"/>
+                        <controls:Label Text="{controls:Loc 'shuttle-console-angular-velocity'}"/>
+                        <controls:Label Name="GridAngularVelocity"
+                               Text="0.0"
+                               HorizontalExpand="True"
+                               Align="Right"/>
+                    </controls:GridContainer>
+                    <controls1:StripeBack
+                        MinSize="48 48">
+                        <controls:Label Name="NavSettingsLabel" Text="{controls:Loc 'shuttle-console-nav-settings'}"
+                               VerticalExpand="True"
+                               HorizontalAlignment="Center"/>
+                    </controls1:StripeBack>
+                    <controls:Button Name="IFFToggle"
+                            Text="{controls:Loc 'shuttle-console-iff-toggle'}"
+                            TextAlign="Center"
+                            ToggleMode="True"/>
+                    <controls:Button Name="DockToggle"
+                            Text="{controls:Loc 'shuttle-console-dock-toggle'}"
+                            TextAlign="Center"
+                            ToggleMode="True"/>
+                </controls:BoxContainer>
+            </controls:BoxContainer>
diff --git a/Content.Client/Shuttles/UI/NavScreen.xaml.cs b/Content.Client/Shuttles/UI/NavScreen.xaml.cs
new file mode 100644 (file)
index 0000000..b7b757e
--- /dev/null
@@ -0,0 +1,85 @@
+using Content.Shared.Shuttles.BUIStates;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Map;
+using Robust.Shared.Physics.Components;
+
+namespace Content.Client.Shuttles.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class NavScreen : BoxContainer
+{
+    [Dependency] private readonly IEntityManager _entManager = default!;
+    private SharedTransformSystem _xformSystem;
+
+    private EntityUid? _shuttleEntity;
+
+    public NavScreen()
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+        _xformSystem = _entManager.System<SharedTransformSystem>();
+
+        IFFToggle.OnToggled += OnIFFTogglePressed;
+        IFFToggle.Pressed = NavRadar.ShowIFF;
+
+        DockToggle.OnToggled += OnDockTogglePressed;
+        DockToggle.Pressed = NavRadar.ShowDocks;
+    }
+
+    public void SetShuttle(EntityUid? shuttle)
+    {
+        _shuttleEntity = shuttle;
+    }
+
+    private void OnIFFTogglePressed(BaseButton.ButtonEventArgs args)
+    {
+        NavRadar.ShowIFF ^= true;
+        args.Button.Pressed = NavRadar.ShowIFF;
+    }
+
+    private void OnDockTogglePressed(BaseButton.ButtonEventArgs args)
+    {
+        NavRadar.ShowDocks ^= true;
+        args.Button.Pressed = NavRadar.ShowDocks;
+    }
+
+    public void UpdateState(NavInterfaceState scc)
+    {
+        NavRadar.UpdateState(scc);
+    }
+
+    public void SetMatrix(EntityCoordinates? coordinates, Angle? angle)
+    {
+        _shuttleEntity = coordinates?.EntityId;
+        NavRadar.SetMatrix(coordinates, angle);
+    }
+
+    protected override void Draw(DrawingHandleScreen handle)
+    {
+        base.Draw(handle);
+
+        if (!_entManager.TryGetComponent(_shuttleEntity, out TransformComponent? gridXform) ||
+            !_entManager.TryGetComponent(_shuttleEntity, out PhysicsComponent? gridBody))
+        {
+            return;
+        }
+
+        var (_, worldRot, worldMatrix) = _xformSystem.GetWorldPositionRotationMatrix(gridXform);
+        var worldPos = worldMatrix.Transform(gridBody.LocalCenter);
+
+        // Get the positive reduced angle.
+        var displayRot = -worldRot.Reduced();
+
+        GridPosition.Text = $"{worldPos.X:0.0}, {worldPos.Y:0.0}";
+        GridOrientation.Text = $"{displayRot.Degrees:0.0}";
+
+        var gridVelocity = gridBody.LinearVelocity;
+        gridVelocity = displayRot.RotateVec(gridVelocity);
+        // Get linear velocity relative to the console entity
+        GridLinearVelocity.Text = $"{gridVelocity.X + 10f * float.Epsilon:0.0}, {gridVelocity.Y + 10f * float.Epsilon:0.0}";
+        GridAngularVelocity.Text = $"{-gridBody.AngularVelocity + 10f * float.Epsilon:0.0}";
+    }
+}
index 26aca5da6294c77540319b7d0176d123d83de23d..f62e59b4ad1e6ea052bb8131b35ef49bde346812 100644 (file)
@@ -4,7 +4,7 @@
                       Title="{Loc 'radar-console-window-title'}"
                       SetSize="648 648"
                       MinSize="256 256">
-    <ui:RadarControl Name="RadarScreen"
+    <ui:ShuttleNavControl Name="RadarScreen"
                      Margin="4"
                      HorizontalExpand = "True"
                      VerticalExpand = "True"/>
index 1a6f216e8b3ff0aa1bb7550c87f2d78dc9b8b0cb..7f1149365b280fd88cefc3c1e8796c8bc1f64eab 100644 (file)
@@ -9,20 +9,15 @@ namespace Content.Client.Shuttles.UI;
 
 [GenerateTypedNameReferences]
 public sealed partial class RadarConsoleWindow : FancyWindow,
-    IComputerWindow<RadarConsoleBoundInterfaceState>
+    IComputerWindow<NavInterfaceState>
 {
     public RadarConsoleWindow()
     {
         RobustXamlLoader.Load(this);
     }
 
-    public void UpdateState(RadarConsoleBoundInterfaceState scc)
+    public void UpdateState(NavInterfaceState scc)
     {
         RadarScreen.UpdateState(scc);
     }
-
-    public void SetMatrix(EntityCoordinates? coordinates, Angle? angle)
-    {
-        RadarScreen.SetMatrix(coordinates, angle);
-    }
 }
diff --git a/Content.Client/Shuttles/UI/RadarControl.cs b/Content.Client/Shuttles/UI/RadarControl.cs
deleted file mode 100644 (file)
index 45e6da2..0000000
+++ /dev/null
@@ -1,433 +0,0 @@
-using System.Numerics;
-using Content.Client.UserInterface.Controls;
-using Content.Shared.Shuttles.BUIStates;
-using Content.Shared.Shuttles.Components;
-using JetBrains.Annotations;
-using Robust.Client.Graphics;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Shared.Collections;
-using Robust.Shared.Input;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Physics;
-using Robust.Shared.Physics.Components;
-using Robust.Shared.Utility;
-
-namespace Content.Client.Shuttles.UI;
-
-/// <summary>
-/// Displays nearby grids inside of a control.
-/// </summary>
-public sealed class RadarControl : MapGridControl
-{
-    [Dependency] private readonly IEntityManager _entManager = default!;
-    [Dependency] private readonly IMapManager _mapManager = default!;
-    private SharedTransformSystem _transform;
-
-    private const float GridLinesDistance = 32f;
-
-    /// <summary>
-    /// Used to transform all of the radar objects. Typically is a shuttle console parented to a grid.
-    /// </summary>
-    private EntityCoordinates? _coordinates;
-
-    private Angle? _rotation;
-
-    /// <summary>
-    /// Shows a label on each radar object.
-    /// </summary>
-    private Dictionary<EntityUid, Control> _iffControls = new();
-
-    private Dictionary<EntityUid, List<DockingInterfaceState>> _docks = new();
-
-    public bool ShowIFF { get; set; } = true;
-    public bool ShowDocks { get; set; } = true;
-
-    /// <summary>
-    /// Currently hovered docked to show on the map.
-    /// </summary>
-    public NetEntity? HighlightedDock;
-
-    /// <summary>
-    /// Raised if the user left-clicks on the radar control with the relevant entitycoordinates.
-    /// </summary>
-    public Action<EntityCoordinates>? OnRadarClick;
-
-    private List<Entity<MapGridComponent>> _grids = new();
-
-    public RadarControl() : base(64f, 256f, 256f)
-    {
-        _transform = _entManager.System<SharedTransformSystem>();
-    }
-
-    public void SetMatrix(EntityCoordinates? coordinates, Angle? angle)
-    {
-        _coordinates = coordinates;
-        _rotation = angle;
-    }
-
-    protected override void KeyBindUp(GUIBoundKeyEventArgs args)
-    {
-        base.KeyBindUp(args);
-
-        if (_coordinates == null || _rotation == null || args.Function != EngineKeyFunctions.UIClick ||
-            OnRadarClick == null)
-        {
-            return;
-        }
-
-        var a = InverseScalePosition(args.RelativePosition);
-        var relativeWorldPos = new Vector2(a.X, -a.Y);
-        relativeWorldPos = _rotation.Value.RotateVec(relativeWorldPos);
-        var coords = _coordinates.Value.Offset(relativeWorldPos);
-        OnRadarClick?.Invoke(coords);
-    }
-
-    /// <summary>
-    /// Gets the entitycoordinates of where the mouseposition is, relative to the control.
-    /// </summary>
-    [PublicAPI]
-    public EntityCoordinates GetMouseCoordinates(ScreenCoordinates screen)
-    {
-        if (_coordinates == null || _rotation == null)
-        {
-            return EntityCoordinates.Invalid;
-        }
-
-        var pos = screen.Position / UIScale - GlobalPosition;
-
-        var a = InverseScalePosition(pos);
-        var relativeWorldPos = new Vector2(a.X, -a.Y);
-        relativeWorldPos = _rotation.Value.RotateVec(relativeWorldPos);
-        var coords = _coordinates.Value.Offset(relativeWorldPos);
-        return coords;
-    }
-
-    public void UpdateState(RadarConsoleBoundInterfaceState ls)
-    {
-        WorldMaxRange = ls.MaxRange;
-
-        if (WorldMaxRange < WorldRange)
-        {
-            ActualRadarRange = WorldMaxRange;
-        }
-
-        if (WorldMaxRange < WorldMinRange)
-            WorldMinRange = WorldMaxRange;
-
-        ActualRadarRange = Math.Clamp(ActualRadarRange, WorldMinRange, WorldMaxRange);
-
-        _docks.Clear();
-
-        foreach (var state in ls.Docks)
-        {
-            var coordinates = state.Coordinates;
-            var grid = _docks.GetOrNew(_entManager.GetEntity(coordinates.NetEntity));
-            grid.Add(state);
-        }
-    }
-
-    protected override void Draw(DrawingHandleScreen handle)
-    {
-        base.Draw(handle);
-
-        var fakeAA = new Color(0.08f, 0.08f, 0.08f);
-
-        handle.DrawCircle(new Vector2(MidPoint, MidPoint), ScaledMinimapRadius + 1, fakeAA);
-        handle.DrawCircle(new Vector2(MidPoint, MidPoint), ScaledMinimapRadius, Color.Black);
-
-        // No data
-        if (_coordinates == null || _rotation == null)
-        {
-            Clear();
-            return;
-        }
-
-        var gridLines = new Color(0.08f, 0.08f, 0.08f);
-        var gridLinesRadial = 8;
-        var gridLinesEquatorial = (int) Math.Floor(WorldRange / GridLinesDistance);
-
-        for (var i = 1; i < gridLinesEquatorial + 1; i++)
-        {
-            handle.DrawCircle(new Vector2(MidPoint, MidPoint), GridLinesDistance * MinimapScale * i, gridLines, false);
-        }
-
-        for (var i = 0; i < gridLinesRadial; i++)
-        {
-            Angle angle = (Math.PI / gridLinesRadial) * i;
-            var aExtent = angle.ToVec() * ScaledMinimapRadius;
-            handle.DrawLine(new Vector2(MidPoint, MidPoint) - aExtent, new Vector2(MidPoint, MidPoint) + aExtent, gridLines);
-        }
-
-        var metaQuery = _entManager.GetEntityQuery<MetaDataComponent>();
-        var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
-        var fixturesQuery = _entManager.GetEntityQuery<FixturesComponent>();
-        var bodyQuery = _entManager.GetEntityQuery<PhysicsComponent>();
-
-        if (!xformQuery.TryGetComponent(_coordinates.Value.EntityId, out var xform)
-            || xform.MapID == MapId.Nullspace)
-        {
-            Clear();
-            return;
-        }
-
-        var (pos, rot) = _transform.GetWorldPositionRotation(xform);
-        var offset = _coordinates.Value.Position;
-        var offsetMatrix = Matrix3.CreateInverseTransform(pos, rot + _rotation.Value);
-
-        // Draw our grid in detail
-        var ourGridId = xform.GridUid;
-        if (_entManager.TryGetComponent<MapGridComponent>(ourGridId, out var ourGrid) &&
-            fixturesQuery.HasComponent(ourGridId.Value))
-        {
-            var ourGridMatrix = _transform.GetWorldMatrix(ourGridId.Value);
-            Matrix3.Multiply(in ourGridMatrix, in offsetMatrix, out var matrix);
-
-            DrawGrid(handle, matrix, ourGrid, Color.MediumSpringGreen, true);
-            DrawDocks(handle, ourGridId.Value, matrix);
-        }
-
-        var invertedPosition = _coordinates.Value.Position - offset;
-        invertedPosition.Y = -invertedPosition.Y;
-        // Don't need to transform the InvWorldMatrix again as it's already offset to its position.
-
-        // Draw radar position on the station
-        handle.DrawCircle(ScalePosition(invertedPosition), 5f, Color.Lime);
-
-        var shown = new HashSet<EntityUid>();
-
-        _grids.Clear();
-        _mapManager.FindGridsIntersecting(xform.MapID, new Box2(pos - MaxRadarRangeVector, pos + MaxRadarRangeVector), ref _grids, approx: true, includeMap: false);
-
-        // Draw other grids... differently
-        foreach (var grid in _grids)
-        {
-            var gUid = grid.Owner;
-            if (gUid == ourGridId || !fixturesQuery.HasComponent(gUid))
-                continue;
-
-            var gridBody = bodyQuery.GetComponent(gUid);
-            if (gridBody.Mass < 10f)
-            {
-                ClearLabel(gUid);
-                continue;
-            }
-
-            _entManager.TryGetComponent<IFFComponent>(gUid, out var iff);
-
-            // Hide it entirely.
-            if (iff != null &&
-                (iff.Flags & IFFFlags.Hide) != 0x0)
-            {
-                continue;
-            }
-
-            shown.Add(gUid);
-            var name = metaQuery.GetComponent(gUid).EntityName;
-
-            if (name == string.Empty)
-                name = Loc.GetString("shuttle-console-unknown");
-
-            var gridMatrix = _transform.GetWorldMatrix(gUid);
-            Matrix3.Multiply(in gridMatrix, in offsetMatrix, out var matty);
-            var color = iff?.Color ?? Color.Gold;
-
-            // Others default:
-            // Color.FromHex("#FFC000FF")
-            // Hostile default: Color.Firebrick
-
-            if (ShowIFF &&
-                (iff == null && IFFComponent.ShowIFFDefault ||
-                 (iff.Flags & IFFFlags.HideLabel) == 0x0))
-            {
-                var gridBounds = grid.Comp.LocalAABB;
-                Label label;
-
-                if (!_iffControls.TryGetValue(gUid, out var control))
-                {
-                    label = new Label()
-                    {
-                        HorizontalAlignment = HAlignment.Left,
-                    };
-
-                    _iffControls[gUid] = label;
-                    AddChild(label);
-                }
-                else
-                {
-                    label = (Label) control;
-                }
-
-                label.FontColorOverride = color;
-                var gridCentre = matty.Transform(gridBody.LocalCenter);
-                gridCentre.Y = -gridCentre.Y;
-                var distance = gridCentre.Length();
-
-                // y-offset the control to always render below the grid (vertically)
-                var yOffset = Math.Max(gridBounds.Height, gridBounds.Width) * MinimapScale / 1.8f / UIScale;
-
-                // The actual position in the UI. We offset the matrix position to render it off by half its width
-                // plus by the offset.
-                var uiPosition = ScalePosition(gridCentre) / UIScale - new Vector2(label.Width / 2f, -yOffset);
-
-                // Look this is uggo so feel free to cleanup. We just need to clamp the UI position to within the viewport.
-                uiPosition = new Vector2(Math.Clamp(uiPosition.X, 0f, Width - label.Width),
-                    Math.Clamp(uiPosition.Y, 10f, Height - label.Height));
-
-                label.Visible = true;
-                label.Text = Loc.GetString("shuttle-console-iff-label", ("name", name), ("distance", $"{distance:0.0}"));
-                LayoutContainer.SetPosition(label, uiPosition);
-            }
-            else
-            {
-                ClearLabel(gUid);
-            }
-
-            // Detailed view
-            DrawGrid(handle, matty, grid, color, true);
-
-            DrawDocks(handle, gUid, matty);
-        }
-
-        foreach (var (ent, _) in _iffControls)
-        {
-            if (shown.Contains(ent)) continue;
-            ClearLabel(ent);
-        }
-    }
-
-    private void Clear()
-    {
-        foreach (var (_, label) in _iffControls)
-        {
-            label.Dispose();
-        }
-
-        _iffControls.Clear();
-    }
-
-    private void ClearLabel(EntityUid uid)
-    {
-        if (!_iffControls.TryGetValue(uid, out var label)) return;
-        label.Dispose();
-        _iffControls.Remove(uid);
-    }
-
-    private void DrawDocks(DrawingHandleScreen handle, EntityUid uid, Matrix3 matrix)
-    {
-        if (!ShowDocks)
-            return;
-
-        const float DockScale = 1f;
-
-        if (_docks.TryGetValue(uid, out var docks))
-        {
-            foreach (var state in docks)
-            {
-                var position = state.Coordinates.Position;
-                var uiPosition = matrix.Transform(position);
-
-                if (uiPosition.Length() > WorldRange - DockScale)
-                    continue;
-
-                var color = HighlightedDock == state.Entity ? state.HighlightedColor : state.Color;
-
-                uiPosition.Y = -uiPosition.Y;
-
-                var verts = new[]
-                {
-                    matrix.Transform(position + new Vector2(-DockScale, -DockScale)),
-                    matrix.Transform(position + new Vector2(DockScale, -DockScale)),
-                    matrix.Transform(position + new Vector2(DockScale, DockScale)),
-                    matrix.Transform(position + new Vector2(-DockScale, DockScale)),
-                };
-
-                for (var i = 0; i < verts.Length; i++)
-                {
-                    var vert = verts[i];
-                    vert.Y = -vert.Y;
-                    verts[i] = ScalePosition(vert);
-                }
-
-                handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, color.WithAlpha(0.8f));
-                handle.DrawPrimitives(DrawPrimitiveTopology.LineStrip, verts, color);
-            }
-        }
-    }
-
-    private void DrawGrid(DrawingHandleScreen handle, Matrix3 matrix, MapGridComponent grid, Color color, bool drawInterior)
-    {
-        var rator = grid.GetAllTilesEnumerator();
-        var edges = new ValueList<Vector2>();
-
-        while (rator.MoveNext(out var tileRef))
-        {
-            // TODO: Short-circuit interior chunk nodes
-            // This can be optimised a lot more if required.
-            Vector2? tileVec = null;
-
-            // Iterate edges and see which we can draw
-            for (var i = 0; i < 4; i++)
-            {
-                var dir = (DirectionFlag) Math.Pow(2, i);
-                var dirVec = dir.AsDir().ToIntVec();
-
-                if (!grid.GetTileRef(tileRef.Value.GridIndices + dirVec).Tile.IsEmpty)
-                    continue;
-
-                Vector2 start;
-                Vector2 end;
-                tileVec ??= (Vector2) tileRef.Value.GridIndices * grid.TileSize;
-
-                // Draw line
-                // Could probably rotate this but this might be faster?
-                switch (dir)
-                {
-                    case DirectionFlag.South:
-                        start = tileVec.Value;
-                        end = tileVec.Value + new Vector2(grid.TileSize, 0f);
-                        break;
-                    case DirectionFlag.East:
-                        start = tileVec.Value + new Vector2(grid.TileSize, 0f);
-                        end = tileVec.Value + new Vector2(grid.TileSize, grid.TileSize);
-                        break;
-                    case DirectionFlag.North:
-                        start = tileVec.Value + new Vector2(grid.TileSize, grid.TileSize);
-                        end = tileVec.Value + new Vector2(0f, grid.TileSize);
-                        break;
-                    case DirectionFlag.West:
-                        start = tileVec.Value + new Vector2(0f, grid.TileSize);
-                        end = tileVec.Value;
-                        break;
-                    default:
-                        throw new NotImplementedException();
-                }
-
-                var adjustedStart = matrix.Transform(start);
-                var adjustedEnd = matrix.Transform(end);
-
-                if (adjustedStart.Length() > ActualRadarRange || adjustedEnd.Length() > ActualRadarRange)
-                    continue;
-
-                start = ScalePosition(new Vector2(adjustedStart.X, -adjustedStart.Y));
-                end = ScalePosition(new Vector2(adjustedEnd.X, -adjustedEnd.Y));
-
-                edges.Add(start);
-                edges.Add(end);
-            }
-        }
-
-        handle.DrawPrimitives(DrawPrimitiveTopology.LineList, edges.Span, color);
-    }
-
-    private Vector2 ScalePosition(Vector2 value)
-    {
-        return value * MinimapScale + MidpointVector;
-    }
-
-    private Vector2 InverseScalePosition(Vector2 value)
-    {
-        return (value - MidpointVector) / MinimapScale;
-    }
-}
index 324386326093dae852eca578ec391ce66153eefd..ec5340e6b47fa0c2750ffc52aa2b29aa47348e70 100644 (file)
                       xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
                       xmlns:ui="clr-namespace:Content.Client.Shuttles.UI"
                       Title="{Loc 'shuttle-console-window-title'}"
-                      SetSize="1180 648"
-                      MinSize="788 320">
-    <GridContainer Columns="3"
-                  HorizontalAlignment="Stretch"
-                  Margin="5 5 5 5">
-        <BoxContainer Name="LeftDisplay"
-                      VerticalAlignment="Top"
-                      HorizontalAlignment="Left"
-                      MinWidth="256"
-                      MaxWidth="256"
-                      Align="Center"
-                      Orientation="Vertical">
-            <BoxContainer Orientation="Vertical">
-                <controls:StripeBack>
-                    <Label Name="DockingPortsLabel" Text="{Loc 'shuttle-console-dock-label'}" HorizontalAlignment="Center"/>
-                </controls:StripeBack>
-                <BoxContainer Name="DockPorts"
-                              Orientation="Vertical"/>
-            </BoxContainer>
-            <BoxContainer Orientation="Vertical">
-                <controls:StripeBack>
-                    <Label Name="HyperspaceLabel" Text="{Loc 'shuttle-console-hyperspace-label'}" HorizontalAlignment="Center"/>
-                </controls:StripeBack>
-                <BoxContainer Name="HyperspaceDestinations"
-                              Orientation="Vertical"/>
-            </BoxContainer>
+                      SetSize="960 762"
+                      MinSize="960 762">
+    <BoxContainer Orientation="Vertical">
+        <!-- Top row mode buttons -->
+        <BoxContainer Name="ModeButtons"
+                       Orientation="Horizontal"
+                       HorizontalAlignment="Stretch"
+                       MinSize="52 52"
+                       Margin="5">
+            <Button Name="NavModeButton"
+                    ToggleMode="True"
+                    HorizontalExpand="True"
+                    VerticalExpand="True"
+                    Text="NAV"
+                    Margin="5"/>
+            <Button Name="MapModeButton"
+                    ToggleMode="True"
+                    HorizontalExpand="True"
+                    VerticalExpand="True"
+                    Text="MAP"
+                    Margin="5"/>
+            <Button Name="DockModeButton"
+                    ToggleMode="True"
+                    HorizontalExpand="True"
+                    VerticalExpand="True"
+                    Text="DOCK"
+                    Margin="5"/>
         </BoxContainer>
-        <PanelContainer MinSize="256 256"
-                        HorizontalAlignment = "Stretch"
-                        HorizontalExpand = "True"
-                        VerticalExpand = "True">
-            <ui:RadarControl Name="RadarScreen"
-                             MouseFilter="Stop"
-                             Margin="4"
-                             HorizontalExpand = "True"
-                             VerticalExpand = "True"/>
-            <ui:DockingControl Name="DockingScreen"
-                               Visible="False"
-                               MouseFilter="Stop"
-                               Margin="4"
-                               HorizontalExpand = "True"
-                               VerticalExpand = "True"/>
-        </PanelContainer>
-        <BoxContainer Name="RightDisplay"
-                      VerticalAlignment="Top"
-                      HorizontalAlignment="Right"
-                      MinWidth="256"
-                      MaxWidth="256"
-                      Align="Center"
-                      Orientation="Vertical">
-            <controls:StripeBack>
-                <Label Name="DisplayLabel" Text="{Loc 'shuttle-console-display-label'}" HorizontalAlignment="Center"/>
-            </controls:StripeBack>
-            <BoxContainer Name="ReadonlyDisplay">
-                <GridContainer Columns="2"
-                               HorizontalAlignment="Stretch"
-                               VerticalAlignment="Top">
-                    <Label Text="{Loc 'shuttle-console-ftl-state'}"/>
-                    <Label Name="FTLState"
-                           Text="{Loc 'ftl-shuttle-console-available'}"
-                           HorizontalAlignment="Right"/>
-                    <Label Text="{Loc 'shuttle-console-ftl-timer'}"/>
-                    <Label Name="FTLTimer"
-                           Text="0.0"
-                           HorizontalAlignment="Right"/>
-                    <Label Text="{Loc 'shuttle-console-max-radar'}"/>
-                    <Label Name="MaxRadarRange"
-                           Text="0.0"
-                           HorizontalAlignment="Right"/>
-                    <Label Text="{Loc 'shuttle-console-radar'}"/>
-                    <Label Name="RadarRange"
-                           Text="0.0"
-                           HorizontalAlignment="Right"/>
-                    <Label Text="{Loc 'shuttle-console-position'}"/>
-                    <Label Name="GridPosition"
-                           Text="0.0, 0.0"
-                           Align="Right"/>
-                    <Label Text="{Loc 'shuttle-console-orientation'}"/>
-                    <Label Name="GridOrientation"
-                           Text="0.0"
-                           Align="Right"/>
-                    <Label Text="{Loc 'shuttle-console-linear-velocity'}"/>
-                    <Label Name="GridLinearVelocity"
-                           Text="0.0, 0.0"
-                           Align="Right"/>
-                    <Label Text="{Loc 'shuttle-console-angular-velocity'}"/>
-                    <Label Name="GridAngularVelocity"
-                           Text="0.0"
-                           Align="Right"/>
-                </GridContainer>
-            </BoxContainer>
-            <Button Name="IFFToggle"
-                    Text="{Loc 'shuttle-console-iff-toggle'}"
-                    TextAlign="Center"
-                    ToggleMode="True"/>
-            <Button Name="DockToggle"
-                    Text="{Loc 'shuttle-console-dock-toggle'}"
-                    TextAlign="Center"
-                    ToggleMode="True"/>
-            <Button Name="UndockButton"
-                    Text="{Loc 'shuttle-console-undock'}"
-                    TextAlign="Center"
-                    Disabled="True"/>
+        <!-- Contents box -->
+        <BoxContainer Name="Contents"
+                      HorizontalAlignment="Stretch"
+                      VerticalExpand="True"
+                      Margin="5">
+            <ui:NavScreen Name="NavContainer" Visible="False"/>
+            <ui:MapScreen Name="MapContainer" Visible="False"/>
+            <ui:DockingScreen Name="DockContainer" Visible="False"/>
         </BoxContainer>
-    </GridContainer>
+    </BoxContainer>
 </controls:FancyWindow>
index d67227549a9071e5244be20ccf15ea81a9f372c4..a4b42fb672c75f479c1666a05bd58b6b8bed4c6f 100644 (file)
+using System.Numerics;
 using Content.Client.Computer;
 using Content.Client.UserInterface.Controls;
 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.Physics.Components;
-using Robust.Shared.Timing;
-using Robust.Shared.Utility;
 
 namespace Content.Client.Shuttles.UI;
 
 [GenerateTypedNameReferences]
 public sealed partial class ShuttleConsoleWindow : FancyWindow,
-    IComputerWindow<ShuttleConsoleBoundInterfaceState>
+    IComputerWindow<ShuttleBoundUserInterfaceState>
 {
-    private readonly IEntityManager _entManager;
-    private readonly IGameTiming _timing;
+    [Dependency] private readonly IEntityManager _entManager = default!;
 
-    private EntityUid? _shuttleEntity;
+    private ShuttleConsoleMode _mode = ShuttleConsoleMode.Nav;
 
-    /// <summary>
-    /// Currently selected dock button for camera.
-    /// </summary>
-    private BaseButton? _selectedDock;
+    public event Action<MapCoordinates, Angle>? RequestFTL;
+    public event Action<NetEntity, Angle>? RequestBeaconFTL;
 
-    /// <summary>
-    /// Stored by grid entityid then by states
-    /// </summary>
-    private readonly Dictionary<NetEntity, List<DockingInterfaceState>> _docks = new();
-
-    private readonly Dictionary<BaseButton, NetEntity> _destinations = new();
-
-    /// <summary>
-    /// Next FTL state change.
-    /// </summary>
-    public TimeSpan FTLTime;
-
-    public Action<NetEntity>? UndockPressed;
-    public Action<NetEntity>? StartAutodockPressed;
-    public Action<NetEntity>? StopAutodockPressed;
-    public Action<NetEntity>? DestinationPressed;
+    public event Action<NetEntity, NetEntity>? DockRequest;
+    public event Action<NetEntity>? UndockRequest;
 
     public ShuttleConsoleWindow()
     {
         RobustXamlLoader.Load(this);
-        _entManager = IoCManager.Resolve<IEntityManager>();
-        _timing = IoCManager.Resolve<IGameTiming>();
-
-        WorldRangeChange(RadarScreen.WorldRange);
-        RadarScreen.WorldRangeChanged += WorldRangeChange;
-
-        IFFToggle.OnToggled += OnIFFTogglePressed;
-        IFFToggle.Pressed = RadarScreen.ShowIFF;
-
-        DockToggle.OnToggled += OnDockTogglePressed;
-        DockToggle.Pressed = RadarScreen.ShowDocks;
-
-        UndockButton.OnPressed += OnUndockPressed;
-    }
-
-    private void WorldRangeChange(float value)
-    {
-        RadarRange.Text = $"{value:0}";
-    }
-
-    private void OnIFFTogglePressed(BaseButton.ButtonEventArgs args)
-    {
-        RadarScreen.ShowIFF ^= true;
-        args.Button.Pressed = RadarScreen.ShowIFF;
-    }
-
-    private void OnDockTogglePressed(BaseButton.ButtonEventArgs args)
-    {
-        RadarScreen.ShowDocks ^= true;
-        args.Button.Pressed = RadarScreen.ShowDocks;
-    }
+        IoCManager.InjectDependencies(this);
 
-    private void OnUndockPressed(BaseButton.ButtonEventArgs args)
-    {
-        if (DockingScreen.ViewedDock == null) return;
-        UndockPressed?.Invoke(DockingScreen.ViewedDock.Value);
-    }
+        // Mode switching
+        NavModeButton.OnPressed += NavPressed;
+        MapModeButton.OnPressed += MapPressed;
+        DockModeButton.OnPressed += DockPressed;
 
-    public void SetMatrix(EntityCoordinates? coordinates, Angle? angle)
-    {
-        _shuttleEntity = coordinates?.EntityId;
-        RadarScreen.SetMatrix(coordinates, angle);
-    }
+        // Modes are exclusive
+        var group = new ButtonGroup();
 
-    public void UpdateState(ShuttleConsoleBoundInterfaceState scc)
-    {
-        UpdateDocks(scc.Docks);
-        UpdateFTL(scc.Destinations, scc.FTLState, scc.FTLTime);
-        RadarScreen.UpdateState(scc);
-        MaxRadarRange.Text = $"{scc.MaxRange:0}";
-    }
+        NavModeButton.Group = group;
+        MapModeButton.Group = group;
+        DockModeButton.Group = group;
 
-    private void UpdateFTL(List<(NetEntity Entity, string Destination, bool Enabled)> destinations, FTLState state, TimeSpan time)
-    {
-        HyperspaceDestinations.DisposeAllChildren();
-        _destinations.Clear();
+        NavModeButton.Pressed = true;
+        SetupMode(_mode);
 
-        if (destinations.Count == 0)
-        {
-            HyperspaceDestinations.AddChild(new Label()
-            {
-                Text = Loc.GetString("shuttle-console-hyperspace-none"),
-                HorizontalAlignment = HAlignment.Center,
-            });
-        }
-        else
+        MapContainer.RequestFTL += (coords, angle) =>
         {
-            destinations.Sort((x, y) => string.Compare(x.Destination, y.Destination, StringComparison.Ordinal));
-
-            foreach (var destination in destinations)
-            {
-                var button = new Button()
-                {
-                    Disabled = !destination.Enabled,
-                    Text = destination.Destination,
-                };
-
-                _destinations[button] = destination.Entity;
-                button.OnPressed += OnHyperspacePressed;
-                HyperspaceDestinations.AddChild(button);
-            }
-        }
-
-        string stateText;
+            RequestFTL?.Invoke(coords, angle);
+        };
 
-        switch (state)
+        MapContainer.RequestBeaconFTL += (ent, angle) =>
         {
-            case Shared.Shuttles.Systems.FTLState.Available:
-                stateText = Loc.GetString("shuttle-console-ftl-available");
-                break;
-            case Shared.Shuttles.Systems.FTLState.Starting:
-                stateText = Loc.GetString("shuttle-console-ftl-starting");
-                break;
-            case Shared.Shuttles.Systems.FTLState.Travelling:
-                stateText = Loc.GetString("shuttle-console-ftl-travelling");
-                break;
-            case Shared.Shuttles.Systems.FTLState.Cooldown:
-                stateText = Loc.GetString("shuttle-console-ftl-cooldown");
-                break;
-            case Shared.Shuttles.Systems.FTLState.Arriving:
-                stateText = Loc.GetString("shuttle-console-ftl-arriving");
-                break;
-            default:
-                throw new ArgumentOutOfRangeException(nameof(state), state, null);
-        }
+            RequestBeaconFTL?.Invoke(ent, angle);
+        };
 
-        FTLState.Text = stateText;
-        // Add a buffer due to lag or whatever
-        time += TimeSpan.FromSeconds(0.3);
-        FTLTime = time;
-        FTLTimer.Text = GetFTLText();
-    }
-
-    private string GetFTLText()
-    {
-        return $"{Math.Max(0, (FTLTime - _timing.CurTime).TotalSeconds):0.0}";
-    }
+        DockContainer.DockRequest += (entity, netEntity) =>
+        {
+            DockRequest?.Invoke(entity, netEntity);
+        };
 
-    private void OnHyperspacePressed(BaseButton.ButtonEventArgs obj)
-    {
-        var ent = _destinations[obj.Button];
-        DestinationPressed?.Invoke(ent);
+        DockContainer.UndockRequest += entity =>
+        {
+            UndockRequest?.Invoke(entity);
+        };
     }
 
-    #region Docking
-
-    private void UpdateDocks(List<DockingInterfaceState> docks)
+    private void ClearModes(ShuttleConsoleMode mode)
     {
-        // TODO: We should check for changes so any existing highlighted doesn't delete.
-        // We also need to make up some pseudonumber as well for these.
-        _docks.Clear();
-
-        foreach (var dock in docks)
+        if (mode != ShuttleConsoleMode.Nav)
         {
-            var grid = _docks.GetOrNew(dock.Coordinates.NetEntity);
-            grid.Add(dock);
+            NavContainer.Visible = false;
         }
 
-        DockPorts.DisposeAllChildren();
-        DockingScreen.Docks = _docks;
-        var shuttleNetEntity = _entManager.GetNetEntity(_shuttleEntity);
-
-        if (shuttleNetEntity != null && _docks.TryGetValue(shuttleNetEntity.Value, out var gridDocks))
+        if (mode != ShuttleConsoleMode.Map)
         {
-            var index = 1;
-
-            foreach (var state in gridDocks)
-            {
-                var pressed = state.Entity == DockingScreen.ViewedDock;
-
-                string suffix;
-
-                if (state.Connected)
-                {
-                    suffix = Loc.GetString("shuttle-console-docked", ("index", index));
-                }
-                else
-                {
-                    suffix = $"{index}";
-                }
-
-                var button = new Button()
-                {
-                    Text = Loc.GetString("shuttle-console-dock-button", ("suffix", suffix)),
-                    ToggleMode = true,
-                    Pressed = pressed,
-                    Margin = new Thickness(0f, 1f),
-                };
-
-                if (pressed)
-                {
-                    _selectedDock = button;
-                }
+            MapContainer.Visible = false;
+            MapContainer.SetMap(MapId.Nullspace, Vector2.Zero);
+        }
 
-                button.OnMouseEntered += args => OnDockMouseEntered(args, state);
-                button.OnMouseExited += args => OnDockMouseExited(args, state);
-                button.OnToggled += args => OnDockToggled(args, state);
-                DockPorts.AddChild(button);
-                index++;
-            }
+        if (mode != ShuttleConsoleMode.Dock)
+        {
+            DockContainer.Visible = false;
         }
     }
 
-    private void OnDockMouseEntered(GUIMouseHoverEventArgs obj, DockingInterfaceState state)
+    private void NavPressed(BaseButton.ButtonEventArgs obj)
     {
-        RadarScreen.HighlightedDock = state.Entity;
+        SwitchMode(ShuttleConsoleMode.Nav);
     }
 
-    private void OnDockMouseExited(GUIMouseHoverEventArgs obj, DockingInterfaceState state)
+    private void MapPressed(BaseButton.ButtonEventArgs obj)
     {
-        RadarScreen.HighlightedDock = null;
+        SwitchMode(ShuttleConsoleMode.Map);
     }
 
-    /// <summary>
-    /// Shows a docking camera instead of radar screen.
-    /// </summary>
-    private void OnDockToggled(BaseButton.ButtonEventArgs obj, DockingInterfaceState state)
+    private void DockPressed(BaseButton.ButtonEventArgs obj)
     {
-        if (_selectedDock != null)
-        {
-            // If it got untoggled via other means then we'll stop viewing the old dock.
-            if (DockingScreen.ViewedDock != null && DockingScreen.ViewedDock != state.Entity)
-            {
-                StopAutodockPressed?.Invoke(DockingScreen.ViewedDock.Value);
-            }
-
-            _selectedDock.Pressed = false;
-            _selectedDock = null;
-        }
-
-        if (!obj.Button.Pressed)
-        {
-            if (DockingScreen.ViewedDock != null)
-            {
-                StopAutodockPressed?.Invoke(DockingScreen.ViewedDock.Value);
-                DockingScreen.ViewedDock = null;
-            }
-
-            UndockButton.Disabled = true;
-            DockingScreen.Visible = false;
-            RadarScreen.Visible = true;
-        }
-        else
-        {
-            if (_shuttleEntity != null)
-            {
-                DockingScreen.Coordinates = _entManager.GetCoordinates(state.Coordinates);
-                DockingScreen.Angle = state.Angle;
-            }
-            else
-            {
-                DockingScreen.Coordinates = null;
-                DockingScreen.Angle = null;
-            }
-
-            UndockButton.Disabled = false;
-            RadarScreen.Visible = false;
-            DockingScreen.Visible = true;
-            DockingScreen.ViewedDock = state.Entity;
-            StartAutodockPressed?.Invoke(state.Entity);
-            DockingScreen.GridEntity = _shuttleEntity;
-            _selectedDock = obj.Button;
-        }
+        SwitchMode(ShuttleConsoleMode.Dock);
     }
 
-    public override void Close()
+    private void SetupMode(ShuttleConsoleMode mode)
     {
-        base.Close();
-        if (DockingScreen.ViewedDock != null)
+        switch (mode)
         {
-            StopAutodockPressed?.Invoke(DockingScreen.ViewedDock.Value);
+            case ShuttleConsoleMode.Nav:
+                NavContainer.Visible = true;
+                break;
+            case ShuttleConsoleMode.Map:
+                MapContainer.Visible = true;
+                MapContainer.Startup();
+                break;
+            case ShuttleConsoleMode.Dock:
+                DockContainer.Visible = true;
+                break;
+            default:
+                throw new NotImplementedException();
         }
     }
 
-    #endregion
-
-    protected override void Draw(DrawingHandleScreen handle)
+    public void SwitchMode(ShuttleConsoleMode mode)
     {
-        base.Draw(handle);
-
-        if (!_entManager.TryGetComponent<PhysicsComponent>(_shuttleEntity, out var gridBody) ||
-            !_entManager.TryGetComponent<TransformComponent>(_shuttleEntity, out var gridXform))
-        {
+        if (_mode == mode)
             return;
-        }
-
-        if (_entManager.TryGetComponent<MetaDataComponent>(_shuttleEntity, out var metadata) && metadata.EntityPaused)
-        {
-            FTLTime += _timing.FrameTime;
-        }
 
-        FTLTimer.Text = GetFTLText();
-
-        var (_, worldRot, worldMatrix) = gridXform.GetWorldPositionRotationMatrix();
-        var worldPos = worldMatrix.Transform(gridBody.LocalCenter);
+        _mode = mode;
+        ClearModes(mode);
+        SetupMode(_mode);
+    }
 
-        // Get the positive reduced angle.
-        var displayRot = -worldRot.Reduced();
+    public enum ShuttleConsoleMode : byte
+    {
+        Nav,
+        Map,
+        Dock,
+    }
 
-        GridPosition.Text = $"{worldPos.X:0.0}, {worldPos.Y:0.0}";
-        GridOrientation.Text = $"{displayRot.Degrees:0.0}";
+    public void UpdateState(EntityUid owner, ShuttleBoundUserInterfaceState cState)
+    {
+        var coordinates = _entManager.GetCoordinates(cState.NavState.Coordinates);
+        NavContainer.SetShuttle(coordinates?.EntityId);
+        MapContainer.SetShuttle(coordinates?.EntityId);
+        MapContainer.SetConsole(owner);
 
-        var gridVelocity = gridBody.LinearVelocity;
-        gridVelocity = displayRot.RotateVec(gridVelocity);
-        // Get linear velocity relative to the console entity
-        GridLinearVelocity.Text = $"{gridVelocity.X:0.0}, {gridVelocity.Y:0.0}";
-        GridAngularVelocity.Text = $"{-gridBody.AngularVelocity:0.0}";
+        NavContainer.UpdateState(cState.NavState);
+        MapContainer.UpdateState(cState.MapState);
+        DockContainer.UpdateState(coordinates?.EntityId, cState.DockState);
     }
 }
diff --git a/Content.Client/Shuttles/UI/ShuttleDockControl.xaml b/Content.Client/Shuttles/UI/ShuttleDockControl.xaml
new file mode 100644 (file)
index 0000000..b1bbb4c
--- /dev/null
@@ -0,0 +1 @@
+<ui:BaseShuttleControl xmlns:ui="clr-namespace:Content.Client.Shuttles.UI"/>
diff --git a/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs
new file mode 100644 (file)
index 0000000..961ec35
--- /dev/null
@@ -0,0 +1,458 @@
+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<Entity<MapGridComponent>> _grids = new();
+
+    private readonly HashSet<DockingPortState> _drawnDocks = new();
+    private readonly Dictionary<DockingPortState, Button> _dockButtons = new();
+
+    /// <summary>
+    /// Store buttons for every other dock
+    /// </summary>
+    private readonly Dictionary<DockingPortState, Control> _dockContainers = new();
+
+    private static readonly TimeSpan DockChangeCooldown = TimeSpan.FromSeconds(0.5);
+
+    /// <summary>
+    /// Rate-limiting for docking changes
+    /// </summary>
+    private TimeSpan _nextDockChange;
+
+    public event Action<NetEntity>? OnViewDock;
+    public event Action<NetEntity, NetEntity>? DockRequest;
+    public event Action<NetEntity>? UndockRequest;
+
+    public ShuttleDockControl() : base(2f, 32f, 8f)
+    {
+        RobustXamlLoader.Load(this);
+        _dockSystem = EntManager.System<DockingSystem>();
+        _shuttles = EntManager.System<SharedShuttleSystem>();
+        _xformSystem = EntManager.System<SharedTransformSystem>();
+        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<TransformComponent>(GridEntity, out var gridXform))
+        {
+            DrawNoSignal(handle);
+            return;
+        }
+
+        DrawCircles(handle);
+        var gridNent = EntManager.GetNetEntity(GridEntity);
+        var mapPos = _xformSystem.ToMapCoordinates(_coordinates.Value);
+        var ourGridMatrix = _xformSystem.GetWorldMatrix(gridXform.Owner);
+        var dockMatrix = Matrix3.CreateTransform(_coordinates.Value.Position, Angle.Zero);
+        Matrix3.Multiply(dockMatrix, ourGridMatrix, out var offsetMatrix);
+
+        offsetMatrix = offsetMatrix.Invert();
+
+        // Draw nearby grids
+        var controlBounds = SizeBox.Scale(1.25f);
+        _grids.Clear();
+        _mapManager.FindGridsIntersecting(gridXform.MapID, new Box2(mapPos.Position - WorldRangeVector, mapPos.Position + WorldRangeVector), 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 gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner);
+            Matrix3.Multiply(in gridMatrix, in offsetMatrix, out var matty);
+            var color = _shuttles.GetIFFColor(grid.Owner, grid.Owner == GridEntity, component: iffComp);
+
+            DrawGrid(handle, matty, 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 position = matty.Transform(dock.Coordinates.Position);
+
+                var otherDockRotation = Matrix3.CreateRotation(dock.Angle);
+                var scaledPos = ScalePosition(position with {Y = -position.Y});
+
+                if (!controlBounds.Contains(scaledPos.Floored()))
+                    continue;
+
+                // Draw the dock's collision
+                var collisionBL = matty.Transform(dock.Coordinates.Position +
+                                                  otherDockRotation.Transform(new Vector2(-0.2f, -0.7f)));
+                var collisionBR = matty.Transform(dock.Coordinates.Position +
+                                                  otherDockRotation.Transform(new Vector2(0.2f, -0.7f)));
+                var collisionTR = matty.Transform(dock.Coordinates.Position +
+                                                  otherDockRotation.Transform(new Vector2(0.2f, -0.5f)));
+                var collisionTL = matty.Transform(dock.Coordinates.Position +
+                                                  otherDockRotation.Transform(new Vector2(-0.2f, -0.5f)));
+
+                var verts = new[]
+                {
+                    collisionBL,
+                    collisionBR,
+                    collisionBR,
+                    collisionTR,
+                    collisionTR,
+                    collisionTL,
+                    collisionTL,
+                    collisionBL,
+                };
+
+                for (var i = 0; i < verts.Length; i++)
+                {
+                    var vert = verts[i];
+                    vert.Y = -vert.Y;
+                    verts[i] = ScalePosition(vert);
+                }
+
+                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 = matty.Transform(dock.Coordinates.Position + new Vector2(-0.5f, -0.5f));
+                var dockBR = matty.Transform(dock.Coordinates.Position + new Vector2(0.5f, -0.5f));
+                var dockTR = matty.Transform(dock.Coordinates.Position + new Vector2(0.5f, 0.5f));
+                var dockTL = matty.Transform(dock.Coordinates.Position + new Vector2(-0.5f, 0.5f));
+
+                verts = new[]
+                {
+                    dockBL,
+                    dockBR,
+                    dockBR,
+                    dockTR,
+                    dockTR,
+                    dockTL,
+                    dockTL,
+                    dockBL
+                };
+
+                for (var i = 0; i < verts.Length; i++)
+                {
+                    var vert = verts[i];
+                    vert.Y = -vert.Y;
+                    verts[i] = ScalePosition(vert);
+                }
+
+                Color otherDockColor;
+
+                if (HighlightedDock == dock.Entity)
+                {
+                    otherDockColor = Color.ToSrgb(Color.Magenta);
+                }
+                else
+                {
+                    otherDockColor = Color.ToSrgb(Color.Purple);
+                }
+
+                /*
+                 * 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;
+
+                    if (range.Length() < SharedDockingSystem.DockingHiglightRange * MinimapScale)
+                    {
+                        if (_viewedState?.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 distance = (mapOne.Position - mapTwo.Position).Length();
+
+                            var inAlignment = _dockSystem.InAlignment(mapOne, rotA, mapTwo, rotB);
+                            var canDock = distance < SharedDockingSystem.DockRange && 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 containerPos = scaledPos - 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 = Matrix3.CreateRotation(-_angle.Value + MathF.PI);
+        var ourDockConnection = new UIBox2(
+            ScalePosition(rotation.Transform(new Vector2(-0.2f, -0.7f))),
+            ScalePosition(rotation.Transform(new Vector2(0.2f, -0.5f))));
+
+        var ourDock = new UIBox2(
+            ScalePosition(rotation.Transform(new Vector2(-0.5f, 0.5f))),
+            ScalePosition(rotation.Transform(new Vector2(0.5f, -0.5f))));
+
+        var dockColor = Color.Magenta;
+        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;
+            }
+        }
+    }
+}
diff --git a/Content.Client/Shuttles/UI/ShuttleMapControl.xaml b/Content.Client/Shuttles/UI/ShuttleMapControl.xaml
new file mode 100644 (file)
index 0000000..18abb9c
--- /dev/null
@@ -0,0 +1 @@
+<ui:ShuttleMapControl xmlns:ui="clr-namespace:Content.Client.Shuttles.UI" />
diff --git a/Content.Client/Shuttles/UI/ShuttleMapControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleMapControl.xaml.cs
new file mode 100644 (file)
index 0000000..5800af2
--- /dev/null
@@ -0,0 +1,609 @@
+using System.Buffers;
+using System.Numerics;
+using Content.Client.Shuttles.Systems;
+using Content.Shared.Shuttles.Components;
+using Content.Shared.Shuttles.UI.MapObjects;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Collections;
+using Robust.Shared.Input;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Shuttles.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class ShuttleMapControl : BaseShuttleControl
+{
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly IInputManager _inputs = default!;
+    [Dependency] private readonly IMapManager _mapManager = default!;
+    private readonly ShuttleSystem _shuttles;
+    private readonly SharedTransformSystem _xformSystem;
+
+    protected override bool Draggable => true;
+
+    public bool ShowBeacons = true;
+    public MapId ViewingMap = MapId.Nullspace;
+
+    private EntityUid? _shuttleEntity;
+
+    private readonly Font _font;
+
+    private readonly EntityQuery<PhysicsComponent> _physicsQuery;
+
+    /// <summary>
+    /// Toggles FTL mode on. This shows a pre-vis for FTLing a grid.
+    /// </summary>
+    public bool FtlMode;
+
+    private Angle _ftlAngle;
+
+    /// <summary>
+    /// Are we currently in FTL.
+    /// </summary>
+    public bool InFtl;
+
+    /// <summary>
+    /// Raised when a request to FTL to a particular spot is raised.
+    /// </summary>
+    public event Action<MapCoordinates, Angle>? RequestFTL;
+
+    public event Action<NetEntity, Angle>? RequestBeaconFTL;
+
+    /// <summary>
+    /// Set every draw to determine the beacons that are clickable for mouse events
+    /// </summary>
+    private List<IMapObject> _beacons = new();
+
+    // Per frame data to avoid re-allocating
+    private readonly List<IMapObject> _mapObjects = new();
+    private readonly Dictionary<Color, List<Vector2>> _verts = new();
+    private readonly Dictionary<Color, List<Vector2>> _edges = new();
+    private readonly Dictionary<Color, List<(Vector2, string)>> _strings = new();
+    private readonly List<ShuttleExclusionObject> _viewportExclusions = new();
+
+    public ShuttleMapControl() : base(256f, 512f, 512f)
+    {
+        RobustXamlLoader.Load(this);
+        _shuttles = EntManager.System<ShuttleSystem>();
+        _xformSystem = EntManager.System<SharedTransformSystem>();
+        var cache = IoCManager.Resolve<IResourceCache>();
+
+        _physicsQuery = EntManager.GetEntityQuery<PhysicsComponent>();
+
+        _font = new VectorFont(cache.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 10);
+    }
+
+    public void SetMap(MapId mapId, Vector2 offset, bool recentering = false)
+    {
+        ViewingMap = mapId;
+        TargetOffset = offset;
+        Recentering = recentering;
+    }
+
+    public void SetShuttle(EntityUid? entity)
+    {
+        _shuttleEntity = entity;
+    }
+
+    protected override void MouseMove(GUIMouseMoveEventArgs args)
+    {
+        // No move for you.
+        if (FtlMode)
+            return;
+
+        base.MouseMove(args);
+    }
+
+    protected override void KeyBindUp(GUIBoundKeyEventArgs args)
+    {
+        if (FtlMode && ViewingMap != MapId.Nullspace)
+        {
+            if (args.Function == EngineKeyFunctions.UIClick)
+            {
+                var mapUid = _mapManager.GetMapEntityId(ViewingMap);
+
+                var beaconsOnly = EntManager.TryGetComponent(mapUid, out FTLDestinationComponent? destComp) &&
+                                  destComp.BeaconsOnly;
+
+                var mapTransform = Matrix3.CreateInverseTransform(Offset, Angle.Zero);
+
+                if (beaconsOnly && TryGetBeacon(_beacons, mapTransform, args.RelativePosition, PixelRect, out var foundBeacon, out _))
+                {
+                    RequestBeaconFTL?.Invoke(foundBeacon.Entity, _ftlAngle);
+                }
+                else
+                {
+                    // We'll send the "adjusted" position and server will adjust it back when relevant.
+                    var mapCoords = new MapCoordinates(InverseMapPosition(args.RelativePosition), ViewingMap);
+                    RequestFTL?.Invoke(mapCoords, _ftlAngle);
+                }
+            }
+        }
+
+        base.KeyBindUp(args);
+    }
+
+    protected override void MouseWheel(GUIMouseWheelEventArgs args)
+    {
+        // Scroll handles FTL rotation if you're in FTL mode.
+        if (FtlMode)
+        {
+            _ftlAngle += Angle.FromDegrees(15f) * args.Delta.Y;
+            _ftlAngle = _ftlAngle.Reduced();
+            return;
+        }
+
+        base.MouseWheel(args);
+    }
+
+    private void DrawParallax(DrawingHandleScreen handle)
+    {
+        if (!EntManager.TryGetComponent(_shuttleEntity, out TransformComponent? shuttleXform) || shuttleXform.MapUid == null)
+            return;
+
+        // TODO: Figure out how the fuck to make this common between the 3 slightly different parallax methods and move to parallaxsystem.
+        // Draw background texture
+        var tex = _shuttles.GetTexture(shuttleXform.MapUid.Value);
+
+        // Size of the texture in world units.
+        var size = tex.Size * MinimapScale * 1f;
+
+        var position = ScalePosition(new Vector2(-Offset.X, Offset.Y));
+        var slowness = 1f;
+
+        // The "home" position is the effective origin of this layer.
+        // Parallax shifting is relative to the home, and shifts away from the home and towards the Eye centre.
+        // The effects of this are such that a slowness of 1 anchors the layer to the centre of the screen, while a slowness of 0 anchors the layer to the world.
+        // (For values 0.0 to 1.0 this is in effect a lerp, but it's deliberately unclamped.)
+        // The ParallaxAnchor adapts the parallax for station positioning and possibly map-specific tweaks.
+        var home = Vector2.Zero;
+        var scrolled = Vector2.Zero;
+
+        // Origin - start with the parallax shift itself.
+        var originBL = (position - home) * slowness + scrolled;
+
+        // Place at the home.
+        originBL += home;
+
+        // Centre the image.
+        originBL -= size / 2;
+
+        // Remove offset so we can floor.
+        var botLeft = new Vector2(0f, 0f);
+        var topRight = botLeft + Size;
+
+        var flooredBL = botLeft - originBL;
+
+        // Floor to background size.
+        flooredBL = (flooredBL / size).Floored() * size;
+
+        // Re-offset.
+        flooredBL += originBL;
+
+        for (var x = flooredBL.X; x < topRight.X; x += size.X)
+        {
+            for (var y = flooredBL.Y; y < topRight.Y; y += size.Y)
+            {
+                handle.DrawTextureRect(tex, new UIBox2(x, y, x + size.X, y + size.Y));
+            }
+        }
+    }
+
+    /// <summary>
+    /// Gets the map objects that intersect the viewport.
+    /// </summary>
+    /// <param name="mapObjects"></param>
+    /// <returns></returns>
+    private List<IMapObject> GetViewportMapObjects(Matrix3 matty, List<IMapObject> mapObjects)
+    {
+        var results = new List<IMapObject>();
+        var viewBox = SizeBox.Scale(1.2f);
+
+        foreach (var mapObj in mapObjects)
+        {
+            var mapCoords = _shuttles.GetMapCoordinates(mapObj);
+
+            var relativePos = matty.Transform(mapCoords.Position);
+            relativePos = relativePos with { Y = -relativePos.Y };
+            var uiPosition = ScalePosition(relativePos);
+
+            if (!viewBox.Contains(uiPosition.Floored()))
+                continue;
+
+            results.Add(mapObj);
+        }
+
+        return results;
+    }
+
+    protected override void Draw(DrawingHandleScreen handle)
+    {
+        base.Draw(handle);
+
+        if (ViewingMap == MapId.Nullspace)
+            return;
+
+        var mapObjects = _mapObjects;
+        DrawRecenter();
+
+        if (InFtl || mapObjects.Count == 0)
+        {
+            DrawBacking(handle);
+            DrawNoSignal(handle);
+            return;
+        }
+
+        DrawParallax(handle);
+
+        var viewedMapUid = _mapManager.GetMapEntityId(ViewingMap);
+        var matty = Matrix3.CreateInverseTransform(Offset, Angle.Zero);
+        var realTime = _timing.RealTime;
+        var viewBox = new Box2(Offset - WorldRangeVector, Offset + WorldRangeVector);
+        var viewportObjects = GetViewportMapObjects(matty, mapObjects);
+        _viewportExclusions.Clear();
+
+        // Draw our FTL range + no FTL zones
+        // Do it up here because we want this layered below most things.
+        if (FtlMode)
+        {
+            if (EntManager.TryGetComponent<TransformComponent>(_shuttleEntity, out var shuttleXform))
+            {
+                var gridUid = _shuttleEntity.Value;
+                var gridPhysics = _physicsQuery.GetComponent(gridUid);
+                var (gridPos, gridRot) = _xformSystem.GetWorldPositionRotation(shuttleXform);
+                gridPos = Maps.GetGridPosition((gridUid, gridPhysics), gridPos, gridRot);
+
+                var gridRelativePos = matty.Transform(gridPos);
+                gridRelativePos = gridRelativePos with { Y = -gridRelativePos.Y };
+                var gridUiPos = ScalePosition(gridRelativePos);
+
+                var range = _shuttles.GetFTLRange(gridUid);
+                range *= MinimapScale;
+                handle.DrawCircle(gridUiPos, range, Color.Gold, filled: false);
+            }
+        }
+
+        var exclusionColor = Color.Red;
+
+        // Exclusions need a bumped range so we check all the ones on the map.
+        foreach (var mapObj in mapObjects)
+        {
+            if (mapObj is not ShuttleExclusionObject exclusion)
+                continue;
+
+            // Check if it even intersects the viewport.
+            var coords = EntManager.GetCoordinates(exclusion.Coordinates);
+            var mapCoords = _xformSystem.ToMapCoordinates(coords);
+            var enlargedBounds = viewBox.Enlarged(exclusion.Range);
+
+            if (mapCoords.MapId != ViewingMap ||
+                !enlargedBounds.Contains(mapCoords.Position))
+            {
+                continue;
+            }
+
+            var adjustedPos = matty.Transform(mapCoords.Position);
+            var localPos = ScalePosition(adjustedPos with { Y = -adjustedPos.Y});
+            handle.DrawCircle(localPos, exclusion.Range * MinimapScale, exclusionColor.WithAlpha(0.05f));
+            handle.DrawCircle(localPos, exclusion.Range * MinimapScale, exclusionColor, filled: false);
+
+            _viewportExclusions.Add(exclusion);
+        }
+
+        _verts.Clear();
+        _edges.Clear();
+        _strings.Clear();
+
+        // Add beacons if relevant.
+        var beaconsOnly = _shuttles.IsBeaconMap(viewedMapUid);
+        var controlLocalBounds = PixelRect;
+        _beacons.Clear();
+
+        if (ShowBeacons)
+        {
+            var beaconColor = Color.AliceBlue;
+
+            foreach (var (beaconName, coords, mapO) in GetBeacons(viewportObjects, matty, controlLocalBounds))
+            {
+                var localPos = matty.Transform(coords.Position);
+                localPos = localPos with { Y = -localPos.Y };
+                var beaconUiPos = ScalePosition(localPos);
+                var mapObject = GetMapObject(localPos, Angle.Zero, scale: 0.75f, scalePosition: true);
+
+                var existingVerts = _verts.GetOrNew(beaconColor);
+                var existingEdges = _edges.GetOrNew(beaconColor);
+
+                AddMapObject(existingEdges, existingVerts, mapObject);
+                _beacons.Add(mapO);
+
+                var existingStrings = _strings.GetOrNew(beaconColor);
+                existingStrings.Add((beaconUiPos, beaconName));
+            }
+        }
+
+        foreach (var mapObj in viewportObjects)
+        {
+            if (mapObj is not GridMapObject gridObj || !EntManager.TryGetComponent(gridObj.Entity, out MapGridComponent? mapGrid))
+                continue;
+
+            Entity<MapGridComponent> grid = (gridObj.Entity, mapGrid);
+            IFFComponent? iffComp = null;
+
+            // Rudimentary IFF for now, if IFF hiding on then we don't show on the map at all
+            if (grid.Owner != _shuttleEntity &&
+                EntManager.TryGetComponent(grid, out iffComp) &&
+                (iffComp.Flags & (IFFFlags.Hide | IFFFlags.HideLabel)) != 0x0)
+            {
+                continue;
+            }
+
+            var gridColor = _shuttles.GetIFFColor(grid, self: _shuttleEntity == grid.Owner, component: iffComp);
+
+            var existingVerts = _verts.GetOrNew(gridColor);
+            var existingEdges = _edges.GetOrNew(gridColor);
+
+            var gridPhysics = _physicsQuery.GetComponent(grid.Owner);
+            var (gridPos, gridRot) = _xformSystem.GetWorldPositionRotation(grid.Owner);
+            gridPos = Maps.GetGridPosition((grid, gridPhysics), gridPos, gridRot);
+
+            var gridRelativePos = matty.Transform(gridPos);
+            gridRelativePos = gridRelativePos with { Y = -gridRelativePos.Y };
+            var gridUiPos = ScalePosition(gridRelativePos);
+
+            var mapObject = GetMapObject(gridRelativePos, Angle.Zero, scalePosition: true);
+            AddMapObject(existingEdges, existingVerts, mapObject);
+
+            // Text
+            // Force drawing it at this point.
+            var iffText = _shuttles.GetIFFLabel(grid, self: true, component: iffComp);
+
+            if (string.IsNullOrEmpty(iffText))
+                continue;
+
+            var existingStrings = _strings.GetOrNew(gridColor);
+            existingStrings.Add((gridUiPos, iffText));
+        }
+
+        // Batch the colors whoopie
+        // really only affects forks with lots of grids.
+        foreach (var (color, sendVerts) in _verts)
+        {
+            handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, sendVerts, color.WithAlpha(0.05f));
+        }
+
+        foreach (var (color, sendEdges) in _edges)
+        {
+            handle.DrawPrimitives(DrawPrimitiveTopology.LineList, sendEdges, color);
+        }
+
+        foreach (var (color, sendStrings) in _strings)
+        {
+            var adjustedColor = Color.FromSrgb(color);
+
+            foreach (var (gridUiPos, iffText) in sendStrings)
+            {
+                var textWidth = handle.GetDimensions(_font, iffText, UIScale);
+                handle.DrawString(_font, gridUiPos + textWidth with { X = -textWidth.X / 2f }, iffText, adjustedColor);
+            }
+        }
+
+        var mousePos = _inputs.MouseScreenPosition;
+        var mouseLocalPos = GetLocalPosition(mousePos);
+
+        // Draw dotted line from our own shuttle entity to mouse.
+        if (FtlMode)
+        {
+            if (mousePos.Window != WindowId.Invalid)
+            {
+                // If mouse inbounds then draw it.
+                if (_shuttleEntity != null && controlLocalBounds.Contains(mouseLocalPos.Floored()) &&
+                    EntManager.TryGetComponent(_shuttleEntity, out TransformComponent? shuttleXform) &&
+                    shuttleXform.MapID != MapId.Nullspace)
+                {
+                    // If it's a beacon only map then snap the mouse to a nearby spot.
+                    ShuttleBeaconObject foundBeacon = default;
+
+                    // Check for beacons around mouse and snap to that.
+                    if (beaconsOnly && TryGetBeacon(viewportObjects, matty, mouseLocalPos, controlLocalBounds, out foundBeacon, out var foundLocalPos))
+                    {
+                        mouseLocalPos = foundLocalPos;
+                    }
+
+                    var grid = EntManager.GetComponent<MapGridComponent>(_shuttleEntity.Value);
+
+                    var (gridPos, gridRot) = _xformSystem.GetWorldPositionRotation(shuttleXform);
+                    gridPos = Maps.GetGridPosition(_shuttleEntity.Value, gridPos, gridRot);
+
+                    // do NOT apply LocalCenter operation here because it will be adjusted in FTLFree.
+                    var mouseMapPos = InverseMapPosition(mouseLocalPos);
+
+                    var ftlFree = (!beaconsOnly || foundBeacon != default) &&
+                                  _shuttles.FTLFree(_shuttleEntity.Value, new EntityCoordinates(viewedMapUid, mouseMapPos), _ftlAngle, _viewportExclusions);
+
+                    var color = ftlFree ? Color.LimeGreen : Color.Magenta;
+
+                    var gridRelativePos = matty.Transform(gridPos);
+                    gridRelativePos = gridRelativePos with { Y = -gridRelativePos.Y };
+                    var gridUiPos = ScalePosition(gridRelativePos);
+
+                    // Draw FTL buffer around the mouse.
+                    var ourFTLBuffer = _shuttles.GetFTLBufferRange(_shuttleEntity.Value, grid);
+                    ourFTLBuffer *= MinimapScale;
+                    handle.DrawCircle(mouseLocalPos, ourFTLBuffer, Color.Magenta.WithAlpha(0.01f));
+                    handle.DrawCircle(mouseLocalPos, ourFTLBuffer, Color.Magenta, filled: false);
+
+                    // Draw line from our shuttle to target
+                    // Might need to clip the line if it's too far? But my brain wasn't working so F.
+                    handle.DrawDottedLine(gridUiPos, mouseLocalPos, color, (float) realTime.TotalSeconds * 30f);
+
+                    // Draw shuttle pre-vis
+                    var mouseVerts = GetMapObject(mouseLocalPos, _ftlAngle, scale: MinimapScale);
+
+                    handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, mouseVerts.Span, color.WithAlpha(0.05f));
+                    handle.DrawPrimitives(DrawPrimitiveTopology.LineLoop, mouseVerts.Span, color);
+
+                    // Draw a notch indicating direction.
+                    var ftlLength = GetMapObjectRadius() + 16f;
+                    var ftlEnd = mouseLocalPos + _ftlAngle.RotateVec(new Vector2(0f, -ftlLength));
+
+                    handle.DrawLine(mouseLocalPos, ftlEnd, color);
+                }
+            }
+        }
+
+        // Draw the coordinates
+        var mapOffset = MidPointVector;
+
+        if (mousePos.Window != WindowId.Invalid &&
+            controlLocalBounds.Contains(mouseLocalPos.Floored()))
+        {
+            mapOffset = mouseLocalPos;
+        }
+
+        mapOffset = InverseMapPosition(mapOffset);
+        var coordsText = $"{mapOffset.X:0.0}, {mapOffset.Y:0.0}";
+        DrawData(handle, coordsText);
+    }
+
+    private void AddMapObject(List<Vector2> edges, List<Vector2> verts, ValueList<Vector2> mapObject)
+    {
+        var bottom = mapObject[0];
+        var right = mapObject[1];
+        var top = mapObject[2];
+        var left = mapObject[3];
+
+        // Diamond interior
+        verts.Add(bottom);
+        verts.Add(right);
+        verts.Add(top);
+
+        verts.Add(bottom);
+        verts.Add(top);
+        verts.Add(left);
+
+        // Diamond edges
+        edges.Add(bottom);
+        edges.Add(right);
+        edges.Add(right);
+        edges.Add(top);
+        edges.Add(top);
+        edges.Add(left);
+        edges.Add(left);
+        edges.Add(bottom);
+    }
+
+    /// <summary>
+    /// Returns the beacons that intersect the viewport.
+    /// </summary>
+    private IEnumerable<(string Beacon, MapCoordinates Coordinates, IMapObject MapObject)> GetBeacons(List<IMapObject> mapObjs, Matrix3 mapTransform, UIBox2i area)
+    {
+        foreach (var mapO in mapObjs)
+        {
+            if (mapO is not ShuttleBeaconObject beacon)
+                continue;
+
+            var beaconCoords = EntManager.GetCoordinates(beacon.Coordinates).ToMap(EntManager, _xformSystem);
+            var position = mapTransform.Transform(beaconCoords.Position);
+            var localPos = ScalePosition(position with {Y = -position.Y});
+
+            // If beacon not on screen then ignore it.
+            if (!area.Contains(localPos.Floored()))
+                continue;
+
+            yield return (beacon.Name, beaconCoords, mapO);
+        }
+    }
+
+    private float GetMapObjectRadius(float scale = 1f) => WorldRange / 40f * scale;
+
+    private ValueList<Vector2> GetMapObject(Vector2 localPos, Angle angle, float scale = 1f, bool scalePosition = false)
+    {
+        // Constant size diamonds
+        var diamondRadius = GetMapObjectRadius();
+
+        var mapObj = new ValueList<Vector2>(4)
+        {
+            localPos + angle.RotateVec(new Vector2(0f, -2f * diamondRadius)) * scale,
+            localPos + angle.RotateVec(new Vector2(diamondRadius, 0f)) * scale,
+            localPos + angle.RotateVec(new Vector2(0f, 2f * diamondRadius)) * scale,
+            localPos + angle.RotateVec(new Vector2(-diamondRadius, 0f)) * scale,
+        };
+
+        if (scalePosition)
+        {
+            for (var i = 0; i < mapObj.Count; i++)
+            {
+                mapObj[i] = ScalePosition(mapObj[i]);
+            }
+        }
+
+        return mapObj;
+    }
+
+    private bool TryGetBeacon(IEnumerable<IMapObject> mapObjects, Matrix3 mapTransform, Vector2 mousePos, UIBox2i area, out ShuttleBeaconObject foundBeacon, out Vector2 foundLocalPos)
+    {
+        // In pixels
+        const float BeaconSnapRange = 32f;
+        float nearestValue = float.MaxValue;
+        foundLocalPos = Vector2.Zero;
+        foundBeacon = default;
+
+        foreach (var mapObj in mapObjects)
+        {
+            if (mapObj is not ShuttleBeaconObject beaconObj)
+                continue;
+
+            var beaconCoords = _xformSystem.ToMapCoordinates(EntManager.GetCoordinates(beaconObj.Coordinates));
+
+            if (beaconCoords.MapId != ViewingMap)
+                continue;
+
+            // Invalid beacon?
+            if (!_shuttles.CanFTLBeacon(beaconObj.Coordinates))
+                continue;
+
+            var position = mapTransform.Transform(beaconCoords.Position);
+            var localPos = ScalePosition(position with {Y = -position.Y});
+
+            // If beacon not on screen then ignore it.
+            if (!area.Contains(localPos.Floored()))
+                continue;
+
+            var distance = (localPos - mousePos).Length();
+
+            if (distance > BeaconSnapRange ||
+                distance > nearestValue)
+            {
+                continue;
+            }
+
+            foundLocalPos = localPos;
+            nearestValue = distance;
+            foundBeacon = beaconObj;
+        }
+
+        return foundBeacon != default;
+    }
+
+    /// <summary>
+    /// Sets the map objects for the next draw.
+    /// </summary>
+    public void SetMapObjects(Dictionary<MapId, List<IMapObject>> mapObjects)
+    {
+        _mapObjects.Clear();
+
+        if (mapObjects.TryGetValue(ViewingMap, out var obbies))
+        {
+            _mapObjects.AddRange(obbies);
+        }
+    }
+}
diff --git a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml
new file mode 100644 (file)
index 0000000..f517a30
--- /dev/null
@@ -0,0 +1 @@
+<ui:BaseShuttleControl xmlns:ui="clr-namespace:Content.Client.Shuttles.UI" />
diff --git a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
new file mode 100644 (file)
index 0000000..1812241
--- /dev/null
@@ -0,0 +1,288 @@
+using System.Numerics;
+using Content.Shared.Shuttles.BUIStates;
+using Content.Shared.Shuttles.Components;
+using Content.Shared.Shuttles.Systems;
+using JetBrains.Annotations;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Collections;
+using Robust.Shared.Input;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Shuttles.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class ShuttleNavControl : BaseShuttleControl
+{
+    [Dependency] private readonly IMapManager _mapManager = default!;
+    private readonly SharedShuttleSystem _shuttles;
+    private readonly SharedTransformSystem _transform;
+
+    /// <summary>
+    /// Used to transform all of the radar objects. Typically is a shuttle console parented to a grid.
+    /// </summary>
+    private EntityCoordinates? _coordinates;
+
+    private Angle? _rotation;
+
+    private Dictionary<NetEntity, List<DockingPortState>> _docks = new();
+
+    public bool ShowIFF { get; set; } = true;
+    public bool ShowDocks { get; set; } = true;
+
+    /// <summary>
+    /// Raised if the user left-clicks on the radar control with the relevant entitycoordinates.
+    /// </summary>
+    public Action<EntityCoordinates>? OnRadarClick;
+
+    private List<Entity<MapGridComponent>> _grids = new();
+
+    public ShuttleNavControl() : base(64f, 256f, 256f)
+    {
+        RobustXamlLoader.Load(this);
+        _shuttles = EntManager.System<SharedShuttleSystem>();
+        _transform = EntManager.System<SharedTransformSystem>();
+    }
+
+    public void SetMatrix(EntityCoordinates? coordinates, Angle? angle)
+    {
+        _coordinates = coordinates;
+        _rotation = angle;
+    }
+
+    protected override void KeyBindUp(GUIBoundKeyEventArgs args)
+    {
+        base.KeyBindUp(args);
+
+        if (_coordinates == null || _rotation == null || args.Function != EngineKeyFunctions.UIClick ||
+            OnRadarClick == null)
+        {
+            return;
+        }
+
+        var a = InverseScalePosition(args.RelativePosition);
+        var relativeWorldPos = new Vector2(a.X, -a.Y);
+        relativeWorldPos = _rotation.Value.RotateVec(relativeWorldPos);
+        var coords = _coordinates.Value.Offset(relativeWorldPos);
+        OnRadarClick?.Invoke(coords);
+    }
+
+    /// <summary>
+    /// Gets the entitycoordinates of where the mouseposition is, relative to the control.
+    /// </summary>
+    [PublicAPI]
+    public EntityCoordinates GetMouseCoordinates(ScreenCoordinates screen)
+    {
+        if (_coordinates == null || _rotation == null)
+        {
+            return EntityCoordinates.Invalid;
+        }
+
+        var pos = screen.Position / UIScale - GlobalPosition;
+
+        var a = InverseScalePosition(pos);
+        var relativeWorldPos = new Vector2(a.X, -a.Y);
+        relativeWorldPos = _rotation.Value.RotateVec(relativeWorldPos);
+        var coords = _coordinates.Value.Offset(relativeWorldPos);
+        return coords;
+    }
+
+    public void UpdateState(NavInterfaceState state)
+    {
+        SetMatrix(EntManager.GetCoordinates(state.Coordinates), state.Angle);
+
+        WorldMaxRange = state.MaxRange;
+
+        if (WorldMaxRange < WorldRange)
+        {
+            ActualRadarRange = WorldMaxRange;
+        }
+
+        if (WorldMaxRange < WorldMinRange)
+            WorldMinRange = WorldMaxRange;
+
+        ActualRadarRange = Math.Clamp(ActualRadarRange, WorldMinRange, WorldMaxRange);
+
+        _docks = state.Docks;
+    }
+
+    protected override void Draw(DrawingHandleScreen handle)
+    {
+        base.Draw(handle);
+
+        DrawBacking(handle);
+        DrawCircles(handle);
+
+        // No data
+        if (_coordinates == null || _rotation == null)
+        {
+            return;
+        }
+
+        var xformQuery = EntManager.GetEntityQuery<TransformComponent>();
+        var fixturesQuery = EntManager.GetEntityQuery<FixturesComponent>();
+        var bodyQuery = EntManager.GetEntityQuery<PhysicsComponent>();
+
+        if (!xformQuery.TryGetComponent(_coordinates.Value.EntityId, out var xform)
+            || xform.MapID == MapId.Nullspace)
+        {
+            return;
+        }
+
+        var mapPos = _transform.ToMapCoordinates(_coordinates.Value);
+        var offset = _coordinates.Value.Position;
+        var posMatrix = Matrix3.CreateTransform(offset, _rotation.Value);
+        var (_, ourEntRot, ourEntMatrix) = _transform.GetWorldPositionRotationMatrix(_coordinates.Value.EntityId);
+        Matrix3.Multiply(posMatrix, ourEntMatrix, out var ourWorldMatrix);
+        var ourWorldMatrixInvert = ourWorldMatrix.Invert();
+
+        // Draw our grid in detail
+        var ourGridId = xform.GridUid;
+        if (EntManager.TryGetComponent<MapGridComponent>(ourGridId, out var ourGrid) &&
+            fixturesQuery.HasComponent(ourGridId.Value))
+        {
+            var ourGridMatrix = _transform.GetWorldMatrix(ourGridId.Value);
+            Matrix3.Multiply(in ourGridMatrix, in ourWorldMatrixInvert, out var matrix);
+            var color = _shuttles.GetIFFColor(ourGridId.Value, self: true);
+
+            DrawGrid(handle, matrix, (ourGridId.Value, ourGrid), color);
+            DrawDocks(handle, ourGridId.Value, matrix);
+        }
+
+        var invertedPosition = _coordinates.Value.Position - offset;
+        invertedPosition.Y = -invertedPosition.Y;
+        // Don't need to transform the InvWorldMatrix again as it's already offset to its position.
+
+        // Draw radar position on the station
+        var radarPos = invertedPosition;
+        const float radarVertRadius = 2f;
+
+        var radarPosVerts = new Vector2[]
+        {
+            ScalePosition(radarPos + new Vector2(0f, -radarVertRadius)),
+            ScalePosition(radarPos + new Vector2(radarVertRadius / 2f, 0f)),
+            ScalePosition(radarPos + new Vector2(0f, radarVertRadius)),
+            ScalePosition(radarPos + new Vector2(radarVertRadius / -2f, 0f)),
+        };
+
+        handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, radarPosVerts, Color.Lime);
+
+        var rot = ourEntRot + _rotation.Value;
+        var viewBounds = new Box2Rotated(new Box2(-WorldRange, -WorldRange, WorldRange, WorldRange).Translated(mapPos.Position), rot, mapPos.Position);
+        var viewAABB = viewBounds.CalcBoundingBox();
+
+        _grids.Clear();
+        _mapManager.FindGridsIntersecting(xform.MapID, new Box2(mapPos.Position - MaxRadarRangeVector, mapPos.Position + MaxRadarRangeVector), ref _grids, approx: true, includeMap: false);
+
+        // Draw other grids... differently
+        foreach (var grid in _grids)
+        {
+            var gUid = grid.Owner;
+            if (gUid == ourGridId || !fixturesQuery.HasComponent(gUid))
+                continue;
+
+            var gridBody = bodyQuery.GetComponent(gUid);
+            EntManager.TryGetComponent<IFFComponent>(gUid, out var iff);
+
+            if (!_shuttles.CanDraw(gUid, gridBody, iff))
+                continue;
+
+            var gridMatrix = _transform.GetWorldMatrix(gUid);
+            Matrix3.Multiply(in gridMatrix, in ourWorldMatrixInvert, out var matty);
+            var color = _shuttles.GetIFFColor(grid, self: false, iff);
+
+            // Others default:
+            // Color.FromHex("#FFC000FF")
+            // Hostile default: Color.Firebrick
+            var labelName = _shuttles.GetIFFLabel(grid, self: false, iff);
+
+            if (ShowIFF &&
+                 labelName != null)
+            {
+                var gridBounds = grid.Comp.LocalAABB;
+
+                var gridCentre = matty.Transform(gridBody.LocalCenter);
+                gridCentre.Y = -gridCentre.Y;
+                var distance = gridCentre.Length();
+                var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName),
+                    ("distance", $"{distance:0.0}"));
+                var labelDimensions = handle.GetDimensions(Font, labelText, UIScale);
+
+                // y-offset the control to always render below the grid (vertically)
+                var yOffset = Math.Max(gridBounds.Height, gridBounds.Width) * MinimapScale / 1.8f / UIScale;
+
+                // The actual position in the UI. We offset the matrix position to render it off by half its width
+                // plus by the offset.
+                var uiPosition = ScalePosition(gridCentre) / UIScale - new Vector2(labelDimensions.X / 2f, -yOffset);
+
+                // Look this is uggo so feel free to cleanup. We just need to clamp the UI position to within the viewport.
+                uiPosition = new Vector2(Math.Clamp(uiPosition.X, 0f, Width - labelDimensions.X),
+                    Math.Clamp(uiPosition.Y, 0f, Height - labelDimensions.Y));
+
+                handle.DrawString(Font, uiPosition, labelText, color);
+            }
+
+            // Detailed view
+            var gridAABB = gridMatrix.TransformBox(grid.Comp.LocalAABB);
+
+            // Skip drawing if it's out of range.
+            if (!gridAABB.Intersects(viewAABB))
+                continue;
+
+            DrawGrid(handle, matty, grid, color);
+            DrawDocks(handle, gUid, matty);
+        }
+    }
+
+    private void DrawDocks(DrawingHandleScreen handle, EntityUid uid, Matrix3 matrix)
+    {
+        if (!ShowDocks)
+            return;
+
+        const float DockScale = 0.6f;
+        var nent = EntManager.GetNetEntity(uid);
+
+        if (_docks.TryGetValue(nent, out var docks))
+        {
+            foreach (var state in docks)
+            {
+                var position = state.Coordinates.Position;
+                var uiPosition = matrix.Transform(position);
+
+                if (uiPosition.Length() > (WorldRange * 2f) - DockScale)
+                    continue;
+
+                var color = Color.ToSrgb(Color.Magenta);
+
+                var verts = new[]
+                {
+                    matrix.Transform(position + new Vector2(-DockScale, -DockScale)),
+                    matrix.Transform(position + new Vector2(DockScale, -DockScale)),
+                    matrix.Transform(position + new Vector2(DockScale, DockScale)),
+                    matrix.Transform(position + new Vector2(-DockScale, DockScale)),
+                };
+
+                for (var i = 0; i < verts.Length; i++)
+                {
+                    var vert = verts[i];
+                    vert.Y = -vert.Y;
+                    verts[i] = ScalePosition(vert);
+                }
+
+                handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, color.WithAlpha(0.8f));
+                handle.DrawPrimitives(DrawPrimitiveTopology.LineStrip, verts, color);
+            }
+        }
+    }
+
+    private Vector2 InverseScalePosition(Vector2 value)
+    {
+        return (value - MidPointVector) / MinimapScale;
+    }
+}
diff --git a/Content.Client/UserInterface/Controls/MapGridControl.cs b/Content.Client/UserInterface/Controls/MapGridControl.cs
deleted file mode 100644 (file)
index d567904..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-using System.Numerics;
-using Robust.Client.Graphics;
-using Robust.Client.UserInterface;
-using Robust.Shared.Timing;
-
-namespace Content.Client.UserInterface.Controls;
-
-/// <summary>
-/// Handles generic grid-drawing data, with zoom and dragging.
-/// </summary>
-public abstract class MapGridControl : Control
-{
-    [Dependency] protected readonly IGameTiming Timing = default!;
-
-    protected const float ScrollSensitivity = 8f;
-
-    /// <summary>
-    /// UI pixel radius.
-    /// </summary>
-    public const int UIDisplayRadius = 320;
-    protected const int MinimapMargin = 4;
-
-    protected float WorldMinRange;
-    protected float WorldMaxRange;
-    public float WorldRange;
-
-    /// <summary>
-    /// We'll lerp between the radarrange and actual range
-    /// </summary>
-    protected float ActualRadarRange;
-
-    /// <summary>
-    /// Controls the maximum distance that will display.
-    /// </summary>
-    public float MaxRadarRange { get; private set; } = 256f * 10f;
-
-    public Vector2 MaxRadarRangeVector => new Vector2(MaxRadarRange, MaxRadarRange);
-
-    protected Vector2 MidpointVector => new Vector2(MidPoint, MidPoint);
-
-    protected int MidPoint => SizeFull / 2;
-    protected int SizeFull => (int) ((UIDisplayRadius + MinimapMargin) * 2 * UIScale);
-    protected int ScaledMinimapRadius => (int) (UIDisplayRadius * UIScale);
-    protected float MinimapScale => WorldRange != 0 ? ScaledMinimapRadius / WorldRange : 0f;
-
-    public event Action<float>? WorldRangeChanged;
-
-    public MapGridControl(float minRange, float maxRange, float range)
-    {
-        IoCManager.InjectDependencies(this);
-        SetSize = new Vector2(SizeFull, SizeFull);
-        RectClipContent = true;
-        MouseFilter = MouseFilterMode.Stop;
-        ActualRadarRange = WorldRange;
-        WorldMinRange = minRange;
-        WorldMaxRange = maxRange;
-        WorldRange = range;
-        ActualRadarRange = range;
-    }
-
-    protected override void MouseWheel(GUIMouseWheelEventArgs args)
-    {
-        base.MouseWheel(args);
-        AddRadarRange(-args.Delta.Y * 1f / ScrollSensitivity * ActualRadarRange);
-    }
-
-    public void AddRadarRange(float value)
-    {
-        ActualRadarRange = Math.Clamp(ActualRadarRange + value, WorldMinRange, WorldMaxRange);
-    }
-
-    protected override void Draw(DrawingHandleScreen handle)
-    {
-        base.Draw(handle);
-        if (!ActualRadarRange.Equals(WorldRange))
-        {
-            var diff = ActualRadarRange - WorldRange;
-            const float lerpRate = 10f;
-
-            WorldRange += (float) Math.Clamp(diff, -lerpRate * MathF.Abs(diff) * Timing.FrameTime.TotalSeconds, lerpRate * MathF.Abs(diff) * Timing.FrameTime.TotalSeconds);
-            WorldRangeChanged?.Invoke(WorldRange);
-        }
-    }
-}
diff --git a/Content.Client/UserInterface/Controls/MapGridControl.xaml b/Content.Client/UserInterface/Controls/MapGridControl.xaml
new file mode 100644 (file)
index 0000000..7003afa
--- /dev/null
@@ -0,0 +1 @@
+<controls:LayoutContainer xmlns:controls="https://spacestation14.io"/>
diff --git a/Content.Client/UserInterface/Controls/MapGridControl.xaml.cs b/Content.Client/UserInterface/Controls/MapGridControl.xaml.cs
new file mode 100644 (file)
index 0000000..adefd3c
--- /dev/null
@@ -0,0 +1,243 @@
+using System.Numerics;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Input;
+using Robust.Shared.Timing;
+
+namespace Content.Client.UserInterface.Controls;
+
+/// <summary>
+/// Handles generic grid-drawing data, with zoom and dragging.
+/// </summary>
+[GenerateTypedNameReferences]
+[Virtual]
+public partial class MapGridControl : LayoutContainer
+{
+    [Dependency] protected readonly IEntityManager EntManager = default!;
+    [Dependency] protected readonly IGameTiming Timing = default!;
+
+    protected static readonly Color BackingColor = new Color(0.08f, 0.08f, 0.08f);
+
+    private Font _largerFont;
+
+    /* Dragging */
+    protected virtual bool Draggable { get; } = false;
+
+    /// <summary>
+    /// Control offset from whatever is being tracked.
+    /// </summary>
+    public Vector2 Offset;
+
+    /// <summary>
+    /// If the control is being recentered what is the target offset to reach.
+    /// </summary>
+    public Vector2 TargetOffset;
+
+    private bool _draggin;
+    protected Vector2 StartDragPosition;
+    protected bool Recentering;
+
+    protected const float ScrollSensitivity = 8f;
+
+    protected float RecenterMinimum = 0.05f;
+
+    /// <summary>
+    /// UI pixel radius.
+    /// </summary>
+    public const int UIDisplayRadius = 320;
+    protected const int MinimapMargin = 4;
+
+    protected float WorldMinRange;
+    protected float WorldMaxRange;
+    public float WorldRange;
+    public Vector2 WorldRangeVector => new Vector2(WorldRange, WorldRange);
+
+    /// <summary>
+    /// We'll lerp between the radarrange and actual range
+    /// </summary>
+    protected float ActualRadarRange;
+
+    protected float CornerRadarRange => MathF.Sqrt(ActualRadarRange * ActualRadarRange + ActualRadarRange * ActualRadarRange) * 1.1f;
+
+    /// <summary>
+    /// Controls the maximum distance that will display.
+    /// </summary>
+    public float MaxRadarRange { get; private set; } = 256f * 10f;
+
+    public Vector2 MaxRadarRangeVector => new Vector2(MaxRadarRange, MaxRadarRange);
+
+    protected Vector2 MidPointVector => new Vector2(MidPoint, MidPoint);
+
+    protected int MidPoint => SizeFull / 2;
+    protected int SizeFull => (int) ((UIDisplayRadius + MinimapMargin) * 2 * UIScale);
+    protected int ScaledMinimapRadius => (int) (UIDisplayRadius * UIScale);
+    protected float MinimapScale => WorldRange != 0 ? ScaledMinimapRadius / WorldRange : 0f;
+
+    public event Action<float>? WorldRangeChanged;
+
+    public MapGridControl() : this(32f, 32f, 32f) {}
+
+    public MapGridControl(float minRange, float maxRange, float range)
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+        SetSize = new Vector2(SizeFull, SizeFull);
+        RectClipContent = true;
+        MouseFilter = MouseFilterMode.Stop;
+        ActualRadarRange = WorldRange;
+        WorldMinRange = minRange;
+        WorldMaxRange = maxRange;
+        WorldRange = range;
+        ActualRadarRange = range;
+
+        var cache = IoCManager.Resolve<IResourceCache>();
+        _largerFont = new VectorFont(cache.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 16);
+    }
+
+    public void ForceRecenter()
+    {
+        Recentering = true;
+    }
+
+    protected override void KeyBindDown(GUIBoundKeyEventArgs args)
+    {
+        base.KeyBindDown(args);
+
+        if (!Draggable)
+            return;
+
+        if (args.Function == EngineKeyFunctions.Use)
+        {
+            StartDragPosition = args.PointerLocation.Position;
+            _draggin = true;
+        }
+    }
+
+    protected override void KeyBindUp(GUIBoundKeyEventArgs args)
+    {
+        if (!Draggable)
+            return;
+
+        if (args.Function == EngineKeyFunctions.Use)
+            _draggin = false;
+    }
+
+    protected override void MouseMove(GUIMouseMoveEventArgs args)
+    {
+        base.MouseMove(args);
+
+        if (!_draggin)
+            return;
+
+        Recentering = false;
+        Offset -= new Vector2(args.Relative.X, -args.Relative.Y) / MidPoint * WorldRange;
+    }
+
+    protected override void MouseWheel(GUIMouseWheelEventArgs args)
+    {
+        base.MouseWheel(args);
+        AddRadarRange(-args.Delta.Y * 1f / ScrollSensitivity * ActualRadarRange);
+    }
+
+    public void AddRadarRange(float value)
+    {
+        ActualRadarRange = Math.Clamp(ActualRadarRange + value, WorldMinRange, WorldMaxRange);
+    }
+
+    /// <summary>
+    /// Converts map coordinates to the local control.
+    /// </summary>
+    protected Vector2 ScalePosition(Vector2 value)
+    {
+        return ScalePosition(value, MinimapScale, MidPointVector);
+    }
+
+    protected static Vector2 ScalePosition(Vector2 value, float minimapScale, Vector2 midpointVector)
+    {
+        return value * minimapScale + midpointVector;
+    }
+
+    /// <summary>
+    /// Converts local coordinates on the control to map coordinates.
+    /// </summary>
+    protected Vector2 InverseMapPosition(Vector2 value)
+    {
+        var inversePos = (value - MidPointVector) / MinimapScale;
+
+        inversePos = inversePos with { Y = -inversePos.Y };
+        inversePos = Matrix3.CreateTransform(Offset, Angle.Zero).Transform(inversePos);
+        return inversePos;
+    }
+
+    /// <summary>
+    /// Handles re-centering the control's offset.
+    /// </summary>
+    /// <returns></returns>
+    public bool DrawRecenter()
+    {
+        // Map re-centering
+        if (Recentering)
+        {
+            var frameTime = Timing.FrameTime;
+            var diff = (TargetOffset - Offset) * (float) frameTime.TotalSeconds;
+
+            if (Offset.LengthSquared() < RecenterMinimum)
+            {
+                Offset = TargetOffset;
+                Recentering = false;
+            }
+            else
+            {
+                Offset += diff * 5f;
+                return false;
+            }
+        }
+
+        return Offset == TargetOffset;
+    }
+
+    protected void DrawBacking(DrawingHandleScreen handle)
+    {
+        var backing = BackingColor;
+        handle.DrawRect(new UIBox2(0f, Height, Width, 0f), backing);
+    }
+
+    protected void DrawNoSignal(DrawingHandleScreen handle)
+    {
+        var greyColor = Color.FromHex("#474F52");
+
+        // Draw funny lines
+        var lineCount = 4f;
+
+        for (var i = 0; i < lineCount; i++)
+        {
+            var angle = Angle.FromDegrees(45 + i * 360f / lineCount);
+            var distance = Width / 2f;
+            var start = MidPointVector + angle.RotateVec(new Vector2(0f, 2.5f * distance / 4f));
+            var end = MidPointVector + angle.RotateVec(new Vector2(0f, 4f * distance / 4f));
+            handle.DrawLine(start, end, greyColor);
+        }
+
+        var signalText = Loc.GetString("shuttle-console-no-signal");
+        var dimensions = handle.GetDimensions(_largerFont, signalText, 1f);
+        var position = MidPointVector - dimensions / 2f;
+        handle.DrawString(_largerFont, position, Loc.GetString("shuttle-console-no-signal"), greyColor);
+    }
+
+    protected override void Draw(DrawingHandleScreen handle)
+    {
+        base.Draw(handle);
+        if (!ActualRadarRange.Equals(WorldRange))
+        {
+            var diff = ActualRadarRange - WorldRange;
+            const float lerpRate = 10f;
+
+            WorldRange += (float) Math.Clamp(diff, -lerpRate * MathF.Abs(diff) * Timing.FrameTime.TotalSeconds, lerpRate * MathF.Abs(diff) * Timing.FrameTime.TotalSeconds);
+            WorldRangeChanged?.Invoke(WorldRange);
+        }
+    }
+}
index 3b89135c58e55c7a32885d3a849e3d7bad902f2c..8a1498cbe96865ac628aaf0ac4b7229f8c76bcce 100644 (file)
@@ -10,6 +10,7 @@ using Content.Shared.Humanoid;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Mobs.Systems;
 using Content.Shared.Salvage.Expeditions;
+using Content.Shared.Shuttles.Components;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Player;
 using Robust.Shared.Utility;
@@ -186,7 +187,7 @@ public sealed partial class SalvageSystem
                             if (shuttleXform.MapUid != uid || HasComp<FTLComponent>(shuttleUid))
                                 continue;
 
-                            _shuttle.FTLTravel(shuttleUid, shuttle, member, ftlTime);
+                            _shuttle.FTLToDock(shuttleUid, shuttle, member, ftlTime);
                         }
 
                         break;
index 5f287e0397060f3e61debab394680d7b368d51eb..62634af2bcd566d23d209df2178ce56f2e7a7710 100644 (file)
@@ -47,7 +47,7 @@ public sealed class DockCommand : IConsoleCommand
         }
 
         var dockSystem = _entManager.System<DockingSystem>();
-        dockSystem.Dock(airlock1.Value, dock1, airlock2.Value, dock2);
+        dockSystem.Dock((airlock1.Value, dock1), (airlock2.Value, dock2));
 
         if (dock1.DockedWith == airlock2)
         {
diff --git a/Content.Server/Shuttles/Components/AutoDockComponent.cs b/Content.Server/Shuttles/Components/AutoDockComponent.cs
deleted file mode 100644 (file)
index fa4cd4d..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Content.Server.Shuttles.Components;
-
-/// <summary>
-/// Added to entities when they are actively trying to dock with something else.
-/// We track it because checking every dock constantly would be expensive.
-/// </summary>
-[RegisterComponent]
-public sealed partial class AutoDockComponent : Component
-{
-    /// <summary>
-    /// Track who has requested autodocking so we can know when to be removed.
-    /// </summary>
-    public HashSet<EntityUid> Requesters = new();
-}
diff --git a/Content.Server/Shuttles/Components/FTLBeaconComponent.cs b/Content.Server/Shuttles/Components/FTLBeaconComponent.cs
new file mode 100644 (file)
index 0000000..0660633
--- /dev/null
@@ -0,0 +1,10 @@
+namespace Content.Server.Shuttles.Components;
+
+/// <summary>
+/// Shows up on a shuttle's map as an FTL target.
+/// </summary>
+[RegisterComponent]
+public sealed partial class FTLBeaconComponent : Component
+{
+
+}
index 105b9eae6d727b738c8e47fe3ed5b12c3e27084c..d15f65a35558e945c22b1fa44c18e75c61593960 100644 (file)
@@ -2,6 +2,7 @@ using Content.Shared.Shuttles.Systems;
 using Content.Shared.Tag;
 using Robust.Shared.Audio;
 using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 
 namespace Content.Server.Shuttles.Components;
@@ -25,25 +26,19 @@ public sealed partial class FTLComponent : Component
     public float Accumulator = 0f;
 
     /// <summary>
-    /// Target Uid to dock with at the end of FTL.
+    /// Coordinates to arrive it: May be relative to another grid (for docking) or map coordinates.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("targetUid")]
-    public EntityUid? TargetUid;
-
-    [ViewVariables(VVAccess.ReadWrite), DataField("targetCoordinates")]
+    [ViewVariables(VVAccess.ReadWrite), DataField]
     public EntityCoordinates TargetCoordinates;
 
-    /// <summary>
-    /// Should we dock with the target when arriving or show up nearby.
-    /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("dock")]
-    public bool Dock;
+    [DataField]
+    public Angle TargetAngle;
 
     /// <summary>
     /// If we're docking after FTL what is the prioritised dock tag (if applicable).
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("priorityTag", customTypeSerializer:typeof(PrototypeIdSerializer<TagPrototype>))]
-    public string? PriorityTag;
+    [ViewVariables(VVAccess.ReadWrite), DataField]
+    public ProtoId<TagPrototype>? PriorityTag;
 
     [ViewVariables(VVAccess.ReadWrite), DataField("soundTravel")]
     public SoundSpecifier? TravelSound = new SoundPathSpecifier("/Audio/Effects/Shuttle/hyperspace_progress.ogg")
@@ -51,5 +46,6 @@ public sealed partial class FTLComponent : Component
         Params = AudioParams.Default.WithVolume(-3f).WithLoop(true)
     };
 
+    [DataField]
     public EntityUid? TravelStream;
 }
diff --git a/Content.Server/Shuttles/Components/FTLDestinationComponent.cs b/Content.Server/Shuttles/Components/FTLDestinationComponent.cs
deleted file mode 100644 (file)
index 6eedcbd..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-using Content.Shared.Whitelist;
-
-namespace Content.Server.Shuttles.Components;
-
-[RegisterComponent]
-public sealed partial class FTLDestinationComponent : Component
-{
-    /// <summary>
-    /// Should this destination be restricted in some form from console visibility.
-    /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("whitelist")]
-    public EntityWhitelist? Whitelist;
-
-    /// <summary>
-    /// Is this destination visible but available to be warped to?
-    /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("enabled")]
-    public bool Enabled = true;
-}
diff --git a/Content.Server/Shuttles/Components/FTLExclusionComponent.cs b/Content.Server/Shuttles/Components/FTLExclusionComponent.cs
new file mode 100644 (file)
index 0000000..db65384
--- /dev/null
@@ -0,0 +1,16 @@
+using Content.Shared.Shuttles.Systems;
+
+namespace Content.Server.Shuttles.Components;
+
+/// <summary>
+/// Prevents FTL from occuring around this entity.
+/// </summary>
+[RegisterComponent, Access(typeof(SharedShuttleSystem))]
+public sealed partial class FTLExclusionComponent : Component
+{
+    [DataField]
+    public bool Enabled = true;
+
+    [DataField(required: true)]
+    public float Range = 32f;
+}
diff --git a/Content.Server/Shuttles/Components/RecentlyDockedComponent.cs b/Content.Server/Shuttles/Components/RecentlyDockedComponent.cs
deleted file mode 100644 (file)
index 6b0667d..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace Content.Server.Shuttles.Components;
-
-/// <summary>
-/// Added to <see cref="DockingComponent"/> that have recently undocked.
-/// This checks for whether they've left the specified radius before allowing them to automatically dock again.
-/// </summary>
-[RegisterComponent]
-public sealed partial class RecentlyDockedComponent : Component
-{
-    [DataField("lastDocked")]
-    public EntityUid LastDocked;
-
-    [ViewVariables(VVAccess.ReadWrite), DataField("radius")]
-    public float Radius = 1.5f;
-}
index 037fcc7566539c2dede5ce0596b3135073afd9cc..9a615a23d813004b0c03f070bc4654e02743e65a 100644 (file)
@@ -437,7 +437,7 @@ public sealed class ArrivalsSystem : EntitySystem
                 if (xform.MapUid != arrivalsXform.MapUid)
                 {
                     if (arrivals.IsValid())
-                        _shuttles.FTLTravel(uid, shuttle, arrivals, dock: true);
+                        _shuttles.FTLToDock(uid, shuttle, arrivals);
 
                     comp.NextArrivalsTime = _timing.CurTime + TimeSpan.FromSeconds(tripTime);
                 }
@@ -447,7 +447,7 @@ public sealed class ArrivalsSystem : EntitySystem
                     var targetGrid = _station.GetLargestGrid(data);
 
                     if (targetGrid != null)
-                        _shuttles.FTLTravel(uid, shuttle, targetGrid.Value, dock: true);
+                        _shuttles.FTLToDock(uid, shuttle, targetGrid.Value);
 
                     // The ArrivalsCooldown includes the trip there, so we only need to add the time taken for
                     // the trip back.
@@ -567,7 +567,7 @@ public sealed class ArrivalsSystem : EntitySystem
             var arrivalsComp = EnsureComp<ArrivalsShuttleComponent>(component.Shuttle);
             arrivalsComp.Station = uid;
             EnsureComp<ProtectedGridComponent>(uid);
-            _shuttles.FTLTravel(component.Shuttle, shuttleComp, arrivals, hyperspaceTime: RoundStartFTLDuration, dock: true);
+            _shuttles.FTLToDock(component.Shuttle, shuttleComp, arrivals, hyperspaceTime: RoundStartFTLDuration);
             arrivalsComp.NextTransfer = _timing.CurTime + TimeSpan.FromSeconds(_cfgManager.GetCVar(CCVars.ArrivalsCooldown));
         }
 
diff --git a/Content.Server/Shuttles/Systems/DockingSystem.AutoDock.cs b/Content.Server/Shuttles/Systems/DockingSystem.AutoDock.cs
deleted file mode 100644 (file)
index a09fff5..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-using Content.Server.Shuttles.Components;
-using Content.Shared.Shuttles.Components;
-using Content.Shared.Shuttles.Events;
-
-namespace Content.Server.Shuttles.Systems;
-
-public sealed partial class DockingSystem
-{
-    private void UpdateAutodock()
-    {
-        // Work out what we can autodock with, what we shouldn't, and when we should stop tracking.
-        // Autodocking only stops when the client closes that dock viewport OR they lose pilotcomponent.
-        var dockingQuery = GetEntityQuery<DockingComponent>();
-        var xformQuery = GetEntityQuery<TransformComponent>();
-        var recentQuery = GetEntityQuery<RecentlyDockedComponent>();
-        var query = EntityQueryEnumerator<AutoDockComponent>();
-
-        while (query.MoveNext(out var dockUid, out var comp))
-        {
-            if (comp.Requesters.Count == 0 || !dockingQuery.TryGetComponent(dockUid, out var dock))
-            {
-                RemComp<AutoDockComponent>(dockUid);
-                continue;
-            }
-
-            // Don't re-dock if we're already docked or recently were.
-            if (dock.Docked || recentQuery.HasComponent(dockUid))
-                continue;
-
-            var dockable = GetDockable(dockUid, xformQuery.GetComponent(dockUid));
-
-            if (dockable == null)
-                continue;
-
-            TryDock(dockUid, dock, dockable.Value);
-        }
-
-        // Work out recent docks that have gone past their designated threshold.
-        var checkedRecent = new HashSet<EntityUid>();
-        var recentQueryEnumerator = EntityQueryEnumerator<RecentlyDockedComponent, TransformComponent>();
-
-        while (recentQueryEnumerator.MoveNext(out var uid, out var comp, out var xform))
-        {
-            if (!checkedRecent.Add(uid))
-                continue;
-
-            if (!dockingQuery.HasComponent(uid))
-            {
-                RemCompDeferred<RecentlyDockedComponent>(uid);
-                continue;
-            }
-
-            if (!xformQuery.TryGetComponent(comp.LastDocked, out var otherXform))
-            {
-                RemCompDeferred<RecentlyDockedComponent>(uid);
-                continue;
-            }
-
-            var worldPos = _transform.GetWorldPosition(xform, xformQuery);
-            var otherWorldPos = _transform.GetWorldPosition(otherXform, xformQuery);
-
-            if ((worldPos - otherWorldPos).Length() < comp.Radius)
-                continue;
-
-            Log.Debug($"Removed RecentlyDocked from {ToPrettyString(uid)} and {ToPrettyString(comp.LastDocked)}");
-            RemComp<RecentlyDockedComponent>(uid);
-            RemComp<RecentlyDockedComponent>(comp.LastDocked);
-        }
-    }
-
-    private void OnRequestUndock(EntityUid uid, ShuttleConsoleComponent component, UndockRequestMessage args)
-    {
-        var dork = GetEntity(args.DockEntity);
-
-        Log.Debug($"Received undock request for {ToPrettyString(dork)}");
-
-        // TODO: Validation
-        if (!TryComp<DockingComponent>(dork, out var dock) ||
-            !dock.Docked ||
-            HasComp<PreventPilotComponent>(Transform(uid).GridUid))
-        {
-            return;
-        }
-
-        Undock(dork, dock);
-    }
-
-    private void OnRequestAutodock(EntityUid uid, ShuttleConsoleComponent component, AutodockRequestMessage args)
-    {
-        var dork = GetEntity(args.DockEntity);
-        Log.Debug($"Received autodock request for {ToPrettyString(dork)}");
-        var player = args.Session.AttachedEntity;
-
-        if (player == null ||
-            !HasComp<DockingComponent>(dork) ||
-            HasComp<PreventPilotComponent>(Transform(uid).GridUid))
-        {
-            return;
-        }
-
-        // TODO: Validation
-        var comp = EnsureComp<AutoDockComponent>(dork);
-        comp.Requesters.Add(player.Value);
-    }
-
-    private void OnRequestStopAutodock(EntityUid uid, ShuttleConsoleComponent component, StopAutodockRequestMessage args)
-    {
-        var dork = GetEntity(args.DockEntity);
-        Log.Debug($"Received stop autodock request for {ToPrettyString(dork)}");
-
-        var player = args.Session.AttachedEntity;
-
-        // TODO: Validation
-        if (player == null || !TryComp<AutoDockComponent>(dork, out var comp))
-            return;
-
-        comp.Requesters.Remove(player.Value);
-
-        if (comp.Requesters.Count == 0)
-            RemComp<AutoDockComponent>(dork);
-    }
-}
index 0fa82c303f64121e3b63d72aa6deb88ba0fb06ea..4ce52947366a1b057108f6591f19f96102dd9bbf 100644 (file)
@@ -19,13 +19,13 @@ public sealed partial class DockingSystem
 
     public Angle GetAngle(EntityUid uid, TransformComponent xform, EntityUid targetUid, TransformComponent targetXform, EntityQuery<TransformComponent> xformQuery)
    {
-       var (shuttlePos, shuttleRot) = _transform.GetWorldPositionRotation(xform, xformQuery);
-       var (targetPos, targetRot) = _transform.GetWorldPositionRotation(targetXform, xformQuery);
+       var (shuttlePos, shuttleRot) = _transform.GetWorldPositionRotation(xform);
+       var (targetPos, targetRot) = _transform.GetWorldPositionRotation(targetXform);
 
        var shuttleCOM = Robust.Shared.Physics.Transform.Mul(new Transform(shuttlePos, shuttleRot),
-           Comp<PhysicsComponent>(uid).LocalCenter);
+           _physicsQuery.GetComponent(uid).LocalCenter);
        var targetCOM = Robust.Shared.Physics.Transform.Mul(new Transform(targetPos, targetRot),
-           Comp<PhysicsComponent>(targetUid).LocalCenter);
+           _physicsQuery.GetComponent(targetUid).LocalCenter);
 
        var mapDiff = shuttleCOM - targetCOM;
        var angle = mapDiff.ToWorldAngle();
@@ -36,7 +36,7 @@ public sealed partial class DockingSystem
    /// <summary>
    /// Checks if 2 docks can be connected by moving the shuttle directly onto docks.
    /// </summary>
-   public bool CanDock(
+   private bool CanDock(
        DockingComponent shuttleDock,
        TransformComponent shuttleDockXform,
        DockingComponent gridDock,
@@ -119,37 +119,63 @@ public sealed partial class DockingSystem
        return GetDockingConfigPrivate(shuttleUid, targetGrid, shuttleDocks, gridDocks, priorityTag);
    }
 
-   private DockingConfig? GetDockingConfigPrivate(
+   /// <summary>
+   /// Tries to get a docking config at the specified coordinates and angle.
+   /// </summary>
+   public DockingConfig? GetDockingConfigAt(EntityUid shuttleUid,
+       EntityUid targetGrid,
+       EntityCoordinates coordinates,
+       Angle angle)
+   {
+       var gridDocks = GetDocks(targetGrid);
+       var shuttleDocks = GetDocks(shuttleUid);
+
+       var configs = GetDockingConfigs(shuttleUid, targetGrid, shuttleDocks, gridDocks);
+
+       foreach (var config in configs)
+       {
+           if (config.Coordinates.Equals(coordinates) && config.Angle.EqualsApprox(angle, 0.01))
+           {
+               return config;
+           }
+       }
+
+       return null;
+   }
+
+   /// <summary>
+   /// Gets all docking configs between the 2 grids.
+   /// </summary>
+   private List<DockingConfig> GetDockingConfigs(
        EntityUid shuttleUid,
        EntityUid targetGrid,
        List<(EntityUid, DockingComponent)> shuttleDocks,
-       List<(EntityUid, DockingComponent)> gridDocks,
-       string? priorityTag = null)
-    {
-        if (gridDocks.Count <= 0)
-            return null;
+       List<(EntityUid, DockingComponent)> gridDocks)
+   {
+       var validDockConfigs = new List<DockingConfig>();
 
-        var xformQuery = GetEntityQuery<TransformComponent>();
-        var targetGridGrid = Comp<MapGridComponent>(targetGrid);
-        var targetGridXform = xformQuery.GetComponent(targetGrid);
+       if (gridDocks.Count <= 0)
+            return validDockConfigs;
+
+        var targetGridGrid = _gridQuery.GetComponent(targetGrid);
+        var targetGridXform = _xformQuery.GetComponent(targetGrid);
         var targetGridAngle = _transform.GetWorldRotation(targetGridXform).Reduced();
         var shuttleFixturesComp = Comp<FixturesComponent>(shuttleUid);
-        var shuttleAABB = Comp<MapGridComponent>(shuttleUid).LocalAABB;
+        var shuttleAABB = _gridQuery.GetComponent(shuttleUid).LocalAABB;
 
         var isMap = HasComp<MapComponent>(targetGrid);
 
-        var validDockConfigs = new List<DockingConfig>();
         var grids = new List<Entity<MapGridComponent>>();
         if (shuttleDocks.Count > 0)
         {
            // We'll try all combinations of shuttle docks and see which one is most suitable
            foreach (var (dockUid, shuttleDock) in shuttleDocks)
            {
-               var shuttleDockXform = xformQuery.GetComponent(dockUid);
+               var shuttleDockXform = _xformQuery.GetComponent(dockUid);
 
                foreach (var (gridDockUid, gridDock) in gridDocks)
                {
-                   var gridXform = xformQuery.GetComponent(gridDockUid);
+                   var gridXform = _xformQuery.GetComponent(gridDockUid);
 
                    if (!CanDock(
                            shuttleDock, shuttleDockXform,
@@ -167,15 +193,15 @@ public sealed partial class DockingSystem
                    }
 
                    // Can't just use the AABB as we want to get bounds as tight as possible.
-                   var spawnPosition = new EntityCoordinates(targetGrid, matty.Transform(Vector2.Zero));
-                   spawnPosition = new EntityCoordinates(targetGridXform.MapUid!.Value, spawnPosition.ToMapPos(EntityManager, _transform));
+                   var gridPosition = new EntityCoordinates(targetGrid, matty.Transform(Vector2.Zero));
+                   var spawnPosition = new EntityCoordinates(targetGridXform.MapUid!.Value, gridPosition.ToMapPos(EntityManager, _transform));
 
                    // TODO: use tight bounds
                    var dockedBounds = new Box2Rotated(shuttleAABB.Translated(spawnPosition.Position), targetAngle, spawnPosition.Position);
 
                    // Check if there's no intersecting grids (AKA oh god it's docking at cargo).
                    grids.Clear();
-                   _mapManager.FindGridsIntersecting(targetGridXform.MapID, dockedBounds, ref grids);
+                   _mapManager.FindGridsIntersecting(targetGridXform.MapID, dockedBounds, ref grids, includeMap: false);
                    if (grids.Any(o => o.Owner != targetGrid && o.Owner != targetGridXform.MapUid))
                    {
                        continue;
@@ -204,9 +230,9 @@ public sealed partial class DockingSystem
 
                            if (!CanDock(
                                    other,
-                                   xformQuery.GetComponent(otherUid),
+                                   _xformQuery.GetComponent(otherUid),
                                    otherGrid,
-                                   xformQuery.GetComponent(otherGridUid),
+                                   _xformQuery.GetComponent(otherGridUid),
                                    shuttleAABB,
                                    targetGridAngle,
                                    shuttleFixturesComp, targetGridGrid,
@@ -234,7 +260,7 @@ public sealed partial class DockingSystem
                    validDockConfigs.Add(new DockingConfig()
                    {
                        Docks = dockedPorts,
-                       Coordinates = spawnPosition,
+                       Coordinates = gridPosition,
                        Area = dockedAABB,
                        Angle = targetAngle,
                    });
@@ -242,9 +268,23 @@ public sealed partial class DockingSystem
            }
         }
 
+        return validDockConfigs;
+   }
+
+   private DockingConfig? GetDockingConfigPrivate(
+       EntityUid shuttleUid,
+       EntityUid targetGrid,
+       List<(EntityUid, DockingComponent)> shuttleDocks,
+       List<(EntityUid, DockingComponent)> gridDocks,
+       string? priorityTag = null)
+   {
+       var validDockConfigs = GetDockingConfigs(shuttleUid, targetGrid, shuttleDocks, gridDocks);
+
         if (validDockConfigs.Count <= 0)
             return null;
 
+        var targetGridAngle = _transform.GetWorldRotation(targetGrid).Reduced();
+
         // Prioritise by priority docks, then by maximum connected ports, then by most similar angle.
         validDockConfigs = validDockConfigs
            .OrderByDescending(x => x.Docks.Any(docks =>
index 7f698850450edf01fef841463516ed365359b160..afe03af1bb8886bc0417c82d0d1be9c352cde793 100644 (file)
@@ -5,7 +5,10 @@ using Content.Server.Shuttles.Components;
 using Content.Server.Shuttles.Events;
 using Content.Shared.Doors;
 using Content.Shared.Doors.Components;
+using Content.Shared.Popups;
+using Content.Shared.Shuttles.Components;
 using Content.Shared.Shuttles.Events;
+using Content.Shared.Shuttles.Systems;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Physics;
@@ -17,27 +20,32 @@ using Robust.Shared.Utility;
 
 namespace Content.Server.Shuttles.Systems
 {
-    public sealed partial class DockingSystem : EntitySystem
+    public sealed partial class DockingSystem : SharedDockingSystem
     {
         [Dependency] private readonly IMapManager _mapManager = default!;
         [Dependency] private readonly DoorSystem _doorSystem = default!;
-        [Dependency] private readonly FixtureSystem _fixtureSystem = default!;
+        [Dependency] private readonly EntityLookupSystem _lookup = default!;
         [Dependency] private readonly PathfindingSystem _pathfinding = default!;
         [Dependency] private readonly ShuttleConsoleSystem _console = default!;
         [Dependency] private readonly SharedJointSystem _jointSystem = default!;
-        [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+        [Dependency] private readonly SharedPopupSystem _popup = default!;
         [Dependency] private readonly SharedTransformSystem _transform = default!;
 
-        private const string DockingFixture = "docking";
         private const string DockingJoint = "docking";
-        private const float DockingRadius = 0.20f;
 
+        private EntityQuery<MapGridComponent> _gridQuery;
         private EntityQuery<PhysicsComponent> _physicsQuery;
+        private EntityQuery<TransformComponent> _xformQuery;
+
+        private readonly HashSet<Entity<DockingComponent>> _dockingSet = new();
+        private readonly HashSet<Entity<DockingComponent, DoorBoltComponent>> _dockingBoltSet = new();
 
         public override void Initialize()
         {
             base.Initialize();
+            _gridQuery = GetEntityQuery<MapGridComponent>();
             _physicsQuery = GetEntityQuery<PhysicsComponent>();
+            _xformQuery = GetEntityQuery<TransformComponent>();
 
             SubscribeLocalEvent<DockingComponent, ComponentStartup>(OnStartup);
             SubscribeLocalEvent<DockingComponent, ComponentShutdown>(OnShutdown);
@@ -48,95 +56,42 @@ namespace Content.Server.Shuttles.Systems
 
             // Yes this isn't in shuttle console; it may be used by other systems technically.
             // in which case I would also add their subs here.
-            SubscribeLocalEvent<ShuttleConsoleComponent, AutodockRequestMessage>(OnRequestAutodock);
-            SubscribeLocalEvent<ShuttleConsoleComponent, StopAutodockRequestMessage>(OnRequestStopAutodock);
+            SubscribeLocalEvent<ShuttleConsoleComponent, DockRequestMessage>(OnRequestDock);
             SubscribeLocalEvent<ShuttleConsoleComponent, UndockRequestMessage>(OnRequestUndock);
         }
 
-        public override void Update(float frameTime)
-        {
-            base.Update(frameTime);
-            UpdateAutodock();
-        }
-
-        private void OnAutoClose(EntityUid uid, DockingComponent component, BeforeDoorAutoCloseEvent args)
-        {
-            // We'll just pin the door open when docked.
-            if (component.Docked)
-                args.Cancel();
-        }
-
-        private Entity<DockingComponent>? GetDockable(EntityUid uid, TransformComponent dockingXform)
+        /// <summary>
+        /// Sets the docks for the provided entity as enabled or disabled.
+        /// </summary>
+        public void SetDocks(EntityUid gridUid, bool enabled)
         {
-            // Did you know Saltern is the most dockable station?
+            _dockingSet.Clear();
+            _lookup.GetChildEntities(gridUid, _dockingSet);
 
-            // Assume the docking port itself (and its body) is valid
-
-            if (!HasComp<ShuttleComponent>(dockingXform.GridUid))
+            foreach (var dock in _dockingSet)
             {
-                return null;
+                Undock(dock);
+                dock.Comp.Enabled = enabled;
             }
+        }
 
-            var transform = _physics.GetPhysicsTransform(uid, dockingXform);
-            var dockingFixture = _fixtureSystem.GetFixtureOrNull(uid, DockingFixture);
-
-            if (dockingFixture == null)
-                return null;
-
-            Box2? aabb = null;
-
-            for (var i = 0; i < dockingFixture.Shape.ChildCount; i++)
-            {
-                aabb = aabb?.Union(dockingFixture.Shape.ComputeAABB(transform, i)) ?? dockingFixture.Shape.ComputeAABB(transform, i);
-            }
-
-            if (aabb == null)
-                return null;
-
-            var enlargedAABB = aabb.Value.Enlarged(DockingRadius * 1.5f);
+        public void SetDockBolts(EntityUid gridUid, bool enabled)
+        {
+            _dockingBoltSet.Clear();
+            _lookup.GetChildEntities(gridUid, _dockingBoltSet);
 
-            // Get any docking ports in range on other grids.
-            var grids = new List<Entity<MapGridComponent>>();
-            _mapManager.FindGridsIntersecting(dockingXform.MapID, enlargedAABB, ref grids);
-            foreach (var otherGrid in grids)
+            foreach (var entity in _dockingBoltSet)
             {
-                if (otherGrid.Owner == dockingXform.GridUid)
-                    continue;
-
-                foreach (var ent in otherGrid.Comp.GetAnchoredEntities(enlargedAABB))
-                {
-                    if (!TryComp(ent, out DockingComponent? otherDocking) ||
-                        !otherDocking.Enabled ||
-                        !TryComp(ent, out FixturesComponent? otherBody))
-                    {
-                        continue;
-                    }
-
-                    var otherTransform = _physics.GetPhysicsTransform(ent);
-                    var otherDockingFixture = _fixtureSystem.GetFixtureOrNull(ent, DockingFixture, manager: otherBody);
-
-                    if (otherDockingFixture == null)
-                    {
-                        DebugTools.Assert(false);
-                        Log.Error($"Found null docking fixture on {ent}");
-                        continue;
-                    }
-
-                    for (var i = 0; i < otherDockingFixture.Shape.ChildCount; i++)
-                    {
-                        var otherAABB = otherDockingFixture.Shape.ComputeAABB(otherTransform, i);
-
-                        if (!aabb.Value.Intersects(otherAABB))
-                            continue;
-
-                        // TODO: Need CollisionManager's GJK for accurate bounds
-                        // Realistically I want 2 fixtures anyway but I'll deal with that later.
-                        return (ent, otherDocking);
-                    }
-                }
+                _doorSystem.TryClose(entity);
+                _doorSystem.SetBoltsDown((entity.Owner, entity.Comp2), enabled);
             }
+        }
 
-            return null;
+        private void OnAutoClose(EntityUid uid, DockingComponent component, BeforeDoorAutoCloseEvent args)
+        {
+            // We'll just pin the door open when docked.
+            if (component.Docked)
+                args.Cancel();
         }
 
         private void OnShutdown(EntityUid uid, DockingComponent component, ComponentShutdown args)
@@ -147,6 +102,13 @@ namespace Content.Server.Shuttles.Systems
                 return;
             }
 
+            var gridUid = Transform(uid).GridUid;
+
+            if (gridUid != null && !Terminating(gridUid.Value))
+            {
+                _console.RefreshShuttleConsoles();
+            }
+
             Cleanup(uid, component);
         }
 
@@ -166,12 +128,6 @@ namespace Content.Server.Shuttles.Systems
                 Log.Error($"Tried to cleanup {dockAUid} but not docked?");
 
                 dockA.DockedWith = null;
-                if (dockA.DockJoint != null)
-                {
-                    // We'll still cleanup the dock joint on release at least
-                    _jointSystem.RemoveJoint(dockA.DockJoint);
-                }
-
                 return;
             }
 
@@ -200,12 +156,16 @@ namespace Content.Server.Shuttles.Systems
             RaiseLocalEvent(msg);
         }
 
-        private void OnStartup(EntityUid uid, DockingComponent component, ComponentStartup args)
+        private void OnStartup(Entity<DockingComponent> entity, ref ComponentStartup args)
         {
+            var uid = entity.Owner;
+            var component = entity.Comp;
+
             // Use startup so transform already initialized
-            if (!EntityManager.GetComponent<TransformComponent>(uid).Anchored) return;
+            if (!EntityManager.GetComponent<TransformComponent>(uid).Anchored)
+                return;
 
-            EnableDocking(uid, component);
+            SetDockingEnabled((uid, component), true);
 
             // This little gem is for docking deserialization
             if (component.DockedWith != null)
@@ -217,75 +177,62 @@ namespace Content.Server.Shuttles.Systems
                 var otherDock = EntityManager.GetComponent<DockingComponent>(component.DockedWith.Value);
                 DebugTools.Assert(otherDock.DockedWith != null);
 
-                Dock(uid, component, component.DockedWith.Value, otherDock);
+                Dock((uid, component), (component.DockedWith.Value, otherDock));
                 DebugTools.Assert(component.Docked && otherDock.Docked);
             }
         }
 
-        private void OnAnchorChange(EntityUid uid, DockingComponent component, ref AnchorStateChangedEvent args)
+        private void OnAnchorChange(Entity<DockingComponent> entity, ref AnchorStateChangedEvent args)
         {
             if (args.Anchored)
             {
-                EnableDocking(uid, component);
+                SetDockingEnabled(entity, true);
             }
             else
             {
-                DisableDocking(uid, component);
+                SetDockingEnabled(entity, false);
             }
 
             _console.RefreshShuttleConsoles();
         }
 
-        private void OnDockingReAnchor(EntityUid uid, DockingComponent component, ref ReAnchorEvent args)
+        private void OnDockingReAnchor(Entity<DockingComponent> entity, ref ReAnchorEvent args)
         {
+            var uid = entity.Owner;
+            var component = entity.Comp;
+
             if (!component.Docked)
                 return;
 
             var otherDock = component.DockedWith;
             var other = Comp<DockingComponent>(otherDock!.Value);
 
-            Undock(uid, component);
-            Dock(uid, component, otherDock.Value, other);
+            Undock(entity);
+            Dock((uid, component), (otherDock.Value, other));
             _console.RefreshShuttleConsoles();
         }
 
-        private void DisableDocking(EntityUid uid, DockingComponent component)
+        public void SetDockingEnabled(Entity<DockingComponent> entity, bool value)
         {
-            if (!component.Enabled)
+            if (entity.Comp.Enabled == value)
                 return;
 
-            component.Enabled = false;
+            entity.Comp.Enabled = value;
 
-            if (component.DockedWith != null)
+            if (!entity.Comp.Enabled && entity.Comp.DockedWith != null)
             {
-                Undock(uid, component);
+                Undock(entity);
             }
         }
 
-        private void EnableDocking(EntityUid uid, DockingComponent component)
-        {
-            if (component.Enabled)
-                return;
-
-            if (!TryComp(uid, out PhysicsComponent? physicsComponent))
-                return;
-
-            component.Enabled = true;
-
-            var shape = new PhysShapeCircle(DockingRadius, new Vector2(0f, -0.5f));
-
-            // Listen it makes intersection tests easier; you can probably dump this but it requires a bunch more boilerplate
-            // TODO: I want this to ideally be 2 fixtures to force them to have some level of alignment buuuttt
-            // I also need collisionmanager for that yet again so they get dis.
-            // TODO: CollisionManager is fine so get to work sloth chop chop.
-            _fixtureSystem.TryCreateFixture(uid, shape, DockingFixture, hard: false, body: physicsComponent);
-        }
-
         /// <summary>
         /// Docks 2 ports together and assumes it is valid.
         /// </summary>
-        public void Dock(EntityUid dockAUid, DockingComponent dockA, EntityUid dockBUid, DockingComponent dockB)
+        public void Dock(Entity<DockingComponent> dockA, Entity<DockingComponent> dockB)
         {
+            var dockAUid = dockA.Owner;
+            var dockBUid = dockB.Owner;
+
             if (dockBUid.GetHashCode() < dockAUid.GetHashCode())
             {
                 (dockA, dockB) = (dockB, dockA);
@@ -322,10 +269,10 @@ namespace Content.Server.Shuttles.Systems
                 WeldJoint joint;
 
                 // Pre-existing joint so use that.
-                if (dockA.DockJointId != null)
+                if (dockA.Comp.DockJointId != null)
                 {
-                    DebugTools.Assert(dockB.DockJointId == dockA.DockJointId);
-                    joint = _jointSystem.GetOrCreateWeldJoint(gridA, gridB, dockA.DockJointId);
+                    DebugTools.Assert(dockB.Comp.DockJointId == dockA.Comp.DockJointId);
+                    joint = _jointSystem.GetOrCreateWeldJoint(gridA, gridB, dockA.Comp.DockJointId);
                 }
                 else
                 {
@@ -345,15 +292,15 @@ namespace Content.Server.Shuttles.Systems
                 joint.Stiffness = stiffness;
                 joint.Damping = damping;
 
-                dockA.DockJoint = joint;
-                dockA.DockJointId = joint.ID;
+                dockA.Comp.DockJoint = joint;
+                dockA.Comp.DockJointId = joint.ID;
 
-                dockB.DockJoint = joint;
-                dockB.DockJointId = joint.ID;
+                dockB.Comp.DockJoint = joint;
+                dockB.Comp.DockJointId = joint.ID;
             }
 
-            dockA.DockedWith = dockBUid;
-            dockB.DockedWith = dockAUid;
+            dockA.Comp.DockedWith = dockBUid;
+            dockB.Comp.DockedWith = dockAUid;
 
             if (TryComp(dockAUid, out DoorComponent? doorA))
             {
@@ -381,8 +328,8 @@ namespace Content.Server.Shuttles.Systems
 
             if (_pathfinding.TryCreatePortal(dockAXform.Coordinates, dockBXform.Coordinates, out var handle))
             {
-                dockA.PathfindHandle = handle;
-                dockB.PathfindHandle = handle;
+                dockA.Comp.PathfindHandle = handle;
+                dockB.Comp.PathfindHandle = handle;
             }
 
             var msg = new DockEvent
@@ -393,89 +340,135 @@ namespace Content.Server.Shuttles.Systems
                 GridBUid = gridB,
             };
 
+            _console.RefreshShuttleConsoles();
             RaiseLocalEvent(dockAUid, msg);
             RaiseLocalEvent(dockBUid, msg);
             RaiseLocalEvent(msg);
         }
 
-        private bool CanDock(EntityUid dockAUid, EntityUid dockBUid, DockingComponent dockA, DockingComponent dockB)
+        /// <summary>
+        /// Attempts to dock 2 ports together and will return early if it's not possible.
+        /// </summary>
+        private void TryDock(Entity<DockingComponent> dockA, Entity<DockingComponent> dockB)
         {
-            if (!dockA.Enabled ||
-                !dockB.Enabled ||
-                dockA.DockedWith != null ||
-                dockB.DockedWith != null)
-            {
-                return false;
-            }
+            if (!CanDock(dockA, dockB))
+                return;
+
+            Dock(dockA, dockB);
+        }
 
-            var fixtureA = _fixtureSystem.GetFixtureOrNull(dockAUid, DockingFixture);
-            var fixtureB = _fixtureSystem.GetFixtureOrNull(dockBUid, DockingFixture);
+        public void Undock(Entity<DockingComponent> dock)
+        {
+            if (dock.Comp.DockedWith == null)
+                return;
+
+            OnUndock(dock.Owner);
+            OnUndock(dock.Comp.DockedWith.Value);
+            Cleanup(dock.Owner, dock);
+            _console.RefreshShuttleConsoles();
+        }
+
+        private void OnUndock(EntityUid dockUid)
+        {
+            if (TerminatingOrDeleted(dockUid))
+                return;
 
-            if (fixtureA == null || fixtureB == null)
+            if (TryComp<DoorBoltComponent>(dockUid, out var airlock))
+                _doorSystem.SetBoltsDown((dockUid, airlock), false);
+
+            if (TryComp(dockUid, out DoorComponent? door) && _doorSystem.TryClose(dockUid, door))
+                door.ChangeAirtight = true;
+        }
+
+        private void OnRequestUndock(EntityUid uid, ShuttleConsoleComponent component, UndockRequestMessage args)
+        {
+            if (!TryGetEntity(args.DockEntity, out var dockEnt) ||
+                !TryComp(dockEnt, out DockingComponent? dockComp))
             {
-                return false;
+                _popup.PopupCursor(Loc.GetString("shuttle-console-undock-fail"));
+                return;
             }
 
-            var transformA = _physics.GetPhysicsTransform(dockAUid);
-            var transformB = _physics.GetPhysicsTransform(dockBUid);
-            var intersect = false;
+            var dock = (dockEnt.Value, dockComp);
 
-            for (var i = 0; i < fixtureA.Shape.ChildCount; i++)
+            if (!CanUndock(dock))
             {
-                var aabb = fixtureA.Shape.ComputeAABB(transformA, i);
+                _popup.PopupCursor(Loc.GetString("shuttle-console-undock-fail"));
+                return;
+            }
 
-                for (var j = 0; j < fixtureB.Shape.ChildCount; j++)
-                {
-                    var otherAABB = fixtureB.Shape.ComputeAABB(transformB, j);
-                    if (!aabb.Intersects(otherAABB))
-                        continue;
+            Undock(dock);
+        }
 
-                    // TODO: Need collisionmanager's GJK for accurate checks don't @ me son
-                    intersect = true;
-                    break;
-                }
+        private void OnRequestDock(EntityUid uid, ShuttleConsoleComponent component, DockRequestMessage args)
+        {
+            var shuttleUid = Transform(uid).GridUid;
 
-                if (intersect)
-                    break;
+            if (!CanShuttleDock(shuttleUid))
+            {
+                _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail"));
+                return;
             }
 
-            return intersect;
-        }
+            if (!TryGetEntity(args.DockEntity, out var ourDock) ||
+                !TryGetEntity(args.TargetDockEntity, out var targetDock) ||
+                !TryComp(ourDock, out DockingComponent? ourDockComp) ||
+                !TryComp(targetDock, out DockingComponent? targetDockComp))
+            {
+                _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail"));
+                return;
+            }
 
-        /// <summary>
-        /// Attempts to dock 2 ports together and will return early if it's not possible.
-        /// </summary>
-        private void TryDock(EntityUid dockAUid, DockingComponent dockA, Entity<DockingComponent> dockB)
-        {
-            if (!CanDock(dockAUid, dockB, dockA, dockB))
+            // Cheating?
+            if (!TryComp(ourDock, out TransformComponent? xformA) ||
+                xformA.GridUid != shuttleUid)
+            {
+                _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail"));
+                return;
+            }
+
+            // TODO: Move the CanDock stuff to the port state and also validate that stuff
+            // Also need to check preventpilot + enabled / dockedwith
+            if (!CanDock((ourDock.Value, ourDockComp), (targetDock.Value, targetDockComp)))
+            {
+                _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail"));
                 return;
+            }
 
-            Dock(dockAUid, dockA, dockB, dockB);
+            Dock((ourDock.Value, ourDockComp), (targetDock.Value, targetDockComp));
         }
 
-        public void Undock(EntityUid dockUid, DockingComponent dock)
+        public bool CanUndock(Entity<DockingComponent?> dock)
         {
-            if (dock.DockedWith == null)
-                return;
+            if (!Resolve(dock, ref dock.Comp) ||
+                !dock.Comp.Docked)
+            {
+                return false;
+            }
 
-            OnUndock(dockUid, dock.DockedWith.Value);
-            OnUndock(dock.DockedWith.Value, dockUid);
-            Cleanup(dockUid, dock);
+            return true;
         }
 
-        private void OnUndock(EntityUid dockUid, EntityUid other)
+        /// <summary>
+        /// Returns true if both docks can connect. Does not consider whether the shuttle allows it.
+        /// </summary>
+        public bool CanDock(Entity<DockingComponent> dockA, Entity<DockingComponent> dockB)
         {
-            if (TerminatingOrDeleted(dockUid))
-                return;
-
-            if (TryComp<DoorBoltComponent>(dockUid, out var airlock))
-                _doorSystem.SetBoltsDown((dockUid, airlock), false);
+            if (!dockA.Comp.Enabled ||
+                !dockB.Comp.Enabled ||
+                dockA.Comp.DockedWith != null ||
+                dockB.Comp.DockedWith != null)
+            {
+                return false;
+            }
 
-            if (TryComp(dockUid, out DoorComponent? door) && _doorSystem.TryClose(dockUid, door))
-                door.ChangeAirtight = true;
+            var xformA = Transform(dockA);
+            var xformB = Transform(dockB);
+            var (worldPosA, worldRotA) = XformSystem.GetWorldPositionRotation(xformA);
+            var (worldPosB, worldRotB) = XformSystem.GetWorldPositionRotation(xformB);
 
-            var recentlyDocked = EnsureComp<RecentlyDockedComponent>(dockUid);
-            recentlyDocked.LastDocked = other;
+            return CanDock(new MapCoordinates(worldPosA, xformA.MapID), worldRotA,
+                new MapCoordinates(worldPosB, xformB.MapID), worldRotB);
         }
     }
 }
index 6fb939d94ad22732a08877e652cd491883ec143f..a28ae07b8a8ddf0e1a9f928e12a919dfe2a410c0 100644 (file)
@@ -163,15 +163,15 @@ public sealed partial class EmergencyShuttleSystem
 
                 if (!Deleted(centcomm.Entity))
                 {
-                    _shuttle.FTLTravel(comp.EmergencyShuttle.Value, shuttle,
-                        centcomm.Entity.Value, _consoleAccumulator, TransitTime, true);
+                    _shuttle.FTLToDock(comp.EmergencyShuttle.Value, shuttle,
+                        centcomm.Entity.Value, _consoleAccumulator, TransitTime);
                     continue;
                 }
 
                 if (!Deleted(centcomm.MapEntity))
                 {
                     // TODO: Need to get non-overlapping positions.
-                    _shuttle.FTLTravel(comp.EmergencyShuttle.Value, shuttle,
+                    _shuttle.FTLToCoordinates(comp.EmergencyShuttle.Value, shuttle,
                         new EntityCoordinates(centcomm.MapEntity.Value,
                             _random.NextVector2(1000f)), _consoleAccumulator, TransitTime);
                 }
@@ -201,7 +201,7 @@ public sealed partial class EmergencyShuttleSystem
             }
 
             // Don't dock them. If you do end up doing this then stagger launch.
-            _shuttle.FTLTravel(uid, shuttle, centcomm.Entity.Value, hyperspaceTime: TransitTime);
+            _shuttle.FTLToDock(uid, shuttle, centcomm.Entity.Value, hyperspaceTime: TransitTime);
             RemCompDeferred<EscapePodComponent>(uid);
         }
 
@@ -217,15 +217,18 @@ public sealed partial class EmergencyShuttleSystem
         // All the others.
         if (_consoleAccumulator < minTime)
         {
-            var query = AllEntityQuery<StationCentcommComponent>();
+            var query = AllEntityQuery<StationCentcommComponent, TransformComponent>();
 
             // Guarantees that emergency shuttle arrives first before anyone else can FTL.
-            while (query.MoveNext(out var comp))
+            while (query.MoveNext(out var comp, out var centcommXform))
             {
                 if (Deleted(comp.Entity))
                     continue;
 
-                _shuttle.AddFTLDestination(comp.Entity.Value, true);
+                if (_shuttle.TryAddFTLDestination(centcommXform.MapID, true, out var ftlComp))
+                {
+                    _shuttle.SetFTLWhitelist((centcommXform.MapUid!.Value, ftlComp), null);
+                }
             }
         }
     }
index 8b2c268300b6fb00b19844caf9644637a2181c43..a7df41d887720419706f709fc4784f9b96fcc6df 100644 (file)
@@ -445,7 +445,7 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem
 
         component.MapEntity = map;
         component.Entity = grid;
-        _shuttle.AddFTLDestination(grid.Value, false);
+        _shuttle.TryAddFTLDestination(mapId, false, out _);
     }
 
     public HashSet<EntityUid> GetCentcommMaps()
index fb32437c6e935255ee86c434dc92d58e7cbfd31c..b7f08b4b349f89218e44273734126c000b38d6de 100644 (file)
@@ -12,6 +12,7 @@ namespace Content.Server.Shuttles.Systems;
 
 public sealed class RadarConsoleSystem : SharedRadarConsoleSystem
 {
+    [Dependency] private readonly ShuttleConsoleSystem _console = default!;
     [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
 
     public override void Initialize()
@@ -39,11 +40,20 @@ public sealed class RadarConsoleSystem : SharedRadarConsoleSystem
         }
 
         if (_uiSystem.TryGetUi(uid, RadarConsoleUiKey.Key, out var bui))
-            _uiSystem.SetUiState(bui, new RadarConsoleBoundInterfaceState(
-                component.MaxRange,
-                GetNetCoordinates(coordinates),
-                angle,
-                new List<DockingInterfaceState>()
-            ));
+        {
+            NavInterfaceState state;
+            var docks = _console.GetAllDocks();
+
+            if (coordinates != null && angle != null)
+            {
+                state = _console.GetNavState(uid, docks, coordinates.Value, angle.Value);
+            }
+            else
+            {
+                state = _console.GetNavState(uid, docks);
+            }
+
+            _uiSystem.SetUiState(bui, new NavBoundUserInterfaceState(state));
+        }
     }
 }
index 99ab54f9afa8dfb3d1466ae181b8bc90a912338a..2970567b3ee3670ddafdc07f73153313fb9f80da 100644 (file)
@@ -7,6 +7,20 @@ namespace Content.Server.Shuttles.Systems;
 
 public sealed partial class ShuttleConsoleSystem
 {
+    /// <summary>
+    /// Gets the drone console target if applicable otherwise returns itself.
+    /// </summary>
+    private EntityUid? GetDroneConsole(EntityUid consoleUid)
+    {
+        var getShuttleEv = new ConsoleShuttleEvent
+        {
+            Console = consoleUid,
+        };
+
+        RaiseLocalEvent(consoleUid, ref getShuttleEv);
+        return getShuttleEv.Console;
+    }
+
     /// <summary>
     /// Refreshes all drone console entities.
     /// </summary>
diff --git a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.FTL.cs b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.FTL.cs
new file mode 100644 (file)
index 0000000..ba18889
--- /dev/null
@@ -0,0 +1,160 @@
+using Content.Server.Shuttles.Components;
+using Content.Server.Shuttles.Events;
+using Content.Shared.Shuttles.BUIStates;
+using Content.Shared.Shuttles.Components;
+using Content.Shared.Shuttles.Events;
+using Content.Shared.Shuttles.UI.MapObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics.Components;
+
+namespace Content.Server.Shuttles.Systems;
+
+public sealed partial class ShuttleConsoleSystem
+{
+    private void InitializeFTL()
+    {
+        SubscribeLocalEvent<FTLBeaconComponent, ComponentStartup>(OnBeaconStartup);
+        SubscribeLocalEvent<FTLBeaconComponent, AnchorStateChangedEvent>(OnBeaconAnchorChanged);
+
+        SubscribeLocalEvent<FTLExclusionComponent, ComponentStartup>(OnExclusionStartup);
+    }
+
+    private void OnExclusionStartup(Entity<FTLExclusionComponent> ent, ref ComponentStartup args)
+    {
+        RefreshShuttleConsoles();
+    }
+
+    private void OnBeaconStartup(Entity<FTLBeaconComponent> ent, ref ComponentStartup args)
+    {
+        RefreshShuttleConsoles();
+    }
+
+    private void OnBeaconAnchorChanged(Entity<FTLBeaconComponent> ent, ref AnchorStateChangedEvent args)
+    {
+        RefreshShuttleConsoles();
+    }
+
+    private void OnBeaconFTLMessage(Entity<ShuttleConsoleComponent> ent, ref ShuttleConsoleFTLBeaconMessage args)
+    {
+        var beaconEnt = GetEntity(args.Beacon);
+        if (!_xformQuery.TryGetComponent(beaconEnt, out var targetXform))
+        {
+            return;
+        }
+
+        var nCoordinates = new NetCoordinates(GetNetEntity(targetXform.ParentUid), targetXform.LocalPosition);
+
+        // Check target exists
+        if (!_shuttle.CanFTLBeacon(nCoordinates))
+        {
+            return;
+        }
+
+        var angle = args.Angle.Reduced();
+        var targetCoordinates = new EntityCoordinates(targetXform.MapUid!.Value, _transform.GetWorldPosition(targetXform));
+
+        ConsoleFTL(ent, true, targetCoordinates, angle, targetXform.MapID);
+    }
+
+    private void OnPositionFTLMessage(Entity<ShuttleConsoleComponent> entity, ref ShuttleConsoleFTLPositionMessage args)
+    {
+        var mapUid = _mapManager.GetMapEntityId(args.Coordinates.MapId);
+
+        // If it's beacons only block all position messages.
+        if (!Exists(mapUid) || _shuttle.IsBeaconMap(mapUid))
+        {
+            return;
+        }
+
+        var targetCoordinates = new EntityCoordinates(mapUid, args.Coordinates.Position);
+        var angle = args.Angle.Reduced();
+        ConsoleFTL(entity, false, targetCoordinates, angle, args.Coordinates.MapId);
+    }
+
+    private void GetBeacons(ref List<ShuttleBeaconObject>? beacons)
+    {
+        var beaconQuery = AllEntityQuery<FTLBeaconComponent>();
+
+        while (beaconQuery.MoveNext(out var destUid, out _))
+        {
+            var meta = _metaQuery.GetComponent(destUid);
+            var name = meta.EntityName;
+
+            if (string.IsNullOrEmpty(name))
+                name = Loc.GetString("shuttle-console-unknown");
+
+            // Can't travel to same map (yet)
+            var destXform = _xformQuery.GetComponent(destUid);
+            beacons ??= new List<ShuttleBeaconObject>();
+            beacons.Add(new ShuttleBeaconObject(GetNetEntity(destUid), GetNetCoordinates(destXform.Coordinates), name));
+        }
+    }
+
+    private void GetExclusions(ref List<ShuttleExclusionObject>? exclusions)
+    {
+        var query = AllEntityQuery<FTLExclusionComponent, TransformComponent>();
+
+        while (query.MoveNext(out var uid, out var comp, out var xform))
+        {
+            if (!comp.Enabled)
+                continue;
+
+            exclusions ??= new List<ShuttleExclusionObject>();
+            exclusions.Add(new ShuttleExclusionObject(GetNetCoordinates(xform.Coordinates), comp.Range, Loc.GetString("shuttle-console-exclusion")));
+        }
+    }
+
+    /// <summary>
+    /// Handles shuttle console FTLs.
+    /// </summary>
+    private void ConsoleFTL(Entity<ShuttleConsoleComponent> ent, bool beacon, EntityCoordinates targetCoordinates, Angle targetAngle, MapId targetMap)
+    {
+        var consoleUid = GetDroneConsole(ent.Owner);
+
+        if (consoleUid == null)
+            return;
+
+        var shuttleUid = _xformQuery.GetComponent(consoleUid.Value).GridUid;
+
+        if (!TryComp(shuttleUid, out ShuttleComponent? shuttleComp))
+            return;
+
+        // Check shuttle can even FTL
+        if (!_shuttle.CanFTL(shuttleUid.Value, out var reason))
+        {
+            // TODO: Session popup
+            return;
+        }
+
+        // Check shuttle can FTL to this target.
+        if (!_shuttle.CanFTLTo(shuttleUid.Value, targetMap))
+        {
+            return;
+        }
+
+        List<ShuttleExclusionObject>? exclusions = null;
+        GetExclusions(ref exclusions);
+
+        if (!beacon && !_shuttle.FTLFree(shuttleUid.Value, targetCoordinates, targetAngle, exclusions))
+        {
+            return;
+        }
+
+        if (!TryComp(shuttleUid.Value, out PhysicsComponent? shuttlePhysics))
+        {
+            return;
+        }
+
+        // Client sends the "adjusted" coordinates and we adjust it back to get the actual transform coordinates.
+        var adjustedCoordinates = targetCoordinates.Offset(targetAngle.RotateVec(-shuttlePhysics.LocalCenter));
+
+        var tagEv = new FTLTagEvent();
+        RaiseLocalEvent(shuttleUid.Value, ref tagEv);
+
+        var ev = new ShuttleConsoleFTLTravelStartEvent(ent.Owner);
+        RaiseLocalEvent(ref ev);
+
+        _shuttle.FTLToCoordinates(shuttleUid.Value, shuttleComp, adjustedCoordinates, targetAngle);
+    }
+}
index 18dd3b0baf04aaed763dc3aedec88496ae73ea28..c47c519d5de2145e4163b76ce232d73401b18bc9 100644 (file)
@@ -3,7 +3,6 @@ using Content.Server.Power.EntitySystems;
 using Content.Server.Shuttles.Components;
 using Content.Server.Shuttles.Events;
 using Content.Server.Station.Systems;
-using Content.Server.UserInterface;
 using Content.Shared.ActionBlocker;
 using Content.Shared.Alert;
 using Content.Shared.Popups;
@@ -13,12 +12,11 @@ using Content.Shared.Shuttles.Events;
 using Content.Shared.Shuttles.Systems;
 using Content.Shared.Tag;
 using Content.Shared.Movement.Systems;
+using Content.Shared.Shuttles.UI.MapObjects;
 using Robust.Server.GameObjects;
 using Robust.Shared.Collections;
 using Robust.Shared.GameStates;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Physics.Components;
-using Robust.Shared.Timing;
+using Robust.Shared.Map;
 using Robust.Shared.Utility;
 using Content.Shared.UserInterface;
 
@@ -26,27 +24,38 @@ namespace Content.Server.Shuttles.Systems;
 
 public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
 {
-    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly IMapManager _mapManager = default!;
     [Dependency] private readonly ActionBlockerSystem _blocker = default!;
     [Dependency] private readonly AlertsSystem _alertsSystem = default!;
+    [Dependency] private readonly EntityLookupSystem _lookup = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
     [Dependency] private readonly ShuttleSystem _shuttle = default!;
     [Dependency] private readonly StationSystem _station = default!;
     [Dependency] private readonly TagSystem _tags = default!;
     [Dependency] private readonly UserInterfaceSystem _ui = default!;
     [Dependency] private readonly SharedContentEyeSystem _eyeSystem = default!;
 
+    private EntityQuery<MetaDataComponent> _metaQuery;
+    private EntityQuery<TransformComponent> _xformQuery;
+
+    private readonly HashSet<Entity<ShuttleConsoleComponent>> _consoles = new();
+
     public override void Initialize()
     {
         base.Initialize();
 
+        _metaQuery = GetEntityQuery<MetaDataComponent>();
+        _xformQuery = GetEntityQuery<TransformComponent>();
+
         SubscribeLocalEvent<ShuttleConsoleComponent, ComponentShutdown>(OnConsoleShutdown);
         SubscribeLocalEvent<ShuttleConsoleComponent, PowerChangedEvent>(OnConsolePowerChange);
         SubscribeLocalEvent<ShuttleConsoleComponent, AnchorStateChangedEvent>(OnConsoleAnchorChange);
         SubscribeLocalEvent<ShuttleConsoleComponent, ActivatableUIOpenAttemptEvent>(OnConsoleUIOpenAttempt);
         Subs.BuiEvents<ShuttleConsoleComponent>(ShuttleConsoleUiKey.Key, subs =>
         {
-            subs.Event<ShuttleConsoleFTLRequestMessage>(OnDestinationMessage);
+            subs.Event<ShuttleConsoleFTLBeaconMessage>(OnBeaconFTLMessage);
+            subs.Event<ShuttleConsoleFTLPositionMessage>(OnPositionFTLMessage);
             subs.Event<BoundUIClosedEvent>(OnConsoleUIClose);
         });
 
@@ -60,11 +69,12 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
         SubscribeLocalEvent<DockEvent>(OnDock);
         SubscribeLocalEvent<UndockEvent>(OnUndock);
 
-        SubscribeLocalEvent<PilotComponent, MoveEvent>(HandlePilotMove);
         SubscribeLocalEvent<PilotComponent, ComponentGetState>(OnGetState);
 
         SubscribeLocalEvent<FTLDestinationComponent, ComponentStartup>(OnFtlDestStartup);
         SubscribeLocalEvent<FTLDestinationComponent, ComponentShutdown>(OnFtlDestShutdown);
+
+        InitializeFTL();
     }
 
     private void OnFtlDestStartup(EntityUid uid, FTLDestinationComponent component, ComponentStartup args)
@@ -77,65 +87,6 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
         RefreshShuttleConsoles();
     }
 
-    private void OnDestinationMessage(EntityUid uid, ShuttleConsoleComponent component,
-        ShuttleConsoleFTLRequestMessage args)
-    {
-        var destination = GetEntity(args.Destination);
-
-        if (!TryComp<FTLDestinationComponent>(destination, out var dest))
-        {
-            return;
-        }
-
-        if (!dest.Enabled)
-            return;
-
-        EntityUid? entity = uid;
-
-        var getShuttleEv = new ConsoleShuttleEvent
-        {
-            Console = uid,
-        };
-
-        RaiseLocalEvent(entity.Value, ref getShuttleEv);
-        entity = getShuttleEv.Console;
-
-        if (!TryComp<TransformComponent>(entity, out var xform) ||
-            !TryComp<ShuttleComponent>(xform.GridUid, out var shuttle))
-        {
-            return;
-        }
-
-        if (dest.Whitelist?.IsValid(entity.Value, EntityManager) == false &&
-            dest.Whitelist?.IsValid(xform.GridUid.Value, EntityManager) == false)
-        {
-            return;
-        }
-
-        var shuttleUid = xform.GridUid.Value;
-
-        if (HasComp<FTLComponent>(shuttleUid))
-        {
-            _popup.PopupCursor(Loc.GetString("shuttle-console-in-ftl"), args.Session);
-            return;
-        }
-
-        if (!_shuttle.CanFTL(xform.GridUid, out var reason))
-        {
-            _popup.PopupCursor(reason, args.Session);
-            return;
-        }
-
-        var dock = HasComp<MapComponent>(destination) && HasComp<MapGridComponent>(destination);
-        var tagEv = new FTLTagEvent();
-        RaiseLocalEvent(xform.GridUid.Value, ref tagEv);
-
-        var ev = new ShuttleConsoleFTLTravelStartEvent(uid);
-        RaiseLocalEvent(ref ev);
-
-        _shuttle.FTLTravel(xform.GridUid.Value, shuttle, destination, dock: dock, priorityTag: tagEv.Tag);
-    }
-
     private void OnDock(DockEvent ev)
     {
         RefreshShuttleConsoles();
@@ -146,10 +97,21 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
         RefreshShuttleConsoles();
     }
 
-    public void RefreshShuttleConsoles(EntityUid _)
+    /// <summary>
+    /// Refreshes all the shuttle console data for a particular grid.
+    /// </summary>
+    public void RefreshShuttleConsoles(EntityUid gridUid)
     {
-        // TODO: Should really call this per shuttle in some instances.
-        RefreshShuttleConsoles();
+        var exclusions = new List<ShuttleExclusionObject>();
+        GetExclusions(ref exclusions);
+        _consoles.Clear();
+        _lookup.GetChildEntities(gridUid, _consoles);
+        DockingInterfaceState? dockState = null;
+
+        foreach (var entity in _consoles)
+        {
+            UpdateState(entity, ref dockState);
+        }
     }
 
     /// <summary>
@@ -157,12 +119,14 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
     /// </summary>
     public void RefreshShuttleConsoles()
     {
-        var docks = GetAllDocks();
+        var exclusions = new List<ShuttleExclusionObject>();
+        GetExclusions(ref exclusions);
         var query = AllEntityQuery<ShuttleConsoleComponent>();
+        DockingInterfaceState? dockState = null;
 
-        while (query.MoveNext(out var uid, out var _))
+        while (query.MoveNext(out var uid, out _))
         {
-            UpdateState(uid, docks);
+            UpdateState(uid,ref dockState);
         }
     }
 
@@ -177,12 +141,6 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
             return;
         }
 
-        // In case they D/C should still clean them up.
-        foreach (var comp in EntityQuery<AutoDockComponent>(true))
-        {
-            comp.Requesters.Remove(user);
-        }
-
         RemovePilot(user);
     }
 
@@ -196,12 +154,14 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
     private void OnConsoleAnchorChange(EntityUid uid, ShuttleConsoleComponent component,
         ref AnchorStateChangedEvent args)
     {
-        UpdateState(uid);
+        DockingInterfaceState? dockState = null;
+        UpdateState(uid, ref dockState);
     }
 
     private void OnConsolePowerChange(EntityUid uid, ShuttleConsoleComponent component, ref PowerChangedEvent args)
     {
-        UpdateState(uid);
+        DockingInterfaceState? dockState = null;
+        UpdateState(uid, ref dockState);
     }
 
     private bool TryPilot(EntityUid user, EntityUid uid)
@@ -239,33 +199,38 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
     /// <summary>
     /// Returns the position and angle of all dockingcomponents.
     /// </summary>
-    private List<DockingInterfaceState> GetAllDocks()
+    public Dictionary<NetEntity, List<DockingPortState>> GetAllDocks()
     {
         // TODO: NEED TO MAKE SURE THIS UPDATES ON ANCHORING CHANGES!
-        var result = new List<DockingInterfaceState>();
-        var query = AllEntityQuery<DockingComponent, TransformComponent>();
+        var result = new Dictionary<NetEntity, List<DockingPortState>>();
+        var query = AllEntityQuery<DockingComponent, TransformComponent, MetaDataComponent>();
 
-        while (query.MoveNext(out var uid, out var comp, out var xform))
+        while (query.MoveNext(out var uid, out var comp, out var xform, out var metadata))
         {
             if (xform.ParentUid != xform.GridUid)
                 continue;
 
-            var state = new DockingInterfaceState()
+            var gridDocks = result.GetOrNew(GetNetEntity(xform.GridUid.Value));
+
+            var state = new DockingPortState()
             {
+                Name = metadata.EntityName,
                 Coordinates = GetNetCoordinates(xform.Coordinates),
                 Angle = xform.LocalRotation,
                 Entity = GetNetEntity(uid),
-                Connected = comp.Docked,
-                Color = comp.RadarColor,
-                HighlightedColor = comp.HighlightedRadarColor,
+                GridDockedWith =
+                    _xformQuery.TryGetComponent(comp.DockedWith, out var otherDockXform) ?
+                    GetNetEntity(otherDockXform.GridUid) :
+                    null,
             };
-            result.Add(state);
+
+            gridDocks.Add(state);
         }
 
         return result;
     }
 
-    private void UpdateState(EntityUid consoleUid, List<DockingInterfaceState>? docks = null)
+    private void UpdateState(EntityUid consoleUid, ref DockingInterfaceState? dockState)
     {
         EntityUid? entity = consoleUid;
 
@@ -278,77 +243,26 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
         entity = getShuttleEv.Console;
 
         TryComp<TransformComponent>(entity, out var consoleXform);
-        TryComp<RadarConsoleComponent>(entity, out var radar);
-        var range = radar?.MaxRange ?? SharedRadarConsoleSystem.DefaultMaxRange;
-
         var shuttleGridUid = consoleXform?.GridUid;
 
-        var destinations = new List<(NetEntity, string, bool)>();
-        var ftlState = FTLState.Available;
-        var ftlTime = TimeSpan.Zero;
+        NavInterfaceState navState;
+        ShuttleMapInterfaceState mapState;
+        dockState ??= GetDockState();
 
-        if (TryComp<FTLComponent>(shuttleGridUid, out var shuttleFtl))
+        if (shuttleGridUid != null && entity != null)
         {
-            ftlState = shuttleFtl.State;
-            ftlTime = _timing.CurTime + TimeSpan.FromSeconds(shuttleFtl.Accumulator);
+            navState = GetNavState(entity.Value, dockState.Docks);
+            mapState = GetMapState(shuttleGridUid.Value);
         }
-
-        // Mass too large
-        if (entity != null && shuttleGridUid != null &&
-            (!TryComp<PhysicsComponent>(shuttleGridUid, out var shuttleBody) || shuttleBody.Mass < 1000f))
+        else
         {
-            var metaQuery = GetEntityQuery<MetaDataComponent>();
-
-            // Can't go anywhere when in FTL.
-            var locked = shuttleFtl != null || Paused(shuttleGridUid.Value);
-
-            // Can't cache it because it may have a whitelist for the particular console.
-            // Include paused as we still want to show CentCom.
-            var destQuery = AllEntityQuery<FTLDestinationComponent>();
-
-            while (destQuery.MoveNext(out var destUid, out var comp))
-            {
-                // Can't warp to itself or if it's not on the whitelist (console or shuttle).
-                if (destUid == shuttleGridUid ||
-                    comp.Whitelist?.IsValid(entity.Value) == false &&
-                    (shuttleGridUid == null || comp.Whitelist?.IsValid(shuttleGridUid.Value, EntityManager) == false))
-                {
-                    continue;
-                }
-
-                var meta = metaQuery.GetComponent(destUid);
-                var name = meta.EntityName;
-
-                if (string.IsNullOrEmpty(name))
-                    name = Loc.GetString("shuttle-console-unknown");
-
-                var canTravel = !locked &&
-                                comp.Enabled &&
-                                (!TryComp<FTLComponent>(destUid, out var ftl) || ftl.State == FTLState.Cooldown);
-
-                // Can't travel to same map (yet)
-                if (canTravel && consoleXform?.MapUid == Transform(destUid).MapUid)
-                {
-                    canTravel = false;
-                }
-
-                destinations.Add((GetNetEntity(destUid), name, canTravel));
-            }
+            navState = new NavInterfaceState(0f, null, null, new Dictionary<NetEntity, List<DockingPortState>>());
+            mapState = new ShuttleMapInterfaceState(FTLState.Invalid, 0f, new List<ShuttleBeaconObject>(), new List<ShuttleExclusionObject>());
         }
 
-        docks ??= GetAllDocks();
-
         if (_ui.TryGetUi(consoleUid, ShuttleConsoleUiKey.Key, out var bui))
         {
-            _ui.SetUiState(bui, new ShuttleConsoleBoundInterfaceState(
-                ftlState,
-                ftlTime,
-                destinations,
-                range,
-                GetNetCoordinates(consoleXform?.Coordinates),
-                consoleXform?.LocalRotation,
-                docks
-            ));
+            _ui.SetUiState(bui, new ShuttleBoundUserInterfaceState(navState, mapState, dockState));
         }
     }
 
@@ -376,27 +290,6 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
         }
     }
 
-    /// <summary>
-    /// If pilot is moved then we'll stop them from piloting.
-    /// </summary>
-    private void HandlePilotMove(EntityUid uid, PilotComponent component, ref MoveEvent args)
-    {
-        if (component.Console == null || component.Position == null)
-        {
-            DebugTools.Assert(component.Position == null && component.Console == null);
-            EntityManager.RemoveComponent<PilotComponent>(uid);
-            return;
-        }
-
-        if (args.NewPosition.TryDistance(EntityManager, component.Position.Value, out var distance) &&
-            distance < PilotComponent.BreakDistance)
-        {
-            return;
-        }
-
-        RemovePilot(uid, component);
-    }
-
     protected override void HandlePilotShutdown(EntityUid uid, PilotComponent component, ComponentShutdown args)
     {
         base.HandlePilotShutdown(uid, component, args);
@@ -467,4 +360,70 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
                 RemovePilot(pilot, pilotComponent);
         }
     }
+
+    /// <summary>
+    /// Specific for a particular shuttle.
+    /// </summary>
+    public NavInterfaceState GetNavState(Entity<RadarConsoleComponent?, TransformComponent?> entity, Dictionary<NetEntity, List<DockingPortState>> docks)
+    {
+        if (!Resolve(entity, ref entity.Comp1, ref entity.Comp2))
+            return new NavInterfaceState(SharedRadarConsoleSystem.DefaultMaxRange, null, null, docks);
+
+        return GetNavState(
+            entity,
+            docks,
+            entity.Comp2.Coordinates,
+            entity.Comp2.LocalRotation);
+    }
+
+    public NavInterfaceState GetNavState(
+        Entity<RadarConsoleComponent?, TransformComponent?> entity,
+        Dictionary<NetEntity, List<DockingPortState>> docks,
+        EntityCoordinates coordinates,
+        Angle angle)
+    {
+        if (!Resolve(entity, ref entity.Comp1, ref entity.Comp2))
+            return new NavInterfaceState(SharedRadarConsoleSystem.DefaultMaxRange, GetNetCoordinates(coordinates), angle, docks);
+
+        return new NavInterfaceState(
+            entity.Comp1.MaxRange,
+            GetNetCoordinates(coordinates),
+            angle,
+            docks);
+    }
+
+    /// <summary>
+    /// Global for all shuttles.
+    /// </summary>
+    /// <returns></returns>
+    public DockingInterfaceState GetDockState()
+    {
+        var docks = GetAllDocks();
+        return new DockingInterfaceState(docks);
+    }
+
+    /// <summary>
+    /// Specific to a particular shuttle.
+    /// </summary>
+    public ShuttleMapInterfaceState GetMapState(Entity<FTLComponent?> shuttle)
+    {
+        FTLState ftlState = FTLState.Available;
+        float stateDuration = 0f;
+
+        if (Resolve(shuttle, ref shuttle.Comp, false) && shuttle.Comp.LifeStage < ComponentLifeStage.Stopped)
+        {
+            ftlState = shuttle.Comp.State;
+            stateDuration = _shuttle.GetStateDuration(shuttle.Comp);
+        }
+
+        List<ShuttleBeaconObject>? beacons = null;
+        List<ShuttleExclusionObject>? exclusions = null;
+        GetBeacons(ref beacons);
+        GetExclusions(ref exclusions);
+
+        return new ShuttleMapInterfaceState(
+            ftlState, stateDuration,
+            beacons ?? new List<ShuttleBeaconObject>(),
+            exclusions ?? new List<ShuttleExclusionObject>());
+    }
 }
index d976b634d5bda94ab710fd9263dda5b304eebbf1..8a935637655b3bc6960a81c3292e6786cd8dfd14 100644 (file)
@@ -3,16 +3,16 @@ using System.Linq;
 using System.Numerics;
 using Content.Server.Shuttles.Components;
 using Content.Server.Shuttles.Events;
-using Content.Server.Station.Systems;
+using Content.Server.Station.Events;
 using Content.Shared.Body.Components;
 using Content.Shared.Buckle.Components;
-using Content.Shared.Doors.Components;
 using Content.Shared.Ghost;
 using Content.Shared.Maps;
 using Content.Shared.Parallax;
 using Content.Shared.Shuttles.Components;
 using Content.Shared.Shuttles.Systems;
 using Content.Shared.StatusEffect;
+using Content.Shared.Whitelist;
 using JetBrains.Annotations;
 using Robust.Shared.Audio;
 using Robust.Shared.Audio.Components;
@@ -21,8 +21,8 @@ using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Physics;
 using Robust.Shared.Physics.Components;
-using Robust.Shared.Player;
 using Robust.Shared.Utility;
+using FTLMapComponent = Content.Shared.Shuttles.Components.FTLMapComponent;
 
 namespace Content.Server.Shuttles.Systems;
 
@@ -32,18 +32,11 @@ public sealed partial class ShuttleSystem
      * This is a way to move a shuttle from one location to another, via an intermediate map for fanciness.
      */
 
-    private MapId? _hyperSpaceMap;
-
     public const float DefaultStartupTime = 5.5f;
     public const float DefaultTravelTime = 20f;
     public const float DefaultArrivalTime = 5f;
     private const float FTLCooldown = 10f;
-    private const float ShuttleFTLRange = 100f;
-
-    /// <summary>
-    /// Minimum mass a grid needs to be to block a shuttle recall.
-    /// </summary>
-    public const float ShuttleFTLMassThreshold = 300f;
+    public const float FTLMassLimit = 100000f;
 
     // I'm too lazy to make CVars.
 
@@ -51,7 +44,7 @@ public sealed partial class ShuttleSystem
     {
         Params = AudioParams.Default.WithVolume(-5f),
     };
-    // private SoundSpecifier _travelSound = new SoundPathSpecifier();
+
     private readonly SoundSpecifier _arrivalSound = new SoundPathSpecifier("/Audio/Effects/Shuttle/hyperspace_end.ogg")
     {
         Params = AudioParams.Default.WithVolume(-5f),
@@ -59,7 +52,9 @@ public sealed partial class ShuttleSystem
 
     private readonly TimeSpan _hyperspaceKnockdownTime = TimeSpan.FromSeconds(5);
 
+    /// <summary>
     /// Left-side of the station we're allowed to use
+    /// </summary>
     private float _index;
 
     /// <summary>
@@ -72,12 +67,7 @@ public sealed partial class ShuttleSystem
     /// </summary>
     private const int FTLProximityIterations = 3;
 
-    /// <summary>
-    /// Minimum mass for an FTL destination
-    /// </summary>
-    public const float FTLDestinationMass = 500f;
-
-    private HashSet<EntityUid> _lookupEnts = new();
+    private readonly HashSet<EntityUid> _lookupEnts = new();
 
     private EntityQuery<BodyComponent> _bodyQuery;
     private EntityQuery<BuckleComponent> _buckleQuery;
@@ -88,68 +78,112 @@ public sealed partial class ShuttleSystem
 
     private void InitializeFTL()
     {
+        SubscribeLocalEvent<StationPostInitEvent>(OnStationPostInit);
         _bodyQuery = GetEntityQuery<BodyComponent>();
         _buckleQuery = GetEntityQuery<BuckleComponent>();
         _ghostQuery = GetEntityQuery<GhostComponent>();
         _physicsQuery = GetEntityQuery<PhysicsComponent>();
         _statusQuery = GetEntityQuery<StatusEffectsComponent>();
         _xformQuery = GetEntityQuery<TransformComponent>();
-
-        SubscribeLocalEvent<StationGridAddedEvent>(OnStationGridAdd);
     }
 
-    private void OnStationGridAdd(StationGridAddedEvent ev)
+    private void OnStationPostInit(ref StationPostInitEvent ev)
     {
-        if (HasComp<MapComponent>(ev.GridId) ||
-            TryComp<PhysicsComponent>(ev.GridId, out var body) &&
-            body.Mass > FTLDestinationMass)
+        // Add all grid maps as ftl destinations that anyone can FTL to.
+        foreach (var gridUid in ev.Station.Comp.Grids)
         {
-            AddFTLDestination(ev.GridId, true);
+            var gridXform = _xformQuery.GetComponent(gridUid);
+
+            if (gridXform.MapUid == null)
+            {
+                continue;
+            }
+
+            TryAddFTLDestination(gridXform.MapID, true, out _);
         }
     }
 
-    public bool CanFTL(EntityUid? uid, [NotNullWhen(false)] out string? reason)
+    /// <summary>
+    /// Ensures the FTL map exists and returns it.
+    /// </summary>
+    private EntityUid EnsureFTLMap()
     {
-        if (HasComp<PreventPilotComponent>(uid))
+        var query = AllEntityQuery<FTLMapComponent>();
+
+        while (query.MoveNext(out var uid, out _))
         {
-            reason = Loc.GetString("shuttle-console-prevent");
-            return false;
+            return uid;
         }
 
-        if (uid != null)
-        {
-            var ev = new ConsoleFTLAttemptEvent(uid.Value, false, string.Empty);
-            RaiseLocalEvent(uid.Value, ref ev, true);
+        var mapId = _mapManager.CreateMap();
+        var mapUid = _mapManager.GetMapEntityId(mapId);
+        var ftlMap = AddComp<FTLMapComponent>(mapUid);
 
-            if (ev.Cancelled)
-            {
-                reason = ev.Reason;
-                return false;
-            }
+        _metadata.SetEntityName(mapUid, "FTL");
+        Log.Debug($"Setup hyperspace map at {mapUid}");
+        DebugTools.Assert(!_mapManager.IsMapPaused(mapId));
+        var parallax = EnsureComp<ParallaxComponent>(mapUid);
+        parallax.Parallax = ftlMap.Parallax;
+
+        return mapUid;
+    }
+
+    public float GetStateDuration(FTLComponent component)
+    {
+        var state = component.State;
+
+        switch (state)
+        {
+            case FTLState.Starting:
+            case FTLState.Travelling:
+            case FTLState.Arriving:
+            case FTLState.Cooldown:
+                return component.Accumulator;
+            case FTLState.Available:
+                return 0f;
+            default:
+                throw new NotImplementedException();
         }
+    }
 
-        reason = null;
-        return true;
+    /// <summary>
+    /// Updates the whitelist for this FTL destination.
+    /// </summary>
+    /// <param name="entity"></param>
+    /// <param name="whitelist"></param>
+    public void SetFTLWhitelist(Entity<FTLDestinationComponent?> entity, EntityWhitelist? whitelist)
+    {
+        if (!Resolve(entity, ref entity.Comp))
+            return;
+
+        if (entity.Comp.Whitelist == whitelist)
+            return;
+
+        entity.Comp.Whitelist = whitelist;
+        _console.RefreshShuttleConsoles();
+        Dirty(entity);
     }
 
     /// <summary>
-    /// Adds a target for hyperspace to every shuttle console.
+    /// Adds the target map as available for FTL.
     /// </summary>
-    public FTLDestinationComponent AddFTLDestination(EntityUid uid, bool enabled)
+    public bool TryAddFTLDestination(MapId mapId, bool enabled, [NotNullWhen(true)] out FTLDestinationComponent? component)
     {
-        if (TryComp<FTLDestinationComponent>(uid, out var destination) && destination.Enabled == enabled)
-            return destination;
+        var mapUid = _mapManager.GetMapEntityId(mapId);
+        component = null;
 
-        destination = EnsureComp<FTLDestinationComponent>(uid);
+        if (!Exists(mapUid))
+            return false;
 
-        if (HasComp<FTLComponent>(uid))
-        {
-            enabled = false;
-        }
+        component = EnsureComp<FTLDestinationComponent>(mapUid);
 
-        destination.Enabled = enabled;
+        if (component.Enabled == enabled)
+            return true;
+
+        component.Enabled = enabled;
         _console.RefreshShuttleConsoles();
-        return destination;
+        Dirty(mapUid, component);
+        return true;
     }
 
     [PublicAPI]
@@ -162,52 +196,111 @@ public sealed partial class ShuttleSystem
     }
 
     /// <summary>
-    /// Moves a shuttle from its current position to the target one. Goes through the hyperspace map while the timer is running.
+    /// Returns true if the grid can FTL. Used to block protected shuttles like the emergency shuttle.
+    /// </summary>
+    public bool CanFTL(EntityUid shuttleUid, [NotNullWhen(false)] out string? reason)
+    {
+        if (HasComp<FTLComponent>(shuttleUid))
+        {
+            reason = Loc.GetString("shuttle-console-in-ftl");
+            return false;
+        }
+
+        if (TryComp(shuttleUid, out PhysicsComponent? shuttlePhysics) && shuttlePhysics.Mass > 300f)
+        {
+            reason = Loc.GetString("shuttle-console-mass");
+            return false;
+        }
+
+        if (HasComp<PreventPilotComponent>(shuttleUid))
+        {
+            reason = Loc.GetString("shuttle-console-prevent");
+            return false;
+        }
+
+        var ev = new ConsoleFTLAttemptEvent(shuttleUid, false, string.Empty);
+        RaiseLocalEvent(shuttleUid, ref ev, true);
+
+        if (ev.Cancelled)
+        {
+            reason = ev.Reason;
+            return false;
+        }
+
+        reason = null;
+        return true;
+    }
+
+    /// <summary>
+    /// Moves a shuttle from its current position to the target one without any checks. Goes through the hyperspace map while the timer is running.
     /// </summary>
-    public void FTLTravel(
+    public void FTLToCoordinates(
         EntityUid shuttleUid,
         ShuttleComponent component,
         EntityCoordinates coordinates,
+        Angle angle,
         float startupTime = DefaultStartupTime,
         float hyperspaceTime = DefaultTravelTime,
         string? priorityTag = null)
     {
         if (!TrySetupFTL(shuttleUid, component, out var hyperspace))
-           return;
+            return;
 
         hyperspace.StartupTime = startupTime;
         hyperspace.TravelTime = hyperspaceTime;
         hyperspace.Accumulator = hyperspace.StartupTime;
         hyperspace.TargetCoordinates = coordinates;
-        hyperspace.Dock = false;
+        hyperspace.TargetAngle = angle;
         hyperspace.PriorityTag = priorityTag;
-        _console.RefreshShuttleConsoles();
-        var ev = new FTLRequestEvent(_mapManager.GetMapEntityId(coordinates.ToMap(EntityManager, _transform).MapId));
+
+        _console.RefreshShuttleConsoles(shuttleUid);
+
+        var mapId = coordinates.GetMapId(EntityManager);
+        var mapUid = _mapManager.GetMapEntityId(mapId);
+        var ev = new FTLRequestEvent(mapUid);
         RaiseLocalEvent(shuttleUid, ref ev, true);
     }
 
     /// <summary>
-    /// Moves a shuttle from its current position to docked on the target one. Goes through the hyperspace map while the timer is running.
+    /// Moves a shuttle from its current position to docked on the target one.
+    /// If no docks are free when FTLing it will arrive in proximity
     /// </summary>
-    public void FTLTravel(
+    public void FTLToDock(
         EntityUid shuttleUid,
         ShuttleComponent component,
         EntityUid target,
         float startupTime = DefaultStartupTime,
         float hyperspaceTime = DefaultTravelTime,
-        bool dock = false,
         string? priorityTag = null)
     {
         if (!TrySetupFTL(shuttleUid, component, out var hyperspace))
             return;
 
+        var config = _dockSystem.GetDockingConfig(shuttleUid, target, priorityTag);
         hyperspace.StartupTime = startupTime;
         hyperspace.TravelTime = hyperspaceTime;
         hyperspace.Accumulator = hyperspace.StartupTime;
-        hyperspace.TargetUid = target;
-        hyperspace.Dock = dock;
         hyperspace.PriorityTag = priorityTag;
-        _console.RefreshShuttleConsoles();
+
+        _console.RefreshShuttleConsoles(shuttleUid);
+
+        // Valid dock for now time so just use that as the target.
+        if (config != null)
+        {
+            hyperspace.TargetCoordinates = config.Coordinates;
+            hyperspace.TargetAngle = config.Angle;
+        }
+        else if (TryGetFTLProximity(shuttleUid, target, out var coords, out var targAngle))
+        {
+            hyperspace.TargetCoordinates = coords;
+            hyperspace.TargetAngle = targAngle;
+        }
+        else
+        {
+            // FTL back to its own position.
+            hyperspace.TargetCoordinates = Transform(shuttleUid).Coordinates;
+            Log.Error($"Unable to FTL grid {ToPrettyString(shuttleUid)} to target properly?");
+        }
     }
 
     private bool TrySetupFTL(EntityUid uid, ShuttleComponent shuttle, [NotNullWhen(true)] out FTLComponent? component)
@@ -216,20 +309,14 @@ public sealed partial class ShuttleSystem
 
         if (HasComp<FTLComponent>(uid))
         {
-            Log.Warning($"Tried queuing {ToPrettyString(uid)} which already has HyperspaceComponent?");
+            Log.Warning($"Tried queuing {ToPrettyString(uid)} which already has {nameof(FTLComponent)}?");
             return false;
         }
 
-        if (TryComp<FTLDestinationComponent>(uid, out var dest))
-        {
-            dest.Enabled = false;
-        }
-
         _thruster.DisableLinearThrusters(shuttle);
         _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.North);
         _thruster.SetAngularThrust(shuttle, false);
-        // TODO: Maybe move this to docking instead?
-        SetDocks(uid, false);
+        _dockSystem.SetDocks(uid, false);
 
         component = AddComp<FTLComponent>(uid);
         component.State = FTLState.Starting;
@@ -241,228 +328,226 @@ public sealed partial class ShuttleSystem
             _transform.SetLocalPosition(audio.Value.Entity, gridPhysics.LocalCenter);
         }
 
+        // TODO: Play previs here for docking arrival.
+
         // Make sure the map is setup before we leave to avoid pop-in (e.g. parallax).
-        SetupHyperspace();
+        EnsureFTLMap();
         return true;
     }
 
+    /// <summary>
+    /// Transitions shuttle to FTL map.
+    /// </summary>
+    private void UpdateFTLStarting(Entity<FTLComponent, ShuttleComponent> entity)
+    {
+        var uid = entity.Owner;
+        var comp = entity.Comp1;
+        var xform = _xformQuery.GetComponent(entity);
+        DoTheDinosaur(xform);
+
+        comp.State = FTLState.Travelling;
+        var fromMapUid = xform.MapUid;
+        var fromMatrix = _transform.GetWorldMatrix(xform);
+        var fromRotation = _transform.GetWorldRotation(xform);
+
+        var width = Comp<MapGridComponent>(uid).LocalAABB.Width;
+        var ftlMap = EnsureFTLMap();
+        var body = _physicsQuery.GetComponent(entity);
+        var shuttleCenter = body.LocalCenter;
+
+        // Offset the start by buffer range just to avoid overlap.
+        var ftlStart = new EntityCoordinates(ftlMap, new Vector2(_index + width / 2f, 0f) - shuttleCenter);
+
+        _transform.SetCoordinates(entity.Owner, ftlStart);
+
+        // Reset rotation so they always face the same direction.
+        xform.LocalRotation = Angle.Zero;
+        _index += width + Buffer;
+        comp.Accumulator += comp.TravelTime - DefaultArrivalTime;
+
+        Enable(uid, component: body);
+        _physics.SetLinearVelocity(uid, new Vector2(0f, 20f), body: body);
+        _physics.SetAngularVelocity(uid, 0f, body: body);
+        _physics.SetLinearDamping(body, 0f);
+        _physics.SetAngularDamping(body, 0f);
+
+        _dockSystem.SetDockBolts(uid, true);
+        _console.RefreshShuttleConsoles(uid);
+
+        var ev = new FTLStartedEvent(uid, comp.TargetCoordinates, fromMapUid, fromMatrix, fromRotation);
+        RaiseLocalEvent(uid, ref ev, true);
+
+        // Audio
+        var wowdio = _audio.PlayPvs(comp.TravelSound, uid);
+        comp.TravelStream = wowdio?.Entity;
+        if (wowdio?.Component != null)
+        {
+            wowdio.Value.Component.Flags |= AudioFlags.GridAudio;
+
+            if (_physicsQuery.TryGetComponent(uid, out var gridPhysics))
+            {
+                _transform.SetLocalPosition(wowdio.Value.Entity, gridPhysics.LocalCenter);
+            }
+        }
+    }
+
+    /// <summary>
+    /// Shuttle arriving.
+    /// </summary>
+    private void UpdateFTLTravelling(Entity<FTLComponent, ShuttleComponent> entity)
+    {
+        var shuttle = entity.Comp2;
+        var comp = entity.Comp1;
+        comp.Accumulator += DefaultArrivalTime;
+        comp.State = FTLState.Arriving;
+        // TODO: Arrival effects
+        // For now we'll just use the ss13 bubbles but we can do fancier.
+
+        _thruster.DisableLinearThrusters(shuttle);
+        _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.South);
+
+        _console.RefreshShuttleConsoles(entity.Owner);
+    }
+
+    /// <summary>
+    ///  Shuttle arrived.
+    /// </summary>
+    private void UpdateFTLArriving(Entity<FTLComponent, ShuttleComponent> entity)
+    {
+        var uid = entity.Owner;
+        var xform = _xformQuery.GetComponent(uid);
+        var body = _physicsQuery.GetComponent(uid);
+        var comp = entity.Comp1;
+        DoTheDinosaur(xform);
+        _dockSystem.SetDockBolts(entity, false);
+        _dockSystem.SetDocks(entity, true);
+
+        _physics.SetLinearVelocity(uid, Vector2.Zero, body: body);
+        _physics.SetAngularVelocity(uid, 0f, body: body);
+        _physics.SetLinearDamping(body, entity.Comp2.LinearDamping);
+        _physics.SetAngularDamping(body, entity.Comp2.AngularDamping);
+
+        var target = entity.Comp1.TargetCoordinates;
+
+        MapId mapId;
+
+        if (!Exists(entity.Comp1.TargetCoordinates.EntityId))
+        {
+            // Uhh good luck
+            // Pick earliest map?
+            var maps = EntityQuery<MapComponent>().Select(o => o.MapId).ToList();
+            var map = maps.Min(o => o.GetHashCode());
+
+            mapId = new MapId(map);
+            TryFTLProximity(uid, _mapManager.GetMapEntityId(mapId));
+        }
+        // Docking FTL
+        else if (HasComp<MapGridComponent>(target.EntityId) &&
+                 !HasComp<MapComponent>(target.EntityId))
+        {
+            var config = _dockSystem.GetDockingConfigAt(uid, target.EntityId, target, entity.Comp1.TargetAngle);
+
+            // Couldn't dock somehow so just fallback to regular position FTL.
+            if (config == null)
+            {
+                _transform.SetCoordinates(uid, xform, target, rotation: entity.Comp1.TargetAngle);
+            }
+
+            mapId = target.GetMapId(EntityManager);
+        }
+        // Position ftl
+        else
+        {
+            mapId = target.GetMapId(EntityManager);
+            _transform.SetCoordinates(uid, xform, target, rotation: entity.Comp1.TargetAngle);
+        }
+
+        if (_physicsQuery.TryGetComponent(uid, out body))
+        {
+            _physics.SetLinearVelocity(uid, Vector2.Zero, body: body);
+            _physics.SetAngularVelocity(uid, 0f, body: body);
+
+            // Disable shuttle if it's on a planet; unfortunately can't do this in parent change messages due
+            // to event ordering and awake body shenanigans (at least for now).
+            if (HasComp<MapGridComponent>(xform.MapUid))
+            {
+                Disable(uid, component: body);
+            }
+
+            Enable(uid, component: body, shuttle: entity.Comp2);
+        }
+
+        _thruster.DisableLinearThrusters(entity.Comp2);
+
+        comp.TravelStream = _audio.Stop(comp.TravelStream);
+        var audio = _audio.PlayPvs(_arrivalSound, uid);
+        audio.Value.Component.Flags |= AudioFlags.GridAudio;
+        // TODO: Shitcode til engine fix
+
+        if (_physicsQuery.TryGetComponent(uid, out var gridPhysics))
+        {
+            _transform.SetLocalPosition(audio.Value.Entity, gridPhysics.LocalCenter);
+        }
+
+        if (TryComp<FTLDestinationComponent>(uid, out var dest))
+        {
+            dest.Enabled = true;
+        }
+
+        comp.State = FTLState.Cooldown;
+        comp.Accumulator += FTLCooldown;
+        _console.RefreshShuttleConsoles(uid);
+        _mapManager.SetMapPaused(mapId, false);
+        Smimsh(uid, xform: xform);
+
+        var ftlEvent = new FTLCompletedEvent(uid, _mapManager.GetMapEntityId(mapId));
+        RaiseLocalEvent(uid, ref ftlEvent, true);
+    }
+
+    private void UpdateFTLCooldown(Entity<FTLComponent, ShuttleComponent> entity)
+    {
+        RemCompDeferred<FTLComponent>(entity);
+        _console.RefreshShuttleConsoles(entity);
+    }
+
     private void UpdateHyperspace(float frameTime)
     {
-        var query = EntityQueryEnumerator<FTLComponent>();
+        var query = EntityQueryEnumerator<FTLComponent, ShuttleComponent>();
 
-        while (query.MoveNext(out var uid, out var comp))
+        while (query.MoveNext(out var uid, out var comp, out var shuttle))
         {
             comp.Accumulator -= frameTime;
 
             if (comp.Accumulator > 0f)
                 continue;
 
-            var xform = Transform(uid);
-            PhysicsComponent? body;
-            ShuttleComponent? shuttle;
-            TryComp(uid, out shuttle);
+            var entity = (uid, comp, shuttle);
 
             switch (comp.State)
             {
                 // Startup time has elapsed and in hyperspace.
                 case FTLState.Starting:
-                {
-                    DoTheDinosaur(xform);
-
-                    comp.State = FTLState.Travelling;
-                    var fromMapUid = xform.MapUid;
-                    var fromMatrix = _transform.GetWorldMatrix(xform);
-                    var fromRotation = _transform.GetWorldRotation(xform);
-
-                    var width = Comp<MapGridComponent>(uid).LocalAABB.Width;
-                    xform.Coordinates = new EntityCoordinates(_mapManager.GetMapEntityId(_hyperSpaceMap!.Value),
-                        new Vector2(_index + width / 2f, 0f));
-                    xform.LocalRotation = Angle.Zero;
-                    _index += width + Buffer;
-                    comp.Accumulator += comp.TravelTime - DefaultArrivalTime;
-
-                    if (TryComp(uid, out body))
-                    {
-                        if (shuttle != null)
-                            Enable(uid, component: body, shuttle: shuttle);
-                        _physics.SetLinearVelocity(uid, new Vector2(0f, 20f), body: body);
-                        _physics.SetAngularVelocity(uid, 0f, body: body);
-                        _physics.SetLinearDamping(body, 0f);
-                        _physics.SetAngularDamping(body, 0f);
-                    }
-
-                    SetDockBolts(uid, true);
-                    _console.RefreshShuttleConsoles(uid);
-                    var target = comp.TargetUid != null
-                        ? new EntityCoordinates(comp.TargetUid.Value, Vector2.Zero)
-                        : comp.TargetCoordinates;
-
-                    var ev = new FTLStartedEvent(uid, target, fromMapUid, fromMatrix, fromRotation);
-                    RaiseLocalEvent(uid, ref ev, true);
-
-                    var wowdio = _audio.PlayPvs(comp.TravelSound, uid);
-                    comp.TravelStream = wowdio?.Entity;
-                    if (wowdio?.Component != null)
-                    {
-                        wowdio.Value.Component.Flags |= AudioFlags.GridAudio;
-
-                        if (_physicsQuery.TryGetComponent(uid, out var gridPhysics))
-                        {
-                            _transform.SetLocalPosition(wowdio.Value.Entity, gridPhysics.LocalCenter);
-                        }
-                    }
-                }
+                    UpdateFTLStarting(entity);
                     break;
                 // Arriving, play effects
                 case FTLState.Travelling:
-                    comp.Accumulator += DefaultArrivalTime;
-                    comp.State = FTLState.Arriving;
-                    // TODO: Arrival effects
-                    // For now we'll just use the ss13 bubbles but we can do fancier.
-
-                    if (shuttle != null)
-                    {
-                        _thruster.DisableLinearThrusters(shuttle);
-                        _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.South);
-                    }
-
-                    _console.RefreshShuttleConsoles(uid);
+                    UpdateFTLTravelling(entity);
                     break;
                 // Arrived
                 case FTLState.Arriving:
-                {
-                    DoTheDinosaur(xform);
-                    SetDockBolts(uid, false);
-                    SetDocks(uid, true);
-
-                    if (TryComp(uid, out body))
-                    {
-                        _physics.SetLinearVelocity(uid, Vector2.Zero, body: body);
-                        _physics.SetAngularVelocity(uid, 0f, body: body);
-                        if (shuttle != null)
-                        {
-                            _physics.SetLinearDamping(body, shuttle.LinearDamping);
-                            _physics.SetAngularDamping(body, shuttle.AngularDamping);
-                        }
-                    }
-
-                    MapId mapId;
-
-                    if (comp.TargetUid != null && shuttle != null)
-                    {
-                        if (!Deleted(comp.TargetUid))
-                        {
-                            if (comp.Dock)
-                                TryFTLDock(uid, shuttle, comp.TargetUid.Value, comp.PriorityTag);
-                            else
-                                TryFTLProximity(uid, shuttle, comp.TargetUid.Value);
-
-                            mapId = Transform(comp.TargetUid.Value).MapID;
-                        }
-                        // oh boy, fallback time
-                        else
-                        {
-                            // Pick earliest map?
-                            var maps = EntityQuery<MapComponent>().Select(o => o.MapId).ToList();
-                            var map = maps.Min(o => o.GetHashCode());
-
-                            mapId = new MapId(map);
-                            TryFTLProximity(uid, shuttle, _mapManager.GetMapEntityId(mapId));
-                        }
-                    }
-                    else
-                    {
-                        xform.Coordinates = comp.TargetCoordinates;
-                        mapId = comp.TargetCoordinates.GetMapId(EntityManager);
-                    }
-
-                    if (TryComp(uid, out body))
-                    {
-                        _physics.SetLinearVelocity(uid, Vector2.Zero, body: body);
-                        _physics.SetAngularVelocity(uid, 0f, body: body);
-
-                        // Disable shuttle if it's on a planet; unfortunately can't do this in parent change messages due
-                        // to event ordering and awake body shenanigans (at least for now).
-                        if (HasComp<MapGridComponent>(xform.MapUid))
-                        {
-                            Disable(uid, component: body);
-                        }
-                        else if (shuttle != null)
-                        {
-                            Enable(uid, component: body, shuttle: shuttle);
-                        }
-                    }
-
-                    if (shuttle != null)
-                    {
-                        _thruster.DisableLinearThrusters(shuttle);
-                    }
-
-                    comp.TravelStream = _audio.Stop(comp.TravelStream);
-                    var audio = _audio.PlayPvs(_arrivalSound, uid);
-                    audio.Value.Component.Flags |= AudioFlags.GridAudio;
-                    // TODO: Shitcode til engine fix
-
-                    if (_physicsQuery.TryGetComponent(uid, out var gridPhysics))
-                    {
-                        _transform.SetLocalPosition(audio.Value.Entity, gridPhysics.LocalCenter);
-                    }
-
-                    if (TryComp<FTLDestinationComponent>(uid, out var dest))
-                    {
-                        dest.Enabled = true;
-                    }
-
-                    comp.State = FTLState.Cooldown;
-                    comp.Accumulator += FTLCooldown;
-                    _console.RefreshShuttleConsoles(uid);
-                    _mapManager.SetMapPaused(mapId, false);
-                    Smimsh(uid, xform: xform);
-
-                    var ftlEvent = new FTLCompletedEvent(uid, _mapManager.GetMapEntityId(mapId));
-                    RaiseLocalEvent(uid, ref ftlEvent, true);
-                    }
+                    UpdateFTLArriving(entity);
                     break;
                 case FTLState.Cooldown:
-                    RemComp<FTLComponent>(uid);
-                    _console.RefreshShuttleConsoles(uid);
+                    UpdateFTLCooldown(entity);
                     break;
                 default:
                     Log.Error($"Found invalid FTL state {comp.State} for {uid}");
-                    RemComp<FTLComponent>(uid);
+                    RemCompDeferred<FTLComponent>(uid);
                     break;
             }
         }
     }
 
-    private void SetDocks(EntityUid uid, bool enabled)
-    {
-        var query = AllEntityQuery<DockingComponent, TransformComponent>();
-
-        while (query.MoveNext(out var dockUid, out var dock, out var xform))
-        {
-            if (xform.ParentUid != uid || dock.Enabled == enabled)
-                continue;
-
-            _dockSystem.Undock(dockUid, dock);
-            dock.Enabled = enabled;
-        }
-    }
-
-    private void SetDockBolts(EntityUid uid, bool enabled)
-    {
-        var query = AllEntityQuery<DockingComponent, DoorBoltComponent, TransformComponent>();
-
-        while (query.MoveNext(out var doorUid, out _, out var door, out var xform))
-        {
-            if (xform.ParentUid != uid)
-                continue;
-
-            _doors.TryClose(doorUid);
-            _doors.SetBoltsDown((doorUid, door), enabled);
-        }
-    }
-
     private float GetSoundRange(EntityUid uid)
     {
         if (!_mapManager.TryGetGrid(uid, out var grid))
@@ -471,31 +556,6 @@ public sealed partial class ShuttleSystem
         return MathF.Max(grid.LocalAABB.Width, grid.LocalAABB.Height) + 12.5f;
     }
 
-    private void SetupHyperspace()
-    {
-        if (_hyperSpaceMap != null)
-            return;
-
-        _hyperSpaceMap = _mapManager.CreateMap();
-        _metadata.SetEntityName(_mapManager.GetMapEntityId(_hyperSpaceMap.Value), "FTL");
-        Log.Debug($"Setup hyperspace map at {_hyperSpaceMap.Value}");
-        DebugTools.Assert(!_mapManager.IsMapPaused(_hyperSpaceMap.Value));
-        var parallax = EnsureComp<ParallaxComponent>(_mapManager.GetMapEntityId(_hyperSpaceMap.Value));
-        parallax.Parallax = "FastSpace";
-    }
-
-    private void CleanupHyperspace()
-    {
-        _index = 0f;
-        if (_hyperSpaceMap == null || !_mapManager.MapExists(_hyperSpaceMap.Value))
-        {
-            _hyperSpaceMap = null;
-            return;
-        }
-        _mapManager.DeleteMap(_hyperSpaceMap.Value);
-        _hyperSpaceMap = null;
-    }
-
     /// <summary>
     /// Puts everyone unbuckled on the floor, paralyzed.
     /// </summary>
@@ -559,11 +619,12 @@ public sealed partial class ShuttleSystem
 
     /// <summary>
     /// Tries to dock with the target grid, otherwise falls back to proximity.
+    /// This bypasses FTL travel time.
     /// </summary>
     public bool TryFTLDock(EntityUid shuttleUid, ShuttleComponent component, EntityUid targetUid, string? priorityTag = null)
     {
-        if (!TryComp<TransformComponent>(shuttleUid, out var shuttleXform) ||
-            !TryComp<TransformComponent>(targetUid, out var targetXform) ||
+        if (!_xformQuery.TryGetComponent(shuttleUid, out var shuttleXform) ||
+            !_xformQuery.TryGetComponent(targetUid, out var targetXform) ||
             targetXform.MapUid == null ||
             !targetXform.MapUid.Value.IsValid())
         {
@@ -574,35 +635,41 @@ public sealed partial class ShuttleSystem
 
         if (config != null)
         {
-            FTLDock(config, shuttleXform);
+            FTLDock((shuttleUid, shuttleXform), config);
             return true;
         }
 
-        TryFTLProximity(shuttleUid, component, targetUid, shuttleXform, targetXform);
+        TryFTLProximity(shuttleUid, targetUid, shuttleXform, targetXform);
         return false;
     }
 
     /// <summary>
     /// Forces an FTL dock.
     /// </summary>
-    public void FTLDock(DockingConfig config, TransformComponent shuttleXform)
+    public void FTLDock(Entity<TransformComponent> shuttle, DockingConfig config)
     {
         // Set position
-        shuttleXform.Coordinates = config.Coordinates;
-        _transform.SetWorldRotation(shuttleXform, config.Angle);
+        var mapCoordinates = _transform.ToMapCoordinates(config.Coordinates);
+        var mapUid = _mapManager.GetMapEntityId(mapCoordinates.MapId);
+        _transform.SetCoordinates(shuttle.Owner, shuttle.Comp, new EntityCoordinates(mapUid, mapCoordinates.Position), rotation: config.Angle);
 
         // Connect everything
         foreach (var (dockAUid, dockBUid, dockA, dockB) in config.Docks)
         {
-            _dockSystem.Dock(dockAUid, dockA, dockBUid, dockB);
+            _dockSystem.Dock((dockAUid, dockA), (dockBUid, dockB));
         }
     }
 
     /// <summary>
-    /// Tries to arrive nearby without overlapping with other grids.
+    /// Tries to get the target position to FTL near to another grid.
     /// </summary>
-    public bool TryFTLProximity(EntityUid shuttleUid, ShuttleComponent component, EntityUid targetUid, TransformComponent? xform = null, TransformComponent? targetXform = null)
+    private bool TryGetFTLProximity(EntityUid shuttleUid, EntityUid targetUid,
+        out EntityCoordinates coordinates, out Angle angle,
+        TransformComponent? xform = null, TransformComponent? targetXform = null)
     {
+        coordinates = EntityCoordinates.Invalid;
+        angle = Angle.Zero;
+
         if (!Resolve(targetUid, ref targetXform) ||
             targetXform.MapUid == null ||
             !targetXform.MapUid.Value.IsValid() ||
@@ -611,6 +678,7 @@ public sealed partial class ShuttleSystem
             return false;
         }
 
+
         var xformQuery = GetEntityQuery<TransformComponent>();
         var shuttleAABB = Comp<MapGridComponent>(shuttleUid).LocalAABB;
         Box2 targetLocalAABB;
@@ -702,17 +770,36 @@ public sealed partial class ShuttleSystem
             spawnPos = _transform.GetWorldPosition(targetXform, xformQuery);
         }
 
-        xform.Coordinates = new EntityCoordinates(targetXform.MapUid.Value, spawnPos);
-
         if (!HasComp<MapComponent>(targetXform.GridUid))
         {
-            _transform.SetLocalRotation(xform, _random.NextAngle());
+            angle = _random.NextAngle();
         }
         else
         {
-            _transform.SetLocalRotation(xform, Angle.Zero);
+            angle = Angle.Zero;
+        }
+
+        coordinates = new EntityCoordinates(targetXform.MapUid.Value, spawnPos);
+        return true;
+    }
+
+    /// <summary>
+    /// Tries to arrive nearby without overlapping with other grids.
+    /// </summary>
+    public bool TryFTLProximity(EntityUid shuttleUid, EntityUid targetUid, TransformComponent? xform = null, TransformComponent? targetXform = null)
+    {
+        if (!Resolve(targetUid, ref targetXform) ||
+            targetXform.MapUid == null ||
+            !targetXform.MapUid.Value.IsValid() ||
+            !Resolve(shuttleUid, ref xform))
+        {
+            return false;
         }
 
+        if (!TryGetFTLProximity(shuttleUid, targetUid, out var coords, out var angle, xform, targetXform))
+            return false;
+
+        _transform.SetCoordinates(shuttleUid, xform, coords, rotation: angle);
         return true;
     }
 
@@ -768,6 +855,9 @@ public sealed partial class ShuttleSystem
                     continue;
                 }
 
+                if (HasComp<FTLBeaconComponent>(ent))
+                    continue;
+
                 QueueDel(ent);
             }
         }
index f87d80736ccea6896c9441dd68ffe6071517b553..c4cf2820e246cc3c9105e6264d3a32be9a880a45 100644 (file)
@@ -71,7 +71,7 @@ public sealed partial class ShuttleSystem
         {
             if (TryComp<ShuttleComponent>(ent[0], out var shuttle))
             {
-                TryFTLProximity(ent[0], shuttle, targetGrid.Value);
+                TryFTLProximity(ent[0], targetGrid.Value);
             }
 
             _station.AddGridToStation(uid, ent[0]);
@@ -127,7 +127,7 @@ public sealed partial class ShuttleSystem
                 {
                     if (TryComp<ShuttleComponent>(ent[0], out var shuttle))
                     {
-                        TryFTLProximity(ent[0], shuttle, targetGrid.Value);
+                        TryFTLProximity(ent[0], targetGrid.Value);
                     }
                     else
                     {
@@ -206,7 +206,7 @@ public sealed partial class ShuttleSystem
 
                 if (config != null)
                 {
-                    FTLDock(config, shuttleXform);
+                    FTLDock((ent[0], shuttleXform), config);
 
                     if (TryComp<StationMemberComponent>(xform.GridUid, out var stationMember))
                     {
index 5f11ce25a4b5286cfd38cc6c51437e74e921b75a..8d44dae4b2e5d56e0cd4cfd778e6df8c06aaea0b 100644 (file)
@@ -33,7 +33,6 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
     [Dependency] private readonly BiomeSystem _biomes = default!;
     [Dependency] private readonly BodySystem _bobby = default!;
     [Dependency] private readonly DockingSystem _dockSystem = default!;
-    [Dependency] private readonly DoorSystem _doors = default!;
     [Dependency] private readonly EntityLookupSystem _lookup = default!;
     [Dependency] private readonly FixtureSystem _fixtures = default!;
     [Dependency] private readonly MapLoaderSystem _loader = default!;
@@ -62,8 +61,6 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
         SubscribeLocalEvent<ShuttleComponent, ComponentStartup>(OnShuttleStartup);
         SubscribeLocalEvent<ShuttleComponent, ComponentShutdown>(OnShuttleShutdown);
 
-        SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
-
         SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
         SubscribeLocalEvent<FixturesComponent, GridFixtureChangeEvent>(OnGridFixtureChange);
     }
@@ -74,11 +71,6 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
         UpdateHyperspace(frameTime);
     }
 
-    private void OnRoundRestart(RoundRestartCleanupEvent ev)
-    {
-        CleanupHyperspace();
-    }
-
     private void OnGridFixtureChange(EntityUid uid, FixturesComponent manager, GridFixtureChangeEvent args)
     {
         foreach (var fixture in args.NewFixtures)
index 79ae855a216509fc63e8742a85fea3e44d0dea50..5df38eec4d226c3a1b443bdd171e75c10cea22f3 100644 (file)
@@ -10,7 +10,7 @@ namespace Content.Shared.Parallax;
 public sealed partial class ParallaxComponent : Component
 {
     // I wish I could use a typeserializer here but parallax is extremely client-dependent.
-    [DataField("parallax"), AutoNetworkedField]
+    [DataField, AutoNetworkedField]
     public string Parallax = "Default";
 
     [UsedImplicitly, ViewVariables(VVAccess.ReadWrite)]
diff --git a/Content.Shared/Shuttles/BUIStates/DockingInterfaceState.cs b/Content.Shared/Shuttles/BUIStates/DockingInterfaceState.cs
new file mode 100644 (file)
index 0000000..9070605
--- /dev/null
@@ -0,0 +1,14 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shuttles.BUIStates;
+
+[Serializable, NetSerializable]
+public sealed class DockingInterfaceState
+{
+    public Dictionary<NetEntity, List<DockingPortState>> Docks;
+
+    public DockingInterfaceState(Dictionary<NetEntity, List<DockingPortState>> docks)
+    {
+        Docks = docks;
+    }
+}
diff --git a/Content.Shared/Shuttles/BUIStates/DockingPortState.cs b/Content.Shared/Shuttles/BUIStates/DockingPortState.cs
new file mode 100644 (file)
index 0000000..a605c2e
--- /dev/null
@@ -0,0 +1,20 @@
+using Robust.Shared.Map;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shuttles.BUIStates;
+
+/// <summary>
+/// State of each individual docking port for interface purposes
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class DockingPortState
+{
+    public string Name = string.Empty;
+
+    public NetCoordinates Coordinates;
+    public Angle Angle;
+    public NetEntity Entity;
+    public bool Connected => GridDockedWith != null;
+
+    public NetEntity? GridDockedWith;
+}
diff --git a/Content.Shared/Shuttles/BUIStates/NavBoundUserInterfaceState.cs b/Content.Shared/Shuttles/BUIStates/NavBoundUserInterfaceState.cs
new file mode 100644 (file)
index 0000000..c18047a
--- /dev/null
@@ -0,0 +1,17 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shuttles.BUIStates;
+
+/// <summary>
+/// Wrapper around <see cref="NavInterfaceState"/>
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class NavBoundUserInterfaceState : BoundUserInterfaceState
+{
+    public NavInterfaceState State;
+
+    public NavBoundUserInterfaceState(NavInterfaceState state)
+    {
+        State = state;
+    }
+}
similarity index 53%
rename from Content.Shared/Shuttles/BUIStates/RadarConsoleBoundInterfaceState.cs
rename to Content.Shared/Shuttles/BUIStates/NavInterfaceState.cs
index d60f5274b535ed9e3fb24cb185f9a6ba6843a38a..a6f4c01657d01385202d0d03a4dce0c31adcfddf 100644 (file)
@@ -4,10 +4,9 @@ using Robust.Shared.Serialization;
 namespace Content.Shared.Shuttles.BUIStates;
 
 [Serializable, NetSerializable]
-[Virtual]
-public class RadarConsoleBoundInterfaceState : BoundUserInterfaceState
+public sealed class NavInterfaceState
 {
-    public readonly float MaxRange;
+    public float MaxRange;
 
     /// <summary>
     /// The relevant coordinates to base the radar around.
@@ -19,13 +18,13 @@ public class RadarConsoleBoundInterfaceState : BoundUserInterfaceState
     /// </summary>
     public Angle? Angle;
 
-    public readonly List<DockingInterfaceState> Docks;
+    public Dictionary<NetEntity, List<DockingPortState>> Docks;
 
-    public RadarConsoleBoundInterfaceState(
+    public NavInterfaceState(
         float maxRange,
         NetCoordinates? coordinates,
         Angle? angle,
-        List<DockingInterfaceState> docks)
+        Dictionary<NetEntity, List<DockingPortState>> docks)
     {
         MaxRange = maxRange;
         Coordinates = coordinates;
@@ -34,20 +33,6 @@ public class RadarConsoleBoundInterfaceState : BoundUserInterfaceState
     }
 }
 
-/// <summary>
-/// State of each individual docking port for interface purposes
-/// </summary>
-[Serializable, NetSerializable]
-public sealed class DockingInterfaceState
-{
-    public NetCoordinates Coordinates;
-    public Angle Angle;
-    public NetEntity Entity;
-    public bool Connected;
-    public Color Color;
-    public Color HighlightedColor;
-}
-
 [Serializable, NetSerializable]
 public enum RadarConsoleUiKey : byte
 {
diff --git a/Content.Shared/Shuttles/BUIStates/ShuttleBoundUserInterfaceState.cs b/Content.Shared/Shuttles/BUIStates/ShuttleBoundUserInterfaceState.cs
new file mode 100644 (file)
index 0000000..8d418f7
--- /dev/null
@@ -0,0 +1,19 @@
+using Content.Shared.Shuttles.UI.MapObjects;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shuttles.BUIStates;
+
+[Serializable, NetSerializable]
+public sealed class ShuttleBoundUserInterfaceState : BoundUserInterfaceState
+{
+    public NavInterfaceState NavState;
+    public ShuttleMapInterfaceState MapState;
+    public DockingInterfaceState DockState;
+
+    public ShuttleBoundUserInterfaceState(NavInterfaceState navState, ShuttleMapInterfaceState mapState, DockingInterfaceState dockState)
+    {
+        NavState = navState;
+        MapState = mapState;
+        DockState = dockState;
+    }
+}
diff --git a/Content.Shared/Shuttles/BUIStates/ShuttleConsoleBoundInterfaceState.cs b/Content.Shared/Shuttles/BUIStates/ShuttleConsoleBoundInterfaceState.cs
deleted file mode 100644 (file)
index db38416..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-using Content.Shared.Shuttles.Components;
-using Content.Shared.Shuttles.Systems;
-using Robust.Shared.Map;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Shuttles.BUIStates;
-
-[Serializable, NetSerializable]
-public sealed class ShuttleConsoleBoundInterfaceState : RadarConsoleBoundInterfaceState
-{
-    /// <summary>
-    /// The current FTL state.
-    /// </summary>
-    public readonly FTLState FTLState;
-
-    /// <summary>
-    ///  When the next FTL state change happens.
-    /// </summary>
-    public readonly TimeSpan FTLTime;
-
-    public List<(NetEntity Entity, string Destination, bool Enabled)> Destinations;
-
-    public ShuttleConsoleBoundInterfaceState(
-        FTLState ftlState,
-        TimeSpan ftlTime,
-        List<(NetEntity Entity, string Destination, bool Enabled)> destinations,
-        float maxRange,
-        NetCoordinates? coordinates,
-        Angle? angle,
-        List<DockingInterfaceState> docks) : base(maxRange, coordinates, angle, docks)
-    {
-        FTLState = ftlState;
-        FTLTime = ftlTime;
-        Destinations = destinations;
-    }
-}
diff --git a/Content.Shared/Shuttles/BUIStates/ShuttleMapInterfaceState.cs b/Content.Shared/Shuttles/BUIStates/ShuttleMapInterfaceState.cs
new file mode 100644 (file)
index 0000000..cee0daa
--- /dev/null
@@ -0,0 +1,38 @@
+using Content.Shared.Shuttles.Systems;
+using Content.Shared.Shuttles.UI.MapObjects;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shuttles.BUIStates;
+
+/// <summary>
+/// Handles BUI data for Map screen.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class ShuttleMapInterfaceState
+{
+    /// <summary>
+    /// The current FTL state.
+    /// </summary>
+    public readonly FTLState FTLState;
+
+    /// <summary>
+    /// How long the FTL state takes.
+    /// </summary>
+    public float FTLDuration;
+
+    public List<ShuttleBeaconObject> Destinations;
+
+    public List<ShuttleExclusionObject> Exclusions;
+
+    public ShuttleMapInterfaceState(
+        FTLState ftlState,
+        float ftlDuration,
+        List<ShuttleBeaconObject> destinations,
+        List<ShuttleExclusionObject> exclusions)
+    {
+        FTLState = ftlState;
+        FTLDuration = ftlDuration;
+        Destinations = destinations;
+        Exclusions = exclusions;
+    }
+}
diff --git a/Content.Shared/Shuttles/Components/FTLDestinationComponent.cs b/Content.Shared/Shuttles/Components/FTLDestinationComponent.cs
new file mode 100644 (file)
index 0000000..58cff96
--- /dev/null
@@ -0,0 +1,26 @@
+using Content.Shared.Whitelist;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Shuttles.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class FTLDestinationComponent : Component
+{
+    /// <summary>
+    /// Should this destination be restricted in some form from console visibility.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
+    public EntityWhitelist? Whitelist;
+
+    /// <summary>
+    /// Is this destination visible but available to be warped to?
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
+    public bool Enabled = true;
+
+    /// <summary>
+    /// Can we only FTL to beacons on this map.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool BeaconsOnly;
+}
diff --git a/Content.Shared/Shuttles/Components/FTLMapComponent.cs b/Content.Shared/Shuttles/Components/FTLMapComponent.cs
new file mode 100644 (file)
index 0000000..4d57b09
--- /dev/null
@@ -0,0 +1,28 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Shuttles.Components;
+
+/// <summary>
+/// Marker that specifies a map as being for FTLing entities.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class FTLMapComponent : Component
+{
+    /// <summary>
+    /// Offset for FTLing shuttles so they don't overlap each other.
+    /// </summary>
+    [DataField]
+    public int Index;
+
+    /// <summary>
+    /// What parallax to use for the background, immediately gets deffered to ParallaxComponent.
+    /// </summary>
+    [DataField]
+    public string Parallax = "FastSpace";
+
+    /// <summary>
+    /// Can FTL on this map only be done to beacons.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool Beacons;
+}
index 24f06e93d4fce889199bbdf12542a6d554d9b527..a7e6ac1152b1e56c0bc45105c984a83c6e93c769 100644 (file)
@@ -15,10 +15,12 @@ public sealed partial class IFFComponent : Component
     /// </summary>
     public const bool ShowIFFDefault = true;
 
+    public static readonly Color SelfColor = Color.MediumSpringGreen;
+
     /// <summary>
     /// Default color to use for IFF if no component is found.
     /// </summary>
-    public static readonly Color IFFColor = Color.Aquamarine;
+    public static readonly Color IFFColor = Color.Gold;
 
     [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
     public IFFFlags Flags = IFFFlags.None;
index e561aebca784b05963419b0b55ad5d2da746bd42..1a6927cf813df184a580d6a8f1ea82dedc4f7d6e 100644 (file)
@@ -22,8 +22,6 @@ namespace Content.Shared.Shuttles.Components
         [ViewVariables]
         public EntityCoordinates? Position { get; set; }
 
-        public const float BreakDistance = 0.25f;
-
         public Vector2 CurTickStrafeMovement = Vector2.Zero;
         public float CurTickRotationMovement;
         public float CurTickBraking;
diff --git a/Content.Shared/Shuttles/Components/ShuttleMapParallaxComponent.cs b/Content.Shared/Shuttles/Components/ShuttleMapParallaxComponent.cs
new file mode 100644 (file)
index 0000000..1d186b3
--- /dev/null
@@ -0,0 +1,17 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Shuttles.Components;
+
+/// <summary>
+/// Shows a parallax background on the shuttle map console.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class ShuttleMapParallaxComponent : Component
+{
+    public static readonly ResPath FallbackTexture = new ResPath("/Textures/Parallaxes/space_map2.png");
+
+    // TODO: This should ideally be shared with parallax stuff to avoid duplication, for now it's just a texture
+    [DataField, AutoNetworkedField]
+    public ResPath TexturePath;
+}
similarity index 64%
rename from Content.Shared/Shuttles/Events/AutodockRequestMessage.cs
rename to Content.Shared/Shuttles/Events/DockRequestMessage.cs
index 39b91e6278c7b2f7af23cd964128d0e6201a5e25..a8e0c3f7d2c8df001023d41477b9866bec43dc76 100644 (file)
@@ -3,10 +3,12 @@ using Robust.Shared.Serialization;
 namespace Content.Shared.Shuttles.Events;
 
 /// <summary>
-/// Raised on the client when it's viewing a particular docking port to try and dock it automatically.
+/// Raised on the client when it's viewing a particular docking port to try and dock it.
 /// </summary>
 [Serializable, NetSerializable]
-public sealed class AutodockRequestMessage : BoundUserInterfaceMessage
+public sealed class DockRequestMessage : BoundUserInterfaceMessage
 {
     public NetEntity DockEntity;
+
+    public NetEntity TargetDockEntity;
 }
diff --git a/Content.Shared/Shuttles/Events/ShuttleConsoleFTLBeaconMessage.cs b/Content.Shared/Shuttles/Events/ShuttleConsoleFTLBeaconMessage.cs
new file mode 100644 (file)
index 0000000..2b04514
--- /dev/null
@@ -0,0 +1,13 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shuttles.Events;
+
+/// <summary>
+/// Raised on a client when it wishes to FTL to a beacon.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class ShuttleConsoleFTLBeaconMessage : BoundUserInterfaceMessage
+{
+    public NetEntity Beacon;
+    public Angle Angle;
+}
similarity index 54%
rename from Content.Shared/Shuttles/Events/ShuttleConsoleFTLRequestMessage.cs
rename to Content.Shared/Shuttles/Events/ShuttleConsoleFTLPositionMessage.cs
index 73fd9d2c6455b042cb1f5cae1d575b37083cb2dc..4375d03721cd94adad2251fdadf5944d229a77cc 100644 (file)
@@ -1,3 +1,4 @@
+using Robust.Shared.Map;
 using Robust.Shared.Serialization;
 
 namespace Content.Shared.Shuttles.Events;
@@ -6,7 +7,8 @@ namespace Content.Shared.Shuttles.Events;
 /// Raised on the client when it wishes to travel somewhere.
 /// </summary>
 [Serializable, NetSerializable]
-public sealed class ShuttleConsoleFTLRequestMessage : BoundUserInterfaceMessage
+public sealed class ShuttleConsoleFTLPositionMessage : BoundUserInterfaceMessage
 {
-    public NetEntity Destination;
+    public MapCoordinates Coordinates;
+    public Angle Angle;
 }
diff --git a/Content.Shared/Shuttles/Events/StopAutodockRequestMessage.cs b/Content.Shared/Shuttles/Events/StopAutodockRequestMessage.cs
deleted file mode 100644 (file)
index e00e155..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Shuttles.Events;
-
-/// <summary>
-/// Raised on a client when it is no longer viewing a dock.
-/// </summary>
-[Serializable, NetSerializable]
-public sealed class StopAutodockRequestMessage : BoundUserInterfaceMessage
-{
-    public NetEntity DockEntity;
-}
diff --git a/Content.Shared/Shuttles/Systems/SharedDockingSystem.cs b/Content.Shared/Shuttles/Systems/SharedDockingSystem.cs
new file mode 100644 (file)
index 0000000..4b3f697
--- /dev/null
@@ -0,0 +1,78 @@
+using Content.Shared.Shuttles.Components;
+using Robust.Shared.Map;
+
+namespace Content.Shared.Shuttles.Systems;
+
+public abstract class SharedDockingSystem : EntitySystem
+{
+    [Dependency] protected readonly SharedTransformSystem XformSystem = default!;
+
+    public const float DockingHiglightRange = 4f;
+    public const float DockRange = 1f + 0.2f;
+    public static readonly double AlignmentTolerance = Angle.FromDegrees(15).Theta;
+
+    public bool CanShuttleDock(EntityUid? shuttle)
+    {
+        if (shuttle == null)
+            return false;
+
+        return !HasComp<PreventPilotComponent>(shuttle.Value);
+    }
+
+    public bool CanShuttleUndock(EntityUid? shuttle)
+    {
+        if (shuttle == null)
+            return false;
+
+        return !HasComp<PreventPilotComponent>(shuttle.Value);
+    }
+
+    public bool CanDock(MapCoordinates mapPosA, Angle worldRotA,
+                        MapCoordinates mapPosB, Angle worldRotB)
+    {
+        // Uh oh
+        if (mapPosA.MapId != mapPosB.MapId)
+            return false;
+
+        return InRange(mapPosA, mapPosB) && InAlignment(mapPosA, worldRotA, mapPosB, worldRotB);
+    }
+
+    public bool InRange(MapCoordinates mapPosA, MapCoordinates mapPosB)
+    {
+        return (mapPosA.Position - mapPosB.Position).Length() <= DockRange;
+    }
+
+    public bool InAlignment(MapCoordinates mapPosA, Angle worldRotA, MapCoordinates mapPosB, Angle worldRotB)
+    {
+        // Check if the nubs are in line with the two docks.
+        var worldRotToB = (mapPosB.Position - mapPosA.Position).ToWorldAngle();
+        var worldRotToA = (mapPosA.Position - mapPosB.Position).ToWorldAngle();
+
+        var aDiff = Angle.ShortestDistance((worldRotA - worldRotToB).Reduced(), Angle.Zero);
+        var bDiff = Angle.ShortestDistance((worldRotB - worldRotToA).Reduced(), Angle.Zero);
+
+        if (Math.Abs(aDiff.Theta) > AlignmentTolerance)
+            return false;
+
+        if (Math.Abs(bDiff.Theta) > AlignmentTolerance)
+            return false;
+
+        return true;
+    }
+
+    public bool CanDock(NetCoordinates coordinatesOne, Angle angleOne,
+                        NetCoordinates coordinatesTwo, Angle angleTwo)
+    {
+        // TODO: Dump the dock fixtures
+        var coordsA = GetCoordinates(coordinatesOne);
+        var coordsB = GetCoordinates(coordinatesTwo);
+
+        var mapPosA = XformSystem.ToMapCoordinates(coordsA);
+        var mapPosB = XformSystem.ToMapCoordinates(coordsB);
+
+        var worldRotA = XformSystem.GetWorldRotation(coordsA.EntityId) + angleOne;
+        var worldRotB = XformSystem.GetWorldRotation(coordsB.EntityId) + angleTwo;
+
+        return CanDock(mapPosA, worldRotA, mapPosB, worldRotB);
+    }
+}
index a069a2d2e2520c907b7c487f0d404c3f06e99ef3..c7e74823741922d1f632665e8f1cd9342d68e004 100644 (file)
@@ -4,7 +4,6 @@ namespace Content.Shared.Shuttles.Systems;
 
 public abstract class SharedRadarConsoleSystem : EntitySystem
 {
-    public const float DefaultMinRange = 64f;
     public const float DefaultMaxRange = 256f;
 
     protected virtual void UpdateState(EntityUid uid, RadarConsoleComponent component)
index f6224a8af6b6b3bb6820f1eccb23f93983488d40..38a76ae6d1f7fffcd9e92dc842166b9b5410b696 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Shared.ActionBlocker;
 using Content.Shared.Movement.Events;
+using Content.Shared.Shuttles.BUIStates;
 using Content.Shared.Shuttles.Components;
 using Robust.Shared.Serialization;
 
index 587b5fafe045c440c3e937b501bce3489ba75adb..72068c71f8fcabb73f5d6b551c3e059f4d1c97c7 100644 (file)
@@ -11,6 +11,43 @@ public abstract partial class SharedShuttleSystem
 
     protected virtual void UpdateIFFInterfaces(EntityUid gridUid, IFFComponent component) {}
 
+    public Color GetIFFColor(EntityUid gridUid, bool self = false, IFFComponent? component = null)
+    {
+        if (self)
+        {
+            return IFFComponent.SelfColor;
+        }
+
+        if (!Resolve(gridUid, ref component, false))
+        {
+            return IFFComponent.IFFColor;
+        }
+
+        return component.Color;
+    }
+
+    public string? GetIFFLabel(EntityUid gridUid, bool self = false, IFFComponent? component = null)
+    {
+        if (!IFFComponent.ShowIFFDefault)
+        {
+            return null;
+        }
+
+        var entName = MetaData(gridUid).EntityName;
+
+        if (self)
+        {
+            return entName;
+        }
+
+        if (Resolve(gridUid, ref component, false) && (component.Flags & (IFFFlags.HideLabel | IFFFlags.Hide)) != 0x0)
+        {
+            return null;
+        }
+
+        return string.IsNullOrEmpty(entName) ? Loc.GetString("shuttle-console-unknown") : entName;
+    }
+
     /// <summary>
     /// Sets the color for this grid to appear as on radar.
     /// </summary>
index 8471e83df51c633c3a7597bcf8d4be857b3f7dcb..324fd65c86044ae5854b48e196ee2bac309fec9e 100644 (file)
@@ -1,7 +1,198 @@
+using Content.Shared.Shuttles.BUIStates;
+using Content.Shared.Shuttles.Components;
+using Content.Shared.Shuttles.UI.MapObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics.Collision.Shapes;
+using Robust.Shared.Physics.Components;
+
 namespace Content.Shared.Shuttles.Systems;
 
 public abstract partial class SharedShuttleSystem : EntitySystem
 {
+    [Dependency] private   readonly IMapManager _mapManager = default!;
+    [Dependency] protected readonly SharedMapSystem Maps = default!;
+    [Dependency] protected readonly SharedTransformSystem XformSystem = default!;
+
+    public const float FTLRange = 512f;
+    public const float FTLBufferRange = 8f;
+
+    private EntityQuery<MapGridComponent> _gridQuery;
+    private EntityQuery<PhysicsComponent> _physicsQuery;
+    private EntityQuery<TransformComponent> _xformQuery;
+
+    private List<Entity<MapGridComponent>> _grids = new();
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        _gridQuery = GetEntityQuery<MapGridComponent>();
+        _physicsQuery = GetEntityQuery<PhysicsComponent>();
+        _xformQuery = GetEntityQuery<TransformComponent>();
+    }
+
+    /// <summary>
+    /// Returns whether an entity can FTL to the specified map.
+    /// </summary>
+    public bool CanFTLTo(EntityUid shuttleUid, MapId targetMap)
+    {
+        var mapUid = _mapManager.GetMapEntityId(targetMap);
+        var shuttleMap = _xformQuery.GetComponent(shuttleUid).MapID;
+
+        if (shuttleMap == targetMap)
+            return true;
+
+        if (!TryComp<FTLDestinationComponent>(mapUid, out var destination) ||
+            !destination.Enabled)
+        {
+            return false;
+        }
+
+        if (HasComp<FTLMapComponent>(mapUid))
+            return false;
+
+        return destination.Whitelist?.IsValid(shuttleUid, EntityManager) != false;
+    }
+
+    /// <summary>
+    /// Gets the list of map objects relevant for the specified map.
+    /// </summary>
+    public IEnumerable<(ShuttleExclusionObject Exclusion, MapCoordinates Coordinates)> GetExclusions(MapId mapId, List<ShuttleExclusionObject> exclusions)
+    {
+        foreach (var exc in exclusions)
+        {
+            var beaconCoords = XformSystem.ToMapCoordinates(GetCoordinates(exc.Coordinates));
+
+            if (beaconCoords.MapId != mapId)
+                continue;
+
+            yield return (exc, beaconCoords);
+        }
+    }
+
+    /// <summary>
+    /// Gets the list of map objects relevant for the specified map.
+    /// </summary>
+    public IEnumerable<(ShuttleBeaconObject Beacon, MapCoordinates Coordinates)> GetBeacons(MapId mapId, List<ShuttleBeaconObject> beacons)
+    {
+        foreach (var beacon in beacons)
+        {
+            var beaconCoords = XformSystem.ToMapCoordinates(GetCoordinates(beacon.Coordinates));
+
+            if (beaconCoords.MapId != mapId)
+                continue;
+
+            yield return (beacon, beaconCoords);
+        }
+    }
+
+    public bool CanDraw(EntityUid gridUid, PhysicsComponent? physics = null, IFFComponent? iffComp = null)
+    {
+        if (!Resolve(gridUid, ref physics))
+            return true;
+
+        if (physics.Mass < 10f)
+        {
+            return false;
+        }
+
+        if (!Resolve(gridUid, ref iffComp, false))
+        {
+            return true;
+        }
+
+        // Hide it entirely.
+        return (iffComp.Flags & IFFFlags.Hide) == 0x0;
+    }
+
+    public bool IsBeaconMap(EntityUid mapUid)
+    {
+        return TryComp(mapUid, out FTLDestinationComponent? ftlDest) && ftlDest.BeaconsOnly;
+    }
+
+    /// <summary>
+    /// Returns true if a beacon can be FTLd to.
+    /// </summary>
+    public bool CanFTLBeacon(NetCoordinates nCoordinates)
+    {
+        // Only beacons parented to map supported.
+        var coordinates = GetCoordinates(nCoordinates);
+        return HasComp<MapComponent>(coordinates.EntityId);
+    }
+
+    public float GetFTLRange(EntityUid shuttleUid) => FTLRange;
+
+    public float GetFTLBufferRange(EntityUid shuttleUid, MapGridComponent? grid = null)
+    {
+        if (!_gridQuery.Resolve(shuttleUid, ref grid))
+            return 0f;
+
+        var localAABB = grid.LocalAABB;
+        var maxExtent = localAABB.MaxDimension / 2f;
+        var range = maxExtent + FTLBufferRange;
+        return range;
+    }
+
+    /// <summary>
+    /// Returns true if the spot is free to be FTLd to (not close to any objects and in range).
+    /// </summary>
+    public bool FTLFree(EntityUid shuttleUid, EntityCoordinates coordinates, Angle angle, List<ShuttleExclusionObject>? exclusionZones)
+    {
+        if (!_physicsQuery.TryGetComponent(shuttleUid, out var shuttlePhysics) ||
+            !_xformQuery.TryGetComponent(shuttleUid, out var shuttleXform))
+        {
+            return false;
+        }
+
+        // Just checks if any grids inside of a buffer range at the target position.
+        _grids.Clear();
+        var ftlRange = FTLRange;
+        var mapCoordinates = coordinates.ToMap(EntityManager, XformSystem);
+
+        var ourPos = Maps.GetGridPosition((shuttleUid, shuttlePhysics, shuttleXform));
+
+        // This is the already adjusted position
+        var targetPosition = mapCoordinates.Position;
+
+        // Check range even if it's cross-map.
+        if ((targetPosition - ourPos).Length() > FTLRange)
+        {
+            return false;
+        }
+
+        // Check exclusion zones.
+        // This needs to be passed in manually due to PVS.
+        if (exclusionZones != null)
+        {
+            foreach (var exclusion in exclusionZones)
+            {
+                var exclusionCoords = XformSystem.ToMapCoordinates(GetCoordinates(exclusion.Coordinates));
+
+                if (exclusionCoords.MapId != mapCoordinates.MapId)
+                    continue;
+
+                if ((mapCoordinates.Position - exclusionCoords.Position).Length() <= exclusion.Range)
+                    return false;
+            }
+        }
+
+        var ourFTLBuffer = GetFTLBufferRange(shuttleUid);
+        var circle = new PhysShapeCircle(ourFTLBuffer + FTLBufferRange, targetPosition);
+
+        _mapManager.FindGridsIntersecting(mapCoordinates.MapId, circle, Robust.Shared.Physics.Transform.Empty,
+            ref _grids, includeMap: false);
+
+        // If any grids in range that aren't us then can't FTL.
+        foreach (var grid in _grids)
+        {
+            if (grid.Owner == shuttleUid)
+                continue;
+
+            return false;
+        }
+
+        return true;
+    }
 }
 
 [Flags]
diff --git a/Content.Shared/Shuttles/UI/MapObjects/GridMapObject.cs b/Content.Shared/Shuttles/UI/MapObjects/GridMapObject.cs
new file mode 100644 (file)
index 0000000..cb4194b
--- /dev/null
@@ -0,0 +1,7 @@
+namespace Content.Shared.Shuttles.UI.MapObjects;
+
+public record struct GridMapObject : IMapObject
+{
+    public string Name { get; set; }
+    public EntityUid Entity;
+}
diff --git a/Content.Shared/Shuttles/UI/MapObjects/IMapObject.cs b/Content.Shared/Shuttles/UI/MapObjects/IMapObject.cs
new file mode 100644 (file)
index 0000000..80e165d
--- /dev/null
@@ -0,0 +1,9 @@
+namespace Content.Shared.Shuttles.UI.MapObjects;
+
+/// <summary>
+/// Abstract map object representing a grid, beacon etc for use on the map screen.
+/// </summary>
+public interface IMapObject
+{
+    string Name { get; }
+}
diff --git a/Content.Shared/Shuttles/UI/MapObjects/ShuttleBeaconObject.cs b/Content.Shared/Shuttles/UI/MapObjects/ShuttleBeaconObject.cs
new file mode 100644 (file)
index 0000000..2be80f4
--- /dev/null
@@ -0,0 +1,7 @@
+using Robust.Shared.Map;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shuttles.UI.MapObjects;
+
+[Serializable, NetSerializable]
+public readonly record struct ShuttleBeaconObject(NetEntity Entity, NetCoordinates Coordinates, string Name) : IMapObject;
diff --git a/Content.Shared/Shuttles/UI/MapObjects/ShuttleExclusionObject.cs b/Content.Shared/Shuttles/UI/MapObjects/ShuttleExclusionObject.cs
new file mode 100644 (file)
index 0000000..a5ac93c
--- /dev/null
@@ -0,0 +1,7 @@
+using Robust.Shared.Map;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shuttles.UI.MapObjects;
+
+[Serializable, NetSerializable]
+public record struct ShuttleExclusionObject(NetCoordinates Coordinates, float Range, string Name = "") : IMapObject;
index 60a665c53605b865add0b1f6c046b2c4c8e1169c..2342ba8fb63cb7e066492ccbb53c1c512e5e3b21 100644 (file)
@@ -9,3 +9,9 @@
   copyright: '"hyperspace_progress.ogg" by /tg/station. Modified for looping by metalgearsloth.'
   license: CC-BY-SA-3.0
   source: https://github.com/tgstation/tgstation/tree/71a79f1f75902d2ccab27cbffb971b12b7ab042d/sound/runtime/hyperspace
+
+- files:
+  - radar_ping.ogg
+  copyright: User unfa from freesound.org Remixed to mono and .ogg by metalgearsloth.
+  license: CC0-1.0
+  source: https://freesound.org/people/unfa/sounds/215415/
\ No newline at end of file
diff --git a/Resources/Audio/Effects/Shuttle/radar_ping.ogg b/Resources/Audio/Effects/Shuttle/radar_ping.ogg
new file mode 100644 (file)
index 0000000..de5b934
Binary files /dev/null and b/Resources/Audio/Effects/Shuttle/radar_ping.ogg differ
index a89a367d06e327edb12ed4a204e15e007e8c1212..80e61a281266db817e6cb51c2b7aacdc2caefbb7 100644 (file)
@@ -1,41 +1,52 @@
 shuttle-pilot-start = Piloting ship
 shuttle-pilot-end = Stopped piloting
 
-shuttle-console-in-ftl = Can't FTL while in FTL!
-shuttle-console-proximity = Too close to nearby objects
-shuttle-console-prevent = You are unable to pilot this ship.
+shuttle-console-in-ftl = Currently in FTL
+shuttle-console-mass = Too large to FTL
+shuttle-console-prevent = You are unable to pilot this ship
 
-# Display
-shuttle-console-display-label = Display
-
-shuttle-console-ftl-state = FTL State
-shuttle-console-ftl-available = Available
-shuttle-console-ftl-starting = Starting
-shuttle-console-ftl-travelling = Travelling
-shuttle-console-ftl-arriving = Arriving
-shuttle-console-ftl-cooldown = Cooldown
+# NAV
 
-shuttle-console-ftl-timer = FTL Time
+shuttle-console-display-label = Display
 
-shuttle-console-max-radar = Max radar range:
-shuttle-console-radar = Radar range:
 shuttle-console-position = Position:
 shuttle-console-orientation = Orientation:
 shuttle-console-linear-velocity = Linear velocity:
 shuttle-console-angular-velocity = Angular velocity:
 
-shuttle-console-dock-label = Docking ports
-shuttle-console-docked = {$index} (Docked)
-shuttle-console-dock-button = Dock {$suffix}
-
-shuttle-console-hyperspace-label = FTL destinations
-shuttle-console-hyperspace-none = No destinations found
-
 shuttle-console-unknown = Unknown
 shuttle-console-iff-label = {$name} ({$distance}m)
+shuttle-console-exclusion = Exclusion area
 
-# Buttons
-shuttle-console-strafing = Strafing mode
+shuttle-console-nav-settings = Settings
 shuttle-console-iff-toggle = Show IFF
 shuttle-console-dock-toggle = Show docks
+
+# MAP
+
+shuttle-console-ftl-label = FTL Status
+shuttle-console-ftl-state-Available = Available
+shuttle-console-ftl-state-Starting = Starting
+shuttle-console-ftl-state-Travelling = Travelling
+shuttle-console-ftl-state-Arriving = Arriving
+shuttle-console-ftl-state-Cooldown = Cooldown
+
+shuttle-console-map-settings = Settings
+shuttle-console-ftl-button = FTL
+shuttle-console-map-rebuild = Scan for objects
+shuttle-console-map-beacons = Show beacons
+
+shuttle-console-no-signal = No signal
+
+shuttle-console-map-objects = Sector objects
+
+# DOCK
+shuttle-console-docked = Docked objects
+
+shuttle-console-view = View
 shuttle-console-undock = Undock
+shuttle-console-dock = Dock
+shuttle-console-docks-label = Docks
+
+shuttle-console-undock-fail = Undocking failed
+shuttle-console-dock-fail = Docking failed
index 909fe6814054db7c3a444410ed6a3a8c87766969..4d1172198cabae45e169cee1b8fef99327d0451e 100644 (file)
@@ -29,10 +29,6 @@ entities:
       name: Syndicate Outpost
     - type: Transform
       parent: 1295
-    - type: FTLDestination
-      whitelist:
-        tags:
-        - Syndicate
     - type: MapGrid
       chunks:
         -1,-1:
@@ -1636,6 +1632,10 @@ entities:
     - type: MetaData
     - type: Transform
     - type: Map
+    - type: FTLDestination
+      whitelist:
+        tags:
+        - Syndicate
     - type: PhysicsMap
     - type: Broadphase
     - type: OccluderTree
index 084d88ff089632531e8001e4b1ed636e9c68c086..4e888ae05cf4486b69d6c1c32a98bfa5313f0cfc 100644 (file)
@@ -24,6 +24,9 @@ entities:
     components:
     - type: MetaData
       name: GX-13 Infiltrator
+    - type: Tag
+      tags:
+      - Syndicate
     - type: Transform
       pos: 0.64252126,4.1776605
       parent: invalid
index 9ef9c0de5a9340a7285d5daa3be7eb558791b4bb..0e9117951ce11496913f511985bf4426c1149882 100644 (file)
@@ -3,6 +3,16 @@
   parent: MarkerBase
   name: FTL point
   components:
-    - type: FTLDestination
+    - type: FTLBeacon
+    - type: Sprite
+      state: pink
+
+- type: entity
+  id: FTLExclusion
+  parent: MarkerBase
+  name: FTL exclusion point
+  components:
+    - type: FTLExclusion
+      range: 32
     - type: Sprite
       state: pink
index c9011ec91f614bd0e3720286b57d88c92b72fb5e..8f58464bd4381406aaf9068d584b7048034eddcd 100644 (file)
     tags:
       - Syndicate
   - type: RadarConsole
-    maxRange: 1536
+    maxRange: 384
   - type: WorldLoader
     radius: 1536
   - type: PointLight
index 8d46dff59a028cb8cc9fe414657425f4e708a91e..f43fac752e5848458884dfa940a2898b0989285d 100644 (file)
@@ -1,6 +1,7 @@
 # Markers
 - type: entity
   id: SalvageShuttleMarker
+  name: Salvage shuttle marker
   parent: FTLPoint
 
 # Biome mods -> at least 1 required
diff --git a/Resources/Textures/Parallaxes/space_map2.png b/Resources/Textures/Parallaxes/space_map2.png
new file mode 100644 (file)
index 0000000..d4e69b6
Binary files /dev/null and b/Resources/Textures/Parallaxes/space_map2.png differ
diff --git a/Resources/Textures/Parallaxes/weh.txt b/Resources/Textures/Parallaxes/weh.txt
new file mode 100644 (file)
index 0000000..0d2bc48
--- /dev/null
@@ -0,0 +1 @@
+spacescape
\ No newline at end of file