]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Station maps (#13027)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Thu, 13 Apr 2023 06:21:24 +0000 (16:21 +1000)
committerGitHub <noreply@github.com>
Thu, 13 Apr 2023 06:21:24 +0000 (16:21 +1000)
45 files changed:
Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs
Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml
Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs
Content.Client/Pinpointer/NavMapSystem.cs [new file with mode: 0644]
Content.Client/Pinpointer/PinpointerSystem.cs [moved from Content.Client/Pinpointer/ClientPinpointerSystem.cs with 97% similarity]
Content.Client/Pinpointer/UI/NavMapControl.cs [new file with mode: 0644]
Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Pinpointer/UI/StationMapWindow.xaml [new file with mode: 0644]
Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs [new file with mode: 0644]
Content.Client/Shuttles/UI/DockingControl.cs
Content.Client/Shuttles/UI/RadarControl.cs
Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs
Content.Client/Stylesheets/StyleNano.cs
Content.Client/UserInterface/Controls/MapGridControl.cs [new file with mode: 0644]
Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs
Content.Server/Medical/SuitSensors/SuitSensorComponent.cs
Content.Server/Medical/SuitSensors/SuitSensorSystem.cs
Content.Server/Pinpointer/NavMapSystem.cs [new file with mode: 0644]
Content.Server/Pinpointer/PinpointerSystem.cs [moved from Content.Server/Pinpointer/ServerPinpointerSystem.cs with 98% similarity]
Content.Server/Pinpointer/StationMapComponent.cs [new file with mode: 0644]
Content.Server/Pinpointer/StationMapSystem.cs [new file with mode: 0644]
Content.Shared/Medical/CrewMonitoring/CrewMonitoringShared.cs
Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs
Content.Shared/Pinpointer/NavMapComponent.cs [new file with mode: 0644]
Content.Shared/Pinpointer/SharedNavMapSystem.cs [new file with mode: 0644]
Content.Shared/Pinpointer/SharedStationMapSystem.cs [new file with mode: 0644]
Content.Shared/Power/SharedPowerDevice.cs
Resources/Locale/en-US/pinpointer/station_map.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Devices/Circuitboards/misc.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Devices/station_map.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml [moved from Resources/Prototypes/Entities/Objects/Specific/Mining/ore_bag.yml with 100% similarity]
Resources/Prototypes/Entities/Structures/Wallmounts/station_map.yml [new file with mode: 0644]
Resources/Textures/Objects/Devices/tablets.rsi/generic.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/tablets.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Objects/Devices/tablets.rsi/tablet.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/tablets.rsi/tabletsol.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/station_map.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Structures/Machines/station_map.rsi/station_map-panel.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/station_map.rsi/station_map0.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/station_map.rsi/station_map_broken.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/station_map.rsi/station_map_frame0.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/station_map.rsi/station_map_frame1.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/station_map.rsi/station_map_frame2.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/station_map.rsi/station_map_frame3.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/station_map.rsi/unshaded.png [new file with mode: 0644]

index 8b37687a24d248784fcfd4ea0bbbd70f9bfdba15..0943f89247d525e352db913bd7472e736389792d 100644 (file)
@@ -7,15 +7,25 @@ namespace Content.Client.Medical.CrewMonitoring
 {
     public sealed class CrewMonitoringBoundUserInterface : BoundUserInterface
     {
+        private readonly IEntityManager _entManager;
         private CrewMonitoringWindow? _menu;
 
-        public CrewMonitoringBoundUserInterface([NotNull] ClientUserInterfaceComponent owner, [NotNull] Enum uiKey) : base(owner, uiKey)
+        public CrewMonitoringBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
         {
+            _entManager = IoCManager.Resolve<IEntityManager>();
         }
 
         protected override void Open()
         {
-            _menu = new CrewMonitoringWindow();
+            EntityUid? gridUid = null;
+
+            if (_entManager.TryGetComponent<TransformComponent>(Owner.Owner, out var xform))
+            {
+                gridUid = xform.GridUid;
+            }
+
+            _menu = new CrewMonitoringWindow(gridUid);
+
             _menu.OpenCentered();
             _menu.OnClose += Close;
         }
@@ -27,7 +37,15 @@ namespace Content.Client.Medical.CrewMonitoring
             switch (state)
             {
                 case CrewMonitoringState st:
-                    _menu?.ShowSensors(st.Sensors, st.WorldPosition, st.Snap, st.Precision);
+                    _entManager.TryGetComponent<TransformComponent>(Owner.Owner, out var xform);
+                    Vector2 localPosition = Vector2.Zero;
+
+                    if (_entManager.TryGetComponent<TransformComponent>(xform?.GridUid, out var gridXform))
+                    {
+                        localPosition = gridXform.InvWorldMatrix.Transform(xform.WorldPosition);
+                    }
+
+                    _menu?.ShowSensors(st.Sensors, localPosition, st.Snap, st.Precision);
                     break;
             }
         }
index 306361916184f190171baad5e115d39132f6f6bf..99224dbec67571a931dfb8aa115003a8a37d4d17 100644 (file)
@@ -1,16 +1,33 @@
 <controls:FancyWindow xmlns="https://spacestation14.io"
-    xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
-    Title="{Loc 'crew-monitoring-user-interface-title'}"
-    SetSize="775 400">
-    <ScrollContainer HorizontalExpand="True" VerticalExpand="True" Margin="5">
-        <GridContainer Name="SensorsTable" HorizontalExpand="True" VerticalExpand="True" HSeparationOverride="5" VSeparationOverride="20" Columns="4">
-            <!-- Category Headers -->
-            <Label Text="{Loc 'crew-monitoring-user-interface-name'}" StyleClasses="LabelHeading"/>
-            <Label Text="{Loc 'crew-monitoring-user-interface-job'}" StyleClasses="LabelHeading"/>
-            <Label Text="{Loc 'crew-monitoring-user-interface-status'}" StyleClasses="LabelHeading"/>
-            <Label Text="{Loc 'crew-monitoring-user-interface-location'}" StyleClasses="LabelHeading"/>
+               xmlns:ui="clr-namespace:Content.Client.Pinpointer.UI"
+               xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+               Title="{Loc 'crew-monitoring-user-interface-title'}"
+               SetSize="1130 700"
+               MinSize="1130 700">
+    <BoxContainer Orientation="Horizontal">
+        <ScrollContainer HorizontalExpand="True"
+                         VerticalExpand="True"
+                         Margin="0, 0, 8, 0">
+            <GridContainer Name="SensorsTable"
+                           HorizontalExpand="True"
+                           VerticalExpand="True"
+                           HSeparationOverride="5"
+                           VSeparationOverride="20"
+                           Columns="4">
+                <!-- Table header -->
+                <Label Text="{Loc 'crew-monitoring-user-interface-name'}"
+                       StyleClasses="LabelHeading"/>
+                <Label Text="{Loc 'crew-monitoring-user-interface-job'}"
+                       StyleClasses="LabelHeading"/>
+                <Label Text="{Loc 'crew-monitoring-user-interface-status'}"
+                       StyleClasses="LabelHeading"/>
+                <Label Text="{Loc 'crew-monitoring-user-interface-location'}"
+                       StyleClasses="LabelHeading"/>
 
-            <!-- Additional table rows are filled by code -->
-        </GridContainer>
-    </ScrollContainer>
+                <!-- Table rows are filled by code -->
+            </GridContainer>
+        </ScrollContainer>
+        <ui:NavMapControl Name="NavMap"
+                          Margin="5 5"/>
+    </BoxContainer>
 </controls:FancyWindow>
index e52c232100c095ed6b342ae31e94c55e36f470cf..6e781ed92f55b1dd18e256cc77a4e7733b7fca23 100644 (file)
@@ -1,11 +1,11 @@
 using System.Linq;
+using Content.Client.Stylesheets;
 using Content.Client.UserInterface.Controls;
 using Content.Shared.Medical.SuitSensor;
 using Robust.Client.AutoGenerated;
 using Robust.Client.Graphics;
 using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
 using Robust.Client.UserInterface.XAML;
 using Robust.Shared.Map;
 using Robust.Shared.Timing;
@@ -18,19 +18,32 @@ namespace Content.Client.Medical.CrewMonitoring
     {
         private List<Control> _rowsContent = new();
         private List<(DirectionIcon Icon, Vector2 Position)> _directionIcons = new();
+        private readonly IEntityManager _entManager;
         private readonly IEyeManager _eye;
-        private readonly IEntityManager _entityManager;
+        private EntityUid? _stationUid;
 
         public static int IconSize = 16; // XAML has a `VSeparationOverride` of 20 for each row.
 
-        public CrewMonitoringWindow()
+        public CrewMonitoringWindow(EntityUid? mapUid)
         {
             RobustXamlLoader.Load(this);
             _eye = IoCManager.Resolve<IEyeManager>();
-            _entityManager = IoCManager.Resolve<IEntityManager>();
+            _entManager = IoCManager.Resolve<IEntityManager>();
+            _stationUid = mapUid;
+
+            if (_entManager.TryGetComponent<TransformComponent>(mapUid, out var xform))
+            {
+                NavMap.MapUid = xform.GridUid;
+            }
+            else
+            {
+                NavMap.Visible = false;
+                SetSize = new Vector2(775, 400);
+                MinSize = SetSize;
+            }
         }
 
-        public void ShowSensors(List<SuitSensorStatus> stSensors, Vector2 worldPosition, bool snap, float precision)
+        public void ShowSensors(List<SuitSensorStatus> stSensors, Vector2 localPosition, bool snap, float precision)
         {
             ClearAllSensors();
 
@@ -43,13 +56,21 @@ namespace Content.Client.Medical.CrewMonitoring
             {
                 // add users name
                 // format: UserName
-                var nameLabel = new Label()
+                var nameLabel = new PanelContainer()
                 {
-                    Text = sensor.Name,
-                    HorizontalExpand = true
+                    PanelOverride = new StyleBoxFlat()
+                    {
+                        BackgroundColor = StyleNano.ButtonColorDisabled,
+                    },
+                    Children =
+                    {
+                        new Label()
+                        {
+                            Text = sensor.Name,
+                            Margin = new Thickness(5f, 5f),
+                        }
+                    }
                 };
-                SensorsTable.AddChild(nameLabel);
-                _rowsContent.Add(nameLabel);
 
                 // add users job
                 // format: JobName
@@ -58,6 +79,9 @@ namespace Content.Client.Medical.CrewMonitoring
                     Text = sensor.Job,
                     HorizontalExpand = true
                 };
+
+                SensorsTable.AddChild(nameLabel);
+                _rowsContent.Add(nameLabel);
                 SensorsTable.AddChild(jobLabel);
                 _rowsContent.Add(jobLabel);
 
@@ -79,9 +103,36 @@ namespace Content.Client.Medical.CrewMonitoring
 
                 // add users positions
                 // format: (x, y)
-                var box = GetPositionBox(sensor.Coordinates, worldPosition, snap, precision);
+                var box = GetPositionBox(sensor.Coordinates, localPosition, snap, precision);
+
                 SensorsTable.AddChild(box);
                 _rowsContent.Add(box);
+
+                if (sensor.Coordinates != null && NavMap.Visible)
+                {
+                    NavMap.TrackedCoordinates.Add(sensor.Coordinates.Value, (true, Color.FromHex("#B02E26")));
+                    nameLabel.MouseFilter = MouseFilterMode.Stop;
+
+                    // Hide all others upon mouseover.
+                    nameLabel.OnMouseEntered += args =>
+                    {
+                        foreach (var (coord, value) in NavMap.TrackedCoordinates)
+                        {
+                            if (coord == sensor.Coordinates)
+                                continue;
+
+                            NavMap.TrackedCoordinates[coord] = (false, value.Color);
+                        }
+                    };
+
+                    nameLabel.OnMouseExited += args =>
+                    {
+                        foreach (var (coord, value) in NavMap.TrackedCoordinates)
+                        {
+                            NavMap.TrackedCoordinates[coord] = (true, value.Color);
+                        }
+                    };
+                }
             }
         }
 
@@ -89,7 +140,7 @@ namespace Content.Client.Medical.CrewMonitoring
         {
             var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal };
 
-            if (coordinates == null)
+            if (coordinates == null || !_entManager.TryGetComponent<TransformComponent>(_stationUid, out var xform))
             {
                 var dirIcon = new DirectionIcon()
                 {
@@ -101,17 +152,18 @@ namespace Content.Client.Medical.CrewMonitoring
             }
             else
             {
-                // todo: add locations names (kitchen, bridge, etc)
-                var pos = (Vector2i) coordinates.Value.Position;
-                var mapCoords = coordinates.Value.ToMap(_entityManager).Position;
+                var position = coordinates.Value.ToMapPos(_entManager);
+                var local = xform.InvWorldMatrix.Transform(position);
+
+                var displayPos = local.Floored();
                 var dirIcon = new DirectionIcon(snap, precision)
                 {
                     SetSize = (IconSize, IconSize),
                     Margin = new(0, 0, 4, 0)
                 };
                 box.AddChild(dirIcon);
-                box.AddChild(new Label() { Text = pos.ToString() });
-                _directionIcons.Add((dirIcon, mapCoords - sensorPosition));
+                box.AddChild(new Label() { Text = displayPos.ToString() });
+                _directionIcons.Add((dirIcon, local - sensorPosition));
             }
 
             return box;
@@ -134,7 +186,9 @@ namespace Content.Client.Medical.CrewMonitoring
             {
                 SensorsTable.RemoveChild(child);
             }
+
             _rowsContent.Clear();
+            NavMap.TrackedCoordinates.Clear();
         }
     }
 }
