#region Copyright & License Information /* * Copyright 2007-2016 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.Drawing; using System.Globalization; using System.IO; using System.Linq; using System.Text; using OpenRA.FileSystem; using OpenRA.Graphics; using OpenRA.Traits; namespace OpenRA.Mods.Common.UtilityCommands { static class UpgradeRules { public const int MinimumSupportedVersion = 20151224; internal static void TryUpdateColor(ref string value) { if (value.Length == 0) return; try { var parts = value.Split(','); if (parts.Length == 3) value = FieldSaver.FormatValue(Color.FromArgb( Exts.ParseIntegerInvariant(parts[0]).Clamp(0, 255), Exts.ParseIntegerInvariant(parts[1]).Clamp(0, 255), Exts.ParseIntegerInvariant(parts[2]).Clamp(0, 255))); else if (parts.Length == 4) value = FieldSaver.FormatValue(Color.FromArgb( Exts.ParseIntegerInvariant(parts[0]).Clamp(0, 255), Exts.ParseIntegerInvariant(parts[1]).Clamp(0, 255), Exts.ParseIntegerInvariant(parts[2]).Clamp(0, 255), Exts.ParseIntegerInvariant(parts[3]).Clamp(0, 255))); } catch { } } internal static void TryUpdateColors(ref string value) { if (value.Length == 0) return; try { var parts = value.Split(','); if (parts.Length % 4 != 0) return; var colors = new Color[parts.Length / 4]; for (var i = 0; i < colors.Length; i++) { colors[i] = Color.FromArgb( Exts.ParseIntegerInvariant(parts[4 * i]).Clamp(0, 255), Exts.ParseIntegerInvariant(parts[4 * i + 1]).Clamp(0, 255), Exts.ParseIntegerInvariant(parts[4 * i + 2]).Clamp(0, 255), Exts.ParseIntegerInvariant(parts[4 * i + 3]).Clamp(0, 255)); } value = FieldSaver.FormatValue(colors); } catch { } } internal static void TryUpdateHSLColor(ref string value) { if (value.Length == 0) return; try { var parts = value.Split(','); if (parts.Length == 3 || parts.Length == 4) value = FieldSaver.FormatValue(new HSLColor( (byte)Exts.ParseIntegerInvariant(parts[0]).Clamp(0, 255), (byte)Exts.ParseIntegerInvariant(parts[1]).Clamp(0, 255), (byte)Exts.ParseIntegerInvariant(parts[2]).Clamp(0, 255))); } catch { } } internal static void UpgradeActorRules(int engineVersion, ref List nodes, MiniYamlNode parent, int depth) { foreach (var node in nodes) { if (engineVersion < 20151225 && depth == 2) { if (node.Key == "Color") { if (parent.Key.StartsWith("FixedColorPalette")) TryUpdateHSLColor(ref node.Value.Value); else TryUpdateColor(ref node.Value.Value); } else if (node.Key == "RadarPingColor" || node.Key == "SelectionBoxColor" || node.Key == "BarColor") TryUpdateColor(ref node.Value.Value); else if (node.Key == "Fog" || node.Key == "Shroud" || node.Key == "ParticleColors") TryUpdateColors(ref node.Value.Value); } // DeathType on Explodes was renamed to DeathTypes if (engineVersion < 20151225) { if (node.Key == "Explodes") { var dt = node.Value.Nodes.FirstOrDefault(n => n.Key == "DeathType"); if (dt != null) dt.Key = "DeathTypes"; } } // Upgrades on DeployToUpgrade were renamed to DeployedUpgrades if (engineVersion < 20151122) { if (node.Key == "DeployToUpgrade") { var u = node.Value.Nodes.FirstOrDefault(n => n.Key == "Upgrades"); if (u != null) u.Key = "DeployedUpgrades"; } } if (engineVersion < 20151225) { // Rename WithTurret to WithSpriteTurret if (depth == 1 && node.Key.StartsWith("WithTurret")) { var parts = node.Key.Split('@'); node.Key = "WithSpriteTurret"; if (parts.Length > 1) node.Key += "@" + parts[1]; } if (depth == 1 && node.Key.StartsWith("-WithTurret")) { var parts = node.Key.Split('@'); node.Key = "-WithSpriteTurret"; if (parts.Length > 1) node.Key += "@" + parts[1]; } // Rename WithBarrel to WithSpriteBarrel if (depth == 1 && node.Key.StartsWith("WithBarrel")) { var parts = node.Key.Split('@'); node.Key = "WithSpriteBarrel"; if (parts.Length > 1) node.Key += "@" + parts[1]; } if (depth == 1 && node.Key.StartsWith("-WithBarrel")) { var parts = node.Key.Split('@'); node.Key = "-WithSpriteBarrel"; if (parts.Length > 1) node.Key += "@" + parts[1]; } // Rename WithReloadingTurret to WithReloadingSpriteTurret if (depth == 1 && node.Key.StartsWith("WithReloadingTurret")) { var parts = node.Key.Split('@'); node.Key = "WithReloadingSpriteTurret"; if (parts.Length > 1) node.Key += "@" + parts[1]; } if (depth == 1 && node.Key.StartsWith("-WithReloadingTurret")) { var parts = node.Key.Split('@'); node.Key = "-WithReloadingSpriteTurret"; if (parts.Length > 1) node.Key += "@" + parts[1]; } } // Mobile actors immobilized by Carryable, Cargo, DeployToUpgrade, and/or others using upgrade(s) if (engineVersion < 20151225 && depth == 0) { var notMobile = "notmobile"; var mobileNode = node.Value.Nodes.Find(n => n.Key == "Mobile"); var carryableNode = node.Value.Nodes.Find(n => n.Key == "Carryable"); var cargoNode = node.Value.Nodes.Find(n => n.Key == "Cargo"); var deployToUpgradeNode = node.Value.Nodes.Find(n => n.Key == "DeployToUpgrade"); var disableUpgradeNode = node.Value.Nodes.Find(n => n.Key == "DisableUpgrade"); var disableMovementOnUpgradeNode = node.Value.Nodes.Find(n => n.Key == "DisableMovementOnUpgrade"); Action addNotMobileToTraitUpgrades = (trait, upgradesKey) => { if (trait != null) { var upgrades = trait.Value.Nodes.Find(u => u.Key == upgradesKey); if (upgrades == null) trait.Value.Nodes.Add(new MiniYamlNode(upgradesKey, notMobile)); else if (string.IsNullOrEmpty(upgrades.Value.Value)) upgrades.Value.Value = notMobile; else if (!upgrades.Value.Value.Contains(notMobile)) upgrades.Value.Value += ", " + notMobile; } }; if (mobileNode != null) { var mobileUpgrades = mobileNode.Value.Nodes.Find(n => n.Key == "UpgradeTypes"); var mobileUpgradeMaxEnabledLevel = mobileNode.Value.Nodes.Find(n => n.Key == "UpgradeMaxEnabledLevel"); var comma = new char[] { ',' }; Func addUpgradeMaxEnabledLevelNode = () => { if (mobileUpgradeMaxEnabledLevel == null) { mobileUpgradeMaxEnabledLevel = new MiniYamlNode("UpgradeMaxEnabledLevel", "0"); mobileNode.Value.Nodes.Add(mobileUpgradeMaxEnabledLevel); return true; } else return mobileUpgradeMaxEnabledLevel.Value.Value == "0"; }; // If exactly one upgrade type is in UpgradeTypes and UpgradeMaxEnabledLevel is/can be 0 , then use it as notmobile if (mobileUpgrades != null && !string.IsNullOrEmpty(mobileUpgrades.Value.Value) && !mobileUpgrades.Value.Value.Contains(",") && addUpgradeMaxEnabledLevelNode()) notMobile = mobileUpgrades.Value.Value; if (mobileUpgradeMaxEnabledLevel != null && mobileUpgradeMaxEnabledLevel.Value.Value != "0") Console.WriteLine("\t\t" + node.Key + " actor rules may require manual upgrading for immobilization upgrade logic."); else { Action addImmobilizeUpgradeType = upgradeType => { if (mobileUpgrades == null) { mobileUpgrades = new MiniYamlNode("UpgradeTypes", upgradeType); mobileNode.Value.Nodes.Add(mobileUpgrades); } else if (string.IsNullOrEmpty(mobileUpgrades.Value.Value)) mobileUpgrades.Value.Value = upgradeType; else if (!mobileUpgrades.Value.Value.Split(comma).Contains(upgradeType)) mobileUpgrades.Value.Value += ", " + upgradeType; }; Predicate addImmobilizeUpgradeTypes = upgradeTypes => { if (string.IsNullOrEmpty(upgradeTypes)) return false; foreach (var upgradeType in upgradeTypes.Split(comma)) addImmobilizeUpgradeType(upgradeType); return true; }; Predicate addUpgradeTypeFromTrait = trait => { var upgradeTypesNode = trait.Value.Nodes.Find(n => n.Key == "UpgradeTypes"); if (upgradeTypesNode == null) return false; addUpgradeMaxEnabledLevelNode(); return addImmobilizeUpgradeTypes(upgradeTypesNode.Value.Value); }; var noticeWritten = false; Action writeNotice = () => { if (noticeWritten) return; Console.WriteLine("\t\t" + node.Key + " actor rules may require manual upgrading for immobilization upgrade logic."); noticeWritten = true; }; if (disableUpgradeNode != null && !addUpgradeTypeFromTrait(disableUpgradeNode)) { writeNotice(); Console.WriteLine("\t\t\tOne or more upgrades may need to be copied from the DisableUpgrade trait to the Mobile trait."); } if (disableMovementOnUpgradeNode != null) { if (addUpgradeTypeFromTrait(disableMovementOnUpgradeNode)) parent.Value.Nodes.Remove(disableMovementOnUpgradeNode); else { writeNotice(); Console.WriteLine("\t\t\tOne or more upgrades may need to be moved from the DisableMovementOnUpgrade trait to the Mobile trait."); Console.WriteLine("\t\t\t\tRemember to remove the DisableMovementOnUpgrade trait."); } } if (carryableNode != null || cargoNode != null || deployToUpgradeNode != null) { addUpgradeMaxEnabledLevelNode(); addImmobilizeUpgradeTypes(notMobile); addNotMobileToTraitUpgrades(carryableNode, "CarryableUpgrades"); addNotMobileToTraitUpgrades(cargoNode, "LoadingUpgrades"); addNotMobileToTraitUpgrades(deployToUpgradeNode, "DeployedUpgrades"); } } } else if (!node.Value.Nodes.Exists(n => n.Key == "Husk" || n.Key == "Building" || n.Key == "Aircraft" || n.Key == "Immobile")) { if (carryableNode != null || cargoNode != null || deployToUpgradeNode != null) { Console.WriteLine("\t\tIf " + node.Key + " has a Mobile trait then adding the following with substituted by an immobilization upgrade for " + node.Key + " may be neeeded:"); if (carryableNode != null) { Console.WriteLine("\t\t\tCarryable:"); Console.WriteLine("\t\t\t\tCarryableUpgrades: "); } if (cargoNode != null) { Console.WriteLine("\t\t\tCargo:"); Console.WriteLine("\t\t\t\tLoadingUpgrades: "); } if (deployToUpgradeNode != null) { Console.WriteLine("\t\t\tDeployToUpgrade:"); Console.WriteLine("\t\t\t\tDeployedUpgrades: "); } } var disableUpgradeUpgradeTypesNode = disableUpgradeNode != null ? disableUpgradeNode.Value.Nodes.Find(n => n.Key == "UpgradeTypes") : null; var disableMovementOnUpgradeUpgradeTypesNode = disableMovementOnUpgradeNode != null ? disableMovementOnUpgradeNode.Value.Nodes.Find(n => n.Key == "UpgradeTypes") : null; if (disableUpgradeUpgradeTypesNode != null || disableMovementOnUpgradeUpgradeTypesNode != null) Console.WriteLine("\t\t" + node.Key + " actor rules may require manual upgrading for immobilization upgrade logic."); if (disableUpgradeUpgradeTypesNode != null) Console.WriteLine("\t\t\tDisableUpgrade UpgradeTypes: " + disableUpgradeUpgradeTypesNode.Value.Value); if (disableMovementOnUpgradeUpgradeTypesNode != null) Console.WriteLine("\t\t\tDisableMovementOnUpgrade UpgradeTypes: " + disableMovementOnUpgradeUpgradeTypesNode.Value.Value); if (disableMovementOnUpgradeNode != null) node.Value.Nodes.Remove(disableMovementOnUpgradeNode); } } // 'CloseEnough' on 'RepairableNear' uses WDist now if (engineVersion < 20151225) { if (node.Key == "RepairableNear") { var ce = node.Value.Nodes.FirstOrDefault(n => n.Key == "CloseEnough"); if (ce != null && !ce.Value.Value.Contains("c")) ce.Value.Value = ce.Value.Value + "c0"; } } // Added width support for line particles if (engineVersion < 20151225 && node.Key == "WeatherOverlay") { var useSquares = node.Value.Nodes.FirstOrDefault(n => n.Key == "UseSquares"); if (useSquares != null && !FieldLoader.GetValue("UseSquares", useSquares.Value.Value)) node.Value.Nodes.Add(new MiniYamlNode("ParticleSize", "1, 1")); } // Overhauled the actor decorations traits if (engineVersion < 20151226) { if (depth == 1 && (node.Key.StartsWith("WithDecoration") || node.Key.StartsWith("WithRankDecoration"))) { node.Value.Nodes.RemoveAll(n => n.Key == "Scale"); node.Value.Nodes.RemoveAll(n => n.Key == "Offset"); var sd = node.Value.Nodes.FirstOrDefault(n => n.Key == "SelectionDecoration"); if (sd != null) sd.Key = "RequiresSelection"; var reference = node.Value.Nodes.FirstOrDefault(n => n.Key == "ReferencePoint"); if (reference != null) { var values = FieldLoader.GetValue("ReferencePoint", reference.Value.Value); values = values.Where(v => v != "HCenter" && v != "VCenter").ToArray(); if (values.Length == 0) values = new[] { "Center" }; reference.Value.Value = FieldSaver.FormatValue(values); } var stance = Stance.Ally; var showToAllies = node.Value.Nodes.FirstOrDefault(n => n.Key == "ShowToAllies"); if (showToAllies != null && !FieldLoader.GetValue("ShowToAllies", showToAllies.Value.Value)) stance ^= Stance.Ally; var showToEnemies = node.Value.Nodes.FirstOrDefault(n => n.Key == "ShowToEnemies"); if (showToEnemies != null && FieldLoader.GetValue("ShowToEnemies", showToEnemies.Value.Value)) stance |= Stance.Enemy; if (stance != Stance.Ally) node.Value.Nodes.Add(new MiniYamlNode("Stance", FieldSaver.FormatValue(stance))); node.Value.Nodes.RemoveAll(n => n.Key == "ShowToAllies"); node.Value.Nodes.RemoveAll(n => n.Key == "ShowToEnemies"); } if (depth == 1 && node.Key == "Fake") { node.Key = "WithDecoration@fake"; node.Value.Nodes.Add(new MiniYamlNode("RequiresSelection", "true")); node.Value.Nodes.Add(new MiniYamlNode("Image", "pips")); node.Value.Nodes.Add(new MiniYamlNode("Sequence", "tag-fake")); node.Value.Nodes.Add(new MiniYamlNode("ReferencePoint", "Top")); node.Value.Nodes.Add(new MiniYamlNode("ZOffset", "256")); } if (depth == 0 && node.Value.Nodes.Any(n => n.Key.StartsWith("PrimaryBuilding"))) { var decNodes = new List(); decNodes.Add(new MiniYamlNode("RequiresSelection", "true")); decNodes.Add(new MiniYamlNode("Image", "pips")); decNodes.Add(new MiniYamlNode("Sequence", "tag-primary")); decNodes.Add(new MiniYamlNode("ReferencePoint", "Top")); decNodes.Add(new MiniYamlNode("ZOffset", "256")); decNodes.Add(new MiniYamlNode("UpgradeTypes", "primary")); decNodes.Add(new MiniYamlNode("UpgradeMinEnabledLevel", "1")); node.Value.Nodes.Add(new MiniYamlNode("WithDecoration@primary", new MiniYaml("", decNodes))); } } // Refactored the low resources notification to a separate trait if (engineVersion < 20151227 && node.Key == "Player") { var resourcesNode = node.Value.Nodes.FirstOrDefault(x => x.Key == "PlayerResources"); if (resourcesNode != null) { var intervalNode = resourcesNode.Value.Nodes.FirstOrDefault(x => x.Key == "AdviceInterval"); var storageNode = new MiniYamlNode("ResourceStorageWarning", ""); if (intervalNode != null) { // The time value is now in seconds, not ticks. We // divide by 25 ticks per second at Normal. int oldInterval; if (int.TryParse(intervalNode.Value.Value, out oldInterval)) storageNode.Value.Nodes.Add(new MiniYamlNode("AdviceInterval", (oldInterval / 25).ToString())); resourcesNode.Value.Nodes.Remove(intervalNode); } node.Value.Nodes.Add(storageNode); } } // Refactored Health.Radius to HitShapes if (engineVersion < 20151227) { if (node.Key.StartsWith("Health")) { var radius = node.Value.Nodes.FirstOrDefault(x => x.Key == "Radius"); if (radius != null) { var radiusValue = FieldLoader.GetValue("Radius", radius.Value.Value); node.Value.Nodes.Add(new MiniYamlNode("Shape", "Circle")); var shape = node.Value.Nodes.First(x => x.Key == "Shape"); shape.Value.Nodes.Add(new MiniYamlNode("Radius", radiusValue)); node.Value.Nodes.Remove(radius); } } } // Remove obsolete TransformOnPassenger trait. if (engineVersion < 20160102) { var removed = node.Value.Nodes.RemoveAll(x => x.Key.Contains("TransformOnPassenger")); if (removed > 0) { Console.WriteLine("TransformOnPassenger has been removed."); Console.WriteLine("Use the upgrades system to apply modifiers to the transport actor instead."); } } if (engineVersion < 20160103) { // Overhauled WithActiveAnimation -> WithIdleAnimation if (node.Key == "WithActiveAnimation") { node.Key = "WithIdleAnimation"; foreach (var n in node.Value.Nodes) if (n.Key == "Sequence") n.Key = "Sequences"; } } if (engineVersion < 20160107 && depth == 1 && node.Key.StartsWith("Cloak")) { var defaultCloakType = Traits.UncloakType.Attack | Traits.UncloakType.Unload | Traits.UncloakType.Infiltrate | Traits.UncloakType.Demolish; // Merge Uncloak types var t = defaultCloakType; for (var i = node.Value.Nodes.Count - 1; i >= 0; i--) { var n = node.Value.Nodes[i]; var v = string.Compare(n.Value.Value, "true", true) == 0; Traits.UncloakType flag; if (n.Key == "UncloakOnAttack") flag = Traits.UncloakType.Attack; else if (n.Key == "UncloakOnMove") flag = Traits.UncloakType.Move; else if (n.Key == "UncloakOnUnload") flag = Traits.UncloakType.Unload; else if (n.Key == "UncloakOnInfiltrate") flag = Traits.UncloakType.Infiltrate; else if (n.Key == "UncloakOnDemolish") flag = Traits.UncloakType.Demolish; else continue; t = v ? t | flag : t & ~flag; node.Value.Nodes.Remove(n); } if (t != defaultCloakType) { Console.WriteLine("\t\tCloak type: " + t.ToString()); var ts = new List(); if (t.HasFlag(Traits.UncloakType.Attack)) ts.Add("Attack"); if (t.HasFlag(Traits.UncloakType.Unload)) ts.Add("Unload"); if (t.HasFlag(Traits.UncloakType.Infiltrate)) ts.Add("Infiltrate"); if (t.HasFlag(Traits.UncloakType.Demolish)) ts.Add("Demolish"); if (t.HasFlag(Traits.UncloakType.Move)) ts.Add("Move"); node.Value.Nodes.Add(new MiniYamlNode("UncloakOn", ts.JoinWith(", "))); } } // Rename WithDockingOverlay to WithDockedOverlay if (engineVersion < 20160116) { if (node.Key.StartsWith("WithDockingOverlay")) node.Key = "WithDockedOverlay" + node.Key.Substring(18); } if (engineVersion < 20160116) { if (node.Key == "DemoTruck") node.Key = "AttackSuicides"; } // Replaced GpsRemoveFrozenActor with FrozenUnderFogUpdatedByGps if (engineVersion < 20160117) { if (node.Key == "GpsRemoveFrozenActor") { node.Key = "FrozenUnderFogUpdatedByGps"; node.Value.Nodes.Clear(); } } // Removed arbitrary defaults from InfiltrateForCash if (engineVersion < 20160118) { if (node.Key == "InfiltrateForCash") { if (!node.Value.Nodes.Any(n => n.Key == "Percentage")) node.Value.Nodes.Add(new MiniYamlNode("Percentage", "50")); if (!node.Value.Nodes.Any(n => n.Key == "Minimum")) node.Value.Nodes.Add(new MiniYamlNode("Minimum", "500")); var sound = node.Value.Nodes.FirstOrDefault(n => n.Key == "SoundToVictim"); if (sound != null) { node.Value.Nodes.Remove(sound); Console.WriteLine("The 'SoundToVictim' property of the 'InfiltrateForCash' trait has been"); Console.WriteLine("replaced with a 'Notification' property. Please add the sound file"); Console.WriteLine("'{0}' to your mod's audio notification yaml and".F(sound.Value.Value)); Console.WriteLine("update your mod's rules accordingly."); Console.WriteLine(); } } } if (engineVersion < 20160301) { // Renamed ROT -> TurnSpeed if (node.Key == "ROT") node.Key = "TurnSpeed"; } if (engineVersion < 20160320) { // Renamed Parachutable.CorpseSequenceCollection to Image if (node.Key == "CorpseSequenceCollection") node.Key = "Image"; // Renamed WithBuildingExplosion.SequenceCollection to Image if (node.Key == "SequenceCollection") node.Key = "Image"; } UpgradeActorRules(engineVersion, ref node.Value.Nodes, node, depth + 1); } } internal static void UpgradeWeaponRules(int engineVersion, ref List nodes, MiniYamlNode parent, int depth) { foreach (var node in nodes) { if (engineVersion < 20160124) { node.Value.Nodes.RemoveAll(x => x.Key == "Charges"); } // Enhance CreateEffectWarhead if (engineVersion < 20160131) { if (node.Key.StartsWith("Warhead") && node.Value.Value == "CreateEffect") { // Add support for multiple explosions to CreateEffectWarhead var explosionNode = node.Value.Nodes.FirstOrDefault(x => x.Key == "Explosion"); if (explosionNode != null) explosionNode.Key = "Explosions"; // Add support for multiple impact sounds to CreateEffectWarhead var impactSoundNode = node.Value.Nodes.FirstOrDefault(x => x.Key == "ImpactSound"); if (impactSoundNode != null) impactSoundNode.Key = "ImpactSounds"; } } // Rename some speed-related Missile properties if (engineVersion < 20160205) { var mod = Game.ModData.Manifest.Mod.Id; if (mod == "ts") { if (node.Key == "Projectile" && node.Value.Value == "Missile") { node.Value.Nodes.Add(new MiniYamlNode("MinimumLaunchSpeed", "75")); node.Value.Nodes.Add(new MiniYamlNode("Speed", "384")); } } else { if (node.Key == "MaximumLaunchSpeed" && parent != null && parent.Value.Value == "Missile") node.Key = "Speed"; } } UpgradeWeaponRules(engineVersion, ref node.Value.Nodes, node, depth + 1); } } internal static void UpgradeTileset(int engineVersion, ref List nodes, MiniYamlNode parent, int depth) { foreach (var node in nodes) { // Add rules here UpgradeTileset(engineVersion, ref node.Value.Nodes, node, depth + 1); } } internal static void UpgradeCursors(int engineVersion, ref List nodes, MiniYamlNode parent, int depth) { foreach (var node in nodes) { // Add rules here UpgradeCursors(engineVersion, ref node.Value.Nodes, node, depth + 1); } } internal static void UpgradePlayers(int engineVersion, ref List nodes, MiniYamlNode parent, int depth) { foreach (var node in nodes) { // Add rules here UpgradePlayers(engineVersion, ref node.Value.Nodes, node, depth + 1); } } internal static void UpgradeChromeMetrics(int engineVersion, ref List nodes, MiniYamlNode parent, int depth) { foreach (var node in nodes) { // Add rules here UpgradeChromeMetrics(engineVersion, ref node.Value.Nodes, node, depth + 1); } } internal static void UpgradeChromeLayout(int engineVersion, ref List nodes, MiniYamlNode parent, int depth) { foreach (var node in nodes) { // Add rules here UpgradeChromeLayout(engineVersion, ref node.Value.Nodes, node, depth + 1); } } internal static void UpgradeActors(int engineVersion, ref List nodes, MiniYamlNode parent, int depth) { foreach (var node in nodes) { // Add rules here UpgradeActors(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 < 6) throw new InvalidDataException("Map format {0} is not supported.\n File: {1}".F(mapFormat, package.Name)); // Format 6 -> 7 combined the Selectable and UseAsShellmap flags into the Class enum if (mapFormat < 7) { MiniYaml useAsShellmap; if (nd.TryGetValue("UseAsShellmap", out useAsShellmap) && bool.Parse(useAsShellmap.Value)) yaml.Nodes.Add(new MiniYamlNode("Visibility", new MiniYaml("Shellmap"))); else if (nd["Type"].Value == "Mission" || nd["Type"].Value == "Campaign") yaml.Nodes.Add(new MiniYamlNode("Visibility", new MiniYaml("MissionSelector"))); } // Format 7 -> 8 replaced normalized HSL triples with rgb(a) hex colors if (mapFormat < 8) { var players = yaml.Nodes.FirstOrDefault(n => n.Key == "Players"); if (players != null) { bool noteHexColors = false; bool noteColorRamp = false; foreach (var player in players.Value.Nodes) { var colorRampNode = player.Value.Nodes.FirstOrDefault(n => n.Key == "ColorRamp"); if (colorRampNode != null) { Color dummy; var parts = colorRampNode.Value.Value.Split(','); if (parts.Length == 3 || parts.Length == 4) { // Try to convert old normalized HSL value to a rgb hex color try { HSLColor color = new HSLColor( (byte)Exts.ParseIntegerInvariant(parts[0].Trim()).Clamp(0, 255), (byte)Exts.ParseIntegerInvariant(parts[1].Trim()).Clamp(0, 255), (byte)Exts.ParseIntegerInvariant(parts[2].Trim()).Clamp(0, 255)); colorRampNode.Value.Value = FieldSaver.FormatValue(color); noteHexColors = true; } catch (Exception) { throw new InvalidDataException("Invalid ColorRamp value.\n File: " + package.Name); } } else if (parts.Length != 1 || !HSLColor.TryParseRGB(parts[0], out dummy)) throw new InvalidDataException("Invalid ColorRamp value.\n File: " + package.Name); colorRampNode.Key = "Color"; noteColorRamp = true; } } if (noteHexColors) Console.WriteLine("ColorRamp is now called Color and uses rgb(a) hex value - rrggbb[aa]."); else if (noteColorRamp) Console.WriteLine("ColorRamp is now called Color."); } } // Format 8 -> 9 moved map options and videos from the map file itself to traits if (mapFormat < 9) { var rules = yaml.Nodes.FirstOrDefault(n => n.Key == "Rules"); var worldNode = rules.Value.Nodes.FirstOrDefault(n => n.Key == "World"); if (worldNode == null) worldNode = new MiniYamlNode("World", new MiniYaml("", new List())); var playerNode = rules.Value.Nodes.FirstOrDefault(n => n.Key == "Player"); if (playerNode == null) playerNode = new MiniYamlNode("Player", new MiniYaml("", new List())); var visibilityNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Visibility"); if (visibilityNode != null) { var visibility = FieldLoader.GetValue("Visibility", visibilityNode.Value.Value); if (visibility.HasFlag(MapVisibility.MissionSelector)) { var missionData = new MiniYamlNode("MissionData", new MiniYaml("", new List())); worldNode.Value.Nodes.Add(missionData); var description = yaml.Nodes.FirstOrDefault(n => n.Key == "Description"); if (description != null) missionData.Value.Nodes.Add(new MiniYamlNode("Briefing", description.Value.Value)); var videos = yaml.Nodes.FirstOrDefault(n => n.Key == "Videos"); if (videos != null && videos.Value.Nodes.Any()) { var backgroundVideo = videos.Value.Nodes.FirstOrDefault(n => n.Key == "BackgroundInfo"); if (backgroundVideo != null) missionData.Value.Nodes.Add(new MiniYamlNode("BackgroundVideo", backgroundVideo.Value.Value)); var briefingVideo = videos.Value.Nodes.FirstOrDefault(n => n.Key == "Briefing"); if (briefingVideo != null) missionData.Value.Nodes.Add(new MiniYamlNode("BriefingVideo", briefingVideo.Value.Value)); var startVideo = videos.Value.Nodes.FirstOrDefault(n => n.Key == "GameStart"); if (startVideo != null) missionData.Value.Nodes.Add(new MiniYamlNode("StartVideo", startVideo.Value.Value)); var winVideo = videos.Value.Nodes.FirstOrDefault(n => n.Key == "GameWon"); if (winVideo != null) missionData.Value.Nodes.Add(new MiniYamlNode("WinVideo", winVideo.Value.Value)); var lossVideo = videos.Value.Nodes.FirstOrDefault(n => n.Key == "GameLost"); if (lossVideo != null) missionData.Value.Nodes.Add(new MiniYamlNode("LossVideo", lossVideo.Value.Value)); } } } var mapOptions = yaml.Nodes.FirstOrDefault(n => n.Key == "Options"); if (mapOptions != null) { var cheats = mapOptions.Value.Nodes.FirstOrDefault(n => n.Key == "Cheats"); if (cheats != null) { worldNode.Value.Nodes.Add(new MiniYamlNode("DeveloperMode", new MiniYaml("", new List() { new MiniYamlNode("Locked", "True"), new MiniYamlNode("Enabled", cheats.Value.Value) }))); } var crates = mapOptions.Value.Nodes.FirstOrDefault(n => n.Key == "Crates"); if (crates != null && !worldNode.Value.Nodes.Any(n => n.Key == "-CrateSpawner")) { if (!FieldLoader.GetValue("crates", crates.Value.Value)) worldNode.Value.Nodes.Add(new MiniYamlNode("-CrateSpawner", new MiniYaml(""))); } var creeps = mapOptions.Value.Nodes.FirstOrDefault(n => n.Key == "Creeps"); if (creeps != null) { worldNode.Value.Nodes.Add(new MiniYamlNode("MapCreeps", new MiniYaml("", new List() { new MiniYamlNode("Locked", "True"), new MiniYamlNode("Enabled", creeps.Value.Value) }))); } var fog = mapOptions.Value.Nodes.FirstOrDefault(n => n.Key == "Fog"); var shroud = mapOptions.Value.Nodes.FirstOrDefault(n => n.Key == "Shroud"); if (fog != null || shroud != null) { var shroudNode = new MiniYamlNode("Shroud", new MiniYaml("", new List())); playerNode.Value.Nodes.Add(shroudNode); if (fog != null) { shroudNode.Value.Nodes.Add(new MiniYamlNode("FogLocked", "True")); shroudNode.Value.Nodes.Add(new MiniYamlNode("FogEnabled", fog.Value.Value)); } if (shroud != null) { var enabled = FieldLoader.GetValue("shroud", shroud.Value.Value); shroudNode.Value.Nodes.Add(new MiniYamlNode("ExploredMapLocked", "True")); shroudNode.Value.Nodes.Add(new MiniYamlNode("ExploredMapEnabled", FieldSaver.FormatValue(!enabled))); } } var allyBuildRadius = mapOptions.Value.Nodes.FirstOrDefault(n => n.Key == "AllyBuildRadius"); if (allyBuildRadius != null) { worldNode.Value.Nodes.Add(new MiniYamlNode("MapBuildRadius", new MiniYaml("", new List() { new MiniYamlNode("AllyBuildRadiusLocked", "True"), new MiniYamlNode("AllyBuildRadiusEnabled", allyBuildRadius.Value.Value) }))); } var startingCash = mapOptions.Value.Nodes.FirstOrDefault(n => n.Key == "StartingCash"); if (startingCash != null) { playerNode.Value.Nodes.Add(new MiniYamlNode("PlayerResources", new MiniYaml("", new List() { new MiniYamlNode("DefaultCashLocked", "True"), new MiniYamlNode("DefaultCash", startingCash.Value.Value) }))); } var startingUnits = mapOptions.Value.Nodes.FirstOrDefault(n => n.Key == "ConfigurableStartingUnits"); if (startingUnits != null && !worldNode.Value.Nodes.Any(n => n.Key == "-SpawnMPUnits")) { worldNode.Value.Nodes.Add(new MiniYamlNode("SpawnMPUnits", new MiniYaml("", new List() { new MiniYamlNode("Locked", "True"), }))); } var techLevel = mapOptions.Value.Nodes.FirstOrDefault(n => n.Key == "TechLevel"); var difficulties = mapOptions.Value.Nodes.FirstOrDefault(n => n.Key == "Difficulties"); var shortGame = mapOptions.Value.Nodes.FirstOrDefault(n => n.Key == "ShortGame"); if (techLevel != null || difficulties != null || shortGame != null) { var optionsNode = new MiniYamlNode("MapOptions", new MiniYaml("", new List())); worldNode.Value.Nodes.Add(optionsNode); if (techLevel != null) { optionsNode.Value.Nodes.Add(new MiniYamlNode("TechLevelLocked", "True")); optionsNode.Value.Nodes.Add(new MiniYamlNode("TechLevel", techLevel.Value.Value)); } if (difficulties != null) optionsNode.Value.Nodes.Add(new MiniYamlNode("Difficulties", difficulties.Value.Value)); if (shortGame != null) { optionsNode.Value.Nodes.Add(new MiniYamlNode("ShortGameLocked", "True")); optionsNode.Value.Nodes.Add(new MiniYamlNode("ShortGameEnabled", shortGame.Value.Value)); } } } if (worldNode.Value.Nodes.Any() && !rules.Value.Nodes.Contains(worldNode)) rules.Value.Nodes.Add(worldNode); if (playerNode.Value.Nodes.Any() && !rules.Value.Nodes.Contains(playerNode)) rules.Value.Nodes.Add(playerNode); } // Format 9 -> 10 moved smudges to SmudgeLayer, and uses map.png for all maps if (mapFormat < 10) { ExtractSmudges(yaml); if (package.Contains("map.png")) yaml.Nodes.Add(new MiniYamlNode("LockPreview", new MiniYaml("True"))); } 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())); } static void ExtractSmudges(MiniYaml yaml) { var smudges = yaml.Nodes.FirstOrDefault(n => n.Key == "Smudges"); if (smudges == null || !smudges.Value.Nodes.Any()) return; var scorches = new List(); var craters = new List(); foreach (var s in smudges.Value.Nodes) { // loc=type,loc,depth var parts = s.Key.Split(' '); var value = "{0},{1}".F(parts[0], parts[2]); var node = new MiniYamlNode(parts[1], value); if (parts[0].StartsWith("sc")) scorches.Add(node); else if (parts[0].StartsWith("cr")) craters.Add(node); } var rulesNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Rules"); if (rulesNode == null) { rulesNode = new MiniYamlNode("Rules", new MiniYaml("", new List())); yaml.Nodes.Add(rulesNode); } var worldNode = rulesNode.Value.Nodes.FirstOrDefault(n => n.Key == "World"); if (worldNode == null) { worldNode = new MiniYamlNode("World", new MiniYaml("", new List())); rulesNode.Value.Nodes.Add(rulesNode); } if (scorches.Any()) { var initialScorches = new MiniYamlNode("InitialSmudges", new MiniYaml("", scorches)); var smudgeLayer = new MiniYamlNode("SmudgeLayer@SCORCH", new MiniYaml("", new List() { initialScorches })); worldNode.Value.Nodes.Add(smudgeLayer); } if (craters.Any()) { var initialCraters = new MiniYamlNode("InitialSmudges", new MiniYaml("", craters)); var smudgeLayer = new MiniYamlNode("SmudgeLayer@CRATER", new MiniYaml("", new List() { initialCraters })); worldNode.Value.Nodes.Add(smudgeLayer); } } } }