Files
OpenRA/OpenRA.Mods.Common/UpdateRules/Rules/20230225/ExplicitSequenceFilenames.cs
RoosterDragon 8f46247dc9 MiniYaml.From* methods support deferred execution.
Previously, the MiniYaml.From* helpers such as FromStream would consume the entire input and then return a list of top-level nodes. Now, the input is processed using deferred execution and top-level nodes are yielded as they are resolved from the input.

The motivating use-case is MapCache, which currently manually buffers nodes before passing to MiniYaml.FromString in order to improve responsiveness when large payloads are processed. Now that MiniYaml.FromStream yields results back as they come in, we can switch to that without disadvantage.

The maintains the performance where map cache can update search results as each node comes in over the network rather than having to wait for the entire batch to be transferred. Now we can also remove the string buffer that captures each node, reducing memory pressure and simplifying the code.
2025-04-13 14:12:25 +03:00

501 lines
18 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, IBeforeUpdateSequences
{
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<MiniYamlNodeBuilder> resolvedImagesNodes;
readonly Dictionary<string, string> tilesetExtensions = [];
readonly Dictionary<string, string> tilesetCodes = [];
bool parseModYaml = true;
bool reportModYamlChanges;
bool disabled;
public IEnumerable<string> BeforeUpdateSequences(ModData modData, List<MiniYamlNodeBuilder> 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(), nameof(BeforeUpdateSequences))
.Select(n => new MiniYamlNodeBuilder(n))
.ToList();
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.Count == 0)
requiredMetadata.Add("TilesetExtensions");
var useTilesetCodeNode = sequenceNode.LastChildMatching("UseTilesetCode");
if (useTilesetCodeNode != null && tilesetCodes.Count == 0)
requiredMetadata.Add("TilesetCodes");
}
}
if (requiredMetadata.Count != 0)
{
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 MiniYamlNodeBuilder("", new MiniYamlBuilder(spriteSequenceFormatYaml));
var defaultSpriteExtensionNode = spriteSequenceFormatNode.LastChildMatching("DefaultSpriteExtension");
if (defaultSpriteExtensionNode != null)
{
reportModYamlChanges = true;
defaultSpriteExtension = defaultSpriteExtensionNode.Value.Value;
}
var fromBackup = false;
var tilesetExtensionsNode = spriteSequenceFormatNode.LastChildMatching("TilesetExtensions")?.Value?.Nodes;
if (tilesetExtensionsNode == null)
{
switch (modData.Manifest.Id)
{
case "cnc":
fromBackup = true;
tilesetExtensionsNode =
[
new("TEMPERAT", ".tem"),
new("SNOW", ".sno"),
new("INTERIOR", ".int"),
new("DESERT", ".des"),
new("JUNGLE", ".jun"),
];
break;
case "ra":
fromBackup = true;
tilesetExtensionsNode =
[
new("TEMPERAT", ".tem"),
new("SNOW", ".sno"),
new("INTERIOR", ".int"),
new("DESERT", ".des"),
];
break;
case "ts":
fromBackup = true;
tilesetExtensionsNode =
[
new("TEMPERATE", ".tem"),
new("SNOW", ".sno"),
];
break;
}
}
if (tilesetExtensionsNode != null)
{
if (!fromBackup)
reportModYamlChanges = true;
foreach (var n in tilesetExtensionsNode)
tilesetExtensions[n.Key] = n.Value.Value;
}
fromBackup = false;
var tilesetCodesNode = spriteSequenceFormatNode.LastChildMatching("TilesetCodes")?.Value?.Nodes;
if (tilesetCodesNode == null && modData.Manifest.Id == "ts")
{
fromBackup = true;
tilesetCodesNode =
[
new("TEMPERATE", "t"),
new("SNOW", "a"),
];
}
if (tilesetCodesNode != null)
{
if (!fromBackup)
reportModYamlChanges = true;
foreach (var n in tilesetCodesNode)
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, MiniYamlNodeBuilder 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")
continue;
// Ignore nodes that are not implicitly named or already processed
if (!string.IsNullOrEmpty(resolvedSequenceNode.Value.Value) || resolvedSequenceNode.LastChildMatching("Filename") != null)
continue;
if (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(
[
resolvedDefaultsNode.Value.Nodes.Select(n => n.Build()).ToArray(),
resolvedSequenceNode.Value.Nodes.Select(n => n.Build()).ToArray()
]).ConvertAll(n => new MiniYamlNodeBuilder(n));
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.NodeWithKeyOrDefault(sequenceNode.Key);
if (resolvedSequenceNode == null)
continue;
ProcessNode(modData, sequenceNode, resolvedSequenceNode, imageNode.Key);
}
// Identify a suitable default for deduplication
MiniYamlNodeBuilder defaultFilenameNode = null;
MiniYamlNodeBuilder 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 MiniYamlNodeBuilder("Defaults", "");
imageNode.Value.Nodes.Insert(inheritsNodeIndex, defaultsNode);
}
var nodes = MiniYaml.FromString(duplicateTilesetCount.First(kv => kv.Value == maxDuplicateTilesetCount).Key, nameof(UpdateSequenceNode)).ToList();
defaultTilesetFilenamesNode = new MiniYamlNodeBuilder("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 MiniYamlNodeBuilder("Defaults", "");
imageNode.Value.Nodes.Insert(inheritsNodeIndex, defaultsNode);
}
defaultFilenameNode = new MiniYamlNodeBuilder("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 MiniYamlNodeBuilder("TilesetFilenames", ""));
if (defaultFilenameNode != null && combineNode != null)
sequenceNode.Value.Nodes.Insert(0, new MiniYamlNodeBuilder("Filename", ""));
if (defaultTilesetFilenamesNode != null && tilesetFilenamesNode == null && filenameNode != null)
{
var index = sequenceNode.Value.Nodes.IndexOf(filenameNode) + 1;
sequenceNode.Value.Nodes.Insert(index, new MiniYamlNodeBuilder("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.Count == 0)
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.Count == 0)
sequenceNode.RemoveNode(tilesetFilenamesNode);
}
}
foreach (var sequenceNode in imageNode.Value.Nodes.ToList())
if (implicitInheritedSequences.Contains(sequenceNode.Key) && sequenceNode.Value.Nodes.Count == 0)
imageNode.RemoveNode(sequenceNode);
}
void ProcessNode(ModData modData, MiniYamlNodeBuilder sequenceNode, MiniYamlNodeBuilder resolvedSequenceNode, string imageName)
{
// "Filename" was introduced with this update rule, so that means this node was already processed and can be skipped
if (sequenceNode.LastChildMatching("Filename") != null)
return;
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[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++.ToStringInvariant();
}
return;
}
var filename = string.IsNullOrEmpty(resolvedSequenceNode.Value.Value) ? imageName : resolvedSequenceNode.Value.Value;
if (filename.StartsWith('^'))
return;
if (useTilesetExtension || useTilesetCode)
{
var tilesetFilenamesNode = new MiniYamlNodeBuilder("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[..1] + tilesetCodes[sequenceTileset] + filename[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 MiniYamlNodeBuilder("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.Count == 0)
sequenceNode.RemoveNode(tilesetFilenamesNode);
sequenceNode.Value.Nodes.Insert(0, filenameNode);
}
}
else
{
if (addExtension)
filename += defaultSpriteExtension;
sequenceNode.Value.Nodes.Insert(0, new MiniYamlNodeBuilder("Filename", filename));
}
sequenceNode.ReplaceValue("");
}
}
}