diff --git a/Content.Client/Pinpointer/NavMapSystem.cs b/Content.Client/Pinpointer/NavMapSystem.cs
new file mode 100644 (file)
index 0000000..23b7fc8
--- /dev/null
@@ -0,0 +1,96 @@
+using Content.Shared.Pinpointer;
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+using Robust.Shared.GameStates;
+using Robust.Shared.Map;
+
+namespace Content.Client.Pinpointer;
+
+public sealed class NavMapSystem : SharedNavMapSystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<NavMapComponent, ComponentHandleState>(OnHandleState);
+    }
+
+    private void OnHandleState(EntityUid uid, NavMapComponent component, ref ComponentHandleState args)
+    {
+        if (args.Current is not NavMapComponentState state)
+            return;
+
+        component.Chunks.Clear();
+
+        foreach (var (origin, data) in state.TileData)
+        {
+            component.Chunks.Add(origin, new NavMapChunk(origin)
+            {
+                TileData = data,
+            });
+        }
+    }
+}
+
+public sealed class NavMapOverlay : Overlay
+{
+    private readonly IEntityManager _entManager;
+    private readonly IMapManager _mapManager;
+
+    public override OverlaySpace Space => OverlaySpace.WorldSpace;
+
+    public NavMapOverlay(IEntityManager entManager, IMapManager mapManager)
+    {
+        _entManager = entManager;
+        _mapManager = mapManager;
+    }
+
+    protected override void Draw(in OverlayDrawArgs args)
+    {
+        var query = _entManager.GetEntityQuery<NavMapComponent>();
+        var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
+        var scale = Matrix3.CreateScale(new Vector2(1f, 1f));
+
+        foreach (var grid in _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds))
+        {
+            if (!query.TryGetComponent(grid.Owner, out var navMap) || !xformQuery.TryGetComponent(grid.Owner, out var xform))
+                continue;
+
+            // TODO: Faster helper method
+            var (_, _, matrix, invMatrix) = xform.GetWorldPositionRotationMatrixWithInv();
+
+            var localAABB = invMatrix.TransformBox(args.WorldBounds);
+            Matrix3.Multiply(in scale, in matrix, out var matty);
+
+            args.WorldHandle.SetTransform(matty);
+
+            for (var x = Math.Floor(localAABB.Left); x <= Math.Ceiling(localAABB.Right); x += SharedNavMapSystem.ChunkSize * grid.TileSize)
+            {
+                for (var y = Math.Floor(localAABB.Bottom); y <= Math.Ceiling(localAABB.Top); y += SharedNavMapSystem.ChunkSize * grid.TileSize)
+                {
+                    var floored = new Vector2i((int) x, (int) y);
+
+                    var chunkOrigin = SharedMapSystem.GetChunkIndices(floored, SharedNavMapSystem.ChunkSize);
+
+                    if (!navMap.Chunks.TryGetValue(chunkOrigin, out var chunk))
+                        continue;
+
+                    // TODO: Okay maybe I should just use ushorts lmao...
+                    for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
+                    {
+                        var value = (int) Math.Pow(2, i);
+
+                        var mask = chunk.TileData & value;
+
+                        if (mask == 0x0)
+                            continue;
+
+                        var tile = chunk.Origin * SharedNavMapSystem.ChunkSize + SharedNavMapSystem.GetTile(mask);
+                        args.WorldHandle.DrawRect(new Box2(tile * grid.TileSize, (tile + 1) * grid.TileSize), Color.Aqua, false);
+                    }
+                }
+            }
+        }
+
+        args.WorldHandle.SetTransform(Matrix3.Identity);
+    }
+}
similarity index 97%
rename from Content.Client/Pinpointer/ClientPinpointerSystem.cs
rename to Content.Client/Pinpointer/PinpointerSystem.cs
index f4e79b0d34a6ce664848b722a2d9d1568dcf24a6..1266cbe452c69e1dce38a499f417639f77dd4a06 100644 (file)
@@ -5,7 +5,7 @@ using Robust.Shared.GameStates;
 
 namespace Content.Client.Pinpointer
 {
-    public sealed class ClientPinpointerSystem : SharedPinpointerSystem
+    public sealed class PinpointerSystem : SharedPinpointerSystem
     {
         [Dependency] private readonly IEyeManager _eyeManager = default!;
         [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
diff --git a/Content.Client/Pinpointer/UI/NavMapControl.cs b/Content.Client/Pinpointer/UI/NavMapControl.cs
new file mode 100644 (file)
index 0000000..723030b
--- /dev/null
@@ -0,0 +1,333 @@
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Pinpointer;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Input;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Collision.Shapes;
+using Robust.Shared.Physics.Components;
+using Vector2 = Robust.Shared.Maths.Vector2;
+
+namespace Content.Client.Pinpointer.UI;
+
+/// <summary>
+/// Displays the nav map data of the specified grid.
+/// </summary>
+public sealed class NavMapControl : MapGridControl
+{
+    [Dependency] private readonly IEntityManager _entManager = default!;
+
+    public EntityUid? MapUid;
+
+
+    public Dictionary<EntityCoordinates, (bool Visible, Color Color)> TrackedCoordinates = new();
+
+    private Vector2 _offset;
+    private bool _draggin;
+
+    private bool _recentering = false;
+
+    private float _recenterMinimum = 0.05f;
+
+    // TODO: https://github.com/space-wizards/RobustToolbox/issues/3818
+    private readonly Label _zoom = new()
+    {
+        VerticalAlignment = VAlignment.Top,
+        Margin = new Thickness(8f, 8f),
+    };
+
+    private readonly Button _recenter = new()
+    {
+        Text = "Recentre",
+        VerticalAlignment = VAlignment.Top,
+        HorizontalAlignment = HAlignment.Right,
+        Margin = new Thickness(8f, 4f),
+        Disabled = true,
+    };
+
+    public NavMapControl() : base(8f, 128f, 48f)
+    {
+        IoCManager.InjectDependencies(this);
+        RectClipContent = true;
+        HorizontalExpand = true;
+        VerticalExpand = true;
+
+        var topPanel = new PanelContainer()
+        {
+            PanelOverride = new StyleBoxFlat()
+            {
+                BackgroundColor = StyleNano.ButtonColorContext.WithAlpha(1f),
+                BorderColor = StyleNano.PanelDark
+            },
+            VerticalExpand = false,
+            Children =
+            {
+                _zoom,
+                _recenter,
+            }
+        };
+
+        var topContainer = new BoxContainer()
+        {
+            Orientation = BoxContainer.LayoutOrientation.Vertical,
+            Children =
+            {
+                topPanel,
+                new Control()
+                {
+                    Name = "DrawingControl",
+                    VerticalExpand = true,
+                    Margin = new Thickness(5f, 5f)
+                }
+            }
+        };
+
+        AddChild(topContainer);
+        topPanel.Measure(Vector2.Infinity);
+
+        _recenter.OnPressed += args =>
+        {
+            _recentering = true;
+        };
+    }
+
+    protected override void KeyBindDown(GUIBoundKeyEventArgs args)
+    {
+        base.KeyBindDown(args);
+
+        if (args.Function == EngineKeyFunctions.Use)
+        {
+            _draggin = true;
+        }
+    }
+
+    protected override void KeyBindUp(GUIBoundKeyEventArgs args)
+    {
+        base.KeyBindUp(args);
+
+        if (args.Function == EngineKeyFunctions.Use)
+        {
+            _draggin = false;
+        }
+    }
+
+    protected override void MouseMove(GUIMouseMoveEventArgs args)
+    {
+        base.MouseMove(args);
+
+        if (!_draggin)
+            return;
+
+        _recentering = false;
+        _offset -= new Vector2(args.Relative.X, -args.Relative.Y) / MidPoint * WorldRange;
+
+        if (_offset != Vector2.Zero)
+        {
+            _recenter.Disabled = false;
+        }
+        else
+        {
+            _recenter.Disabled = true;
+        }
+    }
+
+    protected override void Draw(DrawingHandleScreen handle)
+    {
+        base.Draw(handle);
+
+        if (_recentering)
+        {
+            var frameTime = Timing.FrameTime;
+            var diff = _offset * (float) frameTime.TotalSeconds;
+
+            if (_offset.LengthSquared < _recenterMinimum)
+            {
+                _offset = Vector2.Zero;
+                _recentering = false;
+                _recenter.Disabled = true;
+            }
+            else
+            {
+                _offset -= diff * 5f;
+            }
+        }
+
+        _zoom.Text = $"Zoom: {(WorldRange / WorldMaxRange * 100f):0.00}%";
+
+        if (!_entManager.TryGetComponent<NavMapComponent>(MapUid, out var navMap) ||
+            !_entManager.TryGetComponent<TransformComponent>(MapUid, out var xform) ||
+            !_entManager.TryGetComponent<MapGridComponent>(MapUid, out var grid))
+        {
+            return;
+        }
+
+        var offset = _offset;
+        var tileColor = new Color(30, 67, 30);
+        var lineColor = new Color(102, 217, 102);
+
+        if (_entManager.TryGetComponent<PhysicsComponent>(MapUid, out var physics))
+        {
+            offset += physics.LocalCenter;
+        }
+
+        // Draw tiles
+        if (_entManager.TryGetComponent<FixturesComponent>(MapUid, out var manager))
+        {
+            Span<Vector2> verts = new Vector2[8];
+
+            foreach (var fixture in manager.Fixtures.Values)
+            {
+                if (fixture.Shape is not PolygonShape poly)
+                    continue;
+
+                for (var i = 0; i < poly.VertexCount; i++)
+                {
+                    var vert = poly.Vertices[i] - offset;
+
+                    verts[i] = Scale(new Vector2(vert.X, -vert.Y));
+                }
+
+                handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts[..poly.VertexCount], tileColor);
+            }
+        }
+
+        // Draw the wall data
+        var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset);
+        var tileSize = new Vector2(grid.TileSize, -grid.TileSize);
+
+        for (var x = Math.Floor(area.Left); x <= Math.Ceiling(area.Right); x += SharedNavMapSystem.ChunkSize * grid.TileSize)
+        {
+            for (var y = Math.Floor(area.Bottom); y <= Math.Ceiling(area.Top); y += SharedNavMapSystem.ChunkSize * grid.TileSize)
+            {
+                var floored = new Vector2i((int) x, (int) y);
+
+                var chunkOrigin = SharedMapSystem.GetChunkIndices(floored, SharedNavMapSystem.ChunkSize);
+
+                if (!navMap.Chunks.TryGetValue(chunkOrigin, out var chunk))
+                    continue;
+
+                // TODO: Okay maybe I should just use ushorts lmao...
+                for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
+                {
+                    var value = (int) Math.Pow(2, i);
+
+                    var mask = chunk.TileData & value;
+
+                    if (mask == 0x0)
+                        continue;
+
+                    // Alright now we'll work out our edges
+                    var relativeTile = SharedNavMapSystem.GetTile(mask);
+                    var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize - offset;
+                    var position = new Vector2(tile.X, -tile.Y);
+                    NavMapChunk? neighborChunk;
+                    bool neighbor;
+
+                    // North edge
+                    if (relativeTile.Y == SharedNavMapSystem.ChunkSize - 1)
+                    {
+                        neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) &&
+                                      (neighborChunk.TileData &
+                                       SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, 0))) != 0x0;
+                    }
+                    else
+                    {
+                        var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, 1));
+                        neighbor = (chunk.TileData & flag) != 0x0;
+                    }
+
+                    if (!neighbor)
+                    {
+                        handle.DrawLine(Scale(position + new Vector2(0f, -grid.TileSize)), Scale(position + tileSize), lineColor);
+                    }
+
+                    // East edge
+                    if (relativeTile.X == SharedNavMapSystem.ChunkSize - 1)
+                    {
+                        neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) &&
+                                   (neighborChunk.TileData &
+                                    SharedNavMapSystem.GetFlag(new Vector2i(0, relativeTile.Y))) != 0x0;
+                    }
+                    else
+                    {
+                        var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(1, 0));
+                        neighbor = (chunk.TileData & flag) != 0x0;
+                    }
+
+                    if (!neighbor)
+                    {
+                        handle.DrawLine(Scale(position + tileSize), Scale(position + new Vector2(grid.TileSize, 0f)), lineColor);
+                    }
+
+                    // South edge
+                    if (relativeTile.Y == 0)
+                    {
+                        neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(0, -1), out neighborChunk) &&
+                                   (neighborChunk.TileData &
+                                    SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, SharedNavMapSystem.ChunkSize - 1))) != 0x0;
+                    }
+                    else
+                    {
+                        var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, -1));
+                        neighbor = (chunk.TileData & flag) != 0x0;
+                    }
+
+                    if (!neighbor)
+                    {
+                        handle.DrawLine(Scale(position + new Vector2(grid.TileSize, 0f)), Scale(position), lineColor);
+                    }
+
+                    // West edge
+                    if (relativeTile.X == 0)
+                    {
+                        neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(-1, 0), out neighborChunk) &&
+                                   (neighborChunk.TileData &
+                                    SharedNavMapSystem.GetFlag(new Vector2i(SharedNavMapSystem.ChunkSize - 1, relativeTile.Y))) != 0x0;
+                    }
+                    else
+                    {
+                        var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(-1, 0));
+                        neighbor = (chunk.TileData & flag) != 0x0;
+                    }
+
+                    if (!neighbor)
+                    {
+                        handle.DrawLine(Scale(position), Scale(position + new Vector2(0f, -grid.TileSize)), lineColor);
+                    }
+
+                    // Draw a diagonal line for interiors.
+                    handle.DrawLine(Scale(position + new Vector2(0f, -grid.TileSize)), Scale(position + new Vector2(grid.TileSize, 0f)), lineColor);
+                }
+            }
+        }
+
+        var curTime = Timing.RealTime;
+        var blinkFrequency = 1f / 1f;
+        var lit = curTime.TotalSeconds % blinkFrequency > blinkFrequency / 2f;
+
+        foreach (var (coord, value) in TrackedCoordinates)
+        {
+            if (lit && value.Visible)
+            {
+                var mapPos = coord.ToMap(_entManager);
+
+                if (mapPos.MapId != MapId.Nullspace)
+                {
+                    var position = xform.InvWorldMatrix.Transform(mapPos.Position) - offset;
+                    position = Scale(new Vector2(position.X, -position.Y));
+
+                    handle.DrawCircle(position, MinimapScale / 2f, value.Color);
+                }
+            }
+        }
+    }
+
+    private Vector2 Scale(Vector2 position)
+    {
+        return position * MinimapScale + MidPoint;
+    }
+}
diff --git a/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs b/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..e4c6a2a
--- /dev/null
@@ -0,0 +1,35 @@
+using Robust.Client.GameObjects;
+using Robust.Client.Player;
+
+namespace Content.Client.Pinpointer.UI;
+
+public sealed class StationMapBoundUserInterface : BoundUserInterface
+{
+    private StationMapWindow? _window;
+
+    public StationMapBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
+    {
+    }
+
+    protected override void Open()
+    {
+        base.Open();
+        _window?.Close();
+        EntityUid? gridUid = null;
+
+        if (IoCManager.Resolve<IEntityManager>().TryGetComponent<TransformComponent>(Owner.Owner, out var xform))
+        {
+            gridUid = xform.GridUid;
+        }
+
+        _window = new StationMapWindow(gridUid, Owner.Owner);
+        _window.OpenCentered();
+        _window.OnClose += Close;
+    }
+
+    protected override void Dispose(bool disposing)
+    {
+        base.Dispose(disposing);
+        _window?.Dispose();
+    }
+}
diff --git a/Content.Client/Pinpointer/UI/StationMapWindow.xaml b/Content.Client/Pinpointer/UI/StationMapWindow.xaml
new file mode 100644 (file)
index 0000000..86d2dc3
--- /dev/null
@@ -0,0 +1,7 @@
+<controls:FancyWindow xmlns="https://spacestation14.io"
+                      xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+                      xmlns:ui="clr-namespace:Content.Client.Pinpointer.UI"
+                      Title="{Loc 'station-map-window-title'}"
+                      Resizable="False">
+    <ui:NavMapControl Name="NavMapScreen"/>
+</controls:FancyWindow>
diff --git a/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs b/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs
new file mode 100644 (file)
index 0000000..205987c
--- /dev/null
@@ -0,0 +1,24 @@
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Map;
+
+namespace Content.Client.Pinpointer.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class StationMapWindow : FancyWindow
+{
+    public StationMapWindow(EntityUid? mapUid, EntityUid? trackedEntity)
+    {
+        RobustXamlLoader.Load(this);
+        NavMapScreen.MapUid = mapUid;
+
+        if (trackedEntity != null)
+            NavMapScreen.TrackedCoordinates.Add(new EntityCoordinates(trackedEntity.Value, Vector2.Zero), (true, Color.Red));
+
+        if (IoCManager.Resolve<IEntityManager>().TryGetComponent<MetaDataComponent>(mapUid, out var metadata))
+        {
+            Title = metadata.EntityName;
+        }
+    }
+}
index a0bfe63740074950ed0f3756b3dc1bd2fa6ce745..44d205a8b7cf5b2c8140b6e3757c0301acbc3cfa 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Client.UserInterface.Controls;
 using Content.Shared.Shuttles.BUIStates;
 using Robust.Client.Graphics;
 using Robust.Client.UserInterface;
