wabbajack/Wabbajack.Lib/MO2Installer.cs

508 lines
20 KiB
C#
Raw Normal View History

2019-10-16 21:36:14 +00:00
using System;
2020-03-09 21:07:57 +00:00
using System.ComponentModel.DataAnnotations;
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;
using System.Collections.Generic;
2020-09-12 20:23:03 +00:00
using Wabbajack.VirtualFileSystem;
namespace Wabbajack.Lib
{
public class MO2Installer : AInstaller
{
public bool WarnOnOverwrite { get; set; } = true;
public override ModManager ModManager => ModManager.MO2;
2020-03-25 22:30:43 +00:00
public AbsolutePath? GameFolder { get; set; }
2020-03-26 21:15:44 +00:00
public MO2Installer(AbsolutePath archive, ModList modList, AbsolutePath outputFolder, AbsolutePath downloadFolder, SystemParameters parameters)
: base(
archive: archive,
modList: modList,
outputFolder: outputFolder,
2020-01-07 13:50:11 +00:00
downloadFolder: downloadFolder,
parameters: parameters,
2020-09-08 22:15:33 +00:00
steps: 22,
game: modList.GameType)
{
var gameExe = Consts.GameFolderFilesDir.Combine(modList.GameType.MetaData().MainExecutable!);
RedirectGamePath = modList.Directives.Any(d => d.To == gameExe);
}
public bool RedirectGamePath { get; }
protected override async Task<bool> _Begin(CancellationToken cancel)
{
if (cancel.IsCancellationRequested) return false;
2020-06-14 13:13:29 +00:00
await Metrics.Send(Metrics.BeginInstall, ModList.Name);
2020-03-29 03:29:27 +00:00
Utils.Log("Configuring Processor");
2019-12-15 04:33:48 +00:00
2020-09-12 20:23:03 +00:00
DesiredThreads.OnNext(DiskThreads);
FileExtractor2.FavorPerfOverRAM = FavorPerfOverRam;
if (GameFolder == null)
GameFolder = Game.TryGetGameLocation();
if (GameFolder == null)
{
var otherGame = Game.CommonlyConfusedWith.Where(g => g.MetaData().IsInstalled).Select(g => g.MetaData()).FirstOrDefault();
if (otherGame != null)
{
await Utils.Log(new CriticalFailureIntervention(
$"In order to do a proper install Wabbajack needs to know where your {Game.HumanFriendlyGameName} folder resides. However this game doesn't seem to be installed, we did however find a installed " +
$"copy of {otherGame.HumanFriendlyGameName}, did you install the wrong game?",
$"Could not locate {Game.HumanFriendlyGameName}"))
.Task;
}
else
{
await Utils.Log(new CriticalFailureIntervention(
$"In order to do a proper install Wabbajack needs to know where your {Game.HumanFriendlyGameName} folder resides. However this game doesn't seem to be installed",
$"Could not locate {Game.HumanFriendlyGameName}"))
.Task;
}
Utils.Log("Exiting because we couldn't find the game folder.");
return false;
}
Utils.Log($"Install Folder: {OutputFolder}");
Utils.Log($"Downloads Folder: {DownloadFolder}");
Utils.Log($"Game Folder: {GameFolder.Value}");
Utils.Log($"Wabbajack Folder: {AbsolutePath.EntryPoint}");
2020-07-27 21:33:45 +00:00
var watcher = new DiskSpaceWatcher(cancel, new[]{OutputFolder, DownloadFolder, GameFolder.Value, AbsolutePath.EntryPoint}, (long)2 << 31,
drive =>
{
Utils.Log($"Aborting due to low space on {drive.Name}");
Abort();
});
var watcherTask = watcher.Start();
if (cancel.IsCancellationRequested) return false;
2019-11-24 23:03:36 +00:00
UpdateTracker.NextStep("Validating Game ESMs");
await 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");
2020-03-22 15:50:53 +00:00
await ValidateModlist.RunValidation(ModList);
2020-03-25 22:30:43 +00:00
OutputFolder.CreateDirectory();
DownloadFolder.CreateDirectory();
2020-03-25 22:30:43 +00:00
if (OutputFolder.Combine(Consts.MO2ModFolderName).IsDirectory && 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)
2020-07-07 00:46:39 +00:00
Info($"Unable to download {a.Name} ({a.State.PrimaryKeyString})");
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;
UpdateTracker.NextStep("Extracting Modlist contents");
await ExtractModlist();
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");
2020-03-25 22:30:43 +00:00
await ForcePortable();
UpdateTracker.NextStep("Create Empty Output Mods");
CreateOutputMods();
UpdateTracker.NextStep("Updating System-specific ini settings");
SetScreenSizeInPrefs();
2020-09-08 22:15:33 +00:00
UpdateTracker.NextStep("Compacting files");
await CompactFiles();
2019-11-24 23:03:36 +00:00
UpdateTracker.NextStep("Installation complete! You may exit the program.");
await ExtractedModlistFolder!.DisposeAsync();
2020-06-14 13:13:29 +00:00
await Metrics.Send(Metrics.FinishInstall, ModList.Name);
2019-12-15 04:33:48 +00:00
return true;
}
2020-09-08 22:15:33 +00:00
private async Task CompactFiles()
{
if (this.UseCompression)
{
await OutputFolder.CompactFolder(Queue, FileCompaction.Algorithm.XPRESS16K);
}
}
private void CreateOutputMods()
{
2020-03-25 22:30:43 +00:00
OutputFolder.Combine("profiles")
.EnumerateFiles(true)
.Where(f => f.FileName == Consts.SettingsIni)
.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;
2020-03-25 22:30:43 +00:00
var mod = OutputFolder.Combine(Consts.MO2ModFolderName, (RelativePath)v);
2020-03-25 22:30:43 +00:00
mod.CreateDirectory();
});
}
});
}
2020-03-25 22:30:43 +00:00
private async Task ForcePortable()
{
2020-03-25 22:30:43 +00:00
var path = OutputFolder.Combine("portable.txt");
if (path.Exists) return;
try
{
2020-03-25 22:30:43 +00:00
await path.WriteAllTextAsync("Created by Wabbajack");
}
catch (Exception e)
{
Utils.Error(e, $"Could not create portable.txt in {OutputFolder}");
}
}
private async Task InstallIncludedDownloadMetas()
{
2020-09-11 01:22:07 +00:00
await ModList.Archives
2020-11-03 14:45:08 +00:00
.PMap(Queue, UpdateTracker, async archive =>
{
2020-09-11 01:22:07 +00:00
if (HashedArchives.TryGetValue(archive.Hash, out var paths))
2020-09-09 22:50:43 +00:00
{
2020-09-11 01:22:07 +00:00
var metaPath = paths.WithExtension(Consts.MetaFileExtension);
if (!metaPath.Exists)
2020-09-09 22:50:43 +00:00
{
2020-09-11 01:22:07 +00:00
Status($"Writing {metaPath.FileName}");
var meta = AddInstalled(archive.State.GetMetaIni()).ToArray();
await metaPath.WriteAllLinesAsync(meta);
2020-09-09 22:50:43 +00:00
}
2020-09-11 01:22:07 +00:00
}
});
}
2020-09-09 22:50:43 +00:00
private IEnumerable<string> AddInstalled(string[] getMetaIni)
{
foreach (var f in getMetaIni)
{
yield return f;
if (f == "[General]")
{
yield return "installed=true";
}
}
}
private async ValueTask ValidateGameESMs()
{
foreach (var esm in ModList.Directives.OfType<CleanedESM>().ToList())
{
2020-03-25 22:30:43 +00:00
var filename = esm.To.FileName;
var gameFile = GameFolder!.Value.Combine((RelativePath)"Data", filename);
Utils.Log($"Validating {filename}");
var hash = await gameFile.FileHashAsync();
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}");
Info($"Building {bsa.To}");
2020-03-25 22:30:43 +00:00
var sourceDir = OutputFolder.Combine(Consts.BSACreationDir, bsa.TempID);
2019-07-28 23:04:23 +00:00
2020-03-25 22:30:43 +00:00
var bsaSize = bsa.FileStates.Select(state => sourceDir.Combine(state.Path).Size).Sum();
2020-03-09 21:07:57 +00:00
2020-07-20 01:19:56 +00:00
await using var a = await bsa.State.MakeBuilder(bsaSize);
2020-11-03 14:45:08 +00:00
var streams = await bsa.FileStates.PMap(Queue, UpdateTracker, async state =>
2019-10-11 23:31:36 +00:00
{
Status($"Adding {state.Path} to BSA");
var fs = await sourceDir.Combine(state.Path).OpenRead();
await a.AddFile(state, fs);
return fs;
});
Info($"Writing {bsa.To}");
await a.Build(OutputFolder.Combine(bsa.To));
streams.Do(s => s.Dispose());
2020-08-05 22:01:45 +00:00
2020-10-05 22:12:21 +00:00
await sourceDir.DeleteDirectory();
2020-08-05 22:01:45 +00:00
if (UseCompression)
await OutputFolder.Combine(bsa.To).Compact(FileCompaction.Algorithm.XPRESS16K);
}
2020-03-25 22:30:43 +00:00
var bsaDir = OutputFolder.Combine(Consts.BSACreationDir);
if (bsaDir.Exists)
{
Info($"Removing temp folder {Consts.BSACreationDir}");
2020-03-28 04:33:26 +00:00
await 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>()
2020-11-03 14:45:08 +00:00
.PMap(Queue, UpdateTracker, async directive =>
2019-09-14 04:35:42 +00:00
{
2019-09-26 22:32:15 +00:00
Status($"Writing included file {directive.To}");
2020-03-25 22:30:43 +00:00
var outPath = OutputFolder.Combine(directive.To);
2020-05-26 11:31:11 +00:00
await outPath.DeleteAsync();
2020-03-25 22:30:43 +00:00
switch (directive)
{
case RemappedInlineFile file:
await WriteRemappedFile(file);
break;
case CleanedESM esm:
await GenerateCleanedESM(esm);
break;
default:
await outPath.WriteAllBytesAsync(await LoadBytesFromPath(directive.SourceDataID));
break;
}
2020-08-05 22:01:45 +00:00
if (UseCompression)
await outPath.Compact(FileCompaction.Algorithm.XPRESS16K);
2019-09-14 04:35:42 +00:00
});
2019-07-23 04:27:26 +00:00
}
2020-03-25 22:30:43 +00:00
private async Task GenerateCleanedESM(CleanedESM directive)
2019-08-25 03:46:32 +00:00
{
2020-03-25 22:30:43 +00:00
var filename = directive.To.FileName;
var gameFile = GameFolder!.Value.Combine((RelativePath)"Data", filename);
2019-08-25 03:46:32 +00:00
Info($"Generating cleaned ESM for {filename}");
2020-03-25 22:30:43 +00:00
if (!gameFile.Exists) throw new InvalidDataException($"Missing {filename} at {gameFile}");
2019-08-25 03:46:32 +00:00
Status($"Hashing game version of {filename}");
var sha = await gameFile.FileHashCachedAsync();
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
2020-03-25 22:30:43 +00:00
var patchData = await LoadBytesFromPath(directive.SourceDataID);
var toFile = OutputFolder.Combine(directive.To);
2019-08-25 03:46:32 +00:00
Status($"Patching {filename}");
await using var output = await toFile.Create();
await using var input = await gameFile.OpenRead();
2020-03-25 22:30:43 +00:00
Utils.ApplyPatch(input, () => new MemoryStream(patchData), output);
2019-08-25 03:46:32 +00:00
}
private void SetScreenSizeInPrefs()
{
2020-04-10 01:29:53 +00:00
if (SystemParameters == null)
{
throw new ArgumentNullException("System Parameters was null. Cannot set screen size prefs");
}
var config = new IniParserConfiguration {AllowDuplicateKeys = true, AllowDuplicateSections = true};
2020-03-25 22:49:32 +00:00
foreach (var file in OutputFolder.Combine("profiles").EnumerateFiles()
2020-03-28 02:54:14 +00:00
.Where(f => ((string)f.FileName).EndsWith("refs.ini")))
{
2020-01-03 15:01:17 +00:00
try
{
var parser = new FileIniDataParser(new IniDataParser(config));
2020-03-25 22:49:32 +00:00
var data = parser.ReadFile((string)file);
bool modified = false;
if (data.Sections["Display"] != null)
{
if (data.Sections["Display"]["iSize W"] != null && data.Sections["Display"]["iSize H"] != null)
{
data.Sections["Display"]["iSize W"] =
SystemParameters.ScreenWidth.ToString(CultureInfo.CurrentCulture);
data.Sections["Display"]["iSize H"] =
SystemParameters.ScreenHeight.ToString(CultureInfo.CurrentCulture);
modified = true;
}
}
if (data.Sections["MEMORY"] != null)
{
if (data.Sections["MEMORY"]["VideoMemorySizeMb"] != null)
{
data.Sections["MEMORY"]["VideoMemorySizeMb"] =
SystemParameters.EnbLEVRAMSize.ToString(CultureInfo.CurrentCulture);
modified = true;
}
}
if (modified)
2020-03-25 22:49:32 +00:00
parser.WriteFile((string)file, data);
2020-01-03 15:01:17 +00:00
}
catch (Exception)
2020-01-03 15:01:17 +00:00
{
Utils.Log($"Skipping screen size remap for {file} due to parse error.");
}
}
}
2020-03-25 22:30:43 +00:00
private async Task WriteRemappedFile(RemappedInlineFile directive)
2019-08-24 23:20:54 +00:00
{
2020-03-25 22:30:43 +00:00
var data = Encoding.UTF8.GetString(await LoadBytesFromPath(directive.SourceDataID));
2019-08-24 23:20:54 +00:00
var gameFolder = (string)(RedirectGamePath ? Consts.GameFolderFilesDir.RelativeTo(OutputFolder) : GameFolder!);
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("\\", "/"));
2019-08-24 23:20:54 +00:00
2020-03-25 22:30:43 +00:00
data = data.Replace(Consts.MO2_PATH_MAGIC_BACK, (string)OutputFolder);
data = data.Replace(Consts.MO2_PATH_MAGIC_DOUBLE_BACK, ((string)OutputFolder).Replace("\\", "\\\\"));
data = data.Replace(Consts.MO2_PATH_MAGIC_FORWARD, ((string)OutputFolder).Replace("\\", "/"));
2019-08-24 23:20:54 +00:00
2020-03-25 22:30:43 +00:00
data = data.Replace(Consts.DOWNLOAD_PATH_MAGIC_BACK, (string)DownloadFolder);
data = data.Replace(Consts.DOWNLOAD_PATH_MAGIC_DOUBLE_BACK, ((string)DownloadFolder).Replace("\\", "\\\\"));
data = data.Replace(Consts.DOWNLOAD_PATH_MAGIC_FORWARD, ((string)DownloadFolder).Replace("\\", "/"));
2020-03-25 22:30:43 +00:00
await OutputFolder.Combine(directive.To).WriteAllTextAsync(data);
}
2020-03-27 03:33:24 +00:00
public static IErrorResponse CheckValidInstallPath(AbsolutePath path, AbsolutePath? downloadFolder)
{
2020-03-27 03:33:24 +00:00
if (!path.Exists) return ErrorResponse.Success;
2020-01-13 21:11:07 +00:00
// Check folder does not have a Wabbajack ModList
2020-03-27 03:33:24 +00:00
if (path.EnumerateFiles(false).Where(file => file.Exists).Any(file => file.Extension == Consts.ModListExtension))
{
2020-03-27 03:33:24 +00:00
return ErrorResponse.Fail($"Cannot install into a folder with a Wabbajack ModList inside of it");
}
// Check if folder is empty
2020-03-27 03:33:24 +00:00
if (path.IsEmptyDirectory)
{
2020-03-27 03:33:24 +00:00
return ErrorResponse.Success;
}
// Check if folders indicative of a previous install exist
var checks = new List<RelativePath>() {
Consts.MO2ModFolderName,
Consts.MO2ProfilesFolderName
};
if (checks.All(c => path.Combine(c).Exists))
{
return ErrorResponse.Success;
}
2020-03-27 03:33:24 +00:00
// If we have a MO2 install, assume good to go
if (path.EnumerateFiles(false).Any(file =>
{
if (file.FileName == Consts.ModOrganizer2Exe) return true;
if (file.FileName == Consts.ModOrganizer2Ini) 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 (downloadFolder.HasValue && path.EnumerateFiles(true).All(file => file.InFolder(downloadFolder.Value)))
{
return ErrorResponse.Success;
}
return ErrorResponse.Fail($"Either delete everything except the downloads folder, or pick a new location. Cannot install to this folder as it has unexpected files.");
}
}
2019-09-24 15:26:44 +00:00
}