Merge remote-tracking branch 'wabbajack-tools/master' into settings-pane

This commit is contained in:
Justin Swanson 2020-01-07 19:23:32 -06:00
commit c5dd5c05bc
70 changed files with 1110 additions and 610 deletions

View File

@ -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

View File

@ -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;

View File

@ -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
{

104
Wabbajack.Common/CLI.cs Normal file
View File

@ -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
{
/// <summary>
/// Parses the argument and sets the properties of <see cref="CLIArguments"/>
/// </summary>
/// <param name="args"><see cref="Environment.GetCommandLineArgs"/></param>
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;
}
}
}

View File

@ -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<byte> acc = new List<byte>();
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;
}
}
}
}

View File

@ -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<string, string> ExtList = new Dictionary<string, string>
@ -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()

View File

@ -178,7 +178,7 @@ namespace Wabbajack.Common
MO2Name = "New Vegas",
MO2ArchiveName = "falloutnv",
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\falloutnv",
SteamIDs = new List<int> {22380},
SteamIDs = new List<int> {22380, 22490}, // normal and RU version
RequiredFiles = new List<string>
{
"FalloutNV.exe"

View File

@ -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<T>(this T obj, string filename, SerializerConfig config)
{
byte[] final;
final = ToCERAS(obj, config);
File.WriteAllBytes(filename, final);
}
public static byte[] ToCERAS<T>(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<T>(this Stream data, SerializerConfig config)
@ -1112,6 +1123,20 @@ namespace Wabbajack.Common
return path.ToLower().TrimEnd('\\').StartsWith(parent.ToLower().TrimEnd('\\') + "\\");
}
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> coll)
{
var hs = new HashSet<T>();
coll.Do(v => hs.Add(v));
return hs;
}
public static HashSet<T> ToHashSet<T>(this T[] coll)
{
var hs = new HashSet<T>();
coll.Do(v => hs.Add(v));
return hs;
}
public class NexusErrorResponse
{
public int code;

View File

@ -5,35 +5,35 @@
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
<ItemGroup>
<None Remove="7z.dll.gz" />
<None Remove="7z.exe.gz" />
<None Remove="innounp.exe.gz" />
<None Remove="7z.dll.gz" />
<None Remove="7z.exe.gz" />
<None Remove="innounp.exe.gz" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="7z.dll.gz" />
<EmbeddedResource Include="7z.exe.gz" />
<EmbeddedResource Include="innounp.exe.gz" />
<EmbeddedResource Include="7z.dll.gz" />
<EmbeddedResource Include="7z.exe.gz" />
<EmbeddedResource Include="innounp.exe.gz" />
</ItemGroup>
<ItemGroup>
<Folder Include="KnownFolders\" />
<Folder Include="Properties\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Ceras" Version="4.1.7" />
<PackageReference Include="Genbox.AlphaFS" Version="2.2.2.1" />
<PackageReference Include="ini-parser-netstandard" Version="2.5.2" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ReactiveUI" Version="11.1.1" />
<PackageReference Include="SharpZipLib" Version="1.2.0" />
<PackageReference Include="System.Data.HashFunction.xxHash" Version="2.0.0" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="4.7.0" />
<PackageReference Include="System.Security.Principal.Windows" Version="4.7.0" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="Ceras" Version="4.1.7" />
<PackageReference Include="Genbox.AlphaFS" Version="2.2.2.1" />
<PackageReference Include="ini-parser-netstandard" Version="2.5.2" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ReactiveUI" Version="11.1.1" />
<PackageReference Include="SharpZipLib" Version="1.2.0" />
<PackageReference Include="System.Data.HashFunction.xxHash" Version="2.0.0" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="4.7.0" />
<PackageReference Include="System.Security.Principal.Windows" Version="4.7.0" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Compression.BSA\Compression.BSA.csproj" />
<ProjectReference Include="..\OMODExtractor\OMODExtractor.csproj" />
<ProjectReference Include="..\Wabbajack.Common.CSP\Wabbajack.Common.CSP.csproj" />
<ProjectReference Include="..\Compression.BSA\Compression.BSA.csproj" />
<ProjectReference Include="..\OMODExtractor\OMODExtractor.csproj" />
<ProjectReference Include="..\Wabbajack.Common.CSP\Wabbajack.Common.CSP.csproj" />
</ItemGroup>
</Project>

View File

@ -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<string, string> 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<FromArchive>()

View File

@ -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)
},
};

