#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 Nodes)>; public static class UpdateUtils { /// /// Loads a YamlFileSet from a list of mod files. /// public static YamlFileSet LoadModYaml(ModData modData, IEnumerable 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; } /// /// Loads a YamlFileSet containing any external yaml definitions referenced by a map yaml block. /// public static YamlFileSet LoadExternalMapYaml(ModData modData, MiniYamlBuilder yaml, HashSet externalFilenames) { return FieldLoader.GetValue("value", yaml.Value) .Where(f => f.Contains('|')) .SelectMany(f => LoadModYaml(modData, FilterExternalFiles(modData, new[] { f }, externalFilenames))) .ToList(); } /// /// Loads a YamlFileSet containing any internal definitions yaml referenced by a map yaml block. /// External references or internal references to missing files are ignored. /// public static YamlFileSet LoadInternalMapYaml(ModData modData, IReadWritePackage mapPackage, MiniYamlBuilder yaml, HashSet externalFilenames) { var fileSet = new YamlFileSet() { (null, "map.yaml", yaml.Nodes) }; var files = FieldLoader.GetValue("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; } /// /// 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. /// public static List UpdateMap(ModData modData, IReadWritePackage mapPackage, UpdateRule rule, out YamlFileSet files, HashSet externalFilenames) { var manualSteps = new List(); 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 LoadMapYaml( IReadOnlyFileSystem fileSystem, IReadOnlyPackage mapPackage, IEnumerable 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("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 FilterExternalFiles(ModData modData, IEnumerable files, HashSet externalFilenames) { foreach (var f in files) { if (f.Contains('|') && modData.DefaultFileSystem.IsExternalFile(f)) { externalFilenames.Add(f); continue; } yield return f; } } public static List UpdateMod(ModData modData, UpdateRule rule, out YamlFileSet files, HashSet externalFilenames) { var manualSteps = new List(); 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 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 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 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 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); } } /// Checks if node is a removal (has '-' prefix). public static bool IsRemoval(this MiniYamlNodeBuilder node) { return node.Key[0].ToString() == "-"; } /// Renames a yaml key preserving any @suffix. 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(this MiniYamlNodeBuilder node) { return FieldLoader.GetValue(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); } /// Removes children with keys equal to [match] or [match]@[arbitrary suffix]. 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)); } /// Returns true if the node is of the form [match] or [match]@[arbitrary suffix]. 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; } /// Returns true if the node is of the form [match], [match]@[arbitrary suffix] or [arbitrary suffix]@[match]. 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; } /// Returns children with keys equal to [match] or [match]@[arbitrary suffix]. public static IEnumerable ChildrenMatching( this MiniYamlNodeBuilder node, string match, bool ignoreSuffix = true, bool includeRemovals = true) { return node.Value.Nodes.Where(n => n.KeyMatches(match, ignoreSuffix, includeRemovals)); } /// Returns children whose keys contain 'match' (optionally in the suffix). public static IEnumerable 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); } } }