#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 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:"); var ruleGroups = new Dictionary>(); // Print known tags Console.WriteLine(" Update Paths:"); foreach (var p in UpdatePath.KnownPaths) { Console.WriteLine(" " + p); ruleGroups[p] = UpdatePath.FromSource(modData.ObjectCreator, p, false) .Select(r => r.GetType().Name) .Where(r => !ruleGroups.Values.Any(g => g.Contains(r))) .ToList(); } // Print known rules Console.WriteLine(" Individual Rules:"); foreach (var kv in ruleGroups) { if (!kv.Value.Any()) continue; Console.WriteLine(" " + kv.Key + ":"); foreach (var r in kv.Value) Console.WriteLine(" " + r); } var other = UpdatePath.KnownRules(modData.ObjectCreator) .Where(r => !ruleGroups.Values.Any(g => g.Contains(r))); if (other.Any()) { Console.WriteLine(" Other:"); foreach (var r in other) 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(); 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 Log(StreamWriter logWriter, string format, params object[] args) { logWriter.Write(format, args); Console.Write(format, args); } static void LogLine(StreamWriter logWriter, string format, params object[] args) { logWriter.WriteLine(format, args); Console.WriteLine(format, args); } static void LogLine(StreamWriter logWriter) { logWriter.WriteLine(); Console.WriteLine(); } static void ApplyRules(ModData modData, IEnumerable rules, bool skipMaps) { Console.WriteLine(); var logWriter = File.CreateText("update.log"); logWriter.AutoFlush = true; var externalFilenames = new HashSet(); foreach (var rule in rules) { var manualSteps = new List(); var allFiles = new YamlFileSet(); LogLine(logWriter, "{0}: {1}", rule.GetType().Name, rule.Name); try { Log(logWriter, " Updating mod... "); manualSteps.AddRange(UpdateUtils.UpdateMod(modData, rule, out allFiles, externalFilenames)); LogLine(logWriter, "COMPLETE"); } catch (Exception ex) { Console.WriteLine("FAILED"); LogLine(logWriter); LogLine(logWriter, " The automated changes for this rule were not applied because of an error."); LogLine(logWriter, " After the issue reported below is resolved you should run the updater"); LogLine(logWriter, " with SOURCE set to {0} to retry these changes", rule.GetType().Name); LogLine(logWriter); LogLine(logWriter, " The exception reported was:"); LogLine(logWriter, " " + ex.ToString().Replace("\n", "\n ")); continue; } Log(logWriter, " Updating system maps... "); if (!skipMaps) { var mapsFailed = false; var mapExternalFilenames = new HashSet(); foreach (var package in modData.MapCache.EnumerateMapPackagesWithoutCaching()) { try { YamlFileSet mapFiles; var mapSteps = UpdateUtils.UpdateMap(modData, package, rule, out mapFiles, mapExternalFilenames); allFiles.AddRange(mapFiles); if (mapSteps.Any()) manualSteps.Add("Map: " + package.Name + ":\n" + UpdateUtils.FormatMessageList(mapSteps)); } catch (Exception ex) { LogLine(logWriter, "FAILED"); LogLine(logWriter); LogLine(logWriter, " The automated changes for this rule were not applied because of an error."); LogLine(logWriter, " After the issue reported below is resolved you should run the updater"); LogLine(logWriter, " with SOURCE set to {0} to retry these changes", rule.GetType().Name); LogLine(logWriter); LogLine(logWriter, " The map that caused the error was:"); LogLine(logWriter, " " + package.Name); LogLine(logWriter); LogLine(logWriter, " The exception reported was:"); LogLine(logWriter, " " + ex.ToString().Replace("\n", "\n ")); mapsFailed = true; break; } } if (mapsFailed) continue; LogLine(logWriter, "COMPLETE"); } else LogLine(logWriter, "SKIPPED"); // Files are saved after each successful automated rule update allFiles.Save(); if (manualSteps.Any()) { LogLine(logWriter, " Manual changes are required to complete this update:"); LogLine(logWriter, UpdateUtils.FormatMessageList(manualSteps, 1)); } LogLine(logWriter); } if (externalFilenames.Any()) { LogLine(logWriter, "The following external mod files have been ignored:"); LogLine(logWriter, UpdateUtils.FormatMessageList(externalFilenames)); LogLine(logWriter, "These files should be updated by running --update-mod on the referenced mod(s)"); LogLine(logWriter); } Console.WriteLine("Semi-automated update complete."); Console.WriteLine("Please review the messages above for any manual actions that must be applied."); Console.WriteLine("These messages have also been written to an update.log file in the current directory."); } } }