@@ -20,11 +21,9 @@ public class DockingControl : Control
     private float _rangeSquared = 0f;
     private const float GridLinesDistance = 32f;
 
-    private int MinimapRadius => (int) Math.Min(Size.X, Size.Y) / 2;
-
-    private Vector2 MidPoint => (Size / 2) * UIScale;
-    private int SizeFull => (int) (MinimapRadius * 2 * UIScale);
-    private int ScaledMinimapRadius => (int) (MinimapRadius * UIScale);
+    private int MidPoint => SizeFull / 2;
+    private int SizeFull => (int) (MapGridControl.UIDisplayRadius * 2 * UIScale);
+    private int ScaledMinimapRadius => (int) (MapGridControl.UIDisplayRadius * UIScale);
     private float MinimapScale => _range != 0 ? ScaledMinimapRadius / _range : 0f;
 
     public EntityUid? ViewedDock;
@@ -52,8 +51,8 @@ public class DockingControl : Control
 
         var fakeAA = new Color(0.08f, 0.08f, 0.08f);
 
-        handle.DrawCircle((MidPoint.X, MidPoint.Y), ScaledMinimapRadius + 1, fakeAA);
-        handle.DrawCircle((MidPoint.X, MidPoint.Y), ScaledMinimapRadius, Color.Black);
+        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;
@@ -61,14 +60,14 @@ public class DockingControl : Control
 
         for (var i = 1; i < gridLinesEquatorial + 1; i++)
         {
-            handle.DrawCircle((MidPoint.X, MidPoint.Y), GridLinesDistance * MinimapScale * i, gridLines, false);
+            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.X, MidPoint.Y) - aExtent, (MidPoint.X, MidPoint.Y) + aExtent, gridLines);
+            handle.DrawLine((MidPoint, MidPoint) - aExtent, (MidPoint, MidPoint) + aExtent, gridLines);
         }
 
         if (Coordinates == null ||
index 7c18c98ffaeccccd528a7615d72cbd1a9a4437e7..bc5f48e3f29422408590d8ab5257d57f150e2a88 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Client.UserInterface.Controls;
 using Content.Shared.Shuttles.BUIStates;
 using Content.Shared.Shuttles.Components;
 using JetBrains.Annotations;
@@ -13,7 +14,6 @@ using Robust.Shared.Map.Components;
 using Robust.Shared.Physics;
 using Robust.Shared.Physics.Collision.Shapes;
 using Robust.Shared.Physics.Components;
-using Robust.Shared.Timing;
 using Robust.Shared.Utility;
 
 namespace Content.Client.Shuttles.UI;
@@ -21,13 +21,11 @@ namespace Content.Client.Shuttles.UI;
 /// <summary>
 /// Displays nearby grids inside of a control.
 /// </summary>
-public sealed class RadarControl : Control
+public sealed class RadarControl : MapGridControl
 {
     [Dependency] private readonly IEntityManager _entManager = default!;
-    [Dependency] private readonly IGameTiming _timing = default!;
     [Dependency] private readonly IMapManager _mapManager = default!;
 
-    private const float ScrollSensitivity = 8f;
     private const float GridLinesDistance = 32f;
 
     /// <summary>
@@ -37,26 +35,6 @@ public sealed class RadarControl : Control
 
     private Angle? _rotation;
 
-    private float _radarMinRange = SharedRadarConsoleSystem.DefaultMinRange;
-    private float _radarMaxRange = SharedRadarConsoleSystem.DefaultMaxRange;
-    public float RadarRange { get; private set; } = SharedRadarConsoleSystem.DefaultMinRange;
-
-    /// <summary>
-    /// We'll lerp between the radarrange and actual range
-    /// </summary>
-    private float _actualRadarRange = SharedRadarConsoleSystem.DefaultMinRange;
-
-    /// <summary>
-    /// Controls the maximum distance that IFF labels will display.
-    /// </summary>
-    public float MaxRadarRange { get; private set; } = 256f * 10f;
-
-    private int MinimapRadius => (int) Math.Min(Size.X, Size.Y) / 2;
-    private Vector2 MidPoint => (Size / 2) * UIScale;
-    private int SizeFull => (int) (MinimapRadius * 2 * UIScale);
-    private int ScaledMinimapRadius => (int) (MinimapRadius * UIScale);
-    private float MinimapScale => RadarRange != 0 ? ScaledMinimapRadius / RadarRange : 0f;
-
     /// <summary>
     /// Shows a label on each radar object.
     /// </summary>
@@ -79,11 +57,9 @@ public sealed class RadarControl : Control
     /// </summary>
     public Action<EntityCoordinates>? OnRadarClick;
 
-    public RadarControl()
+    public RadarControl() : base(64f, 256f, 256f)
     {
-        IoCManager.InjectDependencies(this);
-        MinSize = (SizeFull, SizeFull);
-        RectClipContent = true;
+
     }
 
     public void SetMatrix(EntityCoordinates? coordinates, Angle? angle)
@@ -92,25 +68,6 @@ public sealed class RadarControl : Control
         _rotation = angle;
     }
 
-    public void UpdateState(RadarConsoleBoundInterfaceState ls)
-    {
-        _radarMaxRange = ls.MaxRange;
-
-        if (_radarMaxRange < _radarMinRange)
-            _radarMinRange = _radarMaxRange;
-
-        _actualRadarRange = Math.Clamp(_actualRadarRange, _radarMinRange, _radarMaxRange);
-
-        _docks.Clear();
-
-        foreach (var state in ls.Docks)
-        {
-            var coordinates = state.Coordinates;
-            var grid = _docks.GetOrNew(coordinates.EntityId);
-            grid.Add(state);
-        }
-    }
-
     protected override void KeyBindUp(GUIBoundKeyEventArgs args)
     {
         base.KeyBindUp(args);
@@ -148,32 +105,38 @@ public sealed class RadarControl : Control
         return coords;
     }
 
-    protected override void MouseWheel(GUIMouseWheelEventArgs args)
+    public void UpdateState(RadarConsoleBoundInterfaceState ls)
     {
-        base.MouseWheel(args);
-        AddRadarRange(-args.Delta.Y * 1f / ScrollSensitivity * RadarRange);
-    }
+        WorldMaxRange = ls.MaxRange;
 
-    public void AddRadarRange(float value)
-    {
-        _actualRadarRange = Math.Clamp(_actualRadarRange + value, _radarMinRange, _radarMaxRange);
+        if (WorldMaxRange < WorldRange)
+        {
+            ActualRadarRange = WorldMaxRange;
+        }
+
+        if (WorldMaxRange < WorldMinRange)
+            WorldMinRange = WorldMaxRange;
+
+        ActualRadarRange = Math.Clamp(ActualRadarRange, WorldMinRange, WorldMaxRange);
+
+        _docks.Clear();
+
+        foreach (var state in ls.Docks)
+        {
+            var coordinates = state.Coordinates;
+            var grid = _docks.GetOrNew(coordinates.EntityId);
+            grid.Add(state);
+        }
     }
 
     protected override void Draw(DrawingHandleScreen handle)
     {
-        if (!_actualRadarRange.Equals(RadarRange))
-        {
-            var diff = _actualRadarRange - RadarRange;
-            var lerpRate = 10f;
-
-            RadarRange += (float) Math.Clamp(diff, -lerpRate * MathF.Abs(diff) * _timing.FrameTime.TotalSeconds, lerpRate * MathF.Abs(diff) * _timing.FrameTime.TotalSeconds);
-            OnRadarRangeChanged?.Invoke(RadarRange);
-        }
+        base.Draw(handle);
 
         var fakeAA = new Color(0.08f, 0.08f, 0.08f);
 
-        handle.DrawCircle((MidPoint.X, MidPoint.Y), ScaledMinimapRadius + 1, fakeAA);
-        handle.DrawCircle((MidPoint.X, MidPoint.Y), ScaledMinimapRadius, Color.Black);
+        handle.DrawCircle((MidPoint, MidPoint), ScaledMinimapRadius + 1, fakeAA);
+        handle.DrawCircle((MidPoint, MidPoint), ScaledMinimapRadius, Color.Black);
 
         // No data
         if (_coordinates == null || _rotation == null)
@@ -184,18 +147,18 @@ public sealed class RadarControl : Control
 
         var gridLines = new Color(0.08f, 0.08f, 0.08f);
         var gridLinesRadial = 8;
-        var gridLinesEquatorial = (int) Math.Floor(RadarRange / GridLinesDistance);
+        var gridLinesEquatorial = (int) Math.Floor(WorldRange / GridLinesDistance);
 
         for (var i = 1; i < gridLinesEquatorial + 1; i++)
         {
-            handle.DrawCircle((MidPoint.X, MidPoint.Y), GridLinesDistance * MinimapScale * i, gridLines, false);
+            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.X, MidPoint.Y) - aExtent, (MidPoint.X, MidPoint.Y) + aExtent, gridLines);
+            handle.DrawLine((MidPoint, MidPoint) - aExtent, (MidPoint, MidPoint) + aExtent, gridLines);
         }
 
         var metaQuery = _entManager.GetEntityQuery<MetaDataComponent>();
@@ -368,7 +331,7 @@ public sealed class RadarControl : Control
                 var position = state.Coordinates.Position;
                 var uiPosition = matrix.Transform(position);
 
-                if (uiPosition.Length > RadarRange - DockScale) continue;
+                if (uiPosition.Length > WorldRange - DockScale) continue;
 
                 var color = HighlightedDock == ent ? state.HighlightedColor : state.Color;
 
@@ -446,7 +409,7 @@ public sealed class RadarControl : Control
                 var adjustedStart = matrix.Transform(start);
                 var adjustedEnd = matrix.Transform(end);
 
-                if (adjustedStart.Length > RadarRange || adjustedEnd.Length > RadarRange)
+                if (adjustedStart.Length > ActualRadarRange || adjustedEnd.Length > ActualRadarRange)
                     continue;
 
                 start = ScalePosition(new Vector2(adjustedStart.X, -adjustedStart.Y));
index 634920b41a0e484a7805376bc4ca63f45d20b275..61f861bbc765951192b4b4b244a505896ef9f0c0 100644 (file)
@@ -52,8 +52,8 @@ public sealed partial class ShuttleConsoleWindow : FancyWindow,
         _entManager = IoCManager.Resolve<IEntityManager>();
         _timing = IoCManager.Resolve<IGameTiming>();
 