View File

@ -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")))

View File

@ -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<Directive> Run(RawSourceFile source)
{
if (_gameFolderFilesExists)
{
if (source.AbsolutePath.IsInPath(_gameFolder))
{
var result = source.EvolveTo<IgnoredDirectly>();
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);
}
}
}
}

View File

@ -22,8 +22,14 @@ namespace Wabbajack.Lib.CompilationSteps
public override async ValueTask<Directive> 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;

View File

@ -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
/// </summary>
public bool ReadmeIsWebsite;
public ModList Clone()
{
return new MemoryStream(this.ToCERAS(CerasConfig.Config)).FromCERAS<ModList>(CerasConfig.Config);
}
}
public class Directive

View File

@ -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<string, Type> NameToType { get; set; }
public static Dictionary<Type, string> TypeToName { get; set; }

View File

@ -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<TDownloader, TState> : AbstractNeedsLoginDownloader, IDownloader
where TState : AbstractIPS4Downloader<TDownloader, TState>.State<TDownloader>, new()
where TDownloader : IDownloader
{
public override string SiteName { get; }
public override Uri SiteURL { get; }
public async Task<AbstractDownloadState> 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<TDownloader> : 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<Stream> ResolveDownloadStream()
{
var downloader = (AbstractNeedsLoginDownloader)(object)DownloadDispatcher.GetInstance<TDownloader>();
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<Match>();
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<WaitResponse>();
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<bool> Verify()
{
var stream = await ResolveDownloadStream();
if (stream == null)
{
return false;
}
stream.Close();
return true;
}
public override IDownloader GetDownloader()
{
return DownloadDispatcher.GetInstance<TDownloader>();
}
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")
{
}
}
}

View File

@ -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;
/// <summary>
/// Sets up all the login facilites needed for a INeedsLogin downloader based on having the user log
/// in via a browser
/// </summary>
/// <param name="loginUri">The URI to preset for logging in</param>
/// <param name="encryptedKeyName">The name of the encrypted JSON key in which to store cookies</param>
/// <param name="cookieDomain">The cookie domain to scan</param>
/// <param name="cookieName">The cookie name to wait for</param>
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<bool> 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<Helpers.Cookie[]> GetAndCacheCookies(IWebDriver browser, Action<string> 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<HttpClient> GetAuthedClient()
{
Helpers.Cookie[] cookies;
try
{
cookies = Utils.FromEncryptedJson<Helpers.Cookie[]>(_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<Helpers.Cookie[]> _source = new TaskCompletionSource<Helpers.Cookie[]>();
public Task<Helpers.Cookie[]> Task => _source.Task;
public void Resume(Helpers.Cookie[] cookies)
{
Handled = true;
_source.SetResult(cookies);
}
public override void Cancel()
{
Handled = true;
_source.TrySetCanceled();
}
}
}
}

View File

@ -0,0 +1,24 @@
using System;
namespace Wabbajack.Lib.Downloaders
{
public class DeadlyStreamDownloader : AbstractIPS4Downloader<DeadlyStreamDownloader, DeadlyStreamDownloader.State>
{
#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<DeadlyStreamDownloader>
{
}
}
}

View File

@ -18,6 +18,8 @@ namespace Wabbajack.Lib.Downloaders
new NexusDownloader(),
new MediaFireDownloader(),
new LoversLabDownloader(),
new VectorPlexusDownloader(),
new DeadlyStreamDownloader(),
new HTTPDownloader(),
new ManualDownloader(),
};

