private static async Task <int> RunAsync(Options options) { var sourceDirectories = new List <string> { new DirectoryInfo(options.DownloadDirectoryPath).FullName, }; DirectoryInfo dumpDirectory = new DirectoryInfo(options.OutputDirectoryPath); bool needsDelete = dumpDirectory.Exists && dumpDirectory.EnumerateFileSystemInfos().Any(); if (needsDelete && !options.Scorch) { Console.Error.WriteLine("Output folder exists already and is not empty. Aborting..."); Console.WriteLine("(run with -x / --scorch to have us automatically delete instead)."); return(2); } dumpDirectory.Create(); // HACK: these really shouldn't be hardcoded like this, but it's better than nothing. // ideal would be to dynamically detect parameters from the pack files themselves and // offer up a dynamic UI that lets the user specify whatever they want from that. this // works for now, though, and it might be good enough for a long time. uint screenHeight = options.ScreenHeight; uint screenWidth = options.ScreenWidth; bool isFullScreen = options.FullScreenMode.HasFlag(FullScreenMode.IsFullScreen); bool isBorderless = options.FullScreenMode.HasFlag(FullScreenMode.IsBorderless); bool isBorderlessFullScreen = options.FullScreenMode.HasFlag(FullScreenMode.IsBorderless | FullScreenMode.IsFullScreen); // MAGIC1: TAN(65 * PI / 360) / (16 / 10) // MAGIC2: 360 / PI // Magic numbers in MAGIC1's definition come from the claim that the ideal Skyrim FOV is // 65 degrees, based on a 16x10 resolution. const double MAGIC1 = 0.3981689130046832; const double MAGIC2 = 114.591559026164642; double optimalSkyrimFOVDegrees = Math.Atan(screenWidth / (double)screenHeight * MAGIC1) * MAGIC2; var modpacks = new List <Modpack>(); // lots of strings show up multiple times each. StringPool pool = new StringPool(); string gameInstallPath = null; string gameDataPath = null; HashSet <string> seenSoFar = new HashSet <string>(); int longestOutputPathLength = 0; foreach (string packDefinitionFilePath in options.PackDefinitionFilePaths) { bool requiresJava, requiresSteam, requiresSkyrim; string steamPath = null; XDocument doc; using (FileStream packDefinitionFileStream = AsyncFile.OpenReadSequential(packDefinitionFilePath)) using (StreamReader reader = new StreamReader(packDefinitionFileStream, Encoding.UTF8, false, 4096, true)) { string docText = await reader.ReadToEndAsync().ConfigureAwait(false); requiresJava = docText.Contains("{JavaBinFolderForwardSlashes}", StringComparison.Ordinal); requiresSteam = docText.Contains("{SteamInstallFolder}", StringComparison.Ordinal) || docText.Contains("{SteamInstallFolderEscapeBackslashes}", StringComparison.Ordinal); requiresSkyrim = docText.Contains("{SkyrimInstallFolder}", StringComparison.Ordinal) || docText.Contains("{SkyrimInstallFolderForwardSlashes}", StringComparison.Ordinal) || docText.Contains("{SkyrimInstallFolderEscapeBackslashes}", StringComparison.Ordinal); StringBuilder docTextBuilder = new StringBuilder(docText); docTextBuilder = docTextBuilder.Replace("{DumpFolderForwardSlashes}", dumpDirectory.FullName.Replace(Path.DirectorySeparatorChar, '/')) .Replace("{DumpFolderEscapeBackslashes}", dumpDirectory.FullName.Replace("\\", "\\\\")) .Replace("{ScreenHeight}", screenHeight.ToString(CultureInfo.InvariantCulture)) .Replace("{ScreenWidth}", screenWidth.ToString(CultureInfo.InvariantCulture)) .Replace("{IsFullScreenTrueFalse}", isFullScreen ? "true" : "false") .Replace("{IsFullScreenNumeric}", isFullScreen ? "1" : "0") .Replace("{IsBorderlessTrueFalse}", isBorderless ? "true" : "false") .Replace("{IsBorderlessNumeric}", isBorderless ? "1" : "0") .Replace("{IsBorderlessFullScreenTrueFalse}", isBorderlessFullScreen ? "true" : "false") .Replace("{IsBorderlessFullScreenNumeric}", isBorderlessFullScreen ? "1" : "0") .Replace("{OptimalSkyrimFOVDegrees}", optimalSkyrimFOVDegrees.ToString("F2", CultureInfo.InvariantCulture)) .Replace("{GraphicsPreset}", options.GraphicsPreset.ToString().ToLowerInvariant()); if (requiresJava) { string javaBinPath = GetJavaBinDirectoryPath(options); if (String.IsNullOrEmpty(javaBinPath)) { Console.Error.WriteLine("--javaBinFolder is required for {0}.", XDocument.Parse(docTextBuilder.ToString()).Element("Modpack").Attribute("Name").Value); return(6); } DirectoryInfo javaBinDirectory = new DirectoryInfo(javaBinPath); javaBinPath = javaBinDirectory.FullName; if (!javaBinDirectory.EnumerateFiles().Any(fl => "javaw.exe".Equals(fl.Name, StringComparison.OrdinalIgnoreCase))) { Console.Error.WriteLine("Java bin folder {0} does not contain a file called \"javaw.exe\".", javaBinPath); return(14); } docTextBuilder = docTextBuilder.Replace("{JavaBinFolderForwardSlashes}", javaBinPath.Replace(Path.DirectorySeparatorChar, '/')); } if (requiresSteam) { if (options.SteamDirectoryPath == null) { Console.Error.WriteLine("-s / --steamFolder is required for {0}.", XDocument.Parse(docTextBuilder.ToString()).Element("Modpack").Attribute("Name").Value); return(11); } steamPath = new DirectoryInfo(options.SteamDirectoryPath).FullName; docTextBuilder = docTextBuilder.Replace("{SteamInstallFolder}", steamPath) .Replace("{SteamInstallFolderEscapeBackslashes}", steamPath.Replace("\\", "\\\\")); // pack files targeting versions earlier than 0.9.3.0 would need this; other // pack files won't use this anyway, so it's just a small waste of time. gameInstallPath = Path.Combine(steamPath, "steamapps", "common", "Skyrim"); } if (requiresSkyrim) { gameInstallPath = GetSkyrimDirectoryPath(options); if (String.IsNullOrEmpty(gameInstallPath)) { Console.Error.WriteLine("-s / --steamFolder, or a valid Skyrim registry key, is required for {0}.", XDocument.Parse(docTextBuilder.ToString()).Element("Modpack").Attribute("Name").Value); return(12); } gameInstallPath = new DirectoryInfo(gameInstallPath).FullName; docTextBuilder = docTextBuilder.Replace("{SkyrimInstallFolder}", gameInstallPath) .Replace("{SkyrimInstallFolderForwardSlashes}", gameInstallPath.Replace(Path.DirectorySeparatorChar, '/')) .Replace("{SkyrimInstallFolderEscapeBackslashes}", gameInstallPath.Replace("\\", "\\\\")); } doc = XDocument.Parse(docTextBuilder.ToString()); } doc = doc.PoolStrings(pool); foreach (var modpackElement in doc.Descendants("Modpack")) { var modpack = new Modpack(modpackElement); modpacks.Add(modpack); var currentToolVersion = Assembly.GetExecutingAssembly().GetName().Version; if (currentToolVersion < modpack.MinimumToolVersion) { Console.Error.WriteLine("Current tool version ({0}) is lower than the minimum tool version ({1}) required for this pack.", currentToolVersion, modpack.MinimumToolVersion); return(3); } if (modpack.MinimumToolVersion < new Version("2.0.0.0")) { Console.Error.WriteLine("Modpacks designed for tool versions earlier than 2.x are no longer supported in tool versions 2.x and above."); return(15); } if (!seenSoFar.IsSupersetOf(modpack.Requirements)) { Console.Error.WriteLine("{0} needs to be set up in the same run, after all of the following are set up as well: {1}", modpack.Name, String.Join(", ", modpack.Requirements)); return(4); } if (seenSoFar.Contains(modpack.Name)) { Console.Error.WriteLine("Trying to set up {0} twice in the same run", modpack.Name); return(5); } seenSoFar.Add(modpack.Name); seenSoFar.Add(modpack.Name + ", " + modpack.PackVersion); seenSoFar.Add(modpack.Name + ", " + modpack.PackVersion + ", " + modpack.FileVersion); if (longestOutputPathLength < modpack.LongestOutputPathLength) { longestOutputPathLength = modpack.LongestOutputPathLength.Value; } switch (modpack.Game) { case Game.Unknown: break; case Game.Skyrim2011: if (requiresSkyrim) { break; } gameInstallPath = GetSkyrimDirectoryPath(options); if (String.IsNullOrEmpty(gameInstallPath)) { Console.Error.WriteLine("-s / --steamFolder, or a valid Skyrim registry key, is required for {0}.", modpack.Name); return(12); } gameInstallPath = new DirectoryInfo(gameInstallPath).FullName; requiresSkyrim = true; break; default: Console.Error.WriteLine("Unrecognized game: {0}", modpack.Game); return(13); } if (requiresSkyrim) { sourceDirectories.Add(gameDataPath = Path.Combine(gameInstallPath, "Data")); } } } // minus 1 for the path separator char. longestOutputPathLength = 255 - longestOutputPathLength - 1; if (!options.SkipOutputDirectoryPathLengthCheck && longestOutputPathLength < dumpDirectory.FullName.Length) { Console.Error.WriteLine(Invariant($"Output directory ({options.OutputDirectoryPath}, {options.OutputDirectoryPath.Length} chars) exceeds the maximum supported length of {longestOutputPathLength} chars.")); return(7); } Console.WriteLine("Checking existing files..."); var groups = modpacks.SelectMany( modpack => modpack .CheckedFiles .SelectMany(grp => grp.CheckedFiles) .GroupBy(fl => modpack.Name + "|" + (fl.Option ?? fl.Name))) .ToArray(); // we hit all the files in the Skyrim directory, which has lots of gigabytes of BSAs we // don't care about, and the MO download folder itself might have tons of crap we don't // care about either. quick and easy fix is to just look at files whose lengths match. // not absolutely 100% perfect, but good enough. var sizes = new Dictionary <long, Hashes>(); foreach (var fl in groups.SelectMany(grp => grp)) { if (!sizes.TryGetValue(fl.LengthInBytes, out var hashesToCheck)) { hashesToCheck = Hashes.Md5; } if (fl.Sha512Checksum != default) { hashesToCheck |= Hashes.Sha512; } sizes[fl.LengthInBytes] = hashesToCheck; } var sourceFiles = sourceDirectories.Distinct(StringComparer.OrdinalIgnoreCase) .Select(dir => new DirectoryInfo(dir).EnumerateFiles() .Where(fl => sizes.ContainsKey(fl.Length)) .Select(fl => (fl, sizes[fl.Length]))