diff --git a/OpenRA.Game/GameRules/Ruleset.cs b/OpenRA.Game/GameRules/Ruleset.cs index 30e6e6a05e..266a21029b 100644 --- a/OpenRA.Game/GameRules/Ruleset.cs +++ b/OpenRA.Game/GameRules/Ruleset.cs @@ -15,7 +15,6 @@ using System.Linq; using System.Threading.Tasks; using OpenRA.FileSystem; using OpenRA.GameRules; -using OpenRA.Graphics; using OpenRA.Traits; namespace OpenRA @@ -28,7 +27,6 @@ namespace OpenRA public readonly IReadOnlyDictionary Notifications; public readonly IReadOnlyDictionary Music; public readonly ITerrainInfo TerrainInfo; - public readonly SequenceProvider Sequences; public readonly IReadOnlyDictionary ModelSequences; public Ruleset( @@ -38,7 +36,6 @@ namespace OpenRA IReadOnlyDictionary notifications, IReadOnlyDictionary music, ITerrainInfo terrainInfo, - SequenceProvider sequences, IReadOnlyDictionary modelSequences) { Actors = new ActorInfoDictionary(actors); @@ -47,7 +44,6 @@ namespace OpenRA Notifications = notifications; Music = music; TerrainInfo = terrainInfo; - Sequences = sequences; ModelSequences = modelSequences; foreach (var a in Actors.Values) @@ -145,8 +141,8 @@ namespace OpenRA var modelSequences = MergeOrDefault("Manifest,ModelSequences", fs, m.ModelSequences, null, null, k => k); - // The default ruleset does not include a preferred tileset or sequence set - ruleset = new Ruleset(actors, weapons, voices, notifications, music, null, null, modelSequences); + // The default ruleset does not include a preferred tileset + ruleset = new Ruleset(actors, weapons, voices, notifications, music, null, modelSequences); } if (modData.IsOnMainThread) @@ -170,14 +166,13 @@ namespace OpenRA { var dr = modData.DefaultRules; var terrainInfo = modData.DefaultTerrainInfo[tileSet]; - var sequences = modData.DefaultSequences[tileSet]; - return new Ruleset(dr.Actors, dr.Weapons, dr.Voices, dr.Notifications, dr.Music, terrainInfo, sequences, dr.ModelSequences); + return new Ruleset(dr.Actors, dr.Weapons, dr.Voices, dr.Notifications, dr.Music, terrainInfo, dr.ModelSequences); } public static Ruleset Load(ModData modData, IReadOnlyFileSystem fileSystem, string tileSet, MiniYaml mapRules, MiniYaml mapWeapons, MiniYaml mapVoices, MiniYaml mapNotifications, - MiniYaml mapMusic, MiniYaml mapSequences, MiniYaml mapModelSequences) + MiniYaml mapMusic, MiniYaml mapModelSequences) { var m = modData.Manifest; var dr = modData.DefaultRules; @@ -204,16 +199,12 @@ namespace OpenRA // TODO: Add support for merging custom terrain modifications var terrainInfo = modData.DefaultTerrainInfo[tileSet]; - // TODO: Top-level dictionary should be moved into the Ruleset instead of in its own object - var sequences = mapSequences == null ? modData.DefaultSequences[tileSet] : - new SequenceProvider(fileSystem, modData, tileSet, mapSequences); - var modelSequences = dr.ModelSequences; if (mapModelSequences != null) modelSequences = MergeOrDefault("ModelSequences", fileSystem, m.ModelSequences, mapModelSequences, dr.ModelSequences, k => k); - ruleset = new Ruleset(actors, weapons, voices, notifications, music, terrainInfo, sequences, modelSequences); + ruleset = new Ruleset(actors, weapons, voices, notifications, music, terrainInfo, modelSequences); } if (modData.IsOnMainThread) diff --git a/OpenRA.Game/Graphics/Animation.cs b/OpenRA.Game/Graphics/Animation.cs index 71c78b08e5..340582c0f9 100644 --- a/OpenRA.Game/Graphics/Animation.cs +++ b/OpenRA.Game/Graphics/Animation.cs @@ -22,7 +22,7 @@ namespace OpenRA.Graphics public string Name { get; private set; } public bool IsDecoration { get; set; } - readonly SequenceProvider sequences; + readonly SequenceSet sequences; readonly Func facingFunc; readonly Func paused; diff --git a/OpenRA.Game/Graphics/SequenceProvider.cs b/OpenRA.Game/Graphics/SequenceProvider.cs deleted file mode 100644 index 19fd095d0b..0000000000 --- a/OpenRA.Game/Graphics/SequenceProvider.cs +++ /dev/null @@ -1,147 +0,0 @@ -#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 OpenRA.FileSystem; -using OpenRA.Primitives; - -namespace OpenRA.Graphics -{ - using Sequences = IReadOnlyDictionary>>; - using UnitSequences = Lazy>; - - public interface ISpriteSequence - { - string Name { get; } - int Length { get; } - int Facings { get; } - int Tick { get; } - int ZOffset { get; } - int ShadowZOffset { get; } - Rectangle Bounds { get; } - bool IgnoreWorldTint { get; } - float Scale { get; } - - Sprite GetSprite(int frame); - Sprite GetSprite(int frame, WAngle facing); - (Sprite, WAngle) GetSpriteWithRotation(int frame, WAngle facing); - Sprite GetShadow(int frame, WAngle facing); - float GetAlpha(int frame); - } - - public interface ISpriteSequenceLoader - { - IReadOnlyDictionary ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode node); - } - - public class SequenceProvider : IDisposable - { - readonly ModData modData; - readonly string tileSet; - readonly Lazy sequences; - readonly Lazy spriteCache; - public SpriteCache SpriteCache => spriteCache.Value; - - readonly Dictionary sequenceCache = new Dictionary(); - - public SequenceProvider(IReadOnlyFileSystem fileSystem, ModData modData, string tileSet, MiniYaml additionalSequences) - { - this.modData = modData; - this.tileSet = tileSet; - sequences = Exts.Lazy(() => - { - using (new Support.PerfTimer("LoadSequences")) - return Load(fileSystem, additionalSequences); - }); - - spriteCache = Exts.Lazy(() => new SpriteCache(fileSystem, modData.SpriteLoaders)); - } - - public ISpriteSequence GetSequence(string unitName, string sequenceName) - { - if (!sequences.Value.TryGetValue(unitName, out var unitSeq)) - throw new InvalidOperationException($"Unit `{unitName}` does not have any sequences defined."); - - if (!unitSeq.Value.TryGetValue(sequenceName, out var seq)) - throw new InvalidOperationException($"Unit `{unitName}` does not have a sequence named `{sequenceName}`"); - - return seq; - } - - public IEnumerable Images => sequences.Value.Keys; - - public bool HasSequence(string unitName) - { - return sequences.Value.ContainsKey(unitName); - } - - public bool HasSequence(string unitName, string sequenceName) - { - if (!sequences.Value.TryGetValue(unitName, out var unitSeq)) - throw new InvalidOperationException($"Unit `{unitName}` does not have any sequences defined."); - - return unitSeq.Value.ContainsKey(sequenceName); - } - - public IEnumerable Sequences(string unitName) - { - if (!sequences.Value.TryGetValue(unitName, out var unitSeq)) - throw new InvalidOperationException($"Unit `{unitName}` does not have any sequences defined."); - - return unitSeq.Value.Keys; - } - - Sequences Load(IReadOnlyFileSystem fileSystem, MiniYaml additionalSequences) - { - var nodes = MiniYaml.Load(fileSystem, modData.Manifest.Sequences, additionalSequences); - var items = new Dictionary(); - foreach (var node in nodes) - { - // Nodes starting with ^ are inheritable but never loaded directly - if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal)) - continue; - - var key = node.Value.ToLines(node.Key).JoinWith("|"); - - if (sequenceCache.TryGetValue(key, out var t)) - items.Add(node.Key, t); - else - { - t = Exts.Lazy(() => modData.SpriteSequenceLoader.ParseSequences(modData, tileSet, SpriteCache, node)); - sequenceCache.Add(key, t); - items.Add(node.Key, t); - } - } - - return items; - } - - public void Preload() - { - foreach (var sb in SpriteCache.SheetBuilders.Values) - sb.Current.CreateBuffer(); - - foreach (var unitSeq in sequences.Value.Values) - foreach (var seq in unitSeq.Value.Values) { } - - foreach (var sb in SpriteCache.SheetBuilders.Values) - sb.Current.ReleaseBuffer(); - } - - public void Dispose() - { - if (spriteCache.IsValueCreated) - foreach (var sb in SpriteCache.SheetBuilders.Values) - sb.Dispose(); - } - } -} diff --git a/OpenRA.Game/Graphics/SequenceSet.cs b/OpenRA.Game/Graphics/SequenceSet.cs new file mode 100644 index 0000000000..b683d95d37 --- /dev/null +++ b/OpenRA.Game/Graphics/SequenceSet.cs @@ -0,0 +1,119 @@ +#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 OpenRA.FileSystem; +using OpenRA.Primitives; + +namespace OpenRA.Graphics +{ + public interface ISpriteSequence + { + string Name { get; } + int Length { get; } + int Facings { get; } + int Tick { get; } + int ZOffset { get; } + int ShadowZOffset { get; } + Rectangle Bounds { get; } + bool IgnoreWorldTint { get; } + float Scale { get; } + void ResolveSprites(SpriteCache cache); + Sprite GetSprite(int frame); + Sprite GetSprite(int frame, WAngle facing); + (Sprite, WAngle) GetSpriteWithRotation(int frame, WAngle facing); + Sprite GetShadow(int frame, WAngle facing); + float GetAlpha(int frame); + } + + public interface ISpriteSequenceLoader + { + int BgraSheetSize { get; } + int IndexedSheetSize { get; } + IReadOnlyDictionary ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode node); + } + + public sealed class SequenceSet : IDisposable + { + readonly ModData modData; + readonly string tileSet; + readonly IReadOnlyDictionary> images; + public SpriteCache SpriteCache { get; } + + public SequenceSet(IReadOnlyFileSystem fileSystem, ModData modData, string tileSet, MiniYaml additionalSequences) + { + this.modData = modData; + this.tileSet = tileSet; + SpriteCache = new SpriteCache(fileSystem, modData.SpriteLoaders, modData.SpriteSequenceLoader.BgraSheetSize, modData.SpriteSequenceLoader.IndexedSheetSize); + using (new Support.PerfTimer("LoadSequences")) + images = Load(fileSystem, additionalSequences); + } + + public ISpriteSequence GetSequence(string image, string sequence) + { + if (!images.TryGetValue(image, out var sequences)) + throw new InvalidOperationException($"Image `{image}` does not have any sequences defined."); + + if (!sequences.TryGetValue(sequence, out var seq)) + throw new InvalidOperationException($"Image `{image}` does not have a sequence named `{sequence}`."); + + return seq; + } + + public IEnumerable Images => images.Keys; + + public bool HasSequence(string image, string sequence) + { + if (!images.TryGetValue(image, out var sequences)) + throw new InvalidOperationException($"Image `{image}` does not have any sequences defined."); + + return sequences.ContainsKey(sequence); + } + + public IEnumerable Sequences(string image) + { + if (!images.TryGetValue(image, out var sequences)) + throw new InvalidOperationException($"Image `{image}` does not have any sequences defined."); + + return sequences.Keys; + } + + IReadOnlyDictionary> Load(IReadOnlyFileSystem fileSystem, MiniYaml additionalSequences) + { + var nodes = MiniYaml.Load(fileSystem, modData.Manifest.Sequences, additionalSequences); + var images = new Dictionary>(); + foreach (var node in nodes) + { + // Nodes starting with ^ are inheritable but never loaded directly + if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal)) + continue; + + images[node.Key] = modData.SpriteSequenceLoader.ParseSequences(modData, tileSet, SpriteCache, node); + } + + return images; + } + + public void LoadSprites() + { + SpriteCache.LoadReservations(modData); + foreach (var sequences in images.Values) + foreach (var sequence in sequences) + sequence.Value.ResolveSprites(SpriteCache); + } + + public void Dispose() + { + SpriteCache.Dispose(); + } + } +} diff --git a/OpenRA.Game/Graphics/SpriteCache.cs b/OpenRA.Game/Graphics/SpriteCache.cs new file mode 100644 index 0000000000..dc703d7458 --- /dev/null +++ b/OpenRA.Game/Graphics/SpriteCache.cs @@ -0,0 +1,174 @@ +#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 System.Linq; +using OpenRA.FileSystem; +using OpenRA.Primitives; + +namespace OpenRA.Graphics +{ + public class SpriteCache : IDisposable + { + public readonly Dictionary SheetBuilders; + readonly ISpriteLoader[] loaders; + readonly IReadOnlyFileSystem fileSystem; + + readonly Dictionary spriteReservations = new Dictionary(); + readonly Dictionary frameReservations = new Dictionary(); + readonly Dictionary> reservationsByFilename = new Dictionary>(); + + readonly Dictionary resolvedFrames = new Dictionary(); + readonly Dictionary resolvedSprites = new Dictionary(); + + readonly Dictionary missingFiles = new Dictionary(); + + int nextReservationToken = 1; + + public SpriteCache(IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders, int bgraSheetSize, int indexedSheetSize, int bgraSheetMargin = 1, int indexedSheetMargin = 1) + { + SheetBuilders = new Dictionary + { + { SheetType.Indexed, new SheetBuilder(SheetType.Indexed, indexedSheetSize, indexedSheetMargin) }, + { SheetType.BGRA, new SheetBuilder(SheetType.BGRA, bgraSheetSize, bgraSheetMargin) } + }; + + this.fileSystem = fileSystem; + this.loaders = loaders; + } + + public int ReserveSprites(string filename, IEnumerable frames, MiniYamlNode.SourceLocation location) + { + var token = nextReservationToken++; + spriteReservations[token] = (frames?.ToArray(), location); + reservationsByFilename.GetOrAdd(filename, _ => new List()).Add(token); + return token; + } + + public int ReserveFrames(string filename, IEnumerable frames, MiniYamlNode.SourceLocation location) + { + var token = nextReservationToken++; + frameReservations[token] = (frames?.ToArray(), location); + reservationsByFilename.GetOrAdd(filename, _ => new List()).Add(token); + return token; + } + + static ISpriteFrame[] GetFrames(IReadOnlyFileSystem fileSystem, string filename, ISpriteLoader[] loaders, out TypeDictionary metadata) + { + metadata = null; + if (!fileSystem.TryOpen(filename, out var stream)) + return null; + + using (stream) + { + foreach (var loader in loaders) + if (loader.TryParseSprite(stream, filename, out var frames, out metadata)) + return frames; + + return null; + } + } + + public void LoadReservations(ModData modData) + { + foreach (var sb in SheetBuilders.Values) + sb.Current.CreateBuffer(); + + var spriteCache = new Dictionary(); + foreach (var (filename, tokens) in reservationsByFilename) + { + modData.LoadScreen?.Display(); + var loadedFrames = GetFrames(fileSystem, filename, loaders, out _); + foreach (var token in tokens) + { + if (frameReservations.TryGetValue(token, out var r)) + { + if (loadedFrames != null) + { + if (r.Frames != null) + { + var resolved = new ISpriteFrame[loadedFrames.Length]; + foreach (var i in r.Frames) + resolved[i] = loadedFrames[i]; + resolvedFrames[token] = resolved; + } + else + resolvedFrames[token] = loadedFrames; + } + else + { + resolvedFrames[token] = null; + missingFiles[token] = (filename, r.Location); + } + } + + if (spriteReservations.TryGetValue(token, out r)) + { + if (loadedFrames != null) + { + var resolved = new Sprite[loadedFrames.Length]; + var frames = r.Frames ?? Enumerable.Range(0, loadedFrames.Length); + foreach (var i in frames) + resolved[i] = spriteCache.GetOrAdd(i, + f => SheetBuilders[SheetBuilder.FrameTypeToSheetType(loadedFrames[f].Type)].Add(loadedFrames[f])); + + resolvedSprites[token] = resolved; + } + else + { + resolvedSprites[token] = null; + missingFiles[token] = (filename, r.Location); + } + } + } + + spriteCache.Clear(); + } + + spriteReservations.Clear(); + frameReservations.Clear(); + reservationsByFilename.Clear(); + + foreach (var sb in SheetBuilders.Values) + sb.Current.ReleaseBuffer(); + } + + public Sprite[] ResolveSprites(int token) + { + var resolved = resolvedSprites[token]; + resolvedSprites.Remove(token); + if (missingFiles.TryGetValue(token, out var r)) + throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename); + + return resolved; + } + + public ISpriteFrame[] ResolveFrames(int token) + { + var resolved = resolvedFrames[token]; + resolvedFrames.Remove(token); + if (missingFiles.TryGetValue(token, out var r)) + throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename); + + return resolved; + } + + public IEnumerable<(string Filename, MiniYamlNode.SourceLocation Location)> MissingFiles => missingFiles.Values.ToHashSet(); + + public void Dispose() + { + foreach (var sb in SheetBuilders.Values) + sb.Dispose(); + } + } +} diff --git a/OpenRA.Game/Graphics/SpriteLoader.cs b/OpenRA.Game/Graphics/SpriteLoader.cs index 458a4fb68d..ebb96a1e46 100644 --- a/OpenRA.Game/Graphics/SpriteLoader.cs +++ b/OpenRA.Game/Graphics/SpriteLoader.cs @@ -9,10 +9,7 @@ */ #endregion -using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using OpenRA.FileSystem; using OpenRA.Primitives; @@ -67,94 +64,6 @@ namespace OpenRA.Graphics bool DisableExportPadding { get; } } - public class SpriteCache - { - public readonly Cache SheetBuilders; - readonly ISpriteLoader[] loaders; - readonly IReadOnlyFileSystem fileSystem; - - readonly Dictionary> sprites = new Dictionary>(); - readonly Dictionary unloadedFrames = new Dictionary(); - readonly Dictionary metadata = new Dictionary(); - - public SpriteCache(IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders) - { - SheetBuilders = new Cache(t => new SheetBuilder(t)); - - this.fileSystem = fileSystem; - this.loaders = loaders; - } - - /// - /// Returns the first set of sprites with the given filename. - /// If getUsedFrames is defined then the indices returned by the function call - /// are guaranteed to be loaded. The value of other indices in the returned - /// array are undefined and should never be accessed. - /// - public Sprite[] this[string filename, Func> getUsedFrames = null] - { - get - { - var allSprites = sprites.GetOrAdd(filename); - var sprite = allSprites.FirstOrDefault(); - - if (!unloadedFrames.TryGetValue(filename, out var unloaded)) - unloaded = null; - - // This is the first time that the file has been requested - // Load all of the frames into the unused buffer and initialize - // the loaded cache (initially empty) - if (sprite == null) - { - unloaded = FrameLoader.GetFrames(fileSystem, filename, loaders, out var fileMetadata); - unloadedFrames[filename] = unloaded; - metadata[filename] = fileMetadata; - - sprite = new Sprite[unloaded.Length]; - allSprites.Add(sprite); - } - - // HACK: The sequence code relies on side-effects from getUsedFrames - var indices = getUsedFrames != null ? getUsedFrames(sprite.Length) : - Enumerable.Range(0, sprite.Length); - - // Load any unused frames into the SheetBuilder - if (unloaded != null) - { - foreach (var i in indices) - { - if (unloaded[i] != null) - { - sprite[i] = SheetBuilders[SheetBuilder.FrameTypeToSheetType(unloaded[i].Type)].Add(unloaded[i]); - unloaded[i] = null; - } - } - - // All frames have been loaded - if (unloaded.All(f => f == null)) - unloadedFrames.Remove(filename); - } - - return sprite; - } - } - - /// - /// Returns a TypeDictionary containing any metadata defined by the frame - /// or null if the frame does not define metadata. - /// - public TypeDictionary FrameMetadata(string filename) - { - if (!metadata.TryGetValue(filename, out var fileMetadata)) - { - FrameLoader.GetFrames(fileSystem, filename, loaders, out fileMetadata); - metadata[filename] = fileMetadata; - } - - return fileMetadata; - } - } - public class FrameCache { readonly Cache frames; diff --git a/OpenRA.Game/Map/Map.cs b/OpenRA.Game/Map/Map.cs index 07c39a9623..43555d0b25 100644 --- a/OpenRA.Game/Map/Map.cs +++ b/OpenRA.Game/Map/Map.cs @@ -218,7 +218,7 @@ namespace OpenRA public string Uid { get; private set; } public Ruleset Rules { get; private set; } - public SequenceProvider Sequences => Rules.Sequences; + public SequenceSet Sequences { get; private set; } public bool InvalidCustomRules { get; private set; } public Exception InvalidCustomRulesException { get; private set; } @@ -439,7 +439,7 @@ namespace OpenRA try { Rules = Ruleset.Load(modData, this, Tileset, RuleDefinitions, WeaponDefinitions, - VoiceDefinitions, NotificationDefinitions, MusicDefinitions, SequenceDefinitions, ModelSequenceDefinitions); + VoiceDefinitions, NotificationDefinitions, MusicDefinitions, ModelSequenceDefinitions); } catch (Exception e) { @@ -449,8 +449,7 @@ namespace OpenRA Rules = Ruleset.LoadDefaultsForTileSet(modData, Tileset); } - Rules.Sequences.Preload(); - + Sequences = new SequenceSet(this, modData, Tileset, SequenceDefinitions); Translation = new Translation(Game.Settings.Player.Language, Translations, this); var tl = new MPos(0, 0).ToCPos(this); diff --git a/OpenRA.Game/Map/MapPreview.cs b/OpenRA.Game/Map/MapPreview.cs index 6bae46f6f0..eba86340cd 100644 --- a/OpenRA.Game/Map/MapPreview.cs +++ b/OpenRA.Game/Map/MapPreview.cs @@ -226,7 +226,7 @@ namespace OpenRA { return Ruleset.Load(modData, this, TileSet, innerData.RuleDefinitions, innerData.WeaponDefinitions, innerData.VoiceDefinitions, innerData.NotificationDefinitions, - innerData.MusicDefinitions, innerData.SequenceDefinitions, innerData.ModelSequenceDefinitions); + innerData.MusicDefinitions, innerData.ModelSequenceDefinitions); } public MapPreview(ModData modData, string uid, MapGridType gridType, MapCache cache) diff --git a/OpenRA.Game/ModData.cs b/OpenRA.Game/ModData.cs index d98a4b4c78..ff50f2e24b 100644 --- a/OpenRA.Game/ModData.cs +++ b/OpenRA.Game/ModData.cs @@ -48,9 +48,6 @@ namespace OpenRA readonly Lazy> defaultTerrainInfo; public IReadOnlyDictionary DefaultTerrainInfo => defaultTerrainInfo.Value; - readonly Lazy> defaultSequences; - public IReadOnlyDictionary DefaultSequences => defaultSequences.Value; - public ModData(Manifest mod, InstalledMods mods, bool useLoadScreen = false) { Languages = Array.Empty(); @@ -121,12 +118,6 @@ namespace OpenRA return (IReadOnlyDictionary)new ReadOnlyDictionary(items); }); - defaultSequences = Exts.Lazy(() => - { - var items = DefaultTerrainInfo.ToDictionary(t => t.Key, t => new SequenceProvider(DefaultFileSystem, this, t.Key, null)); - return (IReadOnlyDictionary)new ReadOnlyDictionary(items); - }); - initialThreadId = Environment.CurrentManagedThreadId; } @@ -167,6 +158,7 @@ namespace OpenRA // Reinitialize all our assets InitializeLoaders(map); + map.Sequences.LoadSprites(); // Load music with map assets mounted using (new Support.PerfTimer("Map.Music")) diff --git a/OpenRA.Mods.Cnc/Graphics/ClassicSpriteSequence.cs b/OpenRA.Mods.Cnc/Graphics/ClassicSpriteSequence.cs index 0d7b8ef124..a80ccae5b4 100644 --- a/OpenRA.Mods.Cnc/Graphics/ClassicSpriteSequence.cs +++ b/OpenRA.Mods.Cnc/Graphics/ClassicSpriteSequence.cs @@ -22,7 +22,7 @@ namespace OpenRA.Mods.Cnc.Graphics public override ISpriteSequence CreateSequence(ModData modData, string tileset, SpriteCache cache, string image, string sequence, MiniYaml data, MiniYaml defaults) { - return new ClassicSpriteSequence(modData, tileset, cache, this, image, sequence, data, defaults); + return new ClassicSpriteSequence(cache, this, image, sequence, data, defaults); } } @@ -33,8 +33,8 @@ namespace OpenRA.Mods.Cnc.Graphics static readonly SpriteSequenceField UseClassicFacings = new SpriteSequenceField(nameof(UseClassicFacings), false); readonly bool useClassicFacings; - public ClassicSpriteSequence(ModData modData, string tileSet, SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults) - : base(modData, tileSet, cache, loader, image, sequence, data, defaults) + public ClassicSpriteSequence(SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults) + : base(cache, loader, image, sequence, data, defaults) { useClassicFacings = LoadField(UseClassicFacings, data, defaults); diff --git a/OpenRA.Mods.Cnc/Graphics/ClassicTilesetSpecificSpriteSequence.cs b/OpenRA.Mods.Cnc/Graphics/ClassicTilesetSpecificSpriteSequence.cs index 65c7719fb0..b17863b8be 100644 --- a/OpenRA.Mods.Cnc/Graphics/ClassicTilesetSpecificSpriteSequence.cs +++ b/OpenRA.Mods.Cnc/Graphics/ClassicTilesetSpecificSpriteSequence.cs @@ -23,7 +23,7 @@ namespace OpenRA.Mods.Cnc.Graphics public override ISpriteSequence CreateSequence(ModData modData, string tileset, SpriteCache cache, string image, string sequence, MiniYaml data, MiniYaml defaults) { - return new ClassicTilesetSpecificSpriteSequence(modData, tileset, cache, this, image, sequence, data, defaults); + return new ClassicTilesetSpecificSpriteSequence(cache, this, image, sequence, data, defaults); } } @@ -34,20 +34,56 @@ namespace OpenRA.Mods.Cnc.Graphics [Desc("Dictionary of : filename to override the Filename key.")] static readonly SpriteSequenceField> TilesetFilenames = new SpriteSequenceField>(nameof(TilesetFilenames), null); - public ClassicTilesetSpecificSpriteSequence(ModData modData, string tileset, SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults) - : base(modData, tileset, cache, loader, image, sequence, data, defaults) { } + public ClassicTilesetSpecificSpriteSequence(SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults) + : base(cache, loader, image, sequence, data, defaults) { } - protected override string GetSpriteFilename(ModData modData, string tileset, string image, string sequence, MiniYaml data, MiniYaml defaults) + protected override IEnumerable ParseFilenames(ModData modData, string tileset, int[] frames, MiniYaml data, MiniYaml defaults) { var node = data.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key) ?? defaults.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key); if (node != null) { var tilesetNode = node.Value.Nodes.FirstOrDefault(n => n.Key == tileset); if (tilesetNode != null) - return tilesetNode.Value.Value; + { + // Only request the subset of frames that we actually need + int[] loadFrames = null; + if (length != null) + { + loadFrames = CalculateFrameIndices(start, length.Value, stride ?? length.Value, facings, frames, transpose, reverseFacings); + if (shadowStart >= 0) + loadFrames = loadFrames.Concat(loadFrames.Select(i => i + shadowStart - start)).ToArray(); + } + + return new[] { new ReservationInfo(tilesetNode.Value.Value, loadFrames, frames, tilesetNode.Location) }; + } } - return base.GetSpriteFilename(modData, tileset, image, sequence, data, defaults); + return base.ParseFilenames(modData, tileset, frames, data, defaults); + } + + protected override IEnumerable ParseCombineFilenames(ModData modData, string tileset, int[] frames, MiniYaml data) + { + var node = data.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key); + if (node != null) + { + var tilesetNode = node.Value.Nodes.FirstOrDefault(n => n.Key == tileset); + if (tilesetNode != null) + { + if (frames == null) + { + if (LoadField("Length", null, data) != "*") + { + var subStart = LoadField("Start", 0, data); + var subLength = LoadField("Length", 1, data); + frames = Exts.MakeArray(subLength, i => subStart + i); + } + } + + return new[] { new ReservationInfo(tilesetNode.Value.Value, frames, frames, tilesetNode.Location) }; + } + } + + return base.ParseCombineFilenames(modData, tileset, frames, data); } } } diff --git a/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs b/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs index 1c48d7b8ad..bc61b63a7b 100644 --- a/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs +++ b/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs @@ -21,29 +21,35 @@ namespace OpenRA.Mods.Common.Graphics { public class DefaultSpriteSequenceLoader : ISpriteSequenceLoader { + public readonly int BgraSheetSize = 2048; + public readonly int IndexedSheetSize = 2048; + static readonly MiniYaml NoData = new MiniYaml(null); - public DefaultSpriteSequenceLoader(ModData modData) { } + public DefaultSpriteSequenceLoader(ModData modData) + { + var metadata = modData.Manifest.Get().Metadata; + if (metadata.TryGetValue("BgraSheetSize", out var yaml)) + BgraSheetSize = FieldLoader.GetValue("BgraSheetSize", yaml.Value); + + if (metadata.TryGetValue("IndexedSheetSize", out yaml)) + IndexedSheetSize = FieldLoader.GetValue("IndexedSheetSize", yaml.Value); + } public virtual ISpriteSequence CreateSequence(ModData modData, string tileset, SpriteCache cache, string image, string sequence, MiniYaml data, MiniYaml defaults) { - return new DefaultSpriteSequence(modData, tileset, cache, this, image, sequence, data, defaults); + return new DefaultSpriteSequence(cache, this, image, sequence, data, defaults); } - public IReadOnlyDictionary ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode imageNode) + int ISpriteSequenceLoader.BgraSheetSize => BgraSheetSize; + int ISpriteSequenceLoader.IndexedSheetSize => IndexedSheetSize; + + IReadOnlyDictionary ISpriteSequenceLoader.ParseSequences(ModData modData, string tileset, SpriteCache cache, MiniYamlNode imageNode) { var sequences = new Dictionary(); - MiniYaml defaults; - try - { - var node = imageNode.Value.Nodes.SingleOrDefault(n => n.Key == "Defaults"); - defaults = node?.Value ?? NoData; - imageNode.Value.Nodes.Remove(node); - } - catch (Exception e) - { - throw new InvalidDataException($"Error occurred while parsing {imageNode.Key}", e); - } + var node = imageNode.Value.Nodes.SingleOrDefault(n => n.Key == "Defaults"); + var defaults = node?.Value ?? NoData; + imageNode.Value.Nodes.Remove(node); foreach (var sequenceNode in imageNode.Value.Nodes) { @@ -51,18 +57,13 @@ namespace OpenRA.Mods.Common.Graphics { try { - var sequence = CreateSequence(modData, tileSet, cache, imageNode.Key, sequenceNode.Key, sequenceNode.Value, defaults); + var sequence = CreateSequence(modData, tileset, cache, imageNode.Key, sequenceNode.Key, sequenceNode.Value, defaults); + ((DefaultSpriteSequence)sequence).ReserveSprites(modData, tileset, cache, sequenceNode.Value, defaults); sequences.Add(sequenceNode.Key, sequence); } - catch (FileNotFoundException ex) + catch (Exception e) { - // Defer exception until something tries to access the sequence - // This allows the asset installer and OpenRA.Utility to load the game without having the actor assets - sequences.Add(sequenceNode.Key, new FileNotFoundSequence(ex)); - } - catch (FormatException f) - { - throw new FormatException($"Failed to parse sequences for {imageNode.Key}.{sequenceNode.Key} at {imageNode.Value.Nodes[0].Location}:\n{f}"); + throw new InvalidDataException($"Failed to parse sequences for {imageNode.Key}.{sequenceNode.Key} at {imageNode.Value.Nodes[0].Location}:\n{e}"); } } } @@ -71,33 +72,6 @@ namespace OpenRA.Mods.Common.Graphics } } - public class FileNotFoundSequence : ISpriteSequence - { - readonly FileNotFoundException exception; - - public FileNotFoundSequence(FileNotFoundException exception) - { - this.exception = exception; - } - - public string Filename => exception.FileName; - - string ISpriteSequence.Name => throw exception; - int ISpriteSequence.Length => throw exception; - int ISpriteSequence.Facings => throw exception; - int ISpriteSequence.Tick => throw exception; - int ISpriteSequence.ZOffset => throw exception; - int ISpriteSequence.ShadowZOffset => throw exception; - Rectangle ISpriteSequence.Bounds => throw exception; - bool ISpriteSequence.IgnoreWorldTint => throw exception; - float ISpriteSequence.Scale => throw exception; - Sprite ISpriteSequence.GetSprite(int frame) { throw exception; } - Sprite ISpriteSequence.GetSprite(int frame, WAngle facing) { throw exception; } - (Sprite, WAngle) ISpriteSequence.GetSpriteWithRotation(int frame, WAngle facing) { throw exception; } - Sprite ISpriteSequence.GetShadow(int frame, WAngle facing) { throw exception; } - float ISpriteSequence.GetAlpha(int frame) { throw exception; } - } - public struct SpriteSequenceField { public string Key; @@ -113,139 +87,200 @@ namespace OpenRA.Mods.Common.Graphics [Desc("Generic sprite sequence implementation, mostly unencumbered with game- or artwork-specific logic.")] public class DefaultSpriteSequence : ISpriteSequence { - static readonly MiniYaml NoData = new MiniYaml(null); + protected class SpriteReservation + { + public int Token; + public float3 Offset; + public bool FlipX; + public bool FlipY; + public float ZRamp; + public BlendMode BlendMode; + public int[] Frames; + } - readonly string image; - protected Sprite[] sprites; - readonly bool reverseFacings, transpose; + protected readonly struct ReservationInfo + { + public readonly string Filename; + public readonly int[] LoadFrames; + public readonly int[] Frames; + public readonly MiniYamlNode.SourceLocation Location; - protected readonly ISpriteSequenceLoader Loader; - - public Rectangle Bounds { get; } - - public string Name { get; } + public ReservationInfo(string filename, int[] loadFrames, int[] frames, MiniYamlNode.SourceLocation location) + { + Filename = filename; + LoadFrames = loadFrames; + Frames = frames; + Location = location; + } + } [Desc("File name of the sprite to use for this sequence.")] - static readonly SpriteSequenceField Filename = new SpriteSequenceField(nameof(Filename), null); + protected static readonly SpriteSequenceField Filename = new SpriteSequenceField(nameof(Filename), null); [Desc("Frame index to start from.")] - static readonly SpriteSequenceField Start = new SpriteSequenceField(nameof(Start), 0); - int start; + protected static readonly SpriteSequenceField Start = new SpriteSequenceField(nameof(Start), 0); [Desc("Number of frames to use. Does not have to be the total amount the sprite sheet has.")] - static readonly SpriteSequenceField Length = new SpriteSequenceField(nameof(Length), 1); - int ISpriteSequence.Length => length; - int length; + protected static readonly SpriteSequenceField Length = new SpriteSequenceField(nameof(Length), 1); [Desc("Overrides Length if a different number of frames is defined between facings.")] - static readonly SpriteSequenceField Stride = new SpriteSequenceField(nameof(Stride), -1); - int stride; + protected static readonly SpriteSequenceField Stride = new SpriteSequenceField(nameof(Stride), -1); - [Desc("The amount of directions the unit faces. Use negative values to rotate counter-clockwise.")] - static readonly SpriteSequenceField Facings = new SpriteSequenceField(nameof(Facings), 1); - int ISpriteSequence.Facings => interpolatedFacings ?? facings; - protected int facings; + [Desc("The number of facings that are provided by sprite frames. Use negative values to rotate counter-clockwise.")] + protected static readonly SpriteSequenceField Facings = new SpriteSequenceField(nameof(Facings), 1); - [Desc("The amount of directions the unit faces. Use negative values to rotate counter-clockwise.")] - static readonly SpriteSequenceField InterpolatedFacings = new SpriteSequenceField(nameof(InterpolatedFacings), null); - protected int? interpolatedFacings; + [Desc("The total number of facings for the sequence. If >Facings, the closest facing sprite will be rotated to match. Use negative values to rotate counter-clockwise.")] + protected static readonly SpriteSequenceField InterpolatedFacings = new SpriteSequenceField(nameof(InterpolatedFacings), null); [Desc("Time (in milliseconds at default game speed) to wait until playing the next frame in the animation.")] - static readonly SpriteSequenceField Tick = new SpriteSequenceField(nameof(Tick), 40); - int ISpriteSequence.Tick => tick; - readonly int tick; + protected static readonly SpriteSequenceField Tick = new SpriteSequenceField(nameof(Tick), 40); [Desc("Value controlling the Z-order. A higher values means rendering on top of other sprites at the same position. " + - "Use power of 2 values to avoid glitches.")] - static readonly SpriteSequenceField ZOffset = new SpriteSequenceField(nameof(ZOffset), WDist.Zero); - int ISpriteSequence.ZOffset => zOffset; - readonly int zOffset; + "Use power of 2 values to avoid glitches.")] + protected static readonly SpriteSequenceField ZOffset = new SpriteSequenceField(nameof(ZOffset), WDist.Zero); [Desc("Additional sprite depth Z offset to apply as a function of sprite Y (0: vertical, 1: flat on terrain)")] - static readonly SpriteSequenceField ZRamp = new SpriteSequenceField(nameof(ZRamp), 0); + protected static readonly SpriteSequenceField ZRamp = new SpriteSequenceField(nameof(ZRamp), 0); [Desc("If the shadow is not part of the sprite, but baked into the same sprite sheet at a fixed offset, " + "set this to the frame index where it starts.")] - static readonly SpriteSequenceField ShadowStart = new SpriteSequenceField(nameof(ShadowStart), -1); - readonly int shadowStart; + protected static readonly SpriteSequenceField ShadowStart = new SpriteSequenceField(nameof(ShadowStart), -1); [Desc("Set Z-Offset for the separate shadow. Used by the later Westwood 2.5D titles.")] - static readonly SpriteSequenceField ShadowZOffset = new SpriteSequenceField(nameof(ShadowZOffset), new WDist(-5)); - int ISpriteSequence.ShadowZOffset => shadowZOffset; - readonly int shadowZOffset; + protected static readonly SpriteSequenceField ShadowZOffset = new SpriteSequenceField(nameof(ShadowZOffset), new WDist(-5)); [Desc("The individual frames to play instead of going through them sequentially from the `Start`.")] - static readonly SpriteSequenceField Frames = new SpriteSequenceField(nameof(Frames), null); - int[] frames; + protected static readonly SpriteSequenceField Frames = new SpriteSequenceField(nameof(Frames), null); [Desc("Don't apply terrain lighting or colored overlays.")] - static readonly SpriteSequenceField IgnoreWorldTint = new SpriteSequenceField(nameof(IgnoreWorldTint), false); - bool ISpriteSequence.IgnoreWorldTint => ignoreWorldTint; - readonly bool ignoreWorldTint; + protected static readonly SpriteSequenceField IgnoreWorldTint = new SpriteSequenceField(nameof(IgnoreWorldTint), false); [Desc("Adjusts the rendered size of the sprite")] - static readonly SpriteSequenceField Scale = new SpriteSequenceField(nameof(Scale), 1); - float ISpriteSequence.Scale => scale; - readonly float scale; + protected static readonly SpriteSequenceField Scale = new SpriteSequenceField(nameof(Scale), 1); [Desc("Play the sprite sequence back and forth.")] - static readonly SpriteSequenceField Reverses = new SpriteSequenceField(nameof(Reverses), false); + protected static readonly SpriteSequenceField Reverses = new SpriteSequenceField(nameof(Reverses), false); [Desc("Support a frame order where each animation step is split per each direction.")] - static readonly SpriteSequenceField Transpose = new SpriteSequenceField(nameof(Transpose), false); + protected static readonly SpriteSequenceField Transpose = new SpriteSequenceField(nameof(Transpose), false); [Desc("Mirror on the X axis.")] - static readonly SpriteSequenceField FlipX = new SpriteSequenceField(nameof(FlipX), false); + protected static readonly SpriteSequenceField FlipX = new SpriteSequenceField(nameof(FlipX), false); [Desc("Mirror on the Y axis.")] - static readonly SpriteSequenceField FlipY = new SpriteSequenceField(nameof(FlipY), false); + protected static readonly SpriteSequenceField FlipY = new SpriteSequenceField(nameof(FlipY), false); [Desc("Change the position in-game on X, Y, Z.")] - static readonly SpriteSequenceField Offset = new SpriteSequenceField(nameof(Offset), float3.Zero); + protected static readonly SpriteSequenceField Offset = new SpriteSequenceField(nameof(Offset), float3.Zero); [Desc("Apply an OpenGL/Photoshop inspired blend mode.")] - static readonly SpriteSequenceField BlendMode = new SpriteSequenceField(nameof(BlendMode), OpenRA.BlendMode.Alpha); + protected static readonly SpriteSequenceField BlendMode = new SpriteSequenceField(nameof(BlendMode), OpenRA.BlendMode.Alpha); - [Desc("Allows to append multiple sequence definitions which are indented below this node " + - "like when offsets differ per frame or a sequence is spread across individual files.")] - static readonly SpriteSequenceField Combine = new SpriteSequenceField(nameof(Combine), null); + [Desc("Create a virtual sprite file by concatenating one or more frames from multiple files, with optional transformations applied. " + + "All defined frames will be loaded into memory, even if unused, so use this property with care.")] + protected static readonly SpriteSequenceField Combine = new SpriteSequenceField(nameof(Combine), null); [Desc("Sets transparency - use one value to set for all frames or provide a value for each frame.")] - static readonly SpriteSequenceField Alpha = new SpriteSequenceField(nameof(Alpha), null); - readonly float[] alpha; + protected static readonly SpriteSequenceField Alpha = new SpriteSequenceField(nameof(Alpha), null); [Desc("Fade the animation from fully opaque on the first frame to fully transparent after the last frame.")] - static readonly SpriteSequenceField AlphaFade = new SpriteSequenceField(nameof(AlphaFade), false); + protected static readonly SpriteSequenceField AlphaFade = new SpriteSequenceField(nameof(AlphaFade), false); [Desc("Name of the file containing the depth data sprite.")] - static readonly SpriteSequenceField DepthSprite = new SpriteSequenceField(nameof(DepthSprite), null); + protected static readonly SpriteSequenceField DepthSprite = new SpriteSequenceField(nameof(DepthSprite), null); [Desc("Frame index containing the depth data.")] - static readonly SpriteSequenceField DepthSpriteFrame = new SpriteSequenceField(nameof(DepthSpriteFrame), 0); + protected static readonly SpriteSequenceField DepthSpriteFrame = new SpriteSequenceField(nameof(DepthSpriteFrame), 0); [Desc("X, Y offset to apply to the depth sprite.")] - static readonly SpriteSequenceField DepthSpriteOffset = new SpriteSequenceField(nameof(DepthSpriteOffset), float2.Zero); + protected static readonly SpriteSequenceField DepthSpriteOffset = new SpriteSequenceField(nameof(DepthSpriteOffset), float2.Zero); - protected virtual string GetSpriteFilename(ModData modData, string tileset, string image, string sequence, MiniYaml data, MiniYaml defaults) + protected static readonly MiniYaml NoData = new MiniYaml(null); + protected readonly ISpriteSequenceLoader Loader; + + protected string image; + protected List spritesToLoad = new List(); + protected Sprite[] sprites; + protected Sprite[] shadowSprites; + protected bool reverseFacings; + protected bool reverses; + + protected int start; + protected int shadowStart; + protected int? length; + protected int? stride; + protected bool transpose; + + protected int facings; + protected int? interpolatedFacings; + protected int tick; + protected int zOffset; + protected int shadowZOffset; + protected bool ignoreWorldTint; + protected float scale; + protected float[] alpha; + protected bool alphaFade; + protected Rectangle? bounds; + + protected int? depthSpriteReservation; + protected float2 depthSpriteOffset; + + protected void ThrowIfUnresolved() { - return LoadField(Filename, data, defaults); + if (bounds == null) + throw new InvalidOperationException($"Unable to query unresolved sequence {image}.{Name}."); } - protected static T LoadField(string key, T fallback, MiniYaml data, MiniYaml defaults) + int ISpriteSequence.Length { - var node = data.Nodes.FirstOrDefault(n => n.Key == key) ?? defaults.Nodes.FirstOrDefault(n => n.Key == key); + get + { + ThrowIfUnresolved(); + return length.Value; + } + } + + int ISpriteSequence.Facings => interpolatedFacings ?? facings; + int ISpriteSequence.Tick => tick; + int ISpriteSequence.ZOffset => zOffset; + int ISpriteSequence.ShadowZOffset => shadowZOffset; + bool ISpriteSequence.IgnoreWorldTint => ignoreWorldTint; + float ISpriteSequence.Scale => GetScale(); + Rectangle ISpriteSequence.Bounds + { + get + { + ThrowIfUnresolved(); + return bounds.Value; + } + } + + public string Name { get; } + + protected static T LoadField(string key, T fallback, MiniYaml data, MiniYaml defaults = null) + { + var node = data.Nodes.FirstOrDefault(n => n.Key == key) ?? defaults?.Nodes.FirstOrDefault(n => n.Key == key); if (node == null) return fallback; return FieldLoader.GetValue(key, node.Value.Value); } - protected static T LoadField(SpriteSequenceField field, MiniYaml data, MiniYaml defaults) + protected static T LoadField(SpriteSequenceField field, MiniYaml data, MiniYaml defaults = null) { - var node = data.Nodes.FirstOrDefault(n => n.Key == field.Key) ?? defaults.Nodes.FirstOrDefault(n => n.Key == field.Key); - if (node == null) - return field.DefaultValue; + return LoadField(field, data, defaults, out _); + } + protected static T LoadField(SpriteSequenceField field, MiniYaml data, MiniYaml defaults, out MiniYamlNode.SourceLocation location) + { + var node = data.Nodes.FirstOrDefault(n => n.Key == field.Key) ?? defaults?.Nodes.FirstOrDefault(n => n.Key == field.Key); + if (node == null) + { + location = default; + return field.DefaultValue; + } + + location = node.Location; return FieldLoader.GetValue(field.Key, node.Value.Value); } @@ -259,30 +294,92 @@ namespace OpenRA.Mods.Common.Graphics return Rectangle.FromLTRB(left, top, right, bottom); } - public DefaultSpriteSequence(ModData modData, string tileSet, SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults) + protected static int[] CalculateFrameIndices(int start, int length, int stride, int facings, int[] frames, bool transpose, bool reverseFacings) + { + var usedFrames = new List(); + for (var facing = 0; facing < facings; facing++) + { + var facingInner = reverseFacings ? (facings - facing) % facings : facing; + for (var frame = 0; frame < length; frame++) + { + var i = transpose ? frame % length * facings + facingInner : + facingInner * stride + frame % length; + + usedFrames.Add(frames?[i] ?? start + i); + } + } + + return usedFrames.ToArray(); + } + + protected virtual IEnumerable ParseFilenames(ModData modData, string tileset, int[] frames, MiniYaml data, MiniYaml defaults) + { + var filename = LoadField(Filename, data, defaults, out var location); + + // Only request the subset of frames that we actually need. + int[] loadFrames = null; + if (length != null) + { + loadFrames = CalculateFrameIndices(start, length.Value, stride ?? length.Value, facings, frames, transpose, reverseFacings); + if (shadowStart >= 0) + loadFrames = loadFrames.Concat(loadFrames.Select(i => i + shadowStart - start)).ToArray(); + } + + yield return new ReservationInfo(filename, loadFrames, frames, location); + } + + protected virtual IEnumerable ParseCombineFilenames(ModData modData, string tileset, int[] frames, MiniYaml data) + { + var filename = LoadField(Filename, data, null, out var location); + if (frames == null) + { + if (LoadField(Length.Key, null, data) != "*") + { + var subStart = LoadField("Start", 0, data); + var subLength = LoadField("Length", 1, data); + frames = Exts.MakeArray(subLength, i => subStart + i); + } + } + + yield return new ReservationInfo(filename, frames, frames, location); + } + + public DefaultSpriteSequence(SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults) { this.image = image; Name = sequence; Loader = loader; start = LoadField(Start, data, defaults); + + length = null; + var lengthLocation = default(MiniYamlNode.SourceLocation); + if (LoadField(Length.Key, null, data, defaults) != "*") + length = LoadField(Length, data, defaults, out lengthLocation); + + stride = LoadField(Stride.Key, length, data, defaults); + facings = LoadField(Facings, data, defaults, out var facingsLocation); + interpolatedFacings = LoadField(InterpolatedFacings, data, defaults, out var interpolatedFacingsLocation); + + tick = LoadField(Tick, data, defaults); + zOffset = LoadField(ZOffset, data, defaults).Length; + shadowStart = LoadField(ShadowStart, data, defaults); shadowZOffset = LoadField(ShadowZOffset, data, defaults).Length; - zOffset = LoadField(ZOffset, data, defaults).Length; - tick = LoadField(Tick, data, defaults); - transpose = LoadField(Transpose, data, defaults); - frames = LoadField(Frames, data, defaults); + ignoreWorldTint = LoadField(IgnoreWorldTint, data, defaults); scale = LoadField(Scale, data, defaults); - var flipX = LoadField(FlipX, data, defaults); - var flipY = LoadField(FlipY, data, defaults); - var zRamp = LoadField(ZRamp, data, defaults); + reverses = LoadField(Reverses, data, defaults); + transpose = LoadField(Transpose, data, defaults); + alpha = LoadField(Alpha, data, defaults); + alphaFade = LoadField(AlphaFade, data, defaults, out var alphaFadeLocation); - facings = LoadField(Facings, data, defaults); - interpolatedFacings = LoadField(InterpolatedFacings, data, defaults); - if (interpolatedFacings != null && (interpolatedFacings <= 1 || interpolatedFacings <= Math.Abs(facings) || interpolatedFacings > 1024 || !Exts.IsPowerOf2(interpolatedFacings.Value))) - throw new YamlException($"Sequence {image}.{sequence}: InterpolatedFacings must be greater than Facings, within the range of 2 to 1024, and a power of 2."); + var depthSprite = LoadField(DepthSprite, data, defaults, out var depthSpriteLocation); + if (!string.IsNullOrEmpty(depthSprite)) + depthSpriteReservation = cache.ReserveSprites(depthSprite, new[] { LoadField(DepthSpriteFrame, data, defaults) }, depthSpriteLocation); + + depthSpriteOffset = LoadField(DepthSpriteOffset, data, defaults); if (facings < 0) { @@ -290,216 +387,157 @@ namespace OpenRA.Mods.Common.Graphics facings = -facings; } + if (interpolatedFacings != null && (interpolatedFacings < 2 || interpolatedFacings <= facings || interpolatedFacings > 1024 || !Exts.IsPowerOf2(interpolatedFacings.Value))) + throw new YamlException($"{interpolatedFacingsLocation}: {InterpolatedFacings.Key} must be greater than {Facings.Key}, within the range of 2 to 1024, and a power of 2."); + + if (length != null && length <= 0) + throw new YamlException($"{lengthLocation}: {Length.Key} must be positive."); + + if (length == null && facings > 1) + throw new YamlException($"{facingsLocation}: {Facings.Key} cannot be used with {Length.Key}: *."); + + if (alphaFade && alpha != null) + throw new YamlException($"{alphaFadeLocation}: {AlphaFade.Key} cannot be used with {Alpha.Key}."); + } + + public virtual void ReserveSprites(ModData modData, string tileset, SpriteCache cache, MiniYaml data, MiniYaml defaults) + { + var frames = LoadField(Frames, data, defaults); + var flipX = LoadField(FlipX, data, defaults); + var flipY = LoadField(FlipY, data, defaults); + var zRamp = LoadField(ZRamp, data, defaults); var offset = LoadField(Offset, data, defaults); var blendMode = LoadField(BlendMode, data, defaults); - IEnumerable GetUsedFrames(int frameCount) - { - if (LoadField(Length.Key, "", data, defaults) == "*") - length = frames?.Length ?? frameCount - start; - else - length = LoadField(Length, data, defaults); - - // Plays the animation forwards, and then in reverse - if (LoadField(Reverses, data, defaults)) - { - var frames = this.frames != null ? this.frames.Skip(start).Take(length).ToArray() : Exts.MakeArray(length, i => start + i); - this.frames = frames.Concat(frames.Skip(1).Take(length - 2).Reverse()).ToArray(); - length = 2 * length - 2; - start = 0; - } - - // Overrides Length with a custom stride - stride = LoadField(Stride.Key, length, data, defaults); - - if (length > stride) - throw new YamlException($"Sequence {image}.{sequence}: Length must be <= stride"); - - if (frames != null && length > frames.Length) - throw new YamlException($"Sequence {image}.{sequence}: Length must be <= Frames.Length"); - - var end = start + (facings - 1) * stride + length - 1; - if (frames != null) - { - foreach (var f in frames) - if (f < 0 || f >= frameCount) - throw new YamlException($"Sequence {image}.{sequence} defines a Frames override that references frame {f}, but only [{start}..{end}] actually exist"); - - if (start < 0 || end >= frames.Length) - throw new YamlException($"Sequence {image}.{sequence} uses indices [{start}..{end}] of the Frames list, but only {frames.Length} frames are defined"); - } - else if (start < 0 || end >= frameCount) - throw new YamlException($"Sequence {image}.{sequence} uses frames [{start}..{end}], but only [0..{frameCount - 1}] actually exist"); - - if (shadowStart >= 0 && shadowStart + (facings - 1) * stride + length > frameCount) - throw new YamlException($"Sequence {image}.{sequence}'s shadow frames use frames [{shadowStart}..{shadowStart + (facings - 1) * stride + length - 1}], but only [0..{frameCount - 1}] actually exist"); - - var usedFrames = new List(); - for (var facing = 0; facing < facings; facing++) - { - for (var frame = 0; frame < length; frame++) - { - var i = transpose ? frame % length * facings + facing : - facing * stride + frame % length; - - usedFrames.Add(frames != null ? frames[i] : start + i); - } - } - - if (shadowStart >= 0) - return usedFrames.Concat(usedFrames.Select(i => i + shadowStart - start)); - - return usedFrames; - } - var combineNode = data.Nodes.FirstOrDefault(n => n.Key == Combine.Key); if (combineNode != null) { - var combined = Enumerable.Empty(); - foreach (var combineSequenceNode in combineNode.Value.Nodes) + for (var i = 0; i < combineNode.Value.Nodes.Count; i++) { - var combineData = combineSequenceNode.Value; + var subData = combineNode.Value.Nodes[i].Value; + var subOffset = LoadField(Offset, subData, NoData); + var subFlipX = LoadField(FlipX, subData, NoData); + var subFlipY = LoadField(FlipY, subData, NoData); + var subFrames = LoadField(Frames, data); - // Allow per-sprite offset, flipping, start, and length - // These shouldn't inherit Start/Offset/etc from the main definition - var subStart = LoadField(Start, combineData, NoData); - var subOffset = LoadField(Offset, combineData, NoData); - var subFlipX = LoadField(FlipX, combineData, NoData); - var subFlipY = LoadField(FlipY, combineData, NoData); - var subFrames = LoadField(Frames, combineData, NoData); - var subLength = 0; - - IEnumerable SubGetUsedFrames(int subFrameCount) + foreach (var f in ParseCombineFilenames(modData, tileset, subFrames, subData)) { - var combineLengthNode = combineData.Nodes.FirstOrDefault(n => n.Key == Length.Key); - if (combineLengthNode?.Value.Value == "*") - subLength = subFrames?.Length ?? subFrameCount - subStart; - else - subLength = LoadField(Length, combineData, NoData); - - return subFrames != null ? subFrames.Skip(subStart).Take(subLength) : Enumerable.Range(subStart, subLength); + spritesToLoad.Add(new SpriteReservation + { + Token = cache.ReserveSprites(f.Filename, f.LoadFrames, f.Location), + Offset = subOffset + offset, + FlipX = subFlipX ^ flipX, + FlipY = subFlipY ^ flipY, + BlendMode = blendMode, + ZRamp = zRamp, + Frames = f.Frames + }); } - - var subFilename = GetSpriteFilename(modData, tileSet, image, sequence, combineData, NoData); - if (subFilename == null) - throw new YamlException($"Sequence {image}.{sequence}.{combineSequenceNode.Key} does not define a filename."); - - var subSprites = cache[subFilename, SubGetUsedFrames].Select(s => - { - if (s == null) - return null; - - var bounds = FlipRectangle(s.Bounds, subFlipX, subFlipY); - var dx = subOffset.X + offset.X + (subFlipX ? -s.Offset.X : s.Offset.X); - var dy = subOffset.Y + offset.Y + (subFlipY ? -s.Offset.Y : s.Offset.Y); - var dz = subOffset.Z + offset.Z + s.Offset.Z + zRamp * dy; - - return new Sprite(s.Sheet, bounds, zRamp, new float3(dx, dy, dz), s.Channel, blendMode); - }).ToList(); - - var frames = subFrames != null ? subFrames.Skip(subStart).Take(subLength).ToArray() : Exts.MakeArray(subLength, i => subStart + i); - combined = combined.Concat(frames.Select(i => subSprites[i])); } - - sprites = combined.ToArray(); - GetUsedFrames(sprites.Length); } else { - // Apply offset to each sprite in the sequence - // Different sequences may apply different offsets to the same frame - var filename = GetSpriteFilename(modData, tileSet, image, sequence, data, defaults); - if (filename == null) - throw new YamlException($"Sequence {image}.{sequence} does not define a filename."); + foreach (var f in ParseFilenames(modData, tileset, frames, data, defaults)) + { + spritesToLoad.Add(new SpriteReservation + { + Token = cache.ReserveSprites(f.Filename, f.LoadFrames, f.Location), + Offset = offset, + FlipX = flipX, + FlipY = flipY, + BlendMode = blendMode, + ZRamp = zRamp, + Frames = f.Frames, + }); + } + } + } - sprites = cache[filename, GetUsedFrames].Select(s => + public virtual void ResolveSprites(SpriteCache cache) + { + if (bounds != null) + return; + + Sprite depthSprite = null; + if (depthSpriteReservation != null) + depthSprite = cache.ResolveSprites(depthSpriteReservation.Value).First(s => s != null); + + var allSprites = spritesToLoad.SelectMany(r => + { + var resolved = cache.ResolveSprites(r.Token); + if (r.Frames != null) + resolved = r.Frames.Select(f => resolved[f]).ToArray(); + + return resolved.Select(s => { if (s == null) return null; - var bounds = FlipRectangle(s.Bounds, flipX, flipY); - var dx = offset.X + (flipX ? -s.Offset.X : s.Offset.X); - var dy = offset.Y + (flipY ? -s.Offset.Y : s.Offset.Y); - var dz = offset.Z + s.Offset.Z + zRamp * dy; + var dx = r.Offset.X + (r.FlipX ? -s.Offset.X : s.Offset.X); + var dy = r.Offset.Y + (r.FlipY ? -s.Offset.Y : s.Offset.Y); + var dz = r.Offset.Z + s.Offset.Z + r.ZRamp * dy; + var sprite = new Sprite(s.Sheet, FlipRectangle(s.Bounds, r.FlipX, r.FlipY), r.ZRamp, new float3(dx, dy, dz), s.Channel, r.BlendMode); + if (depthSprite == null) + return sprite; - return new Sprite(s.Sheet, bounds, zRamp, new float3(dx, dy, dz), s.Channel, blendMode); - }).ToArray(); - } - - alpha = LoadField(Alpha, data, defaults); - if (alpha != null) - { - if (alpha.Length == 1) - alpha = Exts.MakeArray(length, _ => alpha[0]); - else if (alpha.Length != length) - throw new YamlException($"Sequence {image}.{sequence} must define either 1 or {length} Alpha values."); - } - - if (LoadField(AlphaFade, data, defaults)) - { - if (alpha != null) - throw new YamlException($"Sequence {image}.{sequence} cannot define both AlphaFade and Alpha."); - - alpha = Exts.MakeArray(length, i => float2.Lerp(1f, 0f, i / (length - 1f))); - } - - var depthSprite = LoadField(DepthSprite, data, defaults); - if (!string.IsNullOrEmpty(depthSprite)) - { - var depthSpriteFrame = LoadField(DepthSpriteFrame, data, defaults); - var depthOffset = LoadField(DepthSpriteOffset, data, defaults); - IEnumerable GetDepthFrame(int _) => new[] { depthSpriteFrame }; - var ds = cache[depthSprite, GetDepthFrame][depthSpriteFrame]; - - sprites = sprites.Select(s => - { - if (s == null) - return null; - - var cw = (ds.Bounds.Left + ds.Bounds.Right) / 2 + (int)(s.Offset.X + depthOffset.X); - var ch = (ds.Bounds.Top + ds.Bounds.Bottom) / 2 + (int)(s.Offset.Y + depthOffset.Y); + var cw = (depthSprite.Bounds.Left + depthSprite.Bounds.Right) / 2 + (int)(s.Offset.X + depthSpriteOffset.X); + var ch = (depthSprite.Bounds.Top + depthSprite.Bounds.Bottom) / 2 + (int)(s.Offset.Y + depthSpriteOffset.Y); var w = s.Bounds.Width / 2; var h = s.Bounds.Height / 2; - var r = Rectangle.FromLTRB(cw - w, ch - h, cw + w, ch + h); - return new SpriteWithSecondaryData(s, ds.Sheet, r, ds.Channel); - }).ToArray(); + return new SpriteWithSecondaryData(sprite, depthSprite.Sheet, Rectangle.FromLTRB(cw - w, ch - h, cw + w, ch + h), depthSprite.Channel); + }); + }).ToArray(); + + length = length ?? allSprites.Length - start; + + if (alpha != null) + { + if (alpha.Length == 1) + alpha = Exts.MakeArray(length.Value, _ => alpha[0]); + else if (alpha.Length != length.Value) + throw new YamlException($"Sequence {image}.{Name} must define either 1 or {length.Value} Alpha values."); + } + else if (alphaFade) + alpha = Exts.MakeArray(length.Value, i => float2.Lerp(1f, 0f, i / (length.Value - 1f))); + + // Reindex sprites to order facings anti-clockwise and remove unused frames + var index = CalculateFrameIndices(start, length.Value, stride ?? length.Value, facings, null, transpose, reverseFacings).ToList(); + if (reverses) + { + index.AddRange(index.Skip(1).Take(length.Value - 2).Reverse()); + length = 2 * length - 2; } - var boundSprites = SpriteBounds(sprites, frames, start, facings, length, stride, transpose); - if (shadowStart > 0) - boundSprites = boundSprites.Concat(SpriteBounds(sprites, frames, shadowStart, facings, length, stride, transpose)); + if (!index.Any()) + throw new YamlException($"Sequence {image}.{Name} does not define any frames."); - Bounds = boundSprites.Union(); + var minIndex = index.Min(); + var maxIndex = index.Max(); + if (minIndex < 0 || maxIndex >= allSprites.Length) + throw new YamlException($"Sequence {image}.{Name} uses frames between {minIndex}..{maxIndex}, but only 0..{allSprites.Length - 1} exist."); + + sprites = index.Select(f => allSprites[f]).ToArray(); + if (shadowStart >= 0) + shadowSprites = index.Select(f => allSprites[f - start + shadowStart]).ToArray(); + + bounds = sprites.Concat(shadowSprites ?? Enumerable.Empty()).Select(OffsetSpriteBounds).Union(); } - /// Returns the bounds of all of the sprites that can appear in this animation - static IEnumerable SpriteBounds(Sprite[] sprites, int[] frames, int start, int facings, int length, int stride, bool transpose) + protected static Rectangle OffsetSpriteBounds(Sprite sprite) { - for (var facing = 0; facing < facings; facing++) - { - for (var frame = 0; frame < length; frame++) - { - var i = transpose ? frame % length * facings + facing : - facing * stride + frame % length; - var s = frames != null ? sprites[frames[i]] : sprites[start + i]; - if (!s.Bounds.IsEmpty) - yield return new Rectangle( - (int)(s.Offset.X - s.Size.X / 2), - (int)(s.Offset.Y - s.Size.Y / 2), - s.Bounds.Width, s.Bounds.Height); - } - } + if (sprite == null || sprite.Bounds.IsEmpty) + return Rectangle.Empty; + + return new Rectangle( + (int)(sprite.Offset.X - sprite.Size.X / 2), + (int)(sprite.Offset.Y - sprite.Size.Y / 2), + sprite.Bounds.Width, sprite.Bounds.Height); } public Sprite GetSprite(int frame) { - return GetSprite(start, frame, WAngle.Zero); - } - - public Sprite GetSprite(int frame, WAngle facing) - { - return GetSprite(start, frame, facing); + return GetSprite(frame, WAngle.Zero); } public (Sprite, WAngle) GetSpriteWithRotation(int frame, WAngle facing) @@ -508,28 +546,31 @@ namespace OpenRA.Mods.Common.Graphics if (interpolatedFacings != null) rotation = Util.GetInterpolatedFacingRotation(facing, Math.Abs(facings), interpolatedFacings.Value); - return (GetSprite(start, frame, facing), rotation); + return (GetSprite(frame, facing), rotation); } public Sprite GetShadow(int frame, WAngle facing) { - return shadowStart >= 0 ? GetSprite(shadowStart, frame, facing) : null; + if (shadowSprites == null) + return null; + + var index = GetFacingFrameOffset(facing) * length.Value + frame % length.Value; + var sprite = shadowSprites[index]; + if (sprite == null) + throw new InvalidOperationException($"Attempted to query unloaded shadow sprite from {image}.{Name} frame={frame} facing={facing}."); + + return sprite; } - protected virtual Sprite GetSprite(int start, int frame, WAngle facing) + public virtual Sprite GetSprite(int frame, WAngle facing) { - var f = GetFacingFrameOffset(facing); - if (reverseFacings) - f = (facings - f) % facings; + ThrowIfUnresolved(); + var index = GetFacingFrameOffset(facing) * length.Value + frame % length.Value; + var sprite = sprites[index]; + if (sprite == null) + throw new InvalidOperationException($"Attempted to query unloaded sprite from {image}.{Name} frame={frame} facing={facing}."); - var i = transpose ? frame % length * facings + f : - f * stride + frame % length; - - var j = frames != null ? frames[i] : start + i; - if (sprites[j] == null) - throw new InvalidOperationException($"Attempted to query unloaded sprite from {image}.{Name} start={start} frame={frame} facing={facing}"); - - return sprites[j]; + return sprite; } protected virtual int GetFacingFrameOffset(WAngle facing) @@ -541,5 +582,10 @@ namespace OpenRA.Mods.Common.Graphics { return alpha?[frame] ?? 1f; } + + protected virtual float GetScale() + { + return scale; + } } } diff --git a/OpenRA.Mods.Common/Graphics/TilesetSpecificSpriteSequence.cs b/OpenRA.Mods.Common/Graphics/TilesetSpecificSpriteSequence.cs index eedf5fd6e8..cebedf57e0 100644 --- a/OpenRA.Mods.Common/Graphics/TilesetSpecificSpriteSequence.cs +++ b/OpenRA.Mods.Common/Graphics/TilesetSpecificSpriteSequence.cs @@ -22,7 +22,7 @@ namespace OpenRA.Mods.Common.Graphics public override ISpriteSequence CreateSequence(ModData modData, string tileSet, SpriteCache cache, string image, string sequence, MiniYaml data, MiniYaml defaults) { - return new TilesetSpecificSpriteSequence(modData, tileSet, cache, this, image, sequence, data, defaults); + return new TilesetSpecificSpriteSequence(cache, this, image, sequence, data, defaults); } } @@ -32,20 +32,56 @@ namespace OpenRA.Mods.Common.Graphics [Desc("Dictionary of : filename to override the Filename key.")] static readonly SpriteSequenceField> TilesetFilenames = new SpriteSequenceField>(nameof(TilesetFilenames), null); - public TilesetSpecificSpriteSequence(ModData modData, string tileset, SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults) - : base(modData, tileset, cache, loader, image, sequence, data, defaults) { } + public TilesetSpecificSpriteSequence(SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults) + : base(cache, loader, image, sequence, data, defaults) { } - protected override string GetSpriteFilename(ModData modData, string tileset, string image, string sequence, MiniYaml data, MiniYaml defaults) + protected override IEnumerable ParseFilenames(ModData modData, string tileset, int[] frames, MiniYaml data, MiniYaml defaults) { var node = data.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key) ?? defaults.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key); if (node != null) { var tilesetNode = node.Value.Nodes.FirstOrDefault(n => n.Key == tileset); if (tilesetNode != null) - return tilesetNode.Value.Value; + { + // Only request the subset of frames that we actually need + int[] loadFrames = null; + if (length != null) + { + loadFrames = CalculateFrameIndices(start, length.Value, stride ?? length.Value, facings, frames, transpose, reverseFacings); + if (shadowStart >= 0) + loadFrames = loadFrames.Concat(loadFrames.Select(i => i + shadowStart - start)).ToArray(); + } + + return new[] { new ReservationInfo(tilesetNode.Value.Value, loadFrames, frames, tilesetNode.Location) }; + } } - return base.GetSpriteFilename(modData, tileset, image, sequence, data, defaults); + return base.ParseFilenames(modData, tileset, frames, data, defaults); + } + + protected override IEnumerable ParseCombineFilenames(ModData modData, string tileset, int[] frames, MiniYaml data) + { + var node = data.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key); + if (node != null) + { + var tilesetNode = node.Value.Nodes.FirstOrDefault(n => n.Key == tileset); + if (tilesetNode != null) + { + if (frames == null) + { + if (LoadField("Length", null, data) != "*") + { + var subStart = LoadField("Start", 0, data); + var subLength = LoadField("Length", 1, data); + frames = Exts.MakeArray(subLength, i => subStart + i); + } + } + + return new[] { new ReservationInfo(tilesetNode.Value.Value, frames, frames, tilesetNode.Location) }; + } + } + + return base.ParseCombineFilenames(modData, tileset, frames, data); } } } diff --git a/OpenRA.Mods.Common/Lint/CheckSequences.cs b/OpenRA.Mods.Common/Lint/CheckSequences.cs index ef236dee31..8292c0db60 100644 --- a/OpenRA.Mods.Common/Lint/CheckSequences.cs +++ b/OpenRA.Mods.Common/Lint/CheckSequences.cs @@ -20,7 +20,7 @@ namespace OpenRA.Mods.Common.Lint { class CheckSequences : ILintSequencesPass { - void ILintSequencesPass.Run(Action emitError, Action emitWarning, ModData modData, Ruleset rules, SequenceProvider sequences) + void ILintSequencesPass.Run(Action emitError, Action emitWarning, ModData modData, Ruleset rules, SequenceSet sequences) { var factions = rules.Actors[SystemActors.World].TraitInfos().Select(f => f.InternalName).ToArray(); foreach (var actorInfo in rules.Actors) diff --git a/OpenRA.Mods.Common/Traits/QuantizeFacingsFromSequence.cs b/OpenRA.Mods.Common/Traits/QuantizeFacingsFromSequence.cs index 0d45d00f85..d96134a991 100644 --- a/OpenRA.Mods.Common/Traits/QuantizeFacingsFromSequence.cs +++ b/OpenRA.Mods.Common/Traits/QuantizeFacingsFromSequence.cs @@ -23,7 +23,7 @@ namespace OpenRA.Mods.Common.Traits [Desc("Defines sequence to derive facings from.")] public readonly string Sequence = "idle"; - public int QuantizedBodyFacings(ActorInfo ai, SequenceProvider sequences, string faction) + public int QuantizedBodyFacings(ActorInfo ai, SequenceSet sequences, string faction) { if (string.IsNullOrEmpty(Sequence)) throw new InvalidOperationException($"Actor {ai.Name} is missing sequence to quantize facings from."); diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index bba83eff8b..96fcf6397c 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -35,7 +35,7 @@ namespace OpenRA.Mods.Common.Traits public interface IQuantizeBodyOrientationInfo : ITraitInfoInterface { - int QuantizedBodyFacings(ActorInfo ai, SequenceProvider sequenceProvider, string race); + int QuantizedBodyFacings(ActorInfo ai, SequenceSet sequences, string faction); } public interface IPlaceBuildingDecorationInfo : ITraitInfoInterface diff --git a/OpenRA.Mods.Common/UtilityCommands/CheckMissingSprites.cs b/OpenRA.Mods.Common/UtilityCommands/CheckMissingSprites.cs index c6bd77438f..cf2d55bac5 100644 --- a/OpenRA.Mods.Common/UtilityCommands/CheckMissingSprites.cs +++ b/OpenRA.Mods.Common/UtilityCommands/CheckMissingSprites.cs @@ -10,7 +10,7 @@ #endregion using System; -using OpenRA.Mods.Common.Graphics; +using OpenRA.Graphics; using OpenRA.Mods.Common.Terrain; using OpenRA.Mods.Common.Traits; @@ -38,29 +38,21 @@ namespace OpenRA.Mods.Common.UtilityCommands // any tilesets from being checked further. try { - // DefaultSequences is a dictionary of tileset: SequenceProvider - // so we can also use this to key our tileset checks - foreach (var kv in modData.DefaultSequences) + foreach (var (tileset, terrainInfo) in modData.DefaultTerrainInfo) { try { - Console.WriteLine("Tileset: " + kv.Key); - var terrainInfo = modData.DefaultTerrainInfo[kv.Key]; - + Console.WriteLine("Tileset: " + tileset); if (terrainInfo is ITemplatedTerrainInfo templatedTerrainInfo) - foreach (var r in modData.DefaultRules.Actors[SystemActors.World].TraitInfos()) - failed |= r.ValidateTileSprites(templatedTerrainInfo, Console.WriteLine); + foreach (var ttr in modData.DefaultRules.Actors[SystemActors.World].TraitInfos()) + failed |= ttr.ValidateTileSprites(templatedTerrainInfo, Console.WriteLine); - foreach (var image in kv.Value.Images) + var sequences = new SequenceSet(modData.DefaultFileSystem, modData, tileset, null); + sequences.SpriteCache.LoadReservations(modData); + foreach (var (filename, location) in sequences.SpriteCache.MissingFiles) { - foreach (var sequence in kv.Value.Sequences(image)) - { - if (!(kv.Value.GetSequence(image, sequence) is FileNotFoundSequence s)) - continue; - - Console.WriteLine("\tSequence `{0}.{1}` references sprite `{2}` that does not exist.", image, sequence, s.Filename); - failed = true; - } + Console.WriteLine($"\t{location}: {filename} not found"); + failed = true; } } catch (YamlException e) diff --git a/OpenRA.Mods.Common/UtilityCommands/CheckYaml.cs b/OpenRA.Mods.Common/UtilityCommands/CheckYaml.cs index 0046adad16..1bc54cb031 100644 --- a/OpenRA.Mods.Common/UtilityCommands/CheckYaml.cs +++ b/OpenRA.Mods.Common/UtilityCommands/CheckYaml.cs @@ -67,7 +67,7 @@ namespace OpenRA.Mods.Common.UtilityCommands { Console.WriteLine($"Testing default sequences for {tileset}"); - var sequences = new SequenceProvider(modData.DefaultFileSystem, modData, tileset, null); + var sequences = new SequenceSet(modData.DefaultFileSystem, modData, tileset, null); CheckSequences(modData, modData.DefaultRules, sequences); } @@ -165,7 +165,7 @@ namespace OpenRA.Mods.Common.UtilityCommands } } - void CheckSequences(ModData modData, Ruleset rules, SequenceProvider sequences) + void CheckSequences(ModData modData, Ruleset rules, SequenceSet sequences) { foreach (var customSequencesPassType in modData.ObjectCreator.GetTypesImplementing()) { diff --git a/OpenRA.Mods.Common/UtilityCommands/DumpSequenceSheetsCommand.cs b/OpenRA.Mods.Common/UtilityCommands/DumpSequenceSheetsCommand.cs index b714fece02..47a9e7c3ba 100644 --- a/OpenRA.Mods.Common/UtilityCommands/DumpSequenceSheetsCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/DumpSequenceSheetsCommand.cs @@ -34,8 +34,10 @@ namespace OpenRA.Mods.Common.UtilityCommands var palette = new ImmutablePalette(args[1], new[] { 0 }, Array.Empty()); - SequenceProvider sequences; - if (!modData.DefaultSequences.TryGetValue(args[2], out sequences)) + SequenceSet sequences; + if (modData.DefaultTerrainInfo.ContainsKey(args[2])) + sequences = new SequenceSet(modData.ModFiles, modData, args[2], null); + else { var mapPackage = new Folder(Platform.EngineDir).OpenPackage(args[2], modData.ModFiles); if (mapPackage == null) @@ -44,7 +46,7 @@ namespace OpenRA.Mods.Common.UtilityCommands sequences = new Map(modData, mapPackage).Sequences; } - sequences.Preload(); + sequences.LoadSprites(); var count = 0; diff --git a/OpenRA.Mods.Common/UtilityCommands/ExtractSpriteSequenceDocsCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ExtractSpriteSequenceDocsCommand.cs index 6b7ba62de7..fb6e4f8812 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ExtractSpriteSequenceDocsCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ExtractSpriteSequenceDocsCommand.cs @@ -51,8 +51,7 @@ namespace OpenRA.Mods.Common.UtilityCommands var relatedEnumTypes = new HashSet(); var sequenceTypesInfo = sequenceTypes - .Where(x => !x.ContainsGenericParameters && !x.IsAbstract - && x.Name != nameof(FileNotFoundSequence)) // NOTE: This is the simplest way to exclude FileNotFoundSequence, which shouldn't be added. + .Where(x => !x.ContainsGenericParameters && !x.IsAbstract) .Select(type => new { type.Namespace, diff --git a/OpenRA.Mods.Common/UtilityCommands/LintInterfaces.cs b/OpenRA.Mods.Common/UtilityCommands/LintInterfaces.cs index d5227f4430..b0a0f4290e 100644 --- a/OpenRA.Mods.Common/UtilityCommands/LintInterfaces.cs +++ b/OpenRA.Mods.Common/UtilityCommands/LintInterfaces.cs @@ -17,5 +17,5 @@ namespace OpenRA.Mods.Common.Lint public interface ILintPass { void Run(Action emitError, Action emitWarning, ModData modData); } public interface ILintMapPass { void Run(Action emitError, Action emitWarning, ModData modData, Map map); } public interface ILintRulesPass { void Run(Action emitError, Action emitWarning, ModData modData, Ruleset rules); } - public interface ILintSequencesPass { void Run(Action emitError, Action emitWarning, ModData modData, Ruleset rules, SequenceProvider sequences); } + public interface ILintSequencesPass { void Run(Action emitError, Action emitWarning, ModData modData, Ruleset rules, SequenceSet sequences); } }