View File

@ -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

View File

@ -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;

View File

@ -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<LoversLabDownloader, LoversLabDownloader.State>
{
internal HttpClient _authedClient;
#region INeedsDownload
public ReactiveCommand<Unit, Unit> TriggerLogin { get; }
public ReactiveCommand<Unit, Unit> ClearLogin { get; }
public IObservable<bool> IsLoggedIn => Utils.HaveEncryptedJsonObservable("loverslabcookies");
public string SiteName => "Lovers Lab";
public IObservable<string> 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<AbstractDownloadState> 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<Helpers.Cookie[]> GetAndCacheLoversLabCookies(IWebDriver browser, Action<string> updateStatus, CancellationToken cancel)
{
updateStatus("Please Log Into Lovers Lab");
await browser.NavigateTo(new Uri("https://www.loverslab.com/login"));
async Task<bool> 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<HttpClient> GetAuthedClient()
{
Helpers.Cookie[] cookies;
try
{
cookies = Utils.FromEncryptedJson<Helpers.Cookie[]>("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<Stream> ResolveDownloadStream()
{
var result = DownloadDispatcher.GetInstance<LoversLabDownloader>();
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<Match>().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<WaitResponse>();
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<bool> Verify()
{
var stream = await ResolveDownloadStream();
if (stream == null)
{
return false;
}
stream.Close();
return true;
}
public override IDownloader GetDownloader()
{
return DownloadDispatcher.GetInstance<LoversLabDownloader>();
}
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<Helpers.Cookie[]> _source = new TaskCompletionSource<Helpers.Cookie[]>();
public Task<Helpers.Cookie[]> Task => _source.Task;
public void Resume(Helpers.Cookie[] cookies)
public class State : State<LoversLabDownloader>
{
Handled = true;
_source.SetResult(cookies);
}
public override void Cancel()
{
Handled = true;
_source.TrySetCanceled();
}
}
}

View File

@ -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;

View File

@ -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));

View File

@ -0,0 +1,22 @@
using System;
using System.Threading.Tasks;
namespace Wabbajack.Lib.Downloaders
{
public class VectorPlexusDownloader : AbstractIPS4Downloader<VectorPlexusDownloader, VectorPlexusDownloader.State>
{
#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<VectorPlexusDownloader>
{
}
}
}

View File

@ -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;
}
}
}

View File

@ -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<RawSourceFile> 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<RawSourceFile>();
}
ModMetas = Directory.EnumerateDirectories(Path.Combine(MO2Folder, "mods"))
.Keep(f =>
{
@ -496,6 +507,7 @@ namespace Wabbajack.Lib
Utils.Log("Generating compilation stack");
return new List<ICompilationStep>
{
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"),

View File

@ -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<NexusDownloader.State>()
.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<CreateBSA>().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);

View File

@ -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; }

View File

@ -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

View File

@ -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")]

View File

@ -129,7 +129,7 @@ namespace Wabbajack.Lib
.Concat(lst.Directives
.OfType<PatchedFromArchive>()
.Select(f => (f.To, "patched", SizeForID(f.PatchID))))
.ToHashSet()
.Distinct()
.OrderByDescending(f => f.Item3);
NoWrapText("\n\n### Summary of inlined files in this installer");

View File

@ -0,0 +1,27 @@
using System.Threading.Tasks;
using Wabbajack.Common;
namespace Wabbajack.Lib
{
/// <summary>
/// This should probably be replaced with an error, but this is just to get messageboxes out of the .Lib library
/// </summary>
public class CriticalFailureIntervention : AUserIntervention
{
private TaskCompletionSource<ConfirmationIntervention.Choice> _source = new TaskCompletionSource<ConfirmationIntervention.Choice>();
public Task<ConfirmationIntervention.Choice> 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);
}
}
}

