diff --git a/OpenRA.Game/Exts.cs b/OpenRA.Game/Exts.cs index 1511835fa4..ed7e781d81 100644 --- a/OpenRA.Game/Exts.cs +++ b/OpenRA.Game/Exts.cs @@ -572,6 +572,11 @@ namespace OpenRA return i.ToString(NumberFormatInfo.InvariantInfo); } + public static string ToStringInvariant(this uint i) + { + return i.ToString(NumberFormatInfo.InvariantInfo); + } + public static string ToStringInvariant(this float f) { return f.ToString(NumberFormatInfo.InvariantInfo); diff --git a/OpenRA.Game/Map/CellCoordsRegion.cs b/OpenRA.Game/Map/CellCoordsRegion.cs index 2f6914f2c8..1bfa5f7163 100644 --- a/OpenRA.Game/Map/CellCoordsRegion.cs +++ b/OpenRA.Game/Map/CellCoordsRegion.cs @@ -9,6 +9,7 @@ */ #endregion +using System; using System.Collections; using System.Collections.Generic; @@ -89,6 +90,31 @@ namespace OpenRA return GetEnumerator(); } + /// Returns the minimal region that covers at least the specified cells. + public static CellCoordsRegion BoundingRegion(IReadOnlyCollection cells) + { + if (cells == null || cells.Count == 0) + throw new ArgumentException("cells must not be null or empty.", nameof(cells)); + + var minX = int.MaxValue; + var minY = int.MaxValue; + var maxX = int.MinValue; + var maxY = int.MinValue; + foreach (var cell in cells) + { + if (minX > cell.X) + minX = cell.X; + if (maxX < cell.X) + maxX = cell.X; + if (minY > cell.Y) + minY = cell.Y; + if (maxY < cell.Y) + maxY = cell.Y; + } + + return new CellCoordsRegion(new CPos(minX, minY), new CPos(maxX, maxY)); + } + public CPos TopLeft { get; } public CPos BottomRight { get; } } diff --git a/OpenRA.Mods.Common/EditorBrushes/EditorBlit.cs b/OpenRA.Mods.Common/EditorBrushes/EditorBlit.cs index 3a4fc69231..f132430c55 100644 --- a/OpenRA.Mods.Common/EditorBrushes/EditorBlit.cs +++ b/OpenRA.Mods.Common/EditorBrushes/EditorBlit.cs @@ -12,8 +12,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using OpenRA.Graphics; using OpenRA.Mods.Common.Traits; +using OpenRA.Support; namespace OpenRA.Mods.Common.EditorBrushes { @@ -130,8 +132,7 @@ namespace OpenRA.Mods.Common.EditorBrushes if (blitFilters.HasFlag(MapBlitFilters.Actors)) { // Clear any existing actors in the paste cells. - var regionActors = editorActorLayer.PreviewsInCellRegion(blitRegion.CellCoords).ToList(); - + // // revertBlitSource's mask may be a superset of the commitBlitSource's mask if // - Its a sparse blit; and // - The revert actors removed by the commit are partially outside of the commit mask. @@ -146,9 +147,8 @@ namespace OpenRA.Mods.Common.EditorBrushes // This means we use the commit mask, not the revert one. var commitBlitVec = blitPosition - commitBlitSource.CellRegion.TopLeft; var mask = GetBlitSourceMask(commitBlitSource, commitBlitVec); - foreach (var regionActor in regionActors) - if (regionActor.Footprint.Any(kv => mask.Contains(kv.Key))) - editorActorLayer.Remove(regionActor); + using (new PerfTimer("RemoveActors", 1)) + editorActorLayer.RemoveRegion(blitRegion.CellCoords, mask); } foreach (var tileKeyValuePair in source.Tiles) @@ -181,12 +181,13 @@ namespace OpenRA.Mods.Common.EditorBrushes if (isRevert) { // For reverts, just place the original actors back exactly how they were. - foreach (var actor in source.Actors.Values) - editorActorLayer.Add(actor); + using (new PerfTimer("AddActors", 1)) + editorActorLayer.AddRange(source.Actors.Values.ToArray().AsSpan()); } else { // Create copies of the original actors, update their locations, and place. + var copies = new List(source.Actors.Count); foreach (var actorKeyValuePair in source.Actors) { var copy = actorKeyValuePair.Value.Export(); @@ -201,8 +202,11 @@ namespace OpenRA.Mods.Common.EditorBrushes copy.Add(new LocationInit(actorPosition)); } - editorActorLayer.Add(copy); + copies.Add(copy); } + + using (new PerfTimer("AddActors", 1)) + editorActorLayer.AddRange(CollectionsMarshal.AsSpan(copies)); } } } diff --git a/OpenRA.Mods.Common/EditorBrushes/EditorDefaultBrush.cs b/OpenRA.Mods.Common/EditorBrushes/EditorDefaultBrush.cs index a82faca76d..be9305a725 100644 --- a/OpenRA.Mods.Common/EditorBrushes/EditorDefaultBrush.cs +++ b/OpenRA.Mods.Common/EditorBrushes/EditorDefaultBrush.cs @@ -11,11 +11,12 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Runtime.InteropServices; using OpenRA.Graphics; using OpenRA.Mods.Common.EditorBrushes; using OpenRA.Mods.Common.Graphics; using OpenRA.Mods.Common.Traits; +using OpenRA.Support; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Widgets @@ -384,8 +385,8 @@ namespace OpenRA.Mods.Common.Widgets if (blitFilters.HasFlag(MapBlitFilters.Actors)) { // Clear any existing actors in the paste cells. - foreach (var regionActor in editorActorLayer.PreviewsInCellRegion(area.CellCoords).ToList()) - editorActorLayer.Remove(regionActor); + using (new PerfTimer("RemoveActors", 1)) + editorActorLayer.RemoveRegion(area.CellCoords); } foreach (var tileKeyValuePair in editorBlitSource.Tiles) @@ -432,6 +433,7 @@ namespace OpenRA.Mods.Common.Widgets if (blitFilters.HasFlag(MapBlitFilters.Actors)) { // Create copies of the original actors, update their locations, and place. + var copies = new List(editorBlitSource.Actors.Count); foreach (var actorKeyValuePair in editorBlitSource.Actors) { var copy = actorKeyValuePair.Value.Export(); @@ -445,8 +447,10 @@ namespace OpenRA.Mods.Common.Widgets copy.Add(new LocationInit(locationInit.Value)); } - editorActorLayer.Add(copy); + copies.Add(copy); } + + editorActorLayer.AddRange(CollectionsMarshal.AsSpan(copies)); } } } diff --git a/OpenRA.Mods.Common/Traits/World/EditorActorLayer.cs b/OpenRA.Mods.Common/Traits/World/EditorActorLayer.cs index 11c138864e..330cc3c538 100644 --- a/OpenRA.Mods.Common/Traits/World/EditorActorLayer.cs +++ b/OpenRA.Mods.Common/Traits/World/EditorActorLayer.cs @@ -11,7 +11,9 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Runtime.InteropServices; using OpenRA.Graphics; using OpenRA.Mods.Common.Traits.Render; using OpenRA.Network; @@ -46,6 +48,7 @@ namespace OpenRA.Mods.Common.Traits public readonly EditorActorLayerInfo Info; readonly List previews = []; + readonly HashSet previewIds = []; int2 cellOffset; SpatiallyPartitioned cellMap; @@ -86,12 +89,17 @@ namespace OpenRA.Mods.Common.Traits var height = world.Map.MapSize.Height * ts.Height; screenMap = new SpatiallyPartitioned(width, height, Info.BinSize); - foreach (var kv in world.Map.ActorDefinitions) - Add(kv.Key, new ActorReference(kv.Value.Value, kv.Value.ToDictionary()), true); + var names = new string[world.Map.ActorDefinitions.Count]; + var references = new List(world.Map.ActorDefinitions.Count); - // Update neighbours in one pass - foreach (var p in previews) - UpdateNeighbours(p.Footprint); + for (var i = 0; i < world.Map.ActorDefinitions.Count; i++) + { + var kv = world.Map.ActorDefinitions.ElementAt(i); + names[i] = kv.Key; + references.Add(new ActorReference(kv.Value.Value, kv.Value.ToDictionary())); + } + + AddRange(CollectionsMarshal.AsSpan(references), names); } void ITickRender.TickRender(WorldRenderer wr, Actor self) @@ -121,9 +129,15 @@ namespace OpenRA.Mods.Common.Traits bool IRenderAnnotations.SpatiallyPartitionable => false; - public EditorActorPreview Add(ActorReference reference) { return Add(NextActorName(), reference); } + IEnumerable OccupiedCells(EditorActorPreview preview) + { + // Fallback to the actor's CenterPosition for the ActorMap if it has no Footprint + if (preview.Footprint.Count == 0) + return [worldRenderer.World.Map.CellContaining(preview.CenterPosition)]; + return preview.Footprint.Keys; + } - public EditorActorPreview Add(string id, ActorReference reference, bool initialSetup = false) + PlayerReference GetOrAddOwner(ActorReference reference) { // If an actor's doesn't have a valid owner transfer ownership to neutral var ownerInit = reference.Get(); @@ -134,43 +148,97 @@ namespace OpenRA.Mods.Common.Traits reference.Add(new OwnerInit(worldOwner.Name)); } - var preview = new EditorActorPreview(worldRenderer, id, reference, owner); - Add(preview, initialSetup); + return owner; + } + + public EditorActorPreview Add(ActorReference reference) + { + var owner = GetOrAddOwner(reference); + var preview = new EditorActorPreview(worldRenderer, NextActorName(), reference, owner); + Add(preview); return preview; } - public void Add(EditorActorPreview preview, bool initialSetup = false) + public void AddRange(ReadOnlySpan references, ReadOnlySpan names) + { + if (names.Length != references.Length) + throw new ArgumentException("Member name count must match reference count."); + + var newPreviews = new EditorActorPreview[names.Length]; + using (new PerfTimer("CreatePreviews")) + { + for (var i = 0; i < names.Length; i++) + { + var id = names[i]; + var reference = references[i]; + var owner = GetOrAddOwner(reference); + + newPreviews[i] = new EditorActorPreview(worldRenderer, id, reference, owner); + } + } + + AddRange(newPreviews); + } + + public void AddRange(ReadOnlySpan references) + { + AddRange(references, NextActorNames(references.Length)); + } + + public void Add(EditorActorPreview preview) { previews.Add(preview); + if (TryGetActorId(preview.ID, out var id)) + previewIds.Add(id); + if (!preview.Bounds.IsEmpty) screenMap.Add(preview, preview.Bounds); - var cellFootprintBounds = Footprint(preview).Select( + var cellFootprintBounds = OccupiedCells(preview).Select( cell => new Rectangle(cell.X - cellOffset.X, cell.Y - cellOffset.Y, 1, 1)).Union(); + cellMap.Add(preview, cellFootprintBounds); preview.AddedToEditor(); + UpdateNeighbours(preview.Footprint); - if (!initialSetup) - { - UpdateNeighbours(preview.Footprint); - - if (preview.Type == PlayerSpawnName) - SyncMultiplayerCount(); - } + if (preview.Type == PlayerSpawnName) + SyncMultiplayerCount(); } - IEnumerable Footprint(EditorActorPreview preview) + public void AddRange(ReadOnlySpan newPreviews) { - // Fallback to the actor's CenterPosition for the ActorMap if it has no Footprint - if (preview.Footprint.Count == 0) - return [worldRenderer.World.Map.CellContaining(preview.CenterPosition)]; - return preview.Footprint.Keys; + previews.AddRange(newPreviews); + previewIds.EnsureCapacity(previews.Count * 2); + + foreach (var preview in newPreviews) + { + if (TryGetActorId(preview.ID, out var id)) + previewIds.Add(id); + + if (!preview.Bounds.IsEmpty) + screenMap.Add(preview, preview.Bounds); + + var cellFootprintBounds = OccupiedCells(preview) + .Select(cell => new Rectangle(cell.X - cellOffset.X, cell.Y - cellOffset.Y, 1, 1)).Union(); + + cellMap.Add(preview, cellFootprintBounds); + + preview.AddedToEditor(); + } + + using (new PerfTimer("UpdateNeighbours")) + UpdateNeighbours(newPreviews); + + SyncMultiplayerCount(); } public void Remove(EditorActorPreview preview) { previews.Remove(preview); + if (TryGetActorId(preview.ID, out var id)) + previewIds.Remove(id); + screenMap.Remove(preview); cellMap.Remove(preview); @@ -182,6 +250,38 @@ namespace OpenRA.Mods.Common.Traits SyncMultiplayerCount(); } + public void RemoveRange(ReadOnlySpan removePreviews) + { + foreach (var preview in removePreviews) + { + previews.Remove(preview); + if (TryGetActorId(preview.ID, out var id)) + previewIds.Remove(id); + + screenMap.Remove(preview); + cellMap.Remove(preview); + } + + using (new PerfTimer("RemovedFromEditor", 1)) + foreach (var preview in removePreviews) + preview.RemovedFromEditor(); + + using (new PerfTimer("UpdateNeighbours", 1)) + UpdateNeighbours(removePreviews); + + SyncMultiplayerCount(); + } + + public void RemoveRegion(CellCoordsRegion region) + { + RemoveRange(PreviewsInCellRegion(region).ToArray().AsSpan()); + } + + public void RemoveRegion(CellCoordsRegion region, HashSet mask) + { + RemoveRange(PreviewsInCellRegion(region).Where(p => mask.Overlaps(p.Footprint.Keys)).ToArray().AsSpan()); + } + public void MoveActor(EditorActorPreview preview, CPos location) { Remove(preview); @@ -203,9 +303,12 @@ namespace OpenRA.Mods.Common.Traits void SyncMultiplayerCount() { var newCount = previews.Count(p => p.Info.Name == PlayerSpawnName); - var mp = Players.Players.Where(p => p.Key.StartsWith("Multi", StringComparison.Ordinal)).ToList(); - foreach (var kv in mp) + var playersChanged = false; + foreach (var kv in Players.Players) { + if (!kv.Key.StartsWith("Multi", StringComparison.Ordinal)) + continue; + var name = kv.Key; var index = Exts.ParseInt32Invariant(name[5..]); @@ -213,6 +316,7 @@ namespace OpenRA.Mods.Common.Traits { Players.Players.Remove(name); OnPlayerRemoved(); + playersChanged = true; } } @@ -231,13 +335,34 @@ namespace OpenRA.Mods.Common.Traits Players.Players.Add(pr.Name, pr); worldRenderer.UpdatePalettesForPlayer(pr.Name, pr.Color, true); + playersChanged = true; } + if (!playersChanged) + return; + var creeps = Players.Players.Keys.FirstOrDefault(p => p == "Creeps"); if (!string.IsNullOrEmpty(creeps)) Players.Players[creeps].Enemies = Players.Players.Keys.Where(p => !Players.Players[p].NonCombatant).ToArray(); } + void UpdateNeighbours(ReadOnlySpan previews) + { + var cells = new HashSet(previews.Length * 6); + foreach (var preview in previews) + cells.UnionWith(Util.ExpandFootprint(preview.Footprint.Keys, true)); + + if (cells.Count == 0) + return; + + var bounds = CellCoordsRegion.BoundingRegion(cells); + var touchedPreviews = PreviewsInCellRegion(bounds) + .Where(p => cells.Overlaps(p.Footprint.Keys)); + + foreach (var p in touchedPreviews) + p.ReplaceInit(new RuntimeNeighbourInit(NeighbouringPreviews(p.Footprint))); + } + void UpdateNeighbours(IReadOnlyDictionary footprint) { // Include actors inside the footprint too @@ -269,20 +394,20 @@ namespace OpenRA.Mods.Common.Traits region.TopLeft.Y - cellOffset.Y, region.BottomRight.X - cellOffset.X + 1, region.BottomRight.Y - cellOffset.Y + 1)) - .Where(p => Footprint(p).Any(region.Contains)); + .Where(p => OccupiedCells(p).Any(region.Contains)); } public IEnumerable PreviewsAtCell(CPos cell) { return cellMap.At(new int2(cell.X - cellOffset.X, cell.Y - cellOffset.Y)) - .Where(p => Footprint(p).Any(fp => fp == cell)); + .Where(p => OccupiedCells(p).Any(fp => fp == cell)); } public SubCell FreeSubCellAt(CPos cell) { var map = worldRenderer.World.Map; - var previews = PreviewsAtCell(cell).ToList(); - if (previews.Count == 0) + var previews = PreviewsAtCell(cell).ToArray(); + if (previews.Length == 0) return map.Grid.DefaultSubCell; for (var i = (byte)SubCell.First; i < map.Grid.SubCellOffsets.Length; i++) @@ -303,18 +428,32 @@ namespace OpenRA.Mods.Common.Traits public Action OnPlayerRemoved = () => { }; + static bool TryGetActorId(string name, out uint id) + { + id = 0; + return name.StartsWith(ActorPrefix, StringComparison.Ordinal) + && uint.TryParse(name.AsSpan(5), NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out id); + } + string NextActorName() { - var id = previews.Count; - var possibleName = ActorPrefix + id.ToStringInvariant(); + var currentId = 0u; + while (previewIds.Contains(currentId)) + currentId++; - while (previews.Any(p => p.ID == possibleName)) - { - id++; - possibleName = ActorPrefix + id.ToStringInvariant(); - } + return ActorPrefix + currentId.ToStringInvariant(); + } - return possibleName; + ReadOnlySpan NextActorNames(int count) + { + var newNamesCount = 0u; + var newNames = new string[count]; + + for (var currentId = 0u; newNamesCount < count; currentId++) + if (!previewIds.Contains(currentId)) + newNames[newNamesCount++] = ActorPrefix + currentId.ToStringInvariant(); + + return newNames; } public List Save() @@ -329,7 +468,7 @@ namespace OpenRA.Mods.Common.Traits public void PopulateRadarSignatureCells(Actor self, List<(CPos Cell, Color Color)> destinationBuffer) { foreach (var preview in cellMap.Keys) - foreach (var cell in Footprint(preview)) + foreach (var cell in OccupiedCells(preview)) destinationBuffer.Add((cell, preview.RadarColor)); }