]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Navmap rework (#26713)
authorchromiumboy <50505512+chromiumboy@users.noreply.github.com>
Wed, 17 Apr 2024 17:59:31 +0000 (12:59 -0500)
committerGitHub <noreply@github.com>
Wed, 17 Apr 2024 17:59:31 +0000 (12:59 -0500)
* Optimized the drawing of lines and tracked entities

* Optimized nav map updating and added thin wall support

* Added support for thin doors

* Removed floor tile seams, more line drawing optimizations

* Fixed split grids not updating correctly

* Cleaned up NavMapControl code

* Fix nav map header

* Converted nav map updates from system network messages to delta-states

* Addressed review comments

* Fixed timing issue where NavMapSystem would update before AirtightSystem did

Content.Client/Pinpointer/NavMapSystem.cs
Content.Client/Pinpointer/UI/NavMapControl.cs
Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs
Content.Client/Power/PowerMonitoringWindow.xaml.cs
Content.Server/Atmos/EntitySystems/AirtightSystem.cs
Content.Server/Pinpointer/NavMapSystem.cs
Content.Shared/Pinpointer/NavMapComponent.cs
Content.Shared/Pinpointer/SharedNavMapSystem.cs

index bd7dfc1117fb6c5e9b37432602cfc2381e9f48ce..868bf1fbc48be558d4855527126e3e49c09de6f4 100644 (file)
@@ -1,18 +1,14 @@
-using System.Numerics;
 using Content.Shared.Pinpointer;
-using Robust.Client.Graphics;
-using Robust.Shared.Enums;
 using Robust.Shared.GameStates;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
 
 namespace Content.Client.Pinpointer;
 
-public sealed class NavMapSystem : SharedNavMapSystem
+public sealed partial class NavMapSystem : SharedNavMapSystem
 {
     public override void Initialize()
     {
         base.Initialize();
+
         SubscribeLocalEvent<NavMapComponent, ComponentHandleState>(OnHandleState);
     }
 
@@ -21,89 +17,47 @@ public sealed class NavMapSystem : SharedNavMapSystem
         if (args.Current is not NavMapComponentState state)
             return;
 
-        component.Chunks.Clear();
-
-        foreach (var (origin, data) in state.TileData)
+        if (!state.FullState)
         {
-            component.Chunks.Add(origin, new NavMapChunk(origin)
+            foreach (var index in component.Chunks.Keys)
             {
-                TileData = data,
-            });
-        }
-
-        component.Beacons.Clear();
-        component.Beacons.AddRange(state.Beacons);
-
-        component.Airlocks.Clear();
-        component.Airlocks.AddRange(state.Airlocks);
-    }
-}
-
-public sealed class NavMapOverlay : Overlay
-{
-    private readonly IEntityManager _entManager;
-    private readonly IMapManager _mapManager;
-
-    public override OverlaySpace Space => OverlaySpace.WorldSpace;
-
-    private List<Entity<MapGridComponent>> _grids = new();
-
-    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));
+                if (!state.AllChunks!.Contains(index))
+                    component.Chunks.Remove(index);
+            }
 
-        _grids.Clear();
-        _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids);
+            foreach (var beacon in component.Beacons)
+            {
+                if (!state.AllBeacons!.Contains(beacon))
+                    component.Beacons.Remove(beacon);
+            }
+        }
 
-        foreach (var grid in _grids)
+        else
         {
-            if (!query.TryGetComponent(grid, 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.Comp.TileSize)
+            foreach (var index in component.Chunks.Keys)
             {
-                for (var y = Math.Floor(localAABB.Bottom); y <= Math.Ceiling(localAABB.Top); y += SharedNavMapSystem.ChunkSize * grid.Comp.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;
+                if (!state.Chunks.ContainsKey(index))
+                    component.Chunks.Remove(index);
+            }
 
-                    // 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);
+            foreach (var beacon in component.Beacons)
+            {
+                if (!state.Beacons.Contains(beacon))
+                    component.Beacons.Remove(beacon);
+            }
+        }
 
-                        var mask = chunk.TileData & value;
+        foreach (var ((category, origin), chunk) in state.Chunks)
+        {
+            var newChunk = new NavMapChunk(origin);
 
-                        if (mask == 0x0)
-                            continue;
+            foreach (var (atmosDirection, value) in chunk)
+                newChunk.TileData[atmosDirection] = value;
 
-                        var tile = chunk.Origin * SharedNavMapSystem.ChunkSize + SharedNavMapSystem.GetTile(mask);
-                        args.WorldHandle.DrawRect(new Box2(tile * grid.Comp.TileSize, (tile + 1) * grid.Comp.TileSize), Color.Aqua, false);
-                    }
-                }
-            }
+            component.Chunks[(category, origin)] = newChunk;
         }
 
-        args.WorldHandle.SetTransform(Matrix3.Identity);
+        foreach (var beacon in state.Beacons)
+            component.Beacons.Add(beacon);
     }
 }
index 677092e19181e6d0c1a8c83d4d9cb664e6813b88..3309e7c8df573d9718a7c2e4cb90b110cc8ee893 100644 (file)
@@ -16,6 +16,8 @@ using Robust.Shared.Physics.Components;
 using Robust.Shared.Timing;
 using System.Numerics;
 using JetBrains.Annotations;
+using Content.Shared.Atmos;
+using System.Linq;
 
 namespace Content.Client.Pinpointer.UI;
 
@@ -27,6 +29,7 @@ public partial class NavMapControl : MapGridControl
 {
     [Dependency] private IResourceCache _cache = default!;
     private readonly SharedTransformSystem _transformSystem;
+    private readonly SharedNavMapSystem _navMapSystem;
 
     public EntityUid? Owner;
     public EntityUid? MapUid;
@@ -40,7 +43,10 @@ public partial class NavMapControl : MapGridControl
     // Tracked data
     public Dictionary<EntityCoordinates, (bool Visible, Color Color)> TrackedCoordinates = new();
     public Dictionary<NetEntity, NavMapBlip> TrackedEntities = new();
-    public Dictionary<Vector2i, List<NavMapLine>>? TileGrid = default!;
+
+    public List<(Vector2, Vector2)> TileLines = new();
+    public List<(Vector2, Vector2)> TileRects = new();
+    public List<(Vector2[], Color)> TilePolygons = new();
 
     // Default colors
     public Color WallColor = new(102, 217, 102);
@@ -53,14 +59,23 @@ public partial class NavMapControl : MapGridControl
     protected static float MinDisplayedRange = 8f;
     protected static float MaxDisplayedRange = 128f;
     protected static float DefaultDisplayedRange = 48f;
+    protected float MinmapScaleModifier = 0.075f;
+    protected float FullWallInstep = 0.165f;
+    protected float ThinWallThickness = 0.165f;
+    protected float ThinDoorThickness = 0.30f;
 
     // Local variables
-    private float _updateTimer = 0.25f;
+    private float _updateTimer = 1.0f;
     private Dictionary<Color, Color> _sRGBLookUp = new();
     protected Color BackgroundColor;
     protected float BackgroundOpacity = 0.9f;
     private int _targetFontsize = 8;
 
+    protected Dictionary<(int, Vector2i), (int, Vector2i)> HorizLinesLookup = new();
+    protected Dictionary<(int, Vector2i), (int, Vector2i)> HorizLinesLookupReversed = new();
+    protected Dictionary<(int, Vector2i), (int, Vector2i)> VertLinesLookup = new();
+    protected Dictionary<(int, Vector2i), (int, Vector2i)> VertLinesLookupReversed = new();
+
     // Components
     private NavMapComponent? _navMap;
     private MapGridComponent? _grid;
@@ -72,6 +87,7 @@ public partial class NavMapControl : MapGridControl
     private readonly Label _zoom = new()
     {
         VerticalAlignment = VAlignment.Top,
+        HorizontalExpand = true,
         Margin = new Thickness(8f, 8f),
     };
 
@@ -80,6 +96,7 @@ public partial class NavMapControl : MapGridControl
         Text = Loc.GetString("navmap-recenter"),
         VerticalAlignment = VAlignment.Top,
         HorizontalAlignment = HAlignment.Right,
+        HorizontalExpand = true,
         Margin = new Thickness(8f, 4f),
         Disabled = true,
     };
@@ -87,9 +104,10 @@ public partial class NavMapControl : MapGridControl
     private readonly CheckBox _beacons = new()
     {
         Text = Loc.GetString("navmap-toggle-beacons"),
-        Margin = new Thickness(4f, 0f),
         VerticalAlignment = VAlignment.Center,
         HorizontalAlignment = HAlignment.Center,
+        HorizontalExpand = true,
+        Margin = new Thickness(4f, 0f),
         Pressed = true,
     };
 
@@ -98,6 +116,8 @@ public partial class NavMapControl : MapGridControl
         IoCManager.InjectDependencies(this);
 
         _transformSystem = EntManager.System<SharedTransformSystem>();
+        _navMapSystem = EntManager.System<SharedNavMapSystem>();
+
         BackgroundColor = Color.FromSrgb(TileColor.WithAlpha(BackgroundOpacity));
 
         RectClipContent = true;
@@ -112,6 +132,8 @@ public partial class NavMapControl : MapGridControl
                 BorderColor = StyleNano.PanelDark
             },
             VerticalExpand = false,
