Files
OpenRA/OpenRA.Mods.Common/UpdateRules/UpdateUtils.cs
2024-07-29 21:56:36 +02:00

492 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;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using OpenRA.FileSystem;
namespace OpenRA.Mods.Common.UpdateRules
{
using YamlFileSet = List<(IReadWritePackage Package, string File, List<MiniYamlNodeBuilder> Nodes)>;
public static class UpdateUtils
{
/// <summary>
/// Loads a YamlFileSet from a list of mod files.
/// </summary>
public 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), $"{package.Name}:{name}", false)
.ConvertAll(n => new MiniYamlNodeBuilder(n))));
}
return yaml;
}
/// <summary>
/// Loads a YamlFileSet containing any external yaml definitions referenced by a map yaml block.
/// </summary>
public 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, FilterExternalFiles(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>
public 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), $"{mapPackage.Name}:{filename}", false)
.ConvertAll(n => new MiniYamlNodeBuilder(n))));
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}:map.yaml", 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), $"{mapPackage.Name}:{filename}");
return MiniYaml.FromStream(fileSystem.Open(filename), filename);
}));
}
if (mapNode != null && mapNode.Nodes.Count > 0)
yaml.Add(mapNode.Nodes.ConvertAll(n => n.Build()));
return MiniYaml.Merge(yaml).ConvertAll(n => new MiniYamlNodeBuilder(n));
}
public static IEnumerable<string> FilterExternalFiles(ModData modData, IEnumerable<string> files, HashSet<string> externalFilenames)
{
foreach (var f in files)
{
if (f.Contains('|') && modData.DefaultFileSystem.IsExternalFile(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, FilterExternalFiles(modData, modData.Manifest.Rules, externalFilenames));
var modWeapons = LoadModYaml(modData, FilterExternalFiles(modData, modData.Manifest.Weapons, externalFilenames));
var modSequences = LoadModYaml(modData, FilterExternalFiles(modData, modData.Manifest.Sequences, externalFilenames));
var modTilesets = LoadModYaml(modData, FilterExternalFiles(modData, modData.Manifest.TileSets, externalFilenames));
var modChromeLayout = LoadModYaml(modData, FilterExternalFiles(modData, modData.Manifest.ChromeLayout, externalFilenames));
var modChromeProvider = LoadModYaml(modData, FilterExternalFiles(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}:map.yaml", false)));
var mapRulesNode = yaml.NodeWithKeyOrDefault("Rules");
if (mapRulesNode != null)
foreach (var f in LoadExternalMapYaml(modData, mapRulesNode.Value, externalFilenames))
if (!modRules.Any(m => m.Package == f.Package && m.File == f.File))
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.Package == f.Package && m.File == f.File))
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.Package == f.Package && m.File == f.File))
modSequences.Add(f);
}
}
manualSteps.AddRange(rule.BeforeUpdate(modData));
if (rule is IBeforeUpdateActors beforeActors)
{
var resolvedActors = MiniYaml.Load(modData.DefaultFileSystem, modData.Manifest.Rules, null)
.ConvertAll(n => new MiniYamlNodeBuilder(n));
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)
.ConvertAll(n => new MiniYamlNodeBuilder(n));
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)
.ConvertAll(n => new MiniYamlNodeBuilder(n));
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 (_, _, nodes) in files)
foreach (var node in nodes)
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 (_, _, nodes) in files)
foreach (var node in nodes)
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 bool IsAlreadyTranslated(string translation)
{
if (translation == translation.ToLowerInvariant() && translation.Any(c => c == '-') && translation.All(c => c != ' '))
{
Console.WriteLine($"Skipping {translation} because it is already translated.");
return true;
}
return false;
}
}
public static class UpdateExtensions
{
public static void Save(this YamlFileSet files)
{
foreach (var (package, file, nodes) in files)
{
if (package == null)
continue;
var textData = Encoding.UTF8.GetBytes(nodes.WriteToString());
if (!Enumerable.SequenceEqual(textData, package.GetStream(file).ReadAllBytes()))
package.Update(file, 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('@');
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);
}
}
}