wabbajack/Wabbajack.Lib/MO2Installer.cs

402 lines
16 KiB
C#
Raw Normal View History

2019-10-16 21:36:14 +00:00
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Windows;
using Alphaleonis.Win32.Filesystem;
using IniParser;
2020-01-03 15:01:17 +00:00
using IniParser.Model;
using IniParser.Model.Configuration;
using IniParser.Parser;
using Wabbajack.Common;
2019-12-04 04:12:08 +00:00
using Wabbajack.Lib.CompilationSteps.CompilationErrors;
using Wabbajack.Lib.Downloaders;
using Wabbajack.Lib.NexusApi;
using Wabbajack.Lib.Validation;
using Directory = Alphaleonis.Win32.Filesystem.Directory;
using File = Alphaleonis.Win32.Filesystem.File;
using Path = Alphaleonis.Win32.Filesystem.Path;
using SectionData = Wabbajack.Common.SectionData;
namespace Wabbajack.Lib
{
public class MO2Installer : AInstaller
{
public bool WarnOnOverwrite { get; set; } = true;
public override ModManager ModManager => ModManager.MO2;
2019-09-24 04:20:24 +00:00
public string GameFolder { get; set; }
2020-01-07 13:50:11 +00:00
public MO2Installer(string archive, ModList modList, string outputFolder, string downloadFolder, SystemParameters parameters)
: base(
archive: archive,
modList: modList,
outputFolder: outputFolder,
2020-01-07 13:50:11 +00:00
downloadFolder: downloadFolder,
parameters: parameters)
{
}
protected override async Task<bool> _Begin(CancellationToken cancel)
{
if (cancel.IsCancellationRequested) return false;
var metric = Metrics.Send(Metrics.BeginInstall, ModList.Name);
2019-12-15 04:33:48 +00:00
ConfigureProcessor(20, ConstructDynamicNumThreads(await RecommendQueueSize()));
var game = ModList.GameType.MetaData();
if (GameFolder == null)
GameFolder = game.GameLocation();
if (GameFolder == null)
{
await Utils.Log(new CriticalFailureIntervention(
2020-01-13 21:11:07 +00:00
$"In order to do a proper install Wabbajack needs to know where your {game.MO2Name} folder resides. We tried looking the " +
"game location up in the Windows Registry but were unable to find it, please make sure you launch the game once before running this installer. ",
"Could not find game location")).Task;
Utils.Log("Exiting because we couldn't find the game folder.");
return false;
}
if (cancel.IsCancellationRequested) return false;
2019-11-24 23:03:36 +00:00
UpdateTracker.NextStep("Validating Game ESMs");
ValidateGameESMs();
2019-11-24 23:03:36 +00:00
if (cancel.IsCancellationRequested) return false;
2019-11-24 23:03:36 +00:00
UpdateTracker.NextStep("Validating Modlist");
await ValidateModlist.RunValidation(Queue, ModList);
Directory.CreateDirectory(OutputFolder);
Directory.CreateDirectory(DownloadFolder);
if (Directory.Exists(Path.Combine(OutputFolder, "mods")) && WarnOnOverwrite)
2019-09-04 21:19:37 +00:00
{
2019-12-07 02:54:27 +00:00
if ((await Utils.Log(new ConfirmUpdateOfExistingInstall { ModListName = ModList.Name, OutputFolder = OutputFolder }).Task) == ConfirmUpdateOfExistingInstall.Choice.Abort)
{
2019-12-21 19:37:48 +00:00
Utils.Log("Exiting installation at the request of the user, existing mods folder found.");
return false;
}
2019-09-04 21:19:37 +00:00
}
2019-09-14 04:35:42 +00:00
if (cancel.IsCancellationRequested) return false;
2020-01-13 21:11:07 +00:00
UpdateTracker.NextStep("Optimizing ModList");
await OptimizeModlist();
2019-08-24 23:20:54 +00:00
if (cancel.IsCancellationRequested) return false;
2019-11-24 23:03:36 +00:00
UpdateTracker.NextStep("Hashing Archives");
await HashArchives();
2019-11-24 23:03:36 +00:00
if (cancel.IsCancellationRequested) return false;
2019-11-24 23:03:36 +00:00
UpdateTracker.NextStep("Downloading Missing Archives");
await DownloadArchives();
2019-11-24 23:03:36 +00:00
if (cancel.IsCancellationRequested) return false;
2019-11-24 23:03:36 +00:00
UpdateTracker.NextStep("Hashing Remaining Archives");
await HashArchives();
var missing = ModList.Archives.Where(a => !HashedArchives.ContainsKey(a.Hash)).ToList();
if (missing.Count > 0)
{
foreach (var a in missing)
2019-09-26 22:32:15 +00:00
Info($"Unable to download {a.Name}");
if (IgnoreMissingFiles)
Info("Missing some archives, but continuing anyways at the request of the user");
else
Error("Cannot continue, was unable to download one or more archives");
}
if (cancel.IsCancellationRequested) return false;
2019-11-24 23:03:36 +00:00
UpdateTracker.NextStep("Priming VFS");
2019-12-07 02:54:27 +00:00
await PrimeVFS();
if (cancel.IsCancellationRequested) return false;
2019-11-24 23:03:36 +00:00
UpdateTracker.NextStep("Building Folder Structure");
BuildFolderStructure();
2019-11-24 23:03:36 +00:00
if (cancel.IsCancellationRequested) return false;
2019-11-24 23:03:36 +00:00
UpdateTracker.NextStep("Installing Archives");
await InstallArchives();
2019-11-24 23:03:36 +00:00
if (cancel.IsCancellationRequested) return false;
2019-11-24 23:03:36 +00:00
UpdateTracker.NextStep("Installing Included files");
await InstallIncludedFiles();
2019-11-24 23:03:36 +00:00
if (cancel.IsCancellationRequested) return false;
2019-11-24 23:03:36 +00:00
UpdateTracker.NextStep("Installing Archive Metas");
await InstallIncludedDownloadMetas();
2019-11-24 23:03:36 +00:00
if (cancel.IsCancellationRequested) return false;
2019-11-24 23:03:36 +00:00
UpdateTracker.NextStep("Building BSAs");
await BuildBSAs();
if (cancel.IsCancellationRequested) return false;
2019-11-24 23:03:36 +00:00
UpdateTracker.NextStep("Generating Merges");
await zEditIntegration.GenerateMerges(this);
2019-11-02 15:38:03 +00:00
UpdateTracker.NextStep("Set MO2 into portable");
ForcePortable();
UpdateTracker.NextStep("Create Empty Output Mods");
CreateOutputMods();
UpdateTracker.NextStep("Updating System-specific ini settings");
SetScreenSizeInPrefs();
2019-11-24 23:03:36 +00:00
UpdateTracker.NextStep("Installation complete! You may exit the program.");
var metric2 = Metrics.Send(Metrics.FinishInstall, ModList.Name);
2019-12-15 04:33:48 +00:00
return true;
}
private void CreateOutputMods()
{
Directory.EnumerateFiles(Path.Combine(OutputFolder, "profiles"), "settings.ini", DirectoryEnumerationOptions.Recursive).Do(f =>
{
var ini = f.LoadIniFile();
if (ini == null)
{
Utils.Log($"settings.ini is null for {f}, skipping");
return;
}
var overwrites = ini.custom_overwrites;
if (overwrites == null)
{
Utils.Log("No custom overwrites found, skipping");
return;
}
if (overwrites is SectionData data)
{
data.Coll.Do(keyData =>
{
var v = keyData.Value;
var mod = Path.Combine(OutputFolder, "mods", v);
if (!Directory.Exists(mod))
Directory.CreateDirectory(mod);
});
}
});
}
private void ForcePortable()
{
var path = Path.Combine(OutputFolder, "portable.txt");
if (File.Exists(path)) return;
try
{
File.WriteAllText(path, "Created by Wabbajack");
}
catch (Exception e)
{
Utils.Error(e, $"Could not create portable.txt in {OutputFolder}");
}
}
private async Task InstallIncludedDownloadMetas()
{
await ModList.Directives
.OfType<ArchiveMeta>()
2019-11-17 04:16:42 +00:00
.PMap(Queue, directive =>
{
Status($"Writing included .meta file {directive.To}");
2019-11-21 15:51:57 +00:00
var outPath = Path.Combine(DownloadFolder, directive.To);
if (File.Exists(outPath)) File.Delete(outPath);
File.WriteAllBytes(outPath, LoadBytesFromPath(directive.SourceDataID));
});
}
private void ValidateGameESMs()
{
foreach (var esm in ModList.Directives.OfType<CleanedESM>().ToList())
{
var filename = Path.GetFileName(esm.To);
2019-11-21 15:51:57 +00:00
var gameFile = Path.Combine(GameFolder, "Data", filename);
Utils.Log($"Validating {filename}");
2019-11-21 15:51:57 +00:00
var hash = gameFile.FileHash();
if (hash != esm.SourceESMHash)
{
Utils.ErrorThrow(new InvalidGameESMError(esm, hash, gameFile));
}
}
}
private async Task BuildBSAs()
2019-07-28 23:04:23 +00:00
{
var bsas = ModList.Directives.OfType<CreateBSA>().ToList();
Info($"Building {bsas.Count} bsa files");
2019-07-28 23:04:23 +00:00
foreach (var bsa in bsas)
2019-07-28 23:04:23 +00:00
{
Status($"Building {bsa.To}");
2019-11-21 15:51:57 +00:00
var sourceDir = Path.Combine(OutputFolder, Consts.BSACreationDir, bsa.TempID);
2019-07-28 23:04:23 +00:00
2019-10-11 23:31:36 +00:00
using (var a = bsa.State.MakeBuilder())
{
await bsa.FileStates.PMap(Queue, state =>
2019-07-28 23:04:23 +00:00
{
2019-10-11 23:31:36 +00:00
Status($"Adding {state.Path} to BSA");
2019-11-21 15:51:57 +00:00
using (var fs = File.OpenRead(Path.Combine(sourceDir, state.Path)))
2019-09-14 04:35:42 +00:00
{
2019-10-11 23:31:36 +00:00
a.AddFile(state, fs);
}
});
Info($"Writing {bsa.To}");
a.Build(Path.Combine(OutputFolder, bsa.To));
2019-10-11 23:31:36 +00:00
}
}
2019-11-21 15:51:57 +00:00
var bsaDir = Path.Combine(OutputFolder, Consts.BSACreationDir);
if (Directory.Exists(bsaDir))
{
Info($"Removing temp folder {Consts.BSACreationDir}");
Utils.DeleteDirectory(bsaDir);
}
2019-07-28 23:04:23 +00:00
}
private async Task InstallIncludedFiles()
2019-07-23 04:27:26 +00:00
{
Info("Writing inline files");
await ModList.Directives
2019-09-14 04:35:42 +00:00
.OfType<InlineFile>()
2019-11-17 04:16:42 +00:00
.PMap(Queue, directive =>
2019-09-14 04:35:42 +00:00
{
2019-09-26 22:32:15 +00:00
Status($"Writing included file {directive.To}");
2019-11-21 15:51:57 +00:00
var outPath = Path.Combine(OutputFolder, directive.To);
if (File.Exists(outPath)) File.Delete(outPath);
2019-09-14 04:35:42 +00:00
if (directive is RemappedInlineFile)
2019-10-07 17:33:34 +00:00
WriteRemappedFile((RemappedInlineFile)directive);
2019-09-14 04:35:42 +00:00
else if (directive is CleanedESM)
2019-10-07 17:33:34 +00:00
GenerateCleanedESM((CleanedESM)directive);
2019-09-14 04:35:42 +00:00
else
2019-11-21 15:51:57 +00:00
File.WriteAllBytes(outPath, LoadBytesFromPath(directive.SourceDataID));
2019-09-14 04:35:42 +00:00
});
2019-07-23 04:27:26 +00:00
}
2019-08-25 03:46:32 +00:00
private void GenerateCleanedESM(CleanedESM directive)
{
var filename = Path.GetFileName(directive.To);
2019-11-21 15:51:57 +00:00
var gameFile = Path.Combine(GameFolder, "Data", filename);
2019-08-25 03:46:32 +00:00
Info($"Generating cleaned ESM for {filename}");
2019-11-21 15:51:57 +00:00
if (!File.Exists(gameFile)) throw new InvalidDataException($"Missing {filename} at {gameFile}");
2019-08-25 03:46:32 +00:00
Status($"Hashing game version of {filename}");
2019-11-21 15:51:57 +00:00
var sha = gameFile.FileHash();
2019-08-25 03:46:32 +00:00
if (sha != directive.SourceESMHash)
2019-09-14 04:35:42 +00:00
throw new InvalidDataException(
$"Cannot patch {filename} from the game folder because the hashes do not match. Have you already cleaned the file?");
2019-08-25 03:46:32 +00:00
2019-11-21 15:51:57 +00:00
var patchData = LoadBytesFromPath(directive.SourceDataID);
var toFile = Path.Combine(OutputFolder, directive.To);
2019-08-25 03:46:32 +00:00
Status($"Patching {filename}");
using (var output = File.Open(toFile, FileMode.Create))
2019-11-21 15:51:57 +00:00
using (var input = File.OpenRead(gameFile))
2019-09-14 04:35:42 +00:00
{
2019-11-21 15:51:57 +00:00
BSDiff.Apply(input, () => new MemoryStream(patchData), output);
2019-08-25 03:46:32 +00:00
}
}
private void SetScreenSizeInPrefs()
{
var config = new IniParserConfiguration {AllowDuplicateKeys = true, AllowDuplicateSections = true};
foreach (var file in Directory.EnumerateFiles(Path.Combine(OutputFolder, "profiles"), "*refs.ini",
DirectoryEnumerationOptions.Recursive))
{
2020-01-03 15:01:17 +00:00
try
{
var parser = new FileIniDataParser(new IniDataParser(config));
var data = parser.ReadFile(file);
if (data.Sections["Display"] == null)
return;
if (data.Sections["Display"]["iSize W"] != null && data.Sections["Display"]["iSize H"] != null)
{
2020-01-07 13:50:11 +00:00
data.Sections["Display"]["iSize W"] = SystemParameters.ScreenWidth.ToString(CultureInfo.CurrentCulture);
data.Sections["Display"]["iSize H"] = SystemParameters.ScreenHeight.ToString(CultureInfo.CurrentCulture);
}
parser.WriteFile(file, data);
2020-01-03 15:01:17 +00:00
}
catch (Exception ex)
{
Utils.Log($"Skipping screen size remap for {file} due to parse error.");
continue;
}
}
}
2019-08-24 23:20:54 +00:00
private void WriteRemappedFile(RemappedInlineFile directive)
{
var data = Encoding.UTF8.GetString(LoadBytesFromPath(directive.SourceDataID));
2019-08-24 23:20:54 +00:00
data = data.Replace(Consts.GAME_PATH_MAGIC_BACK, GameFolder);
data = data.Replace(Consts.GAME_PATH_MAGIC_DOUBLE_BACK, GameFolder.Replace("\\", "\\\\"));
data = data.Replace(Consts.GAME_PATH_MAGIC_FORWARD, GameFolder.Replace("\\", "/"));
data = data.Replace(Consts.MO2_PATH_MAGIC_BACK, OutputFolder);
data = data.Replace(Consts.MO2_PATH_MAGIC_DOUBLE_BACK, OutputFolder.Replace("\\", "\\\\"));
data = data.Replace(Consts.MO2_PATH_MAGIC_FORWARD, OutputFolder.Replace("\\", "/"));
2019-08-24 23:20:54 +00:00
data = data.Replace(Consts.DOWNLOAD_PATH_MAGIC_BACK, DownloadFolder);
data = data.Replace(Consts.DOWNLOAD_PATH_MAGIC_DOUBLE_BACK, DownloadFolder.Replace("\\", "\\\\"));
data = data.Replace(Consts.DOWNLOAD_PATH_MAGIC_FORWARD, DownloadFolder.Replace("\\", "/"));
File.WriteAllText(Path.Combine(OutputFolder, directive.To), data);
}
public static IErrorResponse CheckValidInstallPath(string path, string downloadFolder)
{
var ret = Utils.IsDirectoryPathValid(path);
if (!ret.Succeeded) return ret;
if (!Directory.Exists(path)) return ErrorResponse.Success;
2020-01-13 21:11:07 +00:00
// Check folder does not have a Wabbajack ModList
foreach (var file in Directory.EnumerateFiles(path))
{
if (!File.Exists(file)) continue;
if (System.IO.Path.GetExtension(file).Equals(Consts.ModListExtension))
{
2020-01-13 21:11:07 +00:00
return ErrorResponse.Fail($"Cannot install into a folder with a Wabbajack ModList inside of it");
}
}
// Check folder is either empty, or a likely valid previous install
if (!Directory.IsEmpty(path))
{
// If we have a MO2 install, assume good to go
if (Directory.EnumerateFiles(path).Any(file =>
{
var fileName = Path.GetFileName(file);
if (fileName.Equals("ModOrganizer.exe", StringComparison.OrdinalIgnoreCase)) return true;
if (fileName.Equals("ModOrganizer.ini", StringComparison.OrdinalIgnoreCase)) return true;
return false;
}))
{
return ErrorResponse.Success;
}
// If we don't have a MO2 install, and there's any file that's not in the downloads folder, mark failure
if (Directory.EnumerateFiles(path).Any(file =>
{
var fileName = Path.GetFileName(file);
if (string.IsNullOrWhiteSpace(downloadFolder)) return true;
return !Utils.IsUnderneathDirectory(file, downloadFolder);
}))
{
return ErrorResponse.Fail($"Cannot install into a non-empty folder that does not look like a previous WJ installation.\n" +
$"To override, delete all installed files from your target installation folder. Any files in your download folder are okay to keep.");
}
}
return ErrorResponse.Success;
}
}
2019-09-24 15:26:44 +00:00
}