Files
OpenRA/OpenRA.Mods.Common/Traits/World/TerrainRenderer.cs
Ashley Newson ad08045e5f Add path auto-tiling tool to map editor
Exposes the TilingPath code used by the map generator as a map editor
tool to help automate the process of tiling sequences of templates
together.

No additional template stitching definitions are added as part of this
commit. As such, the tool currently only supports the same auto tiling
as the map generator does. This includes:

- RA and CnC support.
- Beaches, land cliffs, and roads.

Support for additional mods and templates can be added in follow-ups
by adding the necessary MultiBrush definitions.

Related changes included in this commit to support this work include:

- Add support for sparse EditorBlits.
- Add support for MultiBrush to EditorBlitSource conversion.
- Add support for EditorBlitSource previewing.
- Adjust the "Road" types used in segmented MultiBrush definitions.
2025-05-15 22:41:43 +03:00

223 lines
7.1 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.IO;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[TraitLocation(SystemActors.World | SystemActors.EditorWorld)]
public class TerrainRendererInfo : TraitInfo, ITiledTerrainRendererInfo
{
bool ITiledTerrainRendererInfo.ValidateTileSprites(ITemplatedTerrainInfo terrainInfo, Action<string> onError)
{
var missingImages = new HashSet<string>();
var failed = false;
void OnMissingImage(uint id, string f)
{
onError($"\tTemplate `{id}` references sprite `{f}` that does not exist.");
missingImages.Add(f);
failed = true;
}
var tileCache = new DefaultTileCache((DefaultTerrain)terrainInfo, OnMissingImage);
foreach (var t in terrainInfo.Templates)
{
var templateInfo = (DefaultTerrainTemplateInfo)t.Value;
for (var v = 0; v < templateInfo.Images.Length; v++)
{
if (!missingImages.Contains(templateInfo.Images[v]))
{
for (var i = 0; i < t.Value.TilesCount; i++)
{
if (t.Value[i] == null || tileCache.HasTileSprite(new TerrainTile(t.Key, (byte)i), v))
continue;
onError($"\tTemplate `{t.Key}` references frame {i} that does not exist in sprite `{templateInfo.Images[v]}`.");
failed = true;
}
}
}
}
return failed;
}
public override object Create(ActorInitializer init) { return new TerrainRenderer(init.World); }
}
public sealed class TerrainRenderer : IRenderTerrain, IWorldLoaded, INotifyActorDisposing, ITiledTerrainRenderer
{
readonly Map map;
TerrainSpriteLayer spriteLayer;
readonly DefaultTerrain terrainInfo;
readonly DefaultTileCache tileCache;
WorldRenderer worldRenderer;
bool disposed;
public TerrainRenderer(World world)
{
map = world.Map;
terrainInfo = map.Rules.TerrainInfo as DefaultTerrain;
if (terrainInfo == null)
throw new InvalidDataException("TerrainRenderer can only be used with the DefaultTerrain parser");
tileCache = new DefaultTileCache(terrainInfo);
}
void IWorldLoaded.WorldLoaded(World world, WorldRenderer wr)
{
worldRenderer = wr;
spriteLayer = new TerrainSpriteLayer(world, wr, tileCache.MissingTile, BlendMode.Alpha, world.Type != WorldType.Editor);
foreach (var cell in map.AllCells)
UpdateCell(cell);
map.Tiles.CellEntryChanged += UpdateCell;
map.Height.CellEntryChanged += UpdateCell;
}
public void UpdateCell(CPos cell)
{
var tile = map.Tiles[cell];
var palette = terrainInfo.Palette;
if (terrainInfo.Templates.TryGetValue(tile.Type, out var template))
palette = ((DefaultTerrainTemplateInfo)template).Palette ?? palette;
var sprite = tileCache.TileSprite(tile);
var paletteReference = worldRenderer.Palette(palette);
spriteLayer.Update(cell, sprite, paletteReference);
}
void IRenderTerrain.RenderTerrain(WorldRenderer wr, Viewport viewport)
{
spriteLayer.Draw(wr.Viewport);
foreach (var r in wr.World.WorldActor.TraitsImplementing<IRenderOverlay>())
r.Render(wr);
}
void INotifyActorDisposing.Disposing(Actor self)
{
if (disposed)
return;
map.Tiles.CellEntryChanged -= UpdateCell;
map.Height.CellEntryChanged -= UpdateCell;
spriteLayer.Dispose();
tileCache.Dispose();
disposed = true;
}
Sprite ITiledTerrainRenderer.MissingTile => tileCache.MissingTile;
Sprite ITiledTerrainRenderer.TileSprite(TerrainTile r, int? variant)
{
return tileCache.TileSprite(r, variant);
}
Rectangle ITiledTerrainRenderer.TemplateBounds(TerrainTemplateInfo template)
{
Rectangle? templateRect = null;
var tileSize = map.Rules.TerrainInfo.TileSize;
var i = 0;
for (var y = 0; y < template.Size.Y; y++)
{
for (var x = 0; x < template.Size.X; x++)
{
var tile = new TerrainTile(template.Id, (byte)i++);
if (!terrainInfo.TryGetTileInfo(tile, out var tileInfo))
continue;
var sprite = tileCache.TileSprite(tile);
var u = map.Grid.Type == MapGridType.Rectangular ? x : (x - y) / 2f;
var v = map.Grid.Type == MapGridType.Rectangular ? y : (x + y) / 2f;
var tl = new float2(u * tileSize.Width, (v - 0.5f * tileInfo.Height) * tileSize.Height) - 0.5f * sprite.Size;
var rect = new Rectangle((int)(tl.X + sprite.Offset.X), (int)(tl.Y + sprite.Offset.Y), (int)sprite.Size.X, (int)sprite.Size.Y);
templateRect = templateRect.HasValue ? Rectangle.Union(templateRect.Value, rect) : rect;
}
}
return templateRect ?? Rectangle.Empty;
}
IEnumerable<IRenderable> ITiledTerrainRenderer.RenderUIPreview(WorldRenderer wr, TerrainTemplateInfo t, int2 origin, float scale)
{
if (t is not DefaultTerrainTemplateInfo template)
yield break;
var ts = map.Rules.TerrainInfo.TileSize;
var gridType = map.Grid.Type;
var i = 0;
for (var y = 0; y < template.Size.Y; y++)
{
for (var x = 0; x < template.Size.X; x++)
{
var tile = new TerrainTile(template.Id, (byte)i++);
if (!terrainInfo.TryGetTileInfo(tile, out var tileInfo))
continue;
var sprite = tileCache.TileSprite(tile, 0);
var u = gridType == MapGridType.Rectangular ? x : (x - y) / 2f;
var v = gridType == MapGridType.Rectangular ? y : (x + y) / 2f;
var offset = scale * (new float2(u * ts.Width, (v - 0.5f * tileInfo.Height) * ts.Height) - 0.5f * sprite.Size.XY);
var palette = template.Palette ?? terrainInfo.Palette;
yield return new UISpriteRenderable(sprite, WPos.Zero, origin + offset.ToInt2(), 0, wr.Palette(palette), scale);
}
}
}
IEnumerable<IRenderable> ITiledTerrainRenderer.RenderPreview(WorldRenderer wr, TerrainTemplateInfo t, WPos origin)
{
if (t is not DefaultTerrainTemplateInfo template)
yield break;
var i = 0;
for (var y = 0; y < template.Size.Y; y++)
{
for (var x = 0; x < template.Size.X; x++)
{
var tile = new TerrainTile(template.Id, (byte)i++);
if (!terrainInfo.TryGetTileInfo(tile, out var tileInfo))
continue;
var sprite = tileCache.TileSprite(tile, 0);
var offset = map.Offset(new CVec(x, y), tileInfo.Height);
var palette = wr.Palette(template.Palette ?? terrainInfo.Palette);
yield return new SpriteRenderable(sprite, origin, offset, 0, palette, 1f, 1f, float3.Ones, TintModifiers.None, false);
}
}
}
IEnumerable<IRenderable> ITiledTerrainRenderer.RenderPreview(WorldRenderer wr, TerrainTile tile, WPos origin)
{
if (!terrainInfo.TryGetTileInfo(tile, out var tileInfo))
yield break;
var sprite = tileCache.TileSprite(tile, 0);
var offset = map.Offset(new CVec(0, 0), tileInfo.Height);
var palette = wr.Palette(terrainInfo.Palette);
yield return new SpriteRenderable(sprite, origin, offset, 0, palette, 1f, 1f, float3.Ones, TintModifiers.None, false);
}
}
}