View File

@ -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; }
}
}

View File

@ -0,0 +1,8 @@
namespace Wabbajack.Lib
{
public class SystemParameters
{
public int ScreenHeight { get; set; }
public int ScreenWidth { get; set; }
}
}

View File

@ -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;

View File

@ -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<SteamMeta>()

View File

@ -1,248 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{0A820830-A298-497D-85E0-E9A89EFEF5FE}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Wabbajack.Lib</RootNamespace>
<AssemblyName>Wabbajack.Lib</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>x64</PlatformTarget>
<NoWarn>CS1998</NoWarn>
<WarningsAsErrors>CS4014</WarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<NoWarn>CS1998</NoWarn>
<WarningsAsErrors>CS4014</WarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<WarningsAsErrors>CS4014</WarningsAsErrors>
<NoWarn>CS1998</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<WarningsAsErrors>CS4014</WarningsAsErrors>
<NoWarn>CS1998</NoWarn>
</PropertyGroup>
<ItemGroup>
<Reference Include="MongoDB.Bson">
<HintPath>..\..\..\Users\tbald\.nuget\packages\mongodb.bson\2.10.0\lib\net452\MongoDB.Bson.dll</HintPath>
</Reference>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Design" />
<Reference Include="System.Drawing" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.Net" />
<Reference Include="System.Security" />
<Reference Include="System.Transactions" />
<Reference Include="System.Web" />
<Reference Include="System.Windows" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xaml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="ABatchProcessor.cs" />
<Compile Include="ACompiler.cs" />
<Compile Include="AInstaller.cs" />
<Compile Include="CerasConfig.cs" />
<Compile Include="CompilationSteps\ACompilationStep.cs" />
<Compile Include="CompilationSteps\CompilationErrors\InvalidGameESMError.cs" />
<Compile Include="CompilationSteps\DeconstructBSAs.cs" />
<Compile Include="CompilationSteps\DirectMatch.cs" />
<Compile Include="CompilationSteps\DropAll.cs" />
<Compile Include="CompilationSteps\IgnoreDisabledMods.cs" />
<Compile Include="CompilationSteps\IgnoreDisabledVortexMods.cs" />
<Compile Include="CompilationSteps\IgnoreEndsWith.cs" />
<Compile Include="CompilationSteps\IgnoreGameFiles.cs" />
<Compile Include="CompilationSteps\IgnorePathContains.cs" />
<Compile Include="CompilationSteps\IgnoreRegex.cs" />
<Compile Include="CompilationSteps\IgnoreStartsWith.cs" />
<Compile Include="CompilationSteps\IgnoreVortex.cs" />
<Compile Include="CompilationSteps\IgnoreWabbajackInstallCruft.cs" />
<Compile Include="CompilationSteps\IncludeAll.cs" />
<Compile Include="CompilationSteps\IncludeAllConfigs.cs" />
<Compile Include="CompilationSteps\IncludeDummyESPs.cs" />
<Compile Include="CompilationSteps\IncludeLootFiles.cs" />
<Compile Include="CompilationSteps\IncludeModIniData.cs" />
<Compile Include="CompilationSteps\IncludeOtherProfiles.cs" />
<Compile Include="CompilationSteps\IncludePatches.cs" />
<Compile Include="CompilationSteps\IncludePropertyFiles.cs" />
<Compile Include="CompilationSteps\IncludeRegex.cs" />
<Compile Include="CompilationSteps\IncludeSteamWorkshopItems.cs" />
<Compile Include="CompilationSteps\IncludeStubbedConfigFiles.cs" />
<Compile Include="CompilationSteps\IncludeTaggedMods.cs" />
<Compile Include="CompilationSteps\IncludeThisProfile.cs" />
<Compile Include="CompilationSteps\IncludeVortexDeployment.cs" />
<Compile Include="CompilationSteps\IStackStep.cs" />
<Compile Include="CompilationSteps\PatchStockESMs.cs" />
<Compile Include="CompilationSteps\Serialization.cs" />
<Compile Include="Downloaders\GameFileSourceDownloader.cs" />
<Compile Include="Downloaders\INeedsLogin.cs" />
<Compile Include="Downloaders\LoversLabDownloader.cs" />
<Compile Include="Downloaders\SteamWorkshopDownloader.cs" />
<Compile Include="Extensions\ReactiveUIExt.cs" />
<Compile Include="LibCefHelpers\Init.cs" />
<Compile Include="MO2Compiler.cs" />
<Compile Include="Data.cs" />
<Compile Include="Downloaders\AbstractDownloadState.cs" />
<Compile Include="Downloaders\DownloadDispatcher.cs" />
<Compile Include="Downloaders\DownloaderUtils.cs" />
<Compile Include="Downloaders\DropboxDownloader.cs" />
<Compile Include="Downloaders\GoogleDriveDownloader.cs" />
<Compile Include="Downloaders\HTTPDownloader.cs" />
<Compile Include="Downloaders\IDownloader.cs" />
<Compile Include="Downloaders\IUrlDownloader.cs" />
<Compile Include="Downloaders\ManualDownloader.cs" />
<Compile Include="Downloaders\MediaFireDownloader.cs" />
<Compile Include="Downloaders\MEGADownloader.cs" />
<Compile Include="Downloaders\ModDBDownloader.cs" />
<Compile Include="Downloaders\NexusDownloader.cs" />
<Compile Include="IBatchProcessor.cs" />
<Compile Include="MO2Installer.cs" />
<Compile Include="ModListRegistry\ModListMetadata.cs" />
<Compile Include="NexusApi\Dtos.cs" />
<Compile Include="NexusApi\NexusApi.cs" />
<Compile Include="NexusApi\NexusApiUtils.cs" />
<Compile Include="NexusApi\RequestNexusAuthorization.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ReportBuilder.cs" />
<Compile Include="StatusMessages\ConfirmUpdateOfExistingInstall.cs" />
<Compile Include="UI\FilePickerVM.cs" />
<Compile Include="UI\UIUtils.cs" />
<Compile Include="Validation\DTOs.cs" />
<Compile Include="Validation\ValidateModlist.cs" />
<Compile Include="ViewModel.cs" />
<Compile Include="VortexCompiler.cs" />
<Compile Include="VortexInstaller.cs" />
<Compile Include="WebAutomation\CefSharpWrapper.cs" />
<Compile Include="WebAutomation\IWebDriver.cs" />
<Compile Include="WebAutomation\WebAutomation.cs" />
<Compile Include="zEditIntegration.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<EmbeddedResource Include="LibCefHelpers\cefsharp.7z" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)\Compression.BSA\Compression.BSA.csproj">
<Project>{ff5d892f-8ff4-44fc-8f7f-cd58f307ad1b}</Project>
<Name>Compression.BSA</Name>
</ProjectReference>
<ProjectReference Include="$(SolutionDir)\Wabbajack.Common\Wabbajack.Common.csproj">
<Project>{b3f3fb6e-b9eb-4f49-9875-d78578bc7ae5}</Project>
<Name>Wabbajack.Common</Name>
</ProjectReference>
<ProjectReference Include="$(SolutionDir)\Wabbajack.VirtualFileSystem\Wabbajack.VirtualFileSystem.csproj">
<Project>{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}</Project>
<Name>Wabbajack.VirtualFileSystem</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="css-min.css" />
</ItemGroup>
<ItemGroup>
<None Include="css.css" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AlphaFS">
<Version>2.2.6</Version>
</PackageReference>
<PackageReference Include="CefSharp.OffScreen">
<Version>75.1.143</Version>
</PackageReference>
<PackageReference Include="Ceras">
<Version>4.1.7</Version>
</PackageReference>
<PackageReference Include="CommonMark.NET">
<Version>0.15.1</Version>
</PackageReference>
<PackageReference Include="HtmlAgilityPack">
<Version>1.11.17</Version>
</PackageReference>
<PackageReference Include="MegaApiClient">
<Version>1.7.1</Version>
</PackageReference>
<PackageReference Include="Microsoft-WindowsAPICodePack-Shell">
<Version>1.1.3.3</Version>
</PackageReference>
<PackageReference Include="Microsoft.Toolkit.Wpf.UI.Controls.WebView">
<Version>6.0.0</Version>
</PackageReference>
<PackageReference Include="ModuleInit.Fody">
<Version>2.1.0</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>12.0.3</Version>
</PackageReference>
<PackageReference Include="ReactiveUI">
<Version>11.1.1</Version>
</PackageReference>
<PackageReference Include="ReactiveUI.Fody">
<Version>11.1.1</Version>
</PackageReference>
<PackageReference Include="SharpCompress">
<Version>0.24.0</Version>
</PackageReference>
<PackageReference Include="Syroot.Windows.IO.KnownFolders">
<Version>1.2.1</Version>
</PackageReference>
<PackageReference Include="System.Reactive">
<Version>4.3.2</Version>
</PackageReference>
<PackageReference Include="WebSocketSharpFork">
<Version>1.0.4</Version>
</PackageReference>
<PackageReference Include="YamlDotNet">
<Version>8.0.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CefSharp.Common">
<Version>75.1.143</Version>
</PackageReference>
<PackageReference Include="CefSharp.OffScreen">
<Version>75.1.143</Version>
</PackageReference>
<PackageReference Include="Ceras">
<Version>4.1.7</Version>
</PackageReference>
<PackageReference Include="CommonMark.NET">
<Version>0.15.1</Version>
</PackageReference>
<PackageReference Include="Fody">
<Version>6.0.6</Version>
</PackageReference>
<PackageReference Include="Genbox.AlphaFS">
<Version>2.2.2.1</Version>
</PackageReference>
<PackageReference Include="HtmlAgilityPack">
<Version>1.11.17</Version>
</PackageReference>
<PackageReference Include="MegaApiClient">
<Version>1.7.1</Version>
</PackageReference>
<PackageReference Include="Microsoft.CSharp">
<Version>4.7.0</Version>
</PackageReference>
<PackageReference Include="ModuleInit.Fody">
<Version>2.1.0</Version>
</PackageReference>
<PackageReference Include="ReactiveUI">
<Version>11.1.6</Version>
</PackageReference>
<PackageReference Include="ReactiveUI.Fody">
<Version>11.1.6</Version>
</PackageReference>
<PackageReference Include="SharpCompress">
<Version>0.24.0</Version>
</PackageReference>
<PackageReference Include="System.Collections.Immutable">
<Version>1.7.0</Version>
</PackageReference>
<PackageReference Include="System.Drawing.Common">
<Version>4.7.0</Version>
</PackageReference>
<PackageReference Include="System.Net.Http">
<Version>4.3.4</Version>
</PackageReference>
<PackageReference Include="WebSocketSharp-netstandard">
<Version>1.0.1</Version>
</PackageReference>
<PackageReference Include="YamlDotNet.NetCore">
<Version>1.0.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Compression.BSA\Compression.BSA.csproj" />
<ProjectReference Include="..\Wabbajack.Common\Wabbajack.Common.csproj" />
<ProjectReference Include="..\Wabbajack.VirtualFileSystem\Wabbajack.VirtualFileSystem.csproj" />
</ItemGroup>
<ItemGroup>
<None Remove="LibCefHelpers\cefsharp.7z" />
<EmbeddedResource Include="LibCefHelpers\cefsharp.7z" />
<None Remove="css-min.css" />
<EmbeddedResource Include="css-min.css" />
</ItemGroup>
</Project>

