diff --git a/Content.Client/Pinpointer/NavMapSystem.cs b/Content.Client/Pinpointer/NavMapSystem.cs index bd7dfc1117..868bf1fbc4 100644 --- a/Content.Client/Pinpointer/NavMapSystem.cs +++ b/Content.Client/Pinpointer/NavMapSystem.cs @@ -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(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, - }); - } + if (!state.AllChunks!.Contains(index)) + component.Chunks.Remove(index); + } - 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> _grids = new(); - - 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)); - - _grids.Clear(); - _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids); - - foreach (var grid in _grids) - { - 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 beacon in component.Beacons) { - 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; - - // 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.Comp.TileSize, (tile + 1) * grid.Comp.TileSize), Color.Aqua, false); - } - } + if (!state.AllBeacons!.Contains(beacon)) + component.Beacons.Remove(beacon); } } - args.WorldHandle.SetTransform(Matrix3.Identity); + else + { + foreach (var index in component.Chunks.Keys) + { + if (!state.Chunks.ContainsKey(index)) + component.Chunks.Remove(index); + } + + foreach (var beacon in component.Beacons) + { + if (!state.Beacons.Contains(beacon)) + component.Beacons.Remove(beacon); + } + } + + foreach (var ((category, origin), chunk) in state.Chunks) + { + var newChunk = new NavMapChunk(origin); + + foreach (var (atmosDirection, value) in chunk) + newChunk.TileData[atmosDirection] = value; + + component.Chunks[(category, origin)] = newChunk; + } + + foreach (var beacon in state.Beacons) + component.Beacons.Add(beacon); } } diff --git a/Content.Client/Pinpointer/UI/NavMapControl.cs b/Content.Client/Pinpointer/UI/NavMapControl.cs index 677092e191..3309e7c8df 100644 --- a/Content.Client/Pinpointer/UI/NavMapControl.cs +++ b/Content.Client/Pinpointer/UI/NavMapControl.cs @@ -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 TrackedCoordinates = new(); public Dictionary TrackedEntities = new(); - public Dictionary>? 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 _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(); + _navMapSystem = EntManager.System(); + 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 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(); + var lines = new ValueList(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; + var origin = ScalePosition(o - offsetVec); + var terminus = ScalePosition(t - offsetVec); - if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right) - continue; - - if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top) - continue; - - 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(); - 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); + var rects = new ValueList(TileRects.Count * 8); - 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); - } - - 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("/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>(); - 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); + var scalingCoefficient = MinmapScaleModifier * float.Sqrt(MinimapScale); + var positionOffset = new Vector2(scalingCoefficient * blip.Texture.Width, scalingCoefficient * blip.Texture.Height); - 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))); + handle.DrawTextureRect(blip.Texture, new UIBox2(position - positionOffset, position + positionOffset), blip.Color); } - - iconVertexUVs[(blip.Texture, blip.Color)] = vertexUVs; - } - - foreach ((var (texture, color), var vertexUVs) in iconVertexUVs) - { - if (!_sRGBLookUp.TryGetValue(color, out var sRGB)) - { - sRGB = Color.ToSrgb(color); - _sRGBLookUp[color] = sRGB; - } - - handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, texture, vertexUVs.Span, sRGB); } } @@ -469,124 +446,294 @@ public partial class NavMapControl : MapGridControl } protected virtual void UpdateNavMap() + { + // Clear stale values + TilePolygons.Clear(); + TileLines.Clear(); + TileRects.Clear(); + + UpdateNavMapFloorTiles(); + UpdateNavMapWallLines(); + UpdateNavMapAirlocks(); + } + + private void UpdateNavMapFloorTiles() + { + if (_fixtures == null) + return; + + 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)); + } + } + + private void UpdateNavMapWallLines() { if (_navMap == null || _grid == null) return; - TileGrid = GetDecodedWallChunks(_navMap.Chunks, _grid); - } + // We'll use the following dictionaries to combine collinear wall lines + HorizLinesLookup.Clear(); + HorizLinesLookupReversed.Clear(); + VertLinesLookup.Clear(); + VertLinesLookupReversed.Clear(); - public Dictionary> GetDecodedWallChunks - (Dictionary chunks, - MapGridComponent grid) - { - var decodedOutput = new Dictionary>(); - - foreach ((var chunkOrigin, var chunk) in chunks) + foreach ((var (category, chunkOrigin), var chunk) in _navMap.Chunks) { - var list = new List(); + 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) - { - // Add point - list.Add(new NavMapLine(position, position + new Vector2(0f, -grid.TileSize))); - } + AddOrUpdateNavMapLine(tile + new Vector2i(0, -_grid.TileSize), tile, VertLinesLookup, VertLinesLookupReversed); - // Draw a diagonal line for interiors. - list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, 0f))); + // 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))); } - - decodedOutput.Add(chunkOrigin, list); } - return decodedOutput; + // 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)) + { + AddRectForThinAirlock(chunk.TileData, tile); + continue; + } + + // 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))); + } + } + } + + private void AddRectForThinWall(Dictionary 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 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); + } + + 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; - } -} diff --git a/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs b/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs index 902d6bb7e6..3d94318be8 100644 --- a/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs +++ b/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs @@ -23,8 +23,8 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl public PowerMonitoringCableNetworksComponent? PowerMonitoringCableNetworks; public List HiddenLineGroups = new(); - public Dictionary>? PowerCableNetwork; - public Dictionary>? FocusCableNetwork; + public List PowerCableNetwork = new(); + public List FocusCableNetwork = new(); private MapGridComponent? _grid; @@ -48,15 +48,15 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl if (!_entManager.TryGetComponent(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> fullCableNetwork, Color modulator) + public void DrawCableNetwork(DrawingHandleScreen handle, List 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[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; + var cableOffset = _powerCableOffsets[(int) line.Group]; + var start = ScalePosition(line.Origin + cableOffset - offset); + var end = ScalePosition(line.Terminus + cableOffset - offset); - 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)); - - 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[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; + var cableOffset = _powerCableOffsets[(int) line.Group]; - foreach (var chunkedLine in chunkedLines) - { - if (HiddenLineGroups.Contains(chunkedLine.Group)) - continue; + 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 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(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(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(line.Origin.X, line.Terminus.X) - 0.1f, + Math.Max(line.Origin.Y, line.Terminus.Y) + 0.1f) + + cableOffset - offset); - 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(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(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); - } + 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>? GetDecodedPowerCableChunks(Dictionary? chunks, MapGridComponent? grid) + public List GetDecodedPowerCableChunks(Dictionary? chunks) { - if (chunks == null || grid == null) - return null; + var decodedOutput = new List(); - var decodedOutput = new Dictionary>(); + if (!_entManager.TryGetComponent(MapUid, out _grid)) + return decodedOutput; + + if (chunks == null) + return decodedOutput; + + // 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(); - 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; } } diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml.cs b/Content.Client/Power/PowerMonitoringWindow.xaml.cs index edc0eaa18a..81fe1f4d04 100644 --- a/Content.Client/Power/PowerMonitoringWindow.xaml.cs +++ b/Content.Client/Power/PowerMonitoringWindow.xaml.cs @@ -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; diff --git a/Content.Server/Atmos/EntitySystems/AirtightSystem.cs b/Content.Server/Atmos/EntitySystems/AirtightSystem.cs index 152fba8fc4..60d90e1e60 100644 --- a/Content.Server/Atmos/EntitySystems/AirtightSystem.cs +++ b/Content.Server/Atmos/EntitySystems/AirtightSystem.cs @@ -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); } diff --git a/Content.Server/Pinpointer/NavMapSystem.cs b/Content.Server/Pinpointer/NavMapSystem.cs index 2a5639886e..34c76a1320 100644 --- a/Content.Server/Pinpointer/NavMapSystem.cs +++ b/Content.Server/Pinpointer/NavMapSystem.cs @@ -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; /// /// Handles data to be used for in-grid map displays. /// -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 _physicsQuery; - private EntityQuery _tagQuery; + [Dependency] private readonly IGameTiming _gameTiming = default!; public const float CloseDistance = 15f; public const float FarDistance = 30f; @@ -39,153 +36,34 @@ public sealed class NavMapSystem : SharedNavMapSystem { base.Initialize(); - _physicsQuery = GetEntityQuery(); - _tagQuery = GetEntityQuery(); - - SubscribeLocalEvent(OnAnchorChange); - SubscribeLocalEvent(OnReAnchor); + // Initialization events SubscribeLocalEvent(OnStationInit); - SubscribeLocalEvent(OnNavMapStartup); - SubscribeLocalEvent(OnGetState); + + // Grid change events SubscribeLocalEvent(OnNavMapSplit); + SubscribeLocalEvent(OnTileChanged); + // Airtight structure change event + SubscribeLocalEvent(OnAirtightChanged); + + // Beacon events SubscribeLocalEvent(OnNavMapBeaconMapInit); - SubscribeLocalEvent(OnNavMapBeaconStartup); SubscribeLocalEvent(OnNavMapBeaconAnchor); - - SubscribeLocalEvent(OnNavMapDoorStartup); - SubscribeLocalEvent(OnNavMapDoorAnchor); - SubscribeLocalEvent(OnConfigureMessage); SubscribeLocalEvent(OnConfigurableMapInit); SubscribeLocalEvent(OnConfigurableExamined); } + #region: Initialization event handling private void OnStationInit(StationGridAddedEvent ev) { var comp = EnsureComp(ev.GridId); RefreshGrid(ev.GridId, comp, Comp(ev.GridId)); } - private void OnNavMapBeaconMapInit(EntityUid uid, NavMapBeaconComponent component, MapInitEvent args) - { - if (component.DefaultText == null || component.Text != null) - return; + #endregion - component.Text = Loc.GetString(component.DefaultText); - Dirty(uid, component); - RefreshNavGrid(uid); - } - - private void OnNavMapBeaconStartup(EntityUid uid, NavMapBeaconComponent component, ComponentStartup args) - { - RefreshNavGrid(uid); - } - - private void OnNavMapBeaconAnchor(EntityUid uid, NavMapBeaconComponent component, ref AnchorStateChangedEvent args) - { - UpdateBeaconEnabledVisuals((uid, component)); - RefreshNavGrid(uid); - } - - private void OnNavMapDoorStartup(Entity ent, ref ComponentStartup args) - { - RefreshNavGrid(ent); - } - - private void OnNavMapDoorAnchor(Entity ent, ref AnchorStateChangedEvent args) - { - RefreshNavGrid(ent); - } - - private void OnConfigureMessage(Entity ent, ref NavMapBeaconConfigureBuiMessage args) - { - if (args.Session.AttachedEntity is not { } user) - return; - - if (!TryComp(ent, out var navMap)) - return; - - if (navMap.Text == args.Text && - navMap.Color == args.Color && - navMap.Enabled == args.Enabled) - return; - - _adminLog.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(user):player} configured NavMapBeacon \'{ToPrettyString(ent):entity}\' with text \'{args.Text}\', color {args.Color.ToHexNoAlpha()}, and {(args.Enabled ? "enabled" : "disabled")} it."); - - if (TryComp(ent, out var warpPoint)) - { - warpPoint.Location = args.Text; - } - - navMap.Text = args.Text; - navMap.Color = args.Color; - navMap.Enabled = args.Enabled; - UpdateBeaconEnabledVisuals((ent, navMap)); - Dirty(ent, navMap); - RefreshNavGrid(ent); - } - - private void OnConfigurableMapInit(Entity ent, ref MapInitEvent args) - { - if (!TryComp(ent, out var navMap)) - return; - - // We set this on mapinit just in case the text was edited via VV or something. - if (TryComp(ent, out var warpPoint)) - { - warpPoint.Location = navMap.Text; - } - - UpdateBeaconEnabledVisuals((ent, navMap)); - } - - private void OnConfigurableExamined(Entity ent, ref ExaminedEvent args) - { - if (!args.IsInDetailsRange || !TryComp(ent, out var navMap)) - return; - - args.PushMarkup(Loc.GetString("nav-beacon-examine-text", - ("enabled", navMap.Enabled), - ("color", navMap.Color.ToHexNoAlpha()), - ("label", navMap.Text ?? string.Empty))); - } - - private void UpdateBeaconEnabledVisuals(Entity ent) - { - _appearance.SetData(ent, NavMapBeaconVisuals.Enabled, ent.Comp.Enabled && Transform(ent).Anchored); - } - - /// - /// Refreshes the grid for the corresponding beacon. - /// - /// - private void RefreshNavGrid(EntityUid uid) - { - var xform = Transform(uid); - - if (!TryComp(xform.GridUid, out var navMap)) - return; - - Dirty(xform.GridUid.Value, navMap); - } - - private bool CanBeacon(EntityUid uid, TransformComponent? xform = null) - { - if (!Resolve(uid, ref xform)) - return false; - - return xform.GridUid != null && xform.Anchored; - } - - private void OnNavMapStartup(EntityUid uid, NavMapComponent component, ComponentStartup args) - { - if (!TryComp(uid, out var grid)) - return; - - RefreshGrid(uid, component, grid); - } + #region: Grid change event handling private void OnNavMapSplit(ref GridSplitEvent args) { @@ -203,180 +81,258 @@ public sealed class NavMapSystem : SharedNavMapSystem RefreshGrid(args.Grid, comp, gridQuery.GetComponent(args.Grid)); } - private void RefreshGrid(EntityUid uid, NavMapComponent component, MapGridComponent grid) + private void OnTileChanged(ref TileChangedEvent ev) { - 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); - component.Chunks[chunkOrigin] = chunk; - } - - RefreshTile(uid, grid, component, chunk, tile.Value.GridIndices); - } - } - - private void OnGetState(EntityUid uid, NavMapComponent component, ref ComponentGetState args) - { - if (!TryComp(uid, out var mapGrid)) + if (!TryComp(ev.NewTile.GridUid, out var navMap)) return; - var data = new Dictionary(component.Chunks.Count); - foreach (var (index, chunk) in component.Chunks) - { - data.Add(index, chunk.TileData); - } - - var beaconQuery = AllEntityQuery(); - var beacons = new List(); - - while (beaconQuery.MoveNext(out var beaconUid, out var beacon, out var xform)) - { - if (!beacon.Enabled || xform.GridUid != uid || !CanBeacon(beaconUid, xform)) - continue; - - // TODO: Make warp points use metadata name instead. - string? name = beacon.Text; - - if (string.IsNullOrEmpty(name)) - { - if (TryComp(beaconUid, out var warpPoint) && warpPoint.Location != null) - { - name = warpPoint.Location; - } - else - { - name = MetaData(beaconUid).EntityName; - } - } - - beacons.Add(new NavMapBeacon(beacon.Color, name, xform.LocalPosition)); - } - - var airlockQuery = EntityQueryEnumerator(); - var airlocks = new List(); - while (airlockQuery.MoveNext(out _, out _, out var xform)) - { - 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)) - { - 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; - } - - if (wallPresent) - continue; - - airlocks.Add(new NavMapAirlock(xform.LocalPosition)); - } - - // TODO: Diffs - args.State = new NavMapComponentState() - { - TileData = data, - Beacons = beacons, - Airlocks = airlocks - }; - } - - 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)) - { - RefreshTile(ev.OldGrid, oldGrid, navMap, chunk, ev.TilePos); - } - } - - 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 tile = ev.NewTile.GridIndices; var chunkOrigin = SharedMapSystem.GetChunkIndices(tile, ChunkSize); - if (!navMap.Chunks.TryGetValue(chunkOrigin, out var chunk)) - { - chunk = new NavMapChunk(chunkOrigin); - navMap.Chunks[chunkOrigin] = chunk; - } + if (!navMap.Chunks.TryGetValue((NavMapChunkType.Floor, chunkOrigin), out var chunk)) + chunk = new(chunkOrigin); - RefreshTile(xform.GridUid.Value, grid, navMap, chunk, tile); + // 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 RefreshTile(EntityUid uid, MapGridComponent grid, NavMapComponent component, NavMapChunk chunk, Vector2i tile) + private void OnAirtightChanged(ref AirtightChanged ev) + { + var gridUid = ev.Position.Grid; + + if (!TryComp(gridUid, out var navMap) || + !TryComp(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); + } + + #endregion + + #region: Beacon event handling + + private void OnNavMapBeaconMapInit(EntityUid uid, NavMapBeaconComponent component, MapInitEvent args) + { + if (component.DefaultText == null || component.Text != null) + return; + + component.Text = Loc.GetString(component.DefaultText); + Dirty(uid, component); + + UpdateNavMapBeaconData(uid, component); + } + + private void OnNavMapBeaconAnchor(EntityUid uid, NavMapBeaconComponent component, ref AnchorStateChangedEvent args) + { + UpdateBeaconEnabledVisuals((uid, component)); + UpdateNavMapBeaconData(uid, component); + } + + private void OnConfigureMessage(Entity ent, ref NavMapBeaconConfigureBuiMessage args) + { + if (args.Session.AttachedEntity is not { } user) + return; + + if (!TryComp(ent, out var beacon)) + return; + + if (beacon.Text == args.Text && + beacon.Color == args.Color && + beacon.Enabled == args.Enabled) + return; + + _adminLog.Add(LogType.Action, LogImpact.Medium, + $"{ToPrettyString(user):player} configured NavMapBeacon \'{ToPrettyString(ent):entity}\' with text \'{args.Text}\', color {args.Color.ToHexNoAlpha()}, and {(args.Enabled ? "enabled" : "disabled")} it."); + + if (TryComp(ent, out var warpPoint)) + { + warpPoint.Location = args.Text; + } + + beacon.Text = args.Text; + beacon.Color = args.Color; + beacon.Enabled = args.Enabled; + + UpdateBeaconEnabledVisuals((ent, beacon)); + UpdateNavMapBeaconData(ent, beacon); + } + + private void OnConfigurableMapInit(Entity ent, ref MapInitEvent args) + { + if (!TryComp(ent, out var navMap)) + return; + + // We set this on mapinit just in case the text was edited via VV or something. + if (TryComp(ent, out var warpPoint)) + warpPoint.Location = navMap.Text; + + UpdateBeaconEnabledVisuals((ent, navMap)); + } + + private void OnConfigurableExamined(Entity ent, ref ExaminedEvent args) + { + if (!args.IsInDetailsRange || !TryComp(ent, out var navMap)) + return; + + args.PushMarkup(Loc.GetString("nav-beacon-examine-text", + ("enabled", navMap.Enabled), + ("color", navMap.Color.ToHexNoAlpha()), + ("label", navMap.Text ?? string.Empty))); + } + + #endregion + + #region: Grid functions + + private void RefreshGrid(EntityUid uid, NavMapComponent component, MapGridComponent mapGrid) + { + // Clear stale data + component.Chunks.Clear(); + component.Beacons.Clear(); + + // Loop over all tiles + var tileRefs = _mapSystem.GetAllTiles(uid, mapGrid); + + foreach (var tileRef in tileRefs) + { + var tile = tileRef.GridIndices; + var chunkOrigin = SharedMapSystem.GetChunkIndices(tile, ChunkSize); + + if (!component.Chunks.TryGetValue((NavMapChunkType.Floor, chunkOrigin), out var chunk)) + chunk = new(chunkOrigin); + + chunk.LastUpdate = _gameTiming.CurTick; + + // Refresh the floor tile + component.Chunks[(NavMapChunkType.Floor, chunkOrigin)] = SetAllEdgesForChunkTile(chunk, tile); + + // Refresh the contents of the tile + RefreshTileEntityContents(uid, component, mapGrid, chunkOrigin, tile); + } + + Dirty(uid, component); + } + + private void RefreshTileEntityContents(EntityUid uid, NavMapComponent component, MapGridComponent mapGrid, Vector2i chunkOrigin, Vector2i tile) { var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize); - var existing = chunk.TileData; - var flag = GetFlag(relative); + var flag = (ushort) GetFlag(relative); + var invFlag = (ushort) ~flag; - chunk.TileData &= ~flag; + // Clear stale data from the tile across all entity associated chunks + foreach (var category in EntityChunkTypes) + { + if (!component.Chunks.TryGetValue((category, chunkOrigin), out var chunk)) + chunk = new(chunkOrigin); - var enumerator = grid.GetAnchoredEntitiesEnumerator(tile); - // TODO: Use something to get convex poly. + foreach (var (direction, _) in chunk.TileData) + chunk.TileData[direction] &= invFlag; + + chunk.LastUpdate = _gameTiming.CurTick; + component.Chunks[(category, chunkOrigin)] = chunk; + } + + // Update the tile data based on what entities are still anchored to the tile + var enumerator = _mapSystem.GetAnchoredEntitiesEnumerator(uid, mapGrid, tile); 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)) - { + if (!TryComp(ent, out var entAirtight)) continue; + + var category = GetAssociatedEntityChunkType(ent.Value); + + if (!component.Chunks.TryGetValue((category, chunkOrigin), out var chunk)) + continue; + + foreach (var (direction, _) in chunk.TileData) + { + if ((direction & entAirtight.AirBlockedDirection) > 0) + chunk.TileData[direction] |= flag; } - chunk.TileData |= flag; - break; + chunk.LastUpdate = _gameTiming.CurTick; + component.Chunks[(category, chunkOrigin)] = chunk; } - if (chunk.TileData == 0) + // 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)) { - component.Chunks.Remove(chunk.Origin); - } + foreach (var (direction, _) in wallChunk.TileData) + { + var airlockInvFlag = (ushort) ~airlockChunk.TileData[direction]; + wallChunk.TileData[direction] &= airlockInvFlag; + } - if (existing == chunk.TileData) + wallChunk.LastUpdate = _gameTiming.CurTick; + component.Chunks[(NavMapChunkType.Wall, chunkOrigin)] = wallChunk; + } + } + + #endregion + + #region: Beacon functions + + private void UpdateNavMapBeaconData(EntityUid uid, NavMapBeaconComponent component, TransformComponent? xform = null) + { + if (!Resolve(uid, ref xform)) return; - Dirty(uid, component); + if (xform.GridUid == null) + return; + + if (!TryComp(xform.GridUid, out var navMap)) + return; + + var netEnt = GetNetEntity(uid); + var oldBeacon = navMap.Beacons.FirstOrNull(x => x.NetEnt == netEnt); + var changed = false; + + if (oldBeacon != null) + { + navMap.Beacons.Remove(oldBeacon.Value); + changed = true; + } + + if (TryCreateNavMapBeaconData(uid, component, xform, out var beaconData)) + { + navMap.Beacons.Add(beaconData.Value); + changed = true; + } + + if (changed) + Dirty(xform.GridUid.Value, navMap); + } + + private void UpdateBeaconEnabledVisuals(Entity ent) + { + _appearance.SetData(ent, NavMapBeaconVisuals.Enabled, ent.Comp.Enabled && Transform(ent).Anchored); } /// @@ -389,9 +345,6 @@ public sealed class NavMapSystem : SharedNavMapSystem comp.Enabled = enabled; UpdateBeaconEnabledVisuals((uid, comp)); - Dirty(uid, comp); - - RefreshNavGrid(uid); } /// @@ -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); } /// @@ -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 } diff --git a/Content.Shared/Pinpointer/NavMapComponent.cs b/Content.Shared/Pinpointer/NavMapComponent.cs index 8c9979ba25..61315b3db1 100644 --- a/Content.Shared/Pinpointer/NavMapComponent.cs +++ b/Content.Shared/Pinpointer/NavMapComponent.cs @@ -1,9 +1,12 @@ +using Content.Shared.Atmos; using Robust.Shared.GameStates; +using Robust.Shared.Serialization; +using Robust.Shared.Timing; namespace Content.Shared.Pinpointer; /// -/// Used to store grid poly data to be used for UIs. +/// Used to store grid data to be used for UIs. /// [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 */ + /// + /// Bitmasks that represent chunked tiles. + /// [ViewVariables] - public readonly Dictionary Chunks = new(); + public Dictionary<(NavMapChunkType, Vector2i), NavMapChunk> Chunks = new(); - [ViewVariables] public readonly List Beacons = new(); - - [ViewVariables] public readonly List Airlocks = new(); + /// + /// List of station beacons. + /// + [ViewVariables] + public HashSet Beacons = new(); } +[Serializable, NetSerializable] public sealed class NavMapChunk { + /// + /// The chunk origin + /// public readonly Vector2i Origin; /// - /// 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 /// - public int TileData; + public Dictionary TileData; + + /// + /// The last game tick that the chunk was updated + /// + [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, +} diff --git a/Content.Shared/Pinpointer/SharedNavMapSystem.cs b/Content.Shared/Pinpointer/SharedNavMapSystem.cs index 17f86ac7e6..ebc4f33f0f 100644 --- a/Content.Shared/Pinpointer/SharedNavMapSystem.cs +++ b/Content.Shared/Pinpointer/SharedNavMapSystem.cs @@ -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(OnGetState); + } + /// /// Converts the chunk's tile into a bitflag for the slot. /// @@ -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) { - public Dictionary TileData = new(); + var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize); + var flag = (ushort) GetFlag(relative); - public List Beacons = new(); + foreach (var (direction, _) in chunk.TileData) + chunk.TileData[direction] |= flag; - public List Airlocks = new(); + 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 tile) + { + ushort combined = 0; + + foreach (var kvp in tile) + combined |= kvp.Value; + + return combined; + } + + public bool AllTileEdgesAreOccupied(Dictionary 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(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) + { + beaconData = null; + + if (!component.Enabled || xform.GridUid == null || !xform.Anchored) + return false; + + 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>(); + var beacons = new HashSet(); + + // 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(chunk.TileData.Count); + + foreach (var (direction, tileData) in chunk.TileData) + chunkDatum[direction] = tileData; + + chunks.Add((category, origin), chunkDatum); + } + + var beaconQuery = AllEntityQuery(); + + 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(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] + protected sealed class NavMapComponentState : ComponentState, IComponentDeltaState + { + public Dictionary<(NavMapChunkType, Vector2i), Dictionary> Chunks = new(); + public HashSet Beacons = new(); + + // Required to infer deleted/missing chunks for delta states + public HashSet<(NavMapChunkType, Vector2i)>? AllChunks; + public HashSet? AllBeacons; + + public NavMapComponentState(Dictionary<(NavMapChunkType, Vector2i), Dictionary> chunks, HashSet 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>(); + var beacons = new HashSet(); + + 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 NavMapBeacon(Color Color, string Text, Vector2 Position); + public record struct NavMapBeacon(NetEntity NetEnt, Color Color, string Text, Vector2 Position) + { + public GameTick LastUpdate; + } - [Serializable, NetSerializable] - public readonly record struct NavMapAirlock(Vector2 Position); + #endregion }