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()));
- }
- }
-}