+            HorizontalExpand = true,
+            SetWidth = 650f,
             Children =
             {
                 new BoxContainer()
@@ -130,6 +152,7 @@ public partial class NavMapControl : MapGridControl
         var topContainer = new BoxContainer()
         {
             Orientation = BoxContainer.LayoutOrientation.Vertical,
+            HorizontalExpand = true,
             Children =
             {
                 topPanel,
@@ -157,6 +180,9 @@ public partial class NavMapControl : MapGridControl
     {
         EntManager.TryGetComponent(MapUid, out _navMap);
         EntManager.TryGetComponent(MapUid, out _grid);
+        EntManager.TryGetComponent(MapUid, out _xform);
+        EntManager.TryGetComponent(MapUid, out _physics);
+        EntManager.TryGetComponent(MapUid, out _fixtures);
 
         UpdateNavMap();
     }
@@ -251,119 +277,93 @@ public partial class NavMapControl : MapGridControl
         EntManager.TryGetComponent(MapUid, out _physics);
         EntManager.TryGetComponent(MapUid, out _fixtures);
 
+        if (_navMap == null || _grid == null || _xform == null)
+            return;
+
         // Map re-centering
         _recenter.Disabled = DrawRecenter();
 
-        _zoom.Text = Loc.GetString("navmap-zoom", ("value", $"{(DefaultDisplayedRange / WorldRange ):0.0}"));
-
-        if (_navMap == null || _xform == null)
-            return;
+        // Update zoom text
+        _zoom.Text = Loc.GetString("navmap-zoom", ("value", $"{(DefaultDisplayedRange / WorldRange):0.0}"));
 
+        // Update offset with physics local center
         var offset = Offset;
 
         if (_physics != null)
             offset += _physics.LocalCenter;
 
-        // Draw tiles
-        if (_fixtures != null)
+        var offsetVec = new Vector2(offset.X, -offset.Y);
+
+        // Wall sRGB
+        if (!_sRGBLookUp.TryGetValue(WallColor, out var wallsRGB))
+        {
+            wallsRGB = Color.ToSrgb(WallColor);
+            _sRGBLookUp[WallColor] = wallsRGB;
+        }
+
+        // Draw floor tiles
+        if (TilePolygons.Any())
         {
             Span<Vector2> verts = new Vector2[8];
 
-            foreach (var fixture in _fixtures.Fixtures.Values)
+            foreach (var (polygonVerts, polygonColor) in TilePolygons)
             {
-                if (fixture.Shape is not PolygonShape poly)
-                    continue;
-
-                for (var i = 0; i < poly.VertexCount; i++)
+                for (var i = 0; i < polygonVerts.Length; i++)
                 {
-                    var vert = poly.Vertices[i] - offset;
-
+                    var vert = polygonVerts[i] - offset;
                     verts[i] = ScalePosition(new Vector2(vert.X, -vert.Y));
                 }
 
-                handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts[..poly.VertexCount], TileColor);
+                handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts[..polygonVerts.Length], polygonColor);
             }
         }
 
-        var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset);
-
-        // Drawing lines can be rather expensive due to the number of neighbors that need to be checked in order
-        // to figure out where they should be drawn. However, we don't *need* to do check these every frame.
-        // Instead, lets periodically update where to draw each line and then store these points in a list.
-        // Then we can just run through the list each frame and draw the lines without any extra computation.
-
-        // Draw walls
-        if (TileGrid != null && TileGrid.Count > 0)
+        // Draw map lines
+        if (TileLines.Any())
         {
-            var walls = new ValueList<Vector2>();
+            var lines = new ValueList<Vector2>(TileLines.Count * 2);
 
-            foreach ((var chunk, var chunkedLines) in TileGrid)
+            foreach (var (o, t) in TileLines)
             {
-                var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
-
-                if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
-                    continue;
-
-                if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
-                    continue;
+                var origin = ScalePosition(o - offsetVec);
+                var terminus = ScalePosition(t - offsetVec);
 
-                foreach (var chunkedLine in chunkedLines)
-                {
-                    var start = ScalePosition(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
-                    var end = ScalePosition(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
-
-                    walls.Add(start);
-                    walls.Add(end);
-                }
+                lines.Add(origin);
+                lines.Add(terminus);
             }
 
-            if (walls.Count > 0)
-            {
-                if (!_sRGBLookUp.TryGetValue(WallColor, out var sRGB))
-                {
-                    sRGB = Color.ToSrgb(WallColor);
-                    _sRGBLookUp[WallColor] = sRGB;
-                }
-
-                handle.DrawPrimitives(DrawPrimitiveTopology.LineList, walls.Span, sRGB);
-            }
+            if (lines.Count > 0)
+                handle.DrawPrimitives(DrawPrimitiveTopology.LineList, lines.Span, wallsRGB);
         }
 
-        var airlockBuffer = Vector2.One * (MinimapScale / 2.25f) * 0.75f;
-        var airlockLines = new ValueList<Vector2>();
-        var foobarVec = new Vector2(1, -1);
-
-        foreach (var airlock in _navMap.Airlocks)
+        // Draw map rects
+        if (TileRects.Any())
         {
-            var position = airlock.Position - offset;
-            position = ScalePosition(position with { Y = -position.Y });
-            airlockLines.Add(position + airlockBuffer);
-            airlockLines.Add(position - airlockBuffer * foobarVec);
-
-            airlockLines.Add(position + airlockBuffer);
-            airlockLines.Add(position + airlockBuffer * foobarVec);
-
-            airlockLines.Add(position - airlockBuffer);
-            airlockLines.Add(position + airlockBuffer * foobarVec);
-
-            airlockLines.Add(position - airlockBuffer);
-            airlockLines.Add(position - airlockBuffer * foobarVec);
-
-            airlockLines.Add(position + airlockBuffer * -Vector2.UnitY);
-            airlockLines.Add(position - airlockBuffer * -Vector2.UnitY);
-        }
+            var rects = new ValueList<Vector2>(TileRects.Count * 8);
 
-        if (airlockLines.Count > 0)
-        {
-            if (!_sRGBLookUp.TryGetValue(WallColor, out var sRGB))
+            foreach (var (lt, rb) in TileRects)
             {
-                sRGB = Color.ToSrgb(WallColor);
-                _sRGBLookUp[WallColor] = sRGB;
+                var leftTop = ScalePosition(lt - offsetVec);
+                var rightBottom = ScalePosition(rb - offsetVec);
+
+                var rightTop = new Vector2(rightBottom.X, leftTop.Y);
+                var leftBottom = new Vector2(leftTop.X, rightBottom.Y);
+
+                rects.Add(leftTop);
+                rects.Add(rightTop);
+                rects.Add(rightTop);
+                rects.Add(rightBottom);
+                rects.Add(rightBottom);
+                rects.Add(leftBottom);
+                rects.Add(leftBottom);
+                rects.Add(leftTop);
             }
 
-            handle.DrawPrimitives(DrawPrimitiveTopology.LineList, airlockLines.Span, sRGB);
+            if (rects.Count > 0)
+                handle.DrawPrimitives(DrawPrimitiveTopology.LineList, rects.Span, wallsRGB);
         }
 
+        // Invoke post wall drawing action
         if (PostWallDrawingAction != null)
             PostWallDrawingAction.Invoke(handle);
 
@@ -373,7 +373,7 @@ public partial class NavMapControl : MapGridControl
             var rectBuffer = new Vector2(5f, 3f);
 
             // Calculate font size for current zoom level
-            var fontSize = (int) Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize , 0);
+            var fontSize = (int) Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize, 0);
             var font = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Bold.ttf"), fontSize);
 
             foreach (var beacon in _navMap.Beacons)
@@ -409,8 +409,6 @@ public partial class NavMapControl : MapGridControl
         }
 
         // Tracked entities (can use a supplied sprite as a marker instead; should probably just replace TrackedCoordinates with this eventually)
-        var iconVertexUVs = new Dictionary<(Texture, Color), ValueList<DrawVertexUV2D>>();
-
         foreach (var blip in TrackedEntities.Values)
         {
             if (blip.Blinks && !lit)
@@ -419,9 +417,6 @@ public partial class NavMapControl : MapGridControl
             if (blip.Texture == null)
                 continue;
 
-            if (!iconVertexUVs.TryGetValue((blip.Texture, blip.Color), out var vertexUVs))
-                vertexUVs = new();
-
             var mapPos = blip.Coordinates.ToMap(EntManager, _transformSystem);
 
             if (mapPos.MapId != MapId.Nullspace)
@@ -429,29 +424,11 @@ public partial class NavMapControl : MapGridControl
                 var position = _transformSystem.GetInvWorldMatrix(_xform).Transform(mapPos.Position) - offset;
                 position = ScalePosition(new Vector2(position.X, -position.Y));
 
-                var scalingCoefficient = 2.5f;
-                var positionOffset = scalingCoefficient * float.Sqrt(MinimapScale);
-
-                vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y - positionOffset), new Vector2(1f, 1f)));
-                vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f)));
-                vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f)));
-                vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f)));
-                vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f)));
-                vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y + positionOffset), new Vector2(0f, 0f)));
-            }
-
-            iconVertexUVs[(blip.Texture, blip.Color)] = vertexUVs;
-        }
+                var scalingCoefficient = MinmapScaleModifier * float.Sqrt(MinimapScale);
+                var positionOffset = new Vector2(scalingCoefficient * blip.Texture.Width, scalingCoefficient * blip.Texture.Height);
 
-        foreach ((var (texture, color), var vertexUVs) in iconVertexUVs)
-        {
-            if (!_sRGBLookUp.TryGetValue(color, out var sRGB))
-            {
-                sRGB = Color.ToSrgb(color);
-                _sRGBLookUp[color] = sRGB;
+                handle.DrawTextureRect(blip.Texture, new UIBox2(position - positionOffset, position + positionOffset), blip.Color);
             }
-
-            handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, texture, vertexUVs.Span, sRGB);
         }
     }
 
@@ -470,123 +447,293 @@ public partial class NavMapControl : MapGridControl
 
     protected virtual void UpdateNavMap()
     {
-        if (_navMap == null || _grid == null)
+        // Clear stale values
+        TilePolygons.Clear();
+        TileLines.Clear();
+        TileRects.Clear();
+
+        UpdateNavMapFloorTiles();
+        UpdateNavMapWallLines();
+        UpdateNavMapAirlocks();
+    }
+
+    private void UpdateNavMapFloorTiles()
+    {
+        if (_fixtures == null)
             return;
 
-        TileGrid = GetDecodedWallChunks(_navMap.Chunks, _grid);
+        var verts = new Vector2[8];
+
+        foreach (var fixture in _fixtures.Fixtures.Values)
+        {
+            if (fixture.Shape is not PolygonShape poly)
+                continue;
+
+            for (var i = 0; i < poly.VertexCount; i++)
+            {
+                var vert = poly.Vertices[i];
+                verts[i] = new Vector2(MathF.Round(vert.X), MathF.Round(vert.Y));
+            }
+
+            TilePolygons.Add((verts[..poly.VertexCount], TileColor));
+        }
     }
 
