Files
OpenRA/OpenRA.Mods.Common/Traits/World/EditorActorLayer.cs
let5sne.win10 9cf6ebb986
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled
Initial commit: OpenRA game engine
Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 21:46:54 +08:00

496 lines
14 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.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<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, INotifyActorDisposing
{
const string ActorPrefix = "Actor";
const string PlayerSpawnName = "mpspawn";
public readonly EditorActorLayerInfo Info;
readonly List<EditorActorPreview> previews = [];
readonly HashSet<uint> previewIds = [];
int2 cellOffset;
SpatiallyPartitioned<EditorActorPreview> cellMap;
SpatiallyPartitioned<EditorActorPreview> 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<EditorActorPreview>(
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<EditorActorPreview>(width, height, Info.BinSize);
var names = new string[world.Map.ActorDefinitions.Count];
var references = new List<ActorReference>(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<IRenderable> 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<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)
{
return PreviewsInScreenBox(wr.Viewport.TopLeft, wr.Viewport.BottomRight)
.SelectMany(p => p.RenderAnnotations());
}
bool IRenderAnnotations.SpatiallyPartitionable => false;
IEnumerable<CPos> 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<OwnerInit>();
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<ActorReference> references, ReadOnlySpan<string> 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<ActorReference> 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<EditorActorPreview> 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<EditorActorPreview> 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<CPos> 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<IOccupySpaceInfo>();
if (ios != null && ios.SharesCell)
{
var actorSubCell = FreeSubCellAt(location);
if (actorSubCell == SubCell.Invalid)
preview.RemoveInit<SubCellInit>();
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[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<EditorActorPreview> previews)
{
var cells = new HashSet<CPos>(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<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 => OccupiedCells(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 => 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<EditorActorPreview> 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<string> 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<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.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)); }
}
}
}