commit 7a144e3c4f8cebc37fe608ed143990777291e75f Author: upstream Date: Sat Oct 10 12:42:09 2026 +0200 Upstream OpenRA/OpenRA state of the touched files before 2fe3fea01e diff --git a/OpenRA.Game/Map/MapPlayers.cs b/OpenRA.Game/Map/MapPlayers.cs new file mode 100644 index 0000000..476bdd6 --- /dev/null +++ b/OpenRA.Game/Map/MapPlayers.cs @@ -0,0 +1,82 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA +{ + public class MapPlayers + { + // Player masks are represented using a 64 bit integer + // The "Everyone" player for spectators is created at runtime, + // reducing the available player count for maps by 1. + public const int MaximumPlayerCount = 63; + public readonly Dictionary Players; + + public MapPlayers() + : this([]) { } + + public MapPlayers(IEnumerable playerDefinitions) + { + Players = playerDefinitions.Select(pr => new PlayerReference(new MiniYaml(pr.Key, pr.Value.Nodes))) + .ToDictionary(player => player.Name); + } + + public MapPlayers(Ruleset rules, int playerCount) + { + var firstFaction = rules.Actors[SystemActors.World].TraitInfos() + .First(f => f.Selectable).InternalName; + + Players = new Dictionary + { + { + "Neutral", new PlayerReference + { + Name = "Neutral", + Faction = firstFaction, + OwnsWorld = true, + NonCombatant = true + } + }, + { + "Creeps", new PlayerReference + { + Name = "Creeps", + Faction = firstFaction, + NonCombatant = true, + Enemies = Exts.MakeArray(playerCount, i => $"Multi{i}").ToImmutableArray() + } + } + }; + + for (var index = 0; index < playerCount; index++) + { + var p = new PlayerReference + { + Name = $"Multi{index}", + Faction = "Random", + Playable = true, + Enemies = ["Creeps"] + }; + Players.Add(p.Name, p); + } + } + + public List ToMiniYaml() + { + return Players.Select(p => new MiniYamlNode($"PlayerReference@{p.Key}", + FieldSaver.SaveDifferences(p.Value, new PlayerReference()))).ToList(); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/World/EditorActorLayer.cs b/OpenRA.Mods.Common/Traits/World/EditorActorLayer.cs new file mode 100644 index 0000000..faefc33 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/World/EditorActorLayer.cs @@ -0,0 +1,495 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Traits.Render; +using OpenRA.Network; +using OpenRA.Primitives; +using OpenRA.Support; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [TraitLocation(SystemActors.EditorWorld)] + [Desc("Required for the map editor to work. Attach this to the world actor.")] + public class EditorActorLayerInfo : TraitInfo, ICreatePlayersInfo + { + [Desc("Size of partition bins (world pixels).")] + public readonly int BinSize = 250; + + [Desc("Facing of new actors.")] + public readonly WAngle DefaultActorFacing = new(384); + + void ICreatePlayersInfo.CreateServerPlayers(MapPreview map, Session lobbyInfo, List players, MersenneTwister playerRandom) + { + throw new NotImplementedException("EditorActorLayer must not be defined on the world actor."); + } + + public override object Create(ActorInitializer init) { return new EditorActorLayer(this); } + } + + public class EditorActorLayer : IWorldLoaded, ITickRender, IRender, IRadarSignature, ICreatePlayers, IRenderAnnotations, INotifyActorDisposing + { + const string ActorPrefix = "Actor"; + const string PlayerSpawnName = "mpspawn"; + + public readonly EditorActorLayerInfo Info; + readonly List previews = []; + readonly HashSet previewIds = []; + + int2 cellOffset; + SpatiallyPartitioned cellMap; + SpatiallyPartitioned screenMap; + WorldRenderer worldRenderer; + + public MapPlayers Players { get; private set; } + PlayerReference worldOwner; + + public EditorActorLayer(EditorActorLayerInfo info) + { + Info = info; + } + + void ICreatePlayers.CreatePlayers(World w, MersenneTwister playerRandom) + { + Players = new MapPlayers(w.Map.PlayerDefinitions); + + worldOwner = Players.Players.Select(kvp => kvp.Value).First(p => !p.Playable && p.OwnsWorld); + w.SetWorldOwner(new Player(w, null, worldOwner, playerRandom)); + } + + public void WorldLoaded(World world, WorldRenderer wr) + { + worldRenderer = wr; + + foreach (var pr in Players.Players.Values) + wr.UpdatePalettesForPlayer(pr.Name, pr.Color, false); + + cellOffset = new int2(world.Map.AllCells.Min(c => c.X), world.Map.AllCells.Min((c) => c.Y)); + var cellOffsetMax = new int2(world.Map.AllCells.Max(c => c.X), world.Map.AllCells.Max((c) => c.Y)); + var mapCellSize = cellOffsetMax - cellOffset; + var ts = world.Map.Rules.TerrainInfo.TileSize; + cellMap = new SpatiallyPartitioned( + mapCellSize.X, mapCellSize.Y, Exts.IntegerDivisionRoundingAwayFromZero(Info.BinSize, ts.Width)); + + var width = world.Map.MapSize.Width * ts.Width; + var height = world.Map.MapSize.Height * ts.Height; + screenMap = new SpatiallyPartitioned(width, height, Info.BinSize); + + var names = new string[world.Map.ActorDefinitions.Count]; + var references = new List(world.Map.ActorDefinitions.Count); + + 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)); + } + + AddRange(CollectionsMarshal.AsSpan(references), names); + + world.Map.Height.CellEntryChanged += UpdatePreviewsOnMapChange; + world.Map.Ramp.CellEntryChanged += UpdatePreviewsOnMapChange; + } + + void ITickRender.TickRender(WorldRenderer wr, Actor self) + { + foreach (var p in previews) + p.Tick(); + } + + public virtual IEnumerable Render(Actor self, WorldRenderer wr) + { + foreach (var p in PreviewsInScreenBox(wr.Viewport.TopLeft, wr.Viewport.BottomRight)) + foreach (var r in p.Render()) + yield return r; + } + + IEnumerable IRender.ScreenBounds(Actor self, WorldRenderer wr) + { + // World-actor render traits don't require screen bounds + yield break; + } + + public IEnumerable RenderAnnotations(Actor self, WorldRenderer wr) + { + return PreviewsInScreenBox(wr.Viewport.TopLeft, wr.Viewport.BottomRight) + .SelectMany(p => p.RenderAnnotations()); + } + + bool IRenderAnnotations.SpatiallyPartitionable => false; + + 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; + } + + PlayerReference GetOrAddOwner(ActorReference reference) + { + // If an actor's doesn't have a valid owner transfer ownership to neutral + var ownerInit = reference.Get(); + if (!Players.Players.TryGetValue(ownerInit.InternalName, out var owner)) + { + owner = worldOwner; + reference.Replace(new OwnerInit(worldOwner.Name)); + } + + 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 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 = 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 (preview.Type == PlayerSpawnName) + SyncMultiplayerCount(); + } + + public void AddRange(ReadOnlySpan newPreviews) + { + 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); + + preview.RemovedFromEditor(); + UpdateNeighbours(preview.Footprint); + + if (preview.Info.Name == PlayerSpawnName) + 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); + preview.ReplaceInit(new LocationInit(location)); + var ios = preview.Info.TraitInfoOrDefault(); + if (ios != null && ios.SharesCell) + { + var actorSubCell = FreeSubCellAt(location); + if (actorSubCell == SubCell.Invalid) + preview.RemoveInit(); + else + preview.ReplaceInit(new SubCellInit(actorSubCell)); + } + + preview.UpdateFromMove(); + Add(preview); + } + + void SyncMultiplayerCount() + { + var newCount = previews.Count(p => p.Info.Name == PlayerSpawnName); + 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.AsSpan(5)); + + if (index >= newCount) + { + Players.Players.Remove(name); + OnPlayerRemoved(); + playersChanged = true; + } + } + + for (var index = 0; index < newCount; index++) + { + if (Players.Players.ContainsKey($"Multi{index}")) + continue; + + var pr = new PlayerReference + { + Name = $"Multi{index}", + Faction = "Random", + Playable = true, + Enemies = ["Creeps"] + }; + + 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).ToImmutableArray(); + } + + 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 + var cells = Util.ExpandFootprint(footprint.Keys, true); + foreach (var p in cells.SelectMany(PreviewsAtCell)) + p.ReplaceInit(new RuntimeNeighbourInit(NeighbouringPreviews(p.Footprint))); + } + + Dictionary NeighbouringPreviews(IReadOnlyDictionary footprint) + { + var cells = Util.ExpandFootprint(footprint.Keys, true).Except(footprint.Keys); + return cells.ToDictionary(c => c, c => PreviewsAtCell(c).Select(p => p.Info.Name).ToArray()); + } + + public IEnumerable PreviewsInScreenBox(int2 a, int2 b) + { + return PreviewsInScreenBox(Rectangle.FromLTRB(Math.Min(a.X, b.X), Math.Min(a.Y, b.Y), Math.Max(a.X, b.X), Math.Max(a.Y, b.Y))); + } + + public IEnumerable PreviewsInScreenBox(Rectangle r) + { + return screenMap.InBox(r); + } + + public IEnumerable PreviewsInCellRegion(CellCoordsRegion region) + { + return cellMap.InBox(Rectangle.FromLTRB( + region.TopLeft.X - cellOffset.X, + region.TopLeft.Y - cellOffset.Y, + region.BottomRight.X - cellOffset.X + 1, + region.BottomRight.Y - cellOffset.Y + 1)) + .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 => OccupiedCells(p).Contains(cell)); + } + + public SubCell FreeSubCellAt(CPos cell) + { + var map = worldRenderer.World.Map; + 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++) + { + var blocked = previews.Any(p => p.Footprint.TryGetValue(cell, out var s) && s == (SubCell)i); + + if (!blocked) + return (SubCell)i; + } + + return SubCell.Invalid; + } + + public IEnumerable PreviewsAtWorldPixel(int2 worldPx) + { + return screenMap.At(worldPx); + } + + void UpdatePreviewsOnMapChange(CPos changedCell) + { + foreach (var preview in PreviewsAtCell(changedCell)) + preview.UpdateFromCellChange(); + } + + 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 currentId = 0u; + while (previewIds.Contains(currentId)) + currentId++; + + return ActorPrefix + currentId.ToStringInvariant(); + } + + 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() + { + var nodes = new List(); + foreach (var a in previews) + nodes.Add(new MiniYamlNode(a.ID, a.Save())); + + return nodes; + } + + public void PopulateRadarSignatureCells(Actor self, List<(CPos Cell, Color Color)> destinationBuffer) + { + foreach (var preview in cellMap.Keys) + foreach (var cell in OccupiedCells(preview)) + destinationBuffer.Add((cell, preview.RadarColor)); + } + + void INotifyActorDisposing.Disposing(Actor self) + { + self.World.Map.Height.CellEntryChanged -= UpdatePreviewsOnMapChange; + self.World.Map.Ramp.CellEntryChanged -= UpdatePreviewsOnMapChange; + } + + public EditorActorPreview this[string id] + { + get { return previews.FirstOrDefault(p => p.ID.Equals(id, StringComparison.OrdinalIgnoreCase)); } + } + } +}