-    public Dictionary<Vector2i, List<NavMapLine>> GetDecodedWallChunks
-        (Dictionary<Vector2i, NavMapChunk> chunks,
-        MapGridComponent grid)
+    private void UpdateNavMapWallLines()
     {
-        var decodedOutput = new Dictionary<Vector2i, List<NavMapLine>>();
+        if (_navMap == null || _grid == null)
+            return;
 
-        foreach ((var chunkOrigin, var chunk) in chunks)
+        // We'll use the following dictionaries to combine collinear wall lines
+        HorizLinesLookup.Clear();
+        HorizLinesLookupReversed.Clear();
+        VertLinesLookup.Clear();
+        VertLinesLookupReversed.Clear();
+
+        foreach ((var (category, chunkOrigin), var chunk) in _navMap.Chunks)
         {
-            var list = new List<NavMapLine>();
+            if (category != NavMapChunkType.Wall)
+                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;
+                var value = (ushort) Math.Pow(2, i);
+                var mask = _navMapSystem.GetCombinedEdgesForChunk(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;
-                var position = new Vector2(tile.X, -tile.Y);
+                var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * _grid.TileSize;
+
+                if (!_navMapSystem.AllTileEdgesAreOccupied(chunk.TileData, relativeTile))
+                {
+                    AddRectForThinWall(chunk.TileData, tile);
+                    continue;
+                }
+
+                tile = tile with { Y = -tile.Y };
+
                 NavMapChunk? neighborChunk;
                 bool neighbor;
 
                 // North edge
                 if (relativeTile.Y == SharedNavMapSystem.ChunkSize - 1)
                 {
-                    neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) &&
-                                  (neighborChunk.TileData &
+                    neighbor = _navMap.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin + new Vector2i(0, 1)), out neighborChunk) &&
+                                  (neighborChunk.TileData[AtmosDirection.South] &
                                    SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, 0))) != 0x0;
                 }
                 else
                 {
                     var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, 1));
-                    neighbor = (chunk.TileData & flag) != 0x0;
+                    neighbor = (chunk.TileData[AtmosDirection.South] & flag) != 0x0;
                 }
 
                 if (!neighbor)
-                {
-                    // Add points
-                    list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, -grid.TileSize)));
-                }
+                    AddOrUpdateNavMapLine(tile + new Vector2i(0, -_grid.TileSize), tile + new Vector2i(_grid.TileSize, -_grid.TileSize), HorizLinesLookup, HorizLinesLookupReversed);
 
                 // East edge
                 if (relativeTile.X == SharedNavMapSystem.ChunkSize - 1)
                 {
-                    neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) &&
-                               (neighborChunk.TileData &
+                    neighbor = _navMap.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin + new Vector2i(1, 0)), out neighborChunk) &&
+                               (neighborChunk.TileData[AtmosDirection.West] &
                                 SharedNavMapSystem.GetFlag(new Vector2i(0, relativeTile.Y))) != 0x0;
                 }
                 else
                 {
                     var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(1, 0));
-                    neighbor = (chunk.TileData & flag) != 0x0;
+                    neighbor = (chunk.TileData[AtmosDirection.West] & flag) != 0x0;
                 }
 
                 if (!neighbor)
-                {
-                    // Add points
-                    list.Add(new NavMapLine(position + new Vector2(grid.TileSize, -grid.TileSize), position + new Vector2(grid.TileSize, 0f)));
-                }
+                    AddOrUpdateNavMapLine(tile + new Vector2i(_grid.TileSize, -_grid.TileSize), tile + new Vector2i(_grid.TileSize, 0), VertLinesLookup, VertLinesLookupReversed);
 
                 // South edge
                 if (relativeTile.Y == 0)
                 {
-                    neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, -1), out neighborChunk) &&
-                               (neighborChunk.TileData &
+                    neighbor = _navMap.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin + new Vector2i(0, -1)), out neighborChunk) &&
+                               (neighborChunk.TileData[AtmosDirection.North] &
                                 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;
+                    neighbor = (chunk.TileData[AtmosDirection.North] & flag) != 0x0;
                 }
 
                 if (!neighbor)
-                {
-                    // Add points
-                    list.Add(new NavMapLine(position + new Vector2(grid.TileSize, 0f), position));
-                }
+                    AddOrUpdateNavMapLine(tile, tile + new Vector2i(_grid.TileSize, 0), HorizLinesLookup, HorizLinesLookupReversed);
 
                 // West edge
                 if (relativeTile.X == 0)
                 {
-                    neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(-1, 0), out neighborChunk) &&
-                               (neighborChunk.TileData &
+                    neighbor = _navMap.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin + new Vector2i(-1, 0)), out neighborChunk) &&
+                               (neighborChunk.TileData[AtmosDirection.East] &
                                 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;
+                    neighbor = (chunk.TileData[AtmosDirection.East] & flag) != 0x0;
                 }
 
                 if (!neighbor)
+                    AddOrUpdateNavMapLine(tile + new Vector2i(0, -_grid.TileSize), tile, VertLinesLookup, VertLinesLookupReversed);
+
+                // Add a diagonal line for interiors. Unless there are a lot of double walls, there is no point combining these
+                TileLines.Add((tile + new Vector2(0, -_grid.TileSize), tile + new Vector2(_grid.TileSize, 0)));
+            }
+        }
+
+        // Record the combined lines 
+        foreach (var (origin, terminal) in HorizLinesLookup)
+            TileLines.Add((origin.Item2, terminal.Item2));
+
+        foreach (var (origin, terminal) in VertLinesLookup)
+            TileLines.Add((origin.Item2, terminal.Item2));
+    }
+
+    private void UpdateNavMapAirlocks()
+    {
+        if (_navMap == null || _grid == null)
+            return;
+
+        foreach (var ((category, _), chunk) in _navMap.Chunks)
+        {
+            if (category != NavMapChunkType.Airlock)
+                continue;
+
+            for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
+            {
+                var value = (int) Math.Pow(2, i);
+                var mask = _navMapSystem.GetCombinedEdgesForChunk(chunk.TileData) & value;
+
+                if (mask == 0x0)
+                    continue;
+
+                var relative = SharedNavMapSystem.GetTile(mask);
+                var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relative) * _grid.TileSize;
+
+                // If the edges of an airlock tile are not all occupied, draw a thin airlock for each edge
+                if (!_navMapSystem.AllTileEdgesAreOccupied(chunk.TileData, relative))
                 {
-                    // Add point
-                    list.Add(new NavMapLine(position, position + new Vector2(0f, -grid.TileSize)));
+                    AddRectForThinAirlock(chunk.TileData, tile);
+                    continue;
                 }
 
-                // Draw a diagonal line for interiors.
-                list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, 0f)));
+                // Otherwise add a single full tile airlock
+                TileRects.Add((new Vector2(tile.X + FullWallInstep, -tile.Y - FullWallInstep),
+                    new Vector2(tile.X - FullWallInstep + 1f, -tile.Y + FullWallInstep - 1)));
+
+                TileLines.Add((new Vector2(tile.X + 0.5f, -tile.Y - FullWallInstep),
+                    new Vector2(tile.X + 0.5f, -tile.Y + FullWallInstep - 1)));
             }
+        }
+    }
 
-            decodedOutput.Add(chunkOrigin, list);
+    private void AddRectForThinWall(Dictionary<AtmosDirection, ushort> tileData, Vector2i tile)
+    {
+        if (_navMapSystem == null || _grid == null)
+            return;
+
+        var leftTop = new Vector2(-0.5f, -0.5f + ThinWallThickness);
+        var rightBottom = new Vector2(0.5f, -0.5f);
+
+        foreach (var (direction, mask) in tileData)
+        {
+            var relative = SharedMapSystem.GetChunkRelative(tile, SharedNavMapSystem.ChunkSize);
+            var flag = (ushort) SharedNavMapSystem.GetFlag(relative);
+
+            if ((mask & flag) == 0)
+                continue;
+
+            var tilePosition = new Vector2(tile.X + 0.5f, -tile.Y - 0.5f);
+            var angle = new Angle(0);
+
+            switch (direction)
+            {
+                case AtmosDirection.East: angle = new Angle(MathF.PI * 0.5f); break;
+                case AtmosDirection.South: angle = new Angle(MathF.PI); break;
+                case AtmosDirection.West: angle = new Angle(MathF.PI * -0.5f); break;
+            }
+
+            TileRects.Add((angle.RotateVec(leftTop) + tilePosition, angle.RotateVec(rightBottom) + tilePosition));
+        }
+    }
+
+    private void AddRectForThinAirlock(Dictionary<AtmosDirection, ushort> tileData, Vector2i tile)
+    {
+        if (_navMapSystem == null || _grid == null)
+            return;
+
+        var leftTop = new Vector2(-0.5f + FullWallInstep, -0.5f + FullWallInstep + ThinDoorThickness);
+        var rightBottom = new Vector2(0.5f - FullWallInstep, -0.5f + FullWallInstep);
+        var centreTop = new Vector2(0f, -0.5f + FullWallInstep + ThinDoorThickness);
+        var centreBottom = new Vector2(0f, -0.5f + FullWallInstep);
+
+        foreach (var (direction, mask) in tileData)
+        {
+            var relative = SharedMapSystem.GetChunkRelative(tile, SharedNavMapSystem.ChunkSize);
+            var flag = (ushort) SharedNavMapSystem.GetFlag(relative);
+
+            if ((mask & flag) == 0)
+                continue;
+
+            var tilePosition = new Vector2(tile.X + 0.5f, -tile.Y - 0.5f);
+            var angle = new Angle(0);
+
+            switch (direction)
+            {
+                case AtmosDirection.East: angle = new Angle(MathF.PI * 0.5f);break;
+                case AtmosDirection.South: angle = new Angle(MathF.PI); break;
+                case AtmosDirection.West: angle = new Angle(MathF.PI * -0.5f); break;
+            }
+
+            TileRects.Add((angle.RotateVec(leftTop) + tilePosition, angle.RotateVec(rightBottom) + tilePosition));
+            TileLines.Add((angle.RotateVec(centreTop) + tilePosition, angle.RotateVec(centreBottom) + tilePosition));
+        }
+    }
+
+    protected void AddOrUpdateNavMapLine
+        (Vector2i origin,
+        Vector2i terminus,
+        Dictionary<(int, Vector2i), (int, Vector2i)> lookup,
+        Dictionary<(int, Vector2i), (int, Vector2i)> lookupReversed,
+        int index = 0)
+    {
+        (int, Vector2i) foundTermiusTuple;
+        (int, Vector2i) foundOriginTuple;
+
+        if (lookup.TryGetValue((index, terminus), out foundTermiusTuple) &&
+            lookupReversed.TryGetValue((index, origin), out foundOriginTuple))
+        {
+            lookup[foundOriginTuple] = foundTermiusTuple;
+            lookupReversed[foundTermiusTuple] = foundOriginTuple;
+
+            lookup.Remove((index, terminus));
+            lookupReversed.Remove((index, origin));
+        }
+
+        else if (lookup.TryGetValue((index, terminus), out foundTermiusTuple))
+        {
+            lookup[(index, origin)] = foundTermiusTuple;
+            lookup.Remove((index, terminus));
+            lookupReversed[foundTermiusTuple] = (index, origin);
+        }
+
+        else if (lookupReversed.TryGetValue((index, origin), out foundOriginTuple))
+        {
+            lookupReversed[(index, terminus)] = foundOriginTuple;
+            lookupReversed.Remove(foundOriginTuple);
+            lookup[foundOriginTuple] = (index, terminus);
         }
 
-        return decodedOutput;
+        else
+        {
+            lookup.Add((index, origin), (index, terminus));
+            lookupReversed.Add((index, terminus), (index, origin));
+        }
     }
 
     protected Vector2 GetOffset()
