Merge pull request #7631 from pchote/sequence-rework

Rework sequence parsing.
This commit is contained in:
Pavel Penev
2015-03-14 00:13:03 +02:00
13 changed files with 157 additions and 61 deletions

View File

@@ -16,7 +16,7 @@ namespace OpenRA.Graphics
public class Animation public class Animation
{ {
readonly int defaultTick = 40; // 25 fps == 40 ms readonly int defaultTick = 40; // 25 fps == 40 ms
public Sequence CurrentSequence { get; private set; } public ISpriteSequence CurrentSequence { get; private set; }
public bool IsDecoration = false; public bool IsDecoration = false;
public Func<bool> Paused; public Func<bool> Paused;
@@ -177,7 +177,7 @@ namespace OpenRA.Graphics
} }
} }
public Sequence GetSequence(string sequenceName) public ISpriteSequence GetSequence(string sequenceName)
{ {
return sequenceProvider.GetSequence(name, sequenceName); return sequenceProvider.GetSequence(name, sequenceName);
} }

View File

@@ -12,11 +12,35 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
namespace OpenRA.Graphics namespace OpenRA.Graphics
{ {
using Sequences = IReadOnlyDictionary<string, Lazy<IReadOnlyDictionary<string, Sequence>>>; using Sequences = IReadOnlyDictionary<string, Lazy<IReadOnlyDictionary<string, ISpriteSequence>>>;
using UnitSequences = Lazy<IReadOnlyDictionary<string, Sequence>>; using UnitSequences = Lazy<IReadOnlyDictionary<string, ISpriteSequence>>;
public interface ISpriteSequence
{
string Name { get; }
int Start { get; }
int Length { get; }
int Stride { get; }
int Facings { get; }
int Tick { get; }
int ZOffset { get; }
int ShadowStart { get; }
int ShadowZOffset { get; }
int[] Frames { get; }
Sprite GetSprite(int frame);
Sprite GetSprite(int frame, int facing);
Sprite GetShadow(int frame, int facing);
}
public interface ISpriteSequenceLoader
{
IReadOnlyDictionary<string, ISpriteSequence> ParseSequences(ModData modData, TileSet tileSet, SpriteCache cache, MiniYamlNode node);
}
public class SequenceProvider public class SequenceProvider
{ {
@@ -29,13 +53,13 @@ namespace OpenRA.Graphics
this.SpriteCache = cache.SpriteCache; this.SpriteCache = cache.SpriteCache;
} }
public Sequence GetSequence(string unitName, string sequenceName) public ISpriteSequence GetSequence(string unitName, string sequenceName)
{ {
UnitSequences unitSeq; UnitSequences unitSeq;
if (!sequences.Value.TryGetValue(unitName, out unitSeq)) if (!sequences.Value.TryGetValue(unitName, out unitSeq))
throw new InvalidOperationException("Unit `{0}` does not have any sequences defined.".F(unitName)); throw new InvalidOperationException("Unit `{0}` does not have any sequences defined.".F(unitName));
Sequence seq; ISpriteSequence seq;
if (!unitSeq.Value.TryGetValue(sequenceName, out seq)) if (!unitSeq.Value.TryGetValue(sequenceName, out seq))
throw new InvalidOperationException("Unit `{0}` does not have a sequence named `{1}`".F(unitName, sequenceName)); throw new InvalidOperationException("Unit `{0}` does not have a sequence named `{1}`".F(unitName, sequenceName));
@@ -77,6 +101,7 @@ namespace OpenRA.Graphics
public sealed class SequenceCache : IDisposable public sealed class SequenceCache : IDisposable
{ {
readonly ModData modData; readonly ModData modData;
readonly TileSet tileSet;
readonly Lazy<SpriteCache> spriteCache; readonly Lazy<SpriteCache> spriteCache;
public SpriteCache SpriteCache { get { return spriteCache.Value; } } public SpriteCache SpriteCache { get { return spriteCache.Value; } }
@@ -85,7 +110,9 @@ namespace OpenRA.Graphics
public SequenceCache(ModData modData, TileSet tileSet) public SequenceCache(ModData modData, TileSet tileSet)
{ {
this.modData = modData; this.modData = modData;
this.tileSet = tileSet;
// Every time we load a tile set, we create a sequence cache for it
spriteCache = Exts.Lazy(() => new SpriteCache(modData.SpriteLoaders, tileSet.Extensions, new SheetBuilder(SheetType.Indexed))); spriteCache = Exts.Lazy(() => new SpriteCache(modData.SpriteLoaders, tileSet.Extensions, new SheetBuilder(SheetType.Indexed)));
} }
@@ -116,7 +143,7 @@ namespace OpenRA.Graphics
items.Add(node.Key, t); items.Add(node.Key, t);
else else
{ {
t = Exts.Lazy(() => CreateUnitSequences(node)); t = Exts.Lazy(() => modData.SpriteSequenceLoader.ParseSequences(modData, tileSet, SpriteCache, node));
sequenceCache.Add(key, t); sequenceCache.Add(key, t);
items.Add(node.Key, t); items.Add(node.Key, t);
} }
@@ -125,28 +152,6 @@ namespace OpenRA.Graphics
return new ReadOnlyDictionary<string, UnitSequences>(items); return new ReadOnlyDictionary<string, UnitSequences>(items);
} }
IReadOnlyDictionary<string, Sequence> CreateUnitSequences(MiniYamlNode node)
{
var unitSequences = new Dictionary<string, Sequence>();
foreach (var kvp in node.Value.ToDictionary())
{
using (new Support.PerfTimer("new Sequence(\"{0}\")".F(node.Key), 20))
{
try
{
unitSequences.Add(kvp.Key, new Sequence(spriteCache.Value, node.Key, kvp.Key, kvp.Value));
}
catch (FileNotFoundException ex)
{
Log.Write("debug", ex.Message);
}
}
}
return new ReadOnlyDictionary<string, Sequence>(unitSequences);
}
public void Dispose() public void Dispose()
{ {
if (spriteCache.IsValueCreated) if (spriteCache.IsValueCreated)

View File

@@ -20,6 +20,17 @@ namespace OpenRA
public enum TileShape { Rectangle, Diamond } public enum TileShape { Rectangle, Diamond }
public interface IGlobalModData { } public interface IGlobalModData { }
public sealed class SpriteSequenceFormat : IGlobalModData
{
public readonly string Type;
public readonly IReadOnlyDictionary<string, MiniYaml> Metadata;
public SpriteSequenceFormat(MiniYaml yaml)
{
Type = yaml.Value;
Metadata = new ReadOnlyDictionary<string, MiniYaml>(yaml.ToDictionary());
}
}
// Describes what is to be loaded in order to run a mod // Describes what is to be loaded in order to run a mod
public class Manifest public class Manifest
{ {
@@ -34,6 +45,7 @@ namespace OpenRA
public readonly IReadOnlyDictionary<string, string> MapFolders; public readonly IReadOnlyDictionary<string, string> MapFolders;
public readonly MiniYaml LoadScreen; public readonly MiniYaml LoadScreen;
public readonly MiniYaml LobbyDefaults; public readonly MiniYaml LobbyDefaults;
public readonly Dictionary<string, Pair<string, int>> Fonts; public readonly Dictionary<string, Pair<string, int>> Fonts;
public readonly Size TileSize = new Size(24, 24); public readonly Size TileSize = new Size(24, 24);
public readonly TileShape TileShape = TileShape.Rectangle; public readonly TileShape TileShape = TileShape.Rectangle;
@@ -92,8 +104,12 @@ namespace OpenRA
Missions = YamlList(yaml, "Missions", true); Missions = YamlList(yaml, "Missions", true);
ServerTraits = YamlList(yaml, "ServerTraits"); ServerTraits = YamlList(yaml, "ServerTraits");
LoadScreen = yaml["LoadScreen"];
LobbyDefaults = yaml["LobbyDefaults"]; if (!yaml.TryGetValue("LoadScreen", out LoadScreen))
throw new InvalidDataException("`LoadScreen` section is not defined.");
if (!yaml.TryGetValue("LobbyDefaults", out LobbyDefaults))
throw new InvalidDataException("`LobbyDefaults` section is not defined.");
Fonts = yaml["Fonts"].ToDictionary(my => Fonts = yaml["Fonts"].ToDictionary(my =>
{ {
@@ -150,8 +166,20 @@ namespace OpenRA
if (t == null || !typeof(IGlobalModData).IsAssignableFrom(t)) if (t == null || !typeof(IGlobalModData).IsAssignableFrom(t))
throw new InvalidDataException("`{0}` is not a valid mod manifest entry.".F(kv.Key)); throw new InvalidDataException("`{0}` is not a valid mod manifest entry.".F(kv.Key));
var module = oc.CreateObject<IGlobalModData>(kv.Key); IGlobalModData module;
FieldLoader.Load(module, kv.Value); var ctor = t.GetConstructor(new Type[] { typeof(MiniYaml) });
if (ctor != null)
{
// Class has opted-in to DIY initialization
module = (IGlobalModData)ctor.Invoke(new object[] { kv.Value });
}
else
{
// Automatically load the child nodes using FieldLoader
module = oc.CreateObject<IGlobalModData>(kv.Key);
FieldLoader.Load(module, kv.Value);
}
modules.Add(module); modules.Add(module);
} }
} }

View File

@@ -25,6 +25,7 @@ namespace OpenRA
public readonly WidgetLoader WidgetLoader; public readonly WidgetLoader WidgetLoader;
public readonly MapCache MapCache; public readonly MapCache MapCache;
public readonly ISpriteLoader[] SpriteLoaders; public readonly ISpriteLoader[] SpriteLoaders;
public readonly ISpriteSequenceLoader SpriteSequenceLoader;
public readonly RulesetCache RulesetCache; public readonly RulesetCache RulesetCache;
public ILoadScreen LoadScreen { get; private set; } public ILoadScreen LoadScreen { get; private set; }
public VoxelLoader VoxelLoader { get; private set; } public VoxelLoader VoxelLoader { get; private set; }
@@ -52,17 +53,25 @@ namespace OpenRA
RulesetCache.LoadingProgress += HandleLoadingProgress; RulesetCache.LoadingProgress += HandleLoadingProgress;
MapCache = new MapCache(this); MapCache = new MapCache(this);
var loaders = new List<ISpriteLoader>(); var spriteLoaders = new List<ISpriteLoader>();
foreach (var format in Manifest.SpriteFormats) foreach (var format in Manifest.SpriteFormats)
{ {
var loader = ObjectCreator.FindType(format + "Loader"); var loader = ObjectCreator.FindType(format + "Loader");
if (loader == null || !loader.GetInterfaces().Contains(typeof(ISpriteLoader))) if (loader == null || !loader.GetInterfaces().Contains(typeof(ISpriteLoader)))
throw new InvalidOperationException("Unable to find a sprite loader for type '{0}'.".F(format)); throw new InvalidOperationException("Unable to find a sprite loader for type '{0}'.".F(format));
loaders.Add((ISpriteLoader)ObjectCreator.CreateBasic(loader)); spriteLoaders.Add((ISpriteLoader)ObjectCreator.CreateBasic(loader));
} }
SpriteLoaders = loaders.ToArray(); SpriteLoaders = spriteLoaders.ToArray();
var sequenceFormat = Manifest.Get<SpriteSequenceFormat>();
var sequenceLoader = ObjectCreator.FindType(sequenceFormat.Type + "Loader");
var ctor = sequenceLoader != null ? sequenceLoader.GetConstructor(new[] { typeof(ModData) }) : null;
if (sequenceLoader == null || !sequenceLoader.GetInterfaces().Contains(typeof(ISpriteSequenceLoader)) || ctor == null)
throw new InvalidOperationException("Unable to find a sequence loader for type '{0}'.".F(sequenceFormat.Type));
SpriteSequenceLoader = (ISpriteSequenceLoader)ctor.Invoke(new[] { this });
// HACK: Mount only local folders so we have a half-working environment for the asset installer // HACK: Mount only local folders so we have a half-working environment for the asset installer
GlobalFileSystem.UnmountAll(); GlobalFileSystem.UnmountAll();

View File

@@ -109,7 +109,6 @@
<Compile Include="Graphics\MappedImage.cs" /> <Compile Include="Graphics\MappedImage.cs" />
<Compile Include="Graphics\Minimap.cs" /> <Compile Include="Graphics\Minimap.cs" />
<Compile Include="Graphics\Renderer.cs" /> <Compile Include="Graphics\Renderer.cs" />
<Compile Include="Graphics\Sequence.cs" />
<Compile Include="Graphics\SequenceProvider.cs" /> <Compile Include="Graphics\SequenceProvider.cs" />
<Compile Include="Graphics\Sheet.cs" /> <Compile Include="Graphics\Sheet.cs" />
<Compile Include="Graphics\SheetBuilder.cs" /> <Compile Include="Graphics\SheetBuilder.cs" />

View File

@@ -9,30 +9,73 @@
#endregion #endregion
using System; using System;
using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using OpenRA.Graphics;
namespace OpenRA.Graphics namespace OpenRA.Mods.Common.Graphics
{ {
public class Sequence public class DefaultSpriteSequenceLoader : ISpriteSequenceLoader
{
public DefaultSpriteSequenceLoader(ModData modData) { }
public virtual ISpriteSequence CreateSequence(ModData modData, TileSet tileSet, SpriteCache cache, string sequence, string animation, MiniYaml info)
{
return new DefaultSpriteSequence(modData, tileSet, cache, this, sequence, animation, info);
}
public IReadOnlyDictionary<string, ISpriteSequence> ParseSequences(ModData modData, TileSet tileSet, SpriteCache cache, MiniYamlNode node)
{
var sequences = new Dictionary<string, ISpriteSequence>();
foreach (var kvp in node.Value.ToDictionary())
{
using (new Support.PerfTimer("new Sequence(\"{0}\")".F(node.Key), 20))
{
try
{
sequences.Add(kvp.Key, CreateSequence(modData, tileSet, cache, node.Key, kvp.Key, kvp.Value));
}
catch (FileNotFoundException ex)
{
// Eat the FileNotFound exceptions from missing sprites
Log.Write("debug", ex.Message);
}
}
}
return new ReadOnlyDictionary<string, ISpriteSequence>(sequences);
}
}
public class DefaultSpriteSequence : ISpriteSequence
{ {
readonly Sprite[] sprites; readonly Sprite[] sprites;
readonly bool reverseFacings, transpose; readonly bool reverseFacings, transpose;
public readonly string Name; protected readonly ISpriteSequenceLoader Loader;
public readonly int Start;
public readonly int Length;
public readonly int Stride;
public readonly int Facings;
public readonly int Tick;
public readonly int ZOffset;
public readonly int ShadowStart;
public readonly int ShadowZOffset;
public readonly int[] Frames;
public Sequence(SpriteCache cache, string unit, string name, MiniYaml info) public string Name { get; private set; }
public int Start { get; private set; }
public int Length { get; private set; }
public int Stride { get; private set; }
public int Facings { get; private set; }
public int Tick { get; private set; }
public int ZOffset { get; private set; }
public int ShadowStart { get; private set; }
public int ShadowZOffset { get; private set; }
public int[] Frames { get; private set; }
protected virtual string GetSpriteSrc(ModData modData, TileSet tileSet, string sequence, string animation, MiniYaml info, Dictionary<string, MiniYaml> d)
{ {
var srcOverride = info.Value; return info.Value ?? sequence;
Name = name; }
public DefaultSpriteSequence(ModData modData, TileSet tileSet, SpriteCache cache, ISpriteSequenceLoader loader, string sequence, string animation, MiniYaml info)
{
Name = animation;
Loader = loader;
var d = info.ToDictionary(); var d = info.ToDictionary();
var offset = float2.Zero; var offset = float2.Zero;
var blendMode = BlendMode.Alpha; var blendMode = BlendMode.Alpha;
@@ -50,7 +93,8 @@ namespace OpenRA.Graphics
// Apply offset to each sprite in the sequence // Apply offset to each sprite in the sequence
// Different sequences may apply different offsets to the same frame // Different sequences may apply different offsets to the same frame
sprites = cache[srcOverride ?? unit].Select( var src = GetSpriteSrc(modData, tileSet, sequence, animation, info, d);
sprites = cache[src].Select(
s => new Sprite(s.Sheet, s.Bounds, s.Offset + offset, s.Channel, blendMode)).ToArray(); s => new Sprite(s.Sheet, s.Bounds, s.Offset + offset, s.Channel, blendMode)).ToArray();
if (!d.ContainsKey("Length")) if (!d.ContainsKey("Length"))
@@ -109,17 +153,17 @@ namespace OpenRA.Graphics
if (Length > Stride) if (Length > Stride)
throw new InvalidOperationException( throw new InvalidOperationException(
"{0}: Sequence {1}.{2}: Length must be <= stride" "{0}: Sequence {1}.{2}: Length must be <= stride"
.F(info.Nodes[0].Location, unit, name)); .F(info.Nodes[0].Location, sequence, animation));
if (Start < 0 || Start + Facings * Stride > sprites.Length || ShadowStart + Facings * Stride > sprites.Length) if (Start < 0 || Start + Facings * Stride > sprites.Length || ShadowStart + Facings * Stride > sprites.Length)
throw new InvalidOperationException( throw new InvalidOperationException(
"{6}: Sequence {0}.{1} uses frames [{2}..{3}] of SHP `{4}`, but only 0..{5} actually exist" "{6}: Sequence {0}.{1} uses frames [{2}..{3}] of SHP `{4}`, but only 0..{5} actually exist"
.F(unit, name, Start, Start + Facings * Stride - 1, srcOverride ?? unit, sprites.Length - 1, .F(sequence, animation, Start, Start + Facings * Stride - 1, src, sprites.Length - 1,
info.Nodes[0].Location)); info.Nodes[0].Location));
} }
catch (FormatException f) catch (FormatException f)
{ {
throw new FormatException("Failed to parse sequences for {0}.{1} at {2}:\n{3}".F(unit, name, info.Nodes[0].Location, f)); throw new FormatException("Failed to parse sequences for {0}.{1} at {2}:\n{3}".F(sequence, animation, info.Nodes[0].Location, f));
} }
} }
@@ -138,9 +182,9 @@ namespace OpenRA.Graphics
return ShadowStart >= 0 ? GetSprite(ShadowStart, frame, facing) : null; return ShadowStart >= 0 ? GetSprite(ShadowStart, frame, facing) : null;
} }
Sprite GetSprite(int start, int frame, int facing) protected virtual Sprite GetSprite(int start, int frame, int facing)
{ {
var f = Traits.Util.QuantizeFacing(facing, Facings); var f = OpenRA.Traits.Util.QuantizeFacing(facing, Facings);
if (reverseFacings) if (reverseFacings)
f = (Facings - f) % Facings; f = (Facings - f) % Facings;

View File

@@ -577,6 +577,7 @@
<Compile Include="UtilityCommands\ReplayMetadataCommand.cs" /> <Compile Include="UtilityCommands\ReplayMetadataCommand.cs" />
<Compile Include="Widgets\Logic\ReplayUtils.cs" /> <Compile Include="Widgets\Logic\ReplayUtils.cs" />
<Compile Include="InstallUtils.cs" /> <Compile Include="InstallUtils.cs" />
<Compile Include="Graphics\DefaultSpriteSequence.cs" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup> <PropertyGroup>

View File

@@ -94,7 +94,7 @@ namespace OpenRA.Mods.RA.Graphics
yield return z; yield return z;
} }
static IEnumerable<IFinalizedRenderable> DrawZapWandering(WorldRenderer wr, float2 from, float2 to, Sequence s, string pal) static IEnumerable<IFinalizedRenderable> DrawZapWandering(WorldRenderer wr, float2 from, float2 to, ISpriteSequence s, string pal)
{ {
var z = float2.Zero; /* hack */ var z = float2.Zero; /* hack */
var dist = to - from; var dist = to - from;
@@ -121,7 +121,7 @@ namespace OpenRA.Mods.RA.Graphics
return renderables; return renderables;
} }
static IEnumerable<IFinalizedRenderable> DrawZap(WorldRenderer wr, float2 from, float2 to, Sequence s, out float2 p, string palette) static IEnumerable<IFinalizedRenderable> DrawZap(WorldRenderer wr, float2 from, float2 to, ISpriteSequence s, out float2 p, string palette)
{ {
var dist = to - from; var dist = to - from;
var q = new float2(-dist.Y, dist.X); var q = new float2(-dist.Y, dist.X);

View File

@@ -203,3 +203,5 @@ Missions:
SupportsMapsFrom: cnc SupportsMapsFrom: cnc
SpriteFormats: ShpTD, TmpTD, ShpTS, TmpRA SpriteFormats: ShpTD, TmpTD, ShpTS, TmpRA
SpriteSequenceFormat: DefaultSpriteSequence

View File

@@ -178,3 +178,5 @@ Fonts:
SupportsMapsFrom: d2k SupportsMapsFrom: d2k
SpriteFormats: R8, ShpTD, TmpRA SpriteFormats: R8, ShpTD, TmpRA
SpriteSequenceFormat: DefaultSpriteSequence

View File

@@ -51,4 +51,6 @@ Fonts:
LobbyDefaults: LobbyDefaults:
SpriteFormats: ShpTD SpriteFormats: ShpTD
SpriteSequenceFormat: DefaultSpriteSequence

View File

@@ -201,3 +201,5 @@ Missions:
SupportsMapsFrom: ra SupportsMapsFrom: ra
SpriteFormats: ShpTD, TmpRA, TmpTD, ShpTS SpriteFormats: ShpTD, TmpRA, TmpTD, ShpTS
SpriteSequenceFormat: DefaultSpriteSequence

View File

@@ -219,3 +219,5 @@ Fonts:
SupportsMapsFrom: ts SupportsMapsFrom: ts
SpriteFormats: ShpTS, TmpTS, ShpTD SpriteFormats: ShpTS, TmpTS, ShpTD
SpriteSequenceFormat: DefaultSpriteSequence