From 04c3cd6ec532c5170fac88c6668dda2b1fa3418f Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sat, 24 Dec 2022 13:23:26 +1300 Subject: [PATCH] Reimplement sequence Defaults parsing: The previous MiniYaml.Merge implementation interacted poorly with yaml inheritance, making it complicated (or impossible) to override certain keys from Defaults. The new implementation is simpler: If a key is defined it will be used. If it isn't, the default (if defined) will be used. Defaults can be masked by making sure the same key is defined (even with an empty value) in the sequence. This also fixes naming within the sequence code to distinguish between images (a group of sequences), sequences (defining a specific sprite/animation), and filenames for a specific sprite/animation. --- .../Graphics/ClassicSpriteSequence.cs | 14 +- .../ClassicTilesetSpecificSpriteSequence.cs | 48 +- .../Graphics/DefaultSpriteSequence.cs | 469 +++++++++--------- .../Graphics/TilesetSpecificSpriteSequence.cs | 47 +- 4 files changed, 279 insertions(+), 299 deletions(-) diff --git a/OpenRA.Mods.Cnc/Graphics/ClassicSpriteSequence.cs b/OpenRA.Mods.Cnc/Graphics/ClassicSpriteSequence.cs index e05296fc6c..0d7b8ef124 100644 --- a/OpenRA.Mods.Cnc/Graphics/ClassicSpriteSequence.cs +++ b/OpenRA.Mods.Cnc/Graphics/ClassicSpriteSequence.cs @@ -20,9 +20,9 @@ namespace OpenRA.Mods.Cnc.Graphics public ClassicSpriteSequenceLoader(ModData modData) : base(modData) { } - public override ISpriteSequence CreateSequence(ModData modData, string tileSet, SpriteCache cache, string sequence, string animation, MiniYaml info) + 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, sequence, animation, info); + return new ClassicSpriteSequence(modData, tileset, cache, this, image, sequence, data, defaults); } } @@ -33,15 +33,13 @@ 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 sequence, string animation, MiniYaml info) - : base(modData, tileSet, cache, loader, sequence, animation, info) + 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) { - var d = info.ToDictionary(); - useClassicFacings = LoadField(d, UseClassicFacings); + useClassicFacings = LoadField(UseClassicFacings, data, defaults); if (useClassicFacings && facings != 32) - throw new InvalidOperationException( - $"{info.Nodes[0].Location}: Sequence {sequence}.{animation}: UseClassicFacings is only valid for 32 facings"); + throw new InvalidOperationException($"Sequence {image}.{sequence}: UseClassicFacings is only valid for 32 facings"); } protected override int GetFacingFrameOffset(WAngle facing) diff --git a/OpenRA.Mods.Cnc/Graphics/ClassicTilesetSpecificSpriteSequence.cs b/OpenRA.Mods.Cnc/Graphics/ClassicTilesetSpecificSpriteSequence.cs index 45f164e175..7428f6443b 100644 --- a/OpenRA.Mods.Cnc/Graphics/ClassicTilesetSpecificSpriteSequence.cs +++ b/OpenRA.Mods.Cnc/Graphics/ClassicTilesetSpecificSpriteSequence.cs @@ -36,9 +36,9 @@ namespace OpenRA.Mods.Cnc.Graphics TilesetCodes = yaml.ToDictionary(kv => kv.Value); } - public override ISpriteSequence CreateSequence(ModData modData, string tileSet, SpriteCache cache, string sequence, string animation, MiniYaml info) + 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, sequence, animation, info); + return new ClassicTilesetSpecificSpriteSequence(modData, tileset, cache, this, image, sequence, data, defaults); } } @@ -61,42 +61,34 @@ namespace OpenRA.Mods.Cnc.Graphics [Desc("Whether `mod.yaml`'s `TilesetExtensions` should be used with the sequence's file name.")] static readonly SpriteSequenceField UseTilesetExtension = new SpriteSequenceField(nameof(UseTilesetExtension), false); - public ClassicTilesetSpecificSpriteSequence(ModData modData, string tileSet, SpriteCache cache, ISpriteSequenceLoader loader, string sequence, string animation, MiniYaml info) - : base(modData, tileSet, cache, loader, sequence, animation, info) { } + 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) { } - static string ResolveTilesetId(string tileSet, Dictionary d) + static string ResolveTilesetId(string tileset, MiniYaml data, MiniYaml defaults) { - if (d.TryGetValue(nameof(TilesetOverrides), out var yaml)) - { - var tsNode = yaml.Nodes.FirstOrDefault(n => n.Key == tileSet); - if (tsNode != null) - tileSet = tsNode.Value.Value; - } - - return tileSet; + var node = data.Nodes.FirstOrDefault(n => n.Key == TilesetOverrides.Key) ?? defaults.Nodes.FirstOrDefault(n => n.Key == TilesetOverrides.Key); + var overrideNode = node?.Value.Nodes.FirstOrDefault(n => n.Key == tileset); + return overrideNode?.Value.Value ?? tileset; } - protected override string GetSpriteSrc(ModData modData, string tileSet, string sequence, string animation, string sprite, Dictionary d) + protected override string GetSpriteFilename(ModData modData, string tileset, string image, string sequence, MiniYaml data, MiniYaml defaults) { var loader = (ClassicTilesetSpecificSpriteSequenceLoader)Loader; + var filename = data.Value ?? defaults.Value ?? image; + if (LoadField(UseTilesetCode, data, defaults)) + if (loader.TilesetCodes.TryGetValue(ResolveTilesetId(tileset, data, defaults), out var tilesetCode)) + filename = filename.Substring(0, 1) + tilesetCode + filename.Substring(2, filename.Length - 2); - var spriteName = sprite ?? sequence; - - if (LoadField(d, UseTilesetCode)) + if (LoadField(AddExtension, data, defaults)) { - if (loader.TilesetCodes.TryGetValue(ResolveTilesetId(tileSet, d), out var code)) - spriteName = spriteName.Substring(0, 1) + code + spriteName.Substring(2, spriteName.Length - 2); + if (LoadField(UseTilesetExtension, data, defaults)) + if (loader.TilesetExtensions.TryGetValue(ResolveTilesetId(tileset, data, defaults), out var tilesetExtension)) + return filename + tilesetExtension; + + return filename + loader.DefaultSpriteExtension; } - if (LoadField(d, AddExtension)) - { - if (LoadField(d, UseTilesetExtension) && loader.TilesetExtensions.TryGetValue(ResolveTilesetId(tileSet, d), out var tilesetExtension)) - return spriteName + tilesetExtension; - - return spriteName + loader.DefaultSpriteExtension; - } - - return spriteName; + return filename; } } } diff --git a/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs b/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs index 601574c27e..062e1710ae 100644 --- a/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs +++ b/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs @@ -41,49 +41,49 @@ namespace OpenRA.Mods.Common.Graphics public class DefaultSpriteSequenceLoader : ISpriteSequenceLoader { + static readonly MiniYaml NoData = new MiniYaml(null); + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "IDE0060:Remove unused parameter", Justification = "Load game API")] public DefaultSpriteSequenceLoader(ModData modData) { } - public virtual ISpriteSequence CreateSequence(ModData modData, string tileSet, SpriteCache cache, string sequence, string animation, MiniYaml info) + 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, sequence, animation, info); + return new DefaultSpriteSequence(modData, tileset, cache, this, image, sequence, data, defaults); } - public IReadOnlyDictionary ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode node) + public IReadOnlyDictionary ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode imageNode) { var sequences = new Dictionary(); - var nodes = node.Value.ToDictionary(); - + MiniYaml defaults; try { - if (nodes.TryGetValue("Defaults", out var defaults)) - { - nodes.Remove("Defaults"); - foreach (var n in nodes) - { - n.Value.Nodes = MiniYaml.Merge(new[] { defaults.Nodes, n.Value.Nodes }); - n.Value.Value = n.Value.Value ?? defaults.Value; - } - } + 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 {node.Key}", e); + throw new InvalidDataException($"Error occurred while parsing {imageNode.Key}", e); } - foreach (var kvp in nodes) + foreach (var sequenceNode in imageNode.Value.Nodes) { - using (new Support.PerfTimer($"new Sequence(\"{node.Key}\")", 20)) + using (new Support.PerfTimer($"new Sequence(\"{imageNode.Key}\")", 20)) { try { - sequences.Add(kvp.Key, CreateSequence(modData, tileSet, cache, node.Key, kvp.Key, kvp.Value)); + var sequence = CreateSequence(modData, tileSet, cache, imageNode.Key, sequenceNode.Key, sequenceNode.Value, defaults); + sequences.Add(sequenceNode.Key, sequence); } catch (FileNotFoundException ex) { // 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(kvp.Key, new FileNotFoundSequence(ex)); + 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}"); } } } @@ -139,9 +139,11 @@ 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); + + readonly string image; protected Sprite[] sprites; readonly bool reverseFacings, transpose; - readonly string sequence; protected readonly ISpriteSequenceLoader Loader; @@ -257,25 +259,27 @@ namespace OpenRA.Mods.Common.Graphics public readonly uint[] EmbeddedPalette; - protected virtual string GetSpriteSrc(ModData modData, string tileSet, string sequence, string animation, string sprite, Dictionary d) + protected virtual string GetSpriteFilename(ModData modData, string tileset, string image, string sequence, MiniYaml data, MiniYaml defaults) { - return sprite ?? sequence; + return data.Value ?? defaults.Value ?? image; } - protected static T LoadField(Dictionary d, string key, T fallback) + protected static T LoadField(string key, T fallback, MiniYaml data, MiniYaml defaults) { - if (d.TryGetValue(key, out var value)) - return FieldLoader.GetValue(key, value.Value); + var node = data.Nodes.FirstOrDefault(n => n.Key == key) ?? defaults.Nodes.FirstOrDefault(n => n.Key == key); + if (node == null) + return fallback; - return fallback; + return FieldLoader.GetValue(key, node.Value.Value); } - protected static T LoadField(Dictionary d, SpriteSequenceField field) + protected static T LoadField(SpriteSequenceField field, MiniYaml data, MiniYaml defaults) { - if (d.TryGetValue(field.Key, out var value)) - return FieldLoader.GetValue(field.Key, value.Value); + 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 field.DefaultValue; + return FieldLoader.GetValue(field.Key, node.Value.Value); } protected static Rectangle FlipRectangle(Rectangle rect, bool flipX, bool flipY) @@ -288,230 +292,223 @@ namespace OpenRA.Mods.Common.Graphics return Rectangle.FromLTRB(left, top, right, bottom); } - public DefaultSpriteSequence(ModData modData, string tileSet, SpriteCache cache, ISpriteSequenceLoader loader, string sequence, string animation, MiniYaml info) + public DefaultSpriteSequence(ModData modData, string tileSet, SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults) { - this.sequence = sequence; - Name = animation; + this.image = image; + Name = sequence; Loader = loader; - var d = info.ToDictionary(); - try + start = LoadField(Start, data, defaults); + 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); + + facings = LoadField(Facings, data, defaults); + interpolatedFacings = LoadField(InterpolatedFacings.Key, -1, data, defaults); + if (interpolatedFacings != -1 && (interpolatedFacings <= 1 || interpolatedFacings <= Math.Abs(facings) || interpolatedFacings > 1024 + || !Exts.IsPowerOf2(interpolatedFacings))) + throw new YamlException($"InterpolatedFacings must be greater than Facings, within the range of 2 to 1024, and a power of 2."); + + if (facings < 0) { - start = LoadField(d, Start); - shadowStart = LoadField(d, ShadowStart); - shadowZOffset = LoadField(d, ShadowZOffset).Length; - zOffset = LoadField(d, ZOffset).Length; - tick = LoadField(d, Tick); - transpose = LoadField(d, Transpose); - frames = LoadField(d, Frames); - ignoreWorldTint = LoadField(d, IgnoreWorldTint); - scale = LoadField(d, Scale); + reverseFacings = true; + facings = -facings; + } - var flipX = LoadField(d, FlipX); - var flipY = LoadField(d, FlipY); - var zRamp = LoadField(d, ZRamp); + var offset = LoadField(Offset, data, defaults); + var blendMode = LoadField(BlendMode, data, defaults); - facings = LoadField(d, Facings); - interpolatedFacings = LoadField(d, nameof(InterpolatedFacings), -1); - if (interpolatedFacings != -1 && (interpolatedFacings <= 1 || interpolatedFacings <= Math.Abs(facings) || interpolatedFacings > 1024 - || !Exts.IsPowerOf2(interpolatedFacings))) - throw new YamlException($"InterpolatedFacings must be greater than Facings, within the range of 2 to 1024, and a power of 2."); - - if (facings < 0) - { - reverseFacings = true; - facings = -facings; - } - - var offset = LoadField(d, Offset); - var blendMode = LoadField(d, BlendMode); - - Func> getUsedFrames = frameCount => - { - if (d.TryGetValue(Length.Key, out var lengthYaml) && lengthYaml.Value == "*") - length = frames?.Length ?? frameCount - start; - else - length = LoadField(d, Length); - - // Plays the animation forwards, and then in reverse - if (LoadField(d, Reverses)) - { - 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(d, Stride.Key, length); - - if (length > stride) - throw new YamlException($"Sequence {sequence}.{animation}: Length must be <= stride"); - - if (frames != null && length > frames.Length) - throw new YamlException($"Sequence {sequence}.{animation}: 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 {sequence}.{animation} defines a Frames override that references frame {f}, but only [{start}..{end}] actually exist"); - - if (start < 0 || end >= frames.Length) - throw new YamlException($"Sequence {sequence}.{animation} 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 {sequence}.{animation} uses frames [{start}..{end}], but only [0..{frameCount - 1}] actually exist"); - - if (shadowStart >= 0 && shadowStart + (facings - 1) * stride + length > frameCount) - throw new YamlException($"Sequence {sequence}.{animation}'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; - }; - - if (d.TryGetValue(Combine.Key, out var combine)) - { - var combined = Enumerable.Empty(); - foreach (var sub in combine.Nodes) - { - var sd = sub.Value.ToDictionary(); - - // Allow per-sprite offset, flipping, start, and length - // These shouldn't inherit Start/Offset/etc from the main definition - var subStart = LoadField(sd, Start); - var subOffset = LoadField(sd, Offset); - var subFlipX = LoadField(sd, FlipX); - var subFlipY = LoadField(sd, FlipY); - var subFrames = LoadField(sd, Frames); - var subLength = 0; - - Func> subGetUsedFrames = subFrameCount => - { - if (sd.TryGetValue(Length.Key, out var subLengthYaml) && subLengthYaml.Value == "*") - subLength = subFrames != null ? subFrames.Length : subFrameCount - subStart; - else - subLength = LoadField(sd, Length); - - return subFrames != null ? subFrames.Skip(subStart).Take(subLength) : Enumerable.Range(subStart, subLength); - }; - - var subSrc = GetSpriteSrc(modData, tileSet, sequence, animation, sub.Key, sd); - var subSprites = cache[subSrc, 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); - } + Func> getUsedFrames = 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)) { - // Apply offset to each sprite in the sequence - // Different sequences may apply different offsets to the same frame - var src = GetSpriteSrc(modData, tileSet, sequence, animation, info.Value, d); - sprites = cache[src, getUsedFrames].Select(s => + 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) + { + var combineData = combineSequenceNode.Value; + + // 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; + + Func> subGetUsedFrames = subFrameCount => + { + 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); + }; + + var subFilename = GetSpriteFilename(modData, tileSet, combineSequenceNode.Key, sequence, combineData, NoData); + var subSprites = cache[subFilename, subGetUsedFrames].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 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); - }).ToArray(); + }).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])); } - alpha = LoadField(d, Alpha); - if (alpha != null) - { - if (alpha.Length == 1) - alpha = Exts.MakeArray(length, _ => alpha[0]); - else if (alpha.Length != length) - throw new YamlException($"Sequence {sequence}.{animation} must define either 1 or {length} Alpha values."); - } - - if (LoadField(d, AlphaFade)) - { - if (alpha != null) - throw new YamlException($"Sequence {sequence}.{animation} cannot define both AlphaFade and Alpha."); - - alpha = Exts.MakeArray(length, i => float2.Lerp(1f, 0f, i / (length - 1f))); - } - - var depthSprite = LoadField(d, DepthSprite); - if (!string.IsNullOrEmpty(depthSprite)) - { - var depthSpriteFrame = LoadField(d, DepthSpriteFrame); - var depthOffset = LoadField(d, DepthSpriteOffset); - 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 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(); - } - - if (LoadField(d, HasEmbeddedPalette)) - { - var src = GetSpriteSrc(modData, tileSet, sequence, animation, info.Value, d); - - var metadata = cache.FrameMetadata(src); - var i = frames != null ? frames[0] : start; - var palettes = metadata?.GetOrDefault(); - if (palettes == null || !palettes.TryGetPaletteForFrame(i, out EmbeddedPalette)) - throw new YamlException($"Cannot export palette from {src}: frame {i} does not define an embedded palette"); - } - - var boundSprites = SpriteBounds(sprites, frames, start, facings, length, stride, transpose); - if (shadowStart > 0) - boundSprites = boundSprites.Concat(SpriteBounds(sprites, frames, shadowStart, facings, length, stride, transpose)); - - Bounds = boundSprites.Union(); + sprites = combined.ToArray(); + getUsedFrames(sprites.Length); } - catch (FormatException f) + else { - throw new FormatException($"Failed to parse sequences for {sequence}.{animation} at {info.Nodes[0].Location}:\n{f}"); + // 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); + sprites = cache[filename, getUsedFrames].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; + + 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 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(); + } + + if (LoadField(HasEmbeddedPalette, data, defaults)) + { + var filename = GetSpriteFilename(modData, tileSet, image, sequence, data, defaults); + var metadata = cache.FrameMetadata(filename); + var i = frames != null ? frames[0] : start; + var palettes = metadata?.GetOrDefault(); + if (palettes == null || !palettes.TryGetPaletteForFrame(i, out EmbeddedPalette)) + throw new YamlException($"Cannot export palette from {filename}: frame {i} does not define an embedded palette"); + } + + var boundSprites = SpriteBounds(sprites, frames, start, facings, length, stride, transpose); + if (shadowStart > 0) + boundSprites = boundSprites.Concat(SpriteBounds(sprites, frames, shadowStart, facings, length, stride, transpose)); + + Bounds = boundSprites.Union(); } /// Returns the bounds of all of the sprites that can appear in this animation @@ -570,7 +567,7 @@ namespace OpenRA.Mods.Common.Graphics var j = frames != null ? frames[i] : start + i; if (sprites[j] == null) - throw new InvalidOperationException($"Attempted to query unloaded sprite from {Name}.{sequence} start={start} frame={frame} facing={facing}"); + throw new InvalidOperationException($"Attempted to query unloaded sprite from {image}.{Name} start={start} frame={frame} facing={facing}"); return sprites[j]; } diff --git a/OpenRA.Mods.Common/Graphics/TilesetSpecificSpriteSequence.cs b/OpenRA.Mods.Common/Graphics/TilesetSpecificSpriteSequence.cs index 211eaf6cb3..478aebe01f 100644 --- a/OpenRA.Mods.Common/Graphics/TilesetSpecificSpriteSequence.cs +++ b/OpenRA.Mods.Common/Graphics/TilesetSpecificSpriteSequence.cs @@ -35,9 +35,9 @@ namespace OpenRA.Mods.Common.Graphics TilesetCodes = yaml.ToDictionary(kv => kv.Value); } - public override ISpriteSequence CreateSequence(ModData modData, string tileSet, SpriteCache cache, string sequence, string animation, MiniYaml info) + 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, sequence, animation, info); + return new TilesetSpecificSpriteSequence(modData, tileSet, cache, this, image, sequence, data, defaults); } } @@ -59,42 +59,35 @@ namespace OpenRA.Mods.Common.Graphics [Desc("Whether `mod.yaml`'s `TilesetExtensions` should be used with the sequence's file name.")] static readonly SpriteSequenceField UseTilesetExtension = new SpriteSequenceField(nameof(UseTilesetExtension), false); - public TilesetSpecificSpriteSequence(ModData modData, string tileSet, SpriteCache cache, ISpriteSequenceLoader loader, string sequence, string animation, MiniYaml info) - : base(modData, tileSet, cache, loader, sequence, animation, info) { } + 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) { } - static string ResolveTilesetId(string tileSet, Dictionary d) + static string ResolveTilesetId(string tileset, MiniYaml data, MiniYaml defaults) { - if (d.TryGetValue(nameof(TilesetOverrides), out var yaml)) - { - var tsNode = yaml.Nodes.FirstOrDefault(n => n.Key == tileSet); - if (tsNode != null) - tileSet = tsNode.Value.Value; - } - - return tileSet; + var node = data.Nodes.FirstOrDefault(n => n.Key == TilesetOverrides.Key) ?? defaults.Nodes.FirstOrDefault(n => n.Key == TilesetOverrides.Key); + var overrideNode = node?.Value.Nodes.FirstOrDefault(n => n.Key == tileset); + return overrideNode?.Value.Value ?? tileset; } - protected override string GetSpriteSrc(ModData modData, string tileSet, string sequence, string animation, string sprite, Dictionary d) + protected override string GetSpriteFilename(ModData modData, string tileset, string image, string sequence, MiniYaml data, MiniYaml defaults) { var loader = (TilesetSpecificSpriteSequenceLoader)Loader; + var filename = data.Value ?? defaults.Value ?? image; - var spriteName = sprite ?? sequence; + if (LoadField(UseTilesetCode, data, defaults)) + if (loader.TilesetCodes.TryGetValue(ResolveTilesetId(tileset, data, defaults), out var tilesetCode)) + filename = filename.Substring(0, 1) + tilesetCode + filename.Substring(2, filename.Length - 2); - if (LoadField(d, UseTilesetCode)) + if (LoadField(AddExtension, data, defaults)) { - if (loader.TilesetCodes.TryGetValue(ResolveTilesetId(tileSet, d), out var code)) - spriteName = spriteName.Substring(0, 1) + code + spriteName.Substring(2, spriteName.Length - 2); + if (LoadField(UseTilesetExtension, data, defaults)) + if (loader.TilesetExtensions.TryGetValue(ResolveTilesetId(tileset, data, defaults), out var tilesetExtension)) + return filename + tilesetExtension; + + return filename + loader.DefaultSpriteExtension; } - if (LoadField(d, AddExtension)) - { - if (LoadField(d, UseTilesetExtension) && loader.TilesetExtensions.TryGetValue(ResolveTilesetId(tileSet, d), out var tilesetExtension)) - return spriteName + tilesetExtension; - - return spriteName + loader.DefaultSpriteExtension; - } - - return spriteName; + return filename; } } }