@@ -612,15 +759,3 @@ public struct NavMapBlip
         Selectable = selectable;
     }
 }
-
-public struct NavMapLine
-{
-    public readonly Vector2 Origin;
-    public readonly Vector2 Terminus;
-
-    public NavMapLine(Vector2 origin, Vector2 terminus)
-    {
-        Origin = origin;
-        Terminus = terminus;
-    }
-}
index 902d6bb7e609ebc6cf6dbd5b889e9fcaeeca0040..3d94318be8260c6172984e418bb29051c12198c3 100644 (file)
@@ -23,8 +23,8 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
 
     public PowerMonitoringCableNetworksComponent? PowerMonitoringCableNetworks;
     public List<PowerMonitoringConsoleLineGroup> HiddenLineGroups = new();
-    public Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>? PowerCableNetwork;
-    public Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>? FocusCableNetwork;
+    public List<PowerMonitoringConsoleLine> PowerCableNetwork = new();
+    public List<PowerMonitoringConsoleLine> FocusCableNetwork = new();
 
     private MapGridComponent? _grid;
 
@@ -48,15 +48,15 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
         if (!_entManager.TryGetComponent<PowerMonitoringCableNetworksComponent>(Owner, out var cableNetworks))
             return;
 
-        if (!_entManager.TryGetComponent(MapUid, out _grid))
-            return;
-
-        PowerCableNetwork = GetDecodedPowerCableChunks(cableNetworks.AllChunks, _grid);
-        FocusCableNetwork = GetDecodedPowerCableChunks(cableNetworks.FocusChunks, _grid);
+        PowerCableNetwork = GetDecodedPowerCableChunks(cableNetworks.AllChunks);
+        FocusCableNetwork = GetDecodedPowerCableChunks(cableNetworks.FocusChunks);
     }
 
     public void DrawAllCableNetworks(DrawingHandleScreen handle)
     {
+        if (!_entManager.TryGetComponent(MapUid, out _grid))
+            return;
+
         // Draw full cable network
         if (PowerCableNetwork != null && PowerCableNetwork.Count > 0)
         {
@@ -69,36 +69,29 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
             DrawCableNetwork(handle, FocusCableNetwork, Color.White);
     }
 
-    public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary<Vector2i, List<PowerMonitoringConsoleLine>> fullCableNetwork, Color modulator)
+    public void DrawCableNetwork(DrawingHandleScreen handle, List<PowerMonitoringConsoleLine> fullCableNetwork, Color modulator)
     {
+        if (!_entManager.TryGetComponent(MapUid, out _grid))
+            return;
+
         var offset = GetOffset();
-        var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset);
+        offset = offset with { Y = -offset.Y };
 
         if (WorldRange / WorldMaxRange > 0.5f)
         {
             var cableNetworks = new ValueList<Vector2>[3];
 
-            foreach ((var chunk, var chunkedLines) in fullCableNetwork)
+            foreach (var line in fullCableNetwork)
             {
-                var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
-
-                if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
-                    continue;
-
-                if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
+                if (HiddenLineGroups.Contains(line.Group))
                     continue;
 
-                foreach (var chunkedLine in chunkedLines)
-                {
-                    if (HiddenLineGroups.Contains(chunkedLine.Group))
-                        continue;
-
-                    var start = ScalePosition(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
-                    var end = ScalePosition(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
+                var cableOffset = _powerCableOffsets[(int) line.Group];
+                var start = ScalePosition(line.Origin + cableOffset - offset);
+                var end = ScalePosition(line.Terminus + cableOffset - offset);
 
-                    cableNetworks[(int) chunkedLine.Group].Add(start);
-                    cableNetworks[(int) chunkedLine.Group].Add(end);
-                }
+                cableNetworks[(int) line.Group].Add(start);
+                cableNetworks[(int) line.Group].Add(end);
             }
 
             for (int cableNetworkIdx = 0; cableNetworkIdx < cableNetworks.Length; cableNetworkIdx++)
@@ -124,48 +117,39 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
         {
             var cableVertexUVs = new ValueList<Vector2>[3];
 
-            foreach ((var chunk, var chunkedLines) in fullCableNetwork)
+            foreach (var line in fullCableNetwork)
             {
-                var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
-
-                if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
+                if (HiddenLineGroups.Contains(line.Group))
                     continue;
 
-                if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
-                    continue;
-
-                foreach (var chunkedLine in chunkedLines)
-                {
-                    if (HiddenLineGroups.Contains(chunkedLine.Group))
-                        continue;
-
-                    var leftTop = ScalePosition(new Vector2
-                        (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
-                        Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
-                        - new Vector2(offset.X, -offset.Y));
-
-                    var rightTop = ScalePosition(new Vector2
-                        (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
-                        Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
-                        - new Vector2(offset.X, -offset.Y));
-
-                    var leftBottom = ScalePosition(new Vector2
-                        (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
-                        Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
-                        - new Vector2(offset.X, -offset.Y));
-
-                    var rightBottom = ScalePosition(new Vector2
-                        (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
-                        Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
-                        - new Vector2(offset.X, -offset.Y));
-
-                    cableVertexUVs[(int) chunkedLine.Group].Add(leftBottom);
-                    cableVertexUVs[(int) chunkedLine.Group].Add(leftTop);
-                    cableVertexUVs[(int) chunkedLine.Group].Add(rightBottom);
-                    cableVertexUVs[(int) chunkedLine.Group].Add(leftTop);
-                    cableVertexUVs[(int) chunkedLine.Group].Add(rightBottom);
-                    cableVertexUVs[(int) chunkedLine.Group].Add(rightTop);
-                }
+                var cableOffset = _powerCableOffsets[(int) line.Group];
+
+                var leftTop = ScalePosition(new Vector2
+                    (Math.Min(line.Origin.X, line.Terminus.X) - 0.1f,
+                    Math.Min(line.Origin.Y, line.Terminus.Y) - 0.1f)
+                    + cableOffset - offset);
+
+                var rightTop = ScalePosition(new Vector2
+                    (Math.Max(line.Origin.X, line.Terminus.X) + 0.1f,
+                    Math.Min(line.Origin.Y, line.Terminus.Y) - 0.1f)
+                    + cableOffset - offset);
+
+                var leftBottom = ScalePosition(new Vector2
+                    (Math.Min(line.Origin.X, line.Terminus.X) - 0.1f,
+                    Math.Max(line.Origin.Y, line.Terminus.Y) + 0.1f)
+                    + cableOffset - offset);
+
+                var rightBottom = ScalePosition(new Vector2
+                    (Math.Max(line.Origin.X, line.Terminus.X) + 0.1f,
+                    Math.Max(line.Origin.Y, line.Terminus.Y) + 0.1f)
+                    + cableOffset - offset);
+
+                cableVertexUVs[(int) line.Group].Add(leftBottom);
+                cableVertexUVs[(int) line.Group].Add(leftTop);
+                cableVertexUVs[(int) line.Group].Add(rightBottom);
+                cableVertexUVs[(int) line.Group].Add(leftTop);
+                cableVertexUVs[(int) line.Group].Add(rightBottom);
+                cableVertexUVs[(int) line.Group].Add(rightTop);
             }
 
             for (int cableNetworkIdx = 0; cableNetworkIdx < cableVertexUVs.Length; cableNetworkIdx++)
@@ -188,23 +172,28 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
         }
     }
 
-    public Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>? GetDecodedPowerCableChunks(Dictionary<Vector2i, PowerCableChunk>? chunks, MapGridComponent? grid)
+    public List<PowerMonitoringConsoleLine> GetDecodedPowerCableChunks(Dictionary<Vector2i, PowerCableChunk>? chunks)
     {
-        if (chunks == null || grid == null)
-            return null;
+        var decodedOutput = new List<PowerMonitoringConsoleLine>();
+
+        if (!_entManager.TryGetComponent(MapUid, out _grid))
+            return decodedOutput;
+
+        if (chunks == null)
+            return decodedOutput;
 
-        var decodedOutput = new Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>();
+        // We'll use the following dictionaries to combine collinear power cable lines
+        HorizLinesLookup.Clear();
+        HorizLinesLookupReversed.Clear();
+        VertLinesLookup.Clear();
+        VertLinesLookupReversed.Clear();
 
         foreach ((var chunkOrigin, var chunk) in chunks)
         {
-            var list = new List<PowerMonitoringConsoleLine>();
-
             for (int cableIdx = 0; cableIdx < chunk.PowerCableData.Length; cableIdx++)
             {
                 var chunkMask = chunk.PowerCableData[cableIdx];
 
-                Vector2 offset = _powerCableOffsets[cableIdx];
-
                 for (var chunkIdx = 0; chunkIdx < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; chunkIdx++)
                 {
                     var value = (int) Math.Pow(2, chunkIdx);
@@ -214,8 +203,8 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
                         continue;
 
                     var relativeTile = SharedNavMapSystem.GetTile(mask);
-                    var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize;
-                    var position = new Vector2(tile.X, -tile.Y);
+                    var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * _grid.TileSize;
+                    tile = tile with { Y = -tile.Y };
 
                     PowerCableChunk neighborChunk;
                     bool neighbor;
@@ -237,12 +226,7 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
                     if (neighbor)
                     {
                         // Add points
-                        var line = new PowerMonitoringConsoleLine
-                            (position + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
-                            position + new Vector2(1f, 0f) + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
-                            (PowerMonitoringConsoleLineGroup) cableIdx);
-
-                        list.Add(line);
+                        AddOrUpdateNavMapLine(tile, tile + new Vector2i(_grid.TileSize, 0), HorizLinesLookup, HorizLinesLookupReversed, cableIdx);
                     }
 
                     // North
@@ -260,21 +244,21 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
                     if (neighbor)
                     {
                         // Add points
-                        var line = new PowerMonitoringConsoleLine
-                            (position + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
-                            position + new Vector2(0f, -1f) + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
-                            (PowerMonitoringConsoleLineGroup) cableIdx);
-
-                        list.Add(line);
+                        AddOrUpdateNavMapLine(tile + new Vector2i(0, -_grid.TileSize), tile, VertLinesLookup, VertLinesLookupReversed, cableIdx);
                     }
                 }
 
             }
-
-            if (list.Count > 0)
-                decodedOutput.Add(chunkOrigin, list);
         }
 
+        var gridOffset = new Vector2(_grid.TileSize * 0.5f, -_grid.TileSize * 0.5f);
+
+        foreach (var (origin, terminal) in HorizLinesLookup)
+            decodedOutput.Add(new PowerMonitoringConsoleLine(origin.Item2 + gridOffset, terminal.Item2 + gridOffset, (PowerMonitoringConsoleLineGroup) origin.Item1));
+
+        foreach (var (origin, terminal) in VertLinesLookup)
+            decodedOutput.Add(new PowerMonitoringConsoleLine(origin.Item2 + gridOffset, terminal.Item2 + gridOffset, (PowerMonitoringConsoleLineGroup) origin.Item1));
+
         return decodedOutput;
     }
 }
index edc0eaa18a851054f4dd76864a22f5be59f06e15..81fe1f4d047831cba14179e2e88338b061e5d9f2 100644 (file)
@@ -170,9 +170,6 @@ public sealed partial class PowerMonitoringWindow : FancyWindow
             NavMap.TrackedEntities[mon.Value] = blip;
         }
 
-        // Update nav map
-        NavMap.ForceNavMapUpdate();
-
         // If the entry group doesn't match the current tab, the data is out dated, do not use it
         if (allEntries.Length > 0 && allEntries[0].Group != GetCurrentPowerMonitoringConsoleGroup())
             return;
index 152fba8fc4d8fc23c6f87a7eafd22c5e473d361b..60d90e1e609456caf423862c4c7c3cd39d52d99e 100644 (file)
@@ -12,6 +12,7 @@ namespace Content.Server.Atmos.EntitySystems
         [Dependency] private readonly SharedTransformSystem _transform = default!;
         [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
         [Dependency] private readonly ExplosionSystem _explosionSystem = default!;
+        [Dependency] private readonly SharedMapSystem _mapSystem = default!;
 
         public override void Initialize()
         {
@@ -59,12 +60,14 @@ namespace Content.Server.Atmos.EntitySystems
 
             var gridId = xform.GridUid;
             var coords = xform.Coordinates;
-
-            var tilePos = grid.TileIndicesFor(coords);
+            var tilePos = _mapSystem.TileIndicesFor(gridId.Value, grid, coords);
 
             // Update and invalidate new position.
             airtight.LastPosition = (gridId.Value, tilePos);
             InvalidatePosition(gridId.Value, tilePos);
+
+            var airtightEv = new AirtightChanged(uid, airtight, (gridId.Value, tilePos));
+            RaiseLocalEvent(uid, ref airtightEv, true);
         }
 
         private void OnAirtightReAnchor(EntityUid uid, AirtightComponent airtight, ref ReAnchorEvent args)
@@ -74,6 +77,9 @@ namespace Content.Server.Atmos.EntitySystems
                 // Update and invalidate new position.
                 airtight.LastPosition = (gridId, args.TilePos);
                 InvalidatePosition(gridId, args.TilePos);
+
+                var airtightEv = new AirtightChanged(uid, airtight, (gridId, args.TilePos));
+                RaiseLocalEvent(uid, ref airtightEv, true);
             }
         }
 
@@ -153,6 +159,5 @@ namespace Content.Server.Atmos.EntitySystems
     }
 
     [ByRefEvent]
-    public readonly record struct AirtightChanged(EntityUid Entity, AirtightComponent Airtight,
-        (EntityUid Grid, Vector2i Tile) Position);
+    public readonly record struct AirtightChanged(EntityUid Entity, AirtightComponent Airtight, (EntityUid Grid, Vector2i Tile) Position);
 }
index 2a5639886ed5654465dd237ae24f30cd37ab97c9..34c76a13206252d4bc616fa8fa650be16b0c4225 100644 (file)
@@ -1,36 +1,33 @@
-using System.Diagnostics.CodeAnalysis;
 using Content.Server.Administration.Logs;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
 using Content.Server.Station.Systems;
 using Content.Server.Warps;
 using Content.Shared.Database;
 using Content.Shared.Examine;
 using Content.Shared.Localizations;
+using Content.Shared.Maps;
 using Content.Shared.Pinpointer;
-using Content.Shared.Tag;
 using JetBrains.Annotations;
-using Robust.Server.GameObjects;
-using Robust.Shared.GameStates;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
-using Robust.Shared.Physics;
-using Robust.Shared.Physics.Components;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+using System.Diagnostics.CodeAnalysis;
 
 namespace Content.Server.Pinpointer;
 
 /// <summary>
 /// Handles data to be used for in-grid map displays.
 /// </summary>
-public sealed class NavMapSystem : SharedNavMapSystem
+public sealed partial class NavMapSystem : SharedNavMapSystem
 {
     [Dependency] private readonly IAdminLogManager _adminLog = default!;
     [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
-    [Dependency] private readonly TagSystem _tags = default!;
-    [Dependency] private readonly MapSystem _map = default!;
+    [Dependency] private readonly SharedMapSystem _mapSystem = default!;
+    [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
     [Dependency] private readonly IMapManager _mapManager = default!;
-    [Dependency] private readonly TransformSystem _transform = default!;
-
-    private EntityQuery<PhysicsComponent> _physicsQuery;
-    private EntityQuery<TagComponent> _tagQuery;
+    [Dependency] private readonly IGameTiming _gameTiming = default!;
 
     public const float CloseDistance = 15f;
     public const float FarDistance = 30f;
@@ -39,63 +36,121 @@ public sealed class NavMapSystem : SharedNavMapSystem
     {
         base.Initialize();
 
-        _physicsQuery = GetEntityQuery<PhysicsComponent>();
-        _tagQuery = GetEntityQuery<TagComponent>();
-
-        SubscribeLocalEvent<AnchorStateChangedEvent>(OnAnchorChange);
-        SubscribeLocalEvent<ReAnchorEvent>(OnReAnchor);
+        // Initialization events
         SubscribeLocalEvent<StationGridAddedEvent>(OnStationInit);
-        SubscribeLocalEvent<NavMapComponent, ComponentStartup>(OnNavMapStartup);
-        SubscribeLocalEvent<NavMapComponent, ComponentGetState>(OnGetState);
+
+        // Grid change events
         SubscribeLocalEvent<GridSplitEvent>(OnNavMapSplit);
+        SubscribeLocalEvent<TileChangedEvent>(OnTileChanged);
 
+        // Airtight structure change event
+        SubscribeLocalEvent<AirtightChanged>(OnAirtightChanged);
+
+        // Beacon events
         SubscribeLocalEvent<NavMapBeaconComponent, MapInitEvent>(OnNavMapBeaconMapInit);
-        SubscribeLocalEvent<NavMapBeaconComponent, ComponentStartup>(OnNavMapBeaconStartup);
         SubscribeLocalEvent<NavMapBeaconComponent, AnchorStateChangedEvent>(OnNavMapBeaconAnchor);
-
-        SubscribeLocalEvent<NavMapDoorComponent, ComponentStartup>(OnNavMapDoorStartup);
-        SubscribeLocalEvent<NavMapDoorComponent, AnchorStateChangedEvent>(OnNavMapDoorAnchor);
-
         SubscribeLocalEvent<ConfigurableNavMapBeaconComponent, NavMapBeaconConfigureBuiMessage>(OnConfigureMessage);
         SubscribeLocalEvent<ConfigurableNavMapBeaconComponent, MapInitEvent>(OnConfigurableMapInit);
         SubscribeLocalEvent<ConfigurableNavMapBeaconComponent, ExaminedEvent>(OnConfigurableExamined);
     }
 
+    #region: Initialization event handling
     private void OnStationInit(StationGridAddedEvent ev)
     {
         var comp = EnsureComp<NavMapComponent>(ev.GridId);
         RefreshGrid(ev.GridId, comp, Comp<MapGridComponent>(ev.GridId));
     }
 
-    private void OnNavMapBeaconMapInit(EntityUid uid, NavMapBeaconComponent component, MapInitEvent args)
+    #endregion
+
+    #region: Grid change event handling
+
+    private void OnNavMapSplit(ref GridSplitEvent args)
     {
-        if (component.DefaultText == null || component.Text != null)
+        if (!TryComp(args.Grid, out NavMapComponent? comp))
             return;
 
-        component.Text = Loc.GetString(component.DefaultText);
-        Dirty(uid, component);
-        RefreshNavGrid(uid);
+        var gridQuery = GetEntityQuery<MapGridComponent>();
+
+        foreach (var grid in args.NewGrids)
+        {
+            var newComp = EnsureComp<NavMapComponent>(grid);
+            RefreshGrid(grid, newComp, gridQuery.GetComponent(grid));
+        }
+
+        RefreshGrid(args.Grid, comp, gridQuery.GetComponent(args.Grid));
     }
 
-    private void OnNavMapBeaconStartup(EntityUid uid, NavMapBeaconComponent component, ComponentStartup args)
+    private void OnTileChanged(ref TileChangedEvent ev)
     {
-        RefreshNavGrid(uid);
+        if (!TryComp<NavMapComponent>(ev.NewTile.GridUid, out var navMap))
+            return;
+
+        var tile = ev.NewTile.GridIndices;
+        var chunkOrigin = SharedMapSystem.GetChunkIndices(tile, ChunkSize);
+
+        if (!navMap.Chunks.TryGetValue((NavMapChunkType.Floor, chunkOrigin), out var chunk))
+            chunk = new(chunkOrigin);
+
+        // This could be easily replaced in the future to accommodate diagonal tiles
+        if (ev.NewTile.IsSpace())
+            chunk = UnsetAllEdgesForChunkTile(chunk, tile);
+
+        else
+            chunk = SetAllEdgesForChunkTile(chunk, tile);
+
+        chunk.LastUpdate = _gameTiming.CurTick;
+        navMap.Chunks[(NavMapChunkType.Floor, chunkOrigin)] = chunk;
+
+        Dirty(ev.NewTile.GridUid, navMap);
     }
 
-    private void OnNavMapBeaconAnchor(EntityUid uid, NavMapBeaconComponent component, ref AnchorStateChangedEvent args)
+    private void OnAirtightChanged(ref AirtightChanged ev)
     {
-        UpdateBeaconEnabledVisuals((uid, component));
-        RefreshNavGrid(uid);
+        var gridUid = ev.Position.Grid;
+
+        if (!TryComp<NavMapComponent>(gridUid, out var navMap) ||
+            !TryComp<MapGridComponent>(gridUid, out var mapGrid))
+            return;
+
+        // Refresh the affected tile
+        var tile = ev.Position.Tile;
+        var chunkOrigin = SharedMapSystem.GetChunkIndices(tile, ChunkSize);
+
+        RefreshTileEntityContents(gridUid, navMap, mapGrid, chunkOrigin, tile);
+
+        // Update potentially affected chunks
+        foreach (var category in EntityChunkTypes)
+        {
+            if (!navMap.Chunks.TryGetValue((category, chunkOrigin), out var chunk))
+                continue;
+
+            chunk.LastUpdate = _gameTiming.CurTick;
+            navMap.Chunks[(category, chunkOrigin)] = chunk;
+        }
+
+        Dirty(gridUid, navMap);
     }
 
-    private void OnNavMapDoorStartup(Entity<NavMapDoorComponent> ent, ref ComponentStartup args)
+    #endregion
+
+    #region: Beacon event handling
+
+    private void OnNavMapBeaconMapInit(EntityUid uid, NavMapBeaconComponent component, MapInitEvent args)
     {
-        RefreshNavGrid(ent);
+        if (component.DefaultText == null || component.Text != null)
+            return;
+
+        component.Text = Loc.GetString(component.DefaultText);
+        Dirty(uid, component);
+
+        UpdateNavMapBeaconData(uid, component);
     }
 
-    private void OnNavMapDoorAnchor(Entity<NavMapDoorComponent> ent, ref AnchorStateChangedEvent args)
+    private void OnNavMapBeaconAnchor(EntityUid uid, NavMapBeaconComponent component, ref AnchorStateChangedEvent args)
     {
-        RefreshNavGrid(ent);
+        UpdateBeaconEnabledVisuals((uid, component));
+        UpdateNavMapBeaconData(uid, component);
     }
 
     private void OnConfigureMessage(Entity<ConfigurableNavMapBeaconComponent> ent, ref NavMapBeaconConfigureBuiMessage args)
@@ -103,12 +158,12 @@ public sealed class NavMapSystem : SharedNavMapSystem
         if (args.Session.AttachedEntity is not { } user)
             return;
 
-        if (!TryComp<NavMapBeaconComponent>(ent, out var navMap))
+        if (!TryComp<NavMapBeaconComponent>(ent, out var beacon))
             return;
 
-        if (navMap.Text == args.Text &&
-            navMap.Color == args.Color &&
-            navMap.Enabled == args.Enabled)
+        if (beacon.Text == args.Text &&
+            beacon.Color == args.Color &&
+            beacon.Enabled == args.Enabled)
             return;
 
         _adminLog.Add(LogType.Action, LogImpact.Medium,
@@ -119,12 +174,12 @@ public sealed class NavMapSystem : SharedNavMapSystem
             warpPoint.Location = args.Text;
         }
 
-        navMap.Text = args.Text;
-        navMap.Color = args.Color;
-        navMap.Enabled = args.Enabled;
-        UpdateBeaconEnabledVisuals((ent, navMap));
-        Dirty(ent, navMap);
-        RefreshNavGrid(ent);
+        beacon.Text = args.Text;
+        beacon.Color = args.Color;
+        beacon.Enabled = args.Enabled;
+
+        UpdateBeaconEnabledVisuals((ent, beacon));
+        UpdateNavMapBeaconData(ent, beacon);
     }
 
     private void OnConfigurableMapInit(Entity<ConfigurableNavMapBeaconComponent> ent, ref MapInitEvent args)
@@ -134,9 +189,7 @@ public sealed class NavMapSystem : SharedNavMapSystem
 
         // We set this on mapinit just in case the text was edited via VV or something.
         if (TryComp<WarpPointComponent>(ent, out var warpPoint))
-        {
             warpPoint.Location = navMap.Text;
-        }
 
         UpdateBeaconEnabledVisuals((ent, navMap));
     }
@@ -152,231 +205,134 @@ public sealed class NavMapSystem : SharedNavMapSystem
             ("label", navMap.Text ?? string.Empty)));
     }
 
