diff --git a/OpenRA.Mods.Cnc/Graphics/ClassicSpriteSequence.cs b/OpenRA.Mods.Cnc/Graphics/ClassicSpriteSequence.cs index 900b9d3b148d..b230bac7b661 100644 --- a/OpenRA.Mods.Cnc/Graphics/ClassicSpriteSequence.cs +++ b/OpenRA.Mods.Cnc/Graphics/ClassicSpriteSequence.cs @@ -40,14 +40,14 @@ public ClassicSpriteSequence(ModData modData, string tileSet, SpriteCache cache, var d = info.ToDictionary(); UseClassicFacings = LoadField(d, nameof(UseClassicFacings), UseClassicFacings); - if (UseClassicFacings && Facings != 32) + if (UseClassicFacings && facings != 32) throw new InvalidOperationException( $"{info.Nodes[0].Location}: Sequence {sequence}.{animation}: UseClassicFacings is only valid for 32 facings"); } protected override int GetFacingFrameOffset(WAngle facing) { - return UseClassicFacings ? Util.ClassicIndexFacing(facing, Facings) : Common.Util.IndexFacing(facing, Facings); + return UseClassicFacings ? Util.ClassicIndexFacing(facing, facings) : Common.Util.IndexFacing(facing, facings); } } } diff --git a/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs b/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs index 959e96546e0b..d1448afd4d71 100644 --- a/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs +++ b/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs @@ -122,6 +122,18 @@ public FileNotFoundSequence(FileNotFoundException exception) float ISpriteSequence.GetAlpha(int frame) { throw exception; } } + public struct SpriteSequenceField + { + public string Key; + public T DefaultValue; + + public SpriteSequenceField(string key, T defaultValue) + { + Key = key; + DefaultValue = defaultValue; + } + } + [Desc("Generic sprite sequence implementation, mostly unencumbered with game- or artwork-specific logic.")] public class DefaultSpriteSequence : ISpriteSequence { @@ -135,19 +147,29 @@ public class DefaultSpriteSequence : ISpriteSequence public string Name { get; } [Desc("Frame index to start from.")] - public int Start { get; private set; } + static readonly SpriteSequenceField Start = new SpriteSequenceField("Start", 0); + int ISpriteSequence.Start => start; + int start; [Desc("Number of frames to use. Does not have to be the total amount the sprite sheet has.")] - public int Length { get; private set; } = 1; + static readonly SpriteSequenceField Length = new SpriteSequenceField("Length", 1); + int ISpriteSequence.Length => length; + int length = 1; - [Desc("Multiplier for the number of facings.")] - public int Stride { get; private set; } + [Desc("Overrides Length if a different number of frames is defined between facings.")] + static readonly SpriteSequenceField Stride = new SpriteSequenceField("Stride", "(Value of Length)"); + int ISpriteSequence.Stride => stride; + int stride; [Desc("The amount of directions the unit faces. Use negative values to rotate counter-clockwise.")] - public int Facings { get; } = 1; + static readonly SpriteSequenceField Facings = new SpriteSequenceField("Facings", 1); + int ISpriteSequence.Facings => facings; + protected int facings; [Desc("Time (in milliseconds) to wait until playing the next frame in the animation.")] - public int Tick { get; } = 40; + static readonly SpriteSequenceField Tick = new SpriteSequenceField("Tick", 40); + int ISpriteSequence.Tick => tick; + int tick; [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.")] @@ -172,42 +194,44 @@ public class DefaultSpriteSequence : ISpriteSequence public bool IgnoreWorldTint { get; } [Desc("")] - public float Scale { get; } = 1f; + static readonly SpriteSequenceField Scale = new SpriteSequenceField("Scale", 1); + float ISpriteSequence.Scale => scale; + float scale; // These need to be public properties for the documentation generation to work. [Desc("Play the sprite sequence back and forth.")] - public static bool Reverses => false; + static readonly SpriteSequenceField Reverses = new SpriteSequenceField("Reverses", false); [Desc("Support a frame order where each animation step is split per each direction.")] - public static bool Transpose => false; + static readonly SpriteSequenceField Transpose = new SpriteSequenceField("Transpose", false); [Desc("Mirror on the X axis.")] - public bool FlipX { get; } + static readonly SpriteSequenceField FlipX = new SpriteSequenceField("FlipX", false); [Desc("Mirror on the Y axis.")] - public bool FlipY { get; } + static readonly SpriteSequenceField FlipY = new SpriteSequenceField("FlipY", false); [Desc("Change the position in-game on X, Y, Z.")] - public float3 Offset { get; } = float3.Zero; + static readonly SpriteSequenceField Offset = new SpriteSequenceField("Offset", float3.Zero); [Desc("Apply an OpenGL/Photoshop inspired blend mode.")] - public BlendMode BlendMode { get; } = BlendMode.Alpha; + static readonly SpriteSequenceField BlendMode = new SpriteSequenceField("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.")] - public static object Combine => null; + static readonly SpriteSequenceField Combine = new SpriteSequenceField("Combine", null); [Desc("Sets transparency - use one value to set for all frames or provide a value for each frame.")] public float[] Alpha { get; } [Desc("Plays a fade out effect.")] - public static bool AlphaFade => false; + static readonly SpriteSequenceField AlphaFade = new SpriteSequenceField("AlphaFade", false); [Desc("Name of the file containing the depth data sprite.")] public string DepthSprite { get; } [Desc("Frame index containing the depth data.")] - public static int DepthSpriteFrame => 0; + static readonly SpriteSequenceField DepthSpriteFrame = new SpriteSequenceField("DepthSpriteFrame", 0); [Desc("")] public static float2 DepthSpriteOffset => float2.Zero; @@ -230,6 +254,14 @@ protected static T LoadField(Dictionary d, string key, T fa return fallback; } + protected static T LoadField(Dictionary d, SpriteSequenceField field) + { + if (d.TryGetValue(field.Key, out var value)) + return FieldLoader.GetValue(field.Key, value.Value); + + return field.DefaultValue; + } + protected static Rectangle FlipRectangle(Rectangle rect, bool flipX, bool flipY) { var left = flipX ? rect.Right : rect.Left; @@ -249,90 +281,90 @@ public DefaultSpriteSequence(ModData modData, string tileSet, SpriteCache cache, try { - Start = LoadField(d, nameof(Start), Start); + start = LoadField(d, Start); ShadowStart = LoadField(d, nameof(ShadowStart), ShadowStart); ShadowZOffset = LoadField(d, nameof(ShadowZOffset), DefaultShadowSpriteZOffset).Length; ZOffset = LoadField(d, nameof(ZOffset), new WDist(ZOffset)).Length; ZRamp = LoadField(d, nameof(ZRamp), ZRamp); - Tick = LoadField(d, nameof(Tick), Tick); - transpose = LoadField(d, nameof(Transpose), Transpose); + tick = LoadField(d, Tick); + transpose = LoadField(d, Transpose); Frames = LoadField(d, nameof(Frames), Frames); IgnoreWorldTint = LoadField(d, nameof(IgnoreWorldTint), IgnoreWorldTint); - Scale = LoadField(d, nameof(Scale), Scale); + scale = LoadField(d, Scale); - FlipX = LoadField(d, nameof(FlipX), FlipX); - FlipY = LoadField(d, nameof(FlipY), FlipY); + var flipX = LoadField(d, FlipX); + var flipY = LoadField(d, FlipY); - Facings = LoadField(d, nameof(Facings), Facings); - if (Facings < 0) + facings = LoadField(d, Facings); + if (facings < 0) { reverseFacings = true; - Facings = -Facings; + facings = -facings; } - Offset = LoadField(d, nameof(Offset), Offset); - BlendMode = LoadField(d, nameof(BlendMode), BlendMode); + var offset = LoadField(d, Offset); + var blendMode = LoadField(d, BlendMode); Func> getUsedFrames = frameCount => { - if (d.TryGetValue(nameof(Length), out var length) && length.Value == "*") - Length = Frames?.Length ?? frameCount - Start; + if (d.TryGetValue(Length.Key, out var lengthYaml) && lengthYaml.Value == "*") + length = Frames?.Length ?? frameCount - start; else - Length = LoadField(d, nameof(Length), Length); + length = LoadField(d, Length); // Plays the animation forwards, and then in reverse - if (LoadField(d, nameof(Reverses), Reverses)) + if (LoadField(d, Reverses)) { - var frames = Frames != null ? Frames.Skip(Start).Take(Length).ToArray() : Exts.MakeArray(Length, i => Start + i); - Frames = frames.Concat(frames.Skip(1).Take(Length - 2).Reverse()).ToArray(); - Length = 2 * Length - 2; - Start = 0; + var frames = Frames != null ? Frames.Skip(start).Take(length).ToArray() : Exts.MakeArray(length, i => start + i); + Frames = frames.Concat(frames.Skip(1).Take(length - 2).Reverse()).ToArray(); + length = 2 * length - 2; + start = 0; } - // Note: Defaulting to Length is intentional - Stride = LoadField(d, nameof(Stride), Length); + // Overrides Length with a custom stride + stride = LoadField(d, Stride.Key, length); - if (Length > Stride) + if (length > stride) throw new YamlException($"Sequence {sequence}.{animation}: Length must be <= stride"); - if (Frames != null && Length > Frames.Length) + 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; + 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"); + 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"); + 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"); + 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"); + 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 facing = 0; facing < facings; facing++) { - for (var frame = 0; frame < Length; frame++) + for (var frame = 0; frame < length; frame++) { - var i = transpose ? (frame % Length) * Facings + facing : - (facing * Stride) + (frame % Length); + var i = transpose ? (frame % length) * facings + facing : + (facing * stride) + (frame % length); - usedFrames.Add(Frames != null ? Frames[i] : Start + i); + usedFrames.Add(Frames != null ? Frames[i] : start + i); } } if (ShadowStart >= 0) - return usedFrames.Concat(usedFrames.Select(i => i + ShadowStart - Start)); + return usedFrames.Concat(usedFrames.Select(i => i + ShadowStart - start)); return usedFrames; }; - if (d.TryGetValue(nameof(Combine), out var combine)) + if (d.TryGetValue(Combine.Key, out var combine)) { var combined = Enumerable.Empty(); foreach (var sub in combine.Nodes) @@ -341,19 +373,19 @@ public DefaultSpriteSequence(ModData modData, string tileSet, SpriteCache cache, // Allow per-sprite offset, flipping, start, and length // These shouldn't inherit Start/Offset/etc from the main definition - var subStart = LoadField(sd, nameof(Start), 0); - var subOffset = LoadField(sd, nameof(Offset), float3.Zero); - var subFlipX = LoadField(sd, nameof(FlipX), false); - var subFlipY = LoadField(sd, nameof(FlipY), false); + var subStart = LoadField(sd, Start); + var subOffset = LoadField(sd, Offset); + var subFlipX = LoadField(sd, FlipX); + var subFlipY = LoadField(sd, FlipY); var subFrames = LoadField(sd, nameof(Frames), null); var subLength = 0; Func> subGetUsedFrames = subFrameCount => { - if (sd.TryGetValue(nameof(Length), out var subLengthYaml) && subLengthYaml.Value == "*") + if (sd.TryGetValue(Length.Key, out var subLengthYaml) && subLengthYaml.Value == "*") subLength = subFrames != null ? subFrames.Length : subFrameCount - subStart; else - subLength = LoadField(sd, nameof(Length), 1); + subLength = LoadField(sd, Length); return subFrames != null ? subFrames.Skip(subStart).Take(subLength) : Enumerable.Range(subStart, subLength); }; @@ -365,11 +397,11 @@ public DefaultSpriteSequence(ModData modData, string tileSet, SpriteCache cache, 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; + 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); + 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); @@ -389,12 +421,12 @@ public DefaultSpriteSequence(ModData modData, string tileSet, SpriteCache cache, 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, 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); + return new Sprite(s.Sheet, bounds, ZRamp, new float3(dx, dy, dz), s.Channel, blendMode); }).ToArray(); } @@ -402,23 +434,23 @@ public DefaultSpriteSequence(ModData modData, string tileSet, SpriteCache cache, 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."); + 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, nameof(AlphaFade), AlphaFade)) + 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))); + Alpha = Exts.MakeArray(length, i => float2.Lerp(1f, 0f, i / (length - 1f))); } DepthSprite = LoadField(d, nameof(DepthSprite), DepthSprite); if (!string.IsNullOrEmpty(DepthSprite)) { - var depthSpriteFrame = LoadField(d, nameof(DepthSpriteFrame), DepthSpriteFrame); + var depthSpriteFrame = LoadField(d, DepthSpriteFrame); var depthOffset = LoadField(d, nameof(DepthSpriteOffset), DepthSpriteOffset); IEnumerable GetDepthFrame(int _) => new[] { depthSpriteFrame }; var ds = cache[DepthSprite, GetDepthFrame][depthSpriteFrame]; @@ -444,15 +476,15 @@ public DefaultSpriteSequence(ModData modData, string tileSet, SpriteCache cache, var src = GetSpriteSrc(modData, tileSet, sequence, animation, info.Value, d); var metadata = cache.FrameMetadata(src); - var i = Frames != null ? Frames[0] : Start; + 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); + var boundSprites = SpriteBounds(sprites, Frames, start, facings, length, stride, transpose); if (ShadowStart > 0) - boundSprites = boundSprites.Concat(SpriteBounds(sprites, Frames, ShadowStart, Facings, Length, Stride, transpose)); + boundSprites = boundSprites.Concat(SpriteBounds(sprites, Frames, ShadowStart, facings, length, stride, transpose)); Bounds = boundSprites.Union(); } @@ -483,12 +515,12 @@ static IEnumerable SpriteBounds(Sprite[] sprites, int[] frames, int s public Sprite GetSprite(int frame) { - return GetSprite(Start, frame, WAngle.Zero); + return GetSprite(start, frame, WAngle.Zero); } public Sprite GetSprite(int frame, WAngle facing) { - return GetSprite(Start, frame, facing); + return GetSprite(start, frame, facing); } public Sprite GetShadow(int frame, WAngle facing) @@ -500,10 +532,10 @@ protected virtual Sprite GetSprite(int start, int frame, WAngle facing) { var f = GetFacingFrameOffset(facing); if (reverseFacings) - f = (Facings - f) % Facings; + f = (facings - f) % facings; - var i = transpose ? (frame % Length) * Facings + f : - (f * Stride) + (frame % Length); + var i = transpose ? (frame % length) * facings + f : + (f * stride) + (frame % length); var j = Frames != null ? Frames[i] : start + i; if (sprites[j] == null) @@ -514,7 +546,7 @@ protected virtual Sprite GetSprite(int start, int frame, WAngle facing) protected virtual int GetFacingFrameOffset(WAngle facing) { - return Util.IndexFacing(facing, Facings); + return Util.IndexFacing(facing, facings); } public virtual float GetAlpha(int frame) diff --git a/OpenRA.Mods.Common/Util.cs b/OpenRA.Mods.Common/Util.cs index 35525044d3c3..0757e8cf0b04 100644 --- a/OpenRA.Mods.Common/Util.cs +++ b/OpenRA.Mods.Common/Util.cs @@ -209,6 +209,13 @@ public static int RandomInRange(MersenneTwister random, int[] range) return random.Next(range[0], range[1]); } + public static string InternalTypeName(Type t) + { + return t.IsGenericType + ? $"{t.Name.Substring(0, t.Name.IndexOf('`'))}<{string.Join(", ", t.GenericTypeArguments.Select(arg => arg.Name))}>" + : t.Name; + } + public static string FriendlyTypeName(Type t) { if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(HashSet<>)) diff --git a/OpenRA.Mods.Common/UtilityCommands/ExtractSequenceDocsCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ExtractSequenceDocsCommand.cs new file mode 100644 index 000000000000..1d3117c61511 --- /dev/null +++ b/OpenRA.Mods.Common/UtilityCommands/ExtractSequenceDocsCommand.cs @@ -0,0 +1,106 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Primitives; + +namespace OpenRA.Mods.Common.UtilityCommands +{ + class ExtractSequenceDocsCommand : IUtilityCommand + { + string IUtilityCommand.Name => "--sequence-docs"; + + bool IUtilityCommand.ValidateArguments(string[] args) + { + return true; + } + + [Desc("[VERSION]", "Generate sprite sequence documentation in JSON format.")] + void IUtilityCommand.Run(Utility utility, string[] args) + { + // HACK: The engine code assumes that Game.modData is set. + Game.ModData = utility.ModData; + + var version = utility.ModData.Manifest.Metadata.Version; + if (args.Length > 1) + version = args[1]; + + var objectCreator = utility.ModData.ObjectCreator; + var sequenceTypes = objectCreator.GetTypesImplementing().OrderBy(t => t.Namespace); + + var json = GenerateJson(version, sequenceTypes); + Console.WriteLine(json); + } + + static string GenerateJson(string version, IEnumerable sequenceTypes) + { + var result = new + { + Version = version, + SequenceTypes = new List() + }; + + foreach (var type in sequenceTypes) + { + var fields = type.GetCustomAttributes(false); + if (fields.Length == 0) + continue; + + var sequenceInfo = new + { + Namespace = type.Namespace, + Name = type.Name, + Description = string.Join(" ", type.GetCustomAttributes(false).SelectMany(d => d.Lines)), + InheritedTypes = type.BaseTypes() + .Select(y => y.Name) + .Where(y => y != type.Name && y != "Object"), + Properties = new List() + }; + + foreach (var field in type.GetFields(BindingFlags.NonPublic | BindingFlags.Static)) + { + if (!field.FieldType.IsGenericType || field.FieldType.GetGenericTypeDefinition() != typeof(SpriteSequenceField<>)) + continue; + + var description = string.Join(" ", field.GetCustomAttributes(false) + .SelectMany(d => d.Lines)); + + var valueType = field.FieldType.GetGenericArguments()[0]; + + var key = (string)field.FieldType + .GetField(nameof(SpriteSequenceField.Key))? + .GetValue(field.GetValue(null)); + + var defaultValue = field.FieldType + .GetField(nameof(SpriteSequenceField.DefaultValue))? + .GetValue(field.GetValue(null)); + + sequenceInfo.Properties.Add(new { + PropertyName = key, + DefaultValue = defaultValue, + Type = Util.FriendlyTypeName(valueType), + InternalType = Util.InternalTypeName(valueType), + UserFriendlyType = Util.FriendlyTypeName(valueType), + Description = description + }); + } + + result.SequenceTypes.Add(sequenceInfo); + } + + return Newtonsoft.Json.JsonConvert.SerializeObject(result); + } + } +}