Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
364 lines
12 KiB
C#
364 lines
12 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 System.Runtime.InteropServices;
|
|
using OpenRA.Graphics;
|
|
using OpenRA.Mods.Common.MapGenerator;
|
|
using OpenRA.Mods.Common.Traits;
|
|
using OpenRA.Support;
|
|
|
|
namespace OpenRA.Mods.Common.EditorBrushes
|
|
{
|
|
public readonly record struct BlitTile(TerrainTile TerrainTile, ResourceTile ResourceTile, ResourceLayerContents? ResourceLayerContents, byte Height);
|
|
|
|
public readonly record struct EditorBlitSource(CellCoordsRegion CellCoords, Dictionary<string, EditorActorPreview> Actors, Dictionary<CPos, BlitTile> Tiles);
|
|
|
|
[Flags]
|
|
public enum MapBlitFilters
|
|
{
|
|
None = 0,
|
|
Terrain = 1,
|
|
Resources = 2,
|
|
Actors = 4,
|
|
All = Terrain | Resources | Actors
|
|
}
|
|
|
|
/// <summary>
|
|
/// Core implementation for EditorActions which overwrite a region of the map (such as
|
|
/// copy-paste).
|
|
/// </summary>
|
|
public sealed class EditorBlit
|
|
{
|
|
readonly MapBlitFilters blitFilters;
|
|
readonly IResourceLayer resourceLayer;
|
|
readonly EditorActorLayer editorActorLayer;
|
|
readonly EditorBlitSource commitBlitSource;
|
|
readonly EditorBlitSource revertBlitSource;
|
|
readonly CPos blitPosition;
|
|
readonly Map map;
|
|
readonly bool respectBounds;
|
|
|
|
public EditorBlit(
|
|
MapBlitFilters blitFilters,
|
|
IResourceLayer resourceLayer,
|
|
CPos blitPosition,
|
|
Map map,
|
|
EditorBlitSource blitSource,
|
|
EditorActorLayer editorActorLayer,
|
|
bool respectBounds)
|
|
{
|
|
this.blitFilters = blitFilters;
|
|
this.resourceLayer = resourceLayer;
|
|
this.blitPosition = blitPosition;
|
|
this.editorActorLayer = editorActorLayer;
|
|
this.map = map;
|
|
this.respectBounds = respectBounds;
|
|
|
|
var blitSize = blitSource.CellCoords.BottomRight - blitSource.CellCoords.TopLeft;
|
|
|
|
// Only include into the revert blit stuff which would be modified by the main blit.
|
|
var mask = GetBlitSourceMask(
|
|
blitSource, blitPosition - blitSource.CellCoords.TopLeft);
|
|
|
|
commitBlitSource = blitSource;
|
|
revertBlitSource = CopyRegionContents(
|
|
map,
|
|
editorActorLayer,
|
|
resourceLayer,
|
|
new CellCoordsRegion(blitPosition, blitPosition + blitSize),
|
|
blitFilters,
|
|
mask);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns an EditorBlitSource containing the map contents for a given region.
|
|
/// If a mask is supplied, only tiles and actors (fully or partially) overlapping the mask
|
|
/// are included in the EditorBlitSource.
|
|
/// </summary>
|
|
public static EditorBlitSource CopyRegionContents(
|
|
Map map,
|
|
EditorActorLayer editorActorLayer,
|
|
IResourceLayer resourceLayer,
|
|
CellCoordsRegion region,
|
|
MapBlitFilters blitFilters,
|
|
IReadOnlySet<CPos> mask = null)
|
|
{
|
|
var mapTiles = map.Tiles;
|
|
var mapHeight = map.Height;
|
|
var mapResources = map.Resources;
|
|
|
|
var previews = new Dictionary<string, EditorActorPreview>();
|
|
var tiles = new Dictionary<CPos, BlitTile>();
|
|
|
|
if (blitFilters.HasFlag(MapBlitFilters.Terrain) || blitFilters.HasFlag(MapBlitFilters.Resources))
|
|
{
|
|
foreach (var cell in region)
|
|
{
|
|
if (!mapTiles.Contains(cell) || (mask != null && !mask.Contains(cell)))
|
|
continue;
|
|
|
|
tiles.Add(
|
|
cell,
|
|
new BlitTile(mapTiles[cell],
|
|
mapResources[cell],
|
|
resourceLayer?.GetResource(cell),
|
|
mapHeight[cell]));
|
|
}
|
|
}
|
|
|
|
if (blitFilters.HasFlag(MapBlitFilters.Actors))
|
|
foreach (var preview in editorActorLayer.PreviewsInCellRegion(region))
|
|
if (mask == null || preview.Footprint.Keys.Any(mask.Contains))
|
|
previews.TryAdd(preview.ID, preview);
|
|
|
|
return new EditorBlitSource(region, previews, tiles);
|
|
}
|
|
|
|
void Blit(bool isRevert)
|
|
{
|
|
var source = isRevert ? revertBlitSource : commitBlitSource;
|
|
var blitPos = isRevert ? source.CellCoords.TopLeft : blitPosition;
|
|
var blitVec = blitPos - source.CellCoords.TopLeft;
|
|
var blitSize = source.CellCoords.BottomRight - source.CellCoords.TopLeft;
|
|
var blitRegion = new CellCoordsRegion(blitPos, blitPos + blitSize);
|
|
|
|
if (blitFilters.HasFlag(MapBlitFilters.Actors))
|
|
{
|
|
// Clear any existing actors in the paste cells.
|
|
//
|
|
// 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.
|
|
// Otherwise, it's a (practically) equal set. (Subject to map bounds.)
|
|
//
|
|
// This implies that:
|
|
// - commitBlitSource's mask will overlap all commit actors.
|
|
// - revertBlitSource's mask will overlap all revert actors.
|
|
// - commitBlitSource's mask will overlap all and no more than the revert actors.
|
|
// - revertBlitSource's mask will overlap all revert actors BUT MAY OVERLAP MORE!
|
|
//
|
|
// This means we use the commit mask, not the revert one.
|
|
var commitBlitVec = blitPosition - commitBlitSource.CellCoords.TopLeft;
|
|
var mask = GetBlitSourceMask(commitBlitSource, commitBlitVec);
|
|
using (new PerfTimer("RemoveActors", 1))
|
|
editorActorLayer.RemoveRegion(blitRegion, mask);
|
|
}
|
|
|
|
foreach (var tileKeyValuePair in source.Tiles)
|
|
{
|
|
var position = tileKeyValuePair.Key + blitVec;
|
|
if (!map.Tiles.Contains(position) || (respectBounds && !map.Contains(position)))
|
|
continue;
|
|
|
|
// Clear any existing resources.
|
|
if (resourceLayer != null && blitFilters.HasFlag(MapBlitFilters.Resources))
|
|
resourceLayer.ClearResources(position);
|
|
|
|
var tile = tileKeyValuePair.Value;
|
|
var resourceLayerContents = tile.ResourceLayerContents;
|
|
|
|
if (blitFilters.HasFlag(MapBlitFilters.Terrain))
|
|
{
|
|
map.Tiles[position] = tile.TerrainTile;
|
|
map.Height[position] = tile.Height;
|
|
}
|
|
|
|
if (blitFilters.HasFlag(MapBlitFilters.Resources) &&
|
|
resourceLayerContents.HasValue &&
|
|
!string.IsNullOrWhiteSpace(resourceLayerContents.Value.Type) &&
|
|
resourceLayer.CanAddResource(resourceLayerContents.Value.Type, position))
|
|
{
|
|
resourceLayer.AddResource(resourceLayerContents.Value.Type, position, resourceLayerContents.Value.Density);
|
|
}
|
|
}
|
|
|
|
if (blitFilters.HasFlag(MapBlitFilters.Actors))
|
|
{
|
|
if (isRevert)
|
|
{
|
|
// For reverts, just place the original actors back exactly how they were.
|
|
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<ActorReference>(source.Actors.Count);
|
|
foreach (var actorKeyValuePair in source.Actors)
|
|
{
|
|
var copy = actorKeyValuePair.Value.Export();
|
|
var locationInit = copy.GetOrDefault<LocationInit>();
|
|
if (locationInit != null)
|
|
{
|
|
var actorPosition = locationInit.Value + blitVec;
|
|
if (respectBounds && !map.Contains(actorPosition))
|
|
continue;
|
|
|
|
copy.RemoveAll<LocationInit>();
|
|
copy.Add(new LocationInit(actorPosition));
|
|
}
|
|
|
|
copies.Add(copy);
|
|
}
|
|
|
|
using (new PerfTimer("AddActors", 1))
|
|
editorActorLayer.AddRange(CollectionsMarshal.AsSpan(copies));
|
|
}
|
|
}
|
|
}
|
|
|
|
public static IEnumerable<IRenderable> PreviewBlitSource(
|
|
EditorBlitSource blitSource,
|
|
MapBlitFilters filters,
|
|
CVec offset,
|
|
WorldRenderer wr,
|
|
bool stickToGround)
|
|
{
|
|
var world = wr.World;
|
|
var map = world.Map;
|
|
var mapHeight = map.Height;
|
|
var mapGrid = map.Grid;
|
|
|
|
if (filters.HasFlag(MapBlitFilters.Terrain))
|
|
{
|
|
var terrainRenderer = world.WorldActor.Trait<ITiledTerrainRenderer>();
|
|
foreach (var (pos, tile) in blitSource.Tiles)
|
|
{
|
|
var cPos = pos + offset;
|
|
var height = stickToGround ? (mapHeight.TryGetValue(cPos, out var isoHeight) ? isoHeight : byte.MinValue) : tile.Height;
|
|
var wPos = CellLayerUtils.CPosToWPos(cPos, height, mapGrid.Type);
|
|
var preview = terrainRenderer.RenderPreview(wr, tile.TerrainTile, wPos);
|
|
|
|
foreach (var renderable in preview)
|
|
yield return renderable;
|
|
}
|
|
}
|
|
|
|
if (filters.HasFlag(MapBlitFilters.Resources))
|
|
{
|
|
var resourceRenderers = world.WorldActor.TraitsImplementing<IResourceRenderer>().ToArray();
|
|
var resourceLayer = world.WorldActor.Trait<IResourceLayer>();
|
|
foreach (var (pos, tile) in blitSource.Tiles)
|
|
{
|
|
if (tile.ResourceLayerContents == null || tile.ResourceLayerContents.Value.Type == null)
|
|
continue;
|
|
|
|
var cPos = pos + offset;
|
|
if (!filters.HasFlag(MapBlitFilters.Terrain) && !resourceLayer.CanAddResource(tile.ResourceLayerContents.Value.Type, cPos))
|
|
continue;
|
|
|
|
byte height;
|
|
if (filters.HasFlag(MapBlitFilters.Terrain) && !stickToGround)
|
|
{
|
|
// We won't change relative tile height, use the saved value.
|
|
height = tile.Height;
|
|
}
|
|
else
|
|
{
|
|
if (!mapHeight.TryGetValue(cPos, out height))
|
|
height = byte.MinValue;
|
|
|
|
// If a tile has inherent height, we know it will raise terrain.
|
|
if (filters.HasFlag(MapBlitFilters.Terrain) && stickToGround)
|
|
height += map.Rules.TerrainInfo.GetTerrainInfo(tile.TerrainTile).Height;
|
|
}
|
|
|
|
var wPos = CellLayerUtils.CPosToWPos(cPos, height, mapGrid.Type);
|
|
var preview = resourceRenderers
|
|
.SelectMany(r => r.RenderPreview(wr, tile.ResourceLayerContents.Value.Type, wPos));
|
|
|
|
foreach (var renderable in preview)
|
|
yield return renderable;
|
|
}
|
|
}
|
|
|
|
if (filters.HasFlag(MapBlitFilters.Actors))
|
|
{
|
|
foreach (var (_, editorActorPreview) in blitSource.Actors)
|
|
{
|
|
var useGround = stickToGround;
|
|
if (!filters.HasFlag(MapBlitFilters.Terrain) || !blitSource.Tiles.ContainsKey(editorActorPreview.Location))
|
|
useGround = true;
|
|
|
|
var wOffset = CellLayerUtils.CVecToWVec(offset, 0, mapGrid.Type);
|
|
if (useGround)
|
|
{
|
|
var actorPos = editorActorPreview.CenterPosition + wOffset;
|
|
wOffset -= new WVec(0, 0, map.DistanceAboveTerrain(actorPos).Length);
|
|
}
|
|
|
|
var preview = editorActorPreview.RenderWithOffset(wOffset)
|
|
.OrderBy(WorldRenderer.RenderableZPositionComparisonKey);
|
|
|
|
foreach (var renderable in preview)
|
|
yield return renderable;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find the set of cells within an EditorBlitSource that are actually occupied by a
|
|
/// BlitTile or actor. Note that all tiles must be inside the CellRegion, and actors must
|
|
/// be at least partially inside the CellRegion. If an actor partially lies outside of the
|
|
/// CellRegion, only cells within the CellRegion are included in the output set.
|
|
/// </summary>
|
|
static HashSet<CPos> GetBlitSourceMask(
|
|
EditorBlitSource blitSource,
|
|
CVec offset)
|
|
{
|
|
var mask = new HashSet<CPos>();
|
|
|
|
var sourceCellCoords = blitSource.CellCoords;
|
|
|
|
foreach (var (cpos, _) in blitSource.Tiles)
|
|
{
|
|
if (!sourceCellCoords.Contains(cpos))
|
|
throw new ArgumentException("EditorBlitSource contains a BlitTile outside of its CellRegion");
|
|
mask.Add(cpos + offset);
|
|
}
|
|
|
|
foreach (var (_, editorActorPreview) in blitSource.Actors)
|
|
{
|
|
var anyContained = false;
|
|
foreach (var cpos in editorActorPreview.Footprint.Keys)
|
|
{
|
|
if (sourceCellCoords.Contains(cpos))
|
|
{
|
|
mask.Add(cpos + offset);
|
|
anyContained = true;
|
|
}
|
|
}
|
|
|
|
if (!anyContained)
|
|
throw new ArgumentException("EditorBlitSource contains an actor entirely outside of its CellRegion");
|
|
}
|
|
|
|
return mask;
|
|
}
|
|
|
|
public void Commit() => Blit(false);
|
|
public void Revert() => Blit(true);
|
|
|
|
public int TileCount()
|
|
{
|
|
return commitBlitSource.Tiles.Count;
|
|
}
|
|
|
|
public int ActorCount()
|
|
{
|
|
return commitBlitSource.Actors.Count;
|
|
}
|
|
}
|
|
}
|