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));
}