-    private void UpdateBeaconEnabledVisuals(Entity<NavMapBeaconComponent> ent)
-    {
-        _appearance.SetData(ent, NavMapBeaconVisuals.Enabled, ent.Comp.Enabled && Transform(ent).Anchored);
-    }
-
-    /// <summary>
-    /// Refreshes the grid for the corresponding beacon.
-    /// </summary>
-    /// <param name="uid"></param>
-    private void RefreshNavGrid(EntityUid uid)
-    {
-        var xform = Transform(uid);
+    #endregion
 
-        if (!TryComp<NavMapComponent>(xform.GridUid, out var navMap))
-            return;
-
-        Dirty(xform.GridUid.Value, navMap);
-    }
+    #region: Grid functions
 
-    private bool CanBeacon(EntityUid uid, TransformComponent? xform = null)
+    private void RefreshGrid(EntityUid uid, NavMapComponent component, MapGridComponent mapGrid)
     {
-        if (!Resolve(uid, ref xform))
-            return false;
+        // Clear stale data
+        component.Chunks.Clear();
+        component.Beacons.Clear();
 
-        return xform.GridUid != null && xform.Anchored;
-    }
+        // Loop over all tiles
+        var tileRefs = _mapSystem.GetAllTiles(uid, mapGrid);
 
