- EditorActorLayer now tracks previews on map with a SpatiallyPartitioned instead of a Dictionary. This allows the copy-paste logic to call an efficient PreviewsInCellRegion method, instead of asking for previews cell-by-cell. - EditorActorPreview subscribes to the CellEntryChanged methods on the map. Previously the preview was refreshed regardless of which cell changed. Now the preview only regenerates if the preview's footprint has been affected.
333 lines
9.9 KiB
C#
333 lines
9.9 KiB
C#
#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<GameInformation.Player> 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<EditorActorPreview> previews = new();
|
|
|
|
int2 cellOffset;
|
|
SpatiallyPartitioned<EditorActorPreview> cellMap;
|
|
SpatiallyPartitioned<EditorActorPreview> 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);
|
|
|
|
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;
|
|
cellMap = new SpatiallyPartitioned<EditorActorPreview>(
|
|
mapCellSize.X, mapCellSize.Y, Exts.IntegerDivisionRoundingAwayFromZero(info.BinSize, world.Map.Grid.TileSize.Width));
|
|
|
|
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<EditorActorPreview>(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<IRenderable> NoRenderables = Enumerable.Empty<IRenderable>();
|
|
public virtual IEnumerable<IRenderable> Render(Actor self, WorldRenderer wr)
|
|
{
|
|
if (wr.World.Type != WorldType.Editor)
|
|
return NoRenderables;
|
|
|
|
return PreviewsInScreenBox(wr.Viewport.TopLeft, wr.Viewport.BottomRight)
|
|
.SelectMany(p => p.Render());
|
|
}
|
|
|
|
IEnumerable<Rectangle> IRender.ScreenBounds(Actor self, WorldRenderer wr)
|
|
{
|
|
// World-actor render traits don't require screen bounds
|
|
yield break;
|
|
}
|
|
|
|
public IEnumerable<IRenderable> RenderAnnotations(Actor self, WorldRenderer wr)
|
|
{
|
|
if (wr.World.Type != WorldType.Editor)
|
|
return NoRenderables;
|
|
|
|
return PreviewsInScreenBox(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<OwnerInit>();
|
|
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);
|
|
|
|
var cellFootprintBounds = Footprint(preview).Select(
|
|
cell => new Rectangle(cell.X - cellOffset.X, cell.Y - cellOffset.Y, 1, 1)).Union();
|
|
cellMap.Add(preview, cellFootprintBounds);
|
|
|
|
preview.AddedToEditor();
|
|
|
|
if (!initialSetup)
|
|
{
|
|
UpdateNeighbours(preview.Footprint);
|
|
|
|
if (preview.Type == "mpspawn")
|
|
SyncMultiplayerCount();
|
|
}
|
|
}
|
|
|
|
IEnumerable<CPos> Footprint(EditorActorPreview preview)
|
|
{
|
|
// Fallback to the actor's CenterPosition for the ActorMap if it has no Footprint
|
|
if (preview.Footprint.Count == 0)
|
|
return new[] { worldRenderer.World.Map.CellContaining(preview.CenterPosition) };
|
|
return preview.Footprint.Keys;
|
|
}
|
|
|
|
public void Remove(EditorActorPreview preview)
|
|
{
|
|
previews.Remove(preview);
|
|
screenMap.Remove(preview);
|
|
|
|
cellMap.Remove(preview);
|
|
|
|
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<CPos, SubCell> 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<CPos, string[]> NeighbouringPreviews(IReadOnlyDictionary<CPos, SubCell> 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<EditorActorPreview> 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<EditorActorPreview> PreviewsInScreenBox(Rectangle r)
|
|
{
|
|
return screenMap.InBox(r);
|
|
}
|
|
|
|
public IEnumerable<EditorActorPreview> 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 => Footprint(p).Any(region.Contains));
|
|
}
|
|
|
|
public IEnumerable<EditorActorPreview> PreviewsAtCell(CPos cell)
|
|
{
|
|
return cellMap.At(new int2(cell.X - cellOffset.X, cell.Y - cellOffset.Y))
|
|
.Where(p => Footprint(p).Any(fp => fp == cell));
|
|
}
|
|
|
|
public SubCell FreeSubCellAt(CPos cell)
|
|
{
|
|
var map = worldRenderer.World.Map;
|
|
var previews = PreviewsAtCell(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<EditorActorPreview> PreviewsAtWorldPixel(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<MiniYamlNode> Save()
|
|
{
|
|
var nodes = new List<MiniYamlNode>();
|
|
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.Items)
|
|
foreach (var cell in Footprint(preview))
|
|
destinationBuffer.Add((cell, preview.RadarColor));
|
|
}
|
|
|
|
public EditorActorPreview this[string id]
|
|
{
|
|
get { return previews.FirstOrDefault(p => p.ID.Equals(id, StringComparison.OrdinalIgnoreCase)); }
|
|
}
|
|
}
|
|
}
|