wabbajack/Wabbajack.Installer/StandardInstaller.cs

461 lines
18 KiB
C#
Raw Normal View History

2021-09-27 12:42:46 +00:00
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
2021-09-27 12:42:46 +00:00
using System.Threading;
using System.Threading.Tasks;
using IniParser;
using IniParser.Model.Configuration;
using IniParser.Parser;
using Microsoft.Extensions.DependencyInjection;
2021-09-27 12:42:46 +00:00
using Microsoft.Extensions.Logging;
using Wabbajack.Common;
using Wabbajack.Compression.BSA;
using Wabbajack.Compression.Zip;
2021-09-27 12:42:46 +00:00
using Wabbajack.Downloaders;
2021-10-13 03:59:54 +00:00
using Wabbajack.Downloaders.GameFile;
2021-09-27 12:42:46 +00:00
using Wabbajack.DTOs;
using Wabbajack.DTOs.Directives;
using Wabbajack.DTOs.DownloadStates;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Installer.Utilities;
using Wabbajack.Networking.WabbajackClientApi;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter;
2021-09-27 12:42:46 +00:00
using Wabbajack.VFS;
2021-10-23 16:51:17 +00:00
namespace Wabbajack.Installer;
public class StandardInstaller : AInstaller<StandardInstaller>
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
public StandardInstaller(ILogger<StandardInstaller> logger,
InstallerConfiguration config,
IGameLocator gameLocator, FileExtractor.FileExtractor extractor,
DTOSerializer jsonSerializer, Context vfs, FileHashCache fileHashCache,
DownloadDispatcher downloadDispatcher, ParallelOptions parallelOptions, IResource<IInstaller> limiter, Client wjClient) :
2021-10-23 16:51:17 +00:00
base(logger, config, gameLocator, extractor, jsonSerializer, vfs, fileHashCache, downloadDispatcher,
parallelOptions, limiter, wjClient)
2021-09-27 12:42:46 +00:00
{
2021-11-03 05:03:41 +00:00
MaxSteps = 14;
2021-10-23 16:51:17 +00:00
}
2021-09-27 12:42:46 +00:00
public static StandardInstaller Create(IServiceProvider provider, InstallerConfiguration configuration)
{
return new StandardInstaller(provider.GetRequiredService<ILogger<StandardInstaller>>(),
configuration,
provider.GetRequiredService<IGameLocator>(),
provider.GetRequiredService<FileExtractor.FileExtractor>(),
provider.GetRequiredService<DTOSerializer>(),
provider.GetRequiredService<Context>(),
provider.GetRequiredService<FileHashCache>(),
provider.GetRequiredService<DownloadDispatcher>(),
provider.GetRequiredService<ParallelOptions>(),
provider.GetRequiredService<IResource<IInstaller>>(),
provider.GetRequiredService<Client>());
}
2021-10-23 16:51:17 +00:00
public override async Task<bool> Begin(CancellationToken token)
{
if (token.IsCancellationRequested) return false;
await _wjClient.SendMetric(MetricNames.BeginInstall, ModList.Name);
NextStep(Consts.StepPreparing, "Configuring Installer", 0);
2021-10-23 16:51:17 +00:00
_logger.LogInformation("Configuring Processor");
if (_configuration.GameFolder == default)
_configuration.GameFolder = _gameLocator.GameLocation(_configuration.Game);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
if (_configuration.GameFolder == default)
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
var otherGame = _configuration.Game.MetaData().CommonlyConfusedWith
.Where(g => _gameLocator.IsInstalled(g)).Select(g => g.MetaData()).FirstOrDefault();
if (otherGame != null)
_logger.LogError(
"In order to do a proper install Wabbajack needs to know where your {lookingFor} folder resides. However this game doesn't seem to be installed, we did however find an installed " +
"copy of {otherGame}, did you install the wrong game?",
_configuration.Game.MetaData().HumanFriendlyGameName, otherGame.HumanFriendlyGameName);
else
_logger.LogError(
"In order to do a proper install Wabbajack needs to know where your {lookingFor} folder resides. However this game doesn't seem to be installed.",
_configuration.Game.MetaData().HumanFriendlyGameName);
return false;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
if (!_configuration.GameFolder.DirectoryExists())
{
_logger.LogError("Located game {game} at \"{gameFolder}\" but the folder does not exist!",
_configuration.Game, _configuration.GameFolder);
return false;
}
2021-09-27 12:42:46 +00:00
2021-12-27 23:15:30 +00:00
_logger.LogInformation("Install Folder: {InstallFolder}", _configuration.Install);
_logger.LogInformation("Downloads Folder: {DownloadFolder}", _configuration.Downloads);
_logger.LogInformation("Game Folder: {GameFolder}", _configuration.GameFolder);
_logger.LogInformation("Wabbajack Folder: {WabbajackFolder}", KnownFolders.EntryPoint);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
_configuration.Install.CreateDirectory();
_configuration.Downloads.CreateDirectory();
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await OptimizeModlist(token);
2021-11-02 13:40:59 +00:00
2021-10-23 16:51:17 +00:00
await HashArchives(token);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await DownloadArchives(token);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await HashArchives(token);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var missing = ModList.Archives.Where(a => !HashedArchives.ContainsKey(a.Hash)).ToList();
if (missing.Count > 0)
{
foreach (var a in missing)
_logger.LogCritical("Unable to download {name} ({primaryKeyString})", a.Name,
a.State.PrimaryKeyString);
_logger.LogCritical("Cannot continue, was unable to download one or more archives");
return false;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await ExtractModlist(token);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await PrimeVFS();
2021-09-27 12:42:46 +00:00
2021-11-03 05:03:41 +00:00
await BuildFolderStructure();
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await InstallArchives(token);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await InstallIncludedFiles(token);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await InstallIncludedDownloadMetas(token);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await BuildBSAs(token);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
// TODO: Port this
await GenerateZEditMerges(token);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await ForcePortable();
await RemapMO2File();
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
CreateOutputMods();
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
SetScreenSizeInPrefs();
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await ExtractedModlistFolder!.DisposeAsync();
await _wjClient.SendMetric(MetricNames.FinishInstall, ModList.Name);
2021-09-27 12:42:46 +00:00
NextStep(Consts.StepFinished, "Finished", 1);
2021-10-23 16:51:17 +00:00
_logger.LogInformation("Finished Installation");
return true;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
private async Task RemapMO2File()
{
var iniFile = _configuration.Install.Combine("ModOrganizer.ini");
if (!iniFile.FileExists()) return;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
_logger.LogInformation("Remapping ModOrganizer.ini");
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var iniData = iniFile.LoadIniFile();
var settings = iniData["Settings"];
settings["download_directory"] = _configuration.Downloads.ToString();
iniData.SaveIniFile(iniFile);
}
2021-10-21 03:18:15 +00:00
2021-10-23 16:51:17 +00:00
private void CreateOutputMods()
{
_configuration.Install.Combine("profiles")
.EnumerateFiles()
.Where(f => f.FileName == Consts.SettingsIni)
.Do(f =>
{
if (!f.FileExists())
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
_logger.LogInformation("settings.ini is null for {profile}, skipping", f);
return;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var ini = f.LoadIniFile();
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var overwrites = ini["custom_overrides"];
if (overwrites == null)
{
_logger.LogInformation("No custom overwrites found, skipping");
return;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
overwrites!.Do(keyData =>
{
var v = keyData.Value;
var mod = _configuration.Install.Combine(Consts.MO2ModFolderName, (RelativePath) v);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
mod.CreateDirectory();
2021-09-27 12:42:46 +00:00
});
2021-10-23 16:51:17 +00:00
});
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
private async Task ForcePortable()
{
var path = _configuration.Install.Combine("portable.txt");
if (path.FileExists()) return;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
try
{
await path.WriteAllTextAsync("Created by Wabbajack");
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
catch (Exception e)
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
_logger.LogCritical(e, "Could not create portable.txt in {_configuration.Install}",
_configuration.Install);
}
}
private async Task InstallIncludedDownloadMetas(CancellationToken token)
{
_logger.LogInformation("Looking for downloads by size");
var bySize = UnoptimizedArchives.ToLookup(x => x.Size);
_logger.LogInformation("Writing Metas");
await _configuration.Downloads.EnumerateFiles()
.PDoAll(async download =>
2021-10-23 16:51:17 +00:00
{
var found = bySize[download.Size()];
var hash = await FileHashCache.FileHashCachedAsync(download, token);
var archive = found.FirstOrDefault(f => f.Hash == hash);
if (archive == default) return;
var metaFile = download.WithExtension(Ext.Meta);
if (metaFile.FileExists())
2021-09-27 12:42:46 +00:00
{
try
{
var parsed = metaFile.LoadIniFile();
if (parsed["General"] != null && parsed["General"]["unknownArchive"] == null)
{
return;
}
}
catch (Exception)
2021-09-27 12:42:46 +00:00
{
// Ignore
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
}
_logger.LogInformation("Writing {FileName}", metaFile.FileName);
var meta = AddInstalled(_downloadDispatcher.MetaIni(archive));
await metaFile.WriteAllLinesAsync(meta, token);
2021-10-23 16:51:17 +00:00
});
}
private IEnumerable<string> AddInstalled(IEnumerable<string> getMetaIni)
{
2022-05-28 22:53:52 +00:00
yield return "[General]";
yield return "installed=true";
2021-10-23 16:51:17 +00:00
foreach (var f in getMetaIni)
{
yield return f;
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
}
private async Task BuildBSAs(CancellationToken token)
{
var bsas = ModList.Directives.OfType<CreateBSA>().ToList();
_logger.LogInformation("Building {bsasCount} bsa files", bsas.Count);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
foreach (var bsa in bsas)
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
_logger.LogInformation("Building {bsaTo}", bsa.To.FileName);
var sourceDir = _configuration.Install.Combine(BSACreationDir, bsa.TempID);
await using var a = BSADispatch.CreateBuilder(bsa.State, _manager);
2021-10-23 16:51:17 +00:00
var streams = await bsa.FileStates.PMapAll(async state =>
2021-09-27 12:42:46 +00:00
{
using var job = await _limiter.Begin($"Adding {state.Path.FileName}", 0, token);
2021-10-23 16:51:17 +00:00
var fs = sourceDir.Combine(state.Path).Open(FileMode.Open, FileAccess.Read, FileShare.Read);
var size = fs.Length;
job.Size = size;
2021-10-23 16:51:17 +00:00
await a.AddFile(state, fs, token);
await job.Report((int)size, token);
2021-10-23 16:51:17 +00:00
return fs;
}).ToList();
_logger.LogInformation("Writing {bsaTo}", bsa.To);
2022-05-17 22:32:53 +00:00
var outPath = _configuration.Install.Combine(bsa.To);
await using var outStream = outPath.Open(FileMode.Create, FileAccess.Write, FileShare.None);
2021-10-23 16:51:17 +00:00
await a.Build(outStream, token);
streams.Do(s => s.Dispose());
2022-05-17 22:32:53 +00:00
FileHashCache.FileHashWriteCache(outPath, bsa.Hash);
2021-10-23 16:51:17 +00:00
sourceDir.DeleteDirectory();
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
var bsaDir = _configuration.Install.Combine(BSACreationDir);
if (bsaDir.DirectoryExists())
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
_logger.LogInformation("Removing temp folder {bsaCreationDir}", BSACreationDir);
bsaDir.DeleteDirectory();
}
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
private async Task InstallIncludedFiles(CancellationToken token)
{
_logger.LogInformation("Writing inline files");
NextStep(Consts.StepInstalling, "Installing Included Files", ModList.Directives.OfType<InlineFile>().Count());
2021-10-23 16:51:17 +00:00
await ModList.Directives
.OfType<InlineFile>()
.PDoAll(async directive =>
2021-09-27 12:42:46 +00:00
{
2021-11-02 13:40:59 +00:00
UpdateProgress(1);
2021-10-23 16:51:17 +00:00
var outPath = _configuration.Install.Combine(directive.To);
outPath.Delete();
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
switch (directive)
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
case RemappedInlineFile file:
await WriteRemappedFile(file);
break;
default:
await outPath.WriteAllBytesAsync(await LoadBytesFromPath(directive.SourceDataID), token);
break;
}
});
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
private void SetScreenSizeInPrefs()
{
if (_configuration.SystemParameters == null)
_logger.LogWarning("No SystemParameters set, ignoring ini settings for system parameters");
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var config = new IniParserConfiguration {AllowDuplicateKeys = true, AllowDuplicateSections = true};
config.CommentRegex = new Regex(@"^(#|;)(.*)");
2021-10-23 16:51:17 +00:00
var oblivionPath = (RelativePath) "Oblivion.ini";
foreach (var file in _configuration.Install.Combine("profiles").EnumerateFiles()
.Where(f => ((string) f.FileName).EndsWith("refs.ini") || f.FileName == oblivionPath))
try
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
var parser = new FileIniDataParser(new IniDataParser(config));
var data = parser.ReadFile(file.ToString());
var 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"] =
_configuration.SystemParameters.ScreenWidth.ToString(CultureInfo.CurrentCulture);
data.Sections["Display"]["iSize H"] =
_configuration.SystemParameters.ScreenHeight.ToString(CultureInfo.CurrentCulture);
modified = true;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
if (data.Sections["MEMORY"] != null)
if (data.Sections["MEMORY"]["VideoMemorySizeMb"] != null)
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
data.Sections["MEMORY"]["VideoMemorySizeMb"] =
_configuration.SystemParameters.EnbLEVRAMSize.ToString(CultureInfo.CurrentCulture);
modified = true;
2021-09-27 12:42:46 +00:00
}
if (!modified) continue;
parser.WriteFile(file.ToString(), data);
_logger.LogTrace("Remapped screen size in {file}", file);
2021-10-23 16:51:17 +00:00
}
catch (Exception ex)
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
_logger.LogCritical(ex, "Skipping screen size remap for {file} due to parse error.", file);
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
var tweaksPath = (RelativePath) "SSEDisplayTweaks.ini";
foreach (var file in _configuration.Install.EnumerateFiles()
.Where(f => f.FileName == tweaksPath))
try
{
var parser = new FileIniDataParser(new IniDataParser(config));
var data = parser.ReadFile(file.ToString());
var modified = false;
if (data.Sections["Render"] != null)
if (data.Sections["Render"]["Resolution"] != null)
{
data.Sections["Render"]["Resolution"] =
$"{_configuration.SystemParameters.ScreenWidth.ToString(CultureInfo.CurrentCulture)}x{_configuration.SystemParameters.ScreenHeight.ToString(CultureInfo.CurrentCulture)}";
modified = true;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
if (modified)
parser.WriteFile(file.ToString(), data);
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Skipping screen size remap for {file} due to parse error.", file);
}
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
private async Task WriteRemappedFile(RemappedInlineFile directive)
{
var data = Encoding.UTF8.GetString(await LoadBytesFromPath(directive.SourceDataID));
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var gameFolder = _configuration.GameFolder.ToString();
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +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("\\", "/"));
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
data = data.Replace(Consts.MO2_PATH_MAGIC_BACK, _configuration.Install.ToString());
data = data.Replace(Consts.MO2_PATH_MAGIC_DOUBLE_BACK,
_configuration.Install.ToString().Replace("\\", "\\\\"));
data = data.Replace(Consts.MO2_PATH_MAGIC_FORWARD, _configuration.Install.ToString().Replace("\\", "/"));
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
data = data.Replace(Consts.DOWNLOAD_PATH_MAGIC_BACK, _configuration.Downloads.ToString());
data = data.Replace(Consts.DOWNLOAD_PATH_MAGIC_DOUBLE_BACK,
_configuration.Downloads.ToString().Replace("\\", "\\\\"));
data = data.Replace(Consts.DOWNLOAD_PATH_MAGIC_FORWARD,
_configuration.Downloads.ToString().Replace("\\", "/"));
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await _configuration.Install.Combine(directive.To).WriteAllTextAsync(data);
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public async Task GenerateZEditMerges(CancellationToken token)
{
await _configuration.ModList
.Directives
.OfType<MergedPatch>()
.PDoAll(async m =>
{
_logger.LogInformation("Generating zEdit merge: {to}", m.To);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var srcData = (await m.Sources.SelectAsync(async s =>
2021-09-27 12:42:46 +00:00
await _configuration.Install.Combine(s.RelativePath).ReadAllBytesAsync(token))
.ToReadOnlyCollection())
2021-10-23 16:51:17 +00:00
.ConcatArrays();
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var patchData = await LoadBytesFromPath(m.PatchID);
await using var fs = _configuration.Install.Combine(m.To)
.Open(FileMode.Create, FileAccess.Write, FileShare.None);
await BinaryPatching.ApplyPatch(new MemoryStream(srcData), new MemoryStream(patchData), fs);
});
2021-09-27 12:42:46 +00:00
}
public static async Task<ModList> Load(DTOSerializer dtos, DownloadDispatcher dispatcher, ModlistMetadata metadata, CancellationToken token)
{
var archive = new Archive
{
State = dispatcher.Parse(new Uri(metadata.Links.Download))!,
Size = metadata.DownloadMetadata!.Size,
Hash = metadata.DownloadMetadata.Hash
};
var stream = await dispatcher.ChunkedSeekableStream(archive, token);
await using var reader = new ZipReader(stream);
var entry = (await reader.GetFiles()).First(e => e.FileName == "modlist");
var ms = new MemoryStream();
await reader.Extract(entry, ms, token);
ms.Position = 0;
return JsonSerializer.Deserialize<ModList>(ms, dtos.Options)!;
}
2021-09-27 12:42:46 +00:00
}