Simplify tileset-specific sequence definitions.

All magic behaviour for constructing sprite filenames
has been removed in favour of an explicit Filename
(and TilesetFilenames for tileset-specific sequences)
property.
This commit is contained in:
Paul Chote
2023-01-21 14:41:32 +00:00
committed by Pavel Penev
parent 04c3cd6ec5
commit 5b8f148c50
41 changed files with 8088 additions and 3718 deletions

View File

@@ -151,6 +151,9 @@ namespace OpenRA.Mods.Common.Graphics
public string Name { get; }
[Desc("File name of the sprite to use for this sequence.")]
static readonly SpriteSequenceField<string> Filename = new SpriteSequenceField<string>(nameof(Filename), null);
[Desc("Frame index to start from.")]
static readonly SpriteSequenceField<int> Start = new SpriteSequenceField<int>(nameof(Start), 0);
int ISpriteSequence.Start => start;
@@ -261,7 +264,7 @@ namespace OpenRA.Mods.Common.Graphics
protected virtual string GetSpriteFilename(ModData modData, string tileset, string image, string sequence, MiniYaml data, MiniYaml defaults)
{
return data.Value ?? defaults.Value ?? image;
return LoadField(Filename, data, defaults);
}
protected static T LoadField<T>(string key, T fallback, MiniYaml data, MiniYaml defaults)
@@ -414,7 +417,10 @@ namespace OpenRA.Mods.Common.Graphics
return subFrames != null ? subFrames.Skip(subStart).Take(subLength) : Enumerable.Range(subStart, subLength);
};
var subFilename = GetSpriteFilename(modData, tileSet, combineSequenceNode.Key, sequence, combineData, NoData);
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)
@@ -440,6 +446,9 @@ namespace OpenRA.Mods.Common.Graphics
// 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.");
sprites = cache[filename, getUsedFrames].Select(s =>
{
if (s == null)
@@ -497,11 +506,14 @@ namespace OpenRA.Mods.Common.Graphics
if (LoadField(HasEmbeddedPalette, data, defaults))
{
var filename = GetSpriteFilename(modData, tileSet, image, sequence, data, defaults);
if (filename == null)
throw new YamlException($"Sequence {image}.{sequence} does not define a filename.");
var metadata = cache.FrameMetadata(filename);
var i = frames != null ? frames[0] : start;
var palettes = metadata?.GetOrDefault<EmbeddedSpritePalette>();
if (palettes == null || !palettes.TryGetPaletteForFrame(i, out EmbeddedPalette))
throw new YamlException($"Cannot export palette from {filename}: frame {i} does not define an embedded palette");
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);

View File

@@ -17,23 +17,8 @@ namespace OpenRA.Mods.Common.Graphics
{
public class TilesetSpecificSpriteSequenceLoader : DefaultSpriteSequenceLoader
{
public readonly string DefaultSpriteExtension = ".shp";
public readonly Dictionary<string, string> TilesetExtensions = new Dictionary<string, string>();
public readonly Dictionary<string, string> TilesetCodes = new Dictionary<string, string>();
public TilesetSpecificSpriteSequenceLoader(ModData modData)
: base(modData)
{
var metadata = modData.Manifest.Get<SpriteSequenceFormat>().Metadata;
if (metadata.TryGetValue("DefaultSpriteExtension", out var yaml))
DefaultSpriteExtension = yaml.Value;
if (metadata.TryGetValue("TilesetExtensions", out yaml))
TilesetExtensions = yaml.ToDictionary(kv => kv.Value);
if (metadata.TryGetValue("TilesetCodes", out yaml))
TilesetCodes = yaml.ToDictionary(kv => kv.Value);
}
: base(modData) { }
public override ISpriteSequence CreateSequence(ModData modData, string tileSet, SpriteCache cache, string image, string sequence, MiniYaml data, MiniYaml defaults)
{
@@ -44,50 +29,23 @@ namespace OpenRA.Mods.Common.Graphics
[Desc("A sprite sequence that can have tileset-specific variants.")]
public class TilesetSpecificSpriteSequence : DefaultSpriteSequence
{
[Desc("Dictionary of <string: string> with tileset name to override -> tileset name to use instead.")]
static readonly SpriteSequenceField<Dictionary<string, string>> TilesetOverrides = new SpriteSequenceField<Dictionary<string, string>>(nameof(TilesetOverrides), null);
[Desc("Use `TilesetCodes` as defined in `mod.yaml` to add a letter as a second character " +
"into the sprite filename like the Westwood 2.5D titles did for tileset-specific variants.")]
static readonly SpriteSequenceField<bool> UseTilesetCode = new SpriteSequenceField<bool>(nameof(UseTilesetCode), false);
[Desc("Append a tileset-specific extension to the file name " +
"- either as defined in `mod.yaml`'s `TilesetExtensions` (if `UseTilesetExtension` is used) " +
"or the default hardcoded one for this sequence type (.shp).")]
static readonly SpriteSequenceField<bool> AddExtension = new SpriteSequenceField<bool>(nameof(AddExtension), true);
[Desc("Whether `mod.yaml`'s `TilesetExtensions` should be used with the sequence's file name.")]
static readonly SpriteSequenceField<bool> UseTilesetExtension = new SpriteSequenceField<bool>(nameof(UseTilesetExtension), false);
[Desc("Dictionary of <tileset name>: filename to override the Filename key.")]
static readonly SpriteSequenceField<Dictionary<string, string>> TilesetFilenames = new SpriteSequenceField<Dictionary<string, string>>(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) { }
static string ResolveTilesetId(string tileset, MiniYaml data, MiniYaml defaults)
{
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 GetSpriteFilename(ModData modData, string tileset, string image, string sequence, MiniYaml data, MiniYaml defaults)
{
var loader = (TilesetSpecificSpriteSequenceLoader)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);
if (LoadField(AddExtension, data, defaults))
var node = data.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key) ?? defaults.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key);
if (node != null)
{
if (LoadField(UseTilesetExtension, data, defaults))
if (loader.TilesetExtensions.TryGetValue(ResolveTilesetId(tileset, data, defaults), out var tilesetExtension))
return filename + tilesetExtension;
return filename + loader.DefaultSpriteExtension;
var tilesetNode = node.Value.Nodes.FirstOrDefault(n => n.Key == tileset);
if (tilesetNode != null)
return tilesetNode.Value.Value;
}
return filename;
return base.GetSpriteFilename(modData, tileset, image, sequence, data, defaults);
}
}
}

View File

@@ -0,0 +1,421 @@
#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.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace OpenRA.Mods.Common.UpdateRules.Rules
{
public class ExplicitSequenceFilenames : UpdateRule
{
public override string Name => "Sequence filenames must be specified explicitly.";
public override string Description =>
"Sequence sprite filenames are no longer automatically inferred, and the AddExtension,\n" +
"UseTilesetExtension, UseTilesetNodes and TilesetOverrides fields have been removed.\n\n" +
"The sprite filename for each sequence must now be defined using the Filename field.\n" +
"Tileset specific overrides can be defined as children of the TilesetFilenames field.";
string defaultSpriteExtension = ".shp";
List<MiniYamlNode> resolvedImagesNodes;
readonly Dictionary<string, string> tilesetExtensions = new Dictionary<string, string>();
readonly Dictionary<string, string> tilesetCodes = new Dictionary<string, string>();
bool parseModYaml = true;
bool reportModYamlChanges;
bool disabled;
public override IEnumerable<string> BeforeUpdateSequences(ModData modData, List<MiniYamlNode> resolvedImagesNodes)
{
// Keep a resolved copy of the sequences so we can account for values imported through inheritance or Defaults.
// This will be modified during processing, so take a deep copy to avoid side-effects on other update rules.
this.resolvedImagesNodes = MiniYaml.FromString(resolvedImagesNodes.WriteToString());
var requiredMetadata = new HashSet<string>();
foreach (var imageNode in resolvedImagesNodes)
{
foreach (var sequenceNode in imageNode.Value.Nodes)
{
var useTilesetExtensionNode = sequenceNode.LastChildMatching("UseTilesetExtension");
if (useTilesetExtensionNode != null && !tilesetExtensions.Any())
requiredMetadata.Add("TilesetExtensions");
var useTilesetCodeNode = sequenceNode.LastChildMatching("UseTilesetCode");
if (useTilesetCodeNode != null && !tilesetCodes.Any())
requiredMetadata.Add("TilesetCodes");
}
}
if (requiredMetadata.Any())
{
yield return $"The ExplicitSequenceFilenames rule requires {requiredMetadata.JoinWith(", ")}\n" +
"to be defined under the SpriteSequenceFormat definition in mod.yaml.\n" +
"Add these definitions back and run the update rule again.";
disabled = true;
}
}
public override IEnumerable<string> BeforeUpdate(ModData modData)
{
// Don't reload data when processing maps
if (!parseModYaml)
yield break;
parseModYaml = false;
// HACK: We need to read the obsolete yaml definitions to be able to update the sequences
// TilesetSpecificSpriteSequence no longer defines fields for these, so we must take them directly from mod.yaml
var yamlField = modData.Manifest.GetType().GetField("yaml", BindingFlags.Instance | BindingFlags.NonPublic);
var yaml = (Dictionary<string, MiniYaml>)yamlField?.GetValue(modData.Manifest);
if (yaml != null && yaml.TryGetValue("SpriteSequenceFormat", out var spriteSequenceFormatYaml))
{
if (spriteSequenceFormatYaml.Value == "DefaultSpriteSequence")
{
defaultSpriteExtension = "";
yield break;
}
var spriteSequenceFormatNode = new MiniYamlNode("", spriteSequenceFormatYaml);
var defaultSpriteExtensionNode = spriteSequenceFormatNode.LastChildMatching("DefaultSpriteExtension");
if (defaultSpriteExtensionNode != null)
{
reportModYamlChanges = true;
defaultSpriteExtension = defaultSpriteExtensionNode.Value.Value;
}
var tilesetExtensionsNode = spriteSequenceFormatNode.LastChildMatching("TilesetExtensions");
if (tilesetExtensionsNode != null)
{
reportModYamlChanges = true;
foreach (var n in tilesetExtensionsNode.Value.Nodes)
tilesetExtensions[n.Key] = n.Value.Value;
}
var tilesetCodesNode = spriteSequenceFormatNode.LastChildMatching("TilesetCodes");
if (tilesetCodesNode != null)
{
reportModYamlChanges = true;
foreach (var n in tilesetCodesNode.Value.Nodes)
tilesetCodes[n.Key] = n.Value.Value;
}
}
}
public override IEnumerable<string> AfterUpdate(ModData modData)
{
if (!reportModYamlChanges)
yield break;
yield return "The DefaultSpriteExtension, TilesetExtensions, and TilesetCodes fields defined\n" +
"under SpriteSequenceFormat in your mod.yaml are no longer used, and can be removed.";
reportModYamlChanges = false;
}
public override IEnumerable<string> UpdateSequenceNode(ModData modData, MiniYamlNode imageNode)
{
if (disabled)
yield break;
var resolvedImageNode = resolvedImagesNodes.Single(n => n.Key == imageNode.Key);
// Add a placeholder for inherited sequences that were previously implicitly named
var implicitInheritedSequences = new List<string>();
foreach (var resolvedSequenceNode in resolvedImageNode.Value.Nodes)
{
if (resolvedSequenceNode.Key != "Defaults" && string.IsNullOrEmpty(resolvedSequenceNode.Value.Value) &&
imageNode.LastChildMatching(resolvedSequenceNode.Key) == null)
{
imageNode.AddNode(resolvedSequenceNode.Key, "");
implicitInheritedSequences.Add(resolvedSequenceNode.Key);
}
}
var resolvedDefaultsNode = resolvedImageNode.LastChildMatching("Defaults");
if (resolvedDefaultsNode != null)
{
foreach (var resolvedSequenceNode in resolvedImageNode.Value.Nodes)
{
if (resolvedSequenceNode == resolvedDefaultsNode)
continue;
resolvedSequenceNode.Value.Nodes = MiniYaml.Merge(new[] { resolvedDefaultsNode.Value.Nodes, resolvedSequenceNode.Value.Nodes });
resolvedSequenceNode.Value.Value = resolvedSequenceNode.Value.Value ?? resolvedDefaultsNode.Value.Value;
}
}
// Sequences that explicitly defined a filename may be inherited by others that depend on the explicit name
// Keep track of these sequences so we don't remove the filenames later!
var explicitlyNamedSequences = new List<string>();
// Add Filename/TilesetFilenames nodes to every sequence
foreach (var sequenceNode in imageNode.Value.Nodes)
{
if (string.IsNullOrEmpty(sequenceNode.Key) || sequenceNode.KeyMatches("Inherits"))
continue;
if (!string.IsNullOrEmpty(sequenceNode.Value.Value))
explicitlyNamedSequences.Add(sequenceNode.Key);
var resolvedSequenceNode = resolvedImageNode.Value.Nodes.SingleOrDefault(n => n.Key == sequenceNode.Key);
if (resolvedSequenceNode == null)
continue;
ProcessNode(modData, sequenceNode, resolvedSequenceNode, imageNode.Key);
}
// Identify a suitable default for deduplication
MiniYamlNode defaultFilenameNode = null;
MiniYamlNode defaultTilesetFilenamesNode = null;
foreach (var defaultsNode in imageNode.ChildrenMatching("Defaults"))
{
defaultFilenameNode = defaultsNode.LastChildMatching("Filename") ?? defaultFilenameNode;
defaultTilesetFilenamesNode = defaultsNode.LastChildMatching("TilesetFilenames") ?? defaultTilesetFilenamesNode;
}
if ((defaultFilenameNode == null || defaultTilesetFilenamesNode == null) && !imageNode.Key.StartsWith("^"))
{
var duplicateCount = new Dictionary<string, int>();
var duplicateTilesetCount = new Dictionary<string, int>();
foreach (var sequenceNode in imageNode.Value.Nodes)
{
if (string.IsNullOrEmpty(sequenceNode.Key) || explicitlyNamedSequences.Contains(sequenceNode.Key))
continue;
var tilesetFilenamesNode = sequenceNode.LastChildMatching("TilesetFilenames");
if (defaultTilesetFilenamesNode == null && tilesetFilenamesNode != null)
{
var key = tilesetFilenamesNode.Value.Nodes.WriteToString();
duplicateTilesetCount[key] = duplicateTilesetCount.GetValueOrDefault(key, 0) + 1;
}
var filenameNode = sequenceNode.LastChildMatching("Filename");
if (defaultFilenameNode == null && filenameNode != null)
{
var key = filenameNode.Value.Value;
duplicateCount[key] = duplicateCount.GetValueOrDefault(key, 0) + 1;
}
}
var maxDuplicateTilesetCount = duplicateTilesetCount.MaxByOrDefault(kv => kv.Value).Value;
if (maxDuplicateTilesetCount > 1)
{
if (imageNode.LastChildMatching("Defaults") == null)
imageNode.Value.Nodes.Insert(0, new MiniYamlNode("Defaults", ""));
var nodes = MiniYaml.FromString(duplicateTilesetCount.First(kv => kv.Value == maxDuplicateTilesetCount).Key);
defaultTilesetFilenamesNode = new MiniYamlNode("TilesetFilenames", "", nodes);
imageNode.LastChildMatching("Defaults").Value.Nodes.Insert(0, defaultTilesetFilenamesNode);
}
var maxDuplicateCount = duplicateCount.MaxByOrDefault(kv => kv.Value).Value;
if (maxDuplicateCount > 1)
{
if (imageNode.LastChildMatching("Defaults") == null)
imageNode.Value.Nodes.Insert(0, new MiniYamlNode("Defaults", ""));
defaultFilenameNode = new MiniYamlNode("Filename", duplicateCount.First(kv => kv.Value == maxDuplicateCount).Key);
imageNode.LastChildMatching("Defaults").Value.Nodes.Insert(0, defaultFilenameNode);
}
}
// Remove redundant definitions
foreach (var sequenceNode in imageNode.Value.Nodes.ToList())
{
if (sequenceNode.Key == "Defaults" || sequenceNode.Key == "Inherits" || string.IsNullOrEmpty(sequenceNode.Key))
continue;
var combineNode = sequenceNode.LastChildMatching("Combine");
var filenameNode = sequenceNode.LastChildMatching("Filename");
var tilesetFilenamesNode = sequenceNode.LastChildMatching("TilesetFilenames");
if (defaultTilesetFilenamesNode != null && combineNode != null)
sequenceNode.Value.Nodes.Insert(0, new MiniYamlNode("TilesetFilenames", ""));
if (defaultFilenameNode != null && combineNode != null)
sequenceNode.Value.Nodes.Insert(0, new MiniYamlNode("Filename", ""));
if (defaultTilesetFilenamesNode != null && tilesetFilenamesNode == null && filenameNode != null)
{
var index = sequenceNode.Value.Nodes.IndexOf(filenameNode) + 1;
sequenceNode.Value.Nodes.Insert(index, new MiniYamlNode("TilesetFilenames", ""));
}
// Remove redundant overrides
if (!explicitlyNamedSequences.Contains(sequenceNode.Key))
{
if (defaultTilesetFilenamesNode != null && tilesetFilenamesNode != null)
{
var allTilesetsMatch = true;
foreach (var overrideNode in tilesetFilenamesNode.Value.Nodes)
if (!defaultTilesetFilenamesNode.Value.Nodes.Any(n => n.Key == overrideNode.Key && n.Value.Value == overrideNode.Value.Value))
allTilesetsMatch = false;
if (allTilesetsMatch)
sequenceNode.RemoveNode(tilesetFilenamesNode);
}
if (filenameNode?.Value.Value != null && filenameNode?.Value.Value == defaultFilenameNode?.Value.Value)
sequenceNode.RemoveNode(filenameNode);
}
}
var allSequencesHaveFilename = true;
var allSequencesHaveTilesetFilenames = true;
foreach (var sequenceNode in imageNode.Value.Nodes.ToList())
{
if (sequenceNode.Key == "Defaults" || sequenceNode.Key == "Inherits" || string.IsNullOrEmpty(sequenceNode.Key))
continue;
if (sequenceNode.LastChildMatching("Filename") == null)
allSequencesHaveFilename = false;
if (sequenceNode.LastChildMatching("TilesetFilenames") == null)
allSequencesHaveTilesetFilenames = false;
}
if (allSequencesHaveFilename || allSequencesHaveTilesetFilenames)
{
foreach (var sequenceNode in imageNode.Value.Nodes.ToList())
{
if (sequenceNode.Key == "Defaults")
{
if (allSequencesHaveFilename)
sequenceNode.RemoveNodes("Filename");
if (allSequencesHaveTilesetFilenames)
sequenceNode.RemoveNodes("TilesetFilenames");
if (!sequenceNode.Value.Nodes.Any())
imageNode.RemoveNode(sequenceNode);
}
if (allSequencesHaveFilename && sequenceNode.LastChildMatching("Combine") != null)
sequenceNode.RemoveNodes("Filename");
if (allSequencesHaveTilesetFilenames && sequenceNode.LastChildMatching("Combine") != null)
sequenceNode.RemoveNodes("TilesetFilenames");
var tilesetFilenamesNode = sequenceNode.LastChildMatching("TilesetFilenames");
if (allSequencesHaveTilesetFilenames && tilesetFilenamesNode != null && !tilesetFilenamesNode.Value.Nodes.Any())
sequenceNode.RemoveNode(tilesetFilenamesNode);
}
}
foreach (var sequenceNode in imageNode.Value.Nodes.ToList())
if (implicitInheritedSequences.Contains(sequenceNode.Key) && !sequenceNode.Value.Nodes.Any())
imageNode.RemoveNode(sequenceNode);
yield break;
}
void ProcessNode(ModData modData, MiniYamlNode sequenceNode, MiniYamlNode resolvedSequenceNode, string imageName)
{
var addExtension = true;
var addExtensionNode = resolvedSequenceNode.LastChildMatching("AddExtension");
if (addExtensionNode != null)
addExtension = FieldLoader.GetValue<bool>("AddExtension", addExtensionNode.Value.Value);
var useTilesetExtension = false;
var useTilesetExtensionNode = resolvedSequenceNode.LastChildMatching("UseTilesetExtension");
if (useTilesetExtensionNode != null)
useTilesetExtension = FieldLoader.GetValue<bool>("UseTilesetExtension", useTilesetExtensionNode.Value.Value);
var useTilesetCode = false;
var useTilesetCodeNode = resolvedSequenceNode.LastChildMatching("UseTilesetCode");
if (useTilesetCodeNode != null)
useTilesetCode = FieldLoader.GetValue<bool>("UseTilesetCode", useTilesetCodeNode.Value.Value);
var tilesetOverrides = new Dictionary<string, string>();
var tilesetOverridesNode = resolvedSequenceNode.LastChildMatching("TilesetOverrides");
if (tilesetOverridesNode != null)
foreach (var tilesetNode in tilesetOverridesNode.Value.Nodes)
tilesetOverrides[tilesetNode.Key] = tilesetNode.Value.Value;
sequenceNode.RemoveNodes("AddExtension");
sequenceNode.RemoveNodes("UseTilesetExtension");
sequenceNode.RemoveNodes("UseTilesetCode");
sequenceNode.RemoveNodes("TilesetOverrides");
// Replace removals with masking
foreach (var node in sequenceNode.Value.Nodes)
if (node.Key?.StartsWith("-") ?? false)
node.Key = node.Key.Substring(1);
var combineNode = sequenceNode.LastChildMatching("Combine");
if (combineNode != null)
{
var i = 0;
foreach (var node in combineNode.Value.Nodes)
{
ProcessNode(modData, node, node, node.Key);
node.Key = (i++).ToString();
}
return;
}
var filename = string.IsNullOrEmpty(resolvedSequenceNode.Value.Value) ? imageName : resolvedSequenceNode.Value.Value;
if (filename.StartsWith("^"))
return;
if (useTilesetExtension || useTilesetCode)
{
var tilesetFilenamesNode = new MiniYamlNode("TilesetFilenames", "");
var duplicateCount = new Dictionary<string, int>();
foreach (var tileset in modData.DefaultTerrainInfo.Keys)
{
if (!tilesetOverrides.TryGetValue(tileset, out var sequenceTileset))
sequenceTileset = tileset;
var overrideFilename = filename;
if (useTilesetCode)
overrideFilename = filename.Substring(0, 1) + tilesetCodes[sequenceTileset] +
filename.Substring(2, filename.Length - 2);
if (addExtension)
overrideFilename += useTilesetExtension ? tilesetExtensions[sequenceTileset] : defaultSpriteExtension;
tilesetFilenamesNode.AddNode(tileset, overrideFilename);
duplicateCount[overrideFilename] = duplicateCount.GetValueOrDefault(overrideFilename, 0) + 1;
}
sequenceNode.Value.Nodes.Insert(0, tilesetFilenamesNode);
// Deduplicate tileset overrides
var maxDuplicateCount = duplicateCount.MaxByOrDefault(kv => kv.Value).Value;
if (maxDuplicateCount > 1)
{
var filenameNode = new MiniYamlNode("Filename", duplicateCount.First(kv => kv.Value == maxDuplicateCount).Key);
foreach (var overrideNode in tilesetFilenamesNode.Value.Nodes.ToList())
if (overrideNode.Value.Value == filenameNode.Value.Value)
tilesetFilenamesNode.Value.Nodes.Remove(overrideNode);
if (!tilesetFilenamesNode.Value.Nodes.Any())
sequenceNode.RemoveNode(tilesetFilenamesNode);
sequenceNode.Value.Nodes.Insert(0, filenameNode);
}
}
else
{
if (addExtension)
filename += defaultSpriteExtension;
sequenceNode.Value.Nodes.Insert(0, new MiniYamlNode("Filename", filename));
}
sequenceNode.ReplaceValue("");
}
}
}

View File

@@ -103,6 +103,7 @@ namespace OpenRA.Mods.Common.UpdateRules
new UpdatePath("playtest-20221203", new UpdateRule[]
{
new TextNotificationsDisplayWidgetRemoveTime(),
new ExplicitSequenceFilenames(),
})
};