#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.Linq; 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; 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 { readonly EditorActorLayerInfo info; readonly List previews = new(); readonly Dictionary> cellMap = new(); SpatiallyPartitioned screenMap; WorldRenderer worldRenderer; public MapPlayers Players { get; private set; } PlayerReference worldOwner; public EditorActorLayer(EditorActorLayerInfo info) { this.info = info; } void ICreatePlayers.CreatePlayers(World w, MersenneTwister playerRandom) { if (w.Type != WorldType.Editor) return; 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) { if (world.Type != WorldType.Editor) return; worldRenderer = wr; foreach (var pr in Players.Players.Values) wr.UpdatePalettesForPlayer(pr.Name, pr.Color, false); var ts = world.Map.Grid.TileSize; var width = world.Map.MapSize.X * ts.Width; var height = world.Map.MapSize.Y * 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); // Update neighbours in one pass foreach (var p in previews) UpdateNeighbours(p.Footprint); } void ITickRender.TickRender(WorldRenderer wr, Actor self) { if (wr.World.Type != WorldType.Editor) return; foreach (var p in previews) p.Tick(); } static readonly IEnumerable NoRenderables = Enumerable.Empty(); public virtual IEnumerable Render(Actor self, WorldRenderer wr) { if (wr.World.Type != WorldType.Editor) return NoRenderables; return PreviewsInBox(wr.Viewport.TopLeft, wr.Viewport.BottomRight) .SelectMany(p => p.Render()); } 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) { if (wr.World.Type != WorldType.Editor) return NoRenderables; return PreviewsInBox(wr.Viewport.TopLeft, wr.Viewport.BottomRight) .SelectMany(p => p.RenderAnnotations()); } bool IRenderAnnotations.SpatiallyPartitionable => false; public EditorActorPreview Add(ActorReference reference) { return Add(NextActorName(), reference); } public EditorActorPreview Add(string id, ActorReference reference, bool initialSetup = false) { // 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.Remove(ownerInit); reference.Add(new OwnerInit(worldOwner.Name)); } var preview = new EditorActorPreview(worldRenderer, id, reference, owner); Add(preview, initialSetup); return preview; } public void Add(EditorActorPreview preview, bool initialSetup = false) { previews.Add(preview); if (!preview.Bounds.IsEmpty) screenMap.Add(preview, preview.Bounds); // Fallback to the actor's CenterPosition for the ActorMap if it has no Footprint var footprint = preview.Footprint.Select(kv => kv.Key).ToArray(); if (footprint.Length == 0) footprint = new[] { worldRenderer.World.Map.CellContaining(preview.CenterPosition) }; foreach (var cell in footprint) AddPreviewLocation(preview, cell); preview.AddedToEditor(); if (!initialSetup) { UpdateNeighbours(preview.Footprint); if (preview.Type == "mpspawn") SyncMultiplayerCount(); } } public void Remove(EditorActorPreview preview) { previews.Remove(preview); screenMap.Remove(preview); // Fallback to the actor's CenterPosition for the ActorMap if it has no Footprint var footprint = preview.Footprint.Select(kv => kv.Key).ToArray(); if (footprint.Length == 0) footprint = new[] { worldRenderer.World.Map.CellContaining(preview.CenterPosition) }; foreach (var cell in footprint) { if (!cellMap.TryGetValue(cell, out var list)) continue; list.Remove(preview); if (list.Count == 0) cellMap.Remove(cell); } preview.RemovedFromEditor(); UpdateNeighbours(preview.Footprint); if (preview.Info.Name == "mpspawn") SyncMultiplayerCount(); } void SyncMultiplayerCount() { var newCount = previews.Count(p => p.Info.Name == "mpspawn"); var mp = Players.Players.Where(p => p.Key.StartsWith("Multi", StringComparison.Ordinal)).ToList(); foreach (var kv in mp) { var name = kv.Key; var index = Exts.ParseInt32Invariant(name[5..]); if (index >= newCount) { Players.Players.Remove(name); OnPlayerRemoved(); } } 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 = new[] { "Creeps" } }; Players.Players.Add(pr.Name, pr); worldRenderer.UpdatePalettesForPlayer(pr.Name, pr.Color, true); } 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(IReadOnlyDictionary footprint) { // Include actors inside the footprint too var cells = Util.ExpandFootprint(footprint.Keys, true); foreach (var p in cells.SelectMany(c => PreviewsAt(c))) p.ReplaceInit(new RuntimeNeighbourInit(NeighbouringPreviews(p.Footprint))); } void AddPreviewLocation(EditorActorPreview preview, CPos location) { if (!cellMap.TryGetValue(location, out var list)) { list = new List(); cellMap.Add(location, list); } list.Add(preview); } Dictionary NeighbouringPreviews(IReadOnlyDictionary footprint) { var cells = Util.ExpandFootprint(footprint.Keys, true).Except(footprint.Keys); return cells.ToDictionary(c => c, c => PreviewsAt(c).Select(p => p.Info.Name).ToArray()); } public IEnumerable PreviewsInBox(int2 a, int2 b) { return screenMap.InBox(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 PreviewsInBox(Rectangle r) { return screenMap.InBox(r); } public IEnumerable PreviewsAt(CPos cell) { if (cellMap.TryGetValue(cell, out var list)) return list; return Enumerable.Empty(); } public SubCell FreeSubCellAt(CPos cell) { var map = worldRenderer.World.Map; var previews = PreviewsAt(cell).ToList(); if (previews.Count == 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 PreviewsAt(int2 worldPx) { return screenMap.At(worldPx); } public Action OnPlayerRemoved = () => { }; string NextActorName() { var id = previews.Count; var possibleName = "Actor" + id.ToStringInvariant(); while (previews.Any(p => p.ID == possibleName)) { id++; possibleName = "Actor" + id.ToStringInvariant(); } return possibleName; } 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 previewsForCell in cellMap) foreach (var preview in previewsForCell.Value) destinationBuffer.Add((previewsForCell.Key, preview.RadarColor)); } public EditorActorPreview this[string id] { get { return previews.FirstOrDefault(p => p.ID.Equals(id, StringComparison.OrdinalIgnoreCase)); } } } }