Move default tileset parsing to Mods.Common.

This commit is contained in:
Paul Chote
2020-12-30 18:44:09 +00:00
committed by reaperrr
parent 207e09fea9
commit 53db1230ab
19 changed files with 404 additions and 355 deletions

View File

@@ -14,6 +14,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Mods.Common.Traits;
namespace OpenRA.Mods.Common.Widgets

View File

@@ -9,8 +9,12 @@
*/
#endregion
using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenRA.FileSystem;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Support;
namespace OpenRA.Mods.Common.Terrain
{
@@ -20,7 +24,166 @@ namespace OpenRA.Mods.Common.Terrain
public ITerrainInfo ParseTerrain(IReadOnlyFileSystem fileSystem, string path)
{
return new TileSet(fileSystem, path);
return new DefaultTerrain(fileSystem, path);
}
}
public class DefaultTerrainTileInfo : TerrainTileInfo
{
public readonly float ZOffset = 0.0f;
public readonly float ZRamp = 1.0f;
}
public class DefaultTerrainTemplateInfo : TerrainTemplateInfo
{
public readonly string[] Images;
public readonly int[] Frames;
public readonly string Palette;
public DefaultTerrainTemplateInfo(ITerrainInfo terrainInfo, MiniYaml my)
: base(terrainInfo, my) { }
protected override TerrainTileInfo LoadTileInfo(ITerrainInfo terrainInfo, MiniYaml my)
{
var tile = new DefaultTerrainTileInfo();
FieldLoader.Load(tile, my);
// Terrain type must be converted from a string to an index
tile.GetType().GetField("TerrainType").SetValue(tile, terrainInfo.GetTerrainIndex(my.Value));
// Fall back to the terrain-type color if necessary
var overrideColor = terrainInfo.TerrainTypes[tile.TerrainType].Color;
if (tile.MinColor == default)
tile.GetType().GetField("MinColor").SetValue(tile, overrideColor);
if (tile.MaxColor == default)
tile.GetType().GetField("MaxColor").SetValue(tile, overrideColor);
return tile;
}
}
public class DefaultTerrain : ITemplatedTerrainInfo, ITerrainInfoNotifyMapCreated
{
public readonly string Name;
public readonly string Id;
public readonly int SheetSize = 512;
public readonly Color[] HeightDebugColors = { Color.Red };
public readonly string[] EditorTemplateOrder;
public readonly bool IgnoreTileSpriteOffsets;
public readonly bool EnableDepth = false;
public readonly float MinHeightColorBrightness = 1.0f;
public readonly float MaxHeightColorBrightness = 1.0f;
[FieldLoader.Ignore]
public readonly IReadOnlyDictionary<ushort, TerrainTemplateInfo> Templates;
[FieldLoader.Ignore]
public readonly TerrainTypeInfo[] TerrainInfo;
readonly Dictionary<string, byte> terrainIndexByType = new Dictionary<string, byte>();
readonly byte defaultWalkableTerrainIndex;
public DefaultTerrain(IReadOnlyFileSystem fileSystem, string filepath)
{
var yaml = MiniYaml.FromStream(fileSystem.Open(filepath), filepath)
.ToDictionary(x => x.Key, x => x.Value);
// General info
FieldLoader.Load(this, yaml["General"]);
// TerrainTypes
TerrainInfo = yaml["Terrain"].ToDictionary().Values
.Select(y => new TerrainTypeInfo(y))
.OrderBy(tt => tt.Type)
.ToArray();
if (TerrainInfo.Length >= byte.MaxValue)
throw new YamlException("Too many terrain types.");
for (byte i = 0; i < TerrainInfo.Length; i++)
{
var tt = TerrainInfo[i].Type;
if (terrainIndexByType.ContainsKey(tt))
throw new YamlException("Duplicate terrain type '{0}' in '{1}'.".F(tt, filepath));
terrainIndexByType.Add(tt, i);
}
defaultWalkableTerrainIndex = GetTerrainIndex("Clear");
// Templates
Templates = yaml["Templates"].ToDictionary().Values
.Select(y => (TerrainTemplateInfo)new DefaultTerrainTemplateInfo(this, y)).ToDictionary(t => t.Id).AsReadOnly();
}
public TerrainTypeInfo this[byte index]
{
get { return TerrainInfo[index]; }
}
public byte GetTerrainIndex(string type)
{
if (terrainIndexByType.TryGetValue(type, out var index))
return index;
throw new InvalidDataException("Tileset '{0}' lacks terrain type '{1}'".F(Id, type));
}
public byte GetTerrainIndex(TerrainTile r)
{
var tile = Templates[r.Type][r.Index];
if (tile.TerrainType != byte.MaxValue)
return tile.TerrainType;
return defaultWalkableTerrainIndex;
}
public TerrainTileInfo GetTileInfo(TerrainTile r)
{
return Templates[r.Type][r.Index];
}
public bool TryGetTileInfo(TerrainTile r, out TerrainTileInfo info)
{
if (!Templates.TryGetValue(r.Type, out var tpl) || !tpl.Contains(r.Index))
{
info = null;
return false;
}
info = tpl[r.Index];
return info != null;
}
string ITerrainInfo.Id { get { return Id; } }
TerrainTypeInfo[] ITerrainInfo.TerrainTypes { get { return TerrainInfo; } }
TerrainTileInfo ITerrainInfo.GetTerrainInfo(TerrainTile r) { return GetTileInfo(r); }
bool ITerrainInfo.TryGetTerrainInfo(TerrainTile r, out TerrainTileInfo info) { return TryGetTileInfo(r, out info); }
Color[] ITerrainInfo.HeightDebugColors { get { return HeightDebugColors; } }
IEnumerable<Color> ITerrainInfo.RestrictedPlayerColors { get { return TerrainInfo.Where(ti => ti.RestrictPlayerColor).Select(ti => ti.Color); } }
float ITerrainInfo.MinHeightColorBrightness { get { return MinHeightColorBrightness; } }
float ITerrainInfo.MaxHeightColorBrightness { get { return MaxHeightColorBrightness; } }
TerrainTile ITerrainInfo.DefaultTerrainTile { get { return new TerrainTile(Templates.First().Key, 0); } }
string[] ITemplatedTerrainInfo.EditorTemplateOrder { get { return EditorTemplateOrder; } }
IReadOnlyDictionary<ushort, TerrainTemplateInfo> ITemplatedTerrainInfo.Templates { get { return Templates; } }
void ITerrainInfoNotifyMapCreated.MapCreated(Map map)
{
// Randomize PickAny tile variants
var r = new MersenneTwister();
for (var j = map.Bounds.Top; j < map.Bounds.Bottom; j++)
{
for (var i = map.Bounds.Left; i < map.Bounds.Right; i++)
{
var type = map.Tiles[new MPos(i, j)].Type;
if (!Templates.TryGetValue(type, out var template) || !template.PickAny)
continue;
map.Tiles[new MPos(i, j)] = new TerrainTile(type, (byte)r.Next(0, template.TilesCount));
}
}
}
}
}