-        OnRadarRangeChange(RadarScreen.RadarRange);
-        RadarScreen.OnRadarRangeChanged += OnRadarRangeChange;
+        WorldRangeChange(RadarScreen.WorldRange);
+        RadarScreen.WorldRangeChanged += WorldRangeChange;
 
         IFFToggle.OnToggled += OnIFFTogglePressed;
         IFFToggle.Pressed = RadarScreen.ShowIFF;
@@ -64,7 +64,7 @@ public sealed partial class ShuttleConsoleWindow : FancyWindow,
         UndockButton.OnPressed += OnUndockPressed;
     }
 
-    private void OnRadarRangeChange(float value)
+    private void WorldRangeChange(float value)
     {
         RadarRange.Text = $"{value:0}";
     }
index 2c1bbb7e928d042b0acc7824f8ce3339abda0c63..a377b7434045b1086f357ffda1e4d16539357685 100644 (file)
@@ -81,6 +81,8 @@ namespace Content.Client.Stylesheets
         public const string StyleClassPopupMessageLarge = "PopupMessageLarge";
         public const string StyleClassPopupMessageLargeCaution = "PopupMessageLargeCaution";
 
+        public static readonly Color PanelDark = Color.FromHex("#1E1E22");
+
         public static readonly Color NanoGold = Color.FromHex("#A88B5E");
         public static readonly Color GoodGreenFore = Color.FromHex("#31843E");
         public static readonly Color ConcerningOrangeFore = Color.FromHex("#A5762F");
@@ -453,7 +455,7 @@ namespace Content.Client.Stylesheets
             var sliderBackBox = new StyleBoxTexture
             {
                 Texture = sliderFillTex,
-                Modulate = Color.FromHex("#1E1E22")
+                Modulate = PanelDark,
             };
 
             var sliderForeBox = new StyleBoxTexture
diff --git a/Content.Client/UserInterface/Controls/MapGridControl.cs b/Content.Client/UserInterface/Controls/MapGridControl.cs
new file mode 100644 (file)
index 0000000..d444249
--- /dev/null
@@ -0,0 +1,79 @@
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Shared.Timing;
+
+namespace Content.Client.UserInterface.Controls;
+
+/// <summary>
+/// Handles generic grid-drawing data, with zoom and dragging.
+/// </summary>
+public abstract class MapGridControl : Control
+{
+    [Dependency] protected readonly IGameTiming Timing = default!;
+
+    protected const float ScrollSensitivity = 8f;
+
+    /// <summary>
+    /// UI pixel radius.
+    /// </summary>
+    public const int UIDisplayRadius = 320;
+    protected const int MinimapMargin = 4;
+
+    protected float WorldMinRange;
+    protected float WorldMaxRange;
+    public float WorldRange;
+
+    /// <summary>
+    /// We'll lerp between the radarrange and actual range
+    /// </summary>
+    protected float ActualRadarRange;
+
+    /// <summary>
+    /// Controls the maximum distance that will display.
+    /// </summary>
+    public float MaxRadarRange { get; private set; } = 256f * 10f;
+
+    protected int MidPoint => SizeFull / 2;
+    protected int SizeFull => (int) ((UIDisplayRadius + MinimapMargin) * 2 * UIScale);
+    protected int ScaledMinimapRadius => (int) (UIDisplayRadius * UIScale);
+    protected float MinimapScale => WorldRange != 0 ? ScaledMinimapRadius / WorldRange : 0f;
+
+    public event Action<float>? WorldRangeChanged;
+
+    public MapGridControl(float minRange, float maxRange, float range)
+    {
+        IoCManager.InjectDependencies(this);
+        SetSize = (SizeFull, SizeFull);
+        RectClipContent = true;
+        MouseFilter = MouseFilterMode.Stop;
+        ActualRadarRange = WorldRange;
+        WorldMinRange = minRange;
+        WorldMaxRange = maxRange;
+        WorldRange = range;
+        ActualRadarRange = range;
+    }
+
+    protected override void MouseWheel(GUIMouseWheelEventArgs args)
+    {
+        base.MouseWheel(args);
+        AddRadarRange(-args.Delta.Y * 1f / ScrollSensitivity * ActualRadarRange);
+    }
+
+    public void AddRadarRange(float value)
+    {
+        ActualRadarRange = Math.Clamp(ActualRadarRange + value, WorldMinRange, WorldMaxRange);
+    }
+
+    protected override void Draw(DrawingHandleScreen handle)
+    {
+        base.Draw(handle);
+        if (!ActualRadarRange.Equals(WorldRange))
+        {
+            var diff = ActualRadarRange - WorldRange;
+            const float lerpRate = 10f;
+
+            WorldRange += (float) Math.Clamp(diff, -lerpRate * MathF.Abs(diff) * Timing.FrameTime.TotalSeconds, lerpRate * MathF.Abs(diff) * Timing.FrameTime.TotalSeconds);
+            WorldRangeChanged?.Invoke(WorldRange);
+        }
+    }
+}
index 98c9c2d8cf7e2039b3bb36e26d9140c38b24ba75..034f0e7e8adcab347ffef45888985c171f2d07a0 100644 (file)
@@ -60,9 +60,8 @@ namespace Content.Server.Medical.CrewMonitoring
                 return;
 
             // update all sensors info
-            var xform = Transform(uid);
             var allSensors = component.ConnectedSensors.Values.ToList();
-            var uiState = new CrewMonitoringState(allSensors, xform.WorldPosition, component.Snap, component.Precision);
+            var uiState = new CrewMonitoringState(allSensors, component.Snap, component.Precision);
             ui.SetState(uiState);
         }
     }
