From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 13 Apr 2023 06:21:24 +0000 (+1000) Subject: Station maps (#13027) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=be4e69b0c078888d86cfe76835792628efa487ca;p=space-station-14.git Station maps (#13027) --- diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs index 8b37687a24..0943f89247 100644 --- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs +++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs @@ -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(); } protected override void Open() { - _menu = new CrewMonitoringWindow(); + EntityUid? gridUid = null; + + if (_entManager.TryGetComponent(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(Owner.Owner, out var xform); + Vector2 localPosition = Vector2.Zero; + + if (_entManager.TryGetComponent(xform?.GridUid, out var gridXform)) + { + localPosition = gridXform.InvWorldMatrix.Transform(xform.WorldPosition); + } + + _menu?.ShowSensors(st.Sensors, localPosition, st.Snap, st.Precision); break; } } diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml index 3063619161..99224dbec6 100644 --- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml +++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml @@ -1,16 +1,33 @@  - - - - + + + diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs index e52c232100..6e781ed92f 100644 --- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs +++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs @@ -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 _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(); - _entityManager = IoCManager.Resolve(); + _entManager = IoCManager.Resolve(); + _stationUid = mapUid; + + if (_entManager.TryGetComponent(mapUid, out var xform)) + { + NavMap.MapUid = xform.GridUid; + } + else + { + NavMap.Visible = false; + SetSize = new Vector2(775, 400); + MinSize = SetSize; + } } - public void ShowSensors(List stSensors, Vector2 worldPosition, bool snap, float precision) + public void ShowSensors(List 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(_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 index 0000000000..23b7fc8365 --- /dev/null +++ b/Content.Client/Pinpointer/NavMapSystem.cs @@ -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(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(); + var xformQuery = _entManager.GetEntityQuery(); + 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); + } +} diff --git a/Content.Client/Pinpointer/ClientPinpointerSystem.cs b/Content.Client/Pinpointer/PinpointerSystem.cs similarity index 97% rename from Content.Client/Pinpointer/ClientPinpointerSystem.cs rename to Content.Client/Pinpointer/PinpointerSystem.cs index f4e79b0d34..1266cbe452 100644 --- a/Content.Client/Pinpointer/ClientPinpointerSystem.cs +++ b/Content.Client/Pinpointer/PinpointerSystem.cs @@ -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 index 0000000000..723030b768 --- /dev/null +++ b/Content.Client/Pinpointer/UI/NavMapControl.cs @@ -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; + +/// +/// Displays the nav map data of the specified grid. +/// +public sealed class NavMapControl : MapGridControl +{ + [Dependency] private readonly IEntityManager _entManager = default!; + + public EntityUid? MapUid; + + + public Dictionary 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(MapUid, out var navMap) || + !_entManager.TryGetComponent(MapUid, out var xform) || + !_entManager.TryGetComponent(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(MapUid, out var physics)) + { + offset += physics.LocalCenter; + } + + // Draw tiles + if (_entManager.TryGetComponent(MapUid, out var manager)) + { + Span 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 index 0000000000..e4c6a2a1d5 --- /dev/null +++ b/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs @@ -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().TryGetComponent(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 index 0000000000..86d2dc3244 --- /dev/null +++ b/Content.Client/Pinpointer/UI/StationMapWindow.xaml @@ -0,0 +1,7 @@ + + + diff --git a/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs b/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs new file mode 100644 index 0000000000..205987ca35 --- /dev/null +++ b/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs @@ -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().TryGetComponent(mapUid, out var metadata)) + { + Title = metadata.EntityName; + } + } +} diff --git a/Content.Client/Shuttles/UI/DockingControl.cs b/Content.Client/Shuttles/UI/DockingControl.cs index a0bfe63740..44d205a8b7 100644 --- a/Content.Client/Shuttles/UI/DockingControl.cs +++ b/Content.Client/Shuttles/UI/DockingControl.cs @@ -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 || diff --git a/Content.Client/Shuttles/UI/RadarControl.cs b/Content.Client/Shuttles/UI/RadarControl.cs index 7c18c98ffa..bc5f48e3f2 100644 --- a/Content.Client/Shuttles/UI/RadarControl.cs +++ b/Content.Client/Shuttles/UI/RadarControl.cs @@ -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; /// /// Displays nearby grids inside of a control. /// -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; /// @@ -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; - - /// - /// We'll lerp between the radarrange and actual range - /// - private float _actualRadarRange = SharedRadarConsoleSystem.DefaultMinRange; - - /// - /// Controls the maximum distance that IFF labels will display. - /// - 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; - /// /// Shows a label on each radar object. /// @@ -79,11 +57,9 @@ public sealed class RadarControl : Control /// public Action? 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(); @@ -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)); diff --git a/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs b/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs index 634920b41a..61f861bbc7 100644 --- a/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs +++ b/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs @@ -52,8 +52,8 @@ public sealed partial class ShuttleConsoleWindow : FancyWindow, _entManager = IoCManager.Resolve(); _timing = IoCManager.Resolve(); - 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}"; } diff --git a/Content.Client/Stylesheets/StyleNano.cs b/Content.Client/Stylesheets/StyleNano.cs index 2c1bbb7e92..a377b74340 100644 --- a/Content.Client/Stylesheets/StyleNano.cs +++ b/Content.Client/Stylesheets/StyleNano.cs @@ -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 index 0000000000..d44424912e --- /dev/null +++ b/Content.Client/UserInterface/Controls/MapGridControl.cs @@ -0,0 +1,79 @@ +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Shared.Timing; + +namespace Content.Client.UserInterface.Controls; + +/// +/// Handles generic grid-drawing data, with zoom and dragging. +/// +public abstract class MapGridControl : Control +{ + [Dependency] protected readonly IGameTiming Timing = default!; + + protected const float ScrollSensitivity = 8f; + + /// + /// UI pixel radius. + /// + public const int UIDisplayRadius = 320; + protected const int MinimapMargin = 4; + + protected float WorldMinRange; + protected float WorldMaxRange; + public float WorldRange; + + /// + /// We'll lerp between the radarrange and actual range + /// + protected float ActualRadarRange; + + /// + /// Controls the maximum distance that will display. + /// + public float MaxRadarRange { get; private set; } = 256f * 10f; + + protected int MidPoint => SizeFull / 2; + protected int SizeFull => (int) ((UIDisplayRadius + MinimapMargin) * 2 * UIScale); + protected int ScaledMinimapRadius => (int) (UIDisplayRadius * UIScale); + protected float MinimapScale => WorldRange != 0 ? ScaledMinimapRadius / WorldRange : 0f; + + public event Action? WorldRangeChanged; + + public MapGridControl(float minRange, float maxRange, float range) + { + IoCManager.InjectDependencies(this); + SetSize = (SizeFull, SizeFull); + RectClipContent = true; + MouseFilter = MouseFilterMode.Stop; + ActualRadarRange = WorldRange; + WorldMinRange = minRange; + WorldMaxRange = maxRange; + WorldRange = range; + ActualRadarRange = range; + } + + protected override void MouseWheel(GUIMouseWheelEventArgs args) + { + base.MouseWheel(args); + AddRadarRange(-args.Delta.Y * 1f / ScrollSensitivity * ActualRadarRange); + } + + public void AddRadarRange(float value) + { + ActualRadarRange = Math.Clamp(ActualRadarRange + value, WorldMinRange, WorldMaxRange); + } + + protected override void Draw(DrawingHandleScreen handle) + { + base.Draw(handle); + if (!ActualRadarRange.Equals(WorldRange)) + { + var diff = ActualRadarRange - WorldRange; + const float lerpRate = 10f; + + WorldRange += (float) Math.Clamp(diff, -lerpRate * MathF.Abs(diff) * Timing.FrameTime.TotalSeconds, lerpRate * MathF.Abs(diff) * Timing.FrameTime.TotalSeconds); + WorldRangeChanged?.Invoke(WorldRange); + } + } +} diff --git a/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs b/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs index 98c9c2d8cf..034f0e7e8a 100644 --- a/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs +++ b/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs @@ -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); } } diff --git a/Content.Server/Medical/SuitSensors/SuitSensorComponent.cs b/Content.Server/Medical/SuitSensors/SuitSensorComponent.cs index 891a5a8a67..17d1feffa3 100644 --- a/Content.Server/Medical/SuitSensors/SuitSensorComponent.cs +++ b/Content.Server/Medical/SuitSensors/SuitSensorComponent.cs @@ -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; /// - /// Last time when sensor updated owners status + /// Next time when sensor updated owners status /// - public TimeSpan LastUpdate = TimeSpan.Zero; + [DataField("nextUpdate", customTypeSerializer:typeof(TimeOffsetSerializer))] + public TimeSpan NextUpdate = TimeSpan.Zero; /// /// 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. diff --git a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs index eecb894255..7786b349ba 100644 --- a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs +++ b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs @@ -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(OnMapInit); + SubscribeLocalEvent(OnUnpaused); SubscribeLocalEvent(OnEquipped); SubscribeLocalEvent(OnUnequipped); SubscribeLocalEvent(OnExamine); @@ -46,30 +44,29 @@ namespace Content.Server.Medical.SuitSensors SubscribeLocalEvent(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(); - foreach (var (sensor, device) in sensors) + var sensors = EntityManager.EntityQueryEnumerator(); + + 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(); - 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(); + + 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 index 0000000000..81de3a4368 --- /dev/null +++ b/Content.Server/Pinpointer/NavMapSystem.cs @@ -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; + +/// +/// Handles data to be used for in-grid map displays. +/// +public sealed class NavMapSystem : SharedNavMapSystem +{ + [Dependency] private readonly TagSystem _tags = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnAnchorChange); + SubscribeLocalEvent(OnReAnchor); + SubscribeLocalEvent(OnGetState); + SubscribeLocalEvent(OnNavMapSplit); + } + + private void OnNavMapSplit(EntityUid uid, NavMapComponent component, ref GridSplitEvent args) + { + var physicsQuery = GetEntityQuery(); + var tagQuery = GetEntityQuery(); + var gridQuery = GetEntityQuery(); + + foreach (var grid in args.NewGrids) + { + var newComp = EnsureComp(grid); + RefreshGrid(newComp, gridQuery.GetComponent(grid), physicsQuery, tagQuery); + } + + RefreshGrid(component, gridQuery.GetComponent(uid), physicsQuery, tagQuery); + } + + private void RefreshGrid(NavMapComponent component, MapGridComponent grid, EntityQuery physicsQuery, EntityQuery 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(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(ev.OldGrid, out var oldGrid) && + TryComp(ev.OldGrid, out var navMap)) + { + var chunkOrigin = SharedMapSystem.GetChunkIndices(ev.TilePos, ChunkSize); + + if (navMap.Chunks.TryGetValue(chunkOrigin, out var chunk)) + { + var physicsQuery = GetEntityQuery(); + var tagQuery = GetEntityQuery(); + 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(xform.GridUid, out var navMap) || + !TryComp(xform.GridUid, out var grid)) + return; + + var tile = grid.LocalToTile(xform.Coordinates); + var chunkOrigin = SharedMapSystem.GetChunkIndices(tile, ChunkSize); + var physicsQuery = GetEntityQuery(); + var tagQuery = GetEntityQuery(); + + 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 physicsQuery, + EntityQuery 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); + } +} diff --git a/Content.Server/Pinpointer/ServerPinpointerSystem.cs b/Content.Server/Pinpointer/PinpointerSystem.cs similarity index 98% rename from Content.Server/Pinpointer/ServerPinpointerSystem.cs rename to Content.Server/Pinpointer/PinpointerSystem.cs index 3ff5425c74..21b96a98e3 100644 --- a/Content.Server/Pinpointer/ServerPinpointerSystem.cs +++ b/Content.Server/Pinpointer/PinpointerSystem.cs @@ -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 index 0000000000..0e60289876 --- /dev/null +++ b/Content.Server/Pinpointer/StationMapComponent.cs @@ -0,0 +1,17 @@ +namespace Content.Server.Pinpointer; + +[RegisterComponent] +public sealed class StationMapComponent : Component +{ + +} + +/// +/// Added to an entity using station map so when its parent changes we reset it. +/// +[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 index 0000000000..3b5eea07cf --- /dev/null +++ b/Content.Server/Pinpointer/StationMapSystem.cs @@ -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(OnUserParentChanged); + SubscribeLocalEvent(OnStationMapOpened); + SubscribeLocalEvent(OnStationMapClosed); + } + + private void OnStationMapClosed(EntityUid uid, StationMapComponent component, BoundUIClosedEvent args) + { + if (!Equals(args.UiKey, StationMapUiKey.Key) || args.Session.AttachedEntity == null) + return; + + RemCompDeferred(args.Session.AttachedEntity.Value); + } + + private void OnUserParentChanged(EntityUid uid, StationMapUserComponent component, ref EntParentChangedMessage args) + { + if (TryComp(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(args.Session.AttachedEntity.Value); + comp.Map = uid; + } +} diff --git a/Content.Shared/Medical/CrewMonitoring/CrewMonitoringShared.cs b/Content.Shared/Medical/CrewMonitoring/CrewMonitoringShared.cs index ddf943e58d..7e5c00558b 100644 --- a/Content.Shared/Medical/CrewMonitoring/CrewMonitoringShared.cs +++ b/Content.Shared/Medical/CrewMonitoring/CrewMonitoringShared.cs @@ -13,14 +13,12 @@ namespace Content.Shared.Medical.CrewMonitoring public sealed class CrewMonitoringState : BoundUserInterfaceState { public List Sensors; - public readonly Vector2 WorldPosition; public readonly bool Snap; public readonly float Precision; - public CrewMonitoringState(List sensors, Vector2 worldPosition, bool snap, float precision) + public CrewMonitoringState(List sensors, bool snap, float precision) { Sensors = sensors; - WorldPosition = worldPosition; Snap = snap; Precision = precision; } diff --git a/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs b/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs index cdf3ee070c..5f59836182 100644 --- a/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs +++ b/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs @@ -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 index 0000000000..67b118c7ce --- /dev/null +++ b/Content.Shared/Pinpointer/NavMapComponent.cs @@ -0,0 +1,29 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Timing; + +namespace Content.Shared.Pinpointer; + +/// +/// Used to store grid poly data to be used for UIs. +/// +[RegisterComponent, NetworkedComponent] +public sealed class NavMapComponent : Component +{ + [ViewVariables] + public readonly Dictionary Chunks = new(); +} + +public sealed class NavMapChunk +{ + public readonly Vector2i Origin; + + /// + /// Bitmask for tiles, 1 for occupied and 0 for empty. + /// + 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 index 0000000000..3601ae9dfa --- /dev/null +++ b/Content.Shared/Pinpointer/SharedNavMapSystem.cs @@ -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; + + /// + /// Converts the chunk's tile into a bitflag for the slot. + /// + public static int GetFlag(Vector2i relativeTile) + { + return 1 << (relativeTile.X * ChunkSize + relativeTile.Y); + } + + /// + /// Converts the chunk's tile into a bitflag for the slot. + /// + 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 TileData = new(); + } + + [Serializable, NetSerializable] + protected sealed class NavMapDiffComponentState : ComponentState + { + public Dictionary TileData = new(); + public List RemovedChunks = new(); + } +} diff --git a/Content.Shared/Pinpointer/SharedStationMapSystem.cs b/Content.Shared/Pinpointer/SharedStationMapSystem.cs new file mode 100644 index 0000000000..13ec5dcd58 --- /dev/null +++ b/Content.Shared/Pinpointer/SharedStationMapSystem.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Pinpointer; + +[Serializable, NetSerializable] +public enum StationMapUiKey : byte +{ + Key, +} diff --git a/Content.Shared/Power/SharedPowerDevice.cs b/Content.Shared/Power/SharedPowerDevice.cs index 8a99dc7d91..4274dde60f 100644 --- a/Content.Shared/Power/SharedPowerDevice.cs +++ b/Content.Shared/Power/SharedPowerDevice.cs @@ -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 index 0000000000..d8172eb729 --- /dev/null +++ b/Resources/Locale/en-US/pinpointer/station_map.ftl @@ -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 index 0000000000..afce7da5f9 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/misc.yml @@ -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 index 0000000000..f88cd896ef --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Devices/station_map.yml @@ -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/Objects/Specific/Mining/ore_bag.yml b/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml similarity index 100% rename from Resources/Prototypes/Entities/Objects/Specific/Mining/ore_bag.yml rename to Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/station_map.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/station_map.yml new file mode 100644 index 0000000000..5fa2f4467a --- /dev/null +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/station_map.yml @@ -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 index 0000000000..5e1130fcc1 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 index 0000000000..11d1d586eb --- /dev/null +++ b/Resources/Textures/Objects/Devices/tablets.rsi/meta.json @@ -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 index 0000000000..417d44fbcc 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 index 0000000000..f79a25da85 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 index 0000000000..493b112b27 --- /dev/null +++ b/Resources/Textures/Structures/Machines/station_map.rsi/meta.json @@ -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 index 0000000000..90f2116848 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 index 0000000000..4a217e218c 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 index 0000000000..8c8af6a535 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 index 0000000000..aa458c9f13 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 index 0000000000..aaef50116a 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 index 0000000000..98c5caf7ff 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 index 0000000000..695418e665 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 index 0000000000..51c53b7a21 Binary files /dev/null and b/Resources/Textures/Structures/Machines/station_map.rsi/unshaded.png differ