View File

@@ -0,0 +1,172 @@
#region Copyright & License Information
/*
* Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
* 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 System.Linq;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Support;
namespace OpenRA.Mods.Common.Terrain
{
public class TheaterTemplate
{
public readonly Sprite[] Sprites;
public readonly int Stride;
public readonly int Variants;
public TheaterTemplate(Sprite[] sprites, int stride, int variants)
{
Sprites = sprites;
Stride = stride;
Variants = variants;
}
}
public sealed class DefaultTileCache : IDisposable
{
readonly Dictionary<ushort, TheaterTemplate> templates = new Dictionary<ushort, TheaterTemplate>();
SheetBuilder sheetBuilder;
readonly Sprite missingTile;
readonly MersenneTwister random;
public DefaultTileCache(DefaultTerrain terrainInfo, Action<uint, string> onMissingImage = null)
{
var allocated = false;
Func<Sheet> allocate = () =>
{
if (allocated)
throw new SheetOverflowException("Terrain sheet overflow. Try increasing the tileset SheetSize parameter.");
allocated = true;
return new Sheet(SheetType.Indexed, new Size(terrainInfo.SheetSize, terrainInfo.SheetSize));
};
random = new MersenneTwister();
var frameCache = new FrameCache(Game.ModData.DefaultFileSystem, Game.ModData.SpriteLoaders);
foreach (var t in terrainInfo.Templates)
{
var variants = new List<Sprite[]>();
var templateInfo = (DefaultTerrainTemplateInfo)t.Value;
foreach (var i in templateInfo.Images)
{
ISpriteFrame[] allFrames;
if (onMissingImage != null)
{
try
{
allFrames = frameCache[i];
}
catch (FileNotFoundException)
{
onMissingImage(t.Key, i);
continue;
}
}
else
allFrames = frameCache[i];
var frameCount = terrainInfo.EnableDepth ? allFrames.Length / 2 : allFrames.Length;
var indices = templateInfo.Frames != null ? templateInfo.Frames : Exts.MakeArray(t.Value.TilesCount, j => j);
var start = indices.Min();
var end = indices.Max();
if (start < 0 || end >= frameCount)
throw new YamlException("Template `{0}` uses frames [{1}..{2}] of {3}, but only [0..{4}] actually exist"
.F(t.Key, start, end, i, frameCount - 1));
variants.Add(indices.Select(j =>
{
var f = allFrames[j];
var tile = t.Value.Contains(j) ? (DefaultTerrainTileInfo)t.Value[j] : null;
// The internal z axis is inverted from expectation (negative is closer)
var zOffset = tile != null ? -tile.ZOffset : 0;
var zRamp = tile != null ? tile.ZRamp : 1f;
var offset = new float3(f.Offset, zOffset);
var type = SheetBuilder.FrameTypeToSheetType(f.Type);
// Defer SheetBuilder creation until we know what type of frames we are loading!
// TODO: Support mixed indexed and BGRA frames
if (sheetBuilder == null)
sheetBuilder = new SheetBuilder(SheetBuilder.FrameTypeToSheetType(f.Type), allocate);
else if (type != sheetBuilder.Type)
throw new YamlException("Sprite type mismatch. Terrain sprites must all be either Indexed or RGBA.");
var s = sheetBuilder.Allocate(f.Size, zRamp, offset);
OpenRA.Graphics.Util.FastCopyIntoChannel(s, f.Data, f.Type);
if (terrainInfo.EnableDepth)
{
var depthFrame = allFrames[j + frameCount];
var ss = sheetBuilder.Allocate(f.Size, zRamp, offset);
OpenRA.Graphics.Util.FastCopyIntoChannel(ss, depthFrame.Data, depthFrame.Type);
// s and ss are guaranteed to use the same sheet
// because of the custom terrain sheet allocation
s = new SpriteWithSecondaryData(s, s.Sheet, ss.Bounds, ss.Channel);
}
return s;
}).ToArray());
}
var allSprites = variants.SelectMany(s => s);
// Ignore the offsets baked into R8 sprites
if (terrainInfo.IgnoreTileSpriteOffsets)
allSprites = allSprites.Select(s => new Sprite(s.Sheet, s.Bounds, s.ZRamp, new float3(float2.Zero, s.Offset.Z), s.Channel, s.BlendMode));
if (onMissingImage != null && !variants.Any())
continue;
templates.Add(t.Value.Id, new TheaterTemplate(allSprites.ToArray(), variants.First().Count(), templateInfo.Images.Length));
}
// 1x1px transparent tile
if (sheetBuilder.Type == SheetType.BGRA)
missingTile = sheetBuilder.Add(new byte[4], SpriteFrameType.Bgra32, new Size(1, 1));
else
missingTile = sheetBuilder.Add(new byte[1], SpriteFrameType.Indexed8, new Size(1, 1));
Sheet.ReleaseBuffer();
}
public bool HasTileSprite(TerrainTile r, int? variant = null)
{
return TileSprite(r, variant) != missingTile;
}
public Sprite TileSprite(TerrainTile r, int? variant = null)
{
if (!templates.TryGetValue(r.Type, out var template))
return missingTile;
if (r.Index >= template.Stride)
return missingTile;
var start = template.Variants > 1 ? variant.HasValue ? variant.Value : random.Next(template.Variants) : 0;
return template.Sprites[start * template.Stride + r.Index];
}
public Sheet Sheet { get { return sheetBuilder.Current; } }
public void Dispose()
{
sheetBuilder.Dispose();
}
}
}

View File

@@ -0,0 +1,103 @@
#region Copyright & License Information
/*
* Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
* 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
namespace OpenRA.Mods.Common.Terrain
{
public interface ITemplatedTerrainInfo : ITerrainInfo
{
string[] EditorTemplateOrder { get; }
IReadOnlyDictionary<ushort, TerrainTemplateInfo> Templates { get; }
}
public interface ITerrainInfoNotifyMapCreated : ITerrainInfo
{
void MapCreated(Map map);
}
public class TerrainTemplateInfo
{
public readonly ushort Id;
public readonly int2 Size;
public readonly bool PickAny;
public readonly string[] Categories;
readonly TerrainTileInfo[] tileInfo;
public TerrainTemplateInfo(ITerrainInfo terrainInfo, MiniYaml my)
{
FieldLoader.Load(this, my);
var nodes = my.ToDictionary()["Tiles"].Nodes;
if (!PickAny)
{
tileInfo = new TerrainTileInfo[Size.X * Size.Y];
foreach (var node in nodes)
{
if (!int.TryParse(node.Key, out var key))
throw new YamlException("Tileset `{0}` template `{1}` defines a frame `{2}` that is not a valid integer.".F(terrainInfo.Id, Id, node.Key));
if (key < 0 || key >= tileInfo.Length)
throw new YamlException("Tileset `{0}` template `{1}` references frame {2}, but only [0..{3}] are valid for a {4}x{5} Size template.".F(terrainInfo.Id, Id, key, tileInfo.Length - 1, Size.X, Size.Y));
tileInfo[key] = LoadTileInfo(terrainInfo, node.Value);
}
}
else
{
tileInfo = new TerrainTileInfo[nodes.Count];
var i = 0;
foreach (var node in nodes)
{
if (!int.TryParse(node.Key, out var key))
throw new YamlException("Tileset `{0}` template `{1}` defines a frame `{2}` that is not a valid integer.".F(terrainInfo.Id, Id, node.Key));
if (key != i++)
throw new YamlException("Tileset `{0}` template `{1}` is missing a definition for frame {2}.".F(terrainInfo.Id, Id, i - 1));
tileInfo[key] = LoadTileInfo(terrainInfo, node.Value);
}
}
}
protected virtual TerrainTileInfo LoadTileInfo(ITerrainInfo terrainInfo, MiniYaml my)
{
var tile = new TerrainTileInfo();
FieldLoader.Load(tile, my);
// Terrain type must be converted from a string to an index
tile.GetType().GetField("TerrainType").SetValue(tile, terrainInfo.GetTerrainIndex(my.Value));
// Fall back to the terrain-type color if necessary
var overrideColor = terrainInfo.TerrainTypes[tile.TerrainType].Color;
if (tile.MinColor == default)
tile.GetType().GetField("MinColor").SetValue(tile, overrideColor);
if (tile.MaxColor == default)
tile.GetType().GetField("MaxColor").SetValue(tile, overrideColor);
return tile;
}
public TerrainTileInfo this[int index] { get { return tileInfo[index]; } }
public bool Contains(int index)
{
return index >= 0 && index < tileInfo.Length;
}
public int TilesCount
{
get { return tileInfo.Length; }
}
}
}

View File

@@ -16,6 +16,7 @@ using System.Linq;
using OpenRA.Effects;
using OpenRA.GameRules;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Primitives;
using OpenRA.Traits;

View File

@@ -12,6 +12,7 @@
using System.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits

View File

@@ -13,6 +13,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Primitives;
using OpenRA.Traits;

View File

@@ -13,6 +13,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Primitives;
using OpenRA.Traits;
@@ -31,19 +32,20 @@ namespace OpenRA.Mods.Common.Traits
failed = true;
};
var tileCache = new Theater((TileSet)terrainInfo, onMissingImage);
var tileCache = new DefaultTileCache((DefaultTerrain)terrainInfo, onMissingImage);
foreach (var t in terrainInfo.Templates)
{
for (var v = 0; v < t.Value.Images.Length; v++)
var templateInfo = (DefaultTerrainTemplateInfo)t.Value;
for (var v = 0; v < templateInfo.Images.Length; v++)
{
if (!missingImages.Contains(t.Value.Images[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 `{0}` references frame {1} that does not exist in sprite `{2}`.".F(t.Key, i, t.Value.Images[v]));
onError("\tTemplate `{0}` references frame {1} that does not exist in sprite `{2}`.".F(t.Key, i, templateInfo.Images[v]));
failed = true;
}
}
@@ -60,25 +62,26 @@ namespace OpenRA.Mods.Common.Traits
{
readonly Map map;
readonly Dictionary<string, TerrainSpriteLayer> spriteLayers = new Dictionary<string, TerrainSpriteLayer>();
readonly TileSet terrainInfo;
readonly Theater tileCache;
readonly DefaultTerrain terrainInfo;
readonly DefaultTileCache tileCache;
bool disposed;
public TerrainRenderer(World world)
{
map = world.Map;
terrainInfo = map.Rules.TerrainInfo as TileSet;
terrainInfo = map.Rules.TerrainInfo as DefaultTerrain;
if (terrainInfo == null)
throw new InvalidDataException("TerrainRenderer can only be used with the default TileSet");
throw new InvalidDataException("TerrainRenderer can only be used with the DefaultTerrain parser");
tileCache = new Theater(terrainInfo);
tileCache = new DefaultTileCache(terrainInfo);
}
void IWorldLoaded.WorldLoaded(World world, WorldRenderer wr)
{
foreach (var template in terrainInfo.Templates)
{
var palette = template.Value.Palette ?? TileSet.TerrainPaletteInternalName;
var templateInfo = (DefaultTerrainTemplateInfo)template.Value;
var palette = templateInfo.Palette ?? TileSet.TerrainPaletteInternalName;
spriteLayers.GetOrAdd(palette, pal =>
new TerrainSpriteLayer(world, wr, tileCache.Sheet, BlendMode.Alpha, wr.Palette(palette), world.Type != WorldType.Editor));
}
@@ -95,7 +98,7 @@ namespace OpenRA.Mods.Common.Traits
var tile = map.Tiles[cell];
var palette = TileSet.TerrainPaletteInternalName;
if (terrainInfo.Templates.TryGetValue(tile.Type, out var template))
palette = template.Palette ?? palette;
palette = ((DefaultTerrainTemplateInfo)template).Palette ?? palette;
foreach (var kv in spriteLayers)
kv.Value.Update(cell, palette == kv.Key ? tileCache.TileSprite(tile) : null, false);
@@ -159,8 +162,9 @@ namespace OpenRA.Mods.Common.Traits
return templateRect ?? Rectangle.Empty;
}
IEnumerable<IRenderable> ITiledTerrainRenderer.RenderUIPreview(WorldRenderer wr, TerrainTemplateInfo template, int2 origin, float scale)
IEnumerable<IRenderable> ITiledTerrainRenderer.RenderUIPreview(WorldRenderer wr, TerrainTemplateInfo t, int2 origin, float scale)
{
var template = t as DefaultTerrainTemplateInfo;
if (template == null)
yield break;
@@ -187,8 +191,11 @@ namespace OpenRA.Mods.Common.Traits
}
}
IEnumerable<IRenderable> ITiledTerrainRenderer.RenderPreview(WorldRenderer wr, TerrainTemplateInfo template, WPos origin)
IEnumerable<IRenderable> ITiledTerrainRenderer.RenderPreview(WorldRenderer wr, TerrainTemplateInfo t, WPos origin)
{
if (!(t is DefaultTerrainTemplateInfo template))
yield break;
var i = 0;
for (var y = 0; y < template.Size.Y; y++)
{

View File

@@ -15,6 +15,7 @@ using OpenRA.Activities;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Activities;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Primitives;
using OpenRA.Traits;

View File

@@ -10,9 +10,8 @@
#endregion
using System;
using System.Collections.Generic;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Mods.Common.Traits;
namespace OpenRA.Mods.Common.UtilityCommands

View File

@@ -16,6 +16,7 @@ using System.Linq;
using System.Text;
using OpenRA.FileSystem;
using OpenRA.Mods.Common.FileFormats;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;

View File

@@ -11,6 +11,7 @@
using System;
using System.Linq;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets.Logic

View File

@@ -13,8 +13,8 @@ using System;
using System.IO;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets.Logic

View File

@@ -12,9 +12,9 @@
using System;
using System.IO;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets