diff --git a/Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml b/Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml
index 9e0f9d182ea..01259b60f7a 100644
--- a/Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml
+++ b/Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml
@@ -12,7 +12,7 @@
-
+
@@ -23,7 +23,7 @@
-
+
diff --git a/Content.Client/Atmos/Monitor/UI/Widgets/PumpControl.xaml b/Content.Client/Atmos/Monitor/UI/Widgets/PumpControl.xaml
index 632e44a4587..5fb4e5f0c8c 100644
--- a/Content.Client/Atmos/Monitor/UI/Widgets/PumpControl.xaml
+++ b/Content.Client/Atmos/Monitor/UI/Widgets/PumpControl.xaml
@@ -1,7 +1,7 @@
-
+
diff --git a/Content.Client/Atmos/Monitor/UI/Widgets/ScrubberControl.xaml b/Content.Client/Atmos/Monitor/UI/Widgets/ScrubberControl.xaml
index 1cb9c9ed5b4..34c1a9dd1a9 100644
--- a/Content.Client/Atmos/Monitor/UI/Widgets/ScrubberControl.xaml
+++ b/Content.Client/Atmos/Monitor/UI/Widgets/ScrubberControl.xaml
@@ -1,7 +1,7 @@
-
+
@@ -26,7 +26,7 @@
-
+
diff --git a/Content.Client/Atmos/Monitor/UI/Widgets/SensorInfo.xaml b/Content.Client/Atmos/Monitor/UI/Widgets/SensorInfo.xaml
index b90ca3f1f66..005e6807b37 100644
--- a/Content.Client/Atmos/Monitor/UI/Widgets/SensorInfo.xaml
+++ b/Content.Client/Atmos/Monitor/UI/Widgets/SensorInfo.xaml
@@ -1,5 +1,5 @@
-
+
@@ -10,7 +10,7 @@
-
+
diff --git a/Content.Client/Atmos/Monitor/UI/Widgets/ThresholdControl.xaml b/Content.Client/Atmos/Monitor/UI/Widgets/ThresholdControl.xaml
index 0f53673da10..635a70f532c 100644
--- a/Content.Client/Atmos/Monitor/UI/Widgets/ThresholdControl.xaml
+++ b/Content.Client/Atmos/Monitor/UI/Widgets/ThresholdControl.xaml
@@ -1,7 +1,7 @@
-
+
diff --git a/Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml b/Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml
index 176acbf29b5..5acb142a607 100644
--- a/Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml
+++ b/Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml
@@ -11,7 +11,7 @@
-
+
diff --git a/Content.Client/Cargo/UI/CargoPalletMenu.xaml b/Content.Client/Cargo/UI/CargoPalletMenu.xaml
index 489c6cb8f61..d791c9ce3af 100644
--- a/Content.Client/Cargo/UI/CargoPalletMenu.xaml
+++ b/Content.Client/Cargo/UI/CargoPalletMenu.xaml
@@ -1,9 +1,9 @@
-
+ MinSize="300 150"
+ Title="{Loc 'cargo-pallet-console-menu-title'}">
+
diff --git a/Content.Client/Cargo/UI/CargoPalletMenu.xaml.cs b/Content.Client/Cargo/UI/CargoPalletMenu.xaml.cs
index c46ecf562d6..3ac0117dc70 100644
--- a/Content.Client/Cargo/UI/CargoPalletMenu.xaml.cs
+++ b/Content.Client/Cargo/UI/CargoPalletMenu.xaml.cs
@@ -17,7 +17,6 @@ public CargoPalletMenu()
RobustXamlLoader.Load(this);
SellButton.OnPressed += OnSellPressed;
AppraiseButton.OnPressed += OnAppraisePressed;
- Title = Loc.GetString("cargo-pallet-console-menu-title");
}
public void SetAppraisal(int amount)
diff --git a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml
index 752c9cc6c2c..f46e319abeb 100644
--- a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml
+++ b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml
@@ -11,7 +11,7 @@
-
+
-
+
-
+
-
+
diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs
index fcecbad465a..340cc9af891 100644
--- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs
+++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs
@@ -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 CrewMonitoringNavMapControl() : base()
{
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 CrewMonitoringNavMapControl() : base()
{
PanelOverride = new StyleBoxFlat
{
- BackgroundColor = _backgroundColor,
+ BackgroundColor = BackgroundColor,
},
Margin = new Thickness(5f, 10f),
@@ -43,9 +44,9 @@ public CrewMonitoringNavMapControl() : base()
this.AddChild(_trackedEntityPanel);
}
- protected override void Draw(DrawingHandleScreen handle)
+ protected override void FrameUpdate(FrameEventArgs args)
{
- base.Draw(handle);
+ base.FrameUpdate(args);
if (Focus == null)
{
diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs
index 645243b0a3a..39326c8a99c 100644
--- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs
+++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs
@@ -23,7 +23,6 @@ namespace Content.Client.Medical.CrewMonitoring;
[GenerateTypedNameReferences]
public sealed partial class CrewMonitoringWindow : FancyWindow
{
- private List _rowsContent = new();
private readonly IEntityManager _entManager;
private readonly IPrototypeManager _prototypeManager;
private readonly SpriteSystem _spriteSystem;
@@ -100,7 +99,6 @@ public void ShowSensors(List sensors, EntityUid monitor, Entit
};
SensorsTable.AddChild(spacer);
- _rowsContent.Add(spacer);
}
var deparmentLabel = new RichTextLabel()
@@ -113,7 +111,6 @@ public void ShowSensors(List sensors, EntityUid monitor, Entit
deparmentLabel.StyleClasses.Add(StyleNano.StyleClassTooltipActionDescription);
SensorsTable.AddChild(deparmentLabel);
- _rowsContent.Add(deparmentLabel);
PopulateDepartmentList(departmentSensors);
}
@@ -129,7 +126,6 @@ public void ShowSensors(List sensors, EntityUid monitor, Entit
};
SensorsTable.AddChild(spacer);
- _rowsContent.Add(spacer);
var deparmentLabel = new RichTextLabel()
{
@@ -141,7 +137,6 @@ public void ShowSensors(List sensors, EntityUid monitor, Entit
deparmentLabel.StyleClasses.Add(StyleNano.StyleClassTooltipActionDescription);
SensorsTable.AddChild(deparmentLabel);
- _rowsContent.Add(deparmentLabel);
PopulateDepartmentList(remainingSensors);
}
@@ -175,7 +170,6 @@ private void PopulateDepartmentList(IEnumerable departmentSens
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 @@ private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollP
private void ClearOutDatedData()
{
SensorsTable.RemoveAllChildren();
- _rowsContent.Clear();
NavMap.TrackedCoordinates.Clear();
NavMap.TrackedEntities.Clear();
NavMap.LocalizedNames.Clear();
diff --git a/Content.Client/Pinpointer/UI/NavMapControl.cs b/Content.Client/Pinpointer/UI/NavMapControl.cs
index 3b426e73d89..a8ec7b37a0b 100644
--- a/Content.Client/Pinpointer/UI/NavMapControl.cs
+++ b/Content.Client/Pinpointer/UI/NavMapControl.cs
@@ -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? TrackedEntitySelectedAction;
public event Action? 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 _sRGBLookUp = new Dictionary();
- public Color _backgroundColor;
- public float _backgroundOpacity = 0.9f;
+ private Dictionary _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();
- _transformSystem = _entManager.System();
- _backgroundColor = Color.FromSrgb(TileColor.WithAlpha(_backgroundOpacity));
+ _transformSystem = EntManager.System();
+ BackgroundColor = Color.FromSrgb(TileColor.WithAlpha(BackgroundOpacity));
RectClipContent = true;
HorizontalExpand = true;
@@ -145,21 +140,16 @@ public NavMapControl() : base(MinDisplayedRange, MaxDisplayedRange, DefaultDispl
_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 void ForceNavMapUpdate()
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 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args)
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 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args)
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 @@ 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;
-
- if (_offset != Vector2.Zero)
+ if (Offset != Vector2.Zero)
_recenter.Disabled = false;
-
else
_recenter.Disabled = true;
}
@@ -269,36 +238,21 @@ protected override void Draw(DrawingHandleScreen handle)
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 @@ protected override void Draw(DrawingHandleScreen handle)
{
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 @@ protected override void Draw(DrawingHandleScreen handle)
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 @@ protected override void Draw(DrawingHandleScreen handle)
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 @@ protected override void Draw(DrawingHandleScreen handle)
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 @@ protected override void Draw(DrawingHandleScreen handle)
{
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 @@ protected override void Draw(DrawingHandleScreen handle)
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 Dictionary> GetDecodedWallChunks
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());
}
}
diff --git a/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs b/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs
index 9f537f38587..902d6bb7e60 100644
--- a/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs
+++ b/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs
@@ -33,7 +33,7 @@ public PowerMonitoringConsoleNavMapControl() : base()
// 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 void DrawCableNetwork(DrawingHandleScreen handle, Dictionary
+ /// Gets the parallax to use for the specified map or uses the fallback if not available.
+ ///
+ public Texture GetTexture(Entity entity)
+ {
+ if (!Resolve(entity, ref entity.Comp, false))
+ {
+ return _resource.GetTexture(ShuttleMapParallaxComponent.FallbackTexture);
+ }
+
+ return _resource.GetTexture(entity.Comp.TexturePath);
+ }
+
+ ///
+ /// Gets the map coordinates of a map object.
+ ///
+ 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);
+
+ if (HasComp(grid.Entity))
+ {
+ return new MapCoordinates(gridXform.LocalPosition, gridXform.MapID);
+ }
+
+ Entity 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
index 00000000000..c3c18021660
--- /dev/null
+++ b/Content.Client/Shuttles/UI/BaseShuttleControl.xaml
@@ -0,0 +1,2 @@
+
diff --git a/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs b/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs
new file mode 100644
index 00000000000..035823af430
--- /dev/null
+++ b/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs
@@ -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;
+
+///
+/// Provides common functionality for radar-like displays on shuttle consoles.
+///
+[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 GridData = new();
+
+ // Per-draw caching
+ private readonly List _gridTileList = new();
+ private readonly HashSet _gridNeighborSet = new();
+ private readonly List<(Vector2 Start, Vector2 End)> _edges = new();
+
+ private Vector2[] _allVertices = Array.Empty();
+
+ 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();
+ Font = new VectorFont(IoCManager.Resolve().GetResource("/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 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(_allVertices, start, count), color.WithAlpha(alpha));
+ }
+
+ handle.DrawPrimitives(DrawPrimitiveTopology.LineList, new Span(_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 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 Vertices = new();
+
+ ///
+ /// Vertices index from when edges start.
+ ///
+ 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
index 00000000000..bae625658b7
--- /dev/null
+++ b/Content.Client/Shuttles/UI/DockObject.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/Content.Client/Shuttles/UI/DockObject.xaml.cs b/Content.Client/Shuttles/UI/DockObject.xaml.cs
new file mode 100644
index 00000000000..9dae6b7a4d3
--- /dev/null
+++ b/Content.Client/Shuttles/UI/DockObject.xaml.cs
@@ -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
index c0ddeff86c2..00000000000
--- a/Content.Client/Shuttles/UI/DockingControl.cs
+++ /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;
-
-///
-/// Displays the docking view from a specific docking port
-///
-[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;
-
- ///
- /// Stored by GridID then by docks
- ///
- public Dictionary> Docks = new();
-
- private List> _grids = new();
-
- public DockingControl()
- {
- _entManager = IoCManager.Resolve();
- _mapManager = IoCManager.Resolve();
- _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(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(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();
-
- _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(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
index 00000000000..7c112b6daf5
--- /dev/null
+++ b/Content.Client/Shuttles/UI/DockingScreen.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Shuttles/UI/DockingScreen.xaml.cs b/Content.Client/Shuttles/UI/DockingScreen.xaml.cs
new file mode 100644
index 00000000000..c0aa7942148
--- /dev/null
+++ b/Content.Client/Shuttles/UI/DockingScreen.xaml.cs
@@ -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;
+
+ ///
+ /// Stored by GridID then by docks
+ ///
+ public Dictionary> Docks = new();
+
+ ///
+ /// Store the dock buttons for the side buttons.
+ ///
+ private readonly Dictionary _ourDockButtons = new();
+
+ public event Action? DockRequest;
+ public event Action? UndockRequest;
+
+ public DockingScreen()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ _shuttles = _entManager.System();
+
+ 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();
+
+ 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
index 00000000000..7db61b9e349
--- /dev/null
+++ b/Content.Client/Shuttles/UI/MapScreen.xaml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Shuttles/UI/MapScreen.xaml.cs b/Content.Client/Shuttles/UI/MapScreen.xaml.cs
new file mode 100644
index 00000000000..65a11d345d7
--- /dev/null
+++ b/Content.Client/Shuttles/UI/MapScreen.xaml.cs
@@ -0,0 +1,535 @@
+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 _beacons = new();
+ private List _exclusions = new();
+
+ ///
+ /// When the next FTL state change happens.
+ ///
+ 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? RequestFTL;
+ public event Action? RequestBeaconFTL;
+
+ private readonly Dictionary _mapHeadings = new();
+ private readonly Dictionary> _mapObjects = new();
+ private readonly List<(MapId mapId, IMapObject mapobj)> _pendingMapObjects = new();
+
+ ///
+ /// Store the names of map object controls for re-sorting later.
+ ///
+ private Dictionary _mapObjectControls = new();
+
+ private List _sortChildren = new();
+
+ public MapScreen()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ _audio = _entManager.System();
+ _maps = _entManager.System();
+ _shuttles = _entManager.System();
+ _xformSystem = _entManager.System();
+
+ 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;
+ // Fallback in case no FTL state or the likes.
+ default:
+ SetFTLAllowed(false);
+ _ftlStyle.BackgroundColor = Color.FromHex("#B02E26");
+ MapRadar.InFtl = false;
+ break;
+ }
+
+ 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);
+ }
+ }
+
+ ///
+ /// Does a sonar-like effect on the map.
+ ///
+ 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();
+ }
+
+ ///
+ /// Clears all sector objects across all maps (e.g. if we start FTLing or need to re-ping).
+ ///
+ private void ClearMapObjects()
+ {
+ _mapObjectControls.Clear();
+ HyperspaceDestinations.DisposeAllChildren();
+ _pendingMapObjects.Clear();
+ _mapObjects.Clear();
+ _mapHeadings.Clear();
+ }
+
+ ///
+ /// Gets all map objects at time of ping and adds them to pending to be added over time.
+ ///
+ private void RebuildMapObjects()
+ {
+ ClearMapObjects();
+
+ if (_shuttleEntity == null)
+ return;
+
+ var mapComps = _entManager.AllEntityQueryEnumerator();
+ 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(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());
+ });
+ }
+
+ ///
+ /// Hides other maps upon the specified collapsible being selected (AKA hacky collapsible groups).
+ ///
+ private void HideOtherCollapsibles(Collapsible collapsible)
+ {
+ foreach (var child in HyperspaceDestinations.Children)
+ {
+ if (child is not Collapsible childCollapse || childCollapse == collapsible)
+ continue;
+
+ childCollapse.BodyVisible = false;
+ }
+ }
+
+ ///
+ /// Returns true if we shouldn't be able to select the FTL button.
+ ///
+ 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;
+ }
+
+ ///
+ /// Adds a map object to the specified sector map.
+ ///
+ 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
index 00000000000..c97aeda05be
--- /dev/null
+++ b/Content.Client/Shuttles/UI/NavScreen.xaml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Shuttles/UI/NavScreen.xaml.cs b/Content.Client/Shuttles/UI/NavScreen.xaml.cs
new file mode 100644
index 00000000000..b7b757ea483
--- /dev/null
+++ b/Content.Client/Shuttles/UI/NavScreen.xaml.cs
@@ -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();
+
+ 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}";
+ }
+}
diff --git a/Content.Client/Shuttles/UI/RadarConsoleWindow.xaml b/Content.Client/Shuttles/UI/RadarConsoleWindow.xaml
index 26aca5da629..f62e59b4ad1 100644
--- a/Content.Client/Shuttles/UI/RadarConsoleWindow.xaml
+++ b/Content.Client/Shuttles/UI/RadarConsoleWindow.xaml
@@ -4,7 +4,7 @@
Title="{Loc 'radar-console-window-title'}"
SetSize="648 648"
MinSize="256 256">
-
diff --git a/Content.Client/Shuttles/UI/RadarConsoleWindow.xaml.cs b/Content.Client/Shuttles/UI/RadarConsoleWindow.xaml.cs
index 1a6f216e8b3..7f1149365b2 100644
--- a/Content.Client/Shuttles/UI/RadarConsoleWindow.xaml.cs
+++ b/Content.Client/Shuttles/UI/RadarConsoleWindow.xaml.cs
@@ -9,20 +9,15 @@ namespace Content.Client.Shuttles.UI;
[GenerateTypedNameReferences]
public sealed partial class RadarConsoleWindow : FancyWindow,
- IComputerWindow
+ IComputerWindow
{
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
index 45e6da22f42..00000000000
--- a/Content.Client/Shuttles/UI/RadarControl.cs
+++ /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;
-
-///
-/// Displays nearby grids inside of a control.
-///
-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;
-
- ///
- /// Used to transform all of the radar objects. Typically is a shuttle console parented to a grid.
- ///
- private EntityCoordinates? _coordinates;
-
- private Angle? _rotation;
-
- ///
- /// Shows a label on each radar object.
- ///
- private Dictionary _iffControls = new();
-
- private Dictionary> _docks = new();
-
- public bool ShowIFF { get; set; } = true;
- public bool ShowDocks { get; set; } = true;
-
- ///
- /// Currently hovered docked to show on the map.
- ///
- public NetEntity? HighlightedDock;
-
- ///
- /// Raised if the user left-clicks on the radar control with the relevant entitycoordinates.
- ///
- public Action? OnRadarClick;
-
- private List> _grids = new();
-
- public RadarControl() : base(64f, 256f, 256f)
- {
- _transform = _entManager.System();
- }
-
- 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);
- }
-
- ///
- /// Gets the entitycoordinates of where the mouseposition is, relative to the control.
- ///
- [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();
- var xformQuery = _entManager.GetEntityQuery();
- var fixturesQuery = _entManager.GetEntityQuery();
- var bodyQuery = _entManager.GetEntityQuery();
-
- 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(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();
-
- _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(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();
-
- 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;
- }
-}
diff --git a/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml b/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml
index 32438632609..ec5340e6b47 100644
--- a/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml
+++ b/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml
@@ -2,109 +2,42 @@
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">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ SetSize="960 762"
+ MinSize="960 762">
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
+
diff --git a/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs b/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs
index d67227549a9..a4b42fb672c 100644
--- a/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs
+++ b/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs
@@ -1,340 +1,148 @@
+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
+ IComputerWindow
{
- private readonly IEntityManager _entManager;
- private readonly IGameTiming _timing;
+ [Dependency] private readonly IEntityManager _entManager = default!;
- private EntityUid? _shuttleEntity;
+ private ShuttleConsoleMode _mode = ShuttleConsoleMode.Nav;
- ///
- /// Currently selected dock button for camera.
- ///
- private BaseButton? _selectedDock;
+ public event Action? RequestFTL;
+ public event Action? RequestBeaconFTL;
- ///
- /// Stored by grid entityid then by states
- ///
- private readonly Dictionary> _docks = new();
-
- private readonly Dictionary _destinations = new();
-
- ///
- /// Next FTL state change.
- ///
- public TimeSpan FTLTime;
-
- public Action? UndockPressed;
- public Action? StartAutodockPressed;
- public Action? StopAutodockPressed;
- public Action? DestinationPressed;
+ public event Action? DockRequest;
+ public event Action? UndockRequest;
public ShuttleConsoleWindow()
{
RobustXamlLoader.Load(this);
- _entManager = IoCManager.Resolve();
- _timing = IoCManager.Resolve();
-
- 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 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);
}
- ///
- /// Shows a docking camera instead of radar screen.
- ///
- 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(_shuttleEntity, out var gridBody) ||
- !_entManager.TryGetComponent(_shuttleEntity, out var gridXform))
- {
+ if (_mode == mode)
return;
- }
-
- if (_entManager.TryGetComponent(_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
index 00000000000..b1bbb4c6290
--- /dev/null
+++ b/Content.Client/Shuttles/UI/ShuttleDockControl.xaml
@@ -0,0 +1 @@
+
diff --git a/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs
new file mode 100644
index 00000000000..f03c4402952
--- /dev/null
+++ b/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs
@@ -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> _grids = new();
+
+ private readonly HashSet _drawnDocks = new();
+ private readonly Dictionary _dockButtons = new();
+
+ ///
+ /// Store buttons for every other dock
+ ///
+ private readonly Dictionary _dockContainers = new();
+
+ private static readonly TimeSpan DockChangeCooldown = TimeSpan.FromSeconds(0.5);
+
+ ///
+ /// Rate-limiting for docking changes
+ ///
+ private TimeSpan _nextDockChange;
+
+ public event Action? OnViewDock;
+ public event Action? DockRequest;
+ public event Action? UndockRequest;
+
+ public ShuttleDockControl() : base(2f, 32f, 8f)
+ {
+ RobustXamlLoader.Load(this);
+ _dockSystem = EntManager.System();
+ _shuttles = EntManager.System();
+ _xformSystem = EntManager.System();
+ MinSize = new Vector2(SizeFull, SizeFull);
+ }
+
+ public void SetViewedDock(DockingPortState? dockState)
+ {
+ _viewedState = dockState;
+
+ if (dockState != null)
+ {
+ _coordinates = EntManager.GetCoordinates(dockState.Coordinates);
+ _angle = dockState.Angle;
+ OnViewDock?.Invoke(dockState.Entity);
+ }
+ else
+ {
+ _coordinates = null;
+ _angle = null;
+ }
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+ HideDocks();
+ _drawnDocks.Clear();
+ }
+
+ protected override void Draw(DrawingHandleScreen handle)
+ {
+ base.Draw(handle);
+
+ DrawBacking(handle);
+
+ if (_coordinates == null ||
+ _angle == null ||
+ DockState == null ||
+ !EntManager.TryGetComponent(GridEntity, out var gridXform))
+ {
+ DrawNoSignal(handle);
+ return;
+ }
+
+ DrawCircles(handle);
+ var gridNent = EntManager.GetNetEntity(GridEntity);
+ var mapPos = _xformSystem.ToMapCoordinates(_coordinates.Value);
+ var 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 = PixelSizeBox;
+ _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 / UIScale - container.DesiredSize / 2 - new Vector2(0f, 0.75f) * MinimapScale;
+ SetPosition(container, containerPos);
+ }
+
+ _drawnDocks.Add(dock);
+ }
+ }
+
+ // Draw the dock's collision
+ var invertedPosition = Vector2.Zero;
+ invertedPosition.Y = -invertedPosition.Y;
+ var rotation = 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
index 00000000000..18abb9c9bc2
--- /dev/null
+++ b/Content.Client/Shuttles/UI/ShuttleMapControl.xaml
@@ -0,0 +1 @@
+
diff --git a/Content.Client/Shuttles/UI/ShuttleMapControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleMapControl.xaml.cs
new file mode 100644
index 00000000000..55ef55a6c77
--- /dev/null
+++ b/Content.Client/Shuttles/UI/ShuttleMapControl.xaml.cs
@@ -0,0 +1,613 @@
+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 _physicsQuery;
+
+ ///
+ /// Toggles FTL mode on. This shows a pre-vis for FTLing a grid.
+ ///
+ public bool FtlMode;
+
+ private Angle _ftlAngle;
+
+ ///
+ /// Are we currently in FTL.
+ ///
+ public bool InFtl;
+
+ ///
+ /// Raised when a request to FTL to a particular spot is raised.
+ ///
+ public event Action? RequestFTL;
+
+ public event Action? RequestBeaconFTL;
+
+ ///
+ /// Set every draw to determine the beacons that are clickable for mouse events
+ ///
+ private List _beacons = new();
+
+ // Per frame data to avoid re-allocating
+ private readonly List _mapObjects = new();
+ private readonly Dictionary> _verts = new();
+ private readonly Dictionary> _edges = new();
+ private readonly Dictionary> _strings = new();
+ private readonly List _viewportExclusions = new();
+
+ public ShuttleMapControl() : base(256f, 512f, 512f)
+ {
+ RobustXamlLoader.Load(this);
+ _shuttles = EntManager.System();
+ _xformSystem = EntManager.System();
+ var cache = IoCManager.Resolve();
+
+ _physicsQuery = EntManager.GetEntityQuery();
+
+ _font = new VectorFont(cache.GetResource("/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));
+ }
+ }
+ }
+
+ ///
+ /// Gets the map objects that intersect the viewport.
+ ///
+ ///
+ ///
+ private List GetViewportMapObjects(Matrix3 matty, List mapObjects)
+ {
+ var results = new List();
+ var viewBox = SizeBox.Scale(1.2f);
+
+ foreach (var mapObj in mapObjects)
+ {
+ // If it's a grid-map skip it.
+ if (mapObj is GridMapObject gridObj && EntManager.HasComponent(gridObj.Entity))
+ continue;
+
+ 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(_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 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(_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 edges, List verts, ValueList 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);
+ }
+
+ ///
+ /// Returns the beacons that intersect the viewport.
+ ///
+ private IEnumerable<(string Beacon, MapCoordinates Coordinates, IMapObject MapObject)> GetBeacons(List 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 GetMapObject(Vector2 localPos, Angle angle, float scale = 1f, bool scalePosition = false)
+ {
+ // Constant size diamonds
+ var diamondRadius = GetMapObjectRadius();
+
+ var mapObj = new ValueList(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 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;
+ }
+
+ ///
+ /// Sets the map objects for the next draw.
+ ///
+ public void SetMapObjects(Dictionary> 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
index 00000000000..f517a30c181
--- /dev/null
+++ b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml
@@ -0,0 +1 @@
+
diff --git a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
new file mode 100644
index 00000000000..00ee6890b28
--- /dev/null
+++ b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
@@ -0,0 +1,290 @@
+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;
+
+ ///
+ /// Used to transform all of the radar objects. Typically is a shuttle console parented to a grid.
+ ///
+ private EntityCoordinates? _coordinates;
+
+ private Angle? _rotation;
+
+ private Dictionary> _docks = new();
+
+ public bool ShowIFF { get; set; } = true;
+ public bool ShowDocks { get; set; } = true;
+
+ ///
+ /// Raised if the user left-clicks on the radar control with the relevant entitycoordinates.
+ ///
+ public Action? OnRadarClick;
+
+ private List> _grids = new();
+
+ public ShuttleNavControl() : base(64f, 256f, 256f)
+ {
+ RobustXamlLoader.Load(this);
+ _shuttles = EntManager.System();
+ _transform = EntManager.System();
+ }
+
+ 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);
+ }
+
+ ///
+ /// Gets the entitycoordinates of where the mouseposition is, relative to the control.
+ ///
+ [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();
+ var fixturesQuery = EntManager.GetEntityQuery();
+ var bodyQuery = EntManager.GetEntityQuery();
+
+ 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(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(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}"));
+
+ // yes 1.0 scale is intended here.
+ var labelDimensions = handle.GetDimensions(Font, labelText, 1f);
+
+ // y-offset the control to always render below the grid (vertically)
+ var yOffset = Math.Max(gridBounds.Height, gridBounds.Width) * MinimapScale / 1.8f;
+
+ // 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)- 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, PixelWidth - labelDimensions.X ),
+ Math.Clamp(uiPosition.Y, 0f, PixelHeight - 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
index d56790431fa..00000000000
--- a/Content.Client/UserInterface/Controls/MapGridControl.cs
+++ /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;
-
-///
-/// Handles generic grid-drawing data, with zoom and dragging.
-///
-public abstract class MapGridControl : Control
-{
- [Dependency] protected readonly IGameTiming Timing = default!;
-
- protected const float ScrollSensitivity = 8f;
-
- ///
- /// UI pixel radius.
- ///
- public const int UIDisplayRadius = 320;
- protected const int MinimapMargin = 4;
-
- protected float WorldMinRange;
- protected float WorldMaxRange;
- public float WorldRange;
-
- ///
- /// We'll lerp between the radarrange and actual range
- ///
- protected float ActualRadarRange;
-
- ///
- /// Controls the maximum distance that will display.
- ///
- 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? 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
index 00000000000..7003afa5265
--- /dev/null
+++ b/Content.Client/UserInterface/Controls/MapGridControl.xaml
@@ -0,0 +1 @@
+
diff --git a/Content.Client/UserInterface/Controls/MapGridControl.xaml.cs b/Content.Client/UserInterface/Controls/MapGridControl.xaml.cs
new file mode 100644
index 00000000000..f6b0929f3b2
--- /dev/null
+++ b/Content.Client/UserInterface/Controls/MapGridControl.xaml.cs
@@ -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;
+
+///
+/// Handles generic grid-drawing data, with zoom and dragging.
+///
+[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;
+
+ ///
+ /// Control offset from whatever is being tracked.
+ ///
+ public Vector2 Offset;
+
+ ///
+ /// If the control is being recentered what is the target offset to reach.
+ ///
+ public Vector2 TargetOffset;
+
+ private bool _draggin;
+ protected Vector2 StartDragPosition;
+ protected bool Recentering;
+
+ protected const float ScrollSensitivity = 8f;
+
+ protected float RecenterMinimum = 0.05f;
+
+ ///
+ /// UI pixel radius.
+ ///
+ 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);
+
+ ///
+ /// We'll lerp between the radarrange and actual range
+ ///
+ protected float ActualRadarRange;
+
+ protected float CornerRadarRange => MathF.Sqrt(ActualRadarRange * ActualRadarRange + ActualRadarRange * ActualRadarRange) * 1.1f;
+
+ ///
+ /// Controls the maximum distance that will display.
+ ///
+ 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? 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();
+ _largerFont = new VectorFont(cache.GetResource("/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);
+ }
+
+ ///
+ /// Converts map coordinates to the local control.
+ ///
+ protected Vector2 ScalePosition(Vector2 value)
+ {
+ return ScalePosition(value, MinimapScale, MidPointVector);
+ }
+
+ protected static Vector2 ScalePosition(Vector2 value, float minimapScale, Vector2 midpointVector)
+ {
+ return value * minimapScale + midpointVector;
+ }
+
+ ///
+ /// Converts local coordinates on the control to map coordinates.
+ ///
+ 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;
+ }
+
+ ///
+ /// Handles re-centering the control's offset.
+ ///
+ ///
+ 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(PixelSizeBox, 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);
+ }
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs
index 0255afa50e9..e14a8264678 100644
--- a/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs
+++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs
@@ -67,7 +67,7 @@ await server.WaitAssertion(() =>
Assert.That(spawnEntitiesBehavior.Spawn.Values.Single(), Is.EqualTo(new MinMax { Min = 1, Max = 1 }));
});
- var entitiesInRange = sEntityManager.System().GetEntitiesInRange(coordinates, 2);
+ var entitiesInRange = sEntityManager.System().GetEntitiesInRange(coordinates, 3, LookupFlags.All | LookupFlags.Approximate);
var found = false;
foreach (var entity in entitiesInRange)
@@ -86,7 +86,7 @@ await server.WaitAssertion(() =>
break;
}
- Assert.That(found, Is.True);
+ Assert.That(found, Is.True, $"Unable to find {SpawnedEntityId} nearby for destructible test; found {entitiesInRange.Count} entities.");
});
await pair.CleanReturnAsync();
}
diff --git a/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs b/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs
index 6c19dbc4a2a..c30db08bbe5 100644
--- a/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs
+++ b/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs
@@ -1,5 +1,6 @@
using Content.Shared.Cargo;
using Content.Shared.Cargo.Prototypes;
+using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Cargo.Components;
@@ -30,4 +31,10 @@ public sealed partial class StationCargoOrderDatabaseComponent : Component
///
[DataField("shuttle")]
public EntityUid? Shuttle;
+
+ ///
+ /// The paper-type prototype to spawn with the order information.
+ ///
+ [DataField]
+ public EntProtoId PrinterOutput = "PaperCargoInvoice";
}
diff --git a/Content.Server/Cargo/Components/TradeStationComponent.cs b/Content.Server/Cargo/Components/TradeStationComponent.cs
new file mode 100644
index 00000000000..0422470cc90
--- /dev/null
+++ b/Content.Server/Cargo/Components/TradeStationComponent.cs
@@ -0,0 +1,10 @@
+namespace Content.Server.Cargo.Components;
+
+///
+/// Target for approved orders to spawn at.
+///
+[RegisterComponent]
+public sealed partial class TradeStationComponent : Component
+{
+
+}
diff --git a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs
index 3a062bae0a0..ebe66ff029e 100644
--- a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs
+++ b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs
@@ -2,6 +2,7 @@
using Content.Server.Cargo.Components;
using Content.Server.Labels.Components;
using Content.Server.Paper;
+using Content.Server.Station.Components;
using Content.Shared.Cargo;
using Content.Shared.Cargo.BUI;
using Content.Shared.Cargo.Components;
@@ -12,6 +13,7 @@
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Cargo.Systems
@@ -110,10 +112,12 @@ private void OnApproveOrderMessage(EntityUid uid, CargoOrderConsoleComponent com
return;
}
- var bankAccount = GetBankAccount(uid, component);
+ var station = _station.GetOwningStation(uid);
// No station to deduct from.
- if (!TryGetOrderDatabase(uid, out var dbUid, out var orderDatabase, component) || bankAccount == null)
+ if (!TryComp(station, out StationBankAccountComponent? bank) ||
+ !TryComp(station, out StationDataComponent? stationData) ||
+ !TryGetOrderDatabase(station, out var orderDatabase))
{
ConsolePopup(args.Session, Loc.GetString("cargo-console-station-not-found"));
PlayDenySound(uid, component);
@@ -159,32 +163,96 @@ private void OnApproveOrderMessage(EntityUid uid, CargoOrderConsoleComponent com
var cost = order.Price * order.OrderQuantity;
// Not enough balance
- if (cost > bankAccount.Balance)
+ if (cost > bank.Balance)
{
ConsolePopup(args.Session, Loc.GetString("cargo-console-insufficient-funds", ("cost", cost)));
PlayDenySound(uid, component);
return;
}
+ var tradeDestination = TryFulfillOrder(stationData, order, orderDatabase);
+
+ if (tradeDestination == null)
+ {
+ ConsolePopup(args.Session, Loc.GetString("cargo-console-unfulfilled"));
+ PlayDenySound(uid, component);
+ return;
+ }
+
_idCardSystem.TryFindIdCard(player, out var idCard);
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
order.SetApproverData(idCard.Comp?.FullName, idCard.Comp?.JobTitle);
- _audio.PlayPvs(_audio.GetSound(component.ConfirmSound), uid);
+ _audio.PlayPvs(component.ConfirmSound, uid);
+
+ ConsolePopup(args.Session, Loc.GetString("cargo-console-trade-station", ("destination", MetaData(tradeDestination.Value).EntityName)));
// Log order approval
_adminLogger.Add(LogType.Action, LogImpact.Low,
- $"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] with balance at {bankAccount.Balance}");
+ $"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] with balance at {bank.Balance}");
+
+ orderDatabase.Orders.Remove(order);
+ DeductFunds(bank, cost);
+ UpdateOrders(station.Value, orderDatabase);
+ }
+
+ private EntityUid? TryFulfillOrder(StationDataComponent stationData, CargoOrderData order, StationCargoOrderDatabaseComponent orderDatabase)
+ {
+ // No slots at the trade station
+ _listEnts.Clear();
+ GetTradeStations(stationData, ref _listEnts);
+ EntityUid? tradeDestination = null;
+
+ // Try to fulfill from any station where possible, if the pad is not occupied.
+ foreach (var trade in _listEnts)
+ {
+ var tradePads = GetCargoPallets(trade);
+ _random.Shuffle(tradePads);
+
+ var freePads = GetFreeCargoPallets(trade, tradePads);
+ if (freePads.Count >= order.OrderQuantity) //check if the station has enough free pallets
+ {
+ foreach (var pad in freePads)
+ {
+ var coordinates = new EntityCoordinates(trade, pad.Transform.LocalPosition);
+
+ if (FulfillOrder(order, coordinates, orderDatabase.PrinterOutput))
+ {
+ tradeDestination = trade;
+ order.NumDispatched++;
+ if (order.OrderQuantity <= order.NumDispatched) //Spawn a crate on free pellets until the order is fulfilled.
+ break;
+ }
+ }
+ }
+
+ if (tradeDestination != null)
+ break;
+ }
+
+ return tradeDestination;
+ }
+
+ private void GetTradeStations(StationDataComponent data, ref List ents)
+ {
+ var tradeStationQuery = AllEntityQuery(); // We *could* cache this, but I don't know where it'd go
+
+ while (tradeStationQuery.MoveNext(out var uid, out _))
+ {
+ //if (!_tradeQuery.HasComponent(uid))
+ // continue;
- DeductFunds(bankAccount, cost);
- UpdateOrders(dbUid!.Value, orderDatabase);
+ ents.Add(uid);
+ }
}
private void OnRemoveOrderMessage(EntityUid uid, CargoOrderConsoleComponent component, CargoConsoleRemoveOrderMessage args)
{
- if (!TryGetOrderDatabase(uid, out var dbUid, out var orderDatabase, component))
+ var station = _station.GetOwningStation(uid);
+
+ if (!TryGetOrderDatabase(station, out var orderDatabase))
return;
- RemoveOrder(dbUid!.Value, args.OrderId, orderDatabase);
+ RemoveOrder(station.Value, args.OrderId, orderDatabase);
}
private void OnAddOrderMessage(EntityUid uid, CargoOrderConsoleComponent component, CargoConsoleAddOrderMessage args)
@@ -195,11 +263,9 @@ private void OnAddOrderMessage(EntityUid uid, CargoOrderConsoleComponent compone
if (args.Amount <= 0)
return;
- var bank = GetBankAccount(uid, component);
- if (bank == null)
- return;
+ var stationUid = _station.GetOwningStation(uid);
- if (!TryGetOrderDatabase(uid, out var dbUid, out var orderDatabase, component))
+ if (!TryGetOrderDatabase(stationUid, out var orderDatabase))
return;
if (!_protoMan.TryIndex(args.CargoProductId, out var product))
@@ -213,7 +279,7 @@ private void OnAddOrderMessage(EntityUid uid, CargoOrderConsoleComponent compone
var data = GetOrderData(args, product, GenerateOrderId(orderDatabase));
- if (!TryAddOrder(dbUid!.Value, data, orderDatabase))
+ if (!TryAddOrder(stationUid.Value, data, orderDatabase))
{
PlayDenySound(uid, component);
return;
@@ -317,7 +383,8 @@ public bool AddAndApproveOrder(
string sender,
string description,
string dest,
- StationCargoOrderDatabaseComponent component
+ StationCargoOrderDatabaseComponent component,
+ StationDataComponent stationData
)
{
DebugTools.Assert(_protoMan.HasIndex(spawnId));
@@ -333,7 +400,7 @@ StationCargoOrderDatabaseComponent component
$"AddAndApproveOrder {description} added order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}]");
// Add it to the list
- return TryAddOrder(dbUid, order, component);
+ return TryAddOrder(dbUid, order, component) && TryFulfillOrder(stationData, order, component).HasValue;
}
private bool TryAddOrder(EntityUid dbUid, CargoOrderData data, StationCargoOrderDatabaseComponent component)
@@ -362,10 +429,10 @@ public void RemoveOrder(EntityUid dbUid, int index, StationCargoOrderDatabaseCom
public void ClearOrders(StationCargoOrderDatabaseComponent component)
{
- if (component.Orders.Count == 0) return;
+ if (component.Orders.Count == 0)
+ return;
component.Orders.Clear();
- Dirty(component);
}
private static bool PopFrontOrder(StationCargoOrderDatabaseComponent orderDB, [NotNullWhen(true)] out CargoOrderData? orderOut)
@@ -388,64 +455,63 @@ private static bool PopFrontOrder(StationCargoOrderDatabaseComponent orderDB, [N
return true;
}
- private bool FulfillOrder(StationCargoOrderDatabaseComponent orderDB, EntityCoordinates whereToPutIt,
- string? paperPrototypeToPrint)
+ ///
+ /// Tries to fulfill the next outstanding order.
+ ///
+ private bool FulfillNextOrder(StationCargoOrderDatabaseComponent orderDB, EntityCoordinates spawn, string? paperProto)
{
- if (PopFrontOrder(orderDB, out var order))
- {
- // Create the item itself
- var item = Spawn(order.ProductId, whereToPutIt);
+ if (!PopFrontOrder(orderDB, out var order))
+ return false;
- // Create a sheet of paper to write the order details on
- var printed = EntityManager.SpawnEntity(paperPrototypeToPrint, whereToPutIt);
- if (TryComp(printed, out var paper))
+ return FulfillOrder(order, spawn, paperProto);
+ }
+
+ ///
+ /// Fulfills the specified cargo order and spawns paper attached to it.
+ ///
+ private bool FulfillOrder(CargoOrderData order, EntityCoordinates spawn, string? paperProto)
+ {
+ // Create the item itself
+ var item = Spawn(order.ProductId, spawn);
+
+ // Create a sheet of paper to write the order details on
+ var printed = EntityManager.SpawnEntity(paperProto, spawn);
+ if (TryComp(printed, out var paper))
+ {
+ // fill in the order data
+ var val = Loc.GetString("cargo-console-paper-print-name", ("orderNumber", order.OrderId));
+ _metaSystem.SetEntityName(printed, val);
+
+ _paperSystem.SetContent(printed, Loc.GetString(
+ "cargo-console-paper-print-text",
+ ("orderNumber", order.OrderId),
+ ("itemName", MetaData(item).EntityName),
+ ("requester", order.Requester),
+ ("reason", order.Reason),
+ ("approver", order.Approver ?? string.Empty)),
+ paper);
+
+ // attempt to attach the label to the item
+ if (TryComp(item, out var label))
{
- // fill in the order data
- var val = Loc.GetString("cargo-console-paper-print-name", ("orderNumber", order.OrderId));
- _metaSystem.SetEntityName(printed, val);
-
- _paperSystem.SetContent(printed, Loc.GetString(
- "cargo-console-paper-print-text",
- ("orderNumber", order.OrderId),
- ("itemName", MetaData(item).EntityName),
- ("requester", order.Requester),
- ("reason", order.Reason),
- ("approver", order.Approver ?? string.Empty)),
- paper);
-
- // attempt to attach the label to the item
- if (TryComp(item, out var label))
- {
- _slots.TryInsert(item, label.LabelSlot, printed, null);
- }
+ _slots.TryInsert(item, label.LabelSlot, printed, null);
}
-
- return true;
}
- return false;
+ return true;
+
}
private void DeductFunds(StationBankAccountComponent component, int amount)
{
component.Balance = Math.Max(0, component.Balance - amount);
- Dirty(component);
}
#region Station
- private StationBankAccountComponent? GetBankAccount(EntityUid uid, CargoOrderConsoleComponent _)
- {
- var station = _station.GetOwningStation(uid);
-
- TryComp(station, out var bankComponent);
- return bankComponent;
- }
-
- private bool TryGetOrderDatabase(EntityUid uid, [MaybeNullWhen(false)] out EntityUid? dbUid, [MaybeNullWhen(false)] out StationCargoOrderDatabaseComponent dbComp, CargoOrderConsoleComponent _)
+ private bool TryGetOrderDatabase([NotNullWhen(true)] EntityUid? stationUid, [MaybeNullWhen(false)] out StationCargoOrderDatabaseComponent dbComp)
{
- dbUid = _station.GetOwningStation(uid);
- return TryComp(dbUid, out dbComp);
+ return TryComp(stationUid, out dbComp);
}
#endregion
diff --git a/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs b/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs
index eb7d80e4761..a20b5c723f4 100644
--- a/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs
+++ b/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs
@@ -2,7 +2,7 @@
using Content.Server.Cargo.Components;
using Content.Server.GameTicking.Events;
using Content.Server.Shuttles.Components;
-using Content.Server.Shuttles.Events;
+using Content.Server.Station.Systems;
using Content.Shared.Stacks;
using Content.Shared.Cargo;
using Content.Shared.Cargo.BUI;
@@ -10,30 +10,31 @@
using Content.Shared.Cargo.Events;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
+using Content.Shared.Shuttles.Components;
+using Content.Shared.Tiles;
using Content.Shared.Whitelist;
-using Robust.Server.GameObjects;
+using Robust.Server.Maps;
using Robust.Shared.Map;
using Robust.Shared.Random;
+using Robust.Shared.Audio;
+using Robust.Shared.Physics.Components;
using Robust.Shared.Utility;
-using Content.Shared.Coordinates;
-using Content.Shared.Mobs;
-using Content.Shared.Mobs.Components;
namespace Content.Server.Cargo.Systems;
public sealed partial class CargoSystem
{
/*
- * Handles cargo shuttle mechanics.
+ * Handles cargo shuttle / trade mechanics.
*/
public MapId? CargoMap { get; private set; }
+ private static readonly SoundPathSpecifier ApproveSound = new("/Audio/Effects/Cargo/ping.ogg");
+
private void InitializeShuttle()
{
- SubscribeLocalEvent(OnCargoFTLStarted);
- SubscribeLocalEvent(OnCargoFTLCompleted);
- SubscribeLocalEvent(OnCargoFTLTag);
+ SubscribeLocalEvent(OnTradeSplit);
SubscribeLocalEvent(OnCargoShuttleConsoleStartup);
@@ -42,34 +43,19 @@ private void InitializeShuttle()
SubscribeLocalEvent(OnPalletUIOpen);
SubscribeLocalEvent(OnRoundRestart);
- SubscribeLocalEvent(OnRoundStart);
+ SubscribeLocalEvent(OnStationInitialize);
- _cfgManager.OnValueChanged(CCVars.GridFill, SetGridFill);
- }
-
- private void ShutdownShuttle()
- {
- _cfgManager.UnsubValueChanged(CCVars.GridFill, SetGridFill);
+ Subs.CVar(_cfgManager, CCVars.GridFill, SetGridFill);
}
private void SetGridFill(bool obj)
{
if (obj)
{
- SetupCargoShuttle();
+ SetupTradePost();
}
}
- private void OnCargoFTLTag(EntityUid uid, CargoShuttleComponent component, ref FTLTagEvent args)
- {
- if (args.Handled)
- return;
-
- // Just saves mappers forgetting.
- args.Handled = true;
- args.Tag = "DockCargo";
- }
-
#region Console
private void UpdateCargoShuttleConsoles(EntityUid shuttleUid, CargoShuttleComponent _)
@@ -156,6 +142,15 @@ private void UpdateShuttleState(EntityUid uid, EntityUid? station = null)
#endregion
+ private void OnTradeSplit(EntityUid uid, TradeStationComponent component, ref GridSplitEvent args)
+ {
+ // If the trade station gets bombed it's still a trade station.
+ foreach (var gridUid in args.NewGrids)
+ {
+ EnsureComp(gridUid);
+ }
+ }
+
#region Shuttle
///
@@ -206,9 +201,9 @@ private int GetCargoSpace(EntityUid gridUid)
return space;
}
- private List<(EntityUid Entity, CargoPalletComponent Component)> GetCargoPallets(EntityUid gridUid)
+ private List<(EntityUid Entity, CargoPalletComponent Component, TransformComponent PalletXform)> GetCargoPallets(EntityUid gridUid)
{
- var pads = new List<(EntityUid, CargoPalletComponent)>();
+ _pads.Clear();
var query = AllEntityQuery();
while (query.MoveNext(out var uid, out var comp, out var compXform))
@@ -219,23 +214,47 @@ private int GetCargoSpace(EntityUid gridUid)
continue;
}
- pads.Add((uid, comp));
+ _pads.Add((uid, comp, compXform));
}
- return pads;
+ return _pads;
+ }
+
+ private List<(EntityUid Entity, CargoPalletComponent Component, TransformComponent Transform)>
+ GetFreeCargoPallets(EntityUid gridUid,
+ List<(EntityUid Entity, CargoPalletComponent Component, TransformComponent Transform)> pallets)
+ {
+ _setEnts.Clear();
+
+ List<(EntityUid Entity, CargoPalletComponent Component, TransformComponent Transform)> outList = new();
+
+ foreach (var pallet in pallets)
+ {
+ var aabb = _lookup.GetAABBNoContainer(pallet.Entity, pallet.Transform.LocalPosition, pallet.Transform.LocalRotation);
+
+ if (_lookup.AnyLocalEntitiesIntersecting(gridUid, aabb, LookupFlags.Dynamic))
+ continue;
+
+ outList.Add(pallet);
+ }
+
+ return outList;
}
#endregion
#region Station
- private void SellPallets(EntityUid gridUid, EntityUid? station, out double amount)
+ private bool SellPallets(EntityUid gridUid, EntityUid? station, out double amount)
{
station ??= _station.GetOwningStation(gridUid);
GetPalletGoods(gridUid, out var toSell, out amount);
Log.Debug($"Cargo sold {toSell.Count} entities for {amount}");
+ if (toSell.Count == 0)
+ return false;
+
if (station != null)
{
var ev = new EntitySoldEvent(station.Value, toSell);
@@ -246,6 +265,8 @@ private void SellPallets(EntityUid gridUid, EntityUid? station, out double amoun
{
Del(ent);
}
+
+ return true;
}
private void GetPalletGoods(EntityUid gridUid, out HashSet toSell, out double amount)
@@ -253,10 +274,15 @@ private void GetPalletGoods(EntityUid gridUid, out HashSet toSell, ou
amount = 0;
toSell = new HashSet();
- foreach (var (palletUid, _) in GetCargoPallets(gridUid))
+ foreach (var (palletUid, _, _) in GetCargoPallets(gridUid))
{
// Containers should already get the sell price of their children so can skip those.
- foreach (var ent in _lookup.GetEntitiesIntersecting(palletUid, LookupFlags.Dynamic | LookupFlags.Sundries | LookupFlags.Approximate))
+ _setEnts.Clear();
+
+ _lookup.GetEntitiesIntersecting(palletUid, _setEnts,
+ LookupFlags.Dynamic | LookupFlags.Sundries);
+
+ foreach (var ent in _setEnts)
{
// Dont sell:
// - anything already being sold
@@ -304,21 +330,6 @@ private bool CanSell(EntityUid uid, TransformComponent xform)
return true;
}
- private void AddCargoContents(EntityUid shuttleUid, CargoShuttleComponent shuttle, StationCargoOrderDatabaseComponent orderDatabase)
- {
- var xformQuery = GetEntityQuery();
-
- var pads = GetCargoPallets(shuttleUid);
- while (pads.Count > 0)
- {
- var coordinates = new EntityCoordinates(shuttleUid, xformQuery.GetComponent(_random.PickAndTake(pads).Entity).LocalPosition);
- if (!FulfillOrder(orderDatabase, coordinates, shuttle.PrinterOutput))
- {
- break;
- }
- }
- }
-
private void OnPalletSale(EntityUid uid, CargoPalletConsoleComponent component, CargoPalletSellMessage args)
{
var player = args.Session.AttachedEntity;
@@ -327,74 +338,41 @@ private void OnPalletSale(EntityUid uid, CargoPalletConsoleComponent component,
return;
var bui = _uiSystem.GetUi(uid, CargoPalletConsoleUiKey.Sale);
- if (Transform(uid).GridUid is not EntityUid gridUid)
+ var xform = Transform(uid);
+
+ if (xform.GridUid is not EntityUid gridUid)
{
_uiSystem.SetUiState(bui,
new CargoPalletConsoleInterfaceState(0, 0, false));
return;
}
- // Delta-V change, on sale, add cash to the stations bank account instead of throwing it on the floor
- var stationUid = _station.GetOwningStation(uid);
-
- if (TryComp(stationUid, out var bank))
- {
- SellPallets(gridUid, null, out var amount);
- bank.Balance += (int) amount;
- }
- // End of Delta-V change
-
- UpdatePalletConsoleInterface(uid);
- }
-
- private void OnCargoFTLStarted(EntityUid uid, CargoShuttleComponent component, ref FTLStartedEvent args)
- {
- var stationUid = _station.GetOwningStation(uid);
-
- // Called
- if (CargoMap == null ||
- args.FromMapUid != _mapManager.GetMapEntityId(CargoMap.Value) ||
- !TryComp(stationUid, out var orderDatabase))
- {
- return;
- }
-
- AddCargoContents(uid, component, orderDatabase);
- UpdateOrders(stationUid!.Value, orderDatabase);
- UpdateCargoShuttleConsoles(uid, component);
- }
-
- private void OnCargoFTLCompleted(EntityUid uid, CargoShuttleComponent component, ref FTLCompletedEvent args)
- {
- var xform = Transform(uid);
- // Recalled
- if (xform.MapID != CargoMap)
+ if (!SellPallets(gridUid, null, out var price))
return;
- var stationUid = _station.GetOwningStation(uid);
-
- if (TryComp(stationUid, out var bank))
- {
- SellPallets(uid, stationUid, out var amount);
- bank.Balance += (int) amount;
- }
+ var stackPrototype = _protoMan.Index(component.CashType);
+ _stack.Spawn((int) price, stackPrototype, xform.Coordinates);
+ _audio.PlayPvs(ApproveSound, uid);
+ UpdatePalletConsoleInterface(uid);
}
#endregion
private void OnRoundRestart(RoundRestartCleanupEvent ev)
{
- Reset();
- CleanupCargoShuttle();
+ CleanupTradeStation();
}
- private void OnRoundStart(RoundStartingEvent ev)
+ private void OnStationInitialize(StationInitializedEvent args)
{
+ if (!HasComp(args.Station)) // No cargo, L
+ return;
+
if (_cfgManager.GetCVar(CCVars.GridFill))
- SetupCargoShuttle();
+ SetupTradePost();
}
- private void CleanupCargoShuttle()
+ private void CleanupTradeStation()
{
if (CargoMap == null || !_mapManager.MapExists(CargoMap.Value))
{
@@ -420,7 +398,7 @@ private void CleanupCargoShuttle()
}
}
- private void SetupCargoShuttle()
+ private void SetupTradePost()
{
if (CargoMap != null && _mapManager.MapExists(CargoMap.Value))
{
@@ -429,17 +407,39 @@ private void SetupCargoShuttle()
// It gets mapinit which is okay... buuutt we still want it paused to avoid power draining.
CargoMap = _mapManager.CreateMap();
+
+ var options = new MapLoadOptions
+ {
+ LoadMap = true,
+ };
+
+ _mapLoader.TryLoad((MapId) CargoMap, "/Maps/Shuttles/trading_outpost.yml", out var rootUids, options); // Oh boy oh boy, hardcoded paths!
+
+ // If this fails to load for whatever reason, cargo is fucked
+ if (rootUids == null || !rootUids.Any())
+ return;
+
+ foreach (var grid in rootUids)
+ {
+ EnsureComp(grid);
+ EnsureComp(grid);
+
+ var shuttleComponent = EnsureComp(grid);
+ shuttleComponent.AngularDamping = 10000;
+ shuttleComponent.LinearDamping = 10000; // This shit ain't going nowhere
+ }
+
var mapUid = _mapManager.GetMapEntityId(CargoMap.Value);
var ftl = EnsureComp(_mapManager.GetMapEntityId(CargoMap.Value));
ftl.Whitelist = new EntityWhitelist()
{
- Components = new[]
- {
+ Components =
+ [
_factory.GetComponentName(typeof(CargoShuttleComponent))
- }
+ ]
};
- _metaSystem.SetEntityName(mapUid, $"Trading post {_random.Next(1000):000}");
+ _metaSystem.SetEntityName(mapUid, $"Automated Trade Station {_random.Next(1000):000}");
_console.RefreshShuttleConsoles();
}
diff --git a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs
index 698d324a801..42aabf2578e 100644
--- a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs
+++ b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs
@@ -61,10 +61,10 @@ private void UpdateTelepad(float frameTime)
}
var xform = Transform(uid);
- if (FulfillOrder(orderDatabase, xform.Coordinates, comp.PrinterOutput))
+ if (FulfillNextOrder(orderDatabase, xform.Coordinates, comp.PrinterOutput))
{
_audio.PlayPvs(_audio.GetSound(comp.TeleportSound), uid, AudioParams.Default.WithVolume(-8f));
- UpdateOrders(station!.Value, orderDatabase);
+ UpdateOrders(station.Value, orderDatabase);
comp.CurrentState = CargoTelepadState.Teleporting;
_appearance.SetData(uid, CargoTelepadVisuals.State, CargoTelepadState.Teleporting, appearance);
diff --git a/Content.Server/Cargo/Systems/CargoSystem.cs b/Content.Server/Cargo/Systems/CargoSystem.cs
index fb6d949e7f4..2609d06b55d 100644
--- a/Content.Server/Cargo/Systems/CargoSystem.cs
+++ b/Content.Server/Cargo/Systems/CargoSystem.cs
@@ -26,9 +26,6 @@ namespace Content.Server.Cargo.Systems;
public sealed partial class CargoSystem : SharedCargoSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly IComponentFactory _factory = default!;
- [Dependency] private readonly IConfigurationManager _cfgManager = default!;
- [Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
@@ -42,15 +39,25 @@ public sealed partial class CargoSystem : SharedCargoSystem
[Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedTransformSystem _xformSystem = default!;
[Dependency] private readonly ShuttleConsoleSystem _console = default!;
[Dependency] private readonly StackSystem _stack = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
+ [Dependency] private readonly IConfigurationManager _cfgManager = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly IComponentFactory _factory = default!;
+ [Dependency] private readonly MapLoaderSystem _mapLoader = default!;
private EntityQuery _xformQuery;
private EntityQuery _blacklistQuery;
private EntityQuery _mobQuery;
+ private EntityQuery _tradeQuery;
+
+ private HashSet _setEnts = new();
+ private List _listEnts = new();
+ private List<(EntityUid, CargoPalletComponent, TransformComponent)> _pads = new();
public override void Initialize()
{
@@ -59,6 +66,7 @@ public override void Initialize()
_xformQuery = GetEntityQuery();
_blacklistQuery = GetEntityQuery();
_mobQuery = GetEntityQuery();
+ _tradeQuery = GetEntityQuery();
InitializeConsole();
InitializeShuttle();
@@ -66,13 +74,6 @@ public override void Initialize()
InitializeBounty();
}
- public override void Shutdown()
- {
- base.Shutdown();
- ShutdownShuttle();
- CleanupCargoShuttle();
- }
-
public override void Update(float frameTime)
{
base.Update(frameTime);
diff --git a/Content.Server/Salvage/SalvageSystem.Runner.cs b/Content.Server/Salvage/SalvageSystem.Runner.cs
index 3b89135c58e..8a1498cbe96 100644
--- a/Content.Server/Salvage/SalvageSystem.Runner.cs
+++ b/Content.Server/Salvage/SalvageSystem.Runner.cs
@@ -10,6 +10,7 @@
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 @@ private void UpdateRunner()
if (shuttleXform.MapUid != uid || HasComp(shuttleUid))
continue;
- _shuttle.FTLTravel(shuttleUid, shuttle, member, ftlTime);
+ _shuttle.FTLToDock(shuttleUid, shuttle, member, ftlTime);
}
break;
diff --git a/Content.Server/Salvage/SpawnSalvageMissionJob.cs b/Content.Server/Salvage/SpawnSalvageMissionJob.cs
index eb370aa1129..2776db2283a 100644
--- a/Content.Server/Salvage/SpawnSalvageMissionJob.cs
+++ b/Content.Server/Salvage/SpawnSalvageMissionJob.cs
@@ -24,6 +24,7 @@
using Content.Shared.Salvage;
using Content.Shared.Salvage.Expeditions;
using Content.Shared.Salvage.Expeditions.Modifiers;
+using Content.Shared.Shuttles.Components;
using Content.Shared.Storage;
using Robust.Shared.Collections;
using Robust.Shared.Map;
@@ -91,6 +92,8 @@ protected override async Task Process()
MetaDataComponent? metadata = null;
var grid = _entManager.EnsureComponent(mapUid);
var random = new Random(_missionParams.Seed);
+ var destComp = _entManager.AddComponent(mapUid);
+ destComp.BeaconsOnly = true;
// Setup mission configs
// As we go through the config the rating will deplete so we'll go for most important to least important.
diff --git a/Content.Server/Shuttles/Commands/DockCommand.cs b/Content.Server/Shuttles/Commands/DockCommand.cs
index 5f287e03970..62634af2bcd 100644
--- a/Content.Server/Shuttles/Commands/DockCommand.cs
+++ b/Content.Server/Shuttles/Commands/DockCommand.cs
@@ -47,7 +47,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
}
var dockSystem = _entManager.System();
- 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
index fa4cd4dfaf3..00000000000
--- a/Content.Server/Shuttles/Components/AutoDockComponent.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Content.Server.Shuttles.Components;
-
-///
-/// Added to entities when they are actively trying to dock with something else.
-/// We track it because checking every dock constantly would be expensive.
-///
-[RegisterComponent]
-public sealed partial class AutoDockComponent : Component
-{
- ///
- /// Track who has requested autodocking so we can know when to be removed.
- ///
- public HashSet Requesters = new();
-}
diff --git a/Content.Server/Shuttles/Components/FTLBeaconComponent.cs b/Content.Server/Shuttles/Components/FTLBeaconComponent.cs
new file mode 100644
index 00000000000..06606336e33
--- /dev/null
+++ b/Content.Server/Shuttles/Components/FTLBeaconComponent.cs
@@ -0,0 +1,10 @@
+namespace Content.Server.Shuttles.Components;
+
+///
+/// Shows up on a shuttle's map as an FTL target.
+///
+[RegisterComponent]
+public sealed partial class FTLBeaconComponent : Component
+{
+
+}
diff --git a/Content.Server/Shuttles/Components/FTLComponent.cs b/Content.Server/Shuttles/Components/FTLComponent.cs
index e8bcbb459e1..077fe087489 100644
--- a/Content.Server/Shuttles/Components/FTLComponent.cs
+++ b/Content.Server/Shuttles/Components/FTLComponent.cs
@@ -2,6 +2,7 @@
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;
///
- /// 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.
///
- [ViewVariables(VVAccess.ReadWrite), DataField("targetUid")]
- public EntityUid? TargetUid;
-
- [ViewVariables(VVAccess.ReadWrite), DataField("targetCoordinates")]
+ [ViewVariables(VVAccess.ReadWrite), DataField]
public EntityCoordinates TargetCoordinates;
- ///
- /// Should we dock with the target when arriving or show up nearby.
- ///
- [ViewVariables(VVAccess.ReadWrite), DataField("dock")]
- public bool Dock;
+ [DataField]
+ public Angle TargetAngle;
///
/// If we're docking after FTL what is the prioritised dock tag (if applicable).
///
- [ViewVariables(VVAccess.ReadWrite), DataField("priorityTag", customTypeSerializer:typeof(PrototypeIdSerializer))]
- public string? PriorityTag;
+ [ViewVariables(VVAccess.ReadWrite), DataField]
+ public ProtoId? PriorityTag;
[ViewVariables(VVAccess.ReadWrite), DataField("soundTravel")]
public SoundSpecifier? TravelSound = new SoundPathSpecifier("/Audio/DeltaV/Effects/Shuttle/hyperspace_progress.ogg") // DeltaV - Replace FTL sound
@@ -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
index 6eedcbd8ce0..00000000000
--- a/Content.Server/Shuttles/Components/FTLDestinationComponent.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using Content.Shared.Whitelist;
-
-namespace Content.Server.Shuttles.Components;
-
-[RegisterComponent]
-public sealed partial class FTLDestinationComponent : Component
-{
- ///
- /// Should this destination be restricted in some form from console visibility.
- ///
- [ViewVariables(VVAccess.ReadWrite), DataField("whitelist")]
- public EntityWhitelist? Whitelist;
-
- ///
- /// Is this destination visible but available to be warped to?
- ///
- [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
index 00000000000..db6538462d5
--- /dev/null
+++ b/Content.Server/Shuttles/Components/FTLExclusionComponent.cs
@@ -0,0 +1,16 @@
+using Content.Shared.Shuttles.Systems;
+
+namespace Content.Server.Shuttles.Components;
+
+///
+/// Prevents FTL from occuring around this entity.
+///
+[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/GridSpawnComponent.cs b/Content.Server/Shuttles/Components/GridSpawnComponent.cs
index 1ddfb8c2442..5f0fa7dd624 100644
--- a/Content.Server/Shuttles/Components/GridSpawnComponent.cs
+++ b/Content.Server/Shuttles/Components/GridSpawnComponent.cs
@@ -1,4 +1,5 @@
using Content.Server.Shuttles.Systems;
+using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Shuttles.Components;
@@ -23,6 +24,11 @@ public record struct GridSpawnGroup
public int MinCount = 1;
public int MaxCount = 1;
+ ///
+ /// Components to be added to any spawned grids.
+ ///
+ public ComponentRegistry AddComponents = new();
+
///
/// Hide the IFF label of the grid.
///
diff --git a/Content.Server/Shuttles/Components/RecentlyDockedComponent.cs b/Content.Server/Shuttles/Components/RecentlyDockedComponent.cs
deleted file mode 100644
index 6b0667d1e6c..00000000000
--- a/Content.Server/Shuttles/Components/RecentlyDockedComponent.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace Content.Server.Shuttles.Components;
-
-///
-/// Added to that have recently undocked.
-/// This checks for whether they've left the specified radius before allowing them to automatically dock again.
-///
-[RegisterComponent]
-public sealed partial class RecentlyDockedComponent : Component
-{
- [DataField("lastDocked")]
- public EntityUid LastDocked;
-
- [ViewVariables(VVAccess.ReadWrite), DataField("radius")]
- public float Radius = 1.5f;
-}
diff --git a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
index 037fcc75665..f4dd502b375 100644
--- a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
+++ b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
@@ -417,13 +417,6 @@ public override void Update(float frameTime)
var curTime = _timing.CurTime;
TryGetArrivals(out var arrivals);
- // TODO: FTL fucker, if on an edge tile every N seconds check for wall or w/e
- // TODO: Docking should be per-grid rather than per dock and bump off when undocking.
-
- // TODO: Stop dispatch if emergency shuttle has arrived.
- // TODO: Need maps
- // TODO: Need emergency suits on shuttle probs
- // TODO: Need some kind of comp to shunt people off if they try to get on?
if (TryComp(arrivals, out var arrivalsXform))
{
while (query.MoveNext(out var uid, out var comp, out var shuttle, out var xform))
@@ -437,7 +430,7 @@ public override void Update(float frameTime)
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 +440,7 @@ public override void Update(float frameTime)
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 +560,7 @@ private void SetupShuttle(EntityUid uid, StationArrivalsComponent component)
var arrivalsComp = EnsureComp(component.Shuttle);
arrivalsComp.Station = uid;
EnsureComp(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
index a09fff5189c..00000000000
--- a/Content.Server/Shuttles/Systems/DockingSystem.AutoDock.cs
+++ /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();
- var xformQuery = GetEntityQuery();
- var recentQuery = GetEntityQuery();
- var query = EntityQueryEnumerator();
-
- while (query.MoveNext(out var dockUid, out var comp))
- {
- if (comp.Requesters.Count == 0 || !dockingQuery.TryGetComponent(dockUid, out var dock))
- {
- RemComp(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();
- var recentQueryEnumerator = EntityQueryEnumerator();
-
- while (recentQueryEnumerator.MoveNext(out var uid, out var comp, out var xform))
- {
- if (!checkedRecent.Add(uid))
- continue;
-
- if (!dockingQuery.HasComponent(uid))
- {
- RemCompDeferred(uid);
- continue;
- }
-
- if (!xformQuery.TryGetComponent(comp.LastDocked, out var otherXform))
- {
- RemCompDeferred(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(uid);
- RemComp(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(dork, out var dock) ||
- !dock.Docked ||
- HasComp(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(dork) ||
- HasComp(Transform(uid).GridUid))
- {
- return;
- }
-
- // TODO: Validation
- var comp = EnsureComp(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(dork, out var comp))
- return;
-
- comp.Requesters.Remove(player.Value);
-
- if (comp.Requesters.Count == 0)
- RemComp(dork);
- }
-}
diff --git a/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs b/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs
index 0fa82c303f6..7bc1be02e37 100644
--- a/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs
+++ b/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs
@@ -19,13 +19,13 @@ public sealed partial class DockingSystem
public Angle GetAngle(EntityUid uid, TransformComponent xform, EntityUid targetUid, TransformComponent targetXform, EntityQuery 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(uid).LocalCenter);
+ _physicsQuery.GetComponent(uid).LocalCenter);
var targetCOM = Robust.Shared.Physics.Transform.Mul(new Transform(targetPos, targetRot),
- Comp(targetUid).LocalCenter);
+ _physicsQuery.GetComponent(targetUid).LocalCenter);
var mapDiff = shuttleCOM - targetCOM;
var angle = mapDiff.ToWorldAngle();
@@ -36,7 +36,7 @@ public Angle GetAngle(EntityUid uid, TransformComponent xform, EntityUid targetU
///
/// Checks if 2 docks can be connected by moving the shuttle directly onto docks.
///
- public bool CanDock(
+ private bool CanDock(
DockingComponent shuttleDock,
TransformComponent shuttleDockXform,
DockingComponent gridDock,
@@ -94,12 +94,12 @@ public bool CanDock(
EntityUid gridDockUid,
DockingComponent gridDock)
{
- var shuttleDocks = new List<(EntityUid, DockingComponent)>(1)
+ var shuttleDocks = new List>(1)
{
(shuttleDockUid, shuttleDock)
};
- var gridDocks = new List<(EntityUid, DockingComponent)>(1)
+ var gridDocks = new List>(1)
{
(gridDockUid, gridDock)
};
@@ -119,37 +119,63 @@ public bool CanDock(
return GetDockingConfigPrivate(shuttleUid, targetGrid, shuttleDocks, gridDocks, priorityTag);
}
- private DockingConfig? GetDockingConfigPrivate(
+ ///
+ /// Tries to get a docking config at the specified coordinates and angle.
+ ///
+ 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;
+ }
+
+ ///
+ /// Gets all docking configs between the 2 grids.
+ ///
+ private List GetDockingConfigs(
EntityUid shuttleUid,
EntityUid targetGrid,
- List<(EntityUid, DockingComponent)> shuttleDocks,
- List<(EntityUid, DockingComponent)> gridDocks,
- string? priorityTag = null)
- {
- if (gridDocks.Count <= 0)
- return null;
+ List> shuttleDocks,
+ List> gridDocks)
+ {
+ var validDockConfigs = new List();
- var xformQuery = GetEntityQuery();
- var targetGridGrid = Comp(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(shuttleUid);
- var shuttleAABB = Comp(shuttleUid).LocalAABB;
+ var shuttleAABB = _gridQuery.GetComponent(shuttleUid).LocalAABB;
var isMap = HasComp(targetGrid);
- var validDockConfigs = new List();
var grids = new List>();
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 bool CanDock(
}
// 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 bool CanDock(
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 bool CanDock(
validDockConfigs.Add(new DockingConfig()
{
Docks = dockedPorts,
- Coordinates = spawnPosition,
+ Coordinates = gridPosition,
Area = dockedAABB,
Angle = targetAngle,
});
@@ -242,9 +268,23 @@ public bool CanDock(
}
}
+ return validDockConfigs;
+ }
+
+ private DockingConfig? GetDockingConfigPrivate(
+ EntityUid shuttleUid,
+ EntityUid targetGrid,
+ List> shuttleDocks,
+ List> 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 =>
@@ -305,19 +345,11 @@ private bool ValidSpawn(MapGridComponent grid, Matrix3 matty, Angle angle, Fixtu
return true;
}
- public List<(EntityUid Uid, DockingComponent Component)> GetDocks(EntityUid uid)
+ public List> GetDocks(EntityUid uid)
{
- var result = new List<(EntityUid Uid, DockingComponent Component)>();
- var query = AllEntityQuery();
-
- while (query.MoveNext(out var dockUid, out var dock, out var xform))
- {
- if (xform.ParentUid != uid || !dock.Enabled)
- continue;
-
- result.Add((dockUid, dock));
- }
+ _dockingSet.Clear();
+ _lookup.GetChildEntities(uid, _dockingSet);
- return result;
+ return _dockingSet.ToList();
}
}
diff --git a/Content.Server/Shuttles/Systems/DockingSystem.cs b/Content.Server/Shuttles/Systems/DockingSystem.cs
index 7f698850450..59a030e83c9 100644
--- a/Content.Server/Shuttles/Systems/DockingSystem.cs
+++ b/Content.Server/Shuttles/Systems/DockingSystem.cs
@@ -5,7 +5,10 @@
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 @@
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 _gridQuery;
private EntityQuery _physicsQuery;
+ private EntityQuery _xformQuery;
+
+ private readonly HashSet> _dockingSet = new();
+ private readonly HashSet> _dockingBoltSet = new();
public override void Initialize()
{
base.Initialize();
+ _gridQuery = GetEntityQuery();
_physicsQuery = GetEntityQuery();
+ _xformQuery = GetEntityQuery();
SubscribeLocalEvent(OnStartup);
SubscribeLocalEvent(OnShutdown);
@@ -48,95 +56,38 @@ public override void Initialize()
// 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(OnRequestAutodock);
- SubscribeLocalEvent(OnRequestStopAutodock);
+ SubscribeLocalEvent(OnRequestDock);
SubscribeLocalEvent(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? GetDockable(EntityUid uid, TransformComponent dockingXform)
+ public void UndockDocks(EntityUid gridUid)
{
- // Did you know Saltern is the most dockable station?
-
- // Assume the docking port itself (and its body) is valid
-
- if (!HasComp(dockingXform.GridUid))
- {
- return null;
- }
-
- var transform = _physics.GetPhysicsTransform(uid, dockingXform);
- var dockingFixture = _fixtureSystem.GetFixtureOrNull(uid, DockingFixture);
-
- if (dockingFixture == null)
- return null;
-
- Box2? aabb = null;
+ _dockingSet.Clear();
+ _lookup.GetChildEntities(gridUid, _dockingSet);
- for (var i = 0; i < dockingFixture.Shape.ChildCount; i++)
+ foreach (var dock in _dockingSet)
{
- aabb = aabb?.Union(dockingFixture.Shape.ComputeAABB(transform, i)) ?? dockingFixture.Shape.ComputeAABB(transform, i);
+ Undock(dock);
}
+ }
- 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>();
- _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 +98,13 @@ private void OnShutdown(EntityUid uid, DockingComponent component, ComponentShut
return;
}
+ var gridUid = Transform(uid).GridUid;
+
+ if (gridUid != null && !Terminating(gridUid.Value))
+ {
+ _console.RefreshShuttleConsoles();
+ }
+
Cleanup(uid, component);
}
@@ -166,12 +124,6 @@ private void Cleanup(EntityUid dockAUid, DockingComponent dockA)
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 +152,14 @@ private void Cleanup(EntityUid dockAUid, DockingComponent dockA)
RaiseLocalEvent(msg);
}
- private void OnStartup(EntityUid uid, DockingComponent component, ComponentStartup args)
+ private void OnStartup(Entity entity, ref ComponentStartup args)
{
- // Use startup so transform already initialized
- if (!EntityManager.GetComponent(uid).Anchored) return;
+ var uid = entity.Owner;
+ var component = entity.Comp;
- EnableDocking(uid, component);
+ // Use startup so transform already initialized
+ if (!EntityManager.GetComponent(uid).Anchored)
+ return;
// This little gem is for docking deserialization
if (component.DockedWith != null)
@@ -217,75 +171,43 @@ private void OnStartup(EntityUid uid, DockingComponent component, ComponentStart
var otherDock = EntityManager.GetComponent(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 entity, ref AnchorStateChangedEvent args)
{
- if (args.Anchored)
- {
- EnableDocking(uid, component);
- }
- else
+ if (!args.Anchored)
{
- DisableDocking(uid, component);
+ Undock(entity);
}
-
- _console.RefreshShuttleConsoles();
}
- private void OnDockingReAnchor(EntityUid uid, DockingComponent component, ref ReAnchorEvent args)
+ private void OnDockingReAnchor(Entity entity, ref ReAnchorEvent args)
{
+ var uid = entity.Owner;
+ var component = entity.Comp;
+
if (!component.Docked)
return;
var otherDock = component.DockedWith;
var other = Comp(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)
- {
- if (!component.Enabled)
- return;
-
- component.Enabled = false;
-
- if (component.DockedWith != null)
- {
- Undock(uid, component);
- }
- }
-
- 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);
- }
-
///
/// Docks 2 ports together and assumes it is valid.
///
- public void Dock(EntityUid dockAUid, DockingComponent dockA, EntityUid dockBUid, DockingComponent dockB)
+ public void Dock(Entity dockA, Entity dockB)
{
+ var dockAUid = dockA.Owner;
+ var dockBUid = dockB.Owner;
+
if (dockBUid.GetHashCode() < dockAUid.GetHashCode())
{
(dockA, dockB) = (dockB, dockA);
@@ -322,10 +244,10 @@ public void Dock(EntityUid dockAUid, DockingComponent dockA, EntityUid dockBUid,
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 +267,15 @@ public void Dock(EntityUid dockAUid, DockingComponent dockA, EntityUid dockBUid,
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 +303,8 @@ public void Dock(EntityUid dockAUid, DockingComponent dockA, EntityUid dockBUid,
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 +315,145 @@ public void Dock(EntityUid dockAUid, DockingComponent dockA, EntityUid dockBUid,
GridBUid = gridB,
};
+ _console.RefreshShuttleConsoles();
RaiseLocalEvent(dockAUid, msg);
RaiseLocalEvent(dockBUid, msg);
RaiseLocalEvent(msg);
}
- private bool CanDock(EntityUid dockAUid, EntityUid dockBUid, DockingComponent dockA, DockingComponent dockB)
+ ///
+ /// Attempts to dock 2 ports together and will return early if it's not possible.
+ ///
+ private void TryDock(Entity dockA, Entity dockB)
+ {
+ if (!CanDock(dockA, dockB))
+ return;
+
+ Dock(dockA, dockB);
+ }
+
+ public void Undock(Entity 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 (!dockA.Enabled ||
- !dockB.Enabled ||
- dockA.DockedWith != null ||
- dockB.DockedWith != null)
+ if (TerminatingOrDeleted(dockUid))
+ return;
+
+ if (TryComp(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 fixtureA = _fixtureSystem.GetFixtureOrNull(dockAUid, DockingFixture);
- var fixtureB = _fixtureSystem.GetFixtureOrNull(dockBUid, DockingFixture);
+ var dock = (dockEnt.Value, dockComp);
- if (fixtureA == null || fixtureB == null)
+ if (!CanUndock(dock))
{
- return false;
+ _popup.PopupCursor(Loc.GetString("shuttle-console-undock-fail"));
+ return;
}
- var transformA = _physics.GetPhysicsTransform(dockAUid);
- var transformB = _physics.GetPhysicsTransform(dockBUid);
- var intersect = false;
+ Undock(dock);
+ }
+
+ private void OnRequestDock(EntityUid uid, ShuttleConsoleComponent component, DockRequestMessage args)
+ {
+ var console = _console.GetDroneConsole(uid);
- for (var i = 0; i < fixtureA.Shape.ChildCount; i++)
+ if (console == null)
{
- var aabb = fixtureA.Shape.ComputeAABB(transformA, i);
+ _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail"));
+ return;
+ }
- for (var j = 0; j < fixtureB.Shape.ChildCount; j++)
- {
- var otherAABB = fixtureB.Shape.ComputeAABB(transformB, j);
- if (!aabb.Intersects(otherAABB))
- continue;
+ var shuttleUid = Transform(console.Value).GridUid;
- // TODO: Need collisionmanager's GJK for accurate checks don't @ me son
- intersect = true;
- break;
- }
+ if (!CanShuttleDock(shuttleUid))
+ {
+ _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail"));
+ return;
+ }
- if (intersect)
- break;
+ 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;
}
- return intersect;
- }
+ // Cheating?
+ if (!TryComp(ourDock, out TransformComponent? xformA) ||
+ xformA.GridUid != shuttleUid)
+ {
+ _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail"));
+ return;
+ }
- ///
- /// Attempts to dock 2 ports together and will return early if it's not possible.
- ///
- private void TryDock(EntityUid dockAUid, DockingComponent dockA, Entity dockB)
- {
- if (!CanDock(dockAUid, dockB, dockA, dockB))
+ // 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 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)
+ ///
+ /// Returns true if both docks can connect. Does not consider whether the shuttle allows it.
+ ///
+ public bool CanDock(Entity dockA, Entity dockB)
{
- if (TerminatingOrDeleted(dockUid))
- return;
+ if (dockA.Comp.DockedWith != null ||
+ dockB.Comp.DockedWith != null)
+ {
+ return false;
+ }
- if (TryComp(dockUid, out var airlock))
- _doorSystem.SetBoltsDown((dockUid, airlock), false);
+ var xformA = Transform(dockA);
+ var xformB = Transform(dockB);
- if (TryComp(dockUid, out DoorComponent? door) && _doorSystem.TryClose(dockUid, door))
- door.ChangeAirtight = true;
+ if (!xformA.Anchored || !xformB.Anchored)
+ return false;
+
+ var (worldPosA, worldRotA) = XformSystem.GetWorldPositionRotation(xformA);
+ var (worldPosB, worldRotB) = XformSystem.GetWorldPositionRotation(xformB);
- var recentlyDocked = EnsureComp(dockUid);
- recentlyDocked.LastDocked = other;
+ return CanDock(new MapCoordinates(worldPosA, xformA.MapID), worldRotA,
+ new MapCoordinates(worldPosB, xformB.MapID), worldRotB);
}
}
}
diff --git a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs
index 15907221b35..6d2c28edcfc 100644
--- a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs
+++ b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs
@@ -163,15 +163,15 @@ private void UpdateEmergencyConsole(float frameTime)
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 @@ private void UpdateEmergencyConsole(float frameTime)
}
// 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(uid);
}
@@ -217,15 +217,18 @@ private void UpdateEmergencyConsole(float frameTime)
// All the others.
if (_consoleAccumulator < minTime)
{
- var query = AllEntityQuery();
+ var query = AllEntityQuery();
// 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);
+ }
}
}
}
diff --git a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs
index 8b2c268300b..a7df41d8877 100644
--- a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs
+++ b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs
@@ -445,7 +445,7 @@ private void AddCentcomm(StationCentcommComponent component)
component.MapEntity = map;
component.Entity = grid;
- _shuttle.AddFTLDestination(grid.Value, false);
+ _shuttle.TryAddFTLDestination(mapId, false, out _);
}
public HashSet GetCentcommMaps()
diff --git a/Content.Server/Shuttles/Systems/RadarConsoleSystem.cs b/Content.Server/Shuttles/Systems/RadarConsoleSystem.cs
index fb32437c6e9..b7f08b4b349 100644
--- a/Content.Server/Shuttles/Systems/RadarConsoleSystem.cs
+++ b/Content.Server/Shuttles/Systems/RadarConsoleSystem.cs
@@ -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 @@ protected override void UpdateState(EntityUid uid, RadarConsoleComponent compone
}
if (_uiSystem.TryGetUi(uid, RadarConsoleUiKey.Key, out var bui))
- _uiSystem.SetUiState(bui, new RadarConsoleBoundInterfaceState(
- component.MaxRange,
- GetNetCoordinates(coordinates),
- angle,
- new List()
- ));
+ {
+ 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));
+ }
}
}
diff --git a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.Drone.cs b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.Drone.cs
index 99ab54f9afa..3af461bedac 100644
--- a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.Drone.cs
+++ b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.Drone.cs
@@ -7,6 +7,20 @@ namespace Content.Server.Shuttles.Systems;
public sealed partial class ShuttleConsoleSystem
{
+ ///
+ /// Gets the drone console target if applicable otherwise returns itself.
+ ///
+ public EntityUid? GetDroneConsole(EntityUid consoleUid)
+ {
+ var getShuttleEv = new ConsoleShuttleEvent
+ {
+ Console = consoleUid,
+ };
+
+ RaiseLocalEvent(consoleUid, ref getShuttleEv);
+ return getShuttleEv.Console;
+ }
+
///
/// Refreshes all drone console entities.
///
diff --git a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.FTL.cs b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.FTL.cs
new file mode 100644
index 00000000000..7606d190a45
--- /dev/null
+++ b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.FTL.cs
@@ -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(OnBeaconStartup);
+ SubscribeLocalEvent(OnBeaconAnchorChanged);
+
+ SubscribeLocalEvent(OnExclusionStartup);
+ }
+
+ private void OnExclusionStartup(Entity ent, ref ComponentStartup args)
+ {
+ RefreshShuttleConsoles();
+ }
+
+ private void OnBeaconStartup(Entity ent, ref ComponentStartup args)
+ {
+ RefreshShuttleConsoles();
+ }
+
+ private void OnBeaconAnchorChanged(Entity ent, ref AnchorStateChangedEvent args)
+ {
+ RefreshShuttleConsoles();
+ }
+
+ private void OnBeaconFTLMessage(Entity 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, targetCoordinates, angle, targetXform.MapID);
+ }
+
+ private void OnPositionFTLMessage(Entity 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, targetCoordinates, angle, args.Coordinates.MapId);
+ }
+
+ private void GetBeacons(ref List? beacons)
+ {
+ var beaconQuery = AllEntityQuery();
+
+ 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();
+ beacons.Add(new ShuttleBeaconObject(GetNetEntity(destUid), GetNetCoordinates(destXform.Coordinates), name));
+ }
+ }
+
+ private void GetExclusions(ref List? exclusions)
+ {
+ var query = AllEntityQuery();
+
+ while (query.MoveNext(out var comp, out var xform))
+ {
+ if (!comp.Enabled)
+ continue;
+
+ exclusions ??= new List();
+ exclusions.Add(new ShuttleExclusionObject(GetNetCoordinates(xform.Coordinates), comp.Range, Loc.GetString("shuttle-console-exclusion")));
+ }
+ }
+
+ ///
+ /// Handles shuttle console FTLs.
+ ///
+ private void ConsoleFTL(Entity ent, 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? exclusions = null;
+ GetExclusions(ref exclusions);
+
+ if (!_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);
+ }
+}
diff --git a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs
index 18dd3b0baf0..c47c519d5de 100644
--- a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs
+++ b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs
@@ -3,7 +3,6 @@
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.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 _metaQuery;
+ private EntityQuery