diff --git a/Content.Client/Radar/RadarConsoleWindow.xaml b/Content.Client/Radar/RadarConsoleWindow.xaml
deleted file mode 100644
index 92e25bf7fcfb..000000000000
--- a/Content.Client/Radar/RadarConsoleWindow.xaml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
diff --git a/Content.Client/Radar/RadarConsoleWindow.xaml.cs b/Content.Client/Radar/RadarConsoleWindow.xaml.cs
deleted file mode 100644
index 048541ebe4b9..000000000000
--- a/Content.Client/Radar/RadarConsoleWindow.xaml.cs
+++ /dev/null
@@ -1,116 +0,0 @@
-using System;
-using Content.Client.Computer;
-using Content.Shared.Radar;
-using JetBrains.Annotations;
-using Robust.Client.AutoGenerated;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.CustomControls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Maths;
-
-namespace Content.Client.Radar;
-
-[GenerateTypedNameReferences]
-public sealed partial class RadarConsoleWindow : DefaultWindow, IComputerWindow
-{
- public RadarConsoleWindow()
- {
- RobustXamlLoader.Load(this);
- }
-
- public void SetupComputerWindow(ComputerBoundUserInterfaceBase cb)
- {
-
- }
-
- public void UpdateState(RadarConsoleBoundInterfaceState scc)
- {
- Radar.UpdateState(scc);
- }
-}
-
-
-public sealed class RadarControl : Control
-{
- private const int MinimapRadius = 256;
- private const int MinimapMargin = 4;
- private const float GridLinesDistance = 32f;
-
- private float _radarRange = 256f;
- private RadarConsoleBoundInterfaceState _lastState = new(256f, Array.Empty());
-
- private int SizeFull => (int) ((MinimapRadius + MinimapMargin) * 2 * UIScale);
- private int ScaledMinimapRadius => (int) (MinimapRadius * UIScale);
- private float MinimapScale => _radarRange != 0 ? ScaledMinimapRadius / _radarRange : 0f;
-
- public RadarControl()
- {
- MinSize = (SizeFull, SizeFull);
- }
-
- public void UpdateState(RadarConsoleBoundInterfaceState ls)
- {
- if (!_radarRange.Equals(ls.Range))
- {
- _radarRange = ls.Range;
- }
-
- _lastState = ls;
- }
-
- protected override void Draw(DrawingHandleScreen handle)
- {
- var point = SizeFull / 2;
- var fakeAA = new Color(0.08f, 0.08f, 0.08f);
- var gridLines = new Color(0.08f, 0.08f, 0.08f);
- var gridLinesRadial = 8;
- var gridLinesEquatorial = (int) Math.Floor(_radarRange / GridLinesDistance);
-
- handle.DrawCircle((point, point), ScaledMinimapRadius + 1, fakeAA);
- handle.DrawCircle((point, point), ScaledMinimapRadius, Color.Black);
-
- for (var i = 1; i < gridLinesEquatorial + 1; i++)
- {
- handle.DrawCircle((point, point), 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((point, point) - aExtent, (point, point) + aExtent, gridLines);
- }
-
- handle.DrawLine((point, point) + new Vector2(8, 8), (point, point) - new Vector2(0, 8), Color.Yellow);
- handle.DrawLine((point, point) + new Vector2(-8, 8), (point, point) - new Vector2(0, 8), Color.Yellow);
-
- foreach (var obj in _lastState.Objects)
- {
- var minimapPos = obj.Position * MinimapScale;
- var radius = obj.Radius * MinimapScale;
-
- if (minimapPos.Length + radius > ScaledMinimapRadius)
- continue;
-
- switch (obj.Shape)
- {
- case RadarObjectShape.CircleFilled:
- case RadarObjectShape.Circle:
- {
- handle.DrawCircle(minimapPos + point, radius, obj.Color, obj.Shape == RadarObjectShape.CircleFilled);
- break;
- }
- default:
- throw new NotImplementedException();
- }
- }
- }
-}
-
-[UsedImplicitly]
-public sealed class RadarConsoleBoundUserInterface : ComputerBoundUserInterface
-{
- public RadarConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
-}
diff --git a/Content.Client/Shuttles/BUI/RadarConsoleBoundUserInterface.cs b/Content.Client/Shuttles/BUI/RadarConsoleBoundUserInterface.cs
new file mode 100644
index 000000000000..4c5b4edea8ab
--- /dev/null
+++ b/Content.Client/Shuttles/BUI/RadarConsoleBoundUserInterface.cs
@@ -0,0 +1,29 @@
+using Content.Client.Shuttles.UI;
+using Content.Shared.Shuttles.BUIStates;
+using JetBrains.Annotations;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Shuttles.BUI;
+
+[UsedImplicitly]
+public sealed class RadarConsoleBoundUserInterface : BoundUserInterface
+{
+ private RadarConsoleWindow? _window;
+
+ public RadarConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
+
+ protected override void Open()
+ {
+ base.Open();
+ _window = new RadarConsoleWindow();
+ _window?.OpenCentered();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+ if (state is not RadarConsoleBoundInterfaceState cState) return;
+
+ _window?.UpdateState(cState);
+ }
+}
diff --git a/Content.Client/Shuttles/BUI/ShuttleConsoleBoundUserInterface.cs b/Content.Client/Shuttles/BUI/ShuttleConsoleBoundUserInterface.cs
new file mode 100644
index 000000000000..cb60508c310c
--- /dev/null
+++ b/Content.Client/Shuttles/BUI/ShuttleConsoleBoundUserInterface.cs
@@ -0,0 +1,70 @@
+using Content.Client.Shuttles.UI;
+using Content.Shared.Shuttles.BUIStates;
+using Content.Shared.Shuttles.Components;
+using Content.Shared.Shuttles.Events;
+using JetBrains.Annotations;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Shuttles.BUI;
+
+[UsedImplicitly]
+public sealed class ShuttleConsoleBoundUserInterface : BoundUserInterface
+{
+ private ShuttleConsoleWindow? _window;
+
+ public ShuttleConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
+
+ protected override void Open()
+ {
+ base.Open();
+ _window = new ShuttleConsoleWindow();
+ _window.ShuttleModePressed += OnShuttleModePressed;
+ _window.UndockPressed += OnUndockPressed;
+ _window.StartAutodockPressed += OnAutodockPressed;
+ _window.StopAutodockPressed += OnStopAutodockPressed;
+ _window.OpenCentered();
+ _window.OnClose += OnClose;
+ }
+
+ private void OnClose()
+ {
+ Close();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (disposing)
+ {
+ _window?.Dispose();
+ }
+ }
+
+ private void OnStopAutodockPressed(EntityUid obj)
+ {
+ SendMessage(new StopAutodockRequestMessage() {Entity = obj});
+ }
+
+ private void OnAutodockPressed(EntityUid obj)
+ {
+ SendMessage(new AutodockRequestMessage() {Entity = obj});
+ }
+
+ private void OnUndockPressed(EntityUid obj)
+ {
+ SendMessage(new UndockRequestMessage() {Entity = obj});
+ }
+
+ private void OnShuttleModePressed(ShuttleMode obj)
+ {
+ SendMessage(new ShuttleModeRequestMessage() {Mode = obj});
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+ if (state is not ShuttleConsoleBoundInterfaceState cState) return;
+ _window?.UpdateState(cState);
+ }
+}
diff --git a/Content.Client/Shuttles/ShuttleConsoleComponent.cs b/Content.Client/Shuttles/ShuttleConsoleComponent.cs
index 86c4410b8698..da6b9147948b 100644
--- a/Content.Client/Shuttles/ShuttleConsoleComponent.cs
+++ b/Content.Client/Shuttles/ShuttleConsoleComponent.cs
@@ -1,13 +1,7 @@
-using Content.Shared.Shuttles;
using Content.Shared.Shuttles.Components;
-using Robust.Shared.GameObjects;
namespace Content.Client.Shuttles
{
[RegisterComponent]
- [ComponentReference(typeof(SharedShuttleConsoleComponent))]
- internal sealed class ShuttleConsoleComponent : SharedShuttleConsoleComponent
- {
-
- }
+ public sealed class ShuttleConsoleComponent : SharedShuttleConsoleComponent {}
}
diff --git a/Content.Client/Shuttles/ShuttleConsoleSystem.cs b/Content.Client/Shuttles/ShuttleConsoleSystem.cs
deleted file mode 100644
index cb0f6f7b0c1e..000000000000
--- a/Content.Client/Shuttles/ShuttleConsoleSystem.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using Content.Shared.Shuttles;
-
-namespace Content.Client.Shuttles
-{
- internal sealed class ShuttleConsoleSystem : SharedShuttleConsoleSystem
- {
-
- }
-}
diff --git a/Content.Client/Shuttles/Systems/DockingSystem.cs b/Content.Client/Shuttles/Systems/DockingSystem.cs
new file mode 100644
index 000000000000..61f0b8d06c96
--- /dev/null
+++ b/Content.Client/Shuttles/Systems/DockingSystem.cs
@@ -0,0 +1,21 @@
+using Content.Shared.Shuttles.Events;
+
+namespace Content.Client.Shuttles.Systems;
+
+public sealed class DockingSystem : EntitySystem
+{
+ public void StartAutodock(EntityUid uid)
+ {
+ RaiseNetworkEvent(new AutodockRequestMessage {Entity = uid});
+ }
+
+ public void StopAutodock(EntityUid uid)
+ {
+ RaiseNetworkEvent(new StopAutodockRequestMessage() {Entity = uid});
+ }
+
+ public void Undock(EntityUid uid)
+ {
+ RaiseNetworkEvent(new UndockRequestMessage() {Entity = uid});
+ }
+}
diff --git a/Content.Client/Shuttles/Systems/RadarConsoleSystem.cs b/Content.Client/Shuttles/Systems/RadarConsoleSystem.cs
new file mode 100644
index 000000000000..0a0ddcf2eef4
--- /dev/null
+++ b/Content.Client/Shuttles/Systems/RadarConsoleSystem.cs
@@ -0,0 +1,8 @@
+using Content.Shared.Shuttles.Systems;
+
+namespace Content.Client.Shuttles.Systems;
+
+public sealed class RadarConsoleSystem : SharedRadarConsoleSystem
+{
+
+}
diff --git a/Content.Client/Shuttles/Systems/ShuttleConsoleSystem.cs b/Content.Client/Shuttles/Systems/ShuttleConsoleSystem.cs
new file mode 100644
index 000000000000..7ae419c358b3
--- /dev/null
+++ b/Content.Client/Shuttles/Systems/ShuttleConsoleSystem.cs
@@ -0,0 +1,37 @@
+using Content.Shared.Shuttles.Components;
+using Content.Shared.Shuttles.Events;
+using Content.Shared.Shuttles.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Client.Shuttles.Systems
+{
+ public sealed class ShuttleConsoleSystem : SharedShuttleConsoleSystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnHandleState);
+ }
+
+ private void OnHandleState(EntityUid uid, PilotComponent component, ref ComponentHandleState args)
+ {
+ if (args.Current is not PilotComponentState state) return;
+
+ var console = state.Console.GetValueOrDefault();
+ if (!console.IsValid())
+ {
+ component.Console = null;
+ return;
+ }
+
+ if (!TryComp(console, out var shuttleConsoleComponent))
+ {
+ Logger.Warning($"Unable to set Helmsman console to {console}");
+ return;
+ }
+
+ component.Console = shuttleConsoleComponent;
+ ActionBlockerSystem.UpdateCanMove(uid);
+ }
+ }
+}
diff --git a/Content.Client/Shuttles/UI/DockingControl.cs b/Content.Client/Shuttles/UI/DockingControl.cs
new file mode 100644
index 000000000000..dfa5023a8a57
--- /dev/null
+++ b/Content.Client/Shuttles/UI/DockingControl.cs
@@ -0,0 +1,251 @@
+using Content.Shared.Shuttles.BUIStates;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Shared.Map;
+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 const int MinimapRadius = 384;
+ private const int MinimapMargin = 4;
+
+ private float _range = 8f;
+ private float _rangeSquared = 0f;
+ private const float GridLinesDistance = 32f;
+
+ private int MidPoint => SizeFull / 2;
+ private int SizeFull => (int) ((MinimapRadius + MinimapMargin) * 2 * UIScale);
+ private int ScaledMinimapRadius => (int) (MinimapRadius * UIScale);
+ private float MinimapScale => _range != 0 ? ScaledMinimapRadius / _range : 0f;
+
+ public EntityUid? ViewedDock;
+ public EntityUid? GridEntity;
+
+ ///
+ /// Stored by GridID then by docks
+ ///
+ public Dictionary> Docks = new();
+
+ public DockingControl()
+ {
+ _entManager = IoCManager.Resolve();
+ _mapManager = IoCManager.Resolve();
+ _rangeSquared = _range * _range;
+ MinSize = (SizeFull, SizeFull);
+ }
+
+ protected override void Draw(DrawingHandleScreen handle)
+ {
+ base.Draw(handle);
+
+ var fakeAA = new Color(0.08f, 0.08f, 0.08f);
+
+ handle.DrawCircle((MidPoint, MidPoint), ScaledMinimapRadius + 1, fakeAA);
+ handle.DrawCircle((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((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((MidPoint, MidPoint) - aExtent, (MidPoint, MidPoint) + aExtent, gridLines);
+ }
+
+ if (!_entManager.TryGetComponent(ViewedDock, out var xform) ||
+ !_entManager.TryGetComponent(GridEntity, out var gridXform)) return;
+
+ var rotation = Matrix3.CreateRotation(xform.LocalRotation);
+ var matrix = Matrix3.CreateTranslation(-xform.LocalPosition);
+
+ // Draw the fixtures around the dock before drawing it
+ if (_entManager.TryGetComponent(GridEntity, out var fixtures))
+ {
+ foreach (var (_, fixture) in fixtures.Fixtures)
+ {
+ 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(xform.LocalPosition);
+ var gridInvMatrix = gridXform.InvWorldMatrix;
+ Matrix3.Multiply(in gridInvMatrix, in matrix, out var invMatrix);
+
+ // TODO: Getting some overdraw so need to fix that.
+
+ foreach (var grid in _mapManager.FindGridsIntersecting(gridXform.MapID,
+ new Box2(worldPos - _range, worldPos + _range)))
+ {
+ if (grid.GridEntityId == GridEntity) continue;
+
+ // Draw the fixtures before drawing any docks in range.
+ if (!_entManager.TryGetComponent(grid.GridEntityId, out var gridFixtures)) continue;
+
+ var gridMatrix = 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(grid.GridEntityId, 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 + MidPoint;
+ }
+}
diff --git a/Content.Client/Shuttles/UI/RadarConsoleWindow.xaml b/Content.Client/Shuttles/UI/RadarConsoleWindow.xaml
new file mode 100644
index 000000000000..7591034b921c
--- /dev/null
+++ b/Content.Client/Shuttles/UI/RadarConsoleWindow.xaml
@@ -0,0 +1,6 @@
+
+
+
diff --git a/Content.Client/Shuttles/UI/RadarConsoleWindow.xaml.cs b/Content.Client/Shuttles/UI/RadarConsoleWindow.xaml.cs
new file mode 100644
index 000000000000..7274cc23c8f7
--- /dev/null
+++ b/Content.Client/Shuttles/UI/RadarConsoleWindow.xaml.cs
@@ -0,0 +1,22 @@
+using Content.Client.Computer;
+using Content.Client.UserInterface;
+using Content.Shared.Shuttles.BUIStates;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Shuttles.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class RadarConsoleWindow : FancyWindow,
+ IComputerWindow
+{
+ public RadarConsoleWindow()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public void UpdateState(RadarConsoleBoundInterfaceState scc)
+ {
+ RadarScreen.UpdateState(scc);
+ }
+}
diff --git a/Content.Client/Shuttles/UI/RadarControl.cs b/Content.Client/Shuttles/UI/RadarControl.cs
new file mode 100644
index 000000000000..f8d2f8c6e87f
--- /dev/null
+++ b/Content.Client/Shuttles/UI/RadarControl.cs
@@ -0,0 +1,348 @@
+using Content.Client.Stylesheets;
+using Content.Shared.Shuttles.BUIStates;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Map;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Collision.Shapes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Shuttles.UI;
+
+///
+/// Displays nearby grids inside of a control.
+///
+public sealed class RadarControl : Control
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
+
+ private const float ScrollSensitivity = 8f;
+
+ private const int MinimapRadius = 384;
+ private const int MinimapMargin = 4;
+ private const float GridLinesDistance = 32f;
+
+ ///
+ /// Entity used to transform all of the radar objects.
+ ///
+ private EntityUid? _entity;
+
+ private float _radarMinRange = 64f;
+ private float _radarMaxRange = 256f;
+ public float RadarRange { get; private set; } = 256f;
+
+ private int MidPoint => SizeFull / 2;
+ private int SizeFull => (int) ((MinimapRadius + MinimapMargin) * 2 * UIScale);
+ private int ScaledMinimapRadius => (int) (MinimapRadius * UIScale);
+ private float MinimapScale => RadarRange != 0 ? ScaledMinimapRadius / RadarRange : 0f;
+
+ ///
+ /// 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 EntityUid? HighlightedDock;
+
+ public Action? OnRadarRangeChanged;
+
+ public RadarControl()
+ {
+ IoCManager.InjectDependencies(this);
+ MinSize = (SizeFull, SizeFull);
+ }
+
+ public void UpdateState(RadarConsoleBoundInterfaceState ls)
+ {
+ _radarMaxRange = ls.MaxRange;
+
+ if (_radarMaxRange < RadarRange)
+ {
+ RadarRange = _radarMaxRange;
+ OnRadarRangeChanged?.Invoke(RadarRange);
+ }
+
+ if (_radarMaxRange < _radarMinRange)
+ _radarMinRange = _radarMaxRange;
+
+ _entity = ls.Entity;
+ _docks.Clear();
+
+ foreach (var state in ls.Docks)
+ {
+ var coordinates = state.Coordinates;
+ var grid = _docks.GetOrNew(coordinates.EntityId);
+ grid.Add(state.Entity, state);
+ }
+ }
+
+ protected override void MouseWheel(GUIMouseWheelEventArgs args)
+ {
+ base.MouseWheel(args);
+ AddRadarRange(-args.Delta.Y * ScrollSensitivity);
+ }
+
+ public void AddRadarRange(float value)
+ {
+ var oldValue = RadarRange;
+ RadarRange = MathF.Max(0f, MathF.Max(_radarMinRange, MathF.Min(RadarRange + value, _radarMaxRange)));
+
+ if (oldValue.Equals(RadarRange)) return;
+
+ OnRadarRangeChanged?.Invoke(RadarRange);
+ }
+
+ protected override void Draw(DrawingHandleScreen handle)
+ {
+ var fakeAA = new Color(0.08f, 0.08f, 0.08f);
+
+ handle.DrawCircle((MidPoint, MidPoint), ScaledMinimapRadius + 1, fakeAA);
+ handle.DrawCircle((MidPoint, MidPoint), ScaledMinimapRadius, Color.Black);
+
+ // No data
+ if (_entity == null)
+ {
+ Clear();
+ return;
+ }
+
+ var gridLines = new Color(0.08f, 0.08f, 0.08f);
+ var gridLinesRadial = 8;
+ var gridLinesEquatorial = (int) Math.Floor(RadarRange / GridLinesDistance);
+
+ for (var i = 1; i < gridLinesEquatorial + 1; i++)
+ {
+ handle.DrawCircle((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((MidPoint, MidPoint) - aExtent, (MidPoint, MidPoint) + aExtent, gridLines);
+ }
+
+ var metaQuery = _entManager.GetEntityQuery();
+ var xformQuery = _entManager.GetEntityQuery();
+ var fixturesQuery = _entManager.GetEntityQuery();
+ var bodyQuery = _entManager.GetEntityQuery();
+ var xform = xformQuery.GetComponent(_entity.Value);
+ var mapPosition = xform.MapPosition;
+
+ if (mapPosition.MapId == MapId.Nullspace)
+ {
+ Clear();
+ return;
+ }
+
+ // Can also use ourGridBody.LocalCenter
+ var offset = xform.Coordinates.Position;
+ var offsetMatrix = Matrix3.CreateTranslation(-offset);
+ Matrix3 matrix;
+
+ // Draw our grid in detail
+ var ourGridId = xform.GridID;
+ if (ourGridId != GridId.Invalid)
+ {
+ matrix = xform.InvWorldMatrix;
+ var ourGridFixtures = fixturesQuery.GetComponent(ourGridId);
+ // Draw our grid; use non-filled boxes so it doesn't look awful.
+ DrawGrid(handle, offsetMatrix, ourGridFixtures, Color.Yellow);
+
+ DrawDocks(handle, xform.GridEntityId, offsetMatrix);
+ }
+ else
+ {
+ matrix = Matrix3.CreateTranslation(-offset);
+ }
+
+ var invertedPosition = xform.Coordinates.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();
+
+ // Draw other grids... differently
+ foreach (var grid in _mapManager.FindGridsIntersecting(mapPosition.MapId,
+ new Box2(mapPosition.Position - RadarRange, mapPosition.Position + RadarRange)))
+ {
+ if (grid.Index == ourGridId) continue;
+
+ var gridBody = bodyQuery.GetComponent(grid.GridEntityId);
+ if (gridBody.Mass < 10f)
+ {
+ ClearLabel(grid.GridEntityId);
+ continue;
+ }
+
+ shown.Add(grid.GridEntityId);
+ var name = metaQuery.GetComponent(grid.GridEntityId).EntityName;
+
+ if (name == string.Empty)
+ name = Loc.GetString("shuttle-console-unknown");
+
+ var gridXform = xformQuery.GetComponent(grid.GridEntityId);
+ var gridFixtures = fixturesQuery.GetComponent(grid.GridEntityId);
+ var gridMatrix = gridXform.WorldMatrix;
+ Matrix3.Multiply(in gridMatrix, in matrix, out var matty);
+
+ if (ShowIFF)
+ {
+ if (!_iffControls.TryGetValue(grid.GridEntityId, out var control))
+ {
+ var label = new Label()
+ {
+ HorizontalAlignment = HAlignment.Left,
+ };
+
+ control = new PanelContainer()
+ {
+ HorizontalAlignment = HAlignment.Left,
+ VerticalAlignment = VAlignment.Top,
+ Children = { label },
+ StyleClasses = { StyleNano.StyleClassTooltipPanel },
+ };
+
+ _iffControls[grid.GridEntityId] = control;
+ AddChild(control);
+ }
+
+ var gridCentre = matty.Transform(gridBody.LocalCenter);
+ gridCentre.Y = -gridCentre.Y;
+
+ // TODO: When we get IFF or whatever we can show controls at a further distance; for now
+ // we don't do that because it would immediately reveal nukies.
+ if (gridCentre.Length < RadarRange)
+ {
+ control.Visible = true;
+ var label = (Label) control.GetChild(0);
+ label.Text = Loc.GetString("shuttle-console-iff-label", ("name", name), ("distance", $"{gridCentre.Length:0.0}"));
+ LayoutContainer.SetPosition(control, ScalePosition(gridCentre) / UIScale);
+ }
+ else
+ {
+ control.Visible = false;
+ }
+ }
+ else
+ {
+ ClearLabel(grid.GridEntityId);
+ }
+
+ // Detailed view
+ DrawGrid(handle, matty, gridFixtures, Color.Aquamarine);
+
+ DrawDocks(handle, grid.GridEntityId, 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 = 1.2f;
+
+ if (_docks.TryGetValue(uid, out var docks))
+ {
+ foreach (var (ent, state) in docks)
+ {
+ var position = state.Coordinates.Position;
+ var uiPosition = matrix.Transform(position);
+
+ if (uiPosition.Length > RadarRange - DockScale) continue;
+
+ var color = HighlightedDock == ent ? Color.Magenta : Color.DarkViolet;
+
+ 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);
+ }
+ }
+ }
+
+ private void DrawGrid(DrawingHandleScreen handle, Matrix3 matrix, FixturesComponent component, Color color)
+ {
+ foreach (var (_, fixture) in component.Fixtures)
+ {
+ // If the fixture has any points out of range we won't draw any of it.
+ var invalid = false;
+ var poly = (PolygonShape) fixture.Shape;
+ var verts = new Vector2[poly.VertexCount + 1];
+
+ for (var i = 0; i < poly.VertexCount; i++)
+ {
+ var vert = matrix.Transform(poly.Vertices[i]);
+
+ if (vert.Length > RadarRange)
+ {
+ invalid = true;
+ break;
+ }
+
+ vert.Y = -vert.Y;
+ verts[i] = ScalePosition(vert);
+ }
+
+ if (invalid) continue;
+
+ // Closed list
+ verts[poly.VertexCount] = verts[0];
+ handle.DrawPrimitives(DrawPrimitiveTopology.LineStrip, verts, color);
+ }
+ }
+
+ private Vector2 ScalePosition(Vector2 value)
+ {
+ return value * MinimapScale + MidPoint;
+ }
+}
diff --git a/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml b/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml
new file mode 100644
index 000000000000..58d06e575ae1
--- /dev/null
+++ b/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs b/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs
new file mode 100644
index 000000000000..3d2c4b1a8089
--- /dev/null
+++ b/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs
@@ -0,0 +1,239 @@
+using Content.Client.Computer;
+using Content.Client.UserInterface;
+using Content.Shared.Shuttles.BUIStates;
+using Content.Shared.Shuttles.Components;
+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.Utility;
+
+namespace Content.Client.Shuttles.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class ShuttleConsoleWindow : FancyWindow,
+ IComputerWindow
+{
+ private readonly IEntityManager _entManager;
+
+ ///
+ /// EntityUid of the open console.
+ ///
+ private EntityUid? _entity;
+
+ ///
+ /// Currently selected dock button for camera.
+ ///
+ private BaseButton? _selectedDock;
+
+ ///
+ /// Stored by grid entityid then by states
+ ///
+ private Dictionary> _docks = new();
+
+ public Action? ShuttleModePressed;
+ public Action? UndockPressed;
+ public Action? StartAutodockPressed;
+ public Action? StopAutodockPressed;
+
+ public ShuttleConsoleWindow()
+ {
+ RobustXamlLoader.Load(this);
+ _entManager = IoCManager.Resolve();
+
+ OnRadarRangeChange(RadarScreen.RadarRange);
+ RadarScreen.OnRadarRangeChanged += OnRadarRangeChange;
+
+ IFFToggle.OnToggled += OnIFFTogglePressed;
+ IFFToggle.Pressed = RadarScreen.ShowIFF;
+
+ DockToggle.OnToggled += OnDockTogglePressed;
+ DockToggle.Pressed = RadarScreen.ShowDocks;
+
+ ShuttleModeDisplay.OnToggled += OnShuttleModePressed;
+
+ UndockButton.OnPressed += OnUndockPressed;
+ }
+
+ private void OnRadarRangeChange(float value)
+ {
+ RadarRange.Text = $"{value:0}";
+ }
+
+ private void OnShuttleModePressed(BaseButton.ButtonEventArgs obj)
+ {
+ ShuttleModePressed?.Invoke(obj.Button.Pressed ? ShuttleMode.Strafing : ShuttleMode.Cruise);
+ }
+
+ 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;
+ }
+
+ private void OnUndockPressed(BaseButton.ButtonEventArgs args)
+ {
+ if (DockingScreen.ViewedDock == null) return;
+ UndockPressed?.Invoke(DockingScreen.ViewedDock.Value);
+ }
+
+ public void UpdateState(ShuttleConsoleBoundInterfaceState scc)
+ {
+ _entity = scc.Entity;
+ UpdateDocks(scc.Docks);
+ RadarScreen.UpdateState(scc);
+ MaxRadarRange.Text = $"{scc.MaxRange:0}";
+ ShuttleModeDisplay.Pressed = scc.Mode == ShuttleMode.Strafing;
+ }
+
+ #region Docking
+
+ private void UpdateDocks(List docks)
+ {
+ // 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)
+ {
+ var grid = _docks.GetOrNew(dock.Coordinates.EntityId);
+ grid.Add(dock);
+ }
+
+ DockPorts.DisposeAllChildren();
+ DockingScreen.Docks = _docks;
+
+ if (!_entManager.TryGetComponent(_entity, out var xform))
+ {
+ // TODO: Show Placeholder
+ return;
+ }
+
+ if (_docks.TryGetValue(xform.GridEntityId, out var gridDocks))
+ {
+ var index = 1;
+
+ foreach (var state in gridDocks)
+ {
+ var ent = state.Entity;
+ var pressed = ent == 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,
+ };
+
+ button.OnMouseEntered += args => OnDockMouseEntered(args, ent);
+ button.OnMouseExited += args => OnDockMouseExited(args, ent);
+ button.OnToggled += args => OnDockToggled(args, ent);
+ DockPorts.AddChild(button);
+ index++;
+ }
+ }
+ }
+
+ private void OnDockMouseEntered(GUIMouseHoverEventArgs obj, EntityUid uid)
+ {
+ RadarScreen.HighlightedDock = uid;
+ }
+
+ private void OnDockMouseExited(GUIMouseHoverEventArgs obj, EntityUid uid)
+ {
+ RadarScreen.HighlightedDock = null;
+ }
+
+ ///
+ /// Shows a docking camera instead of radar screen.
+ ///
+ private void OnDockToggled(BaseButton.ButtonEventArgs obj, EntityUid ent)
+ {
+ if (_selectedDock != null)
+ {
+ _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
+ {
+ // DebugTools.Assert(DockingScreen.ViewedDock == null);
+ _entManager.TryGetComponent(_entity, out var xform);
+
+ UndockButton.Disabled = false;
+ RadarScreen.Visible = false;
+ DockingScreen.Visible = true;
+ DockingScreen.ViewedDock = ent;
+ StartAutodockPressed?.Invoke(ent);
+ DockingScreen.GridEntity = xform?.GridEntityId;
+ _selectedDock = obj.Button;
+ }
+ }
+
+ public override void Close()
+ {
+ base.Close();
+ if (DockingScreen.ViewedDock != null)
+ {
+ StopAutodockPressed?.Invoke(DockingScreen.ViewedDock.Value);
+ }
+ }
+
+ #endregion
+
+ protected override void Draw(DrawingHandleScreen handle)
+ {
+ base.Draw(handle);
+
+ if (!_entManager.TryGetComponent(_entity, out var entXform) ||
+ !_entManager.TryGetComponent(entXform.GridEntityId, out var gridBody) ||
+ !_entManager.TryGetComponent(entXform.GridEntityId, out var gridXform))
+ {
+ return;
+ }
+
+ var (_, worldRot, worldMatrix) = gridXform.GetWorldPositionRotationMatrix();
+ 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:0.0}, {gridVelocity.Y:0.0}";
+ GridAngularVelocity.Text = $"{-gridBody.AngularVelocity:0.0}";
+ }
+}
diff --git a/Content.Server/Alert/Click/StopPiloting.cs b/Content.Server/Alert/Click/StopPiloting.cs
index e25c23a4d35d..e0c3b791d020 100644
--- a/Content.Server/Alert/Click/StopPiloting.cs
+++ b/Content.Server/Alert/Click/StopPiloting.cs
@@ -1,4 +1,4 @@
-using Content.Server.Shuttles.EntitySystems;
+using Content.Server.Shuttles.Systems;
using Content.Shared.Alert;
using Content.Shared.Shuttles.Components;
using JetBrains.Annotations;
diff --git a/Content.Server/Atmos/EntitySystems/AutomaticAtmosSystem.cs b/Content.Server/Atmos/EntitySystems/AutomaticAtmosSystem.cs
index b89599a15692..4b9cb8dba9ca 100644
--- a/Content.Server/Atmos/EntitySystems/AutomaticAtmosSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/AutomaticAtmosSystem.cs
@@ -1,5 +1,5 @@
using Content.Server.Atmos.Components;
-using Content.Server.Shuttles.EntitySystems;
+using Content.Server.Shuttles.Systems;
using Content.Shared.Maps;
using Robust.Shared.Map;
diff --git a/Content.Server/Physics/Controllers/MoverController.cs b/Content.Server/Physics/Controllers/MoverController.cs
index cd24fcf4b990..208534dcdd16 100644
--- a/Content.Server/Physics/Controllers/MoverController.cs
+++ b/Content.Server/Physics/Controllers/MoverController.cs
@@ -1,5 +1,5 @@
using Content.Server.Shuttles.Components;
-using Content.Server.Shuttles.EntitySystems;
+using Content.Server.Shuttles.Systems;
using Content.Shared.Vehicle.Components;
using Content.Shared.Movement;
using Content.Shared.Movement.Components;
@@ -123,7 +123,7 @@ private void HandleShuttleMovement(float frameTime)
angularInput += sprint.X;
}
break;
- case ShuttleMode.Docking:
+ case ShuttleMode.Strafing:
// No angular input possible
foreach (var (pilot, mover) in pilots)
{
diff --git a/Content.Server/Radar/RadarConsoleComponent.cs b/Content.Server/Radar/RadarConsoleComponent.cs
deleted file mode 100644
index 9e101a6e39b3..000000000000
--- a/Content.Server/Radar/RadarConsoleComponent.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Content.Server.Radar;
-
-[RegisterComponent]
-public sealed class RadarConsoleComponent : Component
-{
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("range")]
- public float Range = 512f;
-}
diff --git a/Content.Server/Radar/RadarConsoleSystem.cs b/Content.Server/Radar/RadarConsoleSystem.cs
deleted file mode 100644
index db3a0e083422..000000000000
--- a/Content.Server/Radar/RadarConsoleSystem.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using Content.Server.UserInterface;
-using Content.Shared.Radar;
-using Robust.Shared.Map;
-
-namespace Content.Server.Radar;
-
-public sealed class RadarConsoleSystem : EntitySystem
-{
- [Dependency] private readonly IMapManager _mapManager = default!;
-
- private static float Frequency = 1.5f;
-
- private float _accumulator;
-
- public override void Update(float frameTime)
- {
- _accumulator += frameTime;
-
- if (_accumulator < Frequency)
- return;
-
- _accumulator -= Frequency;
-
- foreach (var (component, xform) in EntityManager.EntityQuery())
- {
- var s = component.Owner.GetUIOrNull(RadarConsoleUiKey.Key);
-
- if (s is null || s.SubscribedSessions.Count == 0)
- continue;
-
- var (radarPos, _, radarInvMatrix) = xform.GetWorldPositionRotationInvMatrix();
-
- var mapId = xform.MapID;
- var objects = new List();
-
- _mapManager.FindGridsIntersectingEnumerator(mapId, new Box2(radarPos - component.Range, radarPos + component.Range), out var enumerator, true);
-
- while (enumerator.MoveNext(out var grid))
- {
- var phy = Comp(grid.GridEntityId);
- var transform = Transform(grid.GridEntityId);
-
- if (phy.Mass < 50)
- continue;
-
- var rad = Math.Log2(phy.Mass);
- var gridCenter = transform.WorldMatrix.Transform(phy.LocalCenter);
-
- var pos = radarInvMatrix.Transform(gridCenter);
- pos.Y = -pos.Y; // Robust has an inverted Y, like BYOND. This undoes that.
-
- if (pos.Length > component.Range)
- continue;
-
- objects.Add(new RadarObjectData {Color = Color.Aqua, Position = pos, Radius = (float)rad});
- }
-
- s.SetState(new RadarConsoleBoundInterfaceState(component.Range, objects.ToArray()));
- }
- }
-}
diff --git a/Content.Server/Shuttles/Components/AutoDockComponent.cs b/Content.Server/Shuttles/Components/AutoDockComponent.cs
new file mode 100644
index 000000000000..91e895cbdf9e
--- /dev/null
+++ b/Content.Server/Shuttles/Components/AutoDockComponent.cs
@@ -0,0 +1,14 @@
+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 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/RecentlyDockedComponent.cs b/Content.Server/Shuttles/Components/RecentlyDockedComponent.cs
new file mode 100644
index 000000000000..3f14a14fbac2
--- /dev/null
+++ b/Content.Server/Shuttles/Components/RecentlyDockedComponent.cs
@@ -0,0 +1,15 @@
+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 class RecentlyDockedComponent : Component
+{
+ [ViewVariables, DataField("lastDocked")]
+ public EntityUid LastDocked;
+
+ [ViewVariables(VVAccess.ReadWrite), DataField("radius")]
+ public float Radius = 1.5f;
+}
diff --git a/Content.Server/Shuttles/Components/ShuttleComponent.cs b/Content.Server/Shuttles/Components/ShuttleComponent.cs
index 0705b6af5378..3fc0461f6fcb 100644
--- a/Content.Server/Shuttles/Components/ShuttleComponent.cs
+++ b/Content.Server/Shuttles/Components/ShuttleComponent.cs
@@ -3,8 +3,14 @@
namespace Content.Server.Shuttles.Components
{
[RegisterComponent]
- public sealed class ShuttleComponent : SharedShuttleComponent
+ public sealed class ShuttleComponent : Component
{
+ [ViewVariables]
+ public bool Enabled = true;
+
+ [ViewVariables]
+ public ShuttleMode Mode = ShuttleMode.Cruise;
+
///
/// The cached thrust available for each cardinal direction
///
diff --git a/Content.Server/Shuttles/Components/ShuttleConsoleComponent.cs b/Content.Server/Shuttles/Components/ShuttleConsoleComponent.cs
index e724835253d7..1ad07afa4c52 100644
--- a/Content.Server/Shuttles/Components/ShuttleConsoleComponent.cs
+++ b/Content.Server/Shuttles/Components/ShuttleConsoleComponent.cs
@@ -3,17 +3,10 @@
namespace Content.Server.Shuttles.Components
{
[RegisterComponent]
- [ComponentReference(typeof(SharedShuttleConsoleComponent))]
- internal sealed class ShuttleConsoleComponent : SharedShuttleConsoleComponent
+ public sealed class ShuttleConsoleComponent : SharedShuttleConsoleComponent
{
[ViewVariables]
- public List SubscribedPilots = new();
-
- ///
- /// Whether the console can be used to pilot. Toggled whenever it gets powered / unpowered.
- ///
- [ViewVariables]
- public bool Enabled { get; set; } = false;
+ public readonly List SubscribedPilots = new();
///
/// How much should the pilot's eye be zoomed by when piloting using this console?
diff --git a/Content.Server/Shuttles/Components/ThrusterComponent.cs b/Content.Server/Shuttles/Components/ThrusterComponent.cs
index 7d972159a990..b184fa841fad 100644
--- a/Content.Server/Shuttles/Components/ThrusterComponent.cs
+++ b/Content.Server/Shuttles/Components/ThrusterComponent.cs
@@ -1,4 +1,4 @@
-using Content.Server.Shuttles.EntitySystems;
+using Content.Server.Shuttles.Systems;
using Content.Shared.Damage;
namespace Content.Server.Shuttles.Components
diff --git a/Content.Server/Shuttles/DockCommand.cs b/Content.Server/Shuttles/DockCommand.cs
index 0f8d15f17844..88e39e3b61bd 100644
--- a/Content.Server/Shuttles/DockCommand.cs
+++ b/Content.Server/Shuttles/DockCommand.cs
@@ -1,6 +1,6 @@
using Content.Server.Administration;
using Content.Server.Shuttles.Components;
-using Content.Server.Shuttles.EntitySystems;
+using Content.Server.Shuttles.Systems;
using Content.Shared.Administration;
using Robust.Shared.Console;
diff --git a/Content.Server/Shuttles/EntitySystems/ShuttleConsoleSystem.cs b/Content.Server/Shuttles/EntitySystems/ShuttleConsoleSystem.cs
deleted file mode 100644
index 3f3b186b0f38..000000000000
--- a/Content.Server/Shuttles/EntitySystems/ShuttleConsoleSystem.cs
+++ /dev/null
@@ -1,250 +0,0 @@
-using Content.Server.Popups;
-using Content.Server.Power.Components;
-using Content.Server.Power.EntitySystems;
-using Content.Server.Shuttles.Components;
-using Content.Shared.ActionBlocker;
-using Content.Shared.Alert;
-using Content.Shared.Interaction;
-using Content.Shared.Popups;
-using Content.Shared.Shuttles;
-using Content.Shared.Shuttles.Components;
-using Content.Shared.Tag;
-using Content.Shared.Verbs;
-using Robust.Shared.Map;
-using Robust.Shared.Player;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Shuttles.EntitySystems
-{
- internal sealed class ShuttleConsoleSystem : SharedShuttleConsoleSystem
- {
- [Dependency] private readonly IMapManager _mapManager = default!;
- [Dependency] private readonly ActionBlockerSystem _blocker = default!;
- [Dependency] private readonly AlertsSystem _alertsSystem = default!;
- [Dependency] private readonly PopupSystem _popup = default!;
- [Dependency] private readonly TagSystem _tags = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(HandleConsoleShutdown);
- SubscribeLocalEvent(HandleConsoleInteract);
- SubscribeLocalEvent(HandlePowerChange);
- SubscribeLocalEvent>(OnConsoleInteract);
-
- SubscribeLocalEvent(HandlePilotMove);
- }
-
- private void OnConsoleInteract(EntityUid uid, ShuttleConsoleComponent component, GetVerbsEvent args)
- {
- if (!args.CanAccess ||
- !args.CanInteract)
- return;
-
- var xform = EntityManager.GetComponent(uid);
-
- // Maybe move mode onto the console instead?
- if (!_mapManager.TryGetGrid(xform.GridEntityId, out var grid) ||
- !EntityManager.TryGetComponent(grid.GridEntityId, out ShuttleComponent? shuttle)) return;
-
- InteractionVerb verb = new()
- {
- Text = Loc.GetString("shuttle-mode-toggle"),
- Act = () => ToggleShuttleMode(args.User, component, shuttle),
- Disabled = !xform.Anchored || !this.IsPowered(uid, EntityManager),
- };
-
- args.Verbs.Add(verb);
- }
-
- private void ToggleShuttleMode(EntityUid user, ShuttleConsoleComponent consoleComponent, ShuttleComponent shuttleComponent, TransformComponent? consoleXform = null)
- {
- // Re-validate
- if (!this.IsPowered(consoleComponent.Owner, EntityManager)) return;
-
- if (!Resolve(consoleComponent.Owner, ref consoleXform)) return;
-
- if (!consoleXform.Anchored || consoleXform.GridEntityId != EntityManager.GetComponent(shuttleComponent.Owner).GridEntityId) return;
-
- switch (shuttleComponent.Mode)
- {
- case ShuttleMode.Cruise:
- shuttleComponent.Mode = ShuttleMode.Docking;
- _popup.PopupEntity(Loc.GetString("shuttle-mode-docking"), consoleComponent.Owner, Filter.Entities(user));
- break;
- case ShuttleMode.Docking:
- shuttleComponent.Mode = ShuttleMode.Cruise;
- _popup.PopupEntity(Loc.GetString("shuttle-mode-cruise"), consoleComponent.Owner, Filter.Entities(user));
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- var toRemove = new RemQueue();
-
- foreach (var comp in EntityManager.EntityQuery())
- {
- if (comp.Console == null) continue;
-
- if (!_blocker.CanInteract(comp.Owner, comp.Console.Owner))
- {
- toRemove.Add(comp);
- }
- }
-
- foreach (var comp in toRemove)
- {
- RemovePilot(comp);
- }
- }
-
- ///
- /// Console requires power to operate.
- ///
- private void HandlePowerChange(EntityUid uid, ShuttleConsoleComponent component, PowerChangedEvent args)
- {
- if (!args.Powered)
- {
- component.Enabled = false;
-
- ClearPilots(component);
- }
- else
- {
- component.Enabled = true;
- }
- }
-
- ///
- /// If pilot is moved then we'll stop them from piloting.
- ///
- private void HandlePilotMove(EntityUid uid, PilotComponent component, ref MoveEvent args)
- {
- if (component.Console == null || component.Position == null)
- {
- DebugTools.Assert(component.Position == null && component.Console == null);
- EntityManager.RemoveComponent(uid);
- return;
- }
-
- if (args.NewPosition.TryDistance(EntityManager, component.Position.Value, out var distance) &&
- distance < PilotComponent.BreakDistance) return;
-
- RemovePilot(component);
- }
-
- ///
- /// For now pilots just interact with the console and can start piloting with wasd.
- ///
- private void HandleConsoleInteract(EntityUid uid, ShuttleConsoleComponent component, ActivateInWorldEvent args)
- {
- if (!_tags.HasTag(args.User, "CanPilot"))
- {
- return;
- }
-
- var pilotComponent = EntityManager.EnsureComponent(args.User);
-
- if (!component.Enabled)
- {
- args.User.PopupMessage($"Console is not powered.");
- return;
- }
-
- args.Handled = true;
- var console = pilotComponent.Console;
-
- if (console != null)
- {
- RemovePilot(pilotComponent);
-
- if (console == component)
- {
- return;
- }
- }
-
- AddPilot(args.User, component);
- }
-
- protected override void HandlePilotShutdown(EntityUid uid, PilotComponent component, ComponentShutdown args)
- {
- base.HandlePilotShutdown(uid, component, args);
- RemovePilot(component);
- }
-
- private void HandleConsoleShutdown(EntityUid uid, ShuttleConsoleComponent component, ComponentShutdown args)
- {
- ClearPilots(component);
- }
-
- public void AddPilot(EntityUid entity, ShuttleConsoleComponent component)
- {
- if (!EntityManager.TryGetComponent(entity, out PilotComponent? pilotComponent) ||
- component.SubscribedPilots.Contains(pilotComponent))
- {
- return;
- }
-
- if (TryComp(entity, out var eye))
- {
- eye.Zoom = component.Zoom;
- }
-
- component.SubscribedPilots.Add(pilotComponent);
-
- _alertsSystem.ShowAlert(entity, AlertType.PilotingShuttle);
-
- entity.PopupMessage(Loc.GetString("shuttle-pilot-start"));
- pilotComponent.Console = component;
- ActionBlockerSystem.UpdateCanMove(entity);
- pilotComponent.Position = EntityManager.GetComponent(entity).Coordinates;
- pilotComponent.Dirty();
- }
-
- public void RemovePilot(PilotComponent pilotComponent)
- {
- var console = pilotComponent.Console;
-
- if (console is not ShuttleConsoleComponent helmsman) return;
-
- pilotComponent.Console = null;
- pilotComponent.Position = null;
-
- if (TryComp(pilotComponent.Owner, out var eye))
- {
- eye.Zoom = new(1.0f, 1.0f);
- }
-
- if (!helmsman.SubscribedPilots.Remove(pilotComponent)) return;
-
- _alertsSystem.ClearAlert(pilotComponent.Owner, AlertType.PilotingShuttle);
-
- pilotComponent.Owner.PopupMessage(Loc.GetString("shuttle-pilot-end"));
-
- if (pilotComponent.LifeStage < ComponentLifeStage.Stopping)
- EntityManager.RemoveComponent(pilotComponent.Owner);
- }
-
- public void RemovePilot(EntityUid entity)
- {
- if (!EntityManager.TryGetComponent(entity, out PilotComponent? pilotComponent)) return;
-
- RemovePilot(pilotComponent);
- }
-
- public void ClearPilots(ShuttleConsoleComponent component)
- {
- while (component.SubscribedPilots.TryGetValue(0, out var pilot))
- {
- RemovePilot(pilot);
- }
- }
- }
-}
diff --git a/Content.Server/Shuttles/Events/DockEvent.cs b/Content.Server/Shuttles/Events/DockEvent.cs
new file mode 100644
index 000000000000..3b9ed46741b1
--- /dev/null
+++ b/Content.Server/Shuttles/Events/DockEvent.cs
@@ -0,0 +1,15 @@
+using Content.Server.Shuttles.Components;
+
+namespace Content.Server.Shuttles.Events;
+
+///
+/// Raised whenever 2 airlocks dock.
+///
+public sealed class DockEvent : EntityEventArgs
+{
+ public DockingComponent DockA = default!;
+ public DockingComponent DockB = default!;
+
+ public EntityUid GridAUid = default!;
+ public EntityUid GridBUid = default!;
+}
\ No newline at end of file
diff --git a/Content.Server/Shuttles/Events/UndockEvent.cs b/Content.Server/Shuttles/Events/UndockEvent.cs
new file mode 100644
index 000000000000..41df647aab76
--- /dev/null
+++ b/Content.Server/Shuttles/Events/UndockEvent.cs
@@ -0,0 +1,15 @@
+using Content.Server.Shuttles.Components;
+
+namespace Content.Server.Shuttles.Events;
+
+///
+/// Raised whenever 2 grids undock.
+///
+public sealed class UndockEvent : EntityEventArgs
+{
+ public DockingComponent DockA = default!;
+ public DockingComponent DockB = default!;
+
+ public EntityUid GridAUid = default!;
+ public EntityUid GridBUid = default!;
+}
\ No newline at end of file
diff --git a/Content.Server/Shuttles/Systems/DockingSystem.AutoDock.cs b/Content.Server/Shuttles/Systems/DockingSystem.AutoDock.cs
new file mode 100644
index 000000000000..a87f721c315d
--- /dev/null
+++ b/Content.Server/Shuttles/Systems/DockingSystem.AutoDock.cs
@@ -0,0 +1,104 @@
+using Content.Server.Shuttles.Components;
+using Content.Shared.Shuttles.Components;
+using Content.Shared.Shuttles.Events;
+using Robust.Shared.Players;
+using Robust.Shared.Utility;
+
+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();
+
+ foreach (var (comp, body) in EntityQuery())
+ {
+ if (comp.Requesters.Count == 0 || !dockingQuery.TryGetComponent(comp.Owner, out var dock))
+ {
+ RemComp(comp.Owner);
+ continue;
+ }
+
+ // Don't re-dock if we're already docked or recently were.
+ if (dock.Docked || recentQuery.HasComponent(comp.Owner)) continue;
+
+ var dockable = GetDockable(body, xformQuery.GetComponent(comp.Owner));
+
+ if (dockable == null) continue;
+
+ TryDock(dock, dockable);
+ }
+
+ // Work out recent docks that have gone past their designated threshold.
+ var checkedRecent = new HashSet();
+
+ foreach (var (comp, xform) in EntityQuery())
+ {
+ if (!checkedRecent.Add(comp.Owner)) continue;
+
+ if (!dockingQuery.TryGetComponent(comp.Owner, out var dock))
+ {
+ RemComp(comp.Owner);
+ continue;
+ }
+
+ if (!xformQuery.TryGetComponent(comp.LastDocked, out var otherXform))
+ {
+ RemComp(comp.Owner);
+ continue;
+ }
+
+ var worldPos = _transformSystem.GetWorldPosition(xform, xformQuery);
+ var otherWorldPos = _transformSystem.GetWorldPosition(otherXform, xformQuery);
+
+ if ((worldPos - otherWorldPos).Length < comp.Radius) continue;
+
+ _sawmill.Debug($"Removed RecentlyDocked from {ToPrettyString(comp.Owner)} and {ToPrettyString(comp.LastDocked)}");
+ RemComp(comp.Owner);
+ RemComp(comp.LastDocked);
+ }
+ }
+
+ private void OnRequestUndock(EntityUid uid, ShuttleConsoleComponent component, UndockRequestMessage args)
+ {
+ _sawmill.Debug($"Received undock request for {ToPrettyString(args.Entity)}");
+
+ // TODO: Validation
+ if (!TryComp(args.Entity, out var dock) ||
+ !dock.Docked) return;
+
+ Undock(dock);
+ }
+
+ private void OnRequestAutodock(EntityUid uid, ShuttleConsoleComponent component, AutodockRequestMessage args)
+ {
+ _sawmill.Debug($"Received autodock request for {ToPrettyString(args.Entity)}");
+ var player = args.Session.AttachedEntity;
+
+ if (player == null || !HasComp(args.Entity)) return;
+
+ // TODO: Validation
+ var comp = EnsureComp(args.Entity);
+ comp.Requesters.Add(player.Value);
+ }
+
+ private void OnRequestStopAutodock(EntityUid uid, ShuttleConsoleComponent component, StopAutodockRequestMessage args)
+ {
+ _sawmill.Debug($"Received stop autodock request for {ToPrettyString(args.Entity)}");
+
+ var player = args.Session.AttachedEntity;
+
+ // TODO: Validation
+ if (player == null || !TryComp(args.Entity, out var comp)) return;
+
+ comp.Requesters.Remove(player.Value);
+
+ if (comp.Requesters.Count == 0)
+ RemComp(args.Entity);
+ }
+}
diff --git a/Content.Server/Shuttles/EntitySystems/DockingSystem.cs b/Content.Server/Shuttles/Systems/DockingSystem.cs
similarity index 82%
rename from Content.Server/Shuttles/EntitySystems/DockingSystem.cs
rename to Content.Server/Shuttles/Systems/DockingSystem.cs
index ef06cf58ca09..a88eeb4254c3 100644
--- a/Content.Server/Shuttles/EntitySystems/DockingSystem.cs
+++ b/Content.Server/Shuttles/Systems/DockingSystem.cs
@@ -1,24 +1,30 @@
using Content.Server.Doors.Systems;
using Content.Server.Power.Components;
using Content.Server.Shuttles.Components;
+using Content.Server.Shuttles.Events;
using Content.Shared.Doors;
using Content.Shared.Doors.Components;
+using Content.Shared.Shuttles.Events;
using Content.Shared.Verbs;
+using Robust.Server.Player;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Utility;
-namespace Content.Server.Shuttles.EntitySystems
+namespace Content.Server.Shuttles.Systems
{
- public sealed class DockingSystem : EntitySystem
+ public sealed partial class DockingSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly FixtureSystem _fixtureSystem = default!;
[Dependency] private readonly SharedJointSystem _jointSystem = default!;
+ [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+ [Dependency] private readonly ShuttleConsoleSystem _console = default!;
[Dependency] private readonly DoorSystem _doorSystem = default!;
+ private ISawmill _sawmill = default!;
private const string DockingFixture = "docking";
private const string DockingJoint = "docking";
private const float DockingRadius = 0.20f;
@@ -26,14 +32,26 @@ public sealed class DockingSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
+ _sawmill = Logger.GetSawmill("docking");
SubscribeLocalEvent(OnStartup);
SubscribeLocalEvent(OnShutdown);
SubscribeLocalEvent(OnPowerChange);
SubscribeLocalEvent(OnAnchorChange);
SubscribeLocalEvent(OnDockingReAnchor);
- SubscribeLocalEvent>(OnVerb);
SubscribeLocalEvent(OnAutoClose);
+
+ // 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(OnRequestUndock);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ UpdateAutodock();
}
private void OnAutoClose(EntityUid uid, DockingComponent component, BeforeDoorAutoCloseEvent args)
@@ -43,56 +61,6 @@ private void OnAutoClose(EntityUid uid, DockingComponent component, BeforeDoorAu
args.Cancel();
}
- private void OnVerb(EntityUid uid, DockingComponent component, GetVerbsEvent args)
- {
- if (!args.CanInteract ||
- !args.CanAccess) return;
-
- InteractionVerb? verb;
-
- // TODO: Have it open the UI and have the UI do this.
- if (!component.Docked &&
- TryComp(uid, out PhysicsComponent? body) &&
- TryComp(uid, out TransformComponent? xform))
- {
- DockingComponent? otherDock = null;
-
- if (component.Enabled)
- otherDock = GetDockable(body, xform);
-
- verb = new InteractionVerb
- {
- Disabled = otherDock == null,
- Text = Loc.GetString("docking-component-dock"),
- Act = () =>
- {
- if (otherDock == null) return;
- TryDock(component, otherDock);
- }
- };
- }
- else if (component.Docked)
- {
- verb = new InteractionVerb
- {
- Disabled = !component.Docked,
- Text = Loc.GetString("docking-component-undock"),
- Act = () =>
- {
- if (component.DockedWith == null || !component.Enabled) return;
-
- Undock(component);
- }
- };
- }
- else
- {
- return;
- }
-
- args.Verbs.Add(verb);
- }
-
private DockingComponent? GetDockable(PhysicsComponent body, TransformComponent dockingXform)
{
// Did you know Saltern is the most dockable station?
@@ -105,12 +73,9 @@ private void OnVerb(EntityUid uid, DockingComponent component, GetVerbsEvent
public void Dock(DockingComponent dockA, DockingComponent dockB)
{
- Logger.DebugS("docking", $"Docking between {dockA.Owner} and {dockB.Owner}");
+ _sawmill.Debug($"Docking between {dockA.Owner} and {dockB.Owner}");
// https://gamedev.stackexchange.com/questions/98772/b2distancejoint-with-frequency-equal-to-0-vs-b2weldjoint
// We could also potentially use a prismatic joint? Depending if we want clamps that can extend or whatever
-
var dockAXform = EntityManager.GetComponent(dockA.Owner);
var dockBXform = EntityManager.GetComponent(dockB.Owner);
@@ -397,10 +364,7 @@ public void Dock(DockingComponent dockA, DockingComponent dockB)
EntityManager.EventBus.RaiseEvent(EventSource.Local, msg);
}
- ///
- /// Attempts to dock 2 ports together and will return early if it's not possible.
- ///
- private void TryDock(DockingComponent dockA, DockingComponent dockB)
+ private bool CanDock(DockingComponent dockA, DockingComponent dockB)
{
if (!TryComp(dockA.Owner, out PhysicsComponent? bodyA) ||
!TryComp(dockB.Owner, out PhysicsComponent? bodyB) ||
@@ -409,7 +373,7 @@ private void TryDock(DockingComponent dockA, DockingComponent dockB)
dockA.DockedWith != null ||
dockB.DockedWith != null)
{
- return;
+ return false;
}
var fixtureA = _fixtureSystem.GetFixtureOrNull(bodyA, DockingFixture);
@@ -417,7 +381,7 @@ private void TryDock(DockingComponent dockA, DockingComponent dockB)
if (fixtureA == null || fixtureB == null)
{
- return;
+ return false;
}
var transformA = bodyA.GetTransform();
@@ -441,7 +405,15 @@ private void TryDock(DockingComponent dockA, DockingComponent dockB)
if (intersect) break;
}
- if (!intersect) return;
+ return intersect;
+ }
+
+ ///
+ /// Attempts to dock 2 ports together and will return early if it's not possible.
+ ///
+ private void TryDock(DockingComponent dockA, DockingComponent dockB)
+ {
+ if (!CanDock(dockA, dockB)) return;
Dock(dockA, dockB);
}
@@ -451,7 +423,7 @@ private void Undock(DockingComponent dock)
if (dock.DockedWith == null)
{
DebugTools.Assert(false);
- Logger.ErrorS("docking", $"Tried to undock {(dock).Owner} but not docked with anything?");
+ _sawmill.Error($"Tried to undock {(dock).Owner} but not docked with anything?");
return;
}
@@ -467,33 +439,12 @@ private void Undock(DockingComponent dock)
_doorSystem.TryClose(doorB.Owner, doorB);
}
- // Could maybe give the shuttle a light push away, or at least if there's no other docks left?
+ var recentlyDocked = EnsureComp(dock.Owner);
+ recentlyDocked.LastDocked = dock.DockedWith.Value;
+ recentlyDocked = EnsureComp(dock.DockedWith.Value);
+ recentlyDocked.LastDocked = dock.DockedWith.Value;
Cleanup(dock);
}
-
- ///
- /// Raised whenever 2 airlocks dock.
- ///
- public sealed class DockEvent : EntityEventArgs
- {
- public DockingComponent DockA = default!;
- public DockingComponent DockB = default!;
-
- public EntityUid GridAUid = default!;
- public EntityUid GridBUid = default!;
- }
-
- ///
- /// Raised whenever 2 grids undock.
- ///
- public sealed class UndockEvent : EntityEventArgs
- {
- public DockingComponent DockA = default!;
- public DockingComponent DockB = default!;
-
- public EntityUid GridAUid = default!;
- public EntityUid GridBUid = default!;
- }
}
}
diff --git a/Content.Server/Shuttles/Systems/RadarConsoleSystem.cs b/Content.Server/Shuttles/Systems/RadarConsoleSystem.cs
new file mode 100644
index 000000000000..3d2e6c002512
--- /dev/null
+++ b/Content.Server/Shuttles/Systems/RadarConsoleSystem.cs
@@ -0,0 +1,29 @@
+using Content.Shared.Shuttles.BUIStates;
+using Content.Shared.Shuttles.Components;
+using Content.Shared.Shuttles.Systems;
+using Robust.Server.GameObjects;
+using Robust.Shared.Map;
+
+namespace Content.Server.Shuttles.Systems;
+
+public sealed class RadarConsoleSystem : SharedRadarConsoleSystem
+{
+ [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnRadarStartup);
+ }
+
+ private void OnRadarStartup(EntityUid uid, RadarConsoleComponent component, ComponentStartup args)
+ {
+ UpdateState(component);
+ }
+
+ protected override void UpdateState(RadarConsoleComponent component)
+ {
+ var radarState = new RadarConsoleBoundInterfaceState(component.MaxRange, component.Owner, new List());
+ _uiSystem.GetUiOrNull(component.Owner, RadarConsoleUiKey.Key)?.SetState(radarState);
+ }
+}
diff --git a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs
new file mode 100644
index 000000000000..2249f357af48
--- /dev/null
+++ b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs
@@ -0,0 +1,337 @@
+using Content.Server.Power.Components;
+using Content.Server.Power.EntitySystems;
+using Content.Server.Shuttles.Components;
+using Content.Server.Shuttles.Events;
+using Content.Server.UserInterface;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Alert;
+using Content.Shared.Popups;
+using Content.Shared.Shuttles.BUIStates;
+using Content.Shared.Shuttles.Components;
+using Content.Shared.Shuttles.Events;
+using Content.Shared.Shuttles.Systems;
+using Content.Shared.Tag;
+using Robust.Server.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Shuttles.Systems
+{
+ public sealed class ShuttleConsoleSystem : SharedShuttleConsoleSystem
+ {
+ [Dependency] private readonly ActionBlockerSystem _blocker = default!;
+ [Dependency] private readonly AlertsSystem _alertsSystem = default!;
+ [Dependency] private readonly TagSystem _tags = default!;
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnConsoleShutdown);
+ SubscribeLocalEvent(OnConsolePowerChange);
+ SubscribeLocalEvent(OnConsoleAnchorChange);
+ SubscribeLocalEvent(OnConsoleUIOpenAttempt);
+ SubscribeLocalEvent(OnModeRequest);
+ SubscribeLocalEvent(OnConsoleUIClose);
+ SubscribeLocalEvent(OnDock);
+ SubscribeLocalEvent(OnUndock);
+
+ SubscribeLocalEvent(HandlePilotMove);
+ SubscribeLocalEvent(OnGetState);
+ }
+
+ private void OnDock(DockEvent ev)
+ {
+ RefreshShuttleConsoles();
+ }
+
+ private void OnUndock(UndockEvent ev)
+ {
+ RefreshShuttleConsoles();
+ }
+
+ ///
+ /// Refreshes all of the data for shuttle consoles.
+ ///
+ public void RefreshShuttleConsoles()
+ {
+ // TODO: Should really call this per shuttle in some instances.
+ var docks = GetAllDocks();
+
+ foreach (var comp in EntityQuery(true))
+ {
+ UpdateState(comp, docks);
+ }
+ }
+
+ ///
+ /// Stop piloting if the window is closed.
+ ///
+ private void OnConsoleUIClose(EntityUid uid, ShuttleConsoleComponent component, BoundUIClosedEvent args)
+ {
+ if ((ShuttleConsoleUiKey) args.UiKey != ShuttleConsoleUiKey.Key ||
+ args.Session.AttachedEntity is not {} user) return;
+
+ // In case they D/C should still clean them up.
+ foreach (var comp in EntityQuery(true))
+ {
+ comp.Requesters.Remove(user);
+ }
+
+ RemovePilot(user);
+ }
+
+ private void OnConsoleUIOpenAttempt(EntityUid uid, ShuttleConsoleComponent component, ActivatableUIOpenAttemptEvent args)
+ {
+ if (!TryPilot(args.User, uid))
+ args.Cancel();
+ }
+
+ private void OnConsoleAnchorChange(EntityUid uid, ShuttleConsoleComponent component, ref AnchorStateChangedEvent args)
+ {
+ UpdateState(component);
+ }
+
+ private void OnConsolePowerChange(EntityUid uid, ShuttleConsoleComponent component, PowerChangedEvent args)
+ {
+ UpdateState(component);
+ }
+
+ private bool TryPilot(EntityUid user, EntityUid uid)
+ {
+ if (!_tags.HasTag(user, "CanPilot") ||
+ !TryComp(uid, out var component) ||
+ !this.IsPowered(uid, EntityManager) ||
+ !Transform(uid).Anchored ||
+ !_blocker.CanInteract(user, uid))
+ {
+ return false;
+ }
+
+ var pilotComponent = EntityManager.EnsureComponent(user);
+ var console = pilotComponent.Console;
+
+ if (console != null)
+ {
+ RemovePilot(pilotComponent);
+
+ if (console == component)
+ {
+ return false;
+ }
+ }
+
+ AddPilot(user, component);
+ return true;
+ }
+
+ private void OnGetState(EntityUid uid, PilotComponent component, ref ComponentGetState args)
+ {
+ args.State = new PilotComponentState(component.Console?.Owner);
+ }
+
+ ///
+ /// Client is requesting a change in the shuttle's driving mode.
+ ///
+ private void OnModeRequest(EntityUid uid, ShuttleConsoleComponent component, ShuttleModeRequestMessage args)
+ {
+ if (args.Session.AttachedEntity is not { } player ||
+ !TryComp(player, out var pilot) ||
+ !TryComp(player, out var xform) ||
+ pilot.Console is not ShuttleConsoleComponent console) return;
+
+ if (!console.SubscribedPilots.Contains(pilot) ||
+ !TryComp(xform.GridEntityId, out var shuttle)) return;
+
+ SetShuttleMode(args.Mode, console, shuttle);
+ }
+
+ ///
+ /// Sets the shuttle's movement mode. Does minimal revalidation.
+ ///
+ private void SetShuttleMode(ShuttleMode mode, ShuttleConsoleComponent consoleComponent,
+ ShuttleComponent shuttleComponent, TransformComponent? consoleXform = null)
+ {
+ // Re-validate
+ if (!this.IsPowered(consoleComponent.Owner, EntityManager) ||
+ !Resolve(consoleComponent.Owner, ref consoleXform) ||
+ !consoleXform.Anchored ||
+ consoleXform.GridID != Transform(shuttleComponent.Owner).GridID)
+ {
+ UpdateState(consoleComponent);
+ return;
+ }
+
+ shuttleComponent.Mode = mode;
+
+ switch (mode)
+ {
+ case ShuttleMode.Strafing:
+ break;
+ case ShuttleMode.Cruise:
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ UpdateState(consoleComponent);
+ }
+
+ ///
+ /// Returns the position and angle of all dockingcomponents.
+ ///
+ private List GetAllDocks()
+ {
+ // TODO: NEED TO MAKE SURE THIS UPDATES ON ANCHORING CHANGES!
+ var result = new List();
+
+ foreach (var (comp, xform) in EntityQuery(true))
+ {
+ if (xform.ParentUid != xform.GridUid) continue;
+
+ var state = new DockingInterfaceState()
+ {
+ Coordinates = xform.Coordinates,
+ Angle = xform.LocalRotation,
+ Entity = comp.Owner,
+ Connected = comp.Docked,
+ };
+ result.Add(state);
+ }
+
+ return result;
+ }
+
+ private void UpdateState(ShuttleConsoleComponent component, List? docks = null)
+ {
+ TryComp(component.Owner, out var radar);
+ var range = radar?.MaxRange ?? 0f;
+
+ TryComp(Transform(component.Owner).GridUid, out var shuttle);
+ var mode = shuttle?.Mode ?? ShuttleMode.Cruise;
+
+ docks ??= GetAllDocks();
+
+ _ui.GetUiOrNull(component.Owner, ShuttleConsoleUiKey.Key)
+ ?.SetState(new ShuttleConsoleBoundInterfaceState(
+ mode,
+ range,
+ component.Owner,
+ docks));
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var toRemove = new RemQueue();
+
+ foreach (var comp in EntityManager.EntityQuery())
+ {
+ if (comp.Console == null) continue;
+
+ if (!_blocker.CanInteract(comp.Owner, comp.Console.Owner))
+ {
+ toRemove.Add(comp);
+ }
+ }
+
+ foreach (var comp in toRemove)
+ {
+ RemovePilot(comp);
+ }
+ }
+
+ ///
+ /// If pilot is moved then we'll stop them from piloting.
+ ///
+ private void HandlePilotMove(EntityUid uid, PilotComponent component, ref MoveEvent args)
+ {
+ if (component.Console == null || component.Position == null)
+ {
+ DebugTools.Assert(component.Position == null && component.Console == null);
+ EntityManager.RemoveComponent(uid);
+ return;
+ }
+
+ if (args.NewPosition.TryDistance(EntityManager, component.Position.Value, out var distance) &&
+ distance < PilotComponent.BreakDistance) return;
+
+ RemovePilot(component);
+ }
+
+ protected override void HandlePilotShutdown(EntityUid uid, PilotComponent component, ComponentShutdown args)
+ {
+ base.HandlePilotShutdown(uid, component, args);
+ RemovePilot(component);
+ }
+
+ private void OnConsoleShutdown(EntityUid uid, ShuttleConsoleComponent component, ComponentShutdown args)
+ {
+ ClearPilots(component);
+ }
+
+ public void AddPilot(EntityUid entity, ShuttleConsoleComponent component)
+ {
+ if (!EntityManager.TryGetComponent(entity, out PilotComponent? pilotComponent) ||
+ component.SubscribedPilots.Contains(pilotComponent))
+ {
+ return;
+ }
+
+ if (TryComp(entity, out var eye))
+ {
+ eye.Zoom = component.Zoom;
+ }
+
+ component.SubscribedPilots.Add(pilotComponent);
+
+ _alertsSystem.ShowAlert(entity, AlertType.PilotingShuttle);
+
+ pilotComponent.Console = component;
+ ActionBlockerSystem.UpdateCanMove(entity);
+ pilotComponent.Position = EntityManager.GetComponent(entity).Coordinates;
+ Dirty(pilotComponent);
+ }
+
+ public void RemovePilot(PilotComponent pilotComponent)
+ {
+ var console = pilotComponent.Console;
+
+ if (console is not ShuttleConsoleComponent helmsman) return;
+
+ pilotComponent.Console = null;
+ pilotComponent.Position = null;
+
+ if (TryComp(pilotComponent.Owner, out var eye))
+ {
+ eye.Zoom = new(1.0f, 1.0f);
+ }
+
+ if (!helmsman.SubscribedPilots.Remove(pilotComponent)) return;
+
+ _alertsSystem.ClearAlert(pilotComponent.Owner, AlertType.PilotingShuttle);
+
+ pilotComponent.Owner.PopupMessage(Loc.GetString("shuttle-pilot-end"));
+
+ if (pilotComponent.LifeStage < ComponentLifeStage.Stopping)
+ EntityManager.RemoveComponent(pilotComponent.Owner);
+ }
+
+ public void RemovePilot(EntityUid entity)
+ {
+ if (!EntityManager.TryGetComponent(entity, out PilotComponent? pilotComponent)) return;
+
+ RemovePilot(pilotComponent);
+ }
+
+ public void ClearPilots(ShuttleConsoleComponent component)
+ {
+ while (component.SubscribedPilots.TryGetValue(0, out var pilot))
+ {
+ RemovePilot(pilot);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Shuttles/EntitySystems/ShuttleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.cs
similarity index 99%
rename from Content.Server/Shuttles/EntitySystems/ShuttleSystem.cs
rename to Content.Server/Shuttles/Systems/ShuttleSystem.cs
index bf525fab8e7e..40f67c818fa3 100644
--- a/Content.Server/Shuttles/EntitySystems/ShuttleSystem.cs
+++ b/Content.Server/Shuttles/Systems/ShuttleSystem.cs
@@ -4,7 +4,7 @@
using Robust.Shared.Configuration;
using Robust.Shared.Physics;
-namespace Content.Server.Shuttles.EntitySystems
+namespace Content.Server.Shuttles.Systems
{
[UsedImplicitly]
public sealed class ShuttleSystem : EntitySystem
diff --git a/Content.Server/Shuttles/EntitySystems/SpaceGarbageSystem.cs b/Content.Server/Shuttles/Systems/SpaceGarbageSystem.cs
similarity index 94%
rename from Content.Server/Shuttles/EntitySystems/SpaceGarbageSystem.cs
rename to Content.Server/Shuttles/Systems/SpaceGarbageSystem.cs
index 59518b6a94f9..dc1b5478df25 100644
--- a/Content.Server/Shuttles/EntitySystems/SpaceGarbageSystem.cs
+++ b/Content.Server/Shuttles/Systems/SpaceGarbageSystem.cs
@@ -2,7 +2,7 @@
using Robust.Shared.Physics;
using Robust.Shared.Physics.Dynamics;
-namespace Content.Server.Shuttles.EntitySystems;
+namespace Content.Server.Shuttles.Systems;
///
/// Deletes anything with that has a cross-grid collision with a static body.
diff --git a/Content.Server/Shuttles/EntitySystems/ThrusterSystem.cs b/Content.Server/Shuttles/Systems/ThrusterSystem.cs
similarity index 99%
rename from Content.Server/Shuttles/EntitySystems/ThrusterSystem.cs
rename to Content.Server/Shuttles/Systems/ThrusterSystem.cs
index fda51ecd39e7..b2cab2c9a00b 100644
--- a/Content.Server/Shuttles/EntitySystems/ThrusterSystem.cs
+++ b/Content.Server/Shuttles/Systems/ThrusterSystem.cs
@@ -9,8 +9,8 @@
using Content.Shared.Interaction;
using Content.Shared.Maps;
using Content.Shared.Physics;
-using Content.Shared.Temperature;
using Content.Shared.Shuttles.Components;
+using Content.Shared.Temperature;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Physics;
@@ -18,7 +18,7 @@
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Utility;
-namespace Content.Server.Shuttles.EntitySystems
+namespace Content.Server.Shuttles.Systems
{
public sealed class ThrusterSystem : EntitySystem
{
diff --git a/Content.Server/Station/Systems/StationSystem.cs b/Content.Server/Station/Systems/StationSystem.cs
index 0bd3fcab6612..8a72b9826cb8 100644
--- a/Content.Server/Station/Systems/StationSystem.cs
+++ b/Content.Server/Station/Systems/StationSystem.cs
@@ -182,24 +182,21 @@ public EntityUid InitializeNewStation(StationConfig? stationConfig, IEnumerable<
if (stationConfig is not null && name is null)
{
- metaData.EntityName = GenerateStationName(stationConfig);
+ name = GenerateStationName(stationConfig);
}
- else if (name is not null)
- {
- metaData.EntityName = name;
- }
- else
+ else if (name is null)
{
_sawmill.Error($"When setting up station {station}, was unable to find a valid name in the config and no name was provided.");
- metaData.EntityName = "unnamed station";
+ name = "unnamed station";
}
+ metaData.EntityName = name;
RaiseLocalEvent(new StationInitializedEvent(station));
_sawmill.Info($"Set up station {metaData.EntityName} ({station}).");
foreach (var grid in gridIds ?? Array.Empty())
{
- AddGridToStation(station, grid, null, data);
+ AddGridToStation(station, grid, null, data, name);
}
return station;
@@ -213,13 +210,16 @@ public EntityUid InitializeNewStation(StationConfig? stationConfig, IEnumerable<
/// Resolve pattern, grid component of mapGrid.
/// Resolve pattern, station data component of station.
/// Thrown when mapGrid or station are not a grid or station, respectively.
- public void AddGridToStation(EntityUid station, EntityUid mapGrid, IMapGridComponent? gridComponent = null, StationDataComponent? stationData = null)
+ public void AddGridToStation(EntityUid station, EntityUid mapGrid, IMapGridComponent? gridComponent = null, StationDataComponent? stationData = null, string? name = null)
{
if (!Resolve(mapGrid, ref gridComponent))
throw new ArgumentException("Tried to initialize a station on a non-grid entity!", nameof(mapGrid));
if (!Resolve(station, ref stationData))
throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
+ if (!string.IsNullOrEmpty(name))
+ MetaData(mapGrid).EntityName = name;
+
var stationMember = AddComp(mapGrid);
stationMember.Station = station;
stationData.Grids.Add(gridComponent.Owner);
diff --git a/Content.Shared/Radar/RadarConsoleBoundInterfaceState.cs b/Content.Shared/Radar/RadarConsoleBoundInterfaceState.cs
deleted file mode 100644
index ba6d858a9fb9..000000000000
--- a/Content.Shared/Radar/RadarConsoleBoundInterfaceState.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Radar;
-
-[Serializable, NetSerializable]
-public sealed class RadarConsoleBoundInterfaceState : BoundUserInterfaceState
-{
- public float Range;
- public RadarObjectData[] Objects;
-
- public RadarConsoleBoundInterfaceState(float range, RadarObjectData[] objects)
- {
- Range = range;
- Objects = objects;
- }
-}
-
-[Serializable, NetSerializable]
-public struct RadarObjectData
-{
- public Color Color;
- public RadarObjectShape Shape;
- public Vector2 Position;
- public float Radius;
-}
-
-public enum RadarObjectShape : byte
-{
- Circle,
- CircleFilled,
-}
-
-[Serializable, NetSerializable]
-public enum RadarConsoleUiKey : byte
-{
- Key
-}
diff --git a/Content.Shared/Shuttles/BUIStates/RadarConsoleBoundInterfaceState.cs b/Content.Shared/Shuttles/BUIStates/RadarConsoleBoundInterfaceState.cs
new file mode 100644
index 000000000000..550c6d28f108
--- /dev/null
+++ b/Content.Shared/Shuttles/BUIStates/RadarConsoleBoundInterfaceState.cs
@@ -0,0 +1,41 @@
+using Robust.Shared.Map;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shuttles.BUIStates;
+
+[Serializable, NetSerializable]
+[Virtual]
+public class RadarConsoleBoundInterfaceState : BoundUserInterfaceState
+{
+ public readonly float MaxRange;
+ public readonly EntityUid? Entity;
+ public readonly List Docks;
+
+ public RadarConsoleBoundInterfaceState(
+ float maxRange,
+ EntityUid? entity,
+ List docks)
+ {
+ MaxRange = maxRange;
+ Entity = entity;
+ Docks = docks;
+ }
+}
+
+///
+/// State of each individual docking port for interface purposes
+///
+[Serializable, NetSerializable]
+public sealed class DockingInterfaceState
+{
+ public EntityCoordinates Coordinates;
+ public Angle Angle;
+ public EntityUid Entity;
+ public bool Connected;
+}
+
+[Serializable, NetSerializable]
+public enum RadarConsoleUiKey : byte
+{
+ Key
+}
diff --git a/Content.Shared/Shuttles/BUIStates/ShuttleConsoleBoundInterfaceState.cs b/Content.Shared/Shuttles/BUIStates/ShuttleConsoleBoundInterfaceState.cs
new file mode 100644
index 000000000000..d210fd5bb646
--- /dev/null
+++ b/Content.Shared/Shuttles/BUIStates/ShuttleConsoleBoundInterfaceState.cs
@@ -0,0 +1,20 @@
+using Content.Shared.Shuttles.Components;
+using Robust.Shared.Map;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shuttles.BUIStates;
+
+[Serializable, NetSerializable]
+public sealed class ShuttleConsoleBoundInterfaceState : RadarConsoleBoundInterfaceState
+{
+ public readonly ShuttleMode Mode;
+
+ public ShuttleConsoleBoundInterfaceState(
+ ShuttleMode mode,
+ float maxRange,
+ EntityUid? entity,
+ List docks) : base(maxRange, entity, docks)
+ {
+ Mode = mode;
+ }
+}
diff --git a/Content.Shared/Shuttles/Components/RadarConsoleComponent.cs b/Content.Shared/Shuttles/Components/RadarConsoleComponent.cs
new file mode 100644
index 000000000000..92d51ab60719
--- /dev/null
+++ b/Content.Shared/Shuttles/Components/RadarConsoleComponent.cs
@@ -0,0 +1,21 @@
+using Content.Shared.Shuttles.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Shuttles.Components;
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedRadarConsoleSystem))]
+public sealed class RadarConsoleComponent : Component
+{
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float RangeVV
+ {
+ get => MaxRange;
+ set => IoCManager
+ .Resolve()
+ .GetEntitySystem()
+ .SetRange(this, value);
+ }
+
+ [ViewVariables, DataField("maxRange")]
+ public float MaxRange = 120f;
+}
diff --git a/Content.Shared/Shuttles/Components/SharedShuttleComponent.cs b/Content.Shared/Shuttles/Components/SharedShuttleComponent.cs
index cce6eb076983..ac2063ef498f 100644
--- a/Content.Shared/Shuttles/Components/SharedShuttleComponent.cs
+++ b/Content.Shared/Shuttles/Components/SharedShuttleComponent.cs
@@ -1,17 +1,8 @@
namespace Content.Shared.Shuttles.Components
{
- public abstract class SharedShuttleComponent : Component
- {
- [ViewVariables]
- public virtual bool Enabled { get; set; } = true;
-
- [ViewVariables]
- public ShuttleMode Mode { get; set; } = ShuttleMode.Cruise;
- }
-
public enum ShuttleMode : byte
{
- Docking,
+ Strafing,
Cruise,
}
}
diff --git a/Content.Shared/Shuttles/Components/SharedShuttleConsoleComponent.cs b/Content.Shared/Shuttles/Components/SharedShuttleConsoleComponent.cs
index f1aee2e2ed1e..88583b87fc85 100644
--- a/Content.Shared/Shuttles/Components/SharedShuttleConsoleComponent.cs
+++ b/Content.Shared/Shuttles/Components/SharedShuttleConsoleComponent.cs
@@ -1,12 +1,20 @@
using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
namespace Content.Shared.Shuttles.Components
{
///
/// Interact with to start piloting a shuttle.
///
- [NetworkedComponent()]
+ [NetworkedComponent]
public abstract class SharedShuttleConsoleComponent : Component
{
+
+ }
+
+ [Serializable, NetSerializable]
+ public enum ShuttleConsoleUiKey : byte
+ {
+ Key,
}
}
diff --git a/Content.Shared/Shuttles/Events/AutodockRequestMessage.cs b/Content.Shared/Shuttles/Events/AutodockRequestMessage.cs
new file mode 100644
index 000000000000..49c504066194
--- /dev/null
+++ b/Content.Shared/Shuttles/Events/AutodockRequestMessage.cs
@@ -0,0 +1,12 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shuttles.Events;
+
+///
+/// Raised on the client when it's viewing a particular docking port to try and dock it automatically.
+///
+[Serializable, NetSerializable]
+public sealed class AutodockRequestMessage : BoundUserInterfaceMessage
+{
+ public EntityUid Entity;
+}
diff --git a/Content.Shared/Shuttles/Events/ShuttleModeRequestMessage.cs b/Content.Shared/Shuttles/Events/ShuttleModeRequestMessage.cs
new file mode 100644
index 000000000000..06ccd1af2f99
--- /dev/null
+++ b/Content.Shared/Shuttles/Events/ShuttleModeRequestMessage.cs
@@ -0,0 +1,13 @@
+using Content.Shared.Shuttles.Components;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shuttles.Events;
+
+///
+/// Raised by the client to request the server change a particular shuttle's mode.
+///
+[Serializable, NetSerializable]
+public sealed class ShuttleModeRequestMessage : BoundUserInterfaceMessage
+{
+ public ShuttleMode Mode;
+}
diff --git a/Content.Shared/Shuttles/Events/StopAutodockRequestMessage.cs b/Content.Shared/Shuttles/Events/StopAutodockRequestMessage.cs
new file mode 100644
index 000000000000..f8773649bca9
--- /dev/null
+++ b/Content.Shared/Shuttles/Events/StopAutodockRequestMessage.cs
@@ -0,0 +1,12 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shuttles.Events;
+
+///
+/// Raised on a client when it is no longer viewing a dock.
+///
+[Serializable, NetSerializable]
+public sealed class StopAutodockRequestMessage : BoundUserInterfaceMessage
+{
+ public EntityUid Entity;
+}
diff --git a/Content.Shared/Shuttles/Events/UndockRequestMessage.cs b/Content.Shared/Shuttles/Events/UndockRequestMessage.cs
new file mode 100644
index 000000000000..9ee78aa4d638
--- /dev/null
+++ b/Content.Shared/Shuttles/Events/UndockRequestMessage.cs
@@ -0,0 +1,12 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shuttles.Events;
+
+///
+/// Raised on the client when it wishes to not have 2 docking ports docked.
+///
+[Serializable, NetSerializable]
+public sealed class UndockRequestMessage : BoundUserInterfaceMessage
+{
+ public EntityUid Entity;
+}
diff --git a/Content.Shared/Shuttles/Systems/SharedRadarConsoleSystem.cs b/Content.Shared/Shuttles/Systems/SharedRadarConsoleSystem.cs
new file mode 100644
index 000000000000..c0754df1e132
--- /dev/null
+++ b/Content.Shared/Shuttles/Systems/SharedRadarConsoleSystem.cs
@@ -0,0 +1,45 @@
+using Content.Shared.Shuttles.Components;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Shuttles.Systems;
+
+public abstract class SharedRadarConsoleSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnGetState);
+ SubscribeLocalEvent(OnHandleState);
+ }
+
+ private void OnHandleState(EntityUid uid, RadarConsoleComponent component, ref ComponentHandleState args)
+ {
+ if (args.Current is not RadarConsoleComponentState state) return;
+ component.MaxRange = state.Range;
+ }
+
+ private void OnGetState(EntityUid uid, RadarConsoleComponent component, ref ComponentGetState args)
+ {
+ args.State = new RadarConsoleComponentState()
+ {
+ Range = component.MaxRange
+ };
+ }
+
+ protected virtual void UpdateState(RadarConsoleComponent component) {}
+
+ public void SetRange(RadarConsoleComponent component, float value)
+ {
+ if (component.MaxRange.Equals(value)) return;
+ component.MaxRange = value;
+ Dirty(component);
+ UpdateState(component);
+ }
+
+ [Serializable, NetSerializable]
+ protected sealed class RadarConsoleComponentState : ComponentState
+ {
+ public float Range;
+ }
+}
diff --git a/Content.Shared/Shuttles/SharedShuttleConsoleSystem.cs b/Content.Shared/Shuttles/Systems/SharedShuttleConsoleSystem.cs
similarity index 55%
rename from Content.Shared/Shuttles/SharedShuttleConsoleSystem.cs
rename to Content.Shared/Shuttles/Systems/SharedShuttleConsoleSystem.cs
index 0e0bd043a11f..506fa84f10d7 100644
--- a/Content.Shared/Shuttles/SharedShuttleConsoleSystem.cs
+++ b/Content.Shared/Shuttles/Systems/SharedShuttleConsoleSystem.cs
@@ -1,10 +1,9 @@
using Content.Shared.ActionBlocker;
using Content.Shared.Movement;
using Content.Shared.Shuttles.Components;
-using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
-namespace Content.Shared.Shuttles
+namespace Content.Shared.Shuttles.Systems
{
public abstract class SharedShuttleConsoleSystem : EntitySystem
{
@@ -16,38 +15,10 @@ public override void Initialize()
SubscribeLocalEvent(HandleMovementBlock);
SubscribeLocalEvent(OnStartup);
SubscribeLocalEvent(HandlePilotShutdown);
- SubscribeLocalEvent(OnGetState);
- SubscribeLocalEvent(OnHandleState);
- }
-
- private void OnGetState(EntityUid uid, PilotComponent component, ref ComponentGetState args)
- {
- args.State = new PilotComponentState(component.Console?.Owner);
- }
-
- private void OnHandleState(EntityUid uid, PilotComponent component, ref ComponentHandleState args)
- {
- if (args.Current is not PilotComponentState state) return;
-
- var console = state.Console.GetValueOrDefault();
- if (!console.IsValid())
- {
- component.Console = null;
- return;
- }
-
- if (!TryComp(console, out var shuttleConsoleComponent))
- {
- Logger.Warning($"Unable to set Helmsman console to {console}");
- return;
- }
-
- component.Console = shuttleConsoleComponent;
- ActionBlockerSystem.UpdateCanMove(uid);
}
[Serializable, NetSerializable]
- private sealed class PilotComponentState : ComponentState
+ protected sealed class PilotComponentState : ComponentState
{
public EntityUid? Console { get; }
diff --git a/Resources/Locale/en-US/misc-computers.ftl b/Resources/Locale/en-US/misc-computers.ftl
index 31d7cdc4184e..f2bd3ab50c8f 100644
--- a/Resources/Locale/en-US/misc-computers.ftl
+++ b/Resources/Locale/en-US/misc-computers.ftl
@@ -1 +1,2 @@
-radar-window-title = Mass Scanner Console
+radar-console-window-title = Mass Scanner Console
+shuttle-console-window-title = Shuttle Console
diff --git a/Resources/Locale/en-US/shuttles/console.ftl b/Resources/Locale/en-US/shuttles/console.ftl
index 32254244f88b..bd77157087a7 100644
--- a/Resources/Locale/en-US/shuttles/console.ftl
+++ b/Resources/Locale/en-US/shuttles/console.ftl
@@ -1,5 +1,22 @@
shuttle-pilot-start = Piloting ship
shuttle-pilot-end = Stopped piloting
-shuttle-mode-cruise = Cruise mode
-shuttle-mode-docking = Docking mode
-shuttle-mode-toggle = Toggle mode
+
+# Display
+shuttle-console-max-radar = Max radar range:
+shuttle-console-radar = Radar range:
+shuttle-console-position = Position:
+shuttle-console-orientation = Orientation:
+shuttle-console-linear-velocity = Linear velocity:
+shuttle-console-angular-velocity = Angular velocity:
+
+shuttle-console-docked = {$index} (Docked)
+shuttle-console-dock-button = Dock {$suffix}
+
+shuttle-console-unknown = Unknown
+shuttle-console-iff-label = "{$name} ({$distance}m)
+
+# Buttons
+shuttle-console-strafing = Strafing mode
+shuttle-console-iff-toggle = Show IFF
+shuttle-console-dock-toggle = Show docks
+shuttle-console-undock = Undock
diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
index 76c3cacbf4ee..2c8bae918e9d 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
@@ -20,6 +20,14 @@
abstract: true
components:
- type: ShuttleConsole
+ - type: ActivatableUI
+ key: enum.ShuttleConsoleUiKey.Key
+ - type: ActivatableUIRequiresPower
+ - type: UserInterface
+ interfaces:
+ - key: enum.ShuttleConsoleUiKey.Key
+ type: ShuttleConsoleBoundUserInterface
+ - type: RadarConsole
- type: ExtensionCableReceiver
- type: PointLight
radius: 1.5
@@ -53,6 +61,8 @@
- type: ComputerVisualizer
key: syndie_key
screen: syndishuttle
+ - type: RadarConsole
+ maxRange: 512
- type: PointLight
radius: 1.5
energy: 1.6