View File

@ -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;

View File

@ -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();

View File

@ -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,
};

View File

@ -310,6 +310,34 @@ namespace Wabbajack.Test
Assert.AreEqual(File.ReadAllText(filename), "Cheese for Everyone!");
}
[TestMethod]
public async Task VectorPlexusDownload()
{
await DownloadDispatcher.GetInstance<VectorPlexusDownloader>().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<string>() }));
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()

View File

@ -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();
}

View File

@ -9,6 +9,7 @@ using DynamicData;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.UI;
namespace Wabbajack.Test
{

View File

@ -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);
}
}
}

View File

@ -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<string, byte[]> {{"/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<string, byte[]> {{"/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));
}

View File

@ -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<string> {"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<string> {".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;
}
}
}

View File

@ -127,6 +127,7 @@
<Compile Include="EndToEndTests.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="FilePickerTests.cs" />
<Compile Include="IniTests.cs" />
<Compile Include="MiscTests.cs" />
<Compile Include="MO2Tests.cs" />
<Compile Include="ModlistMetadataTests.cs" />

View File

@ -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();

View File

@ -14,7 +14,9 @@ namespace Wabbajack
{
public App()
{
// Do initialization in MainWindow ctor
CLI.ParseOptions(Environment.GetCommandLineArgs());
if(CLIArguments.Help)
CLI.DisplayHelpText();
}
}
}

