diff --git a/CHANGELOG.md b/CHANGELOG.md index 949c39cc..d9729dd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ ### Changelog +#### Version - 1.0 beta 15 - 1/6/2020 +* Don't delete the download folder when deleting empty folders during an update +* If `Game Folder Files` exists in the MO2 folder during compilation the Game folder will be ignored as a file source + +#### Version - 1.0 beta 14 - 1/6/2020 +* Updating a list twice without starting WJ no longer deletes your modlist +* .mohidden files will now be correctly detected during binary patching +* Added support for MO2's new path format +* Added support for MO2 2.2.2's `portable.txt` feature +* Added support for VectorPlexus downloads +* Added a new CLI interface for providing Nexus API key overrides +* Several UI backend improvements + #### Version - 1.0 beta 13 - 1/4/22020 * Several fixes for steam game handling * Fixes for metrics reporting diff --git a/Wabbajack.CacheServer/JobQueueEndpoints.cs b/Wabbajack.CacheServer/JobQueueEndpoints.cs index 8cf23027..f68ce0cb 100644 --- a/Wabbajack.CacheServer/JobQueueEndpoints.cs +++ b/Wabbajack.CacheServer/JobQueueEndpoints.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Linq.Expressions; using System.Security.Policy; using System.Threading.Tasks; -using Windows.Media.Playback; using MongoDB.Driver; using MongoDB.Driver.Linq; using Nancy; diff --git a/Wabbajack.CacheServer/ListValidationService.cs b/Wabbajack.CacheServer/ListValidationService.cs index bcf18a93..0c717b1a 100644 --- a/Wabbajack.CacheServer/ListValidationService.cs +++ b/Wabbajack.CacheServer/ListValidationService.cs @@ -158,7 +158,7 @@ namespace Wabbajack.CacheServer using (var queue = new WorkQueue()) { - foreach (var list in modlists.Skip(2).Take(1)) + foreach (var list in modlists) { try { diff --git a/Wabbajack.Common/CLI.cs b/Wabbajack.Common/CLI.cs new file mode 100644 index 00000000..20c5081c --- /dev/null +++ b/Wabbajack.Common/CLI.cs @@ -0,0 +1,104 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace Wabbajack.Common +{ + public static class CLIArguments + { + [CLIOptions("nosettings", HelpText = "Don't load the saved Settings")] + public static bool NoSettings { get; set; } + + [CLIOptions("apikey", HelpText = "Manually input an Nexus api key")] + public static string ApiKey { get; set; } + + [CLIOptions("install", ShortOption = 'i', HelpText = "Install a ModList via CLI")] + public static string InstallPath { get; set; } + + [CLIOptions("help", ShortOption = 'h', HelpText = "Display this message")] + public static bool Help { get; set; } + } + + public static class CLI + { + /// + /// Parses the argument and sets the properties of + /// + /// + public static void ParseOptions(string[] args) + { + if (args.Length == 1) return; + // get all properties of the class Options + typeof(CLIArguments).GetProperties().Do(p => + { + var optionAttr = (CLIOptions[])p.GetCustomAttributes(typeof(CLIOptions)); + if (optionAttr.Length != 1) + return; + + var cur = optionAttr[0]; + if (cur?.Option == null) return; + + FillVariable(cur.Option, ref p, ref args, false); + FillVariable(cur.ShortOption, ref p, ref args, true); + }); + } + + public static void DisplayHelpText() + { + Console.WriteLine("Wabbajack CLI Help Text"); + Console.WriteLine("{0,-20} | {1,-15} | {2,-30}", "Option", "Short Option", "Help Text"); + + typeof(CLIArguments).GetProperties().Do(p => + { + var optionAttr = (CLIOptions[])p.GetCustomAttributes(typeof(CLIOptions)); + if (optionAttr.Length != 1) + return; + + var cur = optionAttr[0]; + if (cur?.Option == null) return; + + var shortText = cur.ShortOption != 0 ? $"-{cur.ShortOption}" : ""; + var helpText = string.IsNullOrWhiteSpace(cur.HelpText) ? "" : cur.HelpText; + Console.WriteLine("{0,-20} | {1,-15} | {2,-30}", $"--{cur.Option}", shortText, helpText); + }); + } + + private static void FillVariable(dynamic option, ref PropertyInfo p, ref string[] args, bool single) + { + var s = single ? $"-{option}" : $"--{option}"; + + if (!args.Any(a => a.Contains(s))) return; + + if (p.PropertyType == typeof(bool)) + { + p.SetValue(p, true); + return; + } + + var filtered = args.Where(a => a.Contains(s)).ToList(); + if (filtered.Count != 1) return; + + var arg = filtered[0]; + arg = arg.Replace($"{s}=", ""); + + if(p.PropertyType == typeof(string)) + p.SetValue(p, arg); + } + } + + [AttributeUsage(AttributeTargets.Property)] + public class CLIOptions : Attribute + { + // --option, long name of the option. Eg: --output + public string Option; + // -shortOption, short name of the option. Eg: -o + public char ShortOption; + // text to be displayed when --help is called + public string HelpText; + + public CLIOptions(string option) + { + Option = option; + } + } +} diff --git a/Wabbajack.Common/DynamicIniData.cs b/Wabbajack.Common/DynamicIniData.cs index 7757f40f..594058b5 100644 --- a/Wabbajack.Common/DynamicIniData.cs +++ b/Wabbajack.Common/DynamicIniData.cs @@ -1,6 +1,11 @@ -using System.Dynamic; +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Numerics; +using System.Text; using System.Text.RegularExpressions; using IniParser; +using IniParser.Exceptions; using IniParser.Model; namespace Wabbajack.Common @@ -44,10 +49,58 @@ namespace Wabbajack.Common public override bool TryGetMember(GetMemberBinder binder, out object result) { result = _coll[binder.Name]; - if (result is string) result = Regex.Unescape(((string) result).Trim('"')); + if (result is string) result = Interpret((string)result); return true; } + private static string Interpret(string s) + { + if (s.StartsWith("@ByteArray(") && s.EndsWith(")")) + { + return UnescapeUTF8(s.Substring("@ByteArray(".Length, s.Length - "@ByteArray(".Length - ")".Length)); + } + + return UnescapeString(s); + } + + private static string UnescapeString(string s) + { + return Regex.Unescape(s.Trim('"')); + } + + private static string UnescapeUTF8(string s) + { + List acc = new List(); + for (var i = 0; i < s.Length; i++) + { + var c = s[i]; + switch (c) + { + case '\\': + i++; + var nc = s[i]; + switch (nc) + { + case '\\': + acc.Add((byte)'\\'); + break; + case 'x': + var chrs = s[i + 1] + s[i + 2].ToString(); + i += 2; + acc.Add(Convert.ToByte(chrs, 16)); + break; + default: + throw new ParsingException($"Not a valid escape characer {nc}"); + } + break; + default: + acc.Add((byte)c); + break; + } + } + return Encoding.UTF8.GetString(acc.ToArray()); + } + public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) { if (indexes.Length > 1) @@ -61,4 +114,4 @@ namespace Wabbajack.Common return true; } } -} \ No newline at end of file +} diff --git a/Wabbajack.Common/ExtensionManager.cs b/Wabbajack.Common/ExtensionManager.cs index 1f0f5075..f8e2dd6b 100644 --- a/Wabbajack.Common/ExtensionManager.cs +++ b/Wabbajack.Common/ExtensionManager.cs @@ -19,7 +19,7 @@ namespace Wabbajack.Common { {"", "Wabbajack"}, {"FriendlyTypeName", "Wabbajack"}, - {"shell\\open\\command", "\"{appPath}\" -i \"%1\""}, + {"shell\\open\\command", "\"{appPath}\" -i=\"%1\""}, }; private static readonly Dictionary ExtList = new Dictionary @@ -34,7 +34,7 @@ namespace Wabbajack.Common var tempKey = progIDKey?.OpenSubKey("shell\\open\\command"); if (progIDKey == null || tempKey == null) return true; var value = tempKey.GetValue(""); - return value == null || value.ToString().Equals($"\"{appPath}\" -i \"%1\""); + return value == null || value.ToString().Equals($"\"{appPath}\" -i=\"%1\""); } public static bool IsAssociated() diff --git a/Wabbajack.Common/GameMetaData.cs b/Wabbajack.Common/GameMetaData.cs index 25646945..d30ed10c 100644 --- a/Wabbajack.Common/GameMetaData.cs +++ b/Wabbajack.Common/GameMetaData.cs @@ -178,7 +178,7 @@ namespace Wabbajack.Common MO2Name = "New Vegas", MO2ArchiveName = "falloutnv", GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\falloutnv", - SteamIDs = new List {22380}, + SteamIDs = new List {22380, 22490}, // normal and RU version RequiredFiles = new List { "FalloutNV.exe" diff --git a/Wabbajack.Common/Utils.cs b/Wabbajack.Common/Utils.cs index 7f80149e..377bf7e0 100644 --- a/Wabbajack.Common/Utils.cs +++ b/Wabbajack.Common/Utils.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Data.HashFunction.xxHash; using System.Diagnostics; @@ -365,16 +366,26 @@ namespace Wabbajack.Common public static void ToCERAS(this T obj, string filename, SerializerConfig config) { + byte[] final; + final = ToCERAS(obj, config); + File.WriteAllBytes(filename, final); + } + + public static byte[] ToCERAS(this T obj, SerializerConfig config) + { + byte[] final; var ceras = new CerasSerializer(config); byte[] buffer = null; ceras.Serialize(obj, ref buffer); - using(var m1 = new MemoryStream(buffer)) + + using (var m1 = new MemoryStream(buffer)) using (var m2 = new MemoryStream()) { BZip2.Compress(m1, m2, false, 9); m2.Seek(0, SeekOrigin.Begin); - File.WriteAllBytes(filename, m2.ToArray()); + final = m2.ToArray(); } + return final; } public static T FromCERAS(this Stream data, SerializerConfig config) @@ -1112,6 +1123,20 @@ namespace Wabbajack.Common return path.ToLower().TrimEnd('\\').StartsWith(parent.ToLower().TrimEnd('\\') + "\\"); } + public static HashSet ToHashSet(this IEnumerable coll) + { + var hs = new HashSet(); + coll.Do(v => hs.Add(v)); + return hs; + } + + public static HashSet ToHashSet(this T[] coll) + { + var hs = new HashSet(); + coll.Do(v => hs.Add(v)); + return hs; + } + public class NexusErrorResponse { public int code; diff --git a/Wabbajack.Common/Wabbajack.Common.csproj b/Wabbajack.Common/Wabbajack.Common.csproj index 430458da..09fecdb0 100644 --- a/Wabbajack.Common/Wabbajack.Common.csproj +++ b/Wabbajack.Common/Wabbajack.Common.csproj @@ -5,35 +5,35 @@ AnyCPU;x64 - - - + + + - - - + + + - - - - - - - - - - - + + + + + + + + + + + - - - + + + \ No newline at end of file diff --git a/Wabbajack.Lib/AInstaller.cs b/Wabbajack.Lib/AInstaller.cs index ce4550d4..78dd8851 100644 --- a/Wabbajack.Lib/AInstaller.cs +++ b/Wabbajack.Lib/AInstaller.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Alphaleonis.Win32.Filesystem; using Wabbajack.Common; @@ -27,13 +28,16 @@ namespace Wabbajack.Lib public string ModListArchive { get; private set; } public ModList ModList { get; private set; } public Dictionary HashedArchives { get; set; } + + public SystemParameters SystemParameters { get; set; } - public AInstaller(string archive, ModList modList, string outputFolder, string downloadFolder) + public AInstaller(string archive, ModList modList, string outputFolder, string downloadFolder, SystemParameters parameters) { ModList = modList; ModListArchive = archive; OutputFolder = outputFolder; DownloadFolder = downloadFolder; + SystemParameters = parameters; } public void Info(string msg) @@ -108,7 +112,7 @@ namespace Wabbajack.Lib Info("Building Folder Structure"); ModList.Directives .Select(d => Path.Combine(OutputFolder, Path.GetDirectoryName(d.To))) - .ToHashSet() + .Distinct() .Do(f => { if (Directory.Exists(f)) return; @@ -344,6 +348,10 @@ namespace Wabbajack.Lib public async Task OptimizeModlist() { Utils.Log("Optimizing Modlist directives"); + + // Clone the modlist so our changes don't modify the original data + ModList = ModList.Clone(); + var indexed = ModList.Directives.ToDictionary(d => d.To); UpdateTracker.NextStep("Looking for files to delete"); @@ -377,6 +385,33 @@ namespace Wabbajack.Lib .Where(d => d != null) .Do(d => indexed.Remove(d.To)); + Utils.Log("Cleaning empty folders"); + var expectedFolders = indexed.Keys + // We ignore the last part of the path, so we need a dummy file name + .Append(Path.Combine(DownloadFolder, "_")) + .SelectMany(path => + { + // Get all the folders and all the folder parents + // so for foo\bar\baz\qux.txt this emits ["foo", "foo\\bar", "foo\\bar\\baz"] + var split = path.Split('\\'); + return Enumerable.Range(1, split.Length - 1).Select(t => string.Join("\\", split.Take(t))); + }).Distinct() + .Select(p => Path.Combine(OutputFolder, p)) + .ToHashSet(); + + try + { + Directory.EnumerateDirectories(OutputFolder, DirectoryEnumerationOptions.Recursive) + .Where(p => !expectedFolders.Contains(p)) + .OrderByDescending(p => p.Length) + .Do(p => Directory.Delete(p)); + } + catch (Exception) + { + // ignored because it's not worth throwing a fit over + Utils.Log("Error when trying to clean empty folders. This doesn't really matter."); + } + UpdateTracker.NextStep("Updating Modlist"); Utils.Log($"Optimized {ModList.Directives.Count} directives to {indexed.Count} required"); var requiredArchives = indexed.Values.OfType() diff --git a/Wabbajack.Lib/CerasConfig.cs b/Wabbajack.Lib/CerasConfig.cs index 3997c8bf..7afa8d53 100644 --- a/Wabbajack.Lib/CerasConfig.cs +++ b/Wabbajack.Lib/CerasConfig.cs @@ -28,7 +28,8 @@ namespace Wabbajack.Lib typeof(BSAStateObject), typeof(BSAFileStateObject), typeof(BA2StateObject), typeof(BA2DX10EntryState), typeof(BA2FileEntryState), typeof(MediaFireDownloader.State), typeof(ArchiveMeta), typeof(PropertyFile), typeof(SteamMeta), typeof(SteamWorkshopDownloader), typeof(SteamWorkshopDownloader.State), - typeof(LoversLabDownloader.State), typeof(GameFileSourceDownloader.State) + typeof(LoversLabDownloader.State), typeof(GameFileSourceDownloader.State), typeof(VectorPlexusDownloader.State), + typeof(DeadlyStreamDownloader.State) }, }; diff --git a/Wabbajack.Lib/CompilationSteps/IgnoreDisabledMods.cs b/Wabbajack.Lib/CompilationSteps/IgnoreDisabledMods.cs index 4f28c933..c39240af 100644 --- a/Wabbajack.Lib/CompilationSteps/IgnoreDisabledMods.cs +++ b/Wabbajack.Lib/CompilationSteps/IgnoreDisabledMods.cs @@ -15,7 +15,7 @@ namespace Wabbajack.Lib.CompilationSteps public IgnoreDisabledMods(ACompiler compiler) : base(compiler) { _mo2Compiler = (MO2Compiler) compiler; - var alwaysEnabled = _mo2Compiler.ModInis.Where(f => IsAlwaysEnabled(f.Value)).Select(f => f.Key).ToHashSet(); + var alwaysEnabled = _mo2Compiler.ModInis.Where(f => IsAlwaysEnabled(f.Value)).Select(f => f.Key).Distinct(); _allEnabledMods = _mo2Compiler.SelectedProfiles .SelectMany(p => File.ReadAllLines(Path.Combine(_mo2Compiler.MO2Folder, "profiles", p, "modlist.txt"))) diff --git a/Wabbajack.Lib/CompilationSteps/IgnoreGameFilesIfGameFolderFilesExist.cs b/Wabbajack.Lib/CompilationSteps/IgnoreGameFilesIfGameFolderFilesExist.cs new file mode 100644 index 00000000..c2dc1cdb --- /dev/null +++ b/Wabbajack.Lib/CompilationSteps/IgnoreGameFilesIfGameFolderFilesExist.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Alphaleonis.Win32.Filesystem; +using Wabbajack.Common; + +namespace Wabbajack.Lib.CompilationSteps +{ + public class IgnoreGameFilesIfGameFolderFilesExist : ACompilationStep + { + private readonly bool _gameFolderFilesExists; + private readonly string _gameFolder; + + public IgnoreGameFilesIfGameFolderFilesExist(ACompiler compiler) : base(compiler) + { + _gameFolderFilesExists = Directory.Exists(Path.Combine(((MO2Compiler)compiler).MO2Folder, Consts.GameFolderFilesDir)); + _gameFolder = compiler.GamePath; + } + + public override async ValueTask Run(RawSourceFile source) + { + if (_gameFolderFilesExists) + { + if (source.AbsolutePath.IsInPath(_gameFolder)) + { + var result = source.EvolveTo(); + result.Reason = $"Ignoring game files because {Consts.GameFolderFilesDir} exists"; + return result; + } + } + + return null; + } + + public override IState GetState() + { + return new State(); + } + + public class State : IState + { + public ICompilationStep CreateStep(ACompiler compiler) + { + return new IgnoreGameFilesIfGameFolderFilesExist(compiler); + } + } + } +} diff --git a/Wabbajack.Lib/CompilationSteps/IncludePatches.cs b/Wabbajack.Lib/CompilationSteps/IncludePatches.cs index 0677fbbf..48f86990 100644 --- a/Wabbajack.Lib/CompilationSteps/IncludePatches.cs +++ b/Wabbajack.Lib/CompilationSteps/IncludePatches.cs @@ -22,8 +22,14 @@ namespace Wabbajack.Lib.CompilationSteps public override async ValueTask Run(RawSourceFile source) { - if (!_indexed.TryGetValue(Path.GetFileName(source.File.Name.ToLower()), out var choices)) - return null; + var name = Path.GetFileName(source.File.Name.ToLower()); + string nameWithoutExt = name; + if (Path.GetExtension(name) == ".mohidden") + nameWithoutExt = Path.GetFileNameWithoutExtension(name); + + if (!_indexed.TryGetValue(Path.GetFileName(name), out var choices)) + if (!_indexed.TryGetValue(Path.GetFileName(nameWithoutExt), out choices)) + return null; var mod_ini = ((MO2Compiler)_compiler).ModMetas.FirstOrDefault(f => source.Path.StartsWith(f.Key)); var installationFile = mod_ini.Value?.General?.installationFile; diff --git a/Wabbajack.Lib/Data.cs b/Wabbajack.Lib/Data.cs index 25c54c89..495fff61 100644 --- a/Wabbajack.Lib/Data.cs +++ b/Wabbajack.Lib/Data.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Ceras; using Compression.BSA; @@ -120,6 +121,11 @@ namespace Wabbajack.Lib /// Whether readme is a website /// public bool ReadmeIsWebsite; + + public ModList Clone() + { + return new MemoryStream(this.ToCERAS(CerasConfig.Config)).FromCERAS(CerasConfig.Config); + } } public class Directive diff --git a/Wabbajack.Lib/Downloaders/AbstractDownloadState.cs b/Wabbajack.Lib/Downloaders/AbstractDownloadState.cs index 4ec030d8..ee9d5e49 100644 --- a/Wabbajack.Lib/Downloaders/AbstractDownloadState.cs +++ b/Wabbajack.Lib/Downloaders/AbstractDownloadState.cs @@ -21,7 +21,9 @@ namespace Wabbajack.Lib.Downloaders typeof(MegaDownloader.State), typeof(ModDBDownloader.State), typeof(NexusDownloader.State), - typeof(SteamWorkshopDownloader.State) + typeof(SteamWorkshopDownloader.State), + typeof(VectorPlexusDownloader.State), + typeof(DeadlyStreamDownloader.State) }; public static Dictionary NameToType { get; set; } public static Dictionary TypeToName { get; set; } diff --git a/Wabbajack.Lib/Downloaders/AbstractIPS4Downloader.cs b/Wabbajack.Lib/Downloaders/AbstractIPS4Downloader.cs new file mode 100644 index 00000000..3635b549 --- /dev/null +++ b/Wabbajack.Lib/Downloaders/AbstractIPS4Downloader.cs @@ -0,0 +1,155 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Web; +using Newtonsoft.Json; +using Wabbajack.Common; +using Wabbajack.Lib.Validation; +using File = System.IO.File; + +namespace Wabbajack.Lib.Downloaders +{ + // IPS4 is the site used by LoversLab, VectorPlexus, etc. the general mechanics of each site are the + // same, so we can fairly easily abstract the state. + // Pass in the state type via TState + public abstract class AbstractIPS4Downloader : AbstractNeedsLoginDownloader, IDownloader + where TState : AbstractIPS4Downloader.State, new() + where TDownloader : IDownloader + { + public override string SiteName { get; } + public override Uri SiteURL { get; } + public async Task GetDownloaderState(dynamic archiveINI) + { + Uri url = DownloaderUtils.GetDirectURL(archiveINI); + if (url == null || url.Host != SiteURL.Host || !url.AbsolutePath.StartsWith("/files/file/")) return null; + var id = HttpUtility.ParseQueryString(url.Query)["r"]; + var file = url.AbsolutePath.Split('/').Last(s => s != ""); + + return new TState + { + FileID = id, + FileName = file + }; + } + + + public class State : AbstractDownloadState where TDownloader : IDownloader + { + public string FileID { get; set; } + public string FileName { get; set; } + + public override object[] PrimaryKey { get => new object[] {FileID, FileName}; } + + public override bool IsWhitelisted(ServerWhitelist whitelist) + { + return true; + } + + public override async Task Download(Archive a, string destination) + { + var stream = await ResolveDownloadStream(); + using (var file = File.OpenWrite(destination)) + { + stream.CopyTo(file); + } + } + + private async Task ResolveDownloadStream() + { + var downloader = (AbstractNeedsLoginDownloader)(object)DownloadDispatcher.GetInstance(); + + TOP: + string csrfurl; + if (FileID == null) + { + csrfurl = $"https://{downloader.SiteURL.Host}/files/file/{FileName}/?do=download"; + } + else + { + csrfurl = $"https://{downloader.SiteURL.Host}/files/file/{FileName}/?do=download&r={FileID}"; + } + var html = await downloader.AuthedClient.GetStringAsync(csrfurl); + + var pattern = new Regex("(?<=csrfKey=).*(?=[&\"\'])|(?<=csrfKey: \").*(?=[&\"\'])"); + var matches = pattern.Matches(html).Cast(); + + var csrfKey = matches.Where(m => m.Length == 32).Select(m => m.ToString()).FirstOrDefault(); + + if (csrfKey == null) + return null; + + string url; + if (FileID == null) + url = $"https://{downloader.SiteURL.Host}/files/file/{FileName}/?do=download&confirm=1&t=1&csrfKey={csrfKey}"; + else + url = $"https://{downloader.SiteURL.Host}/files/file/{FileName}/?do=download&r={FileID}&confirm=1&t=1&csrfKey={csrfKey}"; + + + var streamResult = await downloader.AuthedClient.GetAsync(url); + if (streamResult.StatusCode != HttpStatusCode.OK) + { + Utils.Error(new InvalidOperationException(), $"{downloader.SiteName} servers reported an error for file: {FileID}"); + } + + var content_type = streamResult.Content.Headers.ContentType; + + if (content_type.MediaType == "application/json") + { + // Sometimes LL hands back a json object telling us to wait until a certain time + var times = (await streamResult.Content.ReadAsStringAsync()).FromJSONString(); + var secs = times.Download - times.CurrentTime; + for (int x = 0; x < secs; x++) + { + Utils.Status($"Waiting for {secs} at the request of {downloader.SiteName}", x * 100 / secs); + await Task.Delay(1000); + } + Utils.Status("Retrying download"); + goto TOP; + } + + return await streamResult.Content.ReadAsStreamAsync(); + } + + private class WaitResponse + { + [JsonProperty("download")] + public int Download { get; set; } + [JsonProperty("currentTime")] + public int CurrentTime { get; set; } + } + + public override async Task Verify() + { + var stream = await ResolveDownloadStream(); + if (stream == null) + { + return false; + } + + stream.Close(); + return true; + } + + public override IDownloader GetDownloader() + { + return DownloadDispatcher.GetInstance(); + } + + public override string GetReportEntry(Archive a) + { + var downloader = (INeedsLogin)GetDownloader(); + return $"* {((INeedsLogin)GetDownloader()).SiteName} - [{a.Name}](https://{downloader.SiteURL.Host}/files/file/{FileName}/?do=download&r={FileID})"; + } + } + + protected AbstractIPS4Downloader(Uri loginUri, string encryptedKeyName, string cookieDomain) : + base(loginUri, encryptedKeyName, cookieDomain, "ips4_member_id") + { + } + + + } +} diff --git a/Wabbajack.Lib/Downloaders/AbstractNeedsLoginDownloader.cs b/Wabbajack.Lib/Downloaders/AbstractNeedsLoginDownloader.cs new file mode 100644 index 00000000..1bead261 --- /dev/null +++ b/Wabbajack.Lib/Downloaders/AbstractNeedsLoginDownloader.cs @@ -0,0 +1,142 @@ +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; +using ReactiveUI; +using Wabbajack.Common; +using Wabbajack.Common.StatusFeed; +using Wabbajack.Lib.LibCefHelpers; +using Wabbajack.Lib.WebAutomation; + +namespace Wabbajack.Lib.Downloaders +{ + public abstract class AbstractNeedsLoginDownloader : INeedsLogin + { + private readonly Uri _loginUri; + private readonly string _encryptedKeyName; + private readonly string _cookieDomain; + private readonly string _cookieName; + internal HttpClient AuthedClient; + + /// + /// Sets up all the login facilites needed for a INeedsLogin downloader based on having the user log + /// in via a browser + /// + /// The URI to preset for logging in + /// The name of the encrypted JSON key in which to store cookies + /// The cookie domain to scan + /// The cookie name to wait for + public AbstractNeedsLoginDownloader(Uri loginUri, + string encryptedKeyName, + string cookieDomain, + string cookieName) + { + _loginUri = loginUri; + _encryptedKeyName = encryptedKeyName; + _cookieDomain = cookieDomain; + _cookieName = cookieName; + + TriggerLogin = ReactiveCommand.CreateFromTask( + execute: () => Utils.CatchAndLog(async () => await Utils.Log(new RequestSiteLogin(this)).Task), + canExecute: IsLoggedIn.Select(b => !b).ObserveOn(RxApp.MainThreadScheduler)); + ClearLogin = ReactiveCommand.Create( + execute: () => Utils.CatchAndLog(() => Utils.DeleteEncryptedJson(_encryptedKeyName)), + canExecute: IsLoggedIn.ObserveOn(RxApp.MainThreadScheduler)); + } + + public ICommand TriggerLogin { get; } + public ICommand ClearLogin { get; } + public IObservable IsLoggedIn => Utils.HaveEncryptedJsonObservable(_encryptedKeyName); + public abstract string SiteName { get; } + public virtual string MetaInfo { get; } + public abstract Uri SiteURL { get; } + public virtual Uri IconUri { get; } + + protected virtual async Task WhileWaiting(IWebDriver browser) + { + } + + public async Task GetAndCacheCookies(IWebDriver browser, Action updateStatus, CancellationToken cancel) + { + updateStatus($"Please Log Into {SiteName}"); + await browser.NavigateTo(_loginUri); + var cookies = new Helpers.Cookie[0]; + while (true) + { + cancel.ThrowIfCancellationRequested(); + await WhileWaiting(browser); + cookies = (await browser.GetCookies(_cookieDomain)); + if (cookies.FirstOrDefault(c => c.Name == _cookieName) != null) + break; + await Task.Delay(500, cancel); + } + + cookies.ToEcryptedJson(_encryptedKeyName); + + return cookies; + } + + public async Task GetAuthedClient() + { + Helpers.Cookie[] cookies; + try + { + cookies = Utils.FromEncryptedJson(_encryptedKeyName); + if (cookies != null) + return Helpers.GetClient(cookies, SiteURL.ToString()); + } + catch (FileNotFoundException) { } + + cookies = await Utils.Log(new RequestSiteLogin(this)).Task; + return Helpers.GetClient(cookies, SiteURL.ToString()); + } + + public async Task Prepare() + { + AuthedClient = (await GetAuthedClient()) ?? throw new NotLoggedInError(this); + } + + public class NotLoggedInError : Exception + { + public AbstractNeedsLoginDownloader Downloader { get; } + public NotLoggedInError(AbstractNeedsLoginDownloader downloader) : base( + $"Not logged into {downloader.SiteName}, can't continue") + { + Downloader = downloader; + } + } + + + public class RequestSiteLogin : AUserIntervention + { + public AbstractNeedsLoginDownloader Downloader { get; } + public RequestSiteLogin(AbstractNeedsLoginDownloader downloader) + { + Downloader = downloader; + } + public override string ShortDescription => $"Getting {Downloader.SiteName} Login"; + public override string ExtendedDescription { get; } + + private readonly TaskCompletionSource _source = new TaskCompletionSource(); + public Task Task => _source.Task; + + public void Resume(Helpers.Cookie[] cookies) + { + Handled = true; + _source.SetResult(cookies); + } + + public override void Cancel() + { + Handled = true; + _source.TrySetCanceled(); + } + } + } + + +} diff --git a/Wabbajack.Lib/Downloaders/DeadlyStreamDownloader.cs b/Wabbajack.Lib/Downloaders/DeadlyStreamDownloader.cs new file mode 100644 index 00000000..2da650c1 --- /dev/null +++ b/Wabbajack.Lib/Downloaders/DeadlyStreamDownloader.cs @@ -0,0 +1,24 @@ +using System; + +namespace Wabbajack.Lib.Downloaders +{ + public class DeadlyStreamDownloader : AbstractIPS4Downloader + { + #region INeedsDownload + public override string SiteName => "Deadly Stream"; + public override Uri SiteURL => new Uri("https://www.deadlystream.com"); + public override Uri IconUri => new Uri("https://www.deadlystream.com/favicon.ico"); + #endregion + + public DeadlyStreamDownloader() : base(new Uri("https://deadlystream.com/login"), "deadlystream", + "deadlystream.com") + { + + } + + public class State : State + { + + } + } +} diff --git a/Wabbajack.Lib/Downloaders/DownloadDispatcher.cs b/Wabbajack.Lib/Downloaders/DownloadDispatcher.cs index f2524f95..143db3d6 100644 --- a/Wabbajack.Lib/Downloaders/DownloadDispatcher.cs +++ b/Wabbajack.Lib/Downloaders/DownloadDispatcher.cs @@ -18,6 +18,8 @@ namespace Wabbajack.Lib.Downloaders new NexusDownloader(), new MediaFireDownloader(), new LoversLabDownloader(), + new VectorPlexusDownloader(), + new DeadlyStreamDownloader(), new HTTPDownloader(), new ManualDownloader(), }; diff --git a/Wabbajack.Lib/Downloaders/GoogleDriveDownloader.cs b/Wabbajack.Lib/Downloaders/GoogleDriveDownloader.cs index 32bc3488..385f7212 100644 --- a/Wabbajack.Lib/Downloaders/GoogleDriveDownloader.cs +++ b/Wabbajack.Lib/Downloaders/GoogleDriveDownloader.cs @@ -1,8 +1,8 @@ using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; -using System.Web; using Wabbajack.Common; +using Wabbajack.Lib.Exceptions; using Wabbajack.Lib.Validation; namespace Wabbajack.Lib.Downloaders diff --git a/Wabbajack.Lib/Downloaders/HTTPDownloader.cs b/Wabbajack.Lib/Downloaders/HTTPDownloader.cs index dd353f9b..c9264885 100644 --- a/Wabbajack.Lib/Downloaders/HTTPDownloader.cs +++ b/Wabbajack.Lib/Downloaders/HTTPDownloader.cs @@ -7,11 +7,10 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Reflection.Emit; using System.Threading.Tasks; -using System.Web; -using Windows.Networking.BackgroundTransfer; using Ceras; using SharpCompress.Common; using Wabbajack.Common; +using Wabbajack.Lib.Exceptions; using Wabbajack.Lib.Validation; using File = Alphaleonis.Win32.Filesystem.File; diff --git a/Wabbajack.Lib/Downloaders/LoversLabDownloader.cs b/Wabbajack.Lib/Downloaders/LoversLabDownloader.cs index 8ebe3d4c..1e639860 100644 --- a/Wabbajack.Lib/Downloaders/LoversLabDownloader.cs +++ b/Wabbajack.Lib/Downloaders/LoversLabDownloader.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using System.Web; using System.Windows.Input; +using CefSharp; using ReactiveUI; using Wabbajack.Common; using Wabbajack.Lib.LibCefHelpers; @@ -21,213 +22,32 @@ using File = Alphaleonis.Win32.Filesystem.File; namespace Wabbajack.Lib.Downloaders { - public class LoversLabDownloader : IDownloader, INeedsLogin + public class LoversLabDownloader : AbstractIPS4Downloader { - internal HttpClient _authedClient; - - #region INeedsDownload - - public ReactiveCommand TriggerLogin { get; } - public ReactiveCommand ClearLogin { get; } - public IObservable IsLoggedIn => Utils.HaveEncryptedJsonObservable("loverslabcookies"); - public string SiteName => "Lovers Lab"; - public IObservable MetaInfo => Observable.Return(""); - public Uri SiteURL => new Uri("https://loverslab.com"); - public Uri IconUri => new Uri("https://www.loverslab.com/favicon.ico"); - - + public override string SiteName => "Lovers Lab"; + public override Uri SiteURL => new Uri("https://www.loverslab.com"); + public override Uri IconUri => new Uri("https://www.loverslab.com/favicon.ico"); #endregion - public LoversLabDownloader() + public LoversLabDownloader() : base(new Uri("https://www.loverslab.com/login"), + "loverslabcookies", "loverslab.com") { - TriggerLogin = ReactiveCommand.CreateFromTask( - execute: () => Utils.CatchAndLog(async () => await Utils.Log(new RequestLoversLabLogin()).Task), - canExecute: IsLoggedIn.Select(b => !b).ObserveOn(RxApp.MainThreadScheduler)); - ClearLogin = ReactiveCommand.Create( - execute: () => Utils.CatchAndLog(() => Utils.DeleteEncryptedJson("loverslabcookies")), - canExecute: IsLoggedIn.ObserveOn(RxApp.MainThreadScheduler)); } - - - public async Task GetDownloaderState(dynamic archive_ini) + protected override async Task WhileWaiting(IWebDriver browser) { - Uri url = DownloaderUtils.GetDirectURL(archive_ini); - if (url == null || url.Host != "www.loverslab.com" || !url.AbsolutePath.StartsWith("/files/file/")) return null; - var id = HttpUtility.ParseQueryString(url.Query)["r"]; - var file = url.AbsolutePath.Split('/').Last(s => s != ""); - - return new State - { - FileID = id, - FileName = file - }; - } - - public async Task Prepare() - { - _authedClient = (await GetAuthedClient()) ?? throw new Exception("not logged into LL, TODO"); - } - - public static async Task GetAndCacheLoversLabCookies(IWebDriver browser, Action updateStatus, CancellationToken cancel) - { - updateStatus("Please Log Into Lovers Lab"); - await browser.NavigateTo(new Uri("https://www.loverslab.com/login")); - async Task CleanAds() - { - try - { - await browser.EvaluateJavaScript( - "document.querySelectorAll(\".ll_adblock\").forEach(function (itm) { itm.innerHTML = \"\";});"); - } - catch (Exception ex) - { - Utils.Error(ex); - } - return false; - } - var cookies = new Helpers.Cookie[0]; - while (true) - { - cancel.ThrowIfCancellationRequested(); - await CleanAds(); - cookies = (await browser.GetCookies("loverslab.com")); - if (cookies.FirstOrDefault(c => c.Name == "ips4_member_id") != null) - break; - await Task.Delay(500, cancel); - } - - cookies.ToEcryptedJson("loverslabcookies"); - - return cookies; - } - - public async Task GetAuthedClient() - { - Helpers.Cookie[] cookies; try { - cookies = Utils.FromEncryptedJson("loverslabcookies"); - if (cookies != null) - return Helpers.GetClient(cookies, "https://www.loverslab.com"); + await browser.EvaluateJavaScript( + "document.querySelectorAll(\".ll_adblock\").forEach(function (itm) { itm.innerHTML = \"\";});"); } - catch (FileNotFoundException) { } - - cookies = await Utils.Log(new RequestLoversLabLogin()).Task; - return Helpers.GetClient(cookies, "https://www.loverslab.com"); - } - - public class State : AbstractDownloadState - { - public string FileID { get; set; } - public string FileName { get; set; } - - public override object[] PrimaryKey { get => new object[] {FileID, FileName}; } - - public override bool IsWhitelisted(ServerWhitelist whitelist) + catch (Exception ex) { - return true; - } - - public override async Task Download(Archive a, string destination) - { - var stream = await ResolveDownloadStream(); - using (var file = File.OpenWrite(destination)) - { - stream.CopyTo(file); - } - } - - private async Task ResolveDownloadStream() - { - var result = DownloadDispatcher.GetInstance(); - TOP: - var html = await result._authedClient.GetStringAsync( - $"https://www.loverslab.com/files/file/{FileName}/?do=download&r={FileID}"); - - var pattern = new Regex("(?<=csrfKey=).*(?=[&\"\'])"); - var csrfKey = pattern.Matches(html).Cast().Where(m => m.Length == 32).Select(m => m.ToString()).FirstOrDefault(); - - if (csrfKey == null) - return null; - - var url = - $"https://www.loverslab.com/files/file/{FileName}/?do=download&r={FileID}&confirm=1&t=1&csrfKey={csrfKey}"; - - var streamResult = await result._authedClient.GetAsync(url); - if (streamResult.StatusCode != HttpStatusCode.OK) - { - Utils.Error(new InvalidOperationException(), $"LoversLab servers reported an error for file: {FileID}"); - } - - var content_type = streamResult.Content.Headers.ContentType; - - if (content_type.MediaType == "application/json") - { - // Sometimes LL hands back a json object telling us to wait until a certain time - var times = (await streamResult.Content.ReadAsStringAsync()).FromJSONString(); - var secs = times.download - times.currentTime; - for (int x = 0; x < secs; x++) - { - Utils.Status($"Waiting for {secs} at the request of LoversLab", x * 100 / secs); - await Task.Delay(1000); - } - Utils.Status("Retrying download"); - goto TOP; - } - - return await streamResult.Content.ReadAsStreamAsync(); - } - - internal class WaitResponse - { - public int download { get; set; } - public int currentTime { get; set; } - } - - public override async Task Verify() - { - var stream = await ResolveDownloadStream(); - if (stream == null) - { - return false; - } - - stream.Close(); - return true; - } - - public override IDownloader GetDownloader() - { - return DownloadDispatcher.GetInstance(); - } - - public override string GetReportEntry(Archive a) - { - return $"* Lovers Lab - [{a.Name}](https://www.loverslab.com/files/file/{FileName}/?do=download&r={FileID})"; + Utils.Error(ex); } } - - } - - public class RequestLoversLabLogin : AUserIntervention - { - public override string ShortDescription => "Getting LoversLab information"; - public override string ExtendedDescription { get; } - - private readonly TaskCompletionSource _source = new TaskCompletionSource(); - public Task Task => _source.Task; - - public void Resume(Helpers.Cookie[] cookies) + public class State : State { - Handled = true; - _source.SetResult(cookies); - } - - public override void Cancel() - { - Handled = true; - _source.TrySetCanceled(); } } } diff --git a/Wabbajack.Lib/Downloaders/ManualDownloader.cs b/Wabbajack.Lib/Downloaders/ManualDownloader.cs index c3352514..988dad42 100644 --- a/Wabbajack.Lib/Downloaders/ManualDownloader.cs +++ b/Wabbajack.Lib/Downloaders/ManualDownloader.cs @@ -5,8 +5,8 @@ using System.Linq; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading.Tasks; -using Syroot.Windows.IO; using Wabbajack.Common; +using Wabbajack.Common.IO; using Wabbajack.Lib.Validation; using File = System.IO.File; diff --git a/Wabbajack.Lib/Downloaders/NexusDownloader.cs b/Wabbajack.Lib/Downloaders/NexusDownloader.cs index b2338a34..7acc5875 100644 --- a/Wabbajack.Lib/Downloaders/NexusDownloader.cs +++ b/Wabbajack.Lib/Downloaders/NexusDownloader.cs @@ -36,6 +36,11 @@ namespace Wabbajack.Lib.Downloaders public NexusDownloader() { + if (CLIArguments.ApiKey != null) + { + CLIArguments.ApiKey.ToEcryptedJson("nexusapikey"); + } + TriggerLogin = ReactiveCommand.CreateFromTask( execute: () => Utils.CatchAndLog(NexusApiClient.RequestAndCacheAPIKey), canExecute: IsLoggedIn.Select(b => !b).ObserveOn(RxApp.MainThreadScheduler)); diff --git a/Wabbajack.Lib/Downloaders/VectorPlexusDownloader.cs b/Wabbajack.Lib/Downloaders/VectorPlexusDownloader.cs new file mode 100644 index 00000000..43a5d551 --- /dev/null +++ b/Wabbajack.Lib/Downloaders/VectorPlexusDownloader.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; + +namespace Wabbajack.Lib.Downloaders +{ + public class VectorPlexusDownloader : AbstractIPS4Downloader + { + #region INeedsDownload + public override string SiteName => "Vector Plexus"; + public override Uri SiteURL => new Uri("https://vectorplexus.com"); + public override Uri IconUri => new Uri("https://www.vectorplexus.com/favicon.ico"); + #endregion + + public VectorPlexusDownloader() : base(new Uri("https://vectorplexus.com/login"), + "vectorplexus", "vectorplexus.com") + { + } + public class State : State + { + } + } +} diff --git a/Wabbajack.Lib/Exceptions/HttpException.cs b/Wabbajack.Lib/Exceptions/HttpException.cs new file mode 100644 index 00000000..41d46b03 --- /dev/null +++ b/Wabbajack.Lib/Exceptions/HttpException.cs @@ -0,0 +1,17 @@ +using System; + +namespace Wabbajack.Lib.Exceptions +{ + public class HttpException : Exception + { + public string Reason { get; set; } + public int Code { get; set; } + + public HttpException(int code, string reason) : base($"Http Error {code} - {reason}") + { + Code = code; + Reason = reason; + } + + } +} diff --git a/Wabbajack.Lib/MO2Compiler.cs b/Wabbajack.Lib/MO2Compiler.cs index f6b682dd..c3f2a80f 100644 --- a/Wabbajack.Lib/MO2Compiler.cs +++ b/Wabbajack.Lib/MO2Compiler.cs @@ -1,5 +1,6 @@ using Compression.BSA; using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; @@ -159,11 +160,21 @@ namespace Wabbajack.Lib .Where(p => p.FileExists()) .Select(p => new RawSourceFile(VFS.Index.ByRootPath[p], p.RelativeTo(MO2Folder))); - var gameFiles = Directory.EnumerateFiles(GamePath, "*", SearchOption.AllDirectories) - .Where(p => p.FileExists()) - .Select(p => new RawSourceFile(VFS.Index.ByRootPath[p], Path.Combine(Consts.GameFolderFilesDir, p.RelativeTo(GamePath)))); + // If Game Folder Files exists, ignore the game folder + IEnumerable gameFiles; + if (!Directory.Exists(Path.Combine(MO2Folder, Consts.GameFolderFilesDir))) + { + gameFiles = Directory.EnumerateFiles(GamePath, "*", SearchOption.AllDirectories) + .Where(p => p.FileExists()) + .Select(p => new RawSourceFile(VFS.Index.ByRootPath[p], + Path.Combine(Consts.GameFolderFilesDir, p.RelativeTo(GamePath)))); + } + else + { + gameFiles = new List(); + } + - ModMetas = Directory.EnumerateDirectories(Path.Combine(MO2Folder, "mods")) .Keep(f => { @@ -496,6 +507,7 @@ namespace Wabbajack.Lib Utils.Log("Generating compilation stack"); return new List { + new IgnoreGameFilesIfGameFolderFilesExist(this), new IncludePropertyFiles(this), new IgnoreStartsWith(this,"logs\\"), new IgnoreStartsWith(this, "downloads\\"), @@ -536,7 +548,8 @@ namespace Wabbajack.Lib new IgnoreEndsWith(this, "HavokBehaviorPostProcess.exe"), // Theme file MO2 downloads somehow new IgnoreEndsWith(this, "splash.png"), - + // File to force MO2 into portable mode + new IgnoreEndsWith(this, "portable.txt"), new IgnoreEndsWith(this, ".bin"), new IgnoreEndsWith(this, ".refcache"), diff --git a/Wabbajack.Lib/MO2Installer.cs b/Wabbajack.Lib/MO2Installer.cs index 46642daf..8827739f 100644 --- a/Wabbajack.Lib/MO2Installer.cs +++ b/Wabbajack.Lib/MO2Installer.cs @@ -30,12 +30,13 @@ namespace Wabbajack.Lib public string GameFolder { get; set; } - public MO2Installer(string archive, ModList modList, string outputFolder, string downloadFolder) + public MO2Installer(string archive, ModList modList, string outputFolder, string downloadFolder, SystemParameters parameters) : base( archive: archive, modList: modList, outputFolder: outputFolder, - downloadFolder: downloadFolder) + downloadFolder: downloadFolder, + parameters: parameters) { } @@ -44,7 +45,7 @@ namespace Wabbajack.Lib if (cancel.IsCancellationRequested) return false; var metric = Metrics.Send("begin_install", ModList.Name); - ConfigureProcessor(18, await RecommendQueueSize()); + ConfigureProcessor(19, await RecommendQueueSize()); var game = ModList.GameType.MetaData(); if (GameFolder == null) @@ -52,10 +53,10 @@ namespace Wabbajack.Lib if (GameFolder == null) { - MessageBox.Show( + await Utils.Log(new CriticalFailureIntervention( $"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", MessageBoxButton.OK); + "Could not find game location")).Task; Utils.Log("Exiting because we couldn't find the game folder."); return false; } @@ -135,6 +136,9 @@ namespace Wabbajack.Lib UpdateTracker.NextStep("Generating Merges"); await zEditIntegration.GenerateMerges(this); + UpdateTracker.NextStep("Set MO2 into portable"); + ForcePortable(); + UpdateTracker.NextStep("Updating System-specific ini settings"); SetScreenSizeInPrefs(); @@ -144,6 +148,20 @@ namespace Wabbajack.Lib return true; } + 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() { @@ -173,42 +191,6 @@ namespace Wabbajack.Lib } } - private async Task AskToEndorse() - { - var mods = ModList.Archives - .Select(m => m.State) - .OfType() - .GroupBy(f => (f.GameName, f.ModID)) - .Select(mod => mod.First()) - .ToArray(); - - var result = MessageBox.Show( - $"Installation has completed, but you have installed {mods.Length} from the Nexus, would you like to" + - " endorse these mods to show support to the authors? It will only take a few moments.", "Endorse Mods?", - MessageBoxButton.YesNo, MessageBoxImage.Question); - - if (result != MessageBoxResult.Yes) return; - - // Shuffle mods so that if we hit a API limit we don't always miss the same mods - var r = new Random(); - for (var i = 0; i < mods.Length; i++) - { - var a = r.Next(mods.Length); - var b = r.Next(mods.Length); - var tmp = mods[a]; - mods[a] = mods[b]; - mods[b] = tmp; - } - - await mods.PMap(Queue, async mod => - { - var client = await NexusApiClient.Get(); - var er = await client.EndorseMod(mod); - Utils.Log($"Endorsed {mod.GameName} - {mod.ModID} - Result: {er.message}"); - }); - Info("Done! You may now exit the application!"); - } - private async Task BuildBSAs() { var bsas = ModList.Directives.OfType().ToList(); @@ -299,8 +281,8 @@ namespace Wabbajack.Lib if (data.Sections["Display"]["iSize W"] != null && data.Sections["Display"]["iSize H"] != null) { - data.Sections["Display"]["iSize W"] = SystemParameters.PrimaryScreenWidth.ToString(CultureInfo.CurrentCulture); - data.Sections["Display"]["iSize H"] = SystemParameters.PrimaryScreenHeight.ToString(CultureInfo.CurrentCulture); + data.Sections["Display"]["iSize W"] = SystemParameters.ScreenWidth.ToString(CultureInfo.CurrentCulture); + data.Sections["Display"]["iSize H"] = SystemParameters.ScreenHeight.ToString(CultureInfo.CurrentCulture); } parser.WriteFile(file, data); diff --git a/Wabbajack.Lib/ModListRegistry/ModListMetadata.cs b/Wabbajack.Lib/ModListRegistry/ModListMetadata.cs index c42f54c8..038777a8 100644 --- a/Wabbajack.Lib/ModListRegistry/ModListMetadata.cs +++ b/Wabbajack.Lib/ModListRegistry/ModListMetadata.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using System.Net.Http; using System.Threading.Tasks; -using System.Windows.Media.Imaging; using Newtonsoft.Json; using Wabbajack.Common; using File = System.IO.File; @@ -45,7 +45,7 @@ namespace Wabbajack.Lib.ModListRegistry public string ImageUri { get; set; } [JsonIgnore] - public BitmapImage Image { get; set; } + public Bitmap Image { get; set; } [JsonProperty("readme")] public string Readme { get; set; } diff --git a/Wabbajack.Lib/NexusApi/NexusApi.cs b/Wabbajack.Lib/NexusApi/NexusApi.cs index 4365a6c3..c4e417bb 100644 --- a/Wabbajack.Lib/NexusApi/NexusApi.cs +++ b/Wabbajack.Lib/NexusApi/NexusApi.cs @@ -8,17 +8,12 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Security.Authentication; -using System.Text; using System.Threading.Tasks; using Wabbajack.Common; using Wabbajack.Lib.Downloaders; -using Wabbajack.Lib.LibCefHelpers; using WebSocketSharp; using static Wabbajack.Lib.NexusApi.NexusApiUtils; using System.Threading; -using CefSharp; -using CefSharp.Handler; -using Newtonsoft.Json; using Wabbajack.Lib.WebAutomation; namespace Wabbajack.Lib.NexusApi diff --git a/Wabbajack.Lib/Properties/AssemblyInfo.cs b/Wabbajack.Lib/Properties/AssemblyInfo.cs deleted file mode 100644 index 5d41ef04..00000000 --- a/Wabbajack.Lib/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Wabbajack.Lib")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Wabbajack.Lib")] -[assembly: AssemblyCopyright("Copyright © 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("0a820830-a298-497d-85e0-e9a89efef5fe")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Wabbajack.Lib/ReportBuilder.cs b/Wabbajack.Lib/ReportBuilder.cs index 8096a162..8c64570d 100644 --- a/Wabbajack.Lib/ReportBuilder.cs +++ b/Wabbajack.Lib/ReportBuilder.cs @@ -129,7 +129,7 @@ namespace Wabbajack.Lib .Concat(lst.Directives .OfType() .Select(f => (f.To, "patched", SizeForID(f.PatchID)))) - .ToHashSet() + .Distinct() .OrderByDescending(f => f.Item3); NoWrapText("\n\n### Summary of inlined files in this installer"); diff --git a/Wabbajack.Lib/StatusMessages/CriticalFailureIntervention.cs b/Wabbajack.Lib/StatusMessages/CriticalFailureIntervention.cs new file mode 100644 index 00000000..2062e95a --- /dev/null +++ b/Wabbajack.Lib/StatusMessages/CriticalFailureIntervention.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using Wabbajack.Common; + +namespace Wabbajack.Lib +{ + /// + /// This should probably be replaced with an error, but this is just to get messageboxes out of the .Lib library + /// + public class CriticalFailureIntervention : AUserIntervention + { + private TaskCompletionSource _source = new TaskCompletionSource(); + public Task Task => _source.Task; + + public CriticalFailureIntervention(string description, string title) + { + ExtendedDescription = description; + ShortDescription = title; + } + public override string ShortDescription { get; } + public override string ExtendedDescription { get; } + public override void Cancel() + { + Handled = true; + _source.SetResult(ConfirmationIntervention.Choice.Abort); + } + } +} diff --git a/Wabbajack.Lib/StatusMessages/YesNoIntervention.cs b/Wabbajack.Lib/StatusMessages/YesNoIntervention.cs new file mode 100644 index 00000000..b37a2be6 --- /dev/null +++ b/Wabbajack.Lib/StatusMessages/YesNoIntervention.cs @@ -0,0 +1,15 @@ +using Wabbajack.Common; + +namespace Wabbajack.Lib +{ + public class YesNoIntervention : ConfirmationIntervention + { + public YesNoIntervention(string description, string title) + { + ExtendedDescription = description; + ShortDescription = title; + } + public override string ShortDescription { get; } + public override string ExtendedDescription { get; } + } +} diff --git a/Wabbajack.Lib/SystemParameters.cs b/Wabbajack.Lib/SystemParameters.cs new file mode 100644 index 00000000..ceec3d09 --- /dev/null +++ b/Wabbajack.Lib/SystemParameters.cs @@ -0,0 +1,8 @@ +namespace Wabbajack.Lib +{ + public class SystemParameters + { + public int ScreenHeight { get; set; } + public int ScreenWidth { get; set; } + } +} diff --git a/Wabbajack.Lib/VortexCompiler.cs b/Wabbajack.Lib/VortexCompiler.cs index 14957fd0..adb0bb49 100644 --- a/Wabbajack.Lib/VortexCompiler.cs +++ b/Wabbajack.Lib/VortexCompiler.cs @@ -6,9 +6,9 @@ using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using System.Threading; -using Microsoft.WindowsAPICodePack.Shell; using Newtonsoft.Json; using Wabbajack.Common; +using Wabbajack.Common.IO; using Wabbajack.Common.StoreHandlers; using Wabbajack.Lib.CompilationSteps; using Wabbajack.Lib.NexusApi; diff --git a/Wabbajack.Lib/VortexInstaller.cs b/Wabbajack.Lib/VortexInstaller.cs index fad6aeeb..2d86df47 100644 --- a/Wabbajack.Lib/VortexInstaller.cs +++ b/Wabbajack.Lib/VortexInstaller.cs @@ -20,12 +20,13 @@ namespace Wabbajack.Lib public override ModManager ModManager => ModManager.Vortex; - public VortexInstaller(string archive, ModList modList, string outputFolder, string downloadFolder) + public VortexInstaller(string archive, ModList modList, string outputFolder, string downloadFolder, SystemParameters parameters) : base( archive: archive, modList: modList, outputFolder: outputFolder, - downloadFolder: downloadFolder) + downloadFolder: downloadFolder, + parameters: parameters) { #if DEBUG // TODO: only for testing @@ -39,11 +40,15 @@ namespace Wabbajack.Lib { if (cancel.IsCancellationRequested) return false; var metric = Metrics.Send("begin_install", ModList.Name); - MessageBox.Show( + var result = await Utils.Log(new YesNoIntervention( "Vortex Support is still experimental and may produce unexpected results. " + "If anything fails go to the special vortex support channels on the discord. @erri120#2285 " + - "for support.", "Warning", - MessageBoxButton.OK); + "for support.", "Continue with experimental feature?")).Task; + if (result == ConfirmationIntervention.Choice.Abort) + { + Utils.Log("Exiting at request of user"); + return false; + } if (cancel.IsCancellationRequested) return false; ConfigureProcessor(10, await RecommendQueueSize()); @@ -108,11 +113,11 @@ namespace Wabbajack.Lib if (!ModList.Directives.Any(d => d.To.StartsWith(Consts.ManualGameFilesDir))) return; - var result = MessageBox.Show("Some mods from this ModList must be installed directly into " + + var result = await Utils.Log(new YesNoIntervention("Some mods from this ModList must be installed directly into " + "the game folder. Do you want to do this manually or do you want Wabbajack " + - "to do this for you?", "Question", MessageBoxButton.YesNo); + "to do this for you?", "Install game folder mods?")).Task; - if (result != MessageBoxResult.Yes) + if (result != ConfirmationIntervention.Choice.Continue) return; var manualFilesDir = Path.Combine(OutputFolder, Consts.ManualGameFilesDir); @@ -167,12 +172,13 @@ namespace Wabbajack.Lib if (!ModList.Directives.Any(s => s is SteamMeta)) return; - var result = MessageBox.Show("The ModList you are installing requires Steam Workshop Items to exist. " + - "You can check the Workshop Items in the manifest of this ModList. Wabbajack can start Steam for you " + - "and download the Items automatically. Do you want to proceed with this step?", - "Warning", MessageBoxButton.YesNo); + var result = await Utils.Log(new YesNoIntervention( + "The ModList you are installing requires Steam Workshop Items to exist. " + + "You can check the Workshop Items in the manifest of this ModList. Wabbajack can start Steam for you " + + "and download the Items automatically. Do you want to proceed with this step?", + "Download Steam Workshop Items?")).Task; - if (result != MessageBoxResult.Yes) + if (result != ConfirmationIntervention.Choice.Continue) return; await ModList.Directives.OfType() diff --git a/Wabbajack.Lib/Wabbajack.Lib.csproj b/Wabbajack.Lib/Wabbajack.Lib.csproj index a78d73b3..57b0cf35 100644 --- a/Wabbajack.Lib/Wabbajack.Lib.csproj +++ b/Wabbajack.Lib/Wabbajack.Lib.csproj @@ -1,248 +1,74 @@  - - - - Debug - AnyCPU - {0A820830-A298-497D-85E0-E9A89EFEF5FE} - Library - Properties - Wabbajack.Lib - Wabbajack.Lib - v4.8 - 512 - true - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - x64 - CS1998 - CS4014 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - CS1998 - CS4014 - - - true - bin\x64\Debug\ - DEBUG;TRACE - full - x64 - 7.3 - prompt - MinimumRecommendedRules.ruleset - false - CS4014 - CS1998 - - - bin\x64\Release\ - TRACE - true - pdbonly - x64 - 7.3 - prompt - MinimumRecommendedRules.ruleset - CS4014 - CS1998 - - - - ..\..\..\Users\tbald\.nuget\packages\mongodb.bson\2.10.0\lib\net452\MongoDB.Bson.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {ff5d892f-8ff4-44fc-8f7f-cd58f307ad1b} - Compression.BSA - - - {b3f3fb6e-b9eb-4f49-9875-d78578bc7ae5} - Wabbajack.Common - - - {5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E} - Wabbajack.VirtualFileSystem - - - - - - - - - - - 2.2.6 - - - 75.1.143 - - - 4.1.7 - - - 0.15.1 - - - 1.11.17 - - - 1.7.1 - - - 1.1.3.3 - - - 6.0.0 - - - 2.1.0 - - - 12.0.3 - - - 11.1.1 - - - 11.1.1 - - - 0.24.0 - - - 1.2.1 - - - 4.3.2 - - - 1.0.4 - - - 8.0.0 - - - - + + + netstandard2.0 + AnyCPU;x64 + + + + 75.1.143 + + + 75.1.143 + + + 4.1.7 + + + 0.15.1 + + + 6.0.6 + + + 2.2.2.1 + + + 1.11.17 + + + 1.7.1 + + + 4.7.0 + + + 2.1.0 + + + 11.1.6 + + + 11.1.6 + + + 0.24.0 + + + 1.7.0 + + + 4.7.0 + + + 4.3.4 + + + 1.0.1 + + + 1.0.0 + + + + + + + + + + + + + \ No newline at end of file diff --git a/Wabbajack.Lib/WebAutomation/CefSharpWrapper.cs b/Wabbajack.Lib/WebAutomation/CefSharpWrapper.cs index 84fa7c37..fcb9e919 100644 --- a/Wabbajack.Lib/WebAutomation/CefSharpWrapper.cs +++ b/Wabbajack.Lib/WebAutomation/CefSharpWrapper.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.Remoting.Channels; using System.Text; using System.Threading.Tasks; using CefSharp; diff --git a/Wabbajack.Test/ACompilerTest.cs b/Wabbajack.Test/ACompilerTest.cs index 58f2d5cd..19b8fd07 100644 --- a/Wabbajack.Test/ACompilerTest.cs +++ b/Wabbajack.Test/ACompilerTest.cs @@ -5,6 +5,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Wabbajack.Common; using Wabbajack.Lib; using Wabbajack.Lib.LibCefHelpers; +using Wabbajack.Util; namespace Wabbajack.Test { @@ -57,7 +58,8 @@ namespace Wabbajack.Test archive: compiler.ModListOutputFile, modList: modlist, outputFolder: utils.InstallFolder, - downloadFolder: utils.DownloadsFolder); + downloadFolder: utils.DownloadsFolder, + parameters: SystemParametersConstructor.Create()); installer.WarnOnOverwrite = false; installer.GameFolder = utils.GameFolder; await installer.Begin(); diff --git a/Wabbajack.Test/AVortexCompilerTest.cs b/Wabbajack.Test/AVortexCompilerTest.cs index 1f9c7466..a8691415 100644 --- a/Wabbajack.Test/AVortexCompilerTest.cs +++ b/Wabbajack.Test/AVortexCompilerTest.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using Wabbajack.Common; using Wabbajack.Lib; +using Wabbajack.Util; namespace Wabbajack.Test { @@ -67,7 +68,8 @@ namespace Wabbajack.Test archive: vortexCompiler.ModListOutputFile, modList: modList, outputFolder: utils.InstallFolder, - downloadFolder: utils.DownloadsFolder) + downloadFolder: utils.DownloadsFolder, + parameters: SystemParametersConstructor.Create()) { GameFolder = utils.GameFolder, }; diff --git a/Wabbajack.Test/DownloaderTests.cs b/Wabbajack.Test/DownloaderTests.cs index 395a0be8..9d6fd0c0 100644 --- a/Wabbajack.Test/DownloaderTests.cs +++ b/Wabbajack.Test/DownloaderTests.cs @@ -310,6 +310,34 @@ namespace Wabbajack.Test Assert.AreEqual(File.ReadAllText(filename), "Cheese for Everyone!"); } + + [TestMethod] + public async Task VectorPlexusDownload() + { + await DownloadDispatcher.GetInstance().Prepare(); + var ini = @"[General] + directURL=https://vectorplexus.com/files/file/290-wabbajack-test-file"; + + var state = (AbstractDownloadState)await DownloadDispatcher.ResolveArchive(ini.LoadIniString()); + + Assert.IsNotNull(state); + + /*var url_state = DownloadDispatcher.ResolveArchive("https://www.loverslab.com/files/file/11116-test-file-for-wabbajack-integration/?do=download&r=737123&confirm=1&t=1"); + Assert.AreEqual("http://build.wabbajack.org/WABBAJACK_TEST_FILE.txt", + ((HTTPDownloader.State)url_state).Url); + */ + var converted = state.ViaJSON(); + Assert.IsTrue(await converted.Verify()); + var filename = Guid.NewGuid().ToString(); + + Assert.IsTrue(converted.IsWhitelisted(new ServerWhitelist { AllowedPrefixes = new List() })); + + await converted.Download(new Archive { Name = "Vector Plexus Test.zip" }, filename); + + Assert.AreEqual("eSIyd+KOG3s=", Utils.FileHash(filename)); + + Assert.AreEqual(File.ReadAllText(filename), "Cheese for Everyone!"); + } [TestMethod] public async Task GameFileSourceDownload() diff --git a/Wabbajack.Test/EndToEndTests.cs b/Wabbajack.Test/EndToEndTests.cs index d120ede9..1a9da3ef 100644 --- a/Wabbajack.Test/EndToEndTests.cs +++ b/Wabbajack.Test/EndToEndTests.cs @@ -8,6 +8,7 @@ using Wabbajack.Common; using Wabbajack.Lib; using Wabbajack.Lib.Downloaders; using Wabbajack.Lib.NexusApi; +using Wabbajack.Util; namespace Wabbajack.Test { @@ -153,7 +154,8 @@ namespace Wabbajack.Test archive: compiler.ModListOutputFile, modList: modlist, outputFolder: utils.InstallFolder, - downloadFolder: utils.DownloadsFolder); + downloadFolder: utils.DownloadsFolder, + parameters: SystemParametersConstructor.Create()); installer.GameFolder = utils.GameFolder; await installer.Begin(); } diff --git a/Wabbajack.Test/FilePickerTests.cs b/Wabbajack.Test/FilePickerTests.cs index 5aff8958..33f8b16c 100644 --- a/Wabbajack.Test/FilePickerTests.cs +++ b/Wabbajack.Test/FilePickerTests.cs @@ -9,6 +9,7 @@ using DynamicData; using Microsoft.VisualStudio.TestTools.UnitTesting; using Wabbajack.Common; using Wabbajack.Lib; +using Wabbajack.UI; namespace Wabbajack.Test { diff --git a/Wabbajack.Test/IniTests.cs b/Wabbajack.Test/IniTests.cs new file mode 100644 index 00000000..6ad26a1d --- /dev/null +++ b/Wabbajack.Test/IniTests.cs @@ -0,0 +1,27 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Wabbajack.Common; + +namespace Wabbajack.Test +{ + [TestClass] + public class IniTests + { + + [TestMethod] + public void TestByteArrayParsing() + { + Assert.AreEqual("bar", @"[General] + foo = bar".LoadIniString().General.foo); + + Assert.AreEqual("baz\\bar", @"[General] + foo = baz\\bar".LoadIniString().General.foo); + + Assert.AreEqual("bar", @"[General] + foo = @ByteArray(bar)".LoadIniString().General.foo); + + Assert.AreEqual("foo\\h̴̹͚̎é̶̘͙̐l̶͕̔͑p̴̯̋͂m̶̞̮͘͠e̸͉͙͆̄\\baz", @"[General] + foo = @ByteArray(foo\\\x68\xcc\xb4\xcc\x8e\xcc\xb9\xcd\x9a\x65\xcc\xb6\xcd\x81\xcc\x90\xcc\x98\xcd\x99\x6c\xcc\xb6\xcc\x94\xcd\x91\xcd\x95\x70\xcc\xb4\xcc\x8b\xcd\x82\xcc\xaf\x6d\xcc\xb6\xcd\x98\xcd\xa0\xcc\x9e\xcc\xae\x65\xcc\xb8\xcd\x86\xcc\x84\xcd\x89\xcd\x99\\baz)".LoadIniString().General.foo); + } + + } +} diff --git a/Wabbajack.Test/SanityTests.cs b/Wabbajack.Test/SanityTests.cs index 411ad9d3..053c28c6 100644 --- a/Wabbajack.Test/SanityTests.cs +++ b/Wabbajack.Test/SanityTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -31,6 +32,44 @@ namespace Wabbajack.Test utils.VerifyInstalledFile(mod, @"Data\scripts\test.pex"); } + + [TestMethod] + public async Task TestDirectMatchFromGameFolder() + { + + var profile = utils.AddProfile(); + var mod = utils.AddMod(); + var test_pex = utils.AddGameFile(@"enbstuff\test.pex", 10); + + utils.Configure(); + + utils.AddManualDownload( + new Dictionary {{"/baz/biz.pex", File.ReadAllBytes(test_pex)}}); + + await CompileAndInstall(profile); + + utils.VerifyInstalledGameFile(@"enbstuff\test.pex"); + } + + [TestMethod] + public async Task TestDirectMatchIsIgnoredWhenGameFolderFilesOverrideExists() + { + + var profile = utils.AddProfile(); + var mod = utils.AddMod(); + var test_pex = utils.AddGameFile(@"enbstuff\test.pex", 10); + + utils.Configure(); + + Directory.CreateDirectory(Path.Combine(utils.MO2Folder, Consts.GameFolderFilesDir)); + + utils.AddManualDownload( + new Dictionary {{"/baz/biz.pex", File.ReadAllBytes(test_pex)}}); + + await CompileAndInstall(profile); + + Assert.IsFalse(File.Exists(Path.Combine(utils.InstallFolder, Consts.GameFolderFilesDir, @"enbstuff\test.pex"))); + } [TestMethod] public async Task TestDuplicateFilesAreCopied() @@ -87,6 +126,11 @@ namespace Wabbajack.Test var extra_path = utils.PathOfInstalledFile(mod, @"something_i_made.foo"); File.WriteAllText(extra_path, "bleh"); + var extra_folder = Path.Combine(Path.GetDirectoryName(utils.PathOfInstalledFile(mod, @"something_i_made.foo")), "folder_i_made"); + Directory.CreateDirectory(extra_folder); + + Assert.IsTrue(Directory.Exists(extra_folder)); + var unchanged_modified = File.GetLastWriteTime(unchanged_path); var modified_modified = File.GetLastWriteTime(modified_path); @@ -105,6 +149,7 @@ namespace Wabbajack.Test Assert.AreEqual(unchanged_modified, File.GetLastWriteTime(unchanged_path)); Assert.AreNotEqual(modified_modified, File.GetLastWriteTime(modified_path)); Assert.IsFalse(File.Exists(extra_path)); + Assert.IsFalse(Directory.Exists(extra_folder)); } diff --git a/Wabbajack.Test/TestUtils.cs b/Wabbajack.Test/TestUtils.cs index 0f68c4f0..1ad33df3 100644 --- a/Wabbajack.Test/TestUtils.cs +++ b/Wabbajack.Test/TestUtils.cs @@ -177,7 +177,26 @@ namespace Wabbajack.Test Assert.Fail($"Index {x} of {mod}\\{file} are not the same"); } } + + public void VerifyInstalledGameFile(string file) + { + var src = Path.Combine(GameFolder, file); + Assert.IsTrue(File.Exists(src), src); + var dest = Path.Combine(InstallFolder, Consts.GameFolderFilesDir, file); + Assert.IsTrue(File.Exists(dest), dest); + + var src_data = File.ReadAllBytes(src); + var dest_data = File.ReadAllBytes(dest); + + Assert.AreEqual(src_data.Length, dest_data.Length); + + for(int x = 0; x < src_data.Length; x++) + { + if (src_data[x] != dest_data[x]) + Assert.Fail($"Index {x} of {Consts.GameFolderFilesDir}\\{file} are not the same"); + } + } public string PathOfInstalledFile(string mod, string file) { return Path.Combine(InstallFolder, "mods", mod, file); @@ -185,12 +204,15 @@ namespace Wabbajack.Test public void VerifyAllFiles() { + var skip_files = new HashSet {"portable.txt"}; foreach (var dest_file in Directory.EnumerateFiles(InstallFolder, "*", DirectoryEnumerationOptions.Recursive)) { var rel_file = dest_file.RelativeTo(InstallFolder); if (rel_file.StartsWith(Consts.LOOTFolderFilesDir) || rel_file.StartsWith(Consts.GameFolderFilesDir)) continue; - Assert.IsTrue(File.Exists(Path.Combine(MO2Folder, rel_file)), $"Only in Destination: {rel_file}"); + + if (!skip_files.Contains(rel_file)) + Assert.IsTrue(File.Exists(Path.Combine(MO2Folder, rel_file)), $"Only in Destination: {rel_file}"); } var skip_extensions = new HashSet {".txt", ".ini"}; @@ -215,5 +237,15 @@ namespace Wabbajack.Test } } } + + public string AddGameFile(string path, int i) + { + var full_path = Path.Combine(GameFolder, path); + var dir = Path.GetDirectoryName(full_path); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + GenerateRandomFileData(full_path, i); + return full_path; + } } } diff --git a/Wabbajack.Test/Wabbajack.Test.csproj b/Wabbajack.Test/Wabbajack.Test.csproj index 1b15cf8d..581ff503 100644 --- a/Wabbajack.Test/Wabbajack.Test.csproj +++ b/Wabbajack.Test/Wabbajack.Test.csproj @@ -127,6 +127,7 @@ + diff --git a/Wabbajack.Test/ZEditIntegrationTests.cs b/Wabbajack.Test/ZEditIntegrationTests.cs index a4b19234..4d683aba 100644 --- a/Wabbajack.Test/ZEditIntegrationTests.cs +++ b/Wabbajack.Test/ZEditIntegrationTests.cs @@ -73,6 +73,8 @@ namespace Wabbajack.Test }); + + var modlist = await CompileAndInstall(profile); var directive = modlist.Directives.Where(m => m.To == $"mods\\{moddest}\\merged.esp").FirstOrDefault(); diff --git a/Wabbajack/App.xaml.cs b/Wabbajack/App.xaml.cs index 1daefbf8..ad59de68 100644 --- a/Wabbajack/App.xaml.cs +++ b/Wabbajack/App.xaml.cs @@ -14,7 +14,9 @@ namespace Wabbajack { public App() { - // Do initialization in MainWindow ctor + CLI.ParseOptions(Environment.GetCommandLineArgs()); + if(CLIArguments.Help) + CLI.DisplayHelpText(); } } } diff --git a/Wabbajack/Properties/AssemblyInfo.cs b/Wabbajack/Properties/AssemblyInfo.cs index 02512be1..5a44f29b 100644 --- a/Wabbajack/Properties/AssemblyInfo.cs +++ b/Wabbajack/Properties/AssemblyInfo.cs @@ -49,5 +49,5 @@ using System.Windows; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.9.13.0")] -[assembly: AssemblyFileVersion("0.9.13.0")] +[assembly: AssemblyVersion("0.9.15.0")] +[assembly: AssemblyFileVersion("0.9.15.0")] diff --git a/Wabbajack.Lib/UI/FilePickerVM.cs b/Wabbajack/UI/FilePickerVM.cs similarity index 99% rename from Wabbajack.Lib/UI/FilePickerVM.cs rename to Wabbajack/UI/FilePickerVM.cs index b087fe19..9295660c 100644 --- a/Wabbajack.Lib/UI/FilePickerVM.cs +++ b/Wabbajack/UI/FilePickerVM.cs @@ -10,7 +10,7 @@ using System.Reactive.Linq; using System.Windows.Input; using Wabbajack.Lib; -namespace Wabbajack.Lib +namespace Wabbajack.UI { public class FilePickerVM : ViewModel { diff --git a/Wabbajack.Lib/UI/UIUtils.cs b/Wabbajack/UI/UIUtils.cs similarity index 98% rename from Wabbajack.Lib/UI/UIUtils.cs rename to Wabbajack/UI/UIUtils.cs index 5c0bb5c5..6c2de57c 100644 --- a/Wabbajack.Lib/UI/UIUtils.cs +++ b/Wabbajack/UI/UIUtils.cs @@ -8,7 +8,7 @@ using System.Windows.Forms; using System.Windows.Media.Imaging; using Wabbajack.Common; -namespace Wabbajack.Lib +namespace Wabbajack.UI { public static class UIUtils { diff --git a/Wabbajack/Util/SystemParametersConstructor.cs b/Wabbajack/Util/SystemParametersConstructor.cs new file mode 100644 index 00000000..e8b3ddbc --- /dev/null +++ b/Wabbajack/Util/SystemParametersConstructor.cs @@ -0,0 +1,17 @@ +using MahApps.Metro.Controls; +using Wabbajack.Lib; + +namespace Wabbajack.Util +{ + public static class SystemParametersConstructor + { + public static SystemParameters Create() + { + return new SystemParameters + { + ScreenWidth = (int)System.Windows.SystemParameters.PrimaryScreenWidth, + ScreenHeight = (int)System.Windows.SystemParameters.PrimaryScreenHeight + }; + } + } +} diff --git a/Wabbajack/View Models/Compilers/CompilerVM.cs b/Wabbajack/View Models/Compilers/CompilerVM.cs index afa07298..409e7906 100644 --- a/Wabbajack/View Models/Compilers/CompilerVM.cs +++ b/Wabbajack/View Models/Compilers/CompilerVM.cs @@ -15,6 +15,7 @@ using System.Windows.Media.Imaging; using Wabbajack.Common; using Wabbajack.Common.StatusFeed; using Wabbajack.Lib; +using Wabbajack.UI; namespace Wabbajack { diff --git a/Wabbajack/View Models/Compilers/MO2CompilerVM.cs b/Wabbajack/View Models/Compilers/MO2CompilerVM.cs index eba68e75..a80c826c 100644 --- a/Wabbajack/View Models/Compilers/MO2CompilerVM.cs +++ b/Wabbajack/View Models/Compilers/MO2CompilerVM.cs @@ -9,6 +9,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using Wabbajack.Common; using Wabbajack.Lib; +using Wabbajack.UI; namespace Wabbajack { diff --git a/Wabbajack/View Models/Compilers/ModlistSettingsEditorVM.cs b/Wabbajack/View Models/Compilers/ModlistSettingsEditorVM.cs index 0f487134..2d94c524 100644 --- a/Wabbajack/View Models/Compilers/ModlistSettingsEditorVM.cs +++ b/Wabbajack/View Models/Compilers/ModlistSettingsEditorVM.cs @@ -6,6 +6,7 @@ using Microsoft.WindowsAPICodePack.Dialogs; using ReactiveUI; using ReactiveUI.Fody.Helpers; using Wabbajack.Lib; +using Wabbajack.UI; namespace Wabbajack { diff --git a/Wabbajack/View Models/Compilers/VortexCompilerVM.cs b/Wabbajack/View Models/Compilers/VortexCompilerVM.cs index d87ff48b..30a7567a 100644 --- a/Wabbajack/View Models/Compilers/VortexCompilerVM.cs +++ b/Wabbajack/View Models/Compilers/VortexCompilerVM.cs @@ -11,6 +11,7 @@ using ReactiveUI.Fody.Helpers; using Wabbajack.Common; using Wabbajack.Common.StoreHandlers; using Wabbajack.Lib; +using Wabbajack.UI; namespace Wabbajack { diff --git a/Wabbajack/View Models/Installers/InstallerVM.cs b/Wabbajack/View Models/Installers/InstallerVM.cs index 95294956..3106794e 100644 --- a/Wabbajack/View Models/Installers/InstallerVM.cs +++ b/Wabbajack/View Models/Installers/InstallerVM.cs @@ -21,6 +21,7 @@ using Wabbajack.Common.StatusFeed; using System.Reactive; using System.Collections.Generic; using System.Windows.Input; +using Wabbajack.UI; namespace Wabbajack { @@ -99,12 +100,10 @@ namespace Wabbajack { if (Path.GetDirectoryName(Assembly.GetEntryAssembly().Location.ToLower()) == KnownFolders.Downloads.Path.ToLower()) { - MessageBox.Show( + Utils.Log(new CriticalFailureIntervention( "Wabbajack is running inside your Downloads folder. This folder is often highly monitored by antivirus software and these can often " + "conflict with the operations Wabbajack needs to perform. Please move this executable outside of your Downloads folder and then restart the app.", - "Cannot run inside Downloads", - MessageBoxButton.OK, - MessageBoxImage.Error); + "Cannot run inside Downloads")).Task.Wait(); Environment.Exit(1); } diff --git a/Wabbajack/View Models/Installers/MO2InstallerVM.cs b/Wabbajack/View Models/Installers/MO2InstallerVM.cs index a0909b96..f32b5a1d 100644 --- a/Wabbajack/View Models/Installers/MO2InstallerVM.cs +++ b/Wabbajack/View Models/Installers/MO2InstallerVM.cs @@ -11,6 +11,8 @@ using ReactiveUI; using ReactiveUI.Fody.Helpers; using Wabbajack.Common; using Wabbajack.Lib; +using Wabbajack.UI; +using Wabbajack.Util; namespace Wabbajack { @@ -148,7 +150,8 @@ namespace Wabbajack archive: Parent.ModListLocation.TargetPath, modList: Parent.ModList.SourceModList, outputFolder: Location.TargetPath, - downloadFolder: DownloadLocation.TargetPath); + downloadFolder: DownloadLocation.TargetPath, + parameters: SystemParametersConstructor.Create()); await Task.Run(async () => { diff --git a/Wabbajack/View Models/Installers/VortexInstallerVM.cs b/Wabbajack/View Models/Installers/VortexInstallerVM.cs index efc91e02..6952ee86 100644 --- a/Wabbajack/View Models/Installers/VortexInstallerVM.cs +++ b/Wabbajack/View Models/Installers/VortexInstallerVM.cs @@ -8,6 +8,7 @@ using ReactiveUI; using ReactiveUI.Fody.Helpers; using Wabbajack.Common; using Wabbajack.Lib; +using Wabbajack.Util; namespace Wabbajack { @@ -66,7 +67,8 @@ namespace Wabbajack archive: Parent.ModListLocation.TargetPath, modList: Parent.ModList.SourceModList, outputFolder: staging, - downloadFolder: download); + downloadFolder: download, + parameters: SystemParametersConstructor.Create()); await Task.Run(async () => { diff --git a/Wabbajack/View Models/MainWindowVM.cs b/Wabbajack/View Models/MainWindowVM.cs index 360d50b9..ff55e8ae 100644 --- a/Wabbajack/View Models/MainWindowVM.cs +++ b/Wabbajack/View Models/MainWindowVM.cs @@ -130,16 +130,16 @@ namespace Wabbajack .Select(active => !SettingsPane.IsValueCreated || !object.ReferenceEquals(active, SettingsPane.Value)), execute: () => NavigateTo(SettingsPane.Value)); } + private static bool IsStartingFromModlist(out string modlistPath) { - string[] args = Environment.GetCommandLineArgs(); - if (args.Length != 3 || !args[1].Contains("-i")) + if (CLIArguments.InstallPath == null) { modlistPath = default; return false; } - modlistPath = args[2]; + modlistPath = CLIArguments.InstallPath; return true; } diff --git a/Wabbajack/View Models/ModListVM.cs b/Wabbajack/View Models/ModListVM.cs index 264771d6..17bb3076 100644 --- a/Wabbajack/View Models/ModListVM.cs +++ b/Wabbajack/View Models/ModListVM.cs @@ -8,6 +8,7 @@ using System.Reactive.Linq; using System.Windows.Media.Imaging; using Wabbajack.Common; using Wabbajack.Lib; +using Wabbajack.UI; namespace Wabbajack { diff --git a/Wabbajack/View Models/ModVM.cs b/Wabbajack/View Models/ModVM.cs index 9b72d593..9e2344b7 100644 --- a/Wabbajack/View Models/ModVM.cs +++ b/Wabbajack/View Models/ModVM.cs @@ -8,6 +8,7 @@ using Wabbajack.Common; using Wabbajack.Lib; using Wabbajack.Lib.Downloaders; using Wabbajack.Lib.NexusApi; +using Wabbajack.UI; namespace Wabbajack { diff --git a/Wabbajack/View Models/ModeSelectionVM.cs b/Wabbajack/View Models/ModeSelectionVM.cs index 33330252..cb8b3f8f 100644 --- a/Wabbajack/View Models/ModeSelectionVM.cs +++ b/Wabbajack/View Models/ModeSelectionVM.cs @@ -6,6 +6,7 @@ using System.Reactive.Linq; using System.Windows.Input; using Wabbajack.Common; using Wabbajack.Lib; +using Wabbajack.UI; namespace Wabbajack { diff --git a/Wabbajack/View Models/UserInterventionHandlers.cs b/Wabbajack/View Models/UserInterventionHandlers.cs index 3694bbb8..88c5e731 100644 --- a/Wabbajack/View Models/UserInterventionHandlers.cs +++ b/Wabbajack/View Models/UserInterventionHandlers.cs @@ -8,6 +8,7 @@ using System.Windows; using System.Windows.Threading; using ReactiveUI; using Wabbajack.Common; +using Wabbajack.Lib; using Wabbajack.Lib.Downloaders; using Wabbajack.Lib.NexusApi; using Wabbajack.Lib.WebAutomation; @@ -65,14 +66,27 @@ namespace Wabbajack c.Resume(key); }); break; - case RequestLoversLabLogin c: + case AbstractNeedsLoginDownloader.RequestSiteLogin c: await WrapBrowserJob(msg, async (vm, cancel) => { await vm.Driver.WaitForInitialized(); - var data = await LoversLabDownloader.GetAndCacheLoversLabCookies(new CefSharpWrapper(vm.Browser), m => vm.Instructions = m, cancel.Token); + var data = await c.Downloader.GetAndCacheCookies(new CefSharpWrapper(vm.Browser), m => vm.Instructions = m, cancel.Token); c.Resume(data); }); break; + case YesNoIntervention c: + var result = MessageBox.Show(c.ExtendedDescription, c.ShortDescription, MessageBoxButton.YesNo, + MessageBoxImage.Question); + if (result == MessageBoxResult.Yes) + c.Confirm(); + else + c.Cancel(); + break; + case CriticalFailureIntervention c: + MessageBox.Show(c.ExtendedDescription, c.ShortDescription, MessageBoxButton.OK, + MessageBoxImage.Error); + c.Cancel(); + break; case ConfirmationIntervention c: break; default: diff --git a/Wabbajack/Views/Common/FilePicker.xaml.cs b/Wabbajack/Views/Common/FilePicker.xaml.cs index 601f9203..d1a78e34 100644 --- a/Wabbajack/Views/Common/FilePicker.xaml.cs +++ b/Wabbajack/Views/Common/FilePicker.xaml.cs @@ -2,6 +2,7 @@ using System.Windows.Controls; using System.Windows.Data; using Wabbajack.Lib; +using Wabbajack.UI; namespace Wabbajack { diff --git a/Wabbajack/Views/MainWindow.xaml.cs b/Wabbajack/Views/MainWindow.xaml.cs index 5cf16005..5491cba0 100644 --- a/Wabbajack/Views/MainWindow.xaml.cs +++ b/Wabbajack/Views/MainWindow.xaml.cs @@ -48,9 +48,7 @@ namespace Wabbajack }).FireAndForget(); // Load settings - string[] args = Environment.GetCommandLineArgs(); - if ((args.Length > 1 && args[1] == "nosettings") - || !MainSettings.TryLoadTypicalSettings(out var settings)) + if (CLIArguments.NoSettings || !MainSettings.TryLoadTypicalSettings(out var settings)) { _settings = new MainSettings(); RunWhenLoaded(DefaultSettings); diff --git a/Wabbajack/Wabbajack.csproj b/Wabbajack/Wabbajack.csproj index 5e8640e3..87da201b 100644 --- a/Wabbajack/Wabbajack.csproj +++ b/Wabbajack/Wabbajack.csproj @@ -172,6 +172,9 @@ MSBuild:Compile Designer + + + @@ -555,7 +558,7 @@ 11.1.1 - 11.1.1 + 11.1.6 11.1.1