diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 98a4ed45c9..233b8c4fa1 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -578,9 +578,6 @@ - - - @@ -847,6 +844,17 @@ + + + + + + + + + + + diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/DefineSoundDefaults.cs b/OpenRA.Mods.Common/UpdateRules/Rules/DefineSoundDefaults.cs new file mode 100644 index 0000000000..f69eb90a20 --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/DefineSoundDefaults.cs @@ -0,0 +1,78 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) + * 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; + +namespace OpenRA.Mods.Common.UpdateRules.Rules +{ + public class DefineSoundDefaults : UpdateRule + { + public override string Name { get { return "Move mod-specific sound defaults to yaml"; } } + public override string Description + { + get + { + return "Mod-specific default sound values have been removed from several traits.\n" + + "The original values are added back via yaml."; + } + } + + Tuple>[] fields = + { + Tuple.Create("ParaDrop", "ChuteSound", "chute1.aud", new List()), + Tuple.Create("EjectOnDeath", "ChuteSound", "chute1.aud", new List()), + Tuple.Create("ProductionParadrop", "ChuteSound", "chute1.aud", new List()), + Tuple.Create("Building", "BuildSounds", "placbldg.aud, build5.aud", new List()), + Tuple.Create("Building", "UndeploySounds", "cashturn.aud", new List()) + }; + + public override IEnumerable BeforeUpdate(ModData modData) + { + // Reset state for each mod/map + foreach (var field in fields) + field.Item4.Clear(); + + yield break; + } + + string BuildMessage(Tuple> field) + { + return "The default value for {0}.{1} has been removed.\n".F(field.Item1, field.Item2) + + "You may wish to explicitly define `{0}: {1}` at the following\n".F(field.Item2, field.Item3) + + "locations if the sound has not already been inherited from a parent definition.\n" + + UpdateUtils.FormatMessageList(field.Item4.Select(n => n.Location.ToString())); + } + + public override IEnumerable AfterUpdate(ModData modData) + { + foreach (var field in fields) + if (field.Item4.Any()) + yield return BuildMessage(field); + } + + public override IEnumerable UpdateActorNode(ModData modData, MiniYamlNode actorNode) + { + foreach (var field in fields) + { + foreach (var traitNode in actorNode.ChildrenMatching(field.Item1)) + { + var node = traitNode.LastChildMatching(field.Item2); + if (node == null) + field.Item4.Add(traitNode); + } + } + + yield break; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/RemoveTerrainTypeIsWaterFlag.cs b/OpenRA.Mods.Common/UpdateRules/Rules/RemoveTerrainTypeIsWaterFlag.cs new file mode 100644 index 0000000000..07b18c0a92 --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/RemoveTerrainTypeIsWaterFlag.cs @@ -0,0 +1,37 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) + * 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; + +namespace OpenRA.Mods.Common.UpdateRules.Rules +{ + public class RemoveTerrainTypeIsWaterFlag : UpdateRule + { + public override string Name { get { return "Remove TerrainType IsWater flag"; } } + public override string Description + { + get + { + return "The IsWater flag on terrain type definitions has been unused for some time.\n" + + "This flag has now been removed from the tileset yaml."; + } + } + + public override IEnumerable UpdateTilesetNode(ModData modData, MiniYamlNode tilesetNode) + { + if (tilesetNode.Key == "Terrain") + foreach (var type in tilesetNode.Value.Nodes) + type.Value.Nodes.RemoveAll(n => n.Key == "IsWater"); + + yield break; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/RemoveWeaponScanRadius.cs b/OpenRA.Mods.Common/UpdateRules/Rules/RemoveWeaponScanRadius.cs new file mode 100644 index 0000000000..a1b70aa6c5 --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/RemoveWeaponScanRadius.cs @@ -0,0 +1,53 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) + * 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; + +namespace OpenRA.Mods.Common.UpdateRules.Rules +{ + public class RemoveWeaponScanRadius : UpdateRule + { + public override string Name { get { return "Remove Weapon ScanRadius parameters"; } } + public override string Description + { + get + { + return "The *ScanRadius parameters have been removed from weapon projectiles and warheads.\n" + + "These values are now automatically determined by the engine.\n" + + "CreateEffect.ImpactActors: False has been added to replace VictimScanRadius: 0"; + } + } + + public override IEnumerable UpdateWeaponNode(ModData modData, MiniYamlNode weaponNode) + { + foreach (var node in weaponNode.ChildrenMatching("Warhead")) + { + if (node.Value.Value == "CreateEffect") + { + var victimScanRadius = node.LastChildMatching("VictimScanRadius"); + if (victimScanRadius != null && victimScanRadius.NodeValue() == 0) + node.AddNode("ImpactActors", "false"); + node.RemoveNodes("VictimScanRadius"); + } + } + + var projectile = weaponNode.LastChildMatching("Projectile"); + if (projectile != null) + { + projectile.RemoveNodes("BounceBlockerScanRadius"); + projectile.RemoveNodes("BlockerScanRadius"); + projectile.RemoveNodes("AreaVictimScanRadius"); + } + + yield break; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/RemoveWithReloadingSpriteTurret.cs b/OpenRA.Mods.Common/UpdateRules/Rules/RemoveWithReloadingSpriteTurret.cs new file mode 100644 index 0000000000..273b94fa5d --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/RemoveWithReloadingSpriteTurret.cs @@ -0,0 +1,40 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) + * 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; + +namespace OpenRA.Mods.Common.UpdateRules.Rules +{ + public class RemoveWithReloadingSpriteTurret : UpdateRule + { + public override string Name { get { return "Remove WithReloadingSpriteTurret trait"; } } + public override string Description + { + get + { + return "WithReloadingSpriteTurret has been superseded by conditions.\n" + + "The trait is switched for with WithSpriteTurret.\n"; + } + } + + public override IEnumerable UpdateActorNode(ModData modData, MiniYamlNode actorNode) + { + foreach (var turret in actorNode.ChildrenMatching("WithReloadingSpriteTurret")) + { + turret.RenameKeyPreservingSuffix("WithSpriteTurret"); + yield return turret.Location.ToString() + ": WithReloadingSpriteTurret has been replaced by WithSpriteTurret.\n" + + "You should use AmmoPool.AmmoConditions to switch turret type when reloading to restore the previous behaviour."; + } + + yield break; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/RenameWormSpawner.cs b/OpenRA.Mods.Common/UpdateRules/Rules/RenameWormSpawner.cs new file mode 100644 index 0000000000..86b36ba610 --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/RenameWormSpawner.cs @@ -0,0 +1,44 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) + * 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; + +namespace OpenRA.Mods.Common.UpdateRules.Rules +{ + public class RenameWormSpawner : UpdateRule + { + public override string Name { get { return "WormSpawner renamed and generalized to ActorSpawner"; } } + public override string Description + { + get + { + return "The D2k-specific WormSpawner trait was renamed to ActorSpawner,\n" + + "generalized, and moved into the common mod code."; + } + } + + public override IEnumerable UpdateActorNode(ModData modData, MiniYamlNode actorNode) + { + foreach (var spawner in actorNode.ChildrenMatching("WormSpawner")) + spawner.RenameKeyPreservingSuffix("ActorSpawner"); + + foreach (var manager in actorNode.ChildrenMatching("WormManager")) + { + manager.RenameKeyPreservingSuffix("ActorSpawnManager"); + var signature = manager.LastChildMatching("WormSignature"); + if (signature != null) + signature.RenameKeyPreservingSuffix("Actors"); + } + + yield break; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/SplitTurretAimAnimation.cs b/OpenRA.Mods.Common/UpdateRules/Rules/SplitTurretAimAnimation.cs new file mode 100644 index 0000000000..adcfb12c5c --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/SplitTurretAimAnimation.cs @@ -0,0 +1,91 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) + * 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; + +namespace OpenRA.Mods.Common.UpdateRules.Rules +{ + public class SplitTurretAimAnimation : UpdateRule + { + public override string Name { get { return "Introduce WithTurretAimAnimation trait"; } } + public override string Description + { + get + { + return "WithSpriteTurret.AimSequence and WithTurretAttackAnimation.AimSequence\n" + + "have been split into a new WithTurretAimAnimation trait."; + } + } + + public override IEnumerable UpdateActorNode(ModData modData, MiniYamlNode actorNode) + { + var turretAttack = actorNode.LastChildMatching("WithTurretAttackAnimation"); + if (turretAttack != null) + { + var attackSequence = turretAttack.LastChildMatching("AttackSequence"); + var aimSequence = turretAttack.LastChildMatching("AimSequence"); + + // If only AimSequence is null, just rename AttackSequence to Sequence (ReloadPrefix is very unlikely to be defined in that case). + // If only AttackSequence is null, just rename the trait and property (the delay properties will likely be undefined). + // If both aren't null, split/copy everything relevant to the new WithTurretAimAnimation. + // If both are null (extremely unlikely), do nothing. + if (attackSequence == null && aimSequence != null) + { + turretAttack.RenameKeyPreservingSuffix("WithTurretAimAnimation"); + aimSequence.RenameKeyPreservingSuffix("Sequence"); + } + else if (attackSequence != null && aimSequence == null) + attackSequence.RenameKeyPreservingSuffix("Sequence"); + else if (attackSequence != null && aimSequence != null) + { + var turretAim = new MiniYamlNode("WithTurretAimAnimation", ""); + aimSequence.RenameKeyPreservingSuffix("Sequence"); + turretAim.Value.Nodes.Add(aimSequence); + turretAttack.Value.Nodes.Remove(aimSequence); + + var reloadPrefix = turretAttack.LastChildMatching("ReloadPrefix"); + var turret = turretAttack.LastChildMatching("Turret"); + var armament = turretAttack.LastChildMatching("Armament"); + if (reloadPrefix != null) + { + turretAim.Value.Nodes.Add(reloadPrefix); + turretAttack.Value.Nodes.Remove(reloadPrefix); + } + + if (turret != null) + turretAim.Value.Nodes.Add(turret); + if (armament != null) + turretAim.Value.Nodes.Add(armament); + + attackSequence.RenameKeyPreservingSuffix("Sequence"); + actorNode.Value.Nodes.Add(turretAim); + } + } + + var spriteTurret = actorNode.LastChildMatching("WithSpriteTurret"); + if (spriteTurret != null) + { + var aimSequence = spriteTurret.Value.Nodes.FirstOrDefault(n => n.Key == "AimSequence"); + if (aimSequence != null) + { + var aimAnim = new MiniYamlNode("WithTurretAimAnimation", ""); + aimSequence.RenameKeyPreservingSuffix("Sequence"); + aimAnim.Value.Nodes.Add(aimSequence); + spriteTurret.Value.Nodes.Remove(aimSequence); + actorNode.Value.Nodes.Add(aimAnim); + } + } + + yield break; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs new file mode 100644 index 0000000000..4e83d69d5b --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs @@ -0,0 +1,95 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) + * 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.Diagnostics.CodeAnalysis; +using System.Linq; +using OpenRA.Mods.Common.UpdateRules.Rules; + +namespace OpenRA.Mods.Common.UpdateRules +{ + public class UpdatePath + { + // Define known update paths from stable tags to the current bleed tip + // + // This file should be maintained separately on prep branches vs bleed. + // The bleed version of this file should ignore the presence of the prep branch + // and list rules from the playtest that forked the prep branch and current bleed. + // The prep branch should maintain its own list of rules along the prep branch + // until the eventual final release. + // + // When a final release has been tagged the update paths from the prep branch + // can be merged back into bleed by replacing the forking-playtest-to-bleed path + // with the prep playtest-to-playtest-to-release paths and finally a new/modified + // release-to-bleed path. + [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:ParameterMustNotSpanMultipleLines", + Justification = "Extracting update lists to temporary variables obfuscates the definitions.")] + static readonly UpdatePath[] Paths = + { + new UpdatePath("release-20180218", "release-20180307", new UpdateRule[0]), + + new UpdatePath("release-20180307", new UpdateRule[] + { + // Bleed only changes here + new RemoveTerrainTypeIsWaterFlag(), + new RemoveWeaponScanRadius(), + new SplitTurretAimAnimation(), + new DefineSoundDefaults(), + new RenameWormSpawner(), + new RemoveWithReloadingSpriteTurret() + }) + }; + + public static IEnumerable FromSource(ObjectCreator objectCreator, string source) + { + // Use reflection to identify types + var namedType = objectCreator.FindType(source); + if (namedType != null && namedType.IsSubclassOf(typeof(UpdateRule))) + return new[] { (UpdateRule)objectCreator.CreateBasic(namedType) }; + + var namedPath = Paths.FirstOrDefault(p => p.source == source); + return namedPath != null ? namedPath.Rules : null; + } + + public static IEnumerable KnownPaths { get { return Paths.Select(p => p.source); } } + public static IEnumerable KnownRules(ObjectCreator objectCreator) + { + return objectCreator.GetTypesImplementing().Select(t => t.Name); + } + + readonly string source; + readonly string chainToSource; + readonly UpdateRule[] rules; + UpdatePath(string source, UpdateRule[] rules) + : this(source, null, rules) { } + + UpdatePath(string source, string chainToSource, UpdateRule[] rules) + { + this.source = source; + this.rules = rules; + this.chainToSource = chainToSource; + } + + IEnumerable Rules + { + get + { + if (chainToSource != null) + { + var chain = Paths.First(p => p.source == chainToSource); + return rules.Concat(chain.Rules); + } + + return rules; + } + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/UpdateRule.cs b/OpenRA.Mods.Common/UpdateRules/UpdateRule.cs new file mode 100644 index 0000000000..b24e549ba7 --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/UpdateRule.cs @@ -0,0 +1,34 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) + * 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; + +namespace OpenRA.Mods.Common.UpdateRules +{ + public abstract class UpdateRule + { + public abstract string Name { get; } + public abstract string Description { get; } + + /// Defines a transformation that is run on each top-level node in a yaml file set. + /// An enumerable of manual steps to be run by the user + public delegate IEnumerable TopLevelNodeTransform(ModData modData, MiniYamlNode node); + + public virtual IEnumerable UpdateActorNode(ModData modData, MiniYamlNode actorNode) { yield break; } + public virtual IEnumerable UpdateWeaponNode(ModData modData, MiniYamlNode weaponNode) { yield break; } + public virtual IEnumerable UpdateTilesetNode(ModData modData, MiniYamlNode tilesetNode) { yield break; } + + public virtual IEnumerable BeforeUpdate(ModData modData) { yield break; } + public virtual IEnumerable AfterUpdate(ModData modData) { yield break; } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/UpdateUtils.cs b/OpenRA.Mods.Common/UpdateRules/UpdateUtils.cs new file mode 100644 index 0000000000..4a30788a56 --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/UpdateUtils.cs @@ -0,0 +1,196 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) + * 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>>; + + public static class UpdateUtils + { + static YamlFileSet LoadYaml(ModData modData, IEnumerable files) + { + var yaml = new YamlFileSet(); + foreach (var filename in files) + { + string name; + IReadOnlyPackage package; + if (!modData.ModFiles.TryGetPackageContaining(filename, out package, out name) || !(package is IReadWritePackage)) + { + Console.WriteLine("Failed to load file `{0}` for writing. It will not be updated.", filename); + continue; + } + + yaml.Add(Tuple.Create((IReadWritePackage)package, name, MiniYaml.FromStream(package.GetStream(name), name))); + } + + return yaml; + } + + static YamlFileSet LoadMapYaml(ModData modData, IReadWritePackage mapPackage, MiniYaml yaml) + { + var fileSet = new YamlFileSet() + { + Tuple.Create>(null, "map.yaml", yaml.Nodes) + }; + + var files = FieldLoader.GetValue("value", yaml.Value); + foreach (var filename in files) + { + if (!filename.Contains("|") && mapPackage.Contains(filename)) + fileSet.Add(Tuple.Create(mapPackage, filename, MiniYaml.FromStream(mapPackage.GetStream(filename)))); + else + fileSet.AddRange(LoadYaml(modData, new[] { filename })); + } + + return fileSet; + } + + public static List UpdateMap(ModData modData, IReadWritePackage mapPackage, UpdateRule rule, out YamlFileSet files) + { + var manualSteps = new List(); + + var mapStream = mapPackage.GetStream("map.yaml"); + if (mapStream == null) + { + // Not a valid map + files = new YamlFileSet(); + return manualSteps; + } + + var yaml = new MiniYaml(null, MiniYaml.FromStream(mapStream, mapPackage.Name)); + files = new YamlFileSet() { Tuple.Create(mapPackage, "map.yaml", yaml.Nodes) }; + + manualSteps.AddRange(rule.BeforeUpdate(modData)); + + var mapRulesNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Rules"); + if (mapRulesNode != null) + { + var mapRules = LoadMapYaml(modData, mapPackage, mapRulesNode.Value); + manualSteps.AddRange(ApplyTopLevelTransform(modData, mapRules, rule.UpdateActorNode)); + files.AddRange(mapRules); + } + + var mapWeaponsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Weapons"); + if (mapWeaponsNode != null) + { + var mapWeapons = LoadMapYaml(modData, mapPackage, mapWeaponsNode.Value); + manualSteps.AddRange(ApplyTopLevelTransform(modData, mapWeapons, rule.UpdateWeaponNode)); + files.AddRange(mapWeapons); + } + + manualSteps.AddRange(rule.AfterUpdate(modData)); + + return manualSteps; + } + + public static List UpdateMod(ModData modData, UpdateRule rule, out YamlFileSet files) + { + var manualSteps = new List(); + var modRules = LoadYaml(modData, modData.Manifest.Rules); + var modWeapons = LoadYaml(modData, modData.Manifest.Weapons); + var modTilesets = LoadYaml(modData, modData.Manifest.TileSets); + var modChromeLayout = LoadYaml(modData, modData.Manifest.ChromeLayout); + + manualSteps.AddRange(rule.BeforeUpdate(modData)); + manualSteps.AddRange(ApplyTopLevelTransform(modData, modRules, rule.UpdateActorNode)); + manualSteps.AddRange(ApplyTopLevelTransform(modData, modWeapons, rule.UpdateWeaponNode)); + manualSteps.AddRange(ApplyTopLevelTransform(modData, modTilesets, rule.UpdateTilesetNode)); + manualSteps.AddRange(rule.AfterUpdate(modData)); + + files = modRules.ToList(); + files.AddRange(modWeapons); + files.AddRange(modTilesets); + files.AddRange(modChromeLayout); + + return manualSteps; + } + + static IEnumerable ApplyTopLevelTransform(ModData modData, YamlFileSet files, UpdateRule.TopLevelNodeTransform transform) + { + if (transform == null) + yield break; + + foreach (var file in files) + foreach (var node in file.Item3) + foreach (var manualStep in transform(modData, node)) + yield return manualStep; + } + + public static string FormatMessageList(IEnumerable messages, int indent = 0) + { + var prefix = string.Concat(Enumerable.Repeat(" ", indent)); + return string.Concat(messages.Select(m => prefix + " * {0}\n".F(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) + file.Item1.Update(file.Item2, Encoding.ASCII.GetBytes(file.Item3.WriteToString())); + } + + /// Renames a yaml key preserving any @suffix + public static void RenameKeyPreservingSuffix(this MiniYamlNode node, string newKey) + { + var split = node.Key.IndexOf("@", StringComparison.Ordinal); + if (split == -1) + node.Key = newKey; + else + node.Key = newKey + node.Key.Substring(split); + } + + public static T NodeValue(this MiniYamlNode node) + { + return FieldLoader.GetValue(node.Key, node.Value.Value); + } + + public static void AddNode(this MiniYamlNode node, string key, object value) + { + node.Value.Nodes.Add(new MiniYamlNode(key, FieldSaver.FormatValue(value))); + } + + /// Removes children with keys equal to [match] or [match]@[arbitrary suffix] + public static int RemoveNodes(this MiniYamlNode node, string match) + { + return node.Value.Nodes.RemoveAll(n => n.KeyMatches(match)); + } + + /// Returns true if the node is of the form or @arbitrary + public static bool KeyMatches(this MiniYamlNode node, string match) + { + if (node.Key == match) + return true; + + var atPosition = node.Key.IndexOf('@'); + return atPosition > 0 && node.Key.Substring(0, atPosition) == match; + } + + /// Returns children with keys equal to [match] or [match]@[arbitrary suffix] + public static IEnumerable ChildrenMatching(this MiniYamlNode node, string match) + { + return node.Value.Nodes.Where(n => n.KeyMatches(match)); + } + + public static MiniYamlNode LastChildMatching(this MiniYamlNode node, string match) + { + return node.ChildrenMatching(match).LastOrDefault(); + } + } +} diff --git a/OpenRA.Mods.Common/UtilityCommands/UpdateMapCommand.cs b/OpenRA.Mods.Common/UtilityCommands/UpdateMapCommand.cs new file mode 100644 index 0000000000..4a8e41fe77 --- /dev/null +++ b/OpenRA.Mods.Common/UtilityCommands/UpdateMapCommand.cs @@ -0,0 +1,121 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) + * 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.IO; +using System.Linq; +using OpenRA.FileSystem; +using OpenRA.Mods.Common.UpdateRules; + +namespace OpenRA.Mods.Common.UtilityCommands +{ + using YamlFileSet = List>>; + + class UpdateMapCommand : IUtilityCommand + { + string IUtilityCommand.Name { get { return "--update-map"; } } + + bool IUtilityCommand.ValidateArguments(string[] args) + { + return args.Length >= 2; + } + + [Desc("MAP SOURCE [--detailed] [--apply]", "Updates a map to the latest engine version. SOURCE is either a known tag or the name of an update rule.")] + void IUtilityCommand.Run(Utility utility, string[] args) + { + // HACK: The engine code assumes that Game.modData is set. + var modData = Game.ModData = utility.ModData; + + // HACK: We know that maps can only be oramap or folders, which are ReadWrite + var package = new Folder(".").OpenPackage(args[1], modData.ModFiles) as IReadWritePackage; + if (package == null) + throw new FileNotFoundException(args[1]); + + IEnumerable rules = null; + if (args.Length > 1) + rules = UpdatePath.FromSource(modData.ObjectCreator, args[2]); + + if (rules == null) + { + Console.WriteLine("--update-map MAP SOURCE [--detailed] [--apply]"); + + if (args.Length > 2) + Console.WriteLine("Unknown source: " + args[2]); + + Console.WriteLine("Valid sources are:"); + + // Print known tags + Console.WriteLine(" Update Paths:"); + foreach (var p in UpdatePath.KnownPaths) + Console.WriteLine(" " + p); + + // Print known rules + Console.WriteLine(" Individual Rules:"); + foreach (var r in UpdatePath.KnownRules(modData.ObjectCreator)) + Console.WriteLine(" " + r); + + return; + } + + if (args.Contains("--apply")) + ApplyRules(modData, package, rules); + else + UpdateModCommand.PrintSummary(rules, args.Contains("--detailed")); + } + + static void ApplyRules(ModData modData, IReadWritePackage mapPackage, IEnumerable rules) + { + foreach (var rule in rules) + { + Console.WriteLine("{0}: {1}", rule.GetType().Name, rule.Name); + var mapFiles = new YamlFileSet(); + var manualSteps = new List(); + + Console.Write(" Updating map... "); + + try + { + manualSteps = UpdateUtils.UpdateMap(modData, mapPackage, rule, out mapFiles); + } + catch (Exception ex) + { + Console.WriteLine("FAILED"); + + Console.WriteLine(); + Console.WriteLine(" The automated changes for this rule were not applied because of an error."); + Console.WriteLine(" After the issue reported below is resolved you should run the updater"); + Console.WriteLine(" with SOURCE set to {0} to retry these changes", rule.GetType().Name); + Console.WriteLine(); + Console.WriteLine(" The exception reported was:"); + Console.WriteLine(" " + ex.ToString().Replace("\n", "\n ")); + continue; + } + + // Files are saved after each successful automated rule update + mapFiles.Save(); + Console.WriteLine("COMPLETE"); + + if (manualSteps.Any()) + { + Console.WriteLine(" Manual changes are required to complete this update:"); + foreach (var manualStep in manualSteps) + Console.WriteLine(" * " + manualStep.Replace("\n", "\n ")); + } + + Console.WriteLine(); + } + + Console.WriteLine("Semi-automated update complete."); + Console.WriteLine("Please review the messages above for any manual actions that must be applied."); + } + } +} diff --git a/OpenRA.Mods.Common/UtilityCommands/UpdateModCommand.cs b/OpenRA.Mods.Common/UtilityCommands/UpdateModCommand.cs new file mode 100644 index 0000000000..4b64c28ab3 --- /dev/null +++ b/OpenRA.Mods.Common/UtilityCommands/UpdateModCommand.cs @@ -0,0 +1,206 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) + * 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 OpenRA.FileSystem; +using OpenRA.Mods.Common.UpdateRules; + +namespace OpenRA.Mods.Common.UtilityCommands +{ + using YamlFileSet = List>>; + + class UpdateModCommand : IUtilityCommand + { + string IUtilityCommand.Name { get { return "--update-mod"; } } + + bool IUtilityCommand.ValidateArguments(string[] args) { return true; } + + [Desc("SOURCE [--detailed] [--apply] [--skip-maps]", "Updates a mod to the latest version. SOURCE is either a known tag or the name of an update rule.")] + void IUtilityCommand.Run(Utility utility, string[] args) + { + // HACK: The engine code assumes that Game.modData is set. + var modData = Game.ModData = utility.ModData; + + IEnumerable rules = null; + if (args.Length > 1) + rules = UpdatePath.FromSource(modData.ObjectCreator, args[1]); + + if (rules == null) + { + Console.WriteLine("--update-mod SOURCE [--detailed] [--apply] [--skip-maps]"); + + if (args.Length > 1) + Console.WriteLine("Unknown source: " + args[1]); + + Console.WriteLine("Valid sources are:"); + + // Print known tags + Console.WriteLine(" Update Paths:"); + foreach (var p in UpdatePath.KnownPaths) + Console.WriteLine(" " + p); + + // Print known rules + Console.WriteLine(" Individual Rules:"); + foreach (var r in UpdatePath.KnownRules(modData.ObjectCreator)) + Console.WriteLine(" " + r); + + return; + } + + if (args.Contains("--apply")) + { + if (!args.Contains("--yes")) + { + Console.WriteLine("WARNING: This command will automatically rewrite your mod rules."); + Console.WriteLine("Side effects of this command may include changing the whitespace to "); + Console.WriteLine("match the default conventions, and any yaml comments will be removed."); + Console.WriteLine(); + Console.WriteLine("We strongly recommend that you have a backup of your mod rules, and "); + Console.WriteLine("for best results, to use a Git client to review the line-by-line "); + Console.WriteLine("changes and discard any unwanted side effects."); + Console.WriteLine(); + Console.Write("Press y to continue, or any other key to cancel: "); + + var confirmKey = Console.ReadKey().KeyChar; + Console.WriteLine(); + + if (confirmKey != 'y') + { + Console.WriteLine("Update cancelled."); + return; + } + } + + ApplyRules(modData, rules, args.Contains("--skip-maps")); + } + else + PrintSummary(rules, args.Contains("--detailed")); + } + + public static void PrintSummary(IEnumerable rules, bool detailed) + { + var count = rules.Count(); + if (count == 1) + Console.WriteLine("Found 1 API change:"); + else + Console.WriteLine("Found {0} API changes:", count); + + foreach (var rule in rules) + { + Console.WriteLine(" * {0}: {1}", rule.GetType().Name, rule.Name); + if (detailed) + { + Console.WriteLine(" " + rule.Description.Replace("\n", "\n ")); + Console.WriteLine(); + } + } + + if (!detailed) + { + Console.WriteLine(); + Console.WriteLine("Run this command with the --detailed flag to view detailed information on each change."); + } + + Console.WriteLine("Run this command with the --apply flag to apply the update rules."); + } + + static void ApplyRules(ModData modData, IEnumerable rules, bool skipMaps) + { + Console.WriteLine(); + foreach (var rule in rules) + { + var manualSteps = new List(); + var allFiles = new YamlFileSet(); + + Console.WriteLine("{0}: {1}", rule.GetType().Name, rule.Name); + + try + { + Console.Write(" Updating mod... "); + manualSteps.AddRange(UpdateUtils.UpdateMod(modData, rule, out allFiles)); + Console.WriteLine("COMPLETE"); + } + catch (Exception ex) + { + Console.WriteLine("FAILED"); + + Console.WriteLine(); + Console.WriteLine(" The automated changes for this rule were not applied because of an error."); + Console.WriteLine(" After the issue reported below is resolved you should run the updater"); + Console.WriteLine(" with SOURCE set to {0} to retry these changes", rule.GetType().Name); + Console.WriteLine(); + Console.WriteLine(" The exception reported was:"); + Console.WriteLine(" " + ex.ToString().Replace("\n", "\n ")); + + continue; + } + + Console.Write(" Updating system maps... "); + if (!skipMaps) + { + var mapsFailed = false; + foreach (var package in modData.MapCache.EnumerateMapPackagesWithoutCaching()) + { + try + { + YamlFileSet mapFiles; + var mapSteps = UpdateUtils.UpdateMap(modData, package, rule, out mapFiles); + allFiles.AddRange(mapFiles); + + if (mapSteps.Any()) + manualSteps.Add("Map: " + package.Name + ":\n" + UpdateUtils.FormatMessageList(mapSteps)); + } + catch (Exception ex) + { + Console.WriteLine("FAILED"); + + Console.WriteLine(); + Console.WriteLine(" The automated changes for this rule were not applied because of an error."); + Console.WriteLine(" After the issue reported below is resolved you should run the updater"); + Console.WriteLine(" with SOURCE set to {0} to retry these changes", rule.GetType().Name); + Console.WriteLine(); + Console.WriteLine(" The map that caused the error was:"); + Console.WriteLine(" " + package.Name); + Console.WriteLine(); + Console.WriteLine(" The exception reported was:"); + Console.WriteLine(" " + ex.ToString().Replace("\n", "\n ")); + mapsFailed = true; + break; + } + } + + if (mapsFailed) + continue; + + Console.WriteLine("COMPLETE"); + } + else + Console.WriteLine("SKIPPED"); + + // Files are saved after each successful automated rule update + allFiles.Save(); + + if (manualSteps.Any()) + { + Console.WriteLine(" Manual changes are required to complete this update:"); + Console.WriteLine(UpdateUtils.FormatMessageList(manualSteps, 1)); + } + + Console.WriteLine(); + } + + Console.WriteLine("Semi-automated update complete."); + Console.WriteLine("Please review the messages above for any manual actions that must be applied."); + } + } +} diff --git a/OpenRA.Mods.Common/UtilityCommands/UpgradeMapCommand.cs b/OpenRA.Mods.Common/UtilityCommands/UpgradeMapCommand.cs deleted file mode 100644 index 2858b8402d..0000000000 --- a/OpenRA.Mods.Common/UtilityCommands/UpgradeMapCommand.cs +++ /dev/null @@ -1,122 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) - * 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.IO; -using System.Linq; -using System.Text; -using OpenRA.FileSystem; - -namespace OpenRA.Mods.Common.UtilityCommands -{ - class UpgradeMapCommand : IUtilityCommand - { - string IUtilityCommand.Name { get { return "--upgrade-map"; } } - - bool IUtilityCommand.ValidateArguments(string[] args) - { - return args.Length >= 3; - } - - delegate void UpgradeAction(ModData modData, int engineVersion, ref List nodes, MiniYamlNode parent, int depth); - - static Stream Open(string filename, IReadOnlyPackage package, IReadOnlyFileSystem fallback) - { - // Explicit package paths never refer to a map - if (!filename.Contains("|") && package.Contains(filename)) - return package.GetStream(filename); - - return fallback.Open(filename); - } - - static void ProcessYaml(ModData modData, IReadOnlyPackage package, MiniYaml yaml, int engineDate, UpgradeAction processYaml) - { - if (yaml == null) - return; - - if (yaml.Value != null) - { - var files = FieldLoader.GetValue("value", yaml.Value); - foreach (var filename in files) - { - var fileNodes = MiniYaml.FromStream(Open(filename, package, modData.DefaultFileSystem), filename); - processYaml(modData, engineDate, ref fileNodes, null, 0); - - // HACK: Obtain the writable save path using knowledge of the underlying filesystem workings - var packagePath = filename; - var p = package; - if (filename.Contains("|")) - modData.DefaultFileSystem.TryGetPackageContaining(filename, out p, out packagePath); - - ((IReadWritePackage)p).Update(packagePath, Encoding.ASCII.GetBytes(fileNodes.WriteToString())); - } - } - - processYaml(modData, engineDate, ref yaml.Nodes, null, 1); - } - - public static void UpgradeMap(ModData modData, IReadWritePackage package, int engineDate) - { - UpgradeRules.UpgradeMapFormat(modData, package); - - if (engineDate < UpgradeRules.MinimumSupportedVersion) - { - Console.WriteLine("Unsupported engine version. Use the release-{0} utility to update to that version, and then try again", - UpgradeRules.MinimumSupportedVersion); - return; - } - - var mapStream = package.GetStream("map.yaml"); - if (mapStream == null) - return; - - var yaml = new MiniYaml(null, MiniYaml.FromStream(mapStream, package.Name)); - - var rules = yaml.Nodes.FirstOrDefault(n => n.Key == "Rules"); - if (rules != null) - ProcessYaml(modData, package, rules.Value, engineDate, UpgradeRules.UpgradeActorRules); - - var weapons = yaml.Nodes.FirstOrDefault(n => n.Key == "Weapons"); - if (weapons != null) - ProcessYaml(modData, package, weapons.Value, engineDate, UpgradeRules.UpgradeWeaponRules); - - var sequences = yaml.Nodes.FirstOrDefault(n => n.Key == "Sequences"); - if (sequences != null) - ProcessYaml(modData, package, sequences.Value, engineDate, UpgradeRules.UpgradeSequences); - - var players = yaml.Nodes.FirstOrDefault(n => n.Key == "Players"); - if (players != null) - UpgradeRules.UpgradePlayers(modData, engineDate, ref players.Value.Nodes, null, 0); - - var actors = yaml.Nodes.FirstOrDefault(n => n.Key == "Actors"); - if (actors != null) - UpgradeRules.UpgradeActors(modData, engineDate, ref actors.Value.Nodes, null, 0); - - package.Update("map.yaml", Encoding.UTF8.GetBytes(yaml.Nodes.WriteToString())); - } - - [Desc("MAP", "OLDENGINE", "Upgrade map rules to the latest engine version.")] - void IUtilityCommand.Run(Utility utility, string[] args) - { - // HACK: The engine code assumes that Game.modData is set. - var modData = Game.ModData = utility.ModData; - - // HACK: We know that maps can only be oramap or folders, which are ReadWrite - var package = new Folder(".").OpenPackage(args[1], modData.ModFiles) as IReadWritePackage; - if (package == null) - throw new FileNotFoundException(args[1]); - - var engineDate = Exts.ParseIntegerInvariant(args[2]); - UpgradeMap(modData, package, engineDate); - } - } -} diff --git a/OpenRA.Mods.Common/UtilityCommands/UpgradeModCommand.cs b/OpenRA.Mods.Common/UtilityCommands/UpgradeModCommand.cs deleted file mode 100644 index 49fb281ce5..0000000000 --- a/OpenRA.Mods.Common/UtilityCommands/UpgradeModCommand.cs +++ /dev/null @@ -1,95 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) - * 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.IO; -using System.Linq; -using OpenRA.FileSystem; - -namespace OpenRA.Mods.Common.UtilityCommands -{ - class UpgradeModCommand : IUtilityCommand - { - string IUtilityCommand.Name { get { return "--upgrade-mod"; } } - - bool IUtilityCommand.ValidateArguments(string[] args) - { - return args.Length >= 2; - } - - delegate void UpgradeAction(ModData modData, int engineVersion, ref List nodes, MiniYamlNode parent, int depth); - - void ProcessYaml(string type, IEnumerable files, ModData modData, int engineDate, UpgradeAction processFile) - { - Console.WriteLine("Processing {0}:", type); - foreach (var filename in files) - { - Console.WriteLine("\t" + filename); - string name; - IReadOnlyPackage package; - if (!modData.ModFiles.TryGetPackageContaining(filename, out package, out name) || !(package is Folder)) - { - Console.WriteLine("\t\tFile cannot be opened for writing! Ignoring..."); - continue; - } - - var yaml = MiniYaml.FromStream(package.GetStream(name), name); - processFile(modData, engineDate, ref yaml, null, 0); - - // Generate the on-disk path - var path = Path.Combine(package.Name, name); - using (var file = new StreamWriter(path)) - file.Write(yaml.WriteToString()); - } - } - - [Desc("OLDENGINE", "Upgrade mod rules to the latest engine version.")] - void IUtilityCommand.Run(Utility utility, string[] args) - { - // HACK: The engine code assumes that Game.modData is set. - var modData = Game.ModData = utility.ModData; - - var engineDate = Exts.ParseIntegerInvariant(args[1]); - if (engineDate < UpgradeRules.MinimumSupportedVersion) - { - Console.WriteLine("Unsupported engine version. Use the release-{0} utility to update to that version, and then try again", - UpgradeRules.MinimumSupportedVersion); - return; - } - - ProcessYaml("Rules", modData.Manifest.Rules, modData, engineDate, UpgradeRules.UpgradeActorRules); - ProcessYaml("Weapons", modData.Manifest.Weapons, modData, engineDate, UpgradeRules.UpgradeWeaponRules); - ProcessYaml("Sequences", modData.Manifest.Sequences, modData, engineDate, UpgradeRules.UpgradeSequences); - ProcessYaml("Tilesets", modData.Manifest.TileSets, modData, engineDate, UpgradeRules.UpgradeTileset); - ProcessYaml("Cursors", modData.Manifest.Cursors, modData, engineDate, UpgradeRules.UpgradeCursors); - ProcessYaml("Chrome Metrics", modData.Manifest.ChromeMetrics, modData, engineDate, UpgradeRules.UpgradeChromeMetrics); - ProcessYaml("Chrome Layout", modData.Manifest.ChromeLayout, modData, engineDate, UpgradeRules.UpgradeChromeLayout); - - // The map cache won't be valid if there was a map format upgrade, so walk the map packages manually - // Only upgrade system maps - user maps must be updated manually using --upgrade-map - Console.WriteLine("Processing System Maps:"); - foreach (var package in modData.MapCache.EnumerateMapPackagesWithoutCaching()) - { - try - { - Console.WriteLine(package.Name); - UpgradeMapCommand.UpgradeMap(modData, package, engineDate); - } - catch (Exception e) - { - Console.WriteLine("Failed to upgrade map {0}", package.Name); - Console.WriteLine("Error was: {0}", e.ToString()); - } - } - } - } -} diff --git a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs b/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs deleted file mode 100644 index 98b74414d4..0000000000 --- a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs +++ /dev/null @@ -1,1037 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) - * 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.IO; -using System.Linq; -using System.Text; -using OpenRA.FileSystem; -using OpenRA.Traits; - -namespace OpenRA.Mods.Common.UtilityCommands -{ - static class UpgradeRules - { - public const int MinimumSupportedVersion = 20171014; - - static void RenameNodeKey(MiniYamlNode node, string key) - { - if (node == null) - return; - - var parts = node.Key.Split('@'); - node.Key = key; - if (parts.Length > 1) - node.Key += "@" + parts[1]; - } - - internal static string MultiplyByFactor(int oldValue, int factor) - { - oldValue = oldValue * factor; - return oldValue.ToString(); - } - - internal static void UpgradeActorRules(ModData modData, int engineVersion, ref List nodes, MiniYamlNode parent, int depth) - { - var addNodes = new List(); - - foreach (var node in nodes) - { - // Replace Mobile.OnRails hack with dedicated TDGunboat traits in Mods.Cnc - if (engineVersion < 20171015) - { - var mobile = node.Value.Nodes.FirstOrDefault(n => n.Key == "Mobile"); - if (mobile != null) - { - var onRailsNode = mobile.Value.Nodes.FirstOrDefault(n => n.Key == "OnRails"); - var onRails = onRailsNode != null ? FieldLoader.GetValue("OnRails", onRailsNode.Value.Value) : false; - if (onRails) - { - var speed = mobile.Value.Nodes.FirstOrDefault(n => n.Key == "Speed"); - var initFacing = mobile.Value.Nodes.FirstOrDefault(n => n.Key == "InitialFacing"); - var previewFacing = mobile.Value.Nodes.FirstOrDefault(n => n.Key == "PreviewFacing"); - var tdGunboat = new MiniYamlNode("TDGunboat", ""); - if (speed != null) - tdGunboat.Value.Nodes.Add(speed); - if (initFacing != null) - tdGunboat.Value.Nodes.Add(initFacing); - if (previewFacing != null) - tdGunboat.Value.Nodes.Add(previewFacing); - - node.Value.Nodes.Add(tdGunboat); - - var attackTurreted = node.Value.Nodes.FirstOrDefault(n => n.Key.StartsWith("AttackTurreted", StringComparison.Ordinal)); - if (attackTurreted != null) - RenameNodeKey(attackTurreted, "AttackTDGunboatTurreted"); - - node.Value.Nodes.Remove(mobile); - } - } - } - - // Introduced TakeOffOnCreation and TakeOffOnResupply booleans to aircraft - if (engineVersion < 20171015) - { - if (node.Key.StartsWith("Aircraft", StringComparison.Ordinal)) - { - var canHover = node.Value.Nodes.FirstOrDefault(n => n.Key == "CanHover"); - var isHeli = canHover != null ? FieldLoader.GetValue("CanHover", canHover.Value.Value) : false; - if (isHeli) - { - Console.WriteLine("Helicopters taking off automatically while planes don't is no longer hardcoded."); - Console.WriteLine("Instead, this is controlled via the TakeOffOnResupply field."); - Console.WriteLine("Please check if your aircraft behave as intended or need manual adjustments."); - node.Value.Nodes.Add(new MiniYamlNode("TakeOffOnResupply", "true")); - } - - // Upgrade rule for setting VTOL to true for CanHover actors - if (isHeli) - node.Value.Nodes.Add(new MiniYamlNode("VTOL", "true")); - } - } - - // nuke launch animation is now it's own trait - if (engineVersion < 20171015) - { - if (depth == 1 && node.Key.StartsWith("NukePower", StringComparison.Ordinal)) - { - node.Value.Nodes.RemoveAll(n => n.Key == "ActivationSequence"); - addNodes.Add(new MiniYamlNode("WithNukeLaunchAnimation", new MiniYaml(""))); - } - } - - if (engineVersion < 20171015) - { - if (node.Key.StartsWith("WithTurretedAttackAnimation", StringComparison.Ordinal)) - RenameNodeKey(node, "WithTurretAttackAnimation"); - if (node.Key.StartsWith("WithTurretedSpriteBody", StringComparison.Ordinal)) - RenameNodeKey(node, "WithEmbeddedTurretSpriteBody"); - } - - if (engineVersion < 20171015) - { - if (node.Key.StartsWith("PlayerPaletteFromCurrentTileset", StringComparison.Ordinal)) - { - node.Value.Nodes.Add(new MiniYamlNode("Filename", "")); - node.Value.Nodes.Add(new MiniYamlNode("Tileset", "")); - RenameNodeKey(node, "PaletteFromFile"); - Console.WriteLine("The trait PlayerPaletteFromCurrentTileset has been removed. Use PaletteFromFile with a Tileset filter."); - } - } - - if (engineVersion < 20171021) - { - if (node.Key.StartsWith("Capturable", StringComparison.Ordinal) || node.Key.StartsWith("ExternalCapturable", StringComparison.Ordinal)) - { - // Type renamed to Types - var type = node.Value.Nodes.FirstOrDefault(n => n.Key == "Type"); - if (type != null) - RenameNodeKey(type, "Types"); - - // Allow(Allies|Neutral|Enemies) replaced with a ValidStances enum - var stance = Stance.Neutral | Stance.Enemy; - var allowAllies = node.Value.Nodes.FirstOrDefault(n => n.Key == "AllowAllies"); - if (allowAllies != null) - { - if (FieldLoader.GetValue("AllowAllies", allowAllies.Value.Value)) - stance |= Stance.Ally; - else - stance &= ~Stance.Ally; - - node.Value.Nodes.Remove(allowAllies); - } - - var allowNeutral = node.Value.Nodes.FirstOrDefault(n => n.Key == "AllowNeutral"); - if (allowNeutral != null) - { - if (FieldLoader.GetValue("AllowNeutral", allowNeutral.Value.Value)) - stance |= Stance.Neutral; - else - stance &= ~Stance.Neutral; - - node.Value.Nodes.Remove(allowNeutral); - } - - var allowEnemies = node.Value.Nodes.FirstOrDefault(n => n.Key == "AllowEnemies"); - if (allowEnemies != null) - { - if (FieldLoader.GetValue("AllowEnemies", allowEnemies.Value.Value)) - stance |= Stance.Enemy; - else - stance &= ~Stance.Enemy; - - node.Value.Nodes.Remove(allowEnemies); - } - - if (stance != (Stance.Neutral | Stance.Enemy)) - node.Value.Nodes.Add(new MiniYamlNode("ValidStances", stance.ToString())); - } - } - - // Self-reload properties were decoupled from AmmoPool to ReloadAmmoPool. - if (engineVersion < 20171104) - { - var poolNumber = 0; - var ammoPools = node.Value.Nodes.Where(n => n.Key.StartsWith("AmmoPool", StringComparison.Ordinal)); - foreach (var pool in ammoPools.ToList()) - { - var selfReloads = pool.Value.Nodes.FirstOrDefault(n => n.Key == "SelfReloads"); - if (selfReloads != null && FieldLoader.GetValue("SelfReloads", selfReloads.Value.Value)) - { - poolNumber++; - var name = pool.Value.Nodes.FirstOrDefault(n => n.Key == "Name"); - var selfReloadDelay = pool.Value.Nodes.FirstOrDefault(n => n.Key == "SelfReloadDelay"); - var reloadCount = pool.Value.Nodes.FirstOrDefault(n => n.Key == "ReloadCount"); - var reset = pool.Value.Nodes.FirstOrDefault(n => n.Key == "ResetOnFire"); - var rearmSound = pool.Value.Nodes.FirstOrDefault(n => n.Key == "RearmSound"); - var reloadOnCond = new MiniYamlNode("ReloadAmmoPool@" + poolNumber.ToString(), ""); - - if (name != null) - { - var ap = new MiniYamlNode("AmmoPool", name.Value.Value); - reloadOnCond.Value.Nodes.Add(ap); - } - - if (selfReloadDelay != null) - { - var rd = selfReloadDelay; - RenameNodeKey(rd, "Delay"); - reloadOnCond.Value.Nodes.Add(rd); - pool.Value.Nodes.Remove(selfReloads); - pool.Value.Nodes.Remove(selfReloadDelay); - } - - if (reloadCount != null) - { - var rc = reloadCount; - RenameNodeKey(rc, "Count"); - reloadOnCond.Value.Nodes.Add(rc); - pool.Value.Nodes.Remove(reloadCount); - } - - if (reset != null) - { - reloadOnCond.Value.Nodes.Add(reset); - pool.Value.Nodes.Remove(reset); - } - - if (rearmSound != null) - { - var rs = rearmSound; - RenameNodeKey(rs, "Sound"); - reloadOnCond.Value.Nodes.Add(rs); - pool.Value.Nodes.Remove(rearmSound); - } - - node.Value.Nodes.Add(reloadOnCond); - } - } - } - - // Armament.OutOfAmmo has been replaced by pausing on condition (usually provided by AmmoPool) - if (engineVersion < 20171104) - { - var reloadAmmoPool = node.Value.Nodes.FirstOrDefault(n => n.Key.StartsWith("ReloadAmmoPool", StringComparison.Ordinal)); - var armaments = node.Value.Nodes.Where(n => n.Key.StartsWith("Armament", StringComparison.Ordinal)); - var ammoPools = node.Value.Nodes.Where(n => n.Key.StartsWith("AmmoPool", StringComparison.Ordinal)); - - if (reloadAmmoPool == null && armaments.Any() && ammoPools.Any()) - { - foreach (var pool in ammoPools) - { - var nameNode = pool.Value.Nodes.FirstOrDefault(n => n.Key == "Armaments"); - var name = nameNode != null ? FieldLoader.GetValue("Armaments", nameNode.Value.Value) : "primary, secondary"; - var anyMatchingArmament = false; - var ammoNoAmmo = new MiniYamlNode("AmmoCondition", "ammo"); - var armNoAmmo = new MiniYamlNode("PauseOnCondition", "!ammo"); - - foreach (var arma in armaments) - { - var armaNameNode = arma.Value.Nodes.FirstOrDefault(n => n.Key == "Name"); - var armaName = armaNameNode != null ? FieldLoader.GetValue("Name", armaNameNode.Value.Value) : "primary"; - if (name.Contains(armaName)) - { - anyMatchingArmament = true; - arma.Value.Nodes.Add(armNoAmmo); - } - } - - if (anyMatchingArmament) - { - pool.Value.Nodes.Add(ammoNoAmmo); - Console.WriteLine("Aircraft returning to base is now triggered when all armaments are paused via condition."); - Console.WriteLine("Check if any of your actors with AmmoPools may need further changes."); - } - } - } - } - - if (engineVersion < 20171112) - { - // CanPowerDown now provides a condition instead of triggering Actor.Disabled - var canPowerDown = node.Value.Nodes.FirstOrDefault(n => n.Key.StartsWith("CanPowerDown", StringComparison.Ordinal)); - if (canPowerDown != null) - { - canPowerDown.Value.Nodes.Add(new MiniYamlNode("PowerdownCondition", "powerdown")); - - var image = canPowerDown.Value.Nodes.FirstOrDefault(n => n.Key == "IndicatorImage"); - var seq = canPowerDown.Value.Nodes.FirstOrDefault(n => n.Key == "IndicatorSequence"); - var pal = canPowerDown.Value.Nodes.FirstOrDefault(n => n.Key == "IndicatorPalette"); - var imageValue = image != null ? FieldLoader.GetValue("IndicatorImage", image.Value.Value) : "poweroff"; - var seqValue = seq != null ? FieldLoader.GetValue("IndicatorSequence", seq.Value.Value) : "offline"; - var palValue = pal != null ? FieldLoader.GetValue("IndicatorPalette", pal.Value.Value) : "chrome"; - - var indicator = new MiniYamlNode("WithDecoration@POWERDOWN", ""); - indicator.Value.Nodes.Add(new MiniYamlNode("Image", imageValue)); - indicator.Value.Nodes.Add(new MiniYamlNode("Sequence", seqValue)); - indicator.Value.Nodes.Add(new MiniYamlNode("Palette", palValue)); - indicator.Value.Nodes.Add(new MiniYamlNode("RequiresCondition", "powerdown")); - indicator.Value.Nodes.Add(new MiniYamlNode("ReferencePoint", "Center")); - - node.Value.Nodes.Add(indicator); - if (image != null) - canPowerDown.Value.Nodes.Remove(image); - if (seq != null) - canPowerDown.Value.Nodes.Remove(seq); - if (pal != null) - canPowerDown.Value.Nodes.Remove(pal); - - Console.WriteLine("CanPowerDown now provides a condition instead of disabling the actor directly."); - Console.WriteLine("Review your condition setup to make sure all relevant traits are disabled by that condition."); - Console.WriteLine("Look at the official mods if you need examples."); - } - - // RequiresPower has been replaced with GrantConditionOnPowerState. - var requiresPower = node.Value.Nodes.FirstOrDefault(n => n.Key == "RequiresPower"); - if (requiresPower != null) - { - requiresPower.Key = "GrantConditionOnPowerState@LOWPOWER"; - requiresPower.Value.Nodes.Add(new MiniYamlNode("Condition", "lowpower")); - requiresPower.Value.Nodes.Add(new MiniYamlNode("ValidPowerStates", "Low, Critical")); - - Console.WriteLine("RequiresPower has been replaced with GrantConditionOnPowerState."); - Console.WriteLine("As the name implies, this new trait toggles a condition depending on the power state."); - Console.WriteLine("Review your condition setup to make sure all relevant traits are disabled/enabled by that condition."); - Console.WriteLine("Possible PowerStates are: Normal (0 or positive), Low (negative but higher than 50% of required power) and Critical (below Low)."); - Console.WriteLine("Look at the official mods if you need examples."); - } - - // Made WithSpriteBody a PausableConditionalTrait, allowing to drop the PauseAnimationWhenDisabled property - var wsbPause = node.Value.Nodes.FirstOrDefault(n => n.Key == "PauseAnimationWhenDisabled"); - if (wsbPause != null) - { - wsbPause.Key = "PauseOnCondition"; - wsbPause.Value.Value = "disabled"; - } - } - - if (engineVersion < 20171120) - { - // AreaTypes support is added to GivesBuildableArea and it is required. - var givesBuildableArea = node.Value.Nodes.FirstOrDefault(n => n.Key == "GivesBuildableArea"); - if (givesBuildableArea != null) - givesBuildableArea.Value.Nodes.Add(new MiniYamlNode("AreaTypes", "building")); - - // RequiresBuildableArea trait is added and Building.Adjacent is moved there. - var building = node.Value.Nodes.FirstOrDefault(n => n.Key == "Building"); - if (building != null) - { - var adjacent = building.Value.Nodes.FirstOrDefault(n => n.Key == "Adjacent"); - var areaTypes = new MiniYamlNode("AreaTypes", "building"); - var requiresBuildableArea = new MiniYamlNode("RequiresBuildableArea", ""); - - requiresBuildableArea.Value.Nodes.Add(areaTypes); - if (adjacent != null) - requiresBuildableArea.Value.Nodes.Add(adjacent); - - node.Value.Nodes.Add(requiresBuildableArea); - building.Value.Nodes.Remove(adjacent); - } - } - - // Split Selection- and RenderSize - if (engineVersion < 20171115) - { - var autoSelSize = node.Value.Nodes.FirstOrDefault(n => n.Key.StartsWith("AutoSelectionSize", StringComparison.Ordinal)); - if (autoSelSize != null) - node.Value.Nodes.Add(new MiniYamlNode("AutoRenderSize", "")); - - var customSelSize = node.Value.Nodes.FirstOrDefault(n => n.Key.StartsWith("CustomSelectionSize", StringComparison.Ordinal)); - if (customSelSize != null) - { - var bounds = customSelSize.Value.Nodes.FirstOrDefault(n => n.Key == "CustomBounds"); - var customRenderSize = new MiniYamlNode("CustomRenderSize", ""); - if (bounds != null) - customRenderSize.Value.Nodes.Add(bounds); - - node.Value.Nodes.Add(customRenderSize); - } - } - - if (engineVersion < 20171208) - { - // Move SelectionDecorations.VisualBounds to Selectable.Bounds - if (node.Key.StartsWith("AutoRenderSize", StringComparison.Ordinal)) - RenameNodeKey(node, "Interactable"); - - if (node.Key.StartsWith("CustomRenderSize", StringComparison.Ordinal)) - { - RenameNodeKey(node, "Interactable"); - var boundsNode = node.Value.Nodes.FirstOrDefault(n => n.Key == "CustomBounds"); - if (boundsNode != null) - RenameNodeKey(boundsNode, "Bounds"); - } - - if (node.Key.StartsWith("SelectionDecorations", StringComparison.Ordinal)) - { - var boundsNode = node.Value.Nodes.FirstOrDefault(n => n.Key == "VisualBounds"); - if (boundsNode != null) - { - RenameNodeKey(boundsNode, "DecorationBounds"); - node.Value.Nodes.Remove(boundsNode); - var selectable = parent.Value.Nodes.FirstOrDefault(n => n.Key.StartsWith("Selectable", StringComparison.Ordinal)); - if (selectable == null) - { - selectable = new MiniYamlNode("Selectable", new MiniYaml("")); - addNodes.Add(selectable); - } - - selectable.Value.Nodes.Add(boundsNode); - } - } - - if (node.Key == "-Selectable") - addNodes.Add(new MiniYamlNode("Interactable", new MiniYaml(""))); - - if (depth == 0) - { - node.Value.Nodes.RemoveAll(n => n.Key.StartsWith("CustomSelectionSize", StringComparison.Ordinal)); - node.Value.Nodes.RemoveAll(n => n.Key.StartsWith("AutoSelectionSize", StringComparison.Ordinal)); - } - } - - // Multiply all health and damage in shipping mods by 100 to avoid issues caused by rounding - if (engineVersion < 20171212) - { - var mod = modData.Manifest.Id; - if (mod == "cnc" || mod == "ra" || mod == "d2k" || mod == "ts") - { - if (node.Key == "HP" && parent.Key == "Health") - { - var oldValue = FieldLoader.GetValue(node.Key, node.Value.Value); - if (mod == "d2k") - node.Value.Value = MultiplyByFactor(oldValue, 10); - else - node.Value.Value = MultiplyByFactor(oldValue, 100); - } - - if (node.Key.StartsWith("SelfHealing")) - { - var step = node.Value.Nodes.FirstOrDefault(n => n.Key == "Step"); - if (step == null) - node.Value.Nodes.Add(new MiniYamlNode("Step", "500")); - else if (step != null) - { - var oldValue = FieldLoader.GetValue(step.Key, step.Value.Value); - if (mod == "d2k") - step.Value.Value = MultiplyByFactor(oldValue, 10); - else - step.Value.Value = MultiplyByFactor(oldValue, 100); - } - } - - if (node.Key == "RepairsUnits") - { - var step = node.Value.Nodes.FirstOrDefault(n => n.Key == "HpPerStep"); - if (step == null) - node.Value.Nodes.Add(new MiniYamlNode("HpPerStep", "1000")); - else if (step != null) - { - var oldValue = FieldLoader.GetValue(step.Key, step.Value.Value); - if (mod == "d2k") - step.Value.Value = MultiplyByFactor(oldValue, 10); - else - step.Value.Value = MultiplyByFactor(oldValue, 100); - } - } - - if (node.Key == "RepairableBuilding") - { - var step = node.Value.Nodes.FirstOrDefault(n => n.Key == "RepairStep"); - if (step == null) - node.Value.Nodes.Add(new MiniYamlNode("RepairStep", "700")); - else if (step != null) - { - var oldValue = FieldLoader.GetValue(step.Key, step.Value.Value); - if (mod == "d2k") - step.Value.Value = MultiplyByFactor(oldValue, 10); - else - step.Value.Value = MultiplyByFactor(oldValue, 100); - } - } - - if (node.Key == "Burns") - { - var step = node.Value.Nodes.FirstOrDefault(n => n.Key == "Damage"); - if (step == null) - node.Value.Nodes.Add(new MiniYamlNode("Damage", "100")); - else if (step != null) - { - var oldValue = FieldLoader.GetValue(step.Key, step.Value.Value); - if (mod == "d2k") - step.Value.Value = MultiplyByFactor(oldValue, 10); - else - step.Value.Value = MultiplyByFactor(oldValue, 100); - } - } - - if (node.Key == "DamagedByTerrain") - { - var step = node.Value.Nodes.FirstOrDefault(n => n.Key == "Damage"); - if (step != null) - { - var oldValue = FieldLoader.GetValue(step.Key, step.Value.Value); - if (mod == "d2k") - step.Value.Value = MultiplyByFactor(oldValue, 10); - else - step.Value.Value = MultiplyByFactor(oldValue, 100); - } - } - } - } - - if (engineVersion < 20171212) - { - if (node.Key.StartsWith("SpawnMPUnits", StringComparison.Ordinal)) - { - var locked = node.Value.Nodes.FirstOrDefault(n => n.Key == "Locked"); - if (locked != null) - locked.Key = "DropdownLocked"; - } - - if (node.Key.StartsWith("Shroud", StringComparison.Ordinal)) - { - var fogLocked = node.Value.Nodes.FirstOrDefault(n => n.Key == "FogLocked"); - if (fogLocked != null) - fogLocked.Key = "FogCheckboxLocked"; - - var fogEnabled = node.Value.Nodes.FirstOrDefault(n => n.Key == "FogEnabled"); - if (fogEnabled != null) - fogEnabled.Key = "FogCheckboxEnabled"; - - var exploredMapLocked = node.Value.Nodes.FirstOrDefault(n => n.Key == "ExploredMapLocked"); - if (exploredMapLocked != null) - exploredMapLocked.Key = "ExploredMapCheckboxLocked"; - - var exploredMapEnabled = node.Value.Nodes.FirstOrDefault(n => n.Key == "ExploredMapEnabled"); - if (exploredMapEnabled != null) - exploredMapEnabled.Key = "ExploredMapCheckboxEnabled"; - } - - if (node.Key.StartsWith("MapOptions", StringComparison.Ordinal)) - { - var shortGameLocked = node.Value.Nodes.FirstOrDefault(n => n.Key == "ShortGameLocked"); - if (shortGameLocked != null) - shortGameLocked.Key = "ShortGameCheckboxLocked"; - - var shortGameEnabled = node.Value.Nodes.FirstOrDefault(n => n.Key == "ShortGameEnabled"); - if (shortGameEnabled != null) - shortGameEnabled.Key = "ShortGameCheckboxEnabled"; - - var techLevelLocked = node.Value.Nodes.FirstOrDefault(n => n.Key == "TechLevelLocked"); - if (techLevelLocked != null) - techLevelLocked.Key = "TechLevelDropdownLocked"; - - var gameSpeedLocked = node.Value.Nodes.FirstOrDefault(n => n.Key == "GameSpeedLocked"); - if (gameSpeedLocked != null) - gameSpeedLocked.Key = "GameSpeedDropdownLocked"; - } - - if (node.Key.StartsWith("MapCreeps", StringComparison.Ordinal)) - { - var locked = node.Value.Nodes.FirstOrDefault(n => n.Key == "Locked"); - if (locked != null) - locked.Key = "CheckboxLocked"; - - var enabled = node.Value.Nodes.FirstOrDefault(n => n.Key == "Enabled"); - if (enabled != null) - enabled.Key = "CheckboxEnabled"; - } - - if (node.Key.StartsWith("MapBuildRadius", StringComparison.Ordinal)) - { - var alllyLocked = node.Value.Nodes.FirstOrDefault(n => n.Key == "AllyBuildRadiusLocked"); - if (alllyLocked != null) - alllyLocked.Key = "AllyBuildRadiusCheckboxLocked"; - - var allyEnabled = node.Value.Nodes.FirstOrDefault(n => n.Key == "AllyBuildRadiusEnabled"); - if (allyEnabled != null) - allyEnabled.Key = "AllyBuildRadiusCheckboxEnabled"; - - var buildRadiusLocked = node.Value.Nodes.FirstOrDefault(n => n.Key == "BuildRadiusLocked"); - if (buildRadiusLocked != null) - buildRadiusLocked.Key = "BuildRadiusCheckboxLocked"; - - var buildRadiusEnabled = node.Value.Nodes.FirstOrDefault(n => n.Key == "BuildRadiusEnabled"); - if (buildRadiusEnabled != null) - buildRadiusEnabled.Key = "BuildRadiusCheckboxEnabled"; - } - - if (node.Key.StartsWith("DeveloperMode", StringComparison.Ordinal)) - { - var locked = node.Value.Nodes.FirstOrDefault(n => n.Key == "Locked"); - if (locked != null) - locked.Key = "CheckboxLocked"; - - var enabled = node.Value.Nodes.FirstOrDefault(n => n.Key == "Enabled"); - if (enabled != null) - enabled.Key = "CheckboxEnabled"; - } - - if (node.Key.StartsWith("CrateSpawner", StringComparison.Ordinal)) - { - var locked = node.Value.Nodes.FirstOrDefault(n => n.Key == "Locked"); - if (locked != null) - locked.Key = "CheckboxLocked"; - - var enabled = node.Value.Nodes.FirstOrDefault(n => n.Key == "Enabled"); - if (enabled != null) - enabled.Key = "CheckboxEnabled"; - } - - if (node.Key.StartsWith("PlayerResources", StringComparison.Ordinal)) - { - var locked = node.Value.Nodes.FirstOrDefault(n => n.Key == "Locked"); - if (locked != null) - locked.Key = "DefaultCashDropdownLocked"; - } - } - - // Made Gate not inherit Building - if (engineVersion < 20171119) - { - var gate = node.Value.Nodes.FirstOrDefault(n => n.Key == "Gate"); - if (gate != null) - { - var openSound = gate.Value.Nodes.FirstOrDefault(n => n.Key == "OpeningSound"); - var closeSound = gate.Value.Nodes.FirstOrDefault(n => n.Key == "ClosingSound"); - var closeDelay = gate.Value.Nodes.FirstOrDefault(n => n.Key == "CloseDelay"); - var transitDelay = gate.Value.Nodes.FirstOrDefault(n => n.Key == "TransitionDelay"); - var blockHeight = gate.Value.Nodes.FirstOrDefault(n => n.Key == "BlocksProjectilesHeight"); - - gate.Key = "Building"; - var newGate = new MiniYamlNode("Gate", ""); - - if (openSound != null) - { - newGate.Value.Nodes.Add(openSound); - gate.Value.Nodes.Remove(openSound); - } - - if (closeSound != null) - { - newGate.Value.Nodes.Add(closeSound); - gate.Value.Nodes.Remove(closeSound); - } - - if (closeDelay != null) - { - newGate.Value.Nodes.Add(closeDelay); - gate.Value.Nodes.Remove(closeDelay); - } - - if (transitDelay != null) - { - newGate.Value.Nodes.Add(transitDelay); - gate.Value.Nodes.Remove(transitDelay); - } - - if (blockHeight != null) - { - newGate.Value.Nodes.Add(blockHeight); - gate.Value.Nodes.Remove(blockHeight); - } - - node.Value.Nodes.Add(newGate); - } - } - - // Removed IDisable interface and all remaining usages - if (engineVersion < 20171119) - { - var doc = node.Value.Nodes.FirstOrDefault(n => n.Key.StartsWith("DisableOnCondition", StringComparison.Ordinal)); - if (doc != null) - { - Console.WriteLine("Actor.IsDisabled has been removed in favor of pausing/disabling traits via conditions."); - Console.WriteLine("DisableOnCondition was a stop-gap solution that has been removed along with it."); - Console.WriteLine("You'll have to use RequiresCondition or PauseOnCondition on individual traits to 'disable' actors."); - node.Value.Nodes.Remove(doc); - } - - var grant = node.Value.Nodes.FirstOrDefault(n => n.Key.StartsWith("GrantConditionOnDisabled", StringComparison.Ordinal)); - if (grant != null) - { - Console.WriteLine("Actor.IsDisabled has been removed in favor of pausing/disabling traits via conditions."); - Console.WriteLine("GrantConditionOnDisabled was a stop-gap solution that has been removed along with it."); - Console.WriteLine("You'll have to use RequiresCondition or PauseOnCondition on individual traits to 'disable' actors."); - node.Value.Nodes.Remove(grant); - } - } - - // CanPowerDown was replaced with a more general trait for toggling a condition - if (engineVersion < 20171225) - { - var cpd = node.Value.Nodes.FirstOrDefault(n => n.Key.StartsWith("CanPowerDown", StringComparison.Ordinal)); - if (cpd != null) - { - RenameNodeKey(cpd, "ToggleConditionOnOrder"); - - RenameNodeKey(cpd.Value.Nodes.FirstOrDefault(n => n.Key == "PowerupSound"), "DisabledSound"); - RenameNodeKey(cpd.Value.Nodes.FirstOrDefault(n => n.Key == "PowerupSpeech"), "DisabledSpeech"); - RenameNodeKey(cpd.Value.Nodes.FirstOrDefault(n => n.Key == "PowerdownSound"), "EnabledSound"); - RenameNodeKey(cpd.Value.Nodes.FirstOrDefault(n => n.Key == "PowerdownSpeech"), "EnabledSpeech"); - cpd.Value.Nodes.Add(new MiniYamlNode("OrderName", "PowerDown")); - - var condition = cpd.Value.Nodes.FirstOrDefault(n => n.Key == "PowerdownCondition"); - if (condition != null) - RenameNodeKey(condition, "Condition"); - else - cpd.Value.Nodes.Add(new MiniYamlNode("Condition", "powerdown")); - - if (cpd.Value.Nodes.RemoveAll(n => n.Key == "CancelWhenDisabled") > 0) - { - Console.WriteLine("CancelWhenDisabled was removed when CanPowerDown was replaced by ToggleConditionOnOrder"); - Console.WriteLine("Use PauseOnCondition instead of RequiresCondition to replicate the behavior of 'false'."); - } - - node.Value.Nodes.Add(new MiniYamlNode("PowerMultiplier@POWERDOWN", new MiniYaml("", new List() - { - new MiniYamlNode("RequiresCondition", condition.Value.Value), - new MiniYamlNode("Modifier", "0") - }))); - } - } - - if (engineVersion < 20171228) - { - var chargeTime = node.Value.Nodes.FirstOrDefault(n => n.Key == "ChargeTime"); - if (chargeTime != null) - { - var chargeTimeValue = FieldLoader.GetValue("ChargeTime", chargeTime.Value.Value); - if (chargeTimeValue > 0) - chargeTime.Value.Value = (chargeTimeValue * 25).ToString(); - - RenameNodeKey(chargeTime, "ChargeInterval"); - } - - if (node.Key.StartsWith("GpsPower", StringComparison.Ordinal)) - { - var revealDelay = node.Value.Nodes.FirstOrDefault(n => n.Key == "RevealDelay"); - var revealDelayValue = revealDelay != null ? FieldLoader.GetValue("RevealDelay", revealDelay.Value.Value) : 0; - if (revealDelay != null && revealDelayValue > 0) - revealDelay.Value.Value = (revealDelayValue * 25).ToString(); - } - - if (node.Key.StartsWith("ChronoshiftPower", StringComparison.Ordinal)) - { - var duration = node.Value.Nodes.FirstOrDefault(n => n.Key == "Duration"); - var durationValue = duration != null ? FieldLoader.GetValue("Duration", duration.Value.Value) : 0; - if (duration != null && durationValue > 0) - duration.Value.Value = (durationValue * 25).ToString(); - } - } - - if (engineVersion < 20180308) - { - if (node.Key == "WormSpawner") - RenameNodeKey(node, "ActorSpawner"); - - if (node.Key == "WormManager") - { - RenameNodeKey(node, "ActorSpawnManager"); - - var wormSignature = node.Value.Nodes.FirstOrDefault(n => n.Key == "WormSignature"); - if (wormSignature != null) - wormSignature.Key = "Actors"; - } - } - - // Removed WithReloadingSpriteTurret - if (engineVersion < 20180308) - { - var reloadingTurret = node.Value.Nodes.FirstOrDefault(n => n.Key.StartsWith("WithReloadingSpriteTurret", StringComparison.Ordinal)); - if (reloadingTurret != null) - { - var ammoPool = node.Value.Nodes.FirstOrDefault(n => n.Key.StartsWith("AmmoPool", StringComparison.Ordinal)); - if (ammoPool != null) - ammoPool.Value.Nodes.Add(new MiniYamlNode("AmmoCondition", "ammo")); - - RenameNodeKey(reloadingTurret, "WithSpriteTurret"); - var noAmmoTurret = new MiniYamlNode("WithSpriteTurret@NoAmmo", ""); - var reqAmmoCondition = new MiniYamlNode("RequiresCondition", "ammo"); - var reqNoAmmoCondition = new MiniYamlNode("RequiresCondition", "!ammo"); - - reloadingTurret.Value.Nodes.Add(reqAmmoCondition); - noAmmoTurret.Value.Nodes.Add(reqNoAmmoCondition); - node.Value.Nodes.Add(noAmmoTurret); - - Console.WriteLine("WithReloadingSpriteTurret has been removed in favor of using stacked AmmoPool.AmmoConditions."); - Console.WriteLine("Check if your affected actors need further changes."); - } - } - - if (engineVersion < 20180309) - { - if (node.Key == "ParaDrop") - { - var soundNodePD = node.Value.Nodes.FirstOrDefault(n => n.Key == "ChuteSound"); - if (soundNodePD == null) - node.Value.Nodes.Add(new MiniYamlNode("ChuteSound", "chute1.aud")); - } - - if (depth == 1 && node.Key == "EjectOnDeath") - { - var soundNodeEOD = node.Value.Nodes.FirstOrDefault(n => n.Key == "ChuteSound"); - if (soundNodeEOD == null) - node.Value.Nodes.Add(new MiniYamlNode("ChuteSound", "chute1.aud")); - } - - if (node.Key.StartsWith("ProductionParadrop", StringComparison.Ordinal)) - { - var soundNodePP = node.Value.Nodes.FirstOrDefault(n => n.Key == "ChuteSound"); - if (soundNodePP == null) - node.Value.Nodes.Add(new MiniYamlNode("ChuteSound", "chute1.aud")); - } - - if (node.Key == "Building") - { - var soundNodeB1 = node.Value.Nodes.FirstOrDefault(n => n.Key == "BuildSounds"); - if (soundNodeB1 == null) - node.Value.Nodes.Add(new MiniYamlNode("BuildSounds", "placbldg.aud, build5.aud")); - - var soundNodeB2 = node.Value.Nodes.FirstOrDefault(n => n.Key == "UndeploySounds"); - if (soundNodeB2 == null) - node.Value.Nodes.Add(new MiniYamlNode("UndeploySounds", "cashturn.aud")); - } - } - - // Split aim animation logic from WithTurretAttackAnimation to separate WithTurretAimAnimation - if (engineVersion < 20180309) - { - var turAttackAnim = node.Value.Nodes.FirstOrDefault(n => n.Key.StartsWith("WithTurretAttackAnimation", StringComparison.Ordinal)); - if (turAttackAnim != null) - { - var atkSequence = turAttackAnim.Value.Nodes.FirstOrDefault(n => n.Key == "AttackSequence"); - var aimSequence = turAttackAnim.Value.Nodes.FirstOrDefault(n => n.Key == "AimSequence"); - - // If only AimSequence is null, just rename AttackSequence to Sequence (ReloadPrefix is very unlikely to be defined in that case). - // If only AttackSequence is null, just rename the trait and property (the delay properties will likely be undefined). - // If both aren't null, split/copy everything relevant to the new WithTurretAimAnimation. - // If both are null (extremely unlikely), do nothing. - if (atkSequence == null && aimSequence != null) - { - RenameNodeKey(turAttackAnim, "WithTurretAimAnimation"); - RenameNodeKey(aimSequence, "Sequence"); - } - else if (atkSequence != null && aimSequence == null) - RenameNodeKey(atkSequence, "Sequence"); - else if (atkSequence != null && aimSequence != null) - { - var aimAnim = new MiniYamlNode("WithTurretAimAnimation", ""); - RenameNodeKey(aimSequence, "Sequence"); - aimAnim.Value.Nodes.Add(aimSequence); - turAttackAnim.Value.Nodes.Remove(aimSequence); - - var relPrefix = turAttackAnim.Value.Nodes.FirstOrDefault(n => n.Key == "ReloadPrefix"); - var turr = turAttackAnim.Value.Nodes.FirstOrDefault(n => n.Key == "Turret"); - var arm = turAttackAnim.Value.Nodes.FirstOrDefault(n => n.Key == "Armament"); - if (relPrefix != null) - { - aimAnim.Value.Nodes.Add(relPrefix); - turAttackAnim.Value.Nodes.Remove(relPrefix); - } - - if (turr != null) - aimAnim.Value.Nodes.Add(turr); - if (arm != null) - aimAnim.Value.Nodes.Add(arm); - - RenameNodeKey(atkSequence, "Sequence"); - node.Value.Nodes.Add(aimAnim); - } - } - } - - // Removed AimSequence from WithSpriteTurret, use WithTurretAimAnimation instead - if (engineVersion < 20180309) - { - var spriteTurret = node.Value.Nodes.FirstOrDefault(n => n.Key.StartsWith("WithSpriteTurret", StringComparison.Ordinal)); - if (spriteTurret != null) - { - var aimSequence = spriteTurret.Value.Nodes.FirstOrDefault(n => n.Key == "AimSequence"); - if (aimSequence != null) - { - var aimAnim = new MiniYamlNode("WithTurretAimAnimation", ""); - RenameNodeKey(aimSequence, "Sequence"); - aimAnim.Value.Nodes.Add(aimSequence); - spriteTurret.Value.Nodes.Remove(aimSequence); - node.Value.Nodes.Add(aimAnim); - } - } - } - - UpgradeActorRules(modData, engineVersion, ref node.Value.Nodes, node, depth + 1); - } - - foreach (var a in addNodes) - nodes.Add(a); - } - - internal static void UpgradeWeaponRules(ModData modData, int engineVersion, ref List nodes, MiniYamlNode parent, int depth) - { - foreach (var node in nodes) - { - // Multiply all health and damage in shipping mods by 100 to avoid issues caused by rounding - if (engineVersion < 20171212) - { - var mod = modData.Manifest.Id; - if (mod == "cnc" || mod == "ra" || mod == "d2k" || mod == "ts") - { - if (node.Key == "Damage" && (parent.Value.Value == "SpreadDamage" || parent.Value.Value == "TargetDamage")) - { - var oldValue = FieldLoader.GetValue(node.Key, node.Value.Value); - if (mod == "d2k") - node.Value.Value = MultiplyByFactor(oldValue, 10); - else - node.Value.Value = MultiplyByFactor(oldValue, 100); - } - } - } - - if (engineVersion < 20180219) - { - if (node.Key.StartsWith("Warhead", StringComparison.Ordinal) && node.Value.Value == "CreateEffect") - { - var victimScanRadius = node.Value.Nodes.FirstOrDefault(n => n.Key == "VictimScanRadius"); - if (victimScanRadius != null) - { - if (FieldLoader.GetValue(victimScanRadius.Key, victimScanRadius.Value.Value) == 0) - node.Value.Nodes.Add(new MiniYamlNode("ImpactActors", "false")); - - node.Value.Nodes.Remove(victimScanRadius); - } - } - - if (node.Key.StartsWith("Projectile")) - node.Value.Nodes.RemoveAll(n => n.Key == "BounceBlockerScanRadius" || n.Key == "BlockerScanRadius" || n.Key == "AreaVictimScanRadius"); - } - - UpgradeWeaponRules(modData, engineVersion, ref node.Value.Nodes, node, depth + 1); - } - } - - internal static void UpgradeSequences(ModData modData, int engineVersion, ref List nodes, MiniYamlNode parent, int depth) - { - foreach (var node in nodes) - { - // Add rules here - UpgradeSequences(modData, engineVersion, ref node.Value.Nodes, node, depth + 1); - } - } - - internal static void UpgradeTileset(ModData modData, int engineVersion, ref List nodes, MiniYamlNode parent, int depth) - { - foreach (var node in nodes) - { - // Removed IsWater. - if (engineVersion < 20180222) - { - if (node.Key == "TerrainType" || node.Key.StartsWith("TerrainType@", StringComparison.Ordinal)) - { - var isWater = node.Value.Nodes.FirstOrDefault(n => n.Key == "IsWater"); - if (isWater != null) - node.Value.Nodes.Remove(isWater); - } - } - } - } - - internal static void UpgradeCursors(ModData modData, int engineVersion, ref List nodes, MiniYamlNode parent, int depth) - { - foreach (var node in nodes) - { - // Add rules here - UpgradeCursors(modData, engineVersion, ref node.Value.Nodes, node, depth + 1); - } - } - - internal static void UpgradePlayers(ModData modData, int engineVersion, ref List nodes, MiniYamlNode parent, int depth) - { - foreach (var node in nodes) - { - // Add rules here - UpgradePlayers(modData, engineVersion, ref node.Value.Nodes, node, depth + 1); - } - } - - internal static void UpgradeChromeMetrics(ModData modData, int engineVersion, ref List nodes, MiniYamlNode parent, int depth) - { - foreach (var node in nodes) - { - // Add rules here - UpgradeChromeMetrics(modData, engineVersion, ref node.Value.Nodes, node, depth + 1); - } - } - - internal static void UpgradeChromeLayout(ModData modData, int engineVersion, ref List nodes, MiniYamlNode parent, int depth) - { - foreach (var node in nodes) - { - // Add rules here - UpgradeChromeLayout(modData, engineVersion, ref node.Value.Nodes, node, depth + 1); - } - } - - internal static void UpgradeActors(ModData modData, int engineVersion, ref List nodes, MiniYamlNode parent, int depth) - { - foreach (var node in nodes) - { - // Add rules here - UpgradeActors(modData, engineVersion, ref node.Value.Nodes, node, depth + 1); - } - } - - internal static void UpgradeMapFormat(ModData modData, IReadWritePackage package) - { - if (package == null) - return; - - var yamlStream = package.GetStream("map.yaml"); - if (yamlStream == null) - return; - - var yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, package.Name)); - var nd = yaml.ToDictionary(); - var mapFormat = FieldLoader.GetValue("MapFormat", nd["MapFormat"].Value); - if (mapFormat < 11) - throw new InvalidDataException("Map format {0} is not supported.\n File: {1}".F(mapFormat, package.Name)); - - if (mapFormat < Map.SupportedMapFormat) - { - yaml.Nodes.First(n => n.Key == "MapFormat").Value = new MiniYaml(Map.SupportedMapFormat.ToString()); - Console.WriteLine("Converted {0} to MapFormat {1}.", package.Name, Map.SupportedMapFormat); - } - - package.Update("map.yaml", Encoding.UTF8.GetBytes(yaml.Nodes.WriteToString())); - } - } -}