-    private void OnNavMapStartup(EntityUid uid, NavMapComponent component, ComponentStartup args)
-    {
-        if (!TryComp<MapGridComponent>(uid, out var grid))
-            return;
+        foreach (var tileRef in tileRefs)
+        {
+            var tile = tileRef.GridIndices;
+            var chunkOrigin = SharedMapSystem.GetChunkIndices(tile, ChunkSize);
 
-        RefreshGrid(uid, component, grid);
-    }
+            if (!component.Chunks.TryGetValue((NavMapChunkType.Floor, chunkOrigin), out var chunk))
+                chunk = new(chunkOrigin);
 
-    private void OnNavMapSplit(ref GridSplitEvent args)
-    {
-        if (!TryComp(args.Grid, out NavMapComponent? comp))
-            return;
+            chunk.LastUpdate = _gameTiming.CurTick;
 
-        var gridQuery = GetEntityQuery<MapGridComponent>();
+            // Refresh the floor tile
+            component.Chunks[(NavMapChunkType.Floor, chunkOrigin)] = SetAllEdgesForChunkTile(chunk, tile);
 
-        foreach (var grid in args.NewGrids)
-        {
-            var newComp = EnsureComp<NavMapComponent>(grid);
-            RefreshGrid(grid, newComp, gridQuery.GetComponent(grid));
+            // Refresh the contents of the tile
+            RefreshTileEntityContents(uid, component, mapGrid, chunkOrigin, tile);
         }
 
-        RefreshGrid(args.Grid, comp, gridQuery.GetComponent(args.Grid));
+        Dirty(uid, component);
     }
 
-    private void RefreshGrid(EntityUid uid, NavMapComponent component, MapGridComponent grid)
+    private void RefreshTileEntityContents(EntityUid uid, NavMapComponent component, MapGridComponent mapGrid, Vector2i chunkOrigin, Vector2i tile)
     {
-        component.Chunks.Clear();
-
-        var tiles = grid.GetAllTilesEnumerator();
+        var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
+        var flag = (ushort) GetFlag(relative);
+        var invFlag = (ushort) ~flag;
 
-        while (tiles.MoveNext(out var tile))
+        // Clear stale data from the tile across all entity associated chunks
+        foreach (var category in EntityChunkTypes)
         {
-            var chunkOrigin = SharedMapSystem.GetChunkIndices(tile.Value.GridIndices, ChunkSize);
+            if (!component.Chunks.TryGetValue((category, chunkOrigin), out var chunk))
+                chunk = new(chunkOrigin);
 
-            if (!component.Chunks.TryGetValue(chunkOrigin, out var chunk))
-            {
-                chunk = new NavMapChunk(chunkOrigin);
-                component.Chunks[chunkOrigin] = chunk;
-            }
+            foreach (var (direction, _) in chunk.TileData)
+                chunk.TileData[direction] &= invFlag;
 
-            RefreshTile(uid, grid, component, chunk, tile.Value.GridIndices);
+            chunk.LastUpdate = _gameTiming.CurTick;
+            component.Chunks[(category, chunkOrigin)] = chunk;
         }
-    }
 
-    private void OnGetState(EntityUid uid, NavMapComponent component, ref ComponentGetState args)
-    {
-        if (!TryComp<MapGridComponent>(uid, out var mapGrid))
-            return;
+        // Update the tile data based on what entities are still anchored to the tile
+        var enumerator = _mapSystem.GetAnchoredEntitiesEnumerator(uid, mapGrid, tile);
 
-        var data = new Dictionary<Vector2i, int>(component.Chunks.Count);
-        foreach (var (index, chunk) in component.Chunks)
+        while (enumerator.MoveNext(out var ent))
         {
-            data.Add(index, chunk.TileData);
-        }
+            if (!TryComp<AirtightComponent>(ent, out var entAirtight))
+                continue;
 
-        var beaconQuery = AllEntityQuery<NavMapBeaconComponent, TransformComponent>();
-        var beacons = new List<NavMapBeacon>();
+            var category = GetAssociatedEntityChunkType(ent.Value);
 
-        while (beaconQuery.MoveNext(out var beaconUid, out var beacon, out var xform))
-        {
-            if (!beacon.Enabled || xform.GridUid != uid || !CanBeacon(beaconUid, xform))
+            if (!component.Chunks.TryGetValue((category, chunkOrigin), out var chunk))
                 continue;
 
-            // TODO: Make warp points use metadata name instead.
-            string? name = beacon.Text;
-
-            if (string.IsNullOrEmpty(name))
+            foreach (var (direction, _) in chunk.TileData)
             {
-                if (TryComp<WarpPointComponent>(beaconUid, out var warpPoint) && warpPoint.Location != null)
-                {
-                    name = warpPoint.Location;
-                }
-                else
-                {
-                    name = MetaData(beaconUid).EntityName;
-                }
+                if ((direction & entAirtight.AirBlockedDirection) > 0)
+                    chunk.TileData[direction] |= flag;
             }
 
-            beacons.Add(new NavMapBeacon(beacon.Color, name, xform.LocalPosition));
+            chunk.LastUpdate = _gameTiming.CurTick;
+            component.Chunks[(category, chunkOrigin)] = chunk;
         }
 
-        var airlockQuery = EntityQueryEnumerator<NavMapDoorComponent, TransformComponent>();
-        var airlocks = new List<NavMapAirlock>();
-        while (airlockQuery.MoveNext(out _, out _, out var xform))
+        // Remove walls that intersect with doors (unless they can both physically fit on the same tile)
+        if (component.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin), out var wallChunk) &&
+            component.Chunks.TryGetValue((NavMapChunkType.Airlock, chunkOrigin), out var airlockChunk))
         {
-            if (xform.GridUid != uid || !xform.Anchored)
-                continue;
-
-            var pos = _map.TileIndicesFor(uid, mapGrid, xform.Coordinates);
-            var enumerator = _map.GetAnchoredEntitiesEnumerator(uid, mapGrid, pos);
-
-            var wallPresent = false;
-            while (enumerator.MoveNext(out var ent))
+            foreach (var (direction, _) in wallChunk.TileData)
             {
-                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;
-                }
-
-                wallPresent = true;
-                break;
+                var airlockInvFlag = (ushort) ~airlockChunk.TileData[direction];
+                wallChunk.TileData[direction] &= airlockInvFlag;
             }
 
