-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);
}
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);
}
}
using Robust.Shared.Timing;
using System.Numerics;
using JetBrains.Annotations;
+using Content.Shared.Atmos;
+using System.Linq;
namespace Content.Client.Pinpointer.UI;
{
[Dependency] private IResourceCache _cache = default!;
private readonly SharedTransformSystem _transformSystem;
+ private readonly SharedNavMapSystem _navMapSystem;
public EntityUid? Owner;
public EntityUid? MapUid;
// 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);
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;
private readonly Label _zoom = new()
{
VerticalAlignment = VAlignment.Top,
+ HorizontalExpand = true,
Margin = new Thickness(8f, 8f),
};
Text = Loc.GetString("navmap-recenter"),
VerticalAlignment = VAlignment.Top,
HorizontalAlignment = HAlignment.Right,
+ HorizontalExpand = true,
Margin = new Thickness(8f, 4f),
Disabled = true,
};
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,
};
IoCManager.InjectDependencies(this);
_transformSystem = EntManager.System<SharedTransformSystem>();
+ _navMapSystem = EntManager.System<SharedNavMapSystem>();
+
BackgroundColor = Color.FromSrgb(TileColor.WithAlpha(BackgroundOpacity));
RectClipContent = true;
BorderColor = StyleNano.PanelDark
},
VerticalExpand = false,
+ HorizontalExpand = true,
+ SetWidth = 650f,
Children =
{
new BoxContainer()
var topContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
+ HorizontalExpand = true,
Children =
{
topPanel,
{
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();
}
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);
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)
}
// 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)
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)
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);
}
}
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()
Selectable = selectable;
}
}
-
-public struct NavMapLine
-{
- public readonly Vector2 Origin;
- public readonly Vector2 Terminus;
-
- public NavMapLine(Vector2 origin, Vector2 terminus)
- {
- Origin = origin;
- Terminus = terminus;
- }
-}
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;
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)
{
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++)
{
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++)
}
}
- 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);
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;
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
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;
}
}
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;
[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()
{
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)
// 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);
}
}
}
[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);
}
-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;
{
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)
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,
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)
// 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));
}
("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>
comp.Enabled = enabled;
UpdateBeaconEnabledVisuals((uid, comp));
- Dirty(uid, comp);
-
- RefreshNavGrid(uid);
}
/// <summary>
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>
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;
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)
? 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
}
+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
* 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,
+}
+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>
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
}