View File

@ -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")]

View File

@ -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
{

View File

@ -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
{

View File

@ -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
};
}
}
}

View File

@ -15,6 +15,7 @@ using System.Windows.Media.Imaging;
using Wabbajack.Common;
using Wabbajack.Common.StatusFeed;
using Wabbajack.Lib;
using Wabbajack.UI;
namespace Wabbajack
{

View File

@ -9,6 +9,7 @@ using System.Reactive.Linq;
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.UI;
namespace Wabbajack
{

View File

@ -6,6 +6,7 @@ using Microsoft.WindowsAPICodePack.Dialogs;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Lib;
using Wabbajack.UI;
namespace Wabbajack
{

View File

@ -11,6 +11,7 @@ using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack.Common.StoreHandlers;
using Wabbajack.Lib;
using Wabbajack.UI;
namespace Wabbajack
{

View File

@ -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);
}

View File

@ -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 () =>
{

View File

@ -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 () =>
{

View File

@ -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;
}

View File

@ -8,6 +8,7 @@ using System.Reactive.Linq;
using System.Windows.Media.Imaging;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.UI;
namespace Wabbajack
{

View File

@ -8,6 +8,7 @@ using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.Downloaders;
using Wabbajack.Lib.NexusApi;
using Wabbajack.UI;
namespace Wabbajack
{

View File

@ -6,6 +6,7 @@ using System.Reactive.Linq;
using System.Windows.Input;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.UI;
namespace Wabbajack
{

View File

@ -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:

View File

@ -2,6 +2,7 @@
using System.Windows.Controls;
using System.Windows.Data;
using Wabbajack.Lib;
using Wabbajack.UI;
namespace Wabbajack
{

View File

@ -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);

View File

@ -172,6 +172,9 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="UI\FilePickerVM.cs" />
<Compile Include="UI\UIUtils.cs" />
<Compile Include="Util\SystemParametersConstructor.cs" />
<Compile Include="Converters\IntDownCastConverter.cs" />
<Compile Include="Converters\CommandConverter.cs" />
<Compile Include="Converters\ConverterRegistration.cs" />
@ -555,7 +558,7 @@
<Version>11.1.1</Version>
</PackageReference>
<PackageReference Include="ReactiveUI.Fody">
<Version>11.1.1</Version>
<Version>11.1.6</Version>
</PackageReference>
<PackageReference Include="ReactiveUI.WPF">
<Version>11.1.1</Version>