diff --git a/OpenRA.Game/Graphics/Animation.cs b/OpenRA.Game/Graphics/Animation.cs index 135d2a4e29..fcd3325bea 100644 --- a/OpenRA.Game/Graphics/Animation.cs +++ b/OpenRA.Game/Graphics/Animation.cs @@ -16,7 +16,7 @@ namespace OpenRA.Graphics public class Animation { 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 Func Paused; @@ -177,7 +177,7 @@ namespace OpenRA.Graphics } } - public Sequence GetSequence(string sequenceName) + public ISpriteSequence GetSequence(string sequenceName) { return sequenceProvider.GetSequence(name, sequenceName); } diff --git a/OpenRA.Game/Graphics/SequenceProvider.cs b/OpenRA.Game/Graphics/SequenceProvider.cs index 075d2cdd3e..016d64fb55 100644 --- a/OpenRA.Game/Graphics/SequenceProvider.cs +++ b/OpenRA.Game/Graphics/SequenceProvider.cs @@ -12,11 +12,35 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; namespace OpenRA.Graphics { - using Sequences = IReadOnlyDictionary>>; - using UnitSequences = Lazy>; + using Sequences = IReadOnlyDictionary>>; + using UnitSequences = Lazy>; + + 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 ParseSequences(ModData modData, TileSet tileSet, SpriteCache cache, MiniYamlNode node); + } public class SequenceProvider { @@ -29,13 +53,13 @@ namespace OpenRA.Graphics this.SpriteCache = cache.SpriteCache; } - public Sequence GetSequence(string unitName, string sequenceName) + public ISpriteSequence GetSequence(string unitName, string sequenceName) { UnitSequences unitSeq; if (!sequences.Value.TryGetValue(unitName, out unitSeq)) throw new InvalidOperationException("Unit `{0}` does not have any sequences defined.".F(unitName)); - Sequence seq; + ISpriteSequence seq; if (!unitSeq.Value.TryGetValue(sequenceName, out seq)) 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 { readonly ModData modData; + readonly TileSet tileSet; readonly Lazy spriteCache; public SpriteCache SpriteCache { get { return spriteCache.Value; } } @@ -85,7 +110,9 @@ namespace OpenRA.Graphics public SequenceCache(ModData modData, TileSet tileSet) { 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))); } @@ -116,7 +143,7 @@ namespace OpenRA.Graphics items.Add(node.Key, t); else { - t = Exts.Lazy(() => CreateUnitSequences(node)); + t = Exts.Lazy(() => modData.SpriteSequenceLoader.ParseSequences(modData, tileSet, SpriteCache, node)); sequenceCache.Add(key, t); items.Add(node.Key, t); } @@ -125,28 +152,6 @@ namespace OpenRA.Graphics return new ReadOnlyDictionary(items); } - IReadOnlyDictionary CreateUnitSequences(MiniYamlNode node) - { - var unitSequences = new Dictionary(); - - 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(unitSequences); - } - public void Dispose() { if (spriteCache.IsValueCreated) diff --git a/OpenRA.Game/Manifest.cs b/OpenRA.Game/Manifest.cs index b01f8f7f14..fe3945141e 100644 --- a/OpenRA.Game/Manifest.cs +++ b/OpenRA.Game/Manifest.cs @@ -20,6 +20,17 @@ namespace OpenRA public enum TileShape { Rectangle, Diamond } public interface IGlobalModData { } + public sealed class SpriteSequenceFormat : IGlobalModData + { + public readonly string Type; + public readonly IReadOnlyDictionary Metadata; + public SpriteSequenceFormat(MiniYaml yaml) + { + Type = yaml.Value; + Metadata = new ReadOnlyDictionary(yaml.ToDictionary()); + } + } + // Describes what is to be loaded in order to run a mod public class Manifest { @@ -34,6 +45,7 @@ namespace OpenRA public readonly IReadOnlyDictionary MapFolders; public readonly MiniYaml LoadScreen; public readonly MiniYaml LobbyDefaults; + public readonly Dictionary> Fonts; public readonly Size TileSize = new Size(24, 24); public readonly TileShape TileShape = TileShape.Rectangle; @@ -92,8 +104,12 @@ namespace OpenRA Missions = YamlList(yaml, "Missions", true); 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 => { @@ -150,8 +166,20 @@ namespace OpenRA if (t == null || !typeof(IGlobalModData).IsAssignableFrom(t)) throw new InvalidDataException("`{0}` is not a valid mod manifest entry.".F(kv.Key)); - var module = oc.CreateObject(kv.Key); - FieldLoader.Load(module, kv.Value); + IGlobalModData module; + 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(kv.Key); + FieldLoader.Load(module, kv.Value); + } + modules.Add(module); } } diff --git a/OpenRA.Game/ModData.cs b/OpenRA.Game/ModData.cs index 8cba16ed01..38cacea1fd 100644 --- a/OpenRA.Game/ModData.cs +++ b/OpenRA.Game/ModData.cs @@ -25,6 +25,7 @@ namespace OpenRA public readonly WidgetLoader WidgetLoader; public readonly MapCache MapCache; public readonly ISpriteLoader[] SpriteLoaders; + public readonly ISpriteSequenceLoader SpriteSequenceLoader; public readonly RulesetCache RulesetCache; public ILoadScreen LoadScreen { get; private set; } public VoxelLoader VoxelLoader { get; private set; } @@ -52,17 +53,25 @@ namespace OpenRA RulesetCache.LoadingProgress += HandleLoadingProgress; MapCache = new MapCache(this); - var loaders = new List(); + var spriteLoaders = new List(); foreach (var format in Manifest.SpriteFormats) { var loader = ObjectCreator.FindType(format + "Loader"); if (loader == null || !loader.GetInterfaces().Contains(typeof(ISpriteLoader))) 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(); + 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 GlobalFileSystem.UnmountAll(); diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index ae14e7c544..e86c309d37 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -109,7 +109,6 @@ - diff --git a/OpenRA.Game/Graphics/Sequence.cs b/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs similarity index 57% rename from OpenRA.Game/Graphics/Sequence.cs rename to OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs index 916af8dbac..c5cc18f1e1 100644 --- a/OpenRA.Game/Graphics/Sequence.cs +++ b/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs @@ -9,30 +9,73 @@ #endregion using System; +using System.Collections.Generic; +using System.IO; 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 ParseSequences(ModData modData, TileSet tileSet, SpriteCache cache, MiniYamlNode node) + { + var sequences = new Dictionary(); + + 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(sequences); + } + } + + public class DefaultSpriteSequence : ISpriteSequence { readonly Sprite[] sprites; readonly bool reverseFacings, transpose; - public readonly string Name; - 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; + protected readonly ISpriteSequenceLoader Loader; - 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 d) { - var srcOverride = info.Value; - Name = name; + return info.Value ?? sequence; + } + + 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 offset = float2.Zero; var blendMode = BlendMode.Alpha; @@ -50,7 +93,8 @@ namespace OpenRA.Graphics // Apply offset to each sprite in the sequence // 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(); if (!d.ContainsKey("Length")) @@ -109,17 +153,17 @@ namespace OpenRA.Graphics if (Length > Stride) throw new InvalidOperationException( "{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) throw new InvalidOperationException( "{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)); } 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; } - 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) f = (Facings - f) % Facings; diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 4010600547..c691fb8526 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -577,6 +577,7 @@ + diff --git a/OpenRA.Mods.RA/Graphics/TeslaZapRenderable.cs b/OpenRA.Mods.RA/Graphics/TeslaZapRenderable.cs index 28d25da0ef..194536cf4f 100644 --- a/OpenRA.Mods.RA/Graphics/TeslaZapRenderable.cs +++ b/OpenRA.Mods.RA/Graphics/TeslaZapRenderable.cs @@ -94,7 +94,7 @@ namespace OpenRA.Mods.RA.Graphics yield return z; } - static IEnumerable DrawZapWandering(WorldRenderer wr, float2 from, float2 to, Sequence s, string pal) + static IEnumerable DrawZapWandering(WorldRenderer wr, float2 from, float2 to, ISpriteSequence s, string pal) { var z = float2.Zero; /* hack */ var dist = to - from; @@ -121,7 +121,7 @@ namespace OpenRA.Mods.RA.Graphics return renderables; } - static IEnumerable DrawZap(WorldRenderer wr, float2 from, float2 to, Sequence s, out float2 p, string palette) + static IEnumerable DrawZap(WorldRenderer wr, float2 from, float2 to, ISpriteSequence s, out float2 p, string palette) { var dist = to - from; var q = new float2(-dist.Y, dist.X); diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index 0cfff7afb0..33d5238b0c 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -203,3 +203,5 @@ Missions: SupportsMapsFrom: cnc SpriteFormats: ShpTD, TmpTD, ShpTS, TmpRA + +SpriteSequenceFormat: DefaultSpriteSequence diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index 7e1fcd2fcf..b7936e89e0 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -178,3 +178,5 @@ Fonts: SupportsMapsFrom: d2k SpriteFormats: R8, ShpTD, TmpRA + +SpriteSequenceFormat: DefaultSpriteSequence diff --git a/mods/modchooser/mod.yaml b/mods/modchooser/mod.yaml index 4be6107424..3309f74d2d 100644 --- a/mods/modchooser/mod.yaml +++ b/mods/modchooser/mod.yaml @@ -51,4 +51,6 @@ Fonts: LobbyDefaults: -SpriteFormats: ShpTD \ No newline at end of file +SpriteFormats: ShpTD + +SpriteSequenceFormat: DefaultSpriteSequence diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index 25680a7853..f61fad5276 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -201,3 +201,5 @@ Missions: SupportsMapsFrom: ra SpriteFormats: ShpTD, TmpRA, TmpTD, ShpTS + +SpriteSequenceFormat: DefaultSpriteSequence diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml index fadf21d7a0..8e2a31618f 100644 --- a/mods/ts/mod.yaml +++ b/mods/ts/mod.yaml @@ -219,3 +219,5 @@ Fonts: SupportsMapsFrom: ts SpriteFormats: ShpTS, TmpTS, ShpTD + +SpriteSequenceFormat: DefaultSpriteSequence