diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index e316faf141..134bf7c003 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -283,6 +283,7 @@ namespace Content.Client.Entry
"AtmosAlarmable",
"FireAlarm",
"AirAlarm",
+ "RadarConsole",
"Guardian",
"GuardianCreator",
"GuardianHost",
diff --git a/Content.Client/Radar/RadarConsoleWindow.xaml b/Content.Client/Radar/RadarConsoleWindow.xaml
new file mode 100644
index 0000000000..92e25bf7fc
--- /dev/null
+++ b/Content.Client/Radar/RadarConsoleWindow.xaml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/Content.Client/Radar/RadarConsoleWindow.xaml.cs b/Content.Client/Radar/RadarConsoleWindow.xaml.cs
new file mode 100644
index 0000000000..4bc7c47b7a
--- /dev/null
+++ b/Content.Client/Radar/RadarConsoleWindow.xaml.cs
@@ -0,0 +1,113 @@
+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 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 float _radarArea = 256f;
+
+ private float RadarCircleRadius => MathF.Max(0, _radarArea - 8) / 2;
+
+ private RadarConsoleBoundInterfaceState _lastState = new(256f, Array.Empty());
+
+ private float SizeFull => (int) (_radarArea * UIScale);
+
+ public int RadiusCircle => (int) (RadarCircleRadius * UIScale);
+
+ public RadarControl()
+ {
+ MinSize = (SizeFull, SizeFull);
+ }
+
+ public void UpdateState(RadarConsoleBoundInterfaceState ls)
+ {
+ if (!_radarArea.Equals(ls.Range * 2))
+ {
+ _radarArea = ls.Range * 2;
+ MinSize = (SizeFull, SizeFull);
+ }
+
+ _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 = 8;
+
+ handle.DrawCircle((point, point), RadiusCircle + 1, fakeAA);
+ handle.DrawCircle((point, point), RadiusCircle, Color.Black);
+
+ for (var i = 0; i < gridLinesEquatorial; i++)
+ {
+ handle.DrawCircle((point, point), (RadiusCircle / gridLinesEquatorial) * i, gridLines, false);
+ }
+
+ for (var i = 0; i < gridLinesRadial; i++)
+ {
+ Angle angle = (Math.PI / gridLinesRadial) * i;
+ var aExtent = angle.ToVec() * RadiusCircle;
+ 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)
+ {
+ if (obj.Position.Length > RadiusCircle - 24)
+ continue;
+
+ switch (obj.Shape)
+ {
+ case RadarObjectShape.CircleFilled:
+ case RadarObjectShape.Circle:
+ {
+ handle.DrawCircle(obj.Position + point, obj.Radius, obj.Color, obj.Shape == RadarObjectShape.CircleFilled);
+ break;
+ }
+ default:
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
+
+[UsedImplicitly]
+public class RadarConsoleBoundUserInterface : ComputerBoundUserInterface
+{
+ public RadarConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
+}
diff --git a/Content.Server/Radar/RadarConsoleComponent.cs b/Content.Server/Radar/RadarConsoleComponent.cs
new file mode 100644
index 0000000000..2fa6d38d5b
--- /dev/null
+++ b/Content.Server/Radar/RadarConsoleComponent.cs
@@ -0,0 +1,13 @@
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Radar;
+
+[RegisterComponent, ComponentProtoName("RadarConsole")]
+public class RadarConsoleComponent : Component
+{
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("range")]
+ public float Range = 256f;
+}
diff --git a/Content.Server/Radar/RadarConsoleSystem.cs b/Content.Server/Radar/RadarConsoleSystem.cs
new file mode 100644
index 0000000000..5603ef02db
--- /dev/null
+++ b/Content.Server/Radar/RadarConsoleSystem.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using Content.Server.Solar.Components;
+using Content.Server.UserInterface;
+using Content.Shared.Radar;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+
+namespace Content.Server.Radar;
+
+public 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)
+ 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.Shared/Radar/RadarConsoleBoundInterfaceState.cs b/Content.Shared/Radar/RadarConsoleBoundInterfaceState.cs
new file mode 100644
index 0000000000..1ce897a752
--- /dev/null
+++ b/Content.Shared/Radar/RadarConsoleBoundInterfaceState.cs
@@ -0,0 +1,40 @@
+using System;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Maths;
+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/Resources/Locale/en-US/misc-computers.ftl b/Resources/Locale/en-US/misc-computers.ftl
new file mode 100644
index 0000000000..31d7cdc418
--- /dev/null
+++ b/Resources/Locale/en-US/misc-computers.ftl
@@ -0,0 +1 @@
+radar-window-title = Mass Scanner Console
diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
index be773721da..c10f53c7ca 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
@@ -67,6 +67,14 @@
- type: ComputerBoard
prototype: ComputerComms
+- type: entity
+ parent: BaseComputerCircuitboard
+ id: RadarConsoleCircuitboard
+ name: radar console computer board
+ components:
+ - type: ComputerBoard
+ prototype: ComputerRadar
+
- type: entity
parent: BaseComputerCircuitboard
id: SolarControlComputerCircuitboard
diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
index e58a972998..776a82dc76 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
@@ -272,6 +272,32 @@
energy: 1.6
color: "#e6e227"
+- type: entity
+ parent: ComputerBase
+ id: ComputerRadar
+ name: mass scanner computer
+ description: A computer for detecting nearby bodies, displaying them by position and mass.
+ components:
+ - type: Appearance
+ visuals:
+ - type: ComputerVisualizer
+ key: generic_key
+ screen: solar_screen
+ - type: RadarConsole
+ - type: ActivatableUI
+ key: enum.RadarConsoleUiKey.Key
+ - type: ActivatableUIRequiresPower
+ - type: UserInterface
+ interfaces:
+ - key: enum.RadarConsoleUiKey.Key
+ type: RadarConsoleBoundUserInterface
+ - type: Computer
+ board: RadarConsoleCircuitboard
+ - type: PointLight
+ radius: 1.5
+ energy: 1.6
+ color: "#e6e227"
+
- type: entity
id: ComputerSupplyOrdering
parent: ComputerBase