-            if (wallPresent)
-                continue;
-
-            airlocks.Add(new NavMapAirlock(xform.LocalPosition));
+            wallChunk.LastUpdate = _gameTiming.CurTick;
+            component.Chunks[(NavMapChunkType.Wall, chunkOrigin)] = wallChunk;
         }
-
-        // TODO: Diffs
-        args.State = new NavMapComponentState()
-        {
-            TileData = data,
-            Beacons = beacons,
-            Airlocks = airlocks
-        };
     }
 
-    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))
-            {
-                RefreshTile(ev.OldGrid, oldGrid, navMap, chunk, ev.TilePos);
-            }
-        }
+    #endregion
 
-        HandleAnchor(ev.Xform);
-    }
+    #region: Beacon functions
 
-    private void OnAnchorChange(ref AnchorStateChangedEvent ev)
+    private void UpdateNavMapBeaconData(EntityUid uid, NavMapBeaconComponent component, TransformComponent? xform = null)
     {
-        HandleAnchor(ev.Transform);
-    }
-
-    private void HandleAnchor(TransformComponent xform)
-    {
-        if (!TryComp<NavMapComponent>(xform.GridUid, out var navMap) ||
-            !TryComp<MapGridComponent>(xform.GridUid, out var grid))
+        if (!Resolve(uid, ref xform))
             return;
 
-        var tile = grid.LocalToTile(xform.Coordinates);
-        var chunkOrigin = SharedMapSystem.GetChunkIndices(tile, ChunkSize);
-
-        if (!navMap.Chunks.TryGetValue(chunkOrigin, out var chunk))
-        {
-            chunk = new NavMapChunk(chunkOrigin);
-            navMap.Chunks[chunkOrigin] = chunk;
-        }
-
-        RefreshTile(xform.GridUid.Value, grid, navMap, chunk, tile);
-    }
-
-    private void RefreshTile(EntityUid uid, MapGridComponent grid, NavMapComponent component, NavMapChunk chunk, Vector2i tile)
-    {
-        var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
-        var existing = chunk.TileData;
-        var flag = GetFlag(relative);
+        if (xform.GridUid == null)
+            return;
 
-        chunk.TileData &= ~flag;
+        if (!TryComp<NavMapComponent>(xform.GridUid, out var navMap))
+            return;
 
-        var enumerator = grid.GetAnchoredEntitiesEnumerator(tile);
-        // TODO: Use something to get convex poly.
+        var netEnt = GetNetEntity(uid);
+        var oldBeacon = navMap.Beacons.FirstOrNull(x => x.NetEnt == netEnt);
+        var changed = false;
 
-        while (enumerator.MoveNext(out var ent))
+        if (oldBeacon != null)
         {
-            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;
+            navMap.Beacons.Remove(oldBeacon.Value);
+            changed = true;
         }
 
-        if (chunk.TileData == 0)
+        if (TryCreateNavMapBeaconData(uid, component, xform, out var beaconData))
         {
-            component.Chunks.Remove(chunk.Origin);
+            navMap.Beacons.Add(beaconData.Value);
+            changed = true;
         }
 
-        if (existing == chunk.TileData)
-            return;
+        if (changed)
+            Dirty(xform.GridUid.Value, navMap);
+    }
 
-        Dirty(uid, component);
+    private void UpdateBeaconEnabledVisuals(Entity<NavMapBeaconComponent> ent)
+    {
+        _appearance.SetData(ent, NavMapBeaconVisuals.Enabled, ent.Comp.Enabled && Transform(ent).Anchored);
     }
 
     /// <summary>
@@ -389,9 +345,6 @@ public sealed class NavMapSystem : SharedNavMapSystem
 
         comp.Enabled = enabled;
         UpdateBeaconEnabledVisuals((uid, comp));
-        Dirty(uid, comp);
-
-        RefreshNavGrid(uid);
     }
 
     /// <summary>
@@ -419,7 +372,7 @@ public sealed class NavMapSystem : SharedNavMapSystem
         if (!Resolve(ent, ref ent.Comp))
             return false;
 
-        return TryGetNearestBeacon(_transform.GetMapCoordinates(ent, ent.Comp), out beacon, out beaconCoords);
+        return TryGetNearestBeacon(_transformSystem.GetMapCoordinates(ent, ent.Comp), out beacon, out beaconCoords);
     }
 
     /// <summary>
@@ -446,7 +399,7 @@ public sealed class NavMapSystem : SharedNavMapSystem
             if (coordinates.MapId != xform.MapID)
                 continue;
 
-            var coords = _transform.GetWorldPosition(xform);
+            var coords = _transformSystem.GetWorldPosition(xform);
             var distanceSquared = (coordinates.Position - coords).LengthSquared();
             if (!float.IsInfinity(minDistance) && distanceSquared >= minDistance)
                 continue;
@@ -465,7 +418,7 @@ public sealed class NavMapSystem : SharedNavMapSystem
         if (!Resolve(ent, ref ent.Comp))
             return Loc.GetString("nav-beacon-pos-no-beacons");
 
-        return GetNearestBeaconString(_transform.GetMapCoordinates(ent, ent.Comp));
+        return GetNearestBeaconString(_transformSystem.GetMapCoordinates(ent, ent.Comp));
     }
 
     public string GetNearestBeaconString(MapCoordinates coordinates)
@@ -494,11 +447,13 @@ public sealed class NavMapSystem : SharedNavMapSystem
             ? Loc.GetString("nav-beacon-pos-format-direction-mod-far")
             : string.Empty;
 
-        // we can null suppress the text being null because TRyGetNearestVisibleStationBeacon always gives us a beacon with not-null text.
+        // we can null suppress the text being null because TryGetNearestVisibleStationBeacon always gives us a beacon with not-null text.
         return Loc.GetString("nav-beacon-pos-format-direction",
             ("modifier", modifier),
             ("direction", ContentLocalizationManager.FormatDirection(adjustedDir).ToLowerInvariant()),
             ("color", beacon.Value.Comp.Color),
             ("marker", beacon.Value.Comp.Text!));
     }
+
+    #endregion
 }
index 8c9979ba25a818097b9d1e3894e23ec5bb3609f2..61315b3db145248bc74ab11da99031be2a6c8959 100644 (file)
@@ -1,9 +1,12 @@
+using Content.Shared.Atmos;
 using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Timing;
 
 namespace Content.Shared.Pinpointer;
 
 /// <summary>
-/// Used to store grid poly data to be used for UIs.
+/// Used to store grid data to be used for UIs.
 /// </summary>
 [RegisterComponent, NetworkedComponent]
 public sealed partial class NavMapComponent : Component
@@ -12,25 +15,57 @@ public sealed partial class NavMapComponent : Component
      * Don't need DataFields as this can be reconstructed
      */
 
+    /// <summary>
+    /// Bitmasks that represent chunked tiles.
+    /// </summary>
     [ViewVariables]
-    public readonly Dictionary<Vector2i, NavMapChunk> Chunks = new();
+    public Dictionary<(NavMapChunkType, Vector2i), NavMapChunk> Chunks = new();
 
-    [ViewVariables] public readonly List<SharedNavMapSystem.NavMapBeacon> Beacons = new();
-
-    [ViewVariables] public readonly List<SharedNavMapSystem.NavMapAirlock> Airlocks = new();
+    /// <summary>
+    /// List of station beacons.
+    /// </summary>
+    [ViewVariables]
+    public HashSet<SharedNavMapSystem.NavMapBeacon> Beacons = new();
 }
 
