mono was the bottleneck restricting our ability to use a newer C# version. mono 6.12 is currently available. Although poorly documented on their website, this supports C# 9. https://www.mono-project.com/docs/about-mono/versioning/#mono-source-versioning indicates mono 6.12 uses Roslyn 3.9.0. https://github.com/dotnet/roslyn/blob/main/docs/wiki/NuGet-packages.md#versioning indicates Roslyn 3.9.0 supports C# 9. This unlocks C# 8 and C# 9 features previously unavailable to us. - https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-80 - https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-9 A newer version of StyleCop is required to avoid rules tripping up on the new syntax. Currently only prerelease versions are available but their use is encouraged https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3420#issuecomment-994899135 Fix style rule violations on existing rules where the newer language version makes some existing casts redundant or allows use of the null coalescing assignment operator.
431 lines
17 KiB
C#
431 lines
17 KiB
C#
#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 ??= 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 inheritsNode = imageNode.LastChildMatching("Inherits");
|
|
var inheritsNodeIndex = inheritsNode == null ? 0 : imageNode.Value.Nodes.IndexOf(inheritsNode) + 1;
|
|
|
|
var maxDuplicateTilesetCount = duplicateTilesetCount.MaxByOrDefault(kv => kv.Value).Value;
|
|
if (maxDuplicateTilesetCount > 1)
|
|
{
|
|
var defaultsNode = imageNode.LastChildMatching("Defaults");
|
|
if (defaultsNode == null)
|
|
{
|
|
defaultsNode = new MiniYamlNode("Defaults", "");
|
|
imageNode.Value.Nodes.Insert(inheritsNodeIndex, defaultsNode);
|
|
}
|
|
|
|
var nodes = MiniYaml.FromString(duplicateTilesetCount.First(kv => kv.Value == maxDuplicateTilesetCount).Key);
|
|
defaultTilesetFilenamesNode = new MiniYamlNode("TilesetFilenames", "", nodes);
|
|
defaultsNode.Value.Nodes.Insert(0, defaultTilesetFilenamesNode);
|
|
}
|
|
|
|
var maxDuplicateCount = duplicateCount.MaxByOrDefault(kv => kv.Value).Value;
|
|
if (maxDuplicateCount > 1)
|
|
{
|
|
var defaultsNode = imageNode.LastChildMatching("Defaults");
|
|
if (defaultsNode == null)
|
|
{
|
|
defaultsNode = new MiniYamlNode("Defaults", "");
|
|
imageNode.Value.Nodes.Insert(inheritsNodeIndex, defaultsNode);
|
|
}
|
|
|
|
defaultFilenameNode = new MiniYamlNode("Filename", duplicateCount.First(kv => kv.Value == maxDuplicateCount).Key);
|
|
defaultsNode.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);
|
|
}
|
|
|
|
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("");
|
|
}
|
|
}
|
|
}
|