Files
OpenRA/OpenRA.Mods.Common/UpdateRules/UpdateUtils.cs
RoosterDragon b7e0ed9b87 Improve lookups of nodes by key in MiniYaml.
When handling the Nodes collection in MiniYaml, individual nodes are located via one of two methods:

// Lookup a single key with linear search.
var node = yaml.Nodes.FirstOrDefault(n => n.Key == "SomeKey");

// Convert to dictionary, expecting many key lookups.
var dict = nodes.ToDictionary();

// Lookup a single key in the dictionary.
var node = dict["SomeKey"];

To simplify lookup of individual keys via linear search, provide helper methods NodeWithKeyOrDefault and NodeWithKey. These helpers do the equivalent of Single{OrDefault} searches. Whilst this requires checking the whole list, it provides a useful correctness check. Two duplicated keys in TS yaml are fixed as a result. We can also optimize the helpers to not use LINQ, avoiding allocation of the delegate to search for a key.

Adjust existing code to use either lnear searches or dictionary lookups based on whether it will be resolving many keys. Resolving few keys can be done with linear searches to avoid building a dictionary. Resolving many keys should be done with a dictionary to avoid quaradtic runtime from repeated linear searches.
2023-09-23 14:31:04 +02:00

461 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;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using OpenRA.FileSystem;
namespace OpenRA.Mods.Common.UpdateRules
{
using YamlFileSet = List<(IReadWritePackage, string, List<MiniYamlNodeBuilder>)>;
public static class UpdateUtils
{
/// <summary>
/// Loads a YamlFileSet from a list of mod files.
/// </summary>
static YamlFileSet LoadModYaml(ModData modData, IEnumerable<string> files)
{
var yaml = new YamlFileSet();
foreach (var filename in files)
{
if (!modData.ModFiles.TryGetPackageContaining(filename, out var package, out var name) || package is not IReadWritePackage)
{
Console.WriteLine("Failed to load file `{0}` for writing. It will not be updated.", filename);
continue;
}
yaml.Add(((IReadWritePackage)package, name, MiniYaml.FromStream(package.GetStream(name), name, false).Select(n => new MiniYamlNodeBuilder(n)).ToList()));
}
return yaml;
}
/// <summary>
/// Loads a YamlFileSet containing any external yaml definitions referenced by a map yaml block.
/// </summary>
static YamlFileSet LoadExternalMapYaml(ModData modData, MiniYamlBuilder yaml, HashSet<string> externalFilenames)
{
return FieldLoader.GetValue<string[]>("value", yaml.Value)
.Where(f => f.Contains('|'))
.SelectMany(f => LoadModYaml(modData, FilterExternalModFiles(modData, new[] { f }, externalFilenames)))
.ToList();
}
/// <summary>
/// Loads a YamlFileSet containing any internal definitions yaml referenced by a map yaml block.
/// External references or internal references to missing files are ignored.
/// </summary>
static YamlFileSet LoadInternalMapYaml(ModData modData, IReadWritePackage mapPackage, MiniYamlBuilder yaml, HashSet<string> externalFilenames)
{
var fileSet = new YamlFileSet()
{
(null, "map.yaml", yaml.Nodes)
};
var files = FieldLoader.GetValue<string[]>("value", yaml.Value);
foreach (var filename in files)
{
// Ignore any files that aren't in the map bundle
if (!filename.Contains('|') && mapPackage.Contains(filename))
fileSet.Add((mapPackage, filename, MiniYaml.FromStream(mapPackage.GetStream(filename), filename, false).Select(n => new MiniYamlNodeBuilder(n)).ToList()));
else if (modData.ModFiles.Exists(filename))
externalFilenames.Add(filename);
}
return fileSet;
}
/// <summary>
/// Run a given update rule on a map.
/// The rule is only applied to internal files - external includes are assumed to be handled separately
/// but are noted in the externalFilenames list for informational purposes.
/// </summary>
public static List<string> UpdateMap(ModData modData, IReadWritePackage mapPackage, UpdateRule rule, out YamlFileSet files, HashSet<string> externalFilenames)
{
var manualSteps = new List<string>();
using (var mapStream = mapPackage.GetStream("map.yaml"))
{
if (mapStream == null)
{
// Not a valid map
files = new YamlFileSet();
return manualSteps;
}
var yaml = new MiniYamlBuilder(null, MiniYaml.FromStream(mapStream, mapPackage.Name, false));
files = new YamlFileSet() { (mapPackage, "map.yaml", yaml.Nodes) };
manualSteps.AddRange(rule.BeforeUpdate(modData));
var mapActorsNode = yaml.NodeWithKeyOrDefault("Actors");
if (mapActorsNode != null)
{
var mapActors = new YamlFileSet()
{
(null, "map.yaml", mapActorsNode.Value.Nodes)
};
manualSteps.AddRange(ApplyTopLevelTransform(modData, mapActors, rule.UpdateMapActorNode));
files.AddRange(mapActors);
}
var mapRulesNode = yaml.NodeWithKeyOrDefault("Rules");
if (mapRulesNode != null)
{
if (rule is IBeforeUpdateActors before)
{
var resolvedActors = LoadMapYaml(modData.DefaultFileSystem, mapPackage, modData.Manifest.Rules, mapRulesNode.Value);
manualSteps.AddRange(before.BeforeUpdateActors(modData, resolvedActors));
}
var mapRules = LoadInternalMapYaml(modData, mapPackage, mapRulesNode.Value, externalFilenames);
manualSteps.AddRange(ApplyTopLevelTransform(modData, mapRules, rule.UpdateActorNode));
files.AddRange(mapRules);
}
var mapWeaponsNode = yaml.NodeWithKeyOrDefault("Weapons");
if (mapWeaponsNode != null)
{
if (rule is IBeforeUpdateWeapons before)
{
var resolvedWeapons = LoadMapYaml(modData.DefaultFileSystem, mapPackage, modData.Manifest.Weapons, mapWeaponsNode.Value);
manualSteps.AddRange(before.BeforeUpdateWeapons(modData, resolvedWeapons));
}
var mapWeapons = LoadInternalMapYaml(modData, mapPackage, mapWeaponsNode.Value, externalFilenames);
manualSteps.AddRange(ApplyTopLevelTransform(modData, mapWeapons, rule.UpdateWeaponNode));
files.AddRange(mapWeapons);
}
var mapSequencesNode = yaml.NodeWithKeyOrDefault("Sequences");
if (mapSequencesNode != null)
{
if (rule is IBeforeUpdateSequences before)
{
var resolvedImages = LoadMapYaml(modData.DefaultFileSystem, mapPackage, modData.Manifest.Sequences, mapSequencesNode.Value);
manualSteps.AddRange(before.BeforeUpdateSequences(modData, resolvedImages));
}
var mapSequences = LoadInternalMapYaml(modData, mapPackage, mapSequencesNode.Value, externalFilenames);
manualSteps.AddRange(ApplyTopLevelTransform(modData, mapSequences, rule.UpdateSequenceNode));
files.AddRange(mapSequences);
}
manualSteps.AddRange(rule.AfterUpdate(modData));
}
return manualSteps;
}
public static List<MiniYamlNodeBuilder> LoadMapYaml(IReadOnlyFileSystem fileSystem, IReadOnlyPackage mapPackage, IEnumerable<string> files, MiniYamlBuilder mapNode)
{
var yaml = files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s)).ToList();
if (mapNode != null && mapNode.Value != null)
{
var mapFiles = FieldLoader.GetValue<string[]>("value", mapNode.Value);
yaml.AddRange(mapFiles.Select(filename =>
{
// Explicit package paths never refer to a map
if (!filename.Contains('|') && mapPackage.Contains(filename))
return MiniYaml.FromStream(mapPackage.GetStream(filename));
return MiniYaml.FromStream(fileSystem.Open(filename));
}));
}
if (mapNode != null && mapNode.Nodes.Count > 0)
yaml.Add(mapNode.Nodes.Select(n => n.Build()).ToList());
return MiniYaml.Merge(yaml).Select(n => new MiniYamlNodeBuilder(n)).ToList();
}
static IEnumerable<string> FilterExternalModFiles(ModData modData, IEnumerable<string> files, HashSet<string> externalFilenames)
{
foreach (var f in files)
{
if (f.Contains('|') && modData.DefaultFileSystem.IsExternalModFile(f))
{
externalFilenames.Add(f);
continue;
}
yield return f;
}
}
public static List<string> UpdateMod(ModData modData, UpdateRule rule, out YamlFileSet files, HashSet<string> externalFilenames)
{
var manualSteps = new List<string>();
var modRules = LoadModYaml(modData, FilterExternalModFiles(modData, modData.Manifest.Rules, externalFilenames));
var modWeapons = LoadModYaml(modData, FilterExternalModFiles(modData, modData.Manifest.Weapons, externalFilenames));
var modSequences = LoadModYaml(modData, FilterExternalModFiles(modData, modData.Manifest.Sequences, externalFilenames));
var modTilesets = LoadModYaml(modData, FilterExternalModFiles(modData, modData.Manifest.TileSets, externalFilenames));
var modChromeLayout = LoadModYaml(modData, FilterExternalModFiles(modData, modData.Manifest.ChromeLayout, externalFilenames));
var modChromeProvider = LoadModYaml(modData, FilterExternalModFiles(modData, modData.Manifest.Chrome, externalFilenames));
// Find and add shared map includes
foreach (var package in modData.MapCache.EnumerateMapPackagesWithoutCaching())
{
using (var mapStream = package.GetStream("map.yaml"))
{
if (mapStream == null)
continue;
var yaml = new MiniYamlBuilder(new MiniYaml(null, MiniYaml.FromStream(mapStream, package.Name, false)));
var mapRulesNode = yaml.NodeWithKeyOrDefault("Rules");
if (mapRulesNode != null)
foreach (var f in LoadExternalMapYaml(modData, mapRulesNode.Value, externalFilenames))
if (!modRules.Any(m => m.Item1 == f.Item1 && m.Item2 == f.Item2))
modRules.Add(f);
var mapWeaponsNode = yaml.NodeWithKeyOrDefault("Weapons");
if (mapWeaponsNode != null)
foreach (var f in LoadExternalMapYaml(modData, mapWeaponsNode.Value, externalFilenames))
if (!modWeapons.Any(m => m.Item1 == f.Item1 && m.Item2 == f.Item2))
modWeapons.Add(f);
var mapSequencesNode = yaml.NodeWithKeyOrDefault("Sequences");
if (mapSequencesNode != null)
foreach (var f in LoadExternalMapYaml(modData, mapSequencesNode.Value, externalFilenames))
if (!modSequences.Any(m => m.Item1 == f.Item1 && m.Item2 == f.Item2))
modSequences.Add(f);
}
}
manualSteps.AddRange(rule.BeforeUpdate(modData));
if (rule is IBeforeUpdateActors beforeActors)
{
var resolvedActors = MiniYaml.Load(modData.DefaultFileSystem, modData.Manifest.Rules, null)
.Select(n => new MiniYamlNodeBuilder(n)).ToList();
manualSteps.AddRange(beforeActors.BeforeUpdateActors(modData, resolvedActors));
}
manualSteps.AddRange(ApplyTopLevelTransform(modData, modRules, rule.UpdateActorNode));
if (rule is IBeforeUpdateWeapons beforeWeapons)
{
var resolvedWeapons = MiniYaml.Load(modData.DefaultFileSystem, modData.Manifest.Weapons, null)
.Select(n => new MiniYamlNodeBuilder(n)).ToList();
manualSteps.AddRange(beforeWeapons.BeforeUpdateWeapons(modData, resolvedWeapons));
}
manualSteps.AddRange(ApplyTopLevelTransform(modData, modWeapons, rule.UpdateWeaponNode));
if (rule is IBeforeUpdateSequences beforeSequences)
{
var resolvedImages = MiniYaml.Load(modData.DefaultFileSystem, modData.Manifest.Sequences, null)
.Select(n => new MiniYamlNodeBuilder(n)).ToList();
manualSteps.AddRange(beforeSequences.BeforeUpdateSequences(modData, resolvedImages));
}
manualSteps.AddRange(ApplyTopLevelTransform(modData, modSequences, rule.UpdateSequenceNode));
manualSteps.AddRange(ApplyTopLevelTransform(modData, modTilesets, rule.UpdateTilesetNode));
manualSteps.AddRange(ApplyChromeTransform(modData, modChromeLayout, rule.UpdateChromeNode));
manualSteps.AddRange(ApplyTopLevelTransform(modData, modChromeProvider, rule.UpdateChromeProviderNode));
manualSteps.AddRange(rule.AfterUpdate(modData));
files = modRules.ToList();
files.AddRange(modWeapons);
files.AddRange(modSequences);
files.AddRange(modTilesets);
files.AddRange(modChromeLayout);
files.AddRange(modChromeProvider);
return manualSteps;
}
static IEnumerable<string> ApplyChromeTransformInner(ModData modData, MiniYamlNodeBuilder current, UpdateRule.ChromeNodeTransform transform)
{
foreach (var manualStep in transform(modData, current))
yield return manualStep;
var childrenNode = current.Value.NodeWithKeyOrDefault("Children");
if (childrenNode != null)
foreach (var node in childrenNode.Value.Nodes)
if (node.Key != null)
foreach (var manualStep in ApplyChromeTransformInner(modData, node, transform))
yield return manualStep;
}
static IEnumerable<string> ApplyChromeTransform(ModData modData, YamlFileSet files, UpdateRule.ChromeNodeTransform transform)
{
if (transform == null)
yield break;
foreach (var file in files)
foreach (var node in file.Item3)
if (node.Key != null)
foreach (var manualStep in ApplyChromeTransformInner(modData, node, transform))
yield return manualStep;
}
static IEnumerable<string> ApplyTopLevelTransform(ModData modData, YamlFileSet files, UpdateRule.TopLevelNodeTransform transform)
{
if (transform == null)
yield break;
foreach (var file in files)
foreach (var node in file.Item3)
if (node.Key != null)
foreach (var manualStep in transform(modData, node))
yield return manualStep;
}
public static string FormatMessageList(IEnumerable<string> messages, int indent = 0, string separator = "*")
{
var prefix = string.Concat(Enumerable.Repeat(" ", indent));
return string.Join("\n", messages.Select(m => prefix + $" {separator} {m.Replace("\n", "\n " + prefix)}"));
}
}
public static class UpdateExtensions
{
public static void Save(this YamlFileSet files)
{
foreach (var file in files)
{
if (file.Item1 == null)
continue;
var textData = Encoding.UTF8.GetBytes(file.Item3.WriteToString());
if (!Enumerable.SequenceEqual(textData, file.Item1.GetStream(file.Item2).ReadAllBytes()))
file.Item1.Update(file.Item2, textData);
}
}
/// <summary>Checks if node is a removal (has '-' prefix).</summary>
public static bool IsRemoval(this MiniYamlNodeBuilder node)
{
return node.Key[0].ToString() == "-";
}
/// <summary>Renames a yaml key preserving any @suffix.</summary>
public static void RenameKey(this MiniYamlNodeBuilder node, string newKey, bool preserveSuffix = true, bool includeRemovals = true)
{
var prefix = includeRemovals && node.IsRemoval() ? "-" : "";
var split = node.Key.IndexOf("@", StringComparison.Ordinal);
if (preserveSuffix && split > -1)
node.Key = prefix + newKey + node.Key[split..];
else
node.Key = prefix + newKey;
}
public static T NodeValue<T>(this MiniYamlNodeBuilder node)
{
return FieldLoader.GetValue<T>(node.Key, node.Value.Value);
}
public static void ReplaceValue(this MiniYamlNodeBuilder node, string value)
{
node.Value.Value = value;
}
public static void AddNode(this MiniYamlNodeBuilder node, string key, object value)
{
node.Value.Nodes.Add(new MiniYamlNodeBuilder(key, FieldSaver.FormatValue(value)));
}
public static void AddNode(this MiniYamlNodeBuilder node, MiniYamlNodeBuilder toAdd)
{
node.Value.Nodes.Add(toAdd);
}
public static void RemoveNode(this MiniYamlNodeBuilder node, MiniYamlNodeBuilder toRemove)
{
node.Value.Nodes.Remove(toRemove);
}
public static void MoveNode(this MiniYamlNodeBuilder node, MiniYamlNodeBuilder fromNode, MiniYamlNodeBuilder toNode)
{
toNode.Value.Nodes.Add(node);
fromNode.Value.Nodes.Remove(node);
}
public static void MoveAndRenameNode(this MiniYamlNodeBuilder node,
MiniYamlNodeBuilder fromNode, MiniYamlNodeBuilder toNode, string newKey, bool preserveSuffix = true, bool includeRemovals = true)
{
node.RenameKey(newKey, preserveSuffix, includeRemovals);
node.MoveNode(fromNode, toNode);
}
/// <summary>Removes children with keys equal to [match] or [match]@[arbitrary suffix].</summary>
public static int RemoveNodes(this MiniYamlNodeBuilder node, string match, bool ignoreSuffix = true, bool includeRemovals = true)
{
return node.Value.Nodes.RemoveAll(n => n.KeyMatches(match, ignoreSuffix, includeRemovals));
}
/// <summary>Returns true if the node is of the form [match] or [match]@[arbitrary suffix].</summary>
public static bool KeyMatches(this MiniYamlNodeBuilder node, string match, bool ignoreSuffix = true, bool includeRemovals = true)
{
if (node.Key == null)
return false;
var prefix = includeRemovals && node.IsRemoval() ? "-" : "";
if (node.Key == prefix + match)
return true;
// If the previous check didn't return true and we wanted the suffix to match, return false unconditionally here
if (!ignoreSuffix)
return false;
var atPosition = node.Key.IndexOf('@');
return atPosition > 0 && node.Key[..atPosition] == prefix + match;
}
/// <summary>Returns true if the node is of the form [match], [match]@[arbitrary suffix] or [arbitrary suffix]@[match].</summary>
public static bool KeyContains(this MiniYamlNodeBuilder node, string match, bool ignoreSuffix = true, bool includeRemovals = true)
{
if (node.Key == null)
return false;
var atPosition = node.Key.IndexOf('@');
var relevantPart = ignoreSuffix && atPosition > 0 ? node.Key[..atPosition] : node.Key;
if (relevantPart.Contains(match) && (includeRemovals || !node.IsRemoval()))
return true;
return false;
}
/// <summary>Returns children with keys equal to [match] or [match]@[arbitrary suffix].</summary>
public static IEnumerable<MiniYamlNodeBuilder> ChildrenMatching(this MiniYamlNodeBuilder node, string match, bool ignoreSuffix = true, bool includeRemovals = true)
{
return node.Value.Nodes.Where(n => n.KeyMatches(match, ignoreSuffix, includeRemovals));
}
/// <summary>Returns children whose keys contain 'match' (optionally in the suffix).</summary>
public static IEnumerable<MiniYamlNodeBuilder> ChildrenContaining(this MiniYamlNodeBuilder node, string match, bool ignoreSuffix = true, bool includeRemovals = true)
{
return node.Value.Nodes.Where(n => n.KeyContains(match, ignoreSuffix, includeRemovals));
}
public static MiniYamlNodeBuilder LastChildMatching(this MiniYamlNodeBuilder node, string match, bool includeRemovals = true)
{
return node.ChildrenMatching(match, includeRemovals: includeRemovals).LastOrDefault();
}
public static void RenameChildrenMatching(this MiniYamlNodeBuilder node, string match, string newKey, bool preserveSuffix = true, bool includeRemovals = true)
{
var matching = node.ChildrenMatching(match);
foreach (var m in matching)
m.RenameKey(newKey, preserveSuffix, includeRemovals);
}
}
}