+[Serializable, NetSerializable]
 public sealed class NavMapChunk
 {
+    /// <summary>
+    /// The chunk origin
+    /// </summary>
     public readonly Vector2i Origin;
 
     /// <summary>
-    /// Bitmask for tiles, 1 for occupied and 0 for empty.
+    /// Bitmask for tiles, 1 for occupied and 0 for empty. There is a bitmask for each cardinal direction,
+    /// representing each edge of the tile, in case the entities inside it do not entirely fill it
     /// </summary>
-    public int TileData;
+    public Dictionary<AtmosDirection, ushort> TileData;
+
+    /// <summary>
+    /// The last game tick that the chunk was updated
+    /// </summary>
+    [NonSerialized]
+    public GameTick LastUpdate;
 
     public NavMapChunk(Vector2i origin)
     {
         Origin = origin;
+
+        TileData = new()
+        {
+            [AtmosDirection.North] = 0,
+            [AtmosDirection.East] = 0,
+            [AtmosDirection.South] = 0,
+            [AtmosDirection.West] = 0,
+        };
     }
 }
+
+public enum NavMapChunkType : byte
+{
+    Invalid,
+    Floor,
+    Wall,
+    Airlock,
+}
index 17f86ac7e6807210a1a75d3b9e92c660114f4318..ebc4f33f0f1326180fbf99c45fc1d067d36f30ce 100644 (file)
@@ -1,13 +1,38 @@
+using System.Diagnostics.CodeAnalysis;
 using System.Numerics;
+using Content.Shared.Atmos;
+using Content.Shared.Tag;
+using Robust.Shared.GameStates;
 using Robust.Shared.Serialization;
+using Robust.Shared.Timing;
 using Robust.Shared.Utility;
 
 namespace Content.Shared.Pinpointer;
 
 public abstract class SharedNavMapSystem : EntitySystem
 {
+    [Dependency] private readonly TagSystem _tagSystem = default!;
+    [Dependency] private readonly IGameTiming _gameTiming = default!;
+
     public const byte ChunkSize = 4;
 
+    public readonly NavMapChunkType[] EntityChunkTypes =
+    {
+        NavMapChunkType.Invalid,
+        NavMapChunkType.Wall,
+        NavMapChunkType.Airlock,
+    };
+
+    private readonly string[] _wallTags = ["Wall", "Window"];
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        // Data handling events
+        SubscribeLocalEvent<NavMapComponent, ComponentGetState>(OnGetState);
+    }
+
     /// <summary>
     /// Converts the chunk's tile into a bitflag for the slot.
     /// </summary>
@@ -31,19 +56,236 @@ public abstract class SharedNavMapSystem : EntitySystem
         return new Vector2i(x, y);
     }
 
-    [Serializable, NetSerializable]
-    protected sealed class NavMapComponentState : ComponentState
+    public NavMapChunk SetAllEdgesForChunkTile(NavMapChunk chunk, Vector2i tile)
+    {
+        var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
+        var flag = (ushort) GetFlag(relative);
+
+        foreach (var (direction, _) in chunk.TileData)
+            chunk.TileData[direction] |= flag;
+
+        return chunk;
+    }
+
+    public NavMapChunk UnsetAllEdgesForChunkTile(NavMapChunk chunk, Vector2i tile)
+    {
+        var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
+        var flag = (ushort) GetFlag(relative);
+        var invFlag = (ushort) ~flag;
+
+        foreach (var (direction, _) in chunk.TileData)
+            chunk.TileData[direction] &= invFlag;
+
+        return chunk;
+    }
+
+    public ushort GetCombinedEdgesForChunk(Dictionary<AtmosDirection, ushort> tile)
+    {
+        ushort combined = 0;
+
+        foreach (var kvp in tile)
+            combined |= kvp.Value;
+
+        return combined;
+    }
+
+    public bool AllTileEdgesAreOccupied(Dictionary<AtmosDirection, ushort> tileData, Vector2i tile)
+    {
+        var flag = (ushort) GetFlag(tile);
+
+        foreach (var kvp in tileData)
+        {
+            if ((kvp.Value & flag) == 0)
+                return false;
+        }
+
+        return true;
+    }
+
+    public NavMapChunkType GetAssociatedEntityChunkType(EntityUid uid)
+    {
+        var category = NavMapChunkType.Invalid;
+
+        if (HasComp<NavMapDoorComponent>(uid))
+            category = NavMapChunkType.Airlock;
+
+        else if (_tagSystem.HasAnyTag(uid, _wallTags))
+            category = NavMapChunkType.Wall;
+
+        return category;
+    }
+
+    protected bool TryCreateNavMapBeaconData(EntityUid uid, NavMapBeaconComponent component, TransformComponent xform, [NotNullWhen(true)] out NavMapBeacon? beaconData)
     {
-        public Dictionary<Vector2i, int> TileData = new();
+        beaconData = null;
 
-        public List<NavMapBeacon> Beacons = new();
+        if (!component.Enabled || xform.GridUid == null || !xform.Anchored)
+            return false;
 
-        public List<NavMapAirlock> Airlocks = new();
+        string? name = component.Text;
+        var meta = MetaData(uid);
+
+        if (string.IsNullOrEmpty(name))
+            name = meta.EntityName;
+
+        beaconData = new NavMapBeacon(meta.NetEntity, component.Color, name, xform.LocalPosition)
+        {
+            LastUpdate = _gameTiming.CurTick
+        };
+
+        return true;
     }
 
+    #region: Event handling
+
+    private void OnGetState(EntityUid uid, NavMapComponent component, ref ComponentGetState args)
+    {
+        var chunks = new Dictionary<(NavMapChunkType, Vector2i), Dictionary<AtmosDirection, ushort>>();
+        var beacons = new HashSet<NavMapBeacon>();
+
+        // Should this be a full component state or a delta-state?
+        if (args.FromTick <= component.CreationTick)
+        {
+            foreach (var ((category, origin), chunk) in component.Chunks)
+            {
+                var chunkDatum = new Dictionary<AtmosDirection, ushort>(chunk.TileData.Count);
+
+                foreach (var (direction, tileData) in chunk.TileData)
+                    chunkDatum[direction] = tileData;
+
+                chunks.Add((category, origin), chunkDatum);
+            }
+
+            var beaconQuery = AllEntityQuery<NavMapBeaconComponent, TransformComponent>();
+
+            while (beaconQuery.MoveNext(out var beaconUid, out var beacon, out var xform))
+            {
+                if (xform.GridUid != uid)
+                    continue;
+
+                if (!TryCreateNavMapBeaconData(beaconUid, beacon, xform, out var beaconData))
+                    continue;
+
+                beacons.Add(beaconData.Value);
+            }
+
+            args.State = new NavMapComponentState(chunks, beacons);
+            return;
+        }
+
+        foreach (var ((category, origin), chunk) in component.Chunks)
+        {
+            if (chunk.LastUpdate < args.FromTick)
+                continue;
+
+            var chunkDatum = new Dictionary<AtmosDirection, ushort>(chunk.TileData.Count);
+
+            foreach (var (direction, tileData) in chunk.TileData)
+                chunkDatum[direction] = tileData;
+
+            chunks.Add((category, origin), chunkDatum);
+        }
+
+        foreach (var beacon in component.Beacons)
+        {
+            if (beacon.LastUpdate < args.FromTick)
+                continue;
+
+            beacons.Add(beacon);
+        }
+
+        args.State = new NavMapComponentState(chunks, beacons)
+        {
+            AllChunks = new(component.Chunks.Keys),
+            AllBeacons = new(component.Beacons)
+        };
+    }
+
+    #endregion
+
+    #region: System messages
+
     [Serializable, NetSerializable]
-    public readonly record struct NavMapBeacon(Color Color, string Text, Vector2 Position);
+    protected sealed class NavMapComponentState : ComponentState, IComponentDeltaState
+    {
+        public Dictionary<(NavMapChunkType, Vector2i), Dictionary<AtmosDirection, ushort>> Chunks = new();
+        public HashSet<NavMapBeacon> Beacons = new();
+
+        // Required to infer deleted/missing chunks for delta states
+        public HashSet<(NavMapChunkType, Vector2i)>? AllChunks;
+        public HashSet<NavMapBeacon>? AllBeacons;
+
+        public NavMapComponentState(Dictionary<(NavMapChunkType, Vector2i), Dictionary<AtmosDirection, ushort>> chunks, HashSet<NavMapBeacon> beacons)
+        {
+            Chunks = chunks;
+            Beacons = beacons;
+        }
+
+        public bool FullState => (AllChunks == null || AllBeacons == null);
+
+        public void ApplyToFullState(IComponentState fullState)
+        {
+            DebugTools.Assert(!FullState);
+            var state = (NavMapComponentState) fullState;
+            DebugTools.Assert(state.FullState);
+
+            // Update chunks
+            foreach (var key in state.Chunks.Keys)
+            {
+                if (!AllChunks!.Contains(key))
+                    state.Chunks.Remove(key);
+            }
+
+            foreach (var (chunk, data) in Chunks)
+                state.Chunks[chunk] = new(data);
+
+            // Update beacons
+            foreach (var beacon in state.Beacons)
+            {
+                if (!AllBeacons!.Contains(beacon))
+                    state.Beacons.Remove(beacon);
+            }
+
+            foreach (var beacon in Beacons)
+                state.Beacons.Add(beacon);
+        }
+
+        public IComponentState CreateNewFullState(IComponentState fullState)
+        {
+            DebugTools.Assert(!FullState);
+            var state = (NavMapComponentState) fullState;
+            DebugTools.Assert(state.FullState);
+
+            var chunks = new Dictionary<(NavMapChunkType, Vector2i), Dictionary<AtmosDirection, ushort>>();
+            var beacons = new HashSet<NavMapBeacon>();
+
+            foreach (var (chunk, data) in Chunks)
+                chunks[chunk] = new(data);
+
+            foreach (var (chunk, data) in state.Chunks)
+            {
+                if (AllChunks!.Contains(chunk))
+                    chunks.TryAdd(chunk, new(data));
+            }
+
+            foreach (var beacon in Beacons)
+                beacons.Add(new NavMapBeacon(beacon.NetEnt, beacon.Color, beacon.Text, beacon.Position));
+
+            foreach (var beacon in state.Beacons)
+            {
+                if (AllBeacons!.Contains(beacon))
+                    beacons.Add(new NavMapBeacon(beacon.NetEnt, beacon.Color, beacon.Text, beacon.Position));
+            }
+
+            return new NavMapComponentState(chunks, beacons);
+        }
+    }
 
     [Serializable, NetSerializable]
-    public readonly record struct NavMapAirlock(Vector2 Position);
+    public record struct NavMapBeacon(NetEntity NetEnt, Color Color, string Text, Vector2 Position)
+    {
+        public GameTick LastUpdate;
+    }
+
+    #endregion
 }