diff --git a/Robust.Server/Physics/GridFixtureSystem.cs b/Robust.Server/Physics/GridFixtureSystem.cs index 63cec1e2d3a..8059b925242 100644 --- a/Robust.Server/Physics/GridFixtureSystem.cs +++ b/Robust.Server/Physics/GridFixtureSystem.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -49,6 +50,9 @@ public sealed partial class GridFixtureSystem : SharedGridFixtureSystem private HashSet _entSet = new(); + private readonly Queue _splitFrontier = new(4); + private readonly List> _splitGrids = new(1); + private EntityQuery _gridQuery; private EntityQuery _bodyQuery; private EntityQuery _xformQuery; @@ -194,11 +198,10 @@ public void CheckSplits(EntityUid uid) /// /// Check for splits on the specified nodes. /// - private void CheckSplits(EntityUid uid, HashSet dirtyNodes) + private void CheckSplits(EntityUid uid, HashSet dirtyNodes, MapGridComponent? grid = null) { - // TODO: We already have mapgrid elsewhere if (_isSplitting || !SplitAllowed || - !TryComp(uid, out var grid) || + !Resolve(uid, ref grid, false) || !grid.CanSplit) { return; @@ -206,8 +209,8 @@ private void CheckSplits(EntityUid uid, HashSet dirtyNodes) _isSplitting = true; Log.Debug($"Started split check for {ToPrettyString(uid)}"); - var splitFrontier = new Queue(4); - var grids = new List>(1); + _splitFrontier.Clear(); + _splitGrids.Clear(); while (dirtyNodes.Count > 0) { @@ -215,13 +218,13 @@ private void CheckSplits(EntityUid uid, HashSet dirtyNodes) originEnumerator.MoveNext(); var origin = originEnumerator.Current; originEnumerator.Dispose(); - splitFrontier.Enqueue(origin); + _splitFrontier.Enqueue(origin); var foundSplits = new HashSet { origin }; - while (splitFrontier.TryDequeue(out var split)) + while (_splitFrontier.TryDequeue(out var split)) { dirtyNodes.Remove(split); @@ -229,14 +232,15 @@ private void CheckSplits(EntityUid uid, HashSet dirtyNodes) { if (!foundSplits.Add(neighbor)) continue; - splitFrontier.Enqueue(neighbor); + _splitFrontier.Enqueue(neighbor); } } - grids.Add(foundSplits); + _splitGrids.Add(foundSplits); } - var oldGrid = Comp(uid); + var grids = _splitGrids; + var oldGrid = grid; var oldGridUid = uid; // Split time @@ -248,14 +252,20 @@ private void CheckSplits(EntityUid uid, HashSet dirtyNodes) // We'll leave the biggest group as the original grid // anything smaller gets split off. - grids.Sort((x, y) => - x.Sum(o => o.Indices.Count) - .CompareTo(y.Sum(o => o.Indices.Count))); + var gridSizes = new Dictionary, int>(grids.Count); + foreach (var sizeGroup in grids) + { + var tileCount = 0; + foreach (var sizeNode in sizeGroup) + tileCount += sizeNode.Indices.Count; + gridSizes[sizeGroup] = tileCount; + } + grids.Sort((x, y) => gridSizes[x].CompareTo(gridSizes[y])); var oldGridXform = _xformQuery.GetComponent(oldGridUid); var (gridPos, gridRot) = _xformSystem.GetWorldPositionRotation(oldGridXform); var mapBody = _bodyQuery.GetComponent(oldGridUid); - var oldGridComp = _gridQuery.GetComponent(oldGridUid); + var oldGridComp = grid; var newGrids = new EntityUid[grids.Count - 1]; var mapId = oldGridXform.MapID; @@ -274,7 +284,7 @@ private void CheckSplits(EntityUid uid, HashSet dirtyNodes) _physics.SetAngularVelocity(newGridUid, mapBody.AngularVelocity, body: splitBody); var gridComp = _gridQuery.GetComponent(newGridUid); - var tileData = new List<(Vector2i GridIndices, Tile Tile)>(group.Sum(o => o.Indices.Count)); + var tileData = new List<(Vector2i GridIndices, Tile Tile)>(gridSizes[group]); // Gather all tiles up front and set once to minimise fixture change events foreach (var node in group) @@ -320,25 +330,38 @@ private void CheckSplits(EntityUid uid, HashSet dirtyNodes) // Update lookup ents // Needs to be done before setting old tiles as they will be re-parented to the map. - // TODO: Combine tiles into larger rectangles or something; this is gonna be the killer bit. + // Build tile positions and union bounds so we can query once per node. + var tilePositions = new HashSet(node.Indices.Count); + var nodeBounds = new Box2(); + var first = true; + foreach (var tile in node.Indices) { var tilePos = offset + tile; - var bounds = _lookup.GetLocalBounds(tilePos, oldGrid.TileSize); + tilePositions.Add(tilePos); + var tileBounds = _lookup.GetLocalBounds(tilePos, oldGrid.TileSize); + nodeBounds = first ? tileBounds : nodeBounds.Union(tileBounds); + first = false; + } - _entSet.Clear(); - _lookup.GetLocalEntitiesIntersecting(oldGridUid, tilePos, _entSet, 0f, LookupFlags.All | ~LookupFlags.Uncontained | LookupFlags.Approximate); + _entSet.Clear(); + _lookup.GetLocalEntitiesIntersecting(oldGridUid, nodeBounds, _entSet, LookupFlags.All | ~LookupFlags.Uncontained | LookupFlags.Approximate); - foreach (var ent in _entSet) - { - // Consider centre of entity position maybe? - var entXform = _xformQuery.GetComponent(ent); + foreach (var ent in _entSet) + { + var entXform = _xformQuery.GetComponent(ent); - if (entXform.ParentUid != oldGridUid || - !bounds.Contains(entXform.LocalPosition)) continue; + if (entXform.ParentUid != oldGridUid) + continue; - _xformSystem.SetParent(ent, entXform, newGridUid, _xformQuery, newGridXform); - } + var entTile = new Vector2i( + (int) Math.Floor(entXform.LocalPosition.X / oldGrid.TileSize), + (int) Math.Floor(entXform.LocalPosition.Y / oldGrid.TileSize)); + + if (!tilePositions.Contains(entTile)) + continue; + + _xformSystem.SetParent(ent, entXform, newGridUid, _xformQuery, newGridXform); } _nodes[oldGridUid][node.Group.Chunk.Indices].Nodes.Remove(node); @@ -404,13 +427,14 @@ private ChunkNodeGroup CreateNodes(EntityUid gridEuid, MapGridComponent grid, Ma Chunk = chunk, }; - var tiles = new HashSet(chunk.ChunkSize * chunk.ChunkSize); + var tiles = new HashSet(chunk.FilledTiles); for (var x = 0; x < chunk.ChunkSize; x++) { for (var y = 0; y < chunk.ChunkSize; y++) { - tiles.Add(new Vector2i(x, y)); + if (!chunk.GetTile((ushort) x, (ushort) y).IsEmpty) + tiles.Add(new Vector2i(x, y)); } } @@ -469,11 +493,14 @@ private ChunkNodeGroup CreateNodes(EntityUid gridEuid, MapGridComponent grid, Ma // Check each tile for node neighbours on other chunks (not possible for us to have neighbours on the same chunk // as they would already be in our node). - // TODO: This could be better (maybe only check edges of the chunk or something). foreach (var chunkNode in group.Nodes) { foreach (var index in chunkNode.Indices) { + if (index.X != 0 && index.Y != 0 && + index.X != chunk.ChunkSize - 1 && index.Y != chunk.ChunkSize - 1) + continue; + // Check for edge tiles. if (index.X == 0) { @@ -527,7 +554,7 @@ private ChunkNodeGroup CreateNodes(EntityUid gridEuid, MapGridComponent grid, Ma /// /// Checks for grid split with 1 chunk updated. /// - internal override void CheckSplit(EntityUid gridEuid, MapChunk chunk, List rectangles) + internal override void CheckSplit(EntityUid gridEuid, MapChunk chunk, List rectangles, MapGridComponent? grid = null) { HashSet nodes; @@ -537,16 +564,16 @@ internal override void CheckSplit(EntityUid gridEuid, MapChunk chunk, List /// Checks for grid split with many chunks updated. /// - internal override void CheckSplit(EntityUid gridEuid, Dictionary> mapChunks, List removedChunks) + internal override void CheckSplit(EntityUid gridEuid, Dictionary> mapChunks, List removedChunks, MapGridComponent? grid = null) { var nodes = new HashSet(); @@ -557,7 +584,7 @@ internal override void CheckSplit(EntityUid gridEuid, Dictionary(); @@ -576,7 +603,7 @@ internal override void CheckSplit(EntityUid gridEuid, Dictionary @@ -584,10 +611,10 @@ internal override void CheckSplit(EntityUid gridEuid, Dictionary private HashSet RemoveSplitNode(EntityUid gridEuid, MapChunk chunk) { - var dirtyNodes = new HashSet(); - if (_isSplitting) return new HashSet(); + var dirtyNodes = new HashSet(); + Cleanup(gridEuid, chunk, dirtyNodes); DebugTools.Assert(dirtyNodes.All(o => o.Group.Chunk != chunk)); return dirtyNodes; @@ -596,7 +623,7 @@ private HashSet RemoveSplitNode(EntityUid gridEuid, MapChunk chu /// /// Re-adds this chunk to nodes and dirties its neighbours and itself. /// - private HashSet GenerateSplitNode(EntityUid gridEuid, MapChunk chunk) + private HashSet GenerateSplitNode(EntityUid gridEuid, MapChunk chunk, MapGridComponent? grid = null) { var dirtyNodes = RemoveSplitNode(gridEuid, chunk); @@ -604,7 +631,7 @@ private HashSet GenerateSplitNode(EntityUid gridEuid, MapChunk c DebugTools.Assert(chunk.FilledTiles > 0); - var grid = Comp(gridEuid); + grid ??= Comp(gridEuid); var group = CreateNodes(gridEuid, grid, chunk); _nodes[gridEuid][chunk.Indices] = group; @@ -714,7 +741,7 @@ public bool MoveNext([NotNullWhen(true)] out Vector2i? neighbor) neighbor = new Vector2i(_index.X + 1, _index.Y); return true; case 2: - if (_index.Y == _chunk.ChunkSize + 1) break; + if (_index.Y == _chunk.ChunkSize - 1) break; neighbor = new Vector2i(_index.X, _index.Y + 1); return true; case 3: diff --git a/Robust.Shared/GameObjects/Systems/SharedGridFixtureSystem.cs b/Robust.Shared/GameObjects/Systems/SharedGridFixtureSystem.cs index 6e9532392fd..f68b90f7f63 100644 --- a/Robust.Shared/GameObjects/Systems/SharedGridFixtureSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedGridFixtureSystem.cs @@ -44,7 +44,7 @@ public override void Initialize() private void OnGridBoundsRegenerate(ref RegenerateGridBoundsEvent ev) { - RegenerateCollision(ev.Entity, ev.ChunkRectangles, ev.RemovedChunks); + RegenerateCollision(ev.Entity, ev.ChunkRectangles, ev.RemovedChunks, ev.Grid); } protected virtual void OnGridInit(GridInitializeEvent ev) @@ -53,7 +53,7 @@ protected virtual void OnGridInit(GridInitializeEvent ev) return; // This will also check for grid splits if applicable. - var grid = Comp(ev.EntityUid); + var grid = ev.Grid ?? Comp(ev.EntityUid); _map.RegenerateCollision(ev.EntityUid, grid, _map.GetMapChunks(ev.EntityUid, grid).Values.ToHashSet()); } @@ -64,7 +64,8 @@ protected virtual void OnGridInit(GridInitializeEvent ev) internal void RegenerateCollision( EntityUid uid, Dictionary> mapChunks, - List removedChunks) + List removedChunks, + MapGridComponent? grid = null) { if (!_enabled) return; @@ -102,27 +103,23 @@ internal void RegenerateCollision( EntityManager.EventBus.RaiseLocalEvent(uid,new GridFixtureChangeEvent {NewFixtures = fixtures}, true); _fixtures.FixtureUpdate(uid, manager: manager, body: body); - CheckSplit(uid, mapChunks, removedChunks); + CheckSplit(uid, mapChunks, removedChunks, grid); } internal virtual void CheckSplit(EntityUid gridEuid, Dictionary> mapChunks, - List removedChunks) {} + List removedChunks, MapGridComponent? grid = null) {} - internal virtual void CheckSplit(EntityUid gridEuid, MapChunk chunk, List rectangles) {} + internal virtual void CheckSplit(EntityUid gridEuid, MapChunk chunk, List rectangles, MapGridComponent? grid = null) {} private bool UpdateFixture(EntityUid uid, MapChunk chunk, List rectangles, PhysicsComponent body, FixturesComponent manager, TransformComponent xform) { var origin = chunk.Indices * chunk.ChunkSize; - // So we store a reference to the fixture on the chunk because it's easier to cross-reference it. - // This is because when we get multiple fixtures per chunk there's no easy way to tell which the old one - // corresponds with. // We also ideally want to avoid re-creating the fixture every time a tile changes and pushing that data // to the client hence we diff it. - // Additionally, we need to handle map deserialization where content may have stored its own data // on the grid (e.g. mass) which we want to preserve. - var newFixtures = new ValueList<(string Id, Fixture Fixture)>(); + var newFixtures = new Dictionary(rectangles.Count); Span vertices = stackalloc Vector2[4]; @@ -150,59 +147,41 @@ private bool UpdateFixture(EntityUid uid, MapChunk chunk, List rectangles #pragma warning restore CS0618 var key = string.Create(CultureInfo.InvariantCulture, $"grid_chunk-{bounds.Left}-{bounds.Bottom}"); - newFixtures.Add((key, newFixture)); + newFixtures[key] = newFixture; } - // Check if we even need to issue an eventbus event var updated = false; + var toRemove = new ValueList(); + // Cross-reference old fixtures by ID. If the shape hasn't changed, keep the existing fixture + // to preserve any properties set by content (e.g. density from ShuttleSystem). foreach (var oldId in chunk.Fixtures) { - var oldFixture = manager.Fixtures[oldId]; - var existing = false; - - // Handle deleted / updated fixtures - // (TODO: Check IDs and cross-reference for updates?) - for (var i = newFixtures.Count - 1; i >= 0; i--) + if (newFixtures.TryGetValue(oldId, out var newFixture) && + manager.Fixtures[oldId].Shape is PolygonShape oldPoly && + newFixture.Shape is PolygonShape newPoly && + oldPoly.EqualsApprox(newPoly)) { - var fixture = newFixtures[i].Fixture; - - // TODO GRIDS - // Fix this - // This **only** works if we assume the density is always the default (PhysicsConstants.DefaultDensity). - // Hence, this always fails in SS14 because ShuttleSystem.OnGridFixtureChange changes the density. - // So it constantly creats & destroys fixtures unnecessarily - // AAAAA - if (!oldFixture.Equals(fixture)) - continue; - - existing = true; - newFixtures.RemoveSwap(i); - break; - } - - if (existing) + newFixtures.Remove(oldId); continue; + } - // Doesn't align with any new fixtures so delete - chunk.Fixtures.Remove(oldId); - _fixtures.DestroyFixture(uid, oldId, oldFixture, false, body: body, manager: manager, xform: xform); - updated = true; + toRemove.Add(oldId); } - if (newFixtures.Count > 0) + foreach (var oldId in toRemove) { + chunk.Fixtures.Remove(oldId); + _fixtures.DestroyFixture(uid, oldId, manager.Fixtures[oldId], false, body: body, manager: manager, xform: xform); updated = true; } // Anything remaining is a new fixture (or at least, may have not serialized onto the chunk yet). - foreach (var (id, fixture) in newFixtures.Span) + foreach (var (id, fixture) in newFixtures) { chunk.Fixtures.Add(id); + var existingFixture = _fixtures.GetFixtureOrNull(uid, id, manager: manager); - // Check if it's the same (otherwise remove anyway). - // TODO GRIDS - // wasn't this already checked? if (existingFixture?.Shape is PolygonShape poly && poly.EqualsApprox((PolygonShape) fixture.Shape)) { @@ -210,6 +189,7 @@ private bool UpdateFixture(EntityUid uid, MapChunk chunk, List rectangles } _fixtures.CreateFixture(uid, id, fixture, false, manager, body, xform); + updated = true; } return updated; diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs index 6c21c94534d..d6ee4e3ecc5 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs @@ -524,7 +524,7 @@ private void OnGridInit(EntityUid uid, MapGridComponent component, ComponentInit } } - var msg = new GridInitializeEvent(uid); + var msg = new GridInitializeEvent(uid, component); RaiseLocalEvent(uid, msg, true); } @@ -667,7 +667,7 @@ internal void RegenerateCollision(EntityUid uid, MapGridComponent grid, IReadOnl _physics.WakeBody(uid); OnGridBoundsChange(uid, grid); - var ev = new RegenerateGridBoundsEvent(uid, chunkRectangles, removedChunks); + var ev = new RegenerateGridBoundsEvent(uid, chunkRectangles, removedChunks, grid); RaiseLocalEvent(ref ev); } diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs index 773140f4977..9228909502a 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs @@ -154,10 +154,12 @@ public GridRemovalEvent(EntityUid uid) public sealed class GridInitializeEvent : EntityEventArgs { public EntityUid EntityUid { get; } + public MapGridComponent? Grid { get; } - public GridInitializeEvent(EntityUid uid) + public GridInitializeEvent(EntityUid uid, MapGridComponent? grid = null) { EntityUid = uid; + Grid = grid; } } #pragma warning restore CS0618 diff --git a/Robust.Shared/Map/Events/RegenerateGridBoundsEvent.cs b/Robust.Shared/Map/Events/RegenerateGridBoundsEvent.cs index f7236b8ee1e..07e38ca676b 100644 --- a/Robust.Shared/Map/Events/RegenerateGridBoundsEvent.cs +++ b/Robust.Shared/Map/Events/RegenerateGridBoundsEvent.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Robust.Shared.GameObjects; +using Robust.Shared.Map.Components; using Robust.Shared.Maths; namespace Robust.Shared.Map.Events; @@ -11,11 +12,13 @@ namespace Robust.Shared.Map.Events; /// Really this exists to get around test dependency creeping. /// [ByRefEvent] -internal readonly record struct RegenerateGridBoundsEvent(EntityUid Entity, Dictionary> ChunkRectangles, List RemovedChunks) +internal readonly record struct RegenerateGridBoundsEvent(EntityUid Entity, Dictionary> ChunkRectangles, List RemovedChunks, MapGridComponent? Grid = null) { public readonly EntityUid Entity = Entity; public readonly Dictionary> ChunkRectangles = ChunkRectangles; public readonly List RemovedChunks = RemovedChunks; + + public readonly MapGridComponent? Grid = Grid; }