index 891a5a8a67f7698cda4cff8a67a889f58f97cee8..17d1feffa3e8c89a6e6109c02bed514b6f86168d 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Medical.SuitSensor;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Server.Medical.SuitSensors
 {
@@ -53,9 +54,10 @@ namespace Content.Server.Medical.SuitSensors
         public EntityUid? User = null;
 
         /// <summary>
-        ///     Last time when sensor updated owners status
+        ///     Next time when sensor updated owners status
         /// </summary>
-        public TimeSpan LastUpdate = TimeSpan.Zero;
+        [DataField("nextUpdate", customTypeSerializer:typeof(TimeOffsetSerializer))]
+        public TimeSpan NextUpdate = TimeSpan.Zero;
 
         /// <summary>
         ///     The station this suit sensor belongs to. If it's null the suit didn't spawn on a station and the sensor doesn't work.
index eecb894255368e6301cb0a3e91f0a5432013b7ed..7786b349ba4f647a423218d087c17fab7d10dd39 100644 (file)
@@ -21,23 +21,21 @@ namespace Content.Server.Medical.SuitSensors
 {
     public sealed class SuitSensorSystem : EntitySystem
     {
-        [Dependency] private readonly PopupSystem _popupSystem = default!;
+        [Dependency] private readonly IGameTiming _gameTiming = default!;
         [Dependency] private readonly IRobustRandom _random = default!;
+        [Dependency] private readonly CrewMonitoringServerSystem _monitoringServerSystem = default!;
+        [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
         [Dependency] private readonly IdCardSystem _idCardSystem = default!;
         [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
-        [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
-        [Dependency] private readonly IGameTiming _gameTiming = default!;
-        [Dependency] private readonly CrewMonitoringServerSystem _monitoringServerSystem = default!;
+        [Dependency] private readonly PopupSystem _popupSystem = default!;
+        [Dependency] private readonly SharedTransformSystem _transform = default!;
         [Dependency] private readonly StationSystem _stationSystem = default!;
-        [Dependency] private readonly SharedTransformSystem _xform = default!;
-
-        private const float UpdateRate = 1f;
-        private float _updateDif;
 
         public override void Initialize()
         {
             base.Initialize();
             SubscribeLocalEvent<SuitSensorComponent, MapInitEvent>(OnMapInit);
+            SubscribeLocalEvent<SuitSensorComponent, EntityUnpausedEvent>(OnUnpaused);
             SubscribeLocalEvent<SuitSensorComponent, GotEquippedEvent>(OnEquipped);
             SubscribeLocalEvent<SuitSensorComponent, GotUnequippedEvent>(OnUnequipped);
             SubscribeLocalEvent<SuitSensorComponent, ExaminedEvent>(OnExamine);
@@ -46,30 +44,29 @@ namespace Content.Server.Medical.SuitSensors
             SubscribeLocalEvent<SuitSensorComponent, EntGotRemovedFromContainerMessage>(OnRemove);
         }
 
+        private void OnUnpaused(EntityUid uid, SuitSensorComponent component, ref EntityUnpausedEvent args)
+        {
+            component.NextUpdate += args.PausedTime;
+        }
+
         public override void Update(float frameTime)
         {
             base.Update(frameTime);
 
-            // check update rate
-            _updateDif += frameTime;
-            if (_updateDif < UpdateRate)
-                return;
-
-            _updateDif -= UpdateRate;
-
             var curTime = _gameTiming.CurTime;
-            var sensors = EntityManager.EntityQuery<SuitSensorComponent, DeviceNetworkComponent>();
-            foreach (var (sensor, device) in sensors)
+            var sensors = EntityManager.EntityQueryEnumerator<SuitSensorComponent, DeviceNetworkComponent>();
+
+            while (sensors.MoveNext(out var sensor, out var device))
             {
-                if (!device.TransmitFrequency.HasValue || !sensor.StationId.HasValue)
+                if (device.TransmitFrequency is null || !sensor.StationId.HasValue)
                     continue;
 
                 // check if sensor is ready to update
-                if (curTime - sensor.LastUpdate < sensor.UpdateRate)
+                if (curTime < sensor.NextUpdate)
                     continue;
 
-                // Add a random offset to the next update time that isn't longer than the sensors update rate
-                sensor.LastUpdate = curTime.Add(TimeSpan.FromSeconds(_random.Next(0, sensor.UpdateRate.Seconds)));
+                // TODO: This would cause imprecision at different tick rates.
+                sensor.NextUpdate = curTime + sensor.UpdateRate;
 
                 // get sensor status
                 var status = GetSensorState(sensor.Owner, sensor);
@@ -278,9 +275,6 @@ namespace Content.Server.Medical.SuitSensors
                 totalDamage = damageable.TotalDamage.Int();
 
             // finally, form suit sensor status
-            var xForm = Transform(sensor.User.Value);
-            var xFormQuery = GetEntityQuery<TransformComponent>();
-            var coords = _xform.GetMoverCoordinates(xForm, xFormQuery);
             var status = new SuitSensorStatus(userName, userJob);
             switch (sensor.Mode)
             {
@@ -294,7 +288,26 @@ namespace Content.Server.Medical.SuitSensors
                 case SuitSensorMode.SensorCords:
                     status.IsAlive = isAlive;
                     status.TotalDamage = totalDamage;
-                    status.Coordinates = coords;
+                    EntityCoordinates coordinates;
+                    var xformQuery = GetEntityQuery<TransformComponent>();
+
+                    if (transform.GridUid != null)
+                    {
+                        coordinates = new EntityCoordinates(transform.GridUid.Value,
+                            _transform.GetInvWorldMatrix(xformQuery.GetComponent(transform.GridUid.Value), xformQuery)
+                            .Transform(_transform.GetWorldPosition(transform, xformQuery)));
+                    }
+                    else if (transform.MapUid != null)
+                    {
+                        coordinates = new EntityCoordinates(transform.MapUid.Value,
+                            _transform.GetWorldPosition(transform, xformQuery));
+                    }
+                    else
+                    {
+                        coordinates = EntityCoordinates.Invalid;
+                    }
+
+                    status.Coordinates = coordinates;
                     break;
             }
 
@@ -317,7 +330,7 @@ namespace Content.Server.Medical.SuitSensors
             if (status.TotalDamage != null)
                 payload.Add(SuitSensorConstants.NET_TOTAL_DAMAGE, status.TotalDamage);
             if (status.Coordinates != null)
-                payload.Add(SuitSensorConstants.NET_CORDINATES, status.Coordinates);
+                payload.Add(SuitSensorConstants.NET_COORDINATES, status.Coordinates);
 
 
             return payload;
@@ -341,7 +354,7 @@ namespace Content.Server.Medical.SuitSensors
 
             // try get total damage and cords (optionals)
             payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE, out int? totalDamage);
-            payload.TryGetValue(SuitSensorConstants.NET_CORDINATES, out EntityCoordinates? cords);
+            payload.TryGetValue(SuitSensorConstants.NET_COORDINATES, out EntityCoordinates? cords);
 
             var status = new SuitSensorStatus(name, job)
             {
diff --git a/Content.Server/Pinpointer/NavMapSystem.cs b/Content.Server/Pinpointer/NavMapSystem.cs
new file mode 100644 (file)
index 0000000..81de3a4
--- /dev/null
@@ -0,0 +1,158 @@
+using Content.Shared.Pinpointer;
+using Content.Shared.Tag;
+using Robust.Shared.GameStates;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Components;
+
+namespace Content.Server.Pinpointer;
+
+/// <summary>
+/// Handles data to be used for in-grid map displays.
+/// </summary>
+public sealed class NavMapSystem : SharedNavMapSystem
+{
+    [Dependency] private readonly TagSystem _tags = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<AnchorStateChangedEvent>(OnAnchorChange);
+        SubscribeLocalEvent<ReAnchorEvent>(OnReAnchor);
+        SubscribeLocalEvent<NavMapComponent, ComponentGetState>(OnGetState);
+        SubscribeLocalEvent<NavMapComponent, GridSplitEvent>(OnNavMapSplit);
+    }
+
+    private void OnNavMapSplit(EntityUid uid, NavMapComponent component, ref GridSplitEvent args)
+    {
+        var physicsQuery = GetEntityQuery<PhysicsComponent>();
+        var tagQuery = GetEntityQuery<TagComponent>();
+        var gridQuery = GetEntityQuery<MapGridComponent>();
+
+        foreach (var grid in args.NewGrids)
+        {
+            var newComp = EnsureComp<NavMapComponent>(grid);
+            RefreshGrid(newComp, gridQuery.GetComponent(grid), physicsQuery, tagQuery);
+        }
+
+        RefreshGrid(component, gridQuery.GetComponent(uid), physicsQuery, tagQuery);
+    }
+
+    private void RefreshGrid(NavMapComponent component, MapGridComponent grid, EntityQuery<PhysicsComponent> physicsQuery, EntityQuery<TagComponent> tagQuery)
+    {
+        component.Chunks.Clear();
+
+        var tiles = grid.GetAllTilesEnumerator();
+
+        while (tiles.MoveNext(out var tile))
+        {
+            var chunkOrigin = SharedMapSystem.GetChunkIndices(tile.Value.GridIndices, ChunkSize);
+
+            if (!component.Chunks.TryGetValue(chunkOrigin, out var chunk))
+            {
+                chunk = new NavMapChunk(chunkOrigin);
+            }
+
+            RefreshTile(grid, component, chunk, tile.Value.GridIndices, physicsQuery, tagQuery);
+        }
+    }
+
+    private void OnGetState(EntityUid uid, NavMapComponent component, ref ComponentGetState args)
+    {
+        var data = new Dictionary<Vector2i, int>(component.Chunks.Count);
+        foreach (var (index, chunk) in component.Chunks)
+        {
+            data.Add(index, chunk.TileData);
+        }
+
+        // TODO: Diffs
+        args.State = new NavMapComponentState()
+        {
+            TileData = data,
+        };
+    }
+
+    private void OnReAnchor(ref ReAnchorEvent ev)
+    {
+        if (TryComp<MapGridComponent>(ev.OldGrid, out var oldGrid) &&
+            TryComp<NavMapComponent>(ev.OldGrid, out var navMap))
+        {
+            var chunkOrigin = SharedMapSystem.GetChunkIndices(ev.TilePos, ChunkSize);
+
+            if (navMap.Chunks.TryGetValue(chunkOrigin, out var chunk))
+            {
+                var physicsQuery = GetEntityQuery<PhysicsComponent>();
+                var tagQuery = GetEntityQuery<TagComponent>();
+                RefreshTile(oldGrid, navMap, chunk, ev.TilePos, physicsQuery, tagQuery);
+            }
+        }
+
+        HandleAnchor(ev.Xform);
+    }
+
+    private void OnAnchorChange(ref AnchorStateChangedEvent ev)
+    {
+        HandleAnchor(ev.Transform);
+    }
+
+    private void HandleAnchor(TransformComponent xform)
+    {
+        if (!TryComp<NavMapComponent>(xform.GridUid, out var navMap) ||
+            !TryComp<MapGridComponent>(xform.GridUid, out var grid))
+            return;
+
+        var tile = grid.LocalToTile(xform.Coordinates);
+        var chunkOrigin = SharedMapSystem.GetChunkIndices(tile, ChunkSize);
+        var physicsQuery = GetEntityQuery<PhysicsComponent>();
+        var tagQuery = GetEntityQuery<TagComponent>();
+
+        if (!navMap.Chunks.TryGetValue(chunkOrigin, out var chunk))
+        {
+            chunk = new NavMapChunk(chunkOrigin);
+            navMap.Chunks[chunkOrigin] = chunk;
+        }
+
+        RefreshTile(grid, navMap, chunk, tile, physicsQuery, tagQuery);
+    }
+
+    private void RefreshTile(MapGridComponent grid, NavMapComponent component, NavMapChunk chunk, Vector2i tile,
+        EntityQuery<PhysicsComponent> physicsQuery,
+        EntityQuery<TagComponent> tagQuery)
+    {
+        var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
+
+        var existing = chunk.TileData;
+        var flag = GetFlag(relative);
+
+        chunk.TileData &= ~flag;
+
+        var enumerator = grid.GetAnchoredEntitiesEnumerator(tile);
+        // TODO: Use something to get convex poly.
+
+        while (enumerator.MoveNext(out var ent))
+        {
+            if (!physicsQuery.TryGetComponent(ent, out var body) ||
+                !body.CanCollide ||
+                !body.Hard ||
+                body.BodyType != BodyType.Static ||
+                (!_tags.HasTag(ent.Value, "Wall", tagQuery) &&
+                 !_tags.HasTag(ent.Value, "Window", tagQuery)))
+            {
+                continue;
+            }
+
+            chunk.TileData |= flag;
+            break;
+        }
+
+        if (chunk.TileData == 0)
+        {
+            component.Chunks.Remove(chunk.Origin);
+        }
+
+        if (existing == chunk.TileData)
+            return;
+
+        Dirty(component);
+    }
+}
similarity index 98%
rename from Content.Server/Pinpointer/ServerPinpointerSystem.cs
rename to Content.Server/Pinpointer/PinpointerSystem.cs
index 3ff5425c74c919b7e9b67d77a36484b4851aa025..21b96a98e31ca67c3e2a79700218a505f49b3ce6 100644 (file)
@@ -6,7 +6,7 @@ using Content.Server.Shuttles.Events;
 
 namespace Content.Server.Pinpointer
 {
-    public sealed class ServerPinpointerSystem : SharedPinpointerSystem
+    public sealed class PinpointerSystem : SharedPinpointerSystem
     {
         [Dependency] private readonly SharedTransformSystem _transform = default!;
 
diff --git a/Content.Server/Pinpointer/StationMapComponent.cs b/Content.Server/Pinpointer/StationMapComponent.cs
new file mode 100644 (file)
index 0000000..0e60289
--- /dev/null
@@ -0,0 +1,17 @@
+namespace Content.Server.Pinpointer;
+
+[RegisterComponent]
+public sealed class StationMapComponent : Component
+{
+
+}
+
+/// <summary>
+/// Added to an entity using station map so when its parent changes we reset it.
+/// </summary>
+[RegisterComponent]
+public sealed class StationMapUserComponent : Component
+{
+    [DataField("mapUid")]
+    public EntityUid Map;
+}
diff --git a/Content.Server/Pinpointer/StationMapSystem.cs b/Content.Server/Pinpointer/StationMapSystem.cs
new file mode 100644 (file)
index 0000000..3b5eea0
--- /dev/null
@@ -0,0 +1,43 @@
+using Content.Shared.Interaction.Events;
+using Content.Shared.Pinpointer;
+using Robust.Server.GameObjects;
+
+namespace Content.Server.Pinpointer;
+
+public sealed class StationMapSystem : EntitySystem
+{
+    [Dependency] private readonly UserInterfaceSystem _ui = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<StationMapUserComponent, EntParentChangedMessage>(OnUserParentChanged);
+        SubscribeLocalEvent<StationMapComponent, BoundUIOpenedEvent>(OnStationMapOpened);
+        SubscribeLocalEvent<StationMapComponent, BoundUIClosedEvent>(OnStationMapClosed);
+    }
+
+    private void OnStationMapClosed(EntityUid uid, StationMapComponent component, BoundUIClosedEvent args)
+    {
+        if (!Equals(args.UiKey, StationMapUiKey.Key) || args.Session.AttachedEntity == null)
+            return;
+
+        RemCompDeferred<StationMapUserComponent>(args.Session.AttachedEntity.Value);
+    }
+
+    private void OnUserParentChanged(EntityUid uid, StationMapUserComponent component, ref EntParentChangedMessage args)
+    {
+        if (TryComp<ActorComponent>(uid, out var actor))
+        {
+            _ui.TryClose(component.Map, StationMapUiKey.Key, actor.PlayerSession);
+        }
+    }
+
+    private void OnStationMapOpened(EntityUid uid, StationMapComponent component, BoundUIOpenedEvent args)
+    {
+        if (args.Session.AttachedEntity == null)
+            return;
+
+        var comp = EnsureComp<StationMapUserComponent>(args.Session.AttachedEntity.Value);
+        comp.Map = uid;
+    }
+}
index ddf943e58d62057b53fafb4902b519b63b280100..7e5c00558b015bcb344baac9d48e95596d8d8be5 100644 (file)
@@ -13,14 +13,12 @@ namespace Content.Shared.Medical.CrewMonitoring
     public sealed class CrewMonitoringState : BoundUserInterfaceState
     {
         public List<SuitSensorStatus> Sensors;
-        public readonly Vector2 WorldPosition;
         public readonly bool Snap;
         public readonly float Precision;
 
-        public CrewMonitoringState(List<SuitSensorStatus> sensors, Vector2 worldPosition, bool snap, float precision)
+        public CrewMonitoringState(List<SuitSensorStatus> sensors, bool snap, float precision)
         {
             Sensors = sensors;
-            WorldPosition = worldPosition;
             Snap = snap;
             Precision = precision;
         }
index cdf3ee070c77218d851d4728d556b77ed33db2a8..5f59836182a616e09377522d1a18cfff749a7042 100644 (file)
@@ -50,7 +50,7 @@ namespace Content.Shared.Medical.SuitSensor
         public const string NET_JOB = "job";
         public const string NET_IS_ALIVE = "alive";
         public const string NET_TOTAL_DAMAGE = "vitals";
-        public const string NET_CORDINATES = "cords";
+        public const string NET_COORDINATES = "coords";
 
         ///Used by the CrewMonitoringServerSystem to send the status of all connected suit sensors to each crew monitor
         public const string NET_STATUS_COLLECTION = "suit-status-collection";
diff --git a/Content.Shared/Pinpointer/NavMapComponent.cs b/Content.Shared/Pinpointer/NavMapComponent.cs
new file mode 100644 (file)
index 0000000..67b118c
--- /dev/null
@@ -0,0 +1,29 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Pinpointer;
+
+/// <summary>
+/// Used to store grid poly data to be used for UIs.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed class NavMapComponent : Component
+{
+    [ViewVariables]
+    public readonly Dictionary<Vector2i, NavMapChunk> Chunks = new();
+}
+
+public sealed class NavMapChunk
+{
+    public readonly Vector2i Origin;
+
+    /// <summary>
+    /// Bitmask for tiles, 1 for occupied and 0 for empty.
+    /// </summary>
+    public int TileData;
+
+    public NavMapChunk(Vector2i origin)
+    {
+        Origin = origin;
+    }
+}
diff --git a/Content.Shared/Pinpointer/SharedNavMapSystem.cs b/Content.Shared/Pinpointer/SharedNavMapSystem.cs
new file mode 100644 (file)
index 0000000..3601ae9
--- /dev/null
@@ -0,0 +1,45 @@
+using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Pinpointer;
+
+public abstract class SharedNavMapSystem : EntitySystem
+{
+    public const byte ChunkSize = 4;
+
+    /// <summary>
+    /// Converts the chunk's tile into a bitflag for the slot.
+    /// </summary>
+    public static int GetFlag(Vector2i relativeTile)
+    {
+        return 1 << (relativeTile.X * ChunkSize + relativeTile.Y);
+    }
+
+    /// <summary>
+    /// Converts the chunk's tile into a bitflag for the slot.
+    /// </summary>
+    public static Vector2i GetTile(int flag)
+    {
+        var value = Math.Log2(flag);
+        var x = (int) value / ChunkSize;
+        var y = (int) value % ChunkSize;
+        var result = new Vector2i(x, y);
+
+        DebugTools.Assert(GetFlag(result) == flag);
+
+        return new Vector2i(x, y);
+    }
+
+    [Serializable, NetSerializable]
+    protected sealed class NavMapComponentState : ComponentState
+    {
+        public Dictionary<Vector2i, int> TileData = new();
+    }
+
+    [Serializable, NetSerializable]
+    protected sealed class NavMapDiffComponentState : ComponentState
+    {
+        public Dictionary<Vector2i, int> TileData = new();
+        public List<Vector2i> RemovedChunks = new();
+    }
+}
diff --git a/Content.Shared/Pinpointer/SharedStationMapSystem.cs b/Content.Shared/Pinpointer/SharedStationMapSystem.cs
new file mode 100644 (file)
index 0000000..13ec5dc
--- /dev/null
@@ -0,0 +1,9 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Pinpointer;
+
+[Serializable, NetSerializable]
+public enum StationMapUiKey : byte
+{
+    Key,
+}
index 8a99dc7d91542ad028bfb1c29df4db4aa80bfa9b..4274dde60f901cf53498b9ee4805b8b435870285 100644 (file)
@@ -3,7 +3,7 @@ using Robust.Shared.Serialization;
 namespace Content.Shared.Power
 {
     [Serializable, NetSerializable]
-    public enum PowerDeviceVisuals
+    public enum PowerDeviceVisuals : byte
     {
         VisualState,
         Powered
diff --git a/Resources/Locale/en-US/pinpointer/station_map.ftl b/Resources/Locale/en-US/pinpointer/station_map.ftl
new file mode 100644 (file)
index 0000000..d8172eb
--- /dev/null
@@ -0,0 +1 @@
+station-map-window-title = Station map
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/misc.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/misc.yml
new file mode 100644 (file)
index 0000000..afce7da
--- /dev/null
@@ -0,0 +1,5 @@
+- type: entity
+  parent: BaseElectronics
+  id: StationMapCircuitboard
+  name: station map circuit board
+  description: A printed circuit board for a station map.
diff --git a/Resources/Prototypes/Entities/Objects/Devices/station_map.yml b/Resources/Prototypes/Entities/Objects/Devices/station_map.yml
new file mode 100644 (file)
index 0000000..f88cd89
--- /dev/null
@@ -0,0 +1,33 @@
+- type: entity
+  id: StationMap
+  name: station map
+  description: Displays a readout of the current station.
+  parent: BaseItem
+  suffix: Handheld
+  components:
+    - type: StationMap
+    - type: Sprite
+      netsync: false
+      sprite: Objects/Devices/tablets.rsi
+      layers:
+        - state: tablet
+        - state: generic
+          shader: unshaded
+    - type: ActivatableUI
+      inHandsOnly: true
+      singleUser: true
+      key: enum.StationMapUiKey.Key
+    - type: Damageable
+      damageContainer: Inorganic
+    - type: Destructible
+      thresholds:
+        - trigger:
+            !type:DamageTrigger
+            damage: 100
+          behaviors:
+            - !type:DoActsBehavior
+              acts: [ "Destruction" ]
+    - type: UserInterface
+      interfaces:
+        - key: enum.StationMapUiKey.Key
+          type: StationMapBoundUserInterface
diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/station_map.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/station_map.yml
new file mode 100644 (file)
index 0000000..5fa2f44
--- /dev/null
@@ -0,0 +1,76 @@
+- type: entity
+  id: WallStationMapBroken
+  name: station map
+  description: A virtual map of the surrounding station.
+  suffix: Wall broken
+  placement:
+    mode: SnapgridCenter
+  components:
+    - type: InteractionOutline
+    - type: Clickable
+    - type: Sprite
+      netsync: false
+      sprite: Structures/Machines/station_map.rsi
+      drawdepth: WallMountedItems
+      layers:
+        - state: station_map_broken
+    - type: Damageable
+      damageContainer: Inorganic
+    - type: Destructible
+      thresholds:
+        - trigger:
+            !type:DamageTrigger
+            damage: 100
+          behaviors:
+            - !type:PlaySoundBehavior
+              sound:
+                collection: GlassBreak
+            - !type:DoActsBehavior
+              acts: [ "Destruction" ]
+
+- type: entity
+  id: WallStationMap
+  name: station map
+  parent: WallStationMapBroken
+  suffix: Wall
+  placement:
+    mode: SnapgridCenter
+  components:
+    - type: StationMap
+    - type: Transform
+      anchored: true
+    - type: Sprite
+      layers:
+        - state: station_map0
+        - state: unshaded
+          map: [ "enum.PowerDeviceVisualLayers.Powered" ]
+          shader: unshaded
+    - type: ApcPowerReceiver
+      powerLoad: 200
+      priority: Low
+    - type: WallMount
+      arc: 360
+    - type: ExtensionCableReceiver
+    - type: ActivatableUIRequiresPower
+    - type: ActivatableUI
+      key: enum.StationMapUiKey.Key
+    - type: Destructible
+      thresholds:
+        - trigger:
+            !type:DamageTrigger
+            damage: 100
+          behaviors:
+            - !type:PlaySoundBehavior
+              sound:
+                collection: GlassBreak
+            - !type:SpawnEntitiesBehavior
+              spawn:
+                WallStationMapBroken:
+                  min: 1
+                  max: 1
+            - !type:DoActsBehavior
+              acts: [ "Destruction" ]
+    - type: UserInterface
+      interfaces:
+        - key: enum.StationMapUiKey.Key
+          type: StationMapBoundUserInterface
diff --git a/Resources/Textures/Objects/Devices/tablets.rsi/generic.png b/Resources/Textures/Objects/Devices/tablets.rsi/generic.png
new file mode 100644 (file)
index 0000000..5e1130f
Binary files /dev/null and b/Resources/Textures/Objects/Devices/tablets.rsi/generic.png differ
diff --git a/Resources/Textures/Objects/Devices/tablets.rsi/meta.json b/Resources/Textures/Objects/Devices/tablets.rsi/meta.json
new file mode 100644 (file)
index 0000000..11d1d58
--- /dev/null
@@ -0,0 +1 @@
+{"version":1,"license":"CC-BY-SA-3.0","copyright":"https://github.com/Baystation12/Baystation12/tree/17e84546562b97d775cb26790a08e6d87e7f8077","size":{"x":32,"y":32},"states":[{"name":"tablet"},{"name":"tabletsol"},{"name":"generic","delays":[[0.3,0.3]]}]}
\ No newline at end of file
diff --git a/Resources/Textures/Objects/Devices/tablets.rsi/tablet.png b/Resources/Textures/Objects/Devices/tablets.rsi/tablet.png
new file mode 100644 (file)
index 0000000..417d44f
Binary files /dev/null and b/Resources/Textures/Objects/Devices/tablets.rsi/tablet.png differ
diff --git a/Resources/Textures/Objects/Devices/tablets.rsi/tabletsol.png b/Resources/Textures/Objects/Devices/tablets.rsi/tabletsol.png
new file mode 100644 (file)
index 0000000..f79a25d
Binary files /dev/null and b/Resources/Textures/Objects/Devices/tablets.rsi/tabletsol.png differ
diff --git a/Resources/Textures/Structures/Machines/station_map.rsi/meta.json b/Resources/Textures/Structures/Machines/station_map.rsi/meta.json
new file mode 100644 (file)
index 0000000..493b112
--- /dev/null
@@ -0,0 +1 @@
+{"version":1,"license":"CC-BY-SA-3.0","copyright":"https://github.com/vgstation-coders/vgstation13/tree/c5d2df28ee81ca4c7d57f4a2a3dd548a7995c084","size":{"x":32,"y":32},"states":[{"name":"station_map_frame0"},{"name":"station_map_frame1"},{"name":"station_map_frame2"},{"name":"station_map_frame3"},{"name":"unshaded"},{"name":"station_map_broken"},{"name":"station_map0"},{"name":"station_map-panel"}]}
\ No newline at end of file
diff --git a/Resources/Textures/Structures/Machines/station_map.rsi/station_map-panel.png b/Resources/Textures/Structures/Machines/station_map.rsi/station_map-panel.png
new file mode 100644 (file)
index 0000000..90f2116
Binary files /dev/null and b/Resources/Textures/Structures/Machines/station_map.rsi/station_map-panel.png differ
diff --git a/Resources/Textures/Structures/Machines/station_map.rsi/station_map0.png b/Resources/Textures/Structures/Machines/station_map.rsi/station_map0.png
new file mode 100644 (file)
index 0000000..4a217e2
Binary files /dev/null and b/Resources/Textures/Structures/Machines/station_map.rsi/station_map0.png differ
diff --git a/Resources/Textures/Structures/Machines/station_map.rsi/station_map_broken.png b/Resources/Textures/Structures/Machines/station_map.rsi/station_map_broken.png
new file mode 100644 (file)
index 0000000..8c8af6a
Binary files /dev/null and b/Resources/Textures/Structures/Machines/station_map.rsi/station_map_broken.png differ
diff --git a/Resources/Textures/Structures/Machines/station_map.rsi/station_map_frame0.png b/Resources/Textures/Structures/Machines/station_map.rsi/station_map_frame0.png
new file mode 100644 (file)
index 0000000..aa458c9
Binary files /dev/null and b/Resources/Textures/Structures/Machines/station_map.rsi/station_map_frame0.png differ
diff --git a/Resources/Textures/Structures/Machines/station_map.rsi/station_map_frame1.png b/Resources/Textures/Structures/Machines/station_map.rsi/station_map_frame1.png
new file mode 100644 (file)
index 0000000..aaef501
Binary files /dev/null and b/Resources/Textures/Structures/Machines/station_map.rsi/station_map_frame1.png differ
diff --git a/Resources/Textures/Structures/Machines/station_map.rsi/station_map_frame2.png b/Resources/Textures/Structures/Machines/station_map.rsi/station_map_frame2.png
new file mode 100644 (file)
index 0000000..98c5caf
Binary files /dev/null and b/Resources/Textures/Structures/Machines/station_map.rsi/station_map_frame2.png differ
diff --git a/Resources/Textures/Structures/Machines/station_map.rsi/station_map_frame3.png b/Resources/Textures/Structures/Machines/station_map.rsi/station_map_frame3.png
new file mode 100644 (file)
index 0000000..695418e
Binary files /dev/null and b/Resources/Textures/Structures/Machines/station_map.rsi/station_map_frame3.png differ
diff --git a/Resources/Textures/Structures/Machines/station_map.rsi/unshaded.png b/Resources/Textures/Structures/Machines/station_map.rsi/unshaded.png
new file mode 100644 (file)
index 0000000..51c53b7
Binary files /dev/null and b/Resources/Textures/Structures/Machines/station_map.rsi/unshaded.png differ