Merge branch 'master' into async-exploration

This commit is contained in:
Justin Swanson 2019-12-12 18:40:21 -06:00
commit bcd7c95caf
70 changed files with 1626 additions and 322 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -71,7 +71,7 @@ namespace Compression.BSA.Test
var state = new NexusDownloader.State var state = new NexusDownloader.State
{ {
ModID = info.Item2.ToString(), ModID = info.Item2.ToString(),
GameName = GameRegistry.Games[info.Item1].NexusName, GameName = info.Item1.MetaData().NexusName,
FileID = file.file_id.ToString() FileID = file.file_id.ToString()
}; };
await state.Download(src); await state.Download(src);

View File

@ -96,7 +96,7 @@
<Version>2.0.0</Version> <Version>2.0.0</Version>
</PackageReference> </PackageReference>
<PackageReference Include="System.Reactive"> <PackageReference Include="System.Reactive">
<Version>4.2.0</Version> <Version>4.3.1</Version>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" /> <Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />

View File

@ -60,10 +60,6 @@
<Prefer32Bit>true</Prefer32Bit> <Prefer32Bit>true</Prefer32Bit>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="ReactiveUI, Version=10.5.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\Users\tbald\.nuget\packages\reactiveui\10.5.7\lib\net461\ReactiveUI.dll</HintPath>
</Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Windows" /> <Reference Include="System.Windows" />
@ -103,8 +99,11 @@
<PackageReference Include="Newtonsoft.Json"> <PackageReference Include="Newtonsoft.Json">
<Version>12.0.3</Version> <Version>12.0.3</Version>
</PackageReference> </PackageReference>
<PackageReference Include="ReactiveUI">
<Version>11.0.1</Version>
</PackageReference>
<PackageReference Include="System.Reactive"> <PackageReference Include="System.Reactive">
<Version>4.2.0</Version> <Version>4.3.1</Version>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

View File

@ -103,10 +103,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Reactive"> <PackageReference Include="System.Reactive">
<Version>4.2.0</Version> <Version>4.3.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="System.Runtime.CompilerServices.Unsafe"> <PackageReference Include="System.Runtime.CompilerServices.Unsafe">
<Version>4.6.0</Version> <Version>4.7.0</Version>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

View File

@ -677,4 +677,4 @@ namespace Wabbajack.Common
return a < b ? b < c ? b : a < c ? c : a : b > c ? b : a > c ? c : a; return a < b ? b < c ? b : a < c ? c : a : b > c ? b : a > c ? c : a;
} }
} }
} }

View File

@ -2,6 +2,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Alphaleonis.Win32.Filesystem;
using Syroot.Windows.IO;
namespace Wabbajack.Common namespace Wabbajack.Common
{ {
@ -84,6 +86,7 @@ namespace Wabbajack.Common
} }
public static string HashFileExtension => ".xxHash"; public static string HashFileExtension => ".xxHash";
public static string LocalAppDataPath => Path.Combine(KnownFolders.LocalAppData.Path, "Wabbajack");
public static string WabbajackCacheLocation = "http://build.wabbajack.org/nexus_api_cache/"; public static string WabbajackCacheLocation = "http://build.wabbajack.org/nexus_api_cache/";
} }

View File

@ -67,12 +67,13 @@ namespace Wabbajack.Common
public List<string> RequiredFiles { get; internal set; } public List<string> RequiredFiles { get; internal set; }
public bool Disabled { get; internal set; } public bool Disabled { get; internal set; }
public string GameLocation(bool steam) public string GameLocation()
{ {
if (Consts.TestMode) if (Consts.TestMode)
return Directory.GetCurrentDirectory(); return Directory.GetCurrentDirectory();
return steam ? SteamHandler.Instance.Games.FirstOrDefault(g => g.Game == Game)?.InstallDir : GOGHandler.Instance.Games.FirstOrDefault(g => g.Game == Game)?.Path; return SteamHandler.Instance.Games.FirstOrDefault(g => g.Game == Game)?.InstallDir ??
GOGHandler.Instance.Games.FirstOrDefault(g => g.Game == Game)?.Path;
} }
} }

View File

@ -1,20 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wabbajack.Common.StatusFeed
{
/// <summary>
/// Defines a message that requires user interaction. The user must perform some action
/// or make a choice.
/// </summary>
public interface IUserIntervention : IStatusMessage
{
/// <summary>
/// The user didn't make a choice, so this action should be aborted
/// </summary>
void Cancel();
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using ReactiveUI;
namespace Wabbajack.Common
{
public abstract class AUserIntervention : ReactiveObject, IUserIntervention
{
public DateTime Timestamp { get; } = DateTime.Now;
public abstract string ShortDescription { get; }
public abstract string ExtendedDescription { get; }
private bool _handled;
public bool Handled { get => _handled; set => this.RaiseAndSetIfChanged(ref _handled, value); }
public int CpuID { get; } = WorkQueue.AsyncLocalCurrentQueue.Value?.CpuId ?? WorkQueue.UnassignedCpuId;
public abstract void Cancel();
public ICommand CancelCommand { get; }
public AUserIntervention()
{
CancelCommand = ReactiveCommand.Create(() => Cancel());
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using ReactiveUI;
namespace Wabbajack.Common
{
public abstract class ConfirmationIntervention : AUserIntervention
{
public enum Choice
{
Continue,
Abort
}
private TaskCompletionSource<Choice> _source = new TaskCompletionSource<Choice>();
public Task<Choice> Task => _source.Task;
public ICommand ConfirmCommand { get; }
public ConfirmationIntervention()
{
ConfirmCommand = ReactiveCommand.Create(() => Confirm());
}
public override void Cancel()
{
Handled = true;
_source.SetResult(Choice.Abort);
}
public void Confirm()
{
Handled = true;
_source.SetResult(Choice.Continue);
}
}
}

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ReactiveUI;
using Wabbajack.Common.StatusFeed;
namespace Wabbajack.Common
{
/// <summary>
/// Defines a message that requires user interaction. The user must perform some action
/// or make a choice.
/// </summary>
public interface IUserIntervention : IStatusMessage, IReactiveObject
{
/// <summary>
/// The user didn't make a choice, so this action should be aborted
/// </summary>
void Cancel();
/// <summary>
/// Whether the interaction has been handled and no longer needs attention
/// Note: This needs to be Reactive so that users can monitor its status
/// </summary>
bool Handled { get; }
/// <summary>
/// WorkQueue job ID that is blocking on this intervention
/// </summary>
int CpuID { get; }
}
}

View File

@ -95,6 +95,9 @@ namespace Wabbajack.Common
paths.Add(s); paths.Add(s);
}); });
// Default path in the Steam folder isn't in the configs
paths.Add(Path.Combine(SteamPath, "steamapps"));
InstallFolders = paths; InstallFolders = paths;
} }

View File

@ -688,6 +688,7 @@ namespace Wabbajack.Common
using (var f = File.OpenWrite(tmpName)) using (var f = File.OpenWrite(tmpName))
{ {
Status("Creating Patch");
BSDiff.Create(a, b, f); BSDiff.Create(a, b, f);
} }
@ -936,7 +937,11 @@ namespace Wabbajack.Common
{ {
var bytes = Encoding.UTF8.GetBytes(data.ToJSON()); var bytes = Encoding.UTF8.GetBytes(data.ToJSON());
var encoded = ProtectedData.Protect(bytes, Encoding.UTF8.GetBytes(key), DataProtectionScope.LocalMachine); var encoded = ProtectedData.Protect(bytes, Encoding.UTF8.GetBytes(key), DataProtectionScope.LocalMachine);
var path = Path.Combine(KnownFolders.LocalAppData.Path, "Wabbajack", key);
if (!Directory.Exists(Consts.LocalAppDataPath))
Directory.CreateDirectory(Consts.LocalAppDataPath);
var path = Path.Combine(Consts.LocalAppDataPath, key);
File.WriteAllBytes(path, encoded); File.WriteAllBytes(path, encoded);
} }
@ -947,5 +952,12 @@ namespace Wabbajack.Common
var decoded = ProtectedData.Unprotect(bytes, Encoding.UTF8.GetBytes(key), DataProtectionScope.LocalMachine); var decoded = ProtectedData.Unprotect(bytes, Encoding.UTF8.GetBytes(key), DataProtectionScope.LocalMachine);
return Encoding.UTF8.GetString(decoded).FromJSONString<T>(); return Encoding.UTF8.GetString(decoded).FromJSONString<T>();
} }
public static bool IsInPath(this string path, string parent)
{
return path.ToLower().TrimEnd('\\').StartsWith(parent.ToLower().TrimEnd('\\') + "\\");
}
} }
} }

View File

@ -114,6 +114,7 @@
<Compile Include="SplittingStream.cs" /> <Compile Include="SplittingStream.cs" />
<Compile Include="StatusFeed\AErrorMessage.cs" /> <Compile Include="StatusFeed\AErrorMessage.cs" />
<Compile Include="StatusFeed\AStatusMessage.cs" /> <Compile Include="StatusFeed\AStatusMessage.cs" />
<Compile Include="StatusFeed\Interventions\AUserIntervention.cs" />
<Compile Include="StatusFeed\Errors\7zipReturnError.cs" /> <Compile Include="StatusFeed\Errors\7zipReturnError.cs" />
<Compile Include="StatusFeed\Errors\FileExtractionError.cs" /> <Compile Include="StatusFeed\Errors\FileExtractionError.cs" />
<Compile Include="StatusFeed\Errors\GenericException.cs" /> <Compile Include="StatusFeed\Errors\GenericException.cs" />
@ -122,8 +123,9 @@
<Compile Include="StatusFeed\IError.cs" /> <Compile Include="StatusFeed\IError.cs" />
<Compile Include="StatusFeed\IException.cs" /> <Compile Include="StatusFeed\IException.cs" />
<Compile Include="StatusFeed\IInfo.cs" /> <Compile Include="StatusFeed\IInfo.cs" />
<Compile Include="StatusFeed\Interventions\ConfirmationIntervention.cs" />
<Compile Include="StatusFeed\IStatusMessage.cs" /> <Compile Include="StatusFeed\IStatusMessage.cs" />
<Compile Include="StatusFeed\IUserIntervention.cs" /> <Compile Include="StatusFeed\Interventions\IUserIntervention.cs" />
<Compile Include="StatusFileStream.cs" /> <Compile Include="StatusFileStream.cs" />
<Compile Include="StatusUpdate.cs" /> <Compile Include="StatusUpdate.cs" />
<Compile Include="SteamHandler.cs" /> <Compile Include="SteamHandler.cs" />
@ -171,7 +173,7 @@
<Version>1.0.2</Version> <Version>1.0.2</Version>
</PackageReference> </PackageReference>
<PackageReference Include="ReactiveUI"> <PackageReference Include="ReactiveUI">
<Version>10.5.30</Version> <Version>11.0.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Syroot.Windows.IO.KnownFolders"> <PackageReference Include="Syroot.Windows.IO.KnownFolders">
<Version>1.2.1</Version> <Version>1.2.1</Version>
@ -180,7 +182,7 @@
<Version>2.0.0</Version> <Version>2.0.0</Version>
</PackageReference> </PackageReference>
<PackageReference Include="System.Reactive"> <PackageReference Include="System.Reactive">
<Version>4.2.0</Version> <Version>4.3.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="YamlDotNet"> <PackageReference Include="YamlDotNet">
<Version>8.0.0</Version> <Version>8.0.0</Version>

View File

@ -15,7 +15,10 @@ namespace Wabbajack.Common
internal BlockingCollection<Func<Task>> internal BlockingCollection<Func<Task>>
Queue = new BlockingCollection<Func<Task>>(new ConcurrentStack<Func<Task>>()); Queue = new BlockingCollection<Func<Task>>(new ConcurrentStack<Func<Task>>());
private static readonly AsyncLocal<int> CpuId = new AsyncLocal<int>(); public const int UnassignedCpuId = 0;
private static readonly AsyncLocal<int> _cpuId = new AsyncLocal<int>();
public int CpuId => _cpuId.Value;
internal static bool WorkerThread => ThreadLocalCurrentQueue.Value != null; internal static bool WorkerThread => ThreadLocalCurrentQueue.Value != null;
internal static readonly ThreadLocal<WorkQueue> ThreadLocalCurrentQueue = new ThreadLocal<WorkQueue>(); internal static readonly ThreadLocal<WorkQueue> ThreadLocalCurrentQueue = new ThreadLocal<WorkQueue>();
@ -28,6 +31,11 @@ namespace Wabbajack.Common
private CancellationTokenSource _cancel = new CancellationTokenSource(); private CancellationTokenSource _cancel = new CancellationTokenSource();
// This is currently a lie, as it wires to the Utils singleton stream This is still good to have,
// so that logic related to a single WorkQueue can subscribe to this dummy member so that If/when we
// implement log messages in a non-singleton fashion, they will already be wired up properly.
public IObservable<IStatusMessage> LogMessages => Utils.LogMessages;
public WorkQueue(int threadCount = 0) public WorkQueue(int threadCount = 0)
{ {
StartThreads(threadCount == 0 ? Environment.ProcessorCount : threadCount); StartThreads(threadCount == 0 ? Environment.ProcessorCount : threadCount);
@ -36,7 +44,7 @@ namespace Wabbajack.Common
private void StartThreads(int threadCount) private void StartThreads(int threadCount)
{ {
ThreadCount = threadCount; ThreadCount = threadCount;
Threads = Enumerable.Range(0, threadCount) Threads = Enumerable.Range(1, threadCount)
.Select(idx => .Select(idx =>
{ {
var thread = new Thread(() => ThreadBody(idx).Wait()); var thread = new Thread(() => ThreadBody(idx).Wait());
@ -52,7 +60,7 @@ namespace Wabbajack.Common
private async Task ThreadBody(int idx) private async Task ThreadBody(int idx)
{ {
CpuId.Value = idx; _cpuId.Value = idx;
ThreadLocalCurrentQueue.Value = this; ThreadLocalCurrentQueue.Value = this;
AsyncLocalCurrentQueue.Value = this; AsyncLocalCurrentQueue.Value = this;
@ -79,7 +87,7 @@ namespace Wabbajack.Common
Progress = progress, Progress = progress,
ProgressPercent = progress / 100f, ProgressPercent = progress / 100f,
Msg = msg, Msg = msg,
ID = CpuId.Value, ID = _cpuId.Value,
IsWorking = isWorking IsWorking = isWorking
}); });
} }

View File

@ -1,9 +1,11 @@
using System; using System;
using System.IO; using System.IO;
using System.Reactive.Disposables;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.Common.StatusFeed;
using Wabbajack.VirtualFileSystem; using Wabbajack.VirtualFileSystem;
namespace Wabbajack.Lib namespace Wabbajack.Lib
@ -33,6 +35,9 @@ namespace Wabbajack.Lib
private Subject<CPUStatus> _queueStatus { get; } = new Subject<CPUStatus>(); private Subject<CPUStatus> _queueStatus { get; } = new Subject<CPUStatus>();
public IObservable<CPUStatus> QueueStatus => _queueStatus; public IObservable<CPUStatus> QueueStatus => _queueStatus;
private Subject<IStatusMessage> _logMessages { get; } = new Subject<IStatusMessage>();
public IObservable<IStatusMessage> LogMessages => _logMessages;
private Subject<bool> _isRunning { get; } = new Subject<bool>(); private Subject<bool> _isRunning { get; } = new Subject<bool>();
public IObservable<bool> IsRunning => _isRunning; public IObservable<bool> IsRunning => _isRunning;
@ -40,6 +45,8 @@ namespace Wabbajack.Lib
private int _started; private int _started;
private readonly CancellationTokenSource _cancel = new CancellationTokenSource(); private readonly CancellationTokenSource _cancel = new CancellationTokenSource();
private readonly CompositeDisposable _subs = new CompositeDisposable();
protected void ConfigureProcessor(int steps, int threads = 0) protected void ConfigureProcessor(int steps, int threads = 0)
{ {
if (1 == Interlocked.CompareExchange(ref _configured, 1, 1)) if (1 == Interlocked.CompareExchange(ref _configured, 1, 1))
@ -48,7 +55,10 @@ namespace Wabbajack.Lib
} }
Queue = new WorkQueue(threads); Queue = new WorkQueue(threads);
UpdateTracker = new StatusUpdateTracker(steps); UpdateTracker = new StatusUpdateTracker(steps);
Queue.Status.Subscribe(_queueStatus); Queue.Status.Subscribe(_queueStatus)
.DisposeWith(_subs);
Queue.LogMessages.Subscribe(_logMessages)
.DisposeWith(_subs);
UpdateTracker.Progress.Subscribe(_percentCompleted); UpdateTracker.Progress.Subscribe(_percentCompleted);
UpdateTracker.StepName.Subscribe(_textStatus); UpdateTracker.StepName.Subscribe(_textStatus);
VFS = new Context(Queue) { UpdateTracker = UpdateTracker }; VFS = new Context(Queue) { UpdateTracker = UpdateTracker };

View File

@ -209,35 +209,41 @@ namespace Wabbajack.Lib
{ {
if (archives.TryGetValue(sha, out var found)) if (archives.TryGetValue(sha, out var found))
{ {
if (found.IniData == null) return await ResolveArchive(found);
Error($"No download metadata found for {found.Name}, please use MO2 to query info or add a .meta file and try again.");
var result = new Archive
{
State = await DownloadDispatcher.ResolveArchive(found.IniData)
};
if (result.State == null)
Error($"{found.Name} could not be handled by any of the downloaders");
result.Name = found.Name;
result.Hash = found.File.Hash;
result.Meta = found.Meta;
result.Size = found.File.Size;
Info($"Checking link for {found.Name}");
if (result.State != null && !await result.State.Verify())
Error(
$"Unable to resolve link for {found.Name}. If this is hosted on the Nexus the file may have been removed.");
return result;
} }
Error($"No match found for Archive sha: {sha} this shouldn't happen"); Error($"No match found for Archive sha: {sha} this shouldn't happen");
return null; return null;
} }
public async Task<Archive> ResolveArchive(IndexedArchive archive)
{
if (archive.IniData == null)
Error(
$"No download metadata found for {archive.Name}, please use MO2 to query info or add a .meta file and try again.");
var result = new Archive
{
State = (AbstractDownloadState) DownloadDispatcher.ResolveArchive(archive.IniData)
};
if (result.State == null)
Error($"{archive.Name} could not be handled by any of the downloaders");
result.Name = archive.Name;
result.Hash = archive.File.Hash;
result.Meta = archive.Meta;
result.Size = archive.File.Size;
Info($"Checking link for {archive.Name}");
if (result.State != null && !await result.State.Verify())
Error(
$"Unable to resolve link for {archive.Name}. If this is hosted on the Nexus the file may have been removed.");
return result;
}
public async Task<Directive> RunStack(IEnumerable<ICompilationStep> stack, RawSourceFile source) public async Task<Directive> RunStack(IEnumerable<ICompilationStep> stack, RawSourceFile source)
{ {
Utils.Status($"Compiling {source.Path}"); Utils.Status($"Compiling {source.Path}");

View File

@ -349,10 +349,8 @@ namespace Wabbajack.Lib
{ {
var relative_to = f.RelativeTo(OutputFolder); var relative_to = f.RelativeTo(OutputFolder);
Utils.Status($"Checking if modlist file {relative_to}"); Utils.Status($"Checking if modlist file {relative_to}");
if (indexed.ContainsKey(relative_to) || f.StartsWith(DownloadFolder + Path.DirectorySeparator)) if (indexed.ContainsKey(relative_to) || f.IsInPath(DownloadFolder))
{
return; return;
}
Utils.Log($"Deleting {relative_to} it's not part of this modlist"); Utils.Log($"Deleting {relative_to} it's not part of this modlist");
File.Delete(f); File.Delete(f);

View File

@ -23,7 +23,8 @@ namespace Wabbajack.Lib
typeof(MegaDownloader.State), typeof(ModDBDownloader.State), typeof(NexusDownloader.State), typeof(MegaDownloader.State), typeof(ModDBDownloader.State), typeof(NexusDownloader.State),
typeof(BSAStateObject), typeof(BSAFileStateObject), typeof(BA2StateObject), typeof(BA2DX10EntryState), typeof(BSAStateObject), typeof(BSAFileStateObject), typeof(BA2StateObject), typeof(BA2DX10EntryState),
typeof(BA2FileEntryState), typeof(MediaFireDownloader.State), typeof(ArchiveMeta), typeof(BA2FileEntryState), typeof(MediaFireDownloader.State), typeof(ArchiveMeta),
typeof(PropertyFile), typeof(SteamMeta), typeof(SteamWorkshopDownloader), typeof(SteamWorkshopDownloader.State) typeof(PropertyFile), typeof(SteamMeta), typeof(SteamWorkshopDownloader), typeof(SteamWorkshopDownloader.State),
typeof(LoversLabDownloader.State), typeof(GameFileSourceDownloader.State)
} }
}; };

View File

@ -10,12 +10,14 @@ namespace Wabbajack.Lib.Downloaders
{ {
public static readonly List<IDownloader> Downloaders = new List<IDownloader>() public static readonly List<IDownloader> Downloaders = new List<IDownloader>()
{ {
new GameFileSourceDownloader(),
new MegaDownloader(), new MegaDownloader(),
new DropboxDownloader(), new DropboxDownloader(),
new GoogleDriveDownloader(), new GoogleDriveDownloader(),
new ModDBDownloader(), new ModDBDownloader(),
new NexusDownloader(), new NexusDownloader(),
new MediaFireDownloader(), new MediaFireDownloader(),
new LoversLabDownloader(),
new HTTPDownloader(), new HTTPDownloader(),
new ManualDownloader(), new ManualDownloader(),
}; };
@ -27,9 +29,11 @@ namespace Wabbajack.Lib.Downloaders
IndexedDownloaders = Downloaders.ToDictionary(d => d.GetType()); IndexedDownloaders = Downloaders.ToDictionary(d => d.GetType());
} }
public static T GetInstance<T>() public static T GetInstance<T>() where T : IDownloader
{ {
return (T)IndexedDownloaders[typeof(T)]; var inst = (T)IndexedDownloaders[typeof(T)];
inst.Prepare();
return inst;
} }
public static async Task<AbstractDownloadState> ResolveArchive(dynamic ini) public static async Task<AbstractDownloadState> ResolveArchive(dynamic ini)

View File

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
using Wabbajack.Common;
using Wabbajack.Lib.Validation;
using File = Alphaleonis.Win32.Filesystem.File;
using Game = Wabbajack.Common.Game;
namespace Wabbajack.Lib.Downloaders
{
public class GameFileSourceDownloader : IDownloader
{
public async Task<AbstractDownloadState> GetDownloaderState(dynamic archiveINI)
{
var gameName = (string)archiveINI?.General?.gameName;
var gameFile = (string)archiveINI?.General?.gameFile;
if (gameFile == null || gameFile == null)
return null;
var game = GameRegistry.GetByMO2ArchiveName(gameName);
if (game == null) return null;
var path = game.GameLocation();
var filePath = Path.Combine(path, gameFile);
if (!File.Exists(filePath))
return null;
var hash = filePath.FileHashCached();
return new State
{
Game = GameRegistry.GetByMO2ArchiveName(gameName).Game,
GameFile = gameFile,
Hash = hash,
};
}
public async Task Prepare()
{
}
public class State : AbstractDownloadState
{
public Game Game { get; set; }
public string GameFile { get; set; }
public string Hash { get; set; }
internal string SourcePath => Path.Combine(Game.MetaData().GameLocation(), GameFile);
public override bool IsWhitelisted(ServerWhitelist whitelist)
{
return true;
}
public override async Task Download(Archive a, string destination)
{
using(var src = File.OpenRead(SourcePath))
using (var dest = File.OpenWrite(destination))
{
var size = new FileInfo(SourcePath).Length;
src.CopyToWithStatus(size, dest, "Copying from Game folder");
}
}
public override async Task<bool> Verify()
{
return File.Exists(SourcePath) && SourcePath.FileHashCached() == Hash;
}
public override IDownloader GetDownloader()
{
return DownloadDispatcher.GetInstance<GameFileSourceDownloader>();
}
public override string GetReportEntry(Archive a)
{
return $"* Game File {Game} - {GameFile}";
}
}
}
}

View File

@ -0,0 +1,201 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Wabbajack.Common;
using Wabbajack.Lib.LibCefHelpers;
using Wabbajack.Lib.Validation;
using Xilium.CefGlue.Common;
using File = Alphaleonis.Win32.Filesystem.File;
namespace Wabbajack.Lib.Downloaders
{
public class LoversLabDownloader : IDownloader
{
internal HttpClient _authedClient;
public async Task<AbstractDownloadState> GetDownloaderState(dynamic archive_ini)
{
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(BaseCefBrowser browser, Action<string> updateStatus, CancellationToken cancel)
{
updateStatus("Please Log Into Lovers Lab");
browser.Address = "https://www.loverslab.com/login";
async Task<bool> CleanAds()
{
try
{
await browser.EvaluateJavaScript<string>(
"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 Helpers.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");
}
catch (FileNotFoundException) { }
cookies = Utils.Log(new RequestLoversLabLogin()).Task.Result;
return Helpers.GetClient(cookies, "https://www.loverslab.com");
}
public class State : AbstractDownloadState
{
public string FileID { get; set; }
public string FileName { get; set; }
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 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})";
}
}
}
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)
{
Handled = true;
_source.SetResult(cookies);
}
public override void Cancel()
{
Handled = true;
_source.SetCanceled();
}
}
}

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@ -31,6 +33,27 @@ namespace Wabbajack.Lib.LibCefHelpers
FileExtractor.ExtractAll(wq, "cefglue.7z", "."); FileExtractor.ExtractAll(wq, "cefglue.7z", ".");
} }
public static HttpClient GetClient(IEnumerable<Cookie> cookies, string referer)
{
var container = ToCookieContainer(cookies);
var handler = new HttpClientHandler { CookieContainer = container };
var client = new HttpClient(handler);
client.DefaultRequestHeaders.Referrer = new Uri(referer);
return client;
}
private static CookieContainer ToCookieContainer(IEnumerable<Cookie> cookies)
{
var container = new CookieContainer();
cookies
.Do(cookie =>
{
container.Add(new System.Net.Cookie(cookie.Name, cookie.Value, cookie.Path, cookie.Domain));
});
return container;
}
public static async Task<Cookie[]> GetCookies(string domainEnding) public static async Task<Cookie[]> GetCookies(string domainEnding)
{ {
var manager = CefCookieManager.GetGlobal(null); var manager = CefCookieManager.GetGlobal(null);
@ -42,6 +65,7 @@ namespace Wabbajack.Lib.LibCefHelpers
return (await visitor.Task).Where(c => c.Domain.EndsWith(domainEnding)).ToArray(); return (await visitor.Task).Where(c => c.Domain.EndsWith(domainEnding)).ToArray();
} }
private class CookieVisitor : CefCookieVisitor private class CookieVisitor : CefCookieVisitor
{ {
TaskCompletionSource<List<Cookie>> _source = new TaskCompletionSource<List<Cookie>>(); TaskCompletionSource<List<Cookie>> _source = new TaskCompletionSource<List<Cookie>>();
@ -68,6 +92,8 @@ namespace Wabbajack.Lib.LibCefHelpers
if (disposing) if (disposing)
_source.SetResult(Cookies); _source.SetResult(Cookies);
} }
} }
public class Cookie public class Cookie

View File

@ -6,12 +6,15 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.Lib.CompilationSteps; using Wabbajack.Lib.CompilationSteps;
using Wabbajack.Lib.NexusApi; using Wabbajack.Lib.NexusApi;
using Wabbajack.Lib.Validation; using Wabbajack.Lib.Validation;
using Directory = Alphaleonis.Win32.Filesystem.Directory; using Directory = Alphaleonis.Win32.Filesystem.Directory;
using File = Alphaleonis.Win32.Filesystem.File; using File = Alphaleonis.Win32.Filesystem.File;
using FileInfo = Alphaleonis.Win32.Filesystem.FileInfo;
using Game = Wabbajack.Common.Game;
using Path = Alphaleonis.Win32.Filesystem.Path; using Path = Alphaleonis.Win32.Filesystem.Path;
namespace Wabbajack.Lib namespace Wabbajack.Lib
@ -30,6 +33,8 @@ namespace Wabbajack.Lib
public override string GamePath { get; } public override string GamePath { get; }
public GameMetaData CompilingGame { get; set; }
public override string ModListOutputFolder => "output_folder"; public override string ModListOutputFolder => "output_folder";
public override string ModListOutputFile { get; } public override string ModListOutputFile { get; }
@ -39,6 +44,8 @@ namespace Wabbajack.Lib
MO2Folder = mo2Folder; MO2Folder = mo2Folder;
MO2Profile = mo2Profile; MO2Profile = mo2Profile;
MO2Ini = Path.Combine(MO2Folder, "ModOrganizer.ini").LoadIniFile(); MO2Ini = Path.Combine(MO2Folder, "ModOrganizer.ini").LoadIniFile();
var mo2game = (string)MO2Ini.General.gameName;
CompilingGame = GameRegistry.Games.First(g => g.Value.MO2Name == mo2game).Value;
GamePath = ((string)MO2Ini.General.gamePath).Replace("\\\\", "\\"); GamePath = ((string)MO2Ini.General.gamePath).Replace("\\\\", "\\");
ModListOutputFile = outputFile; ModListOutputFile = outputFile;
} }
@ -74,7 +81,7 @@ namespace Wabbajack.Lib
protected override async Task<bool> _Begin(CancellationToken cancel) protected override async Task<bool> _Begin(CancellationToken cancel)
{ {
if (cancel.IsCancellationRequested) return false; if (cancel.IsCancellationRequested) return false;
ConfigureProcessor(16); ConfigureProcessor(19);
UpdateTracker.Reset(); UpdateTracker.Reset();
UpdateTracker.NextStep("Gathering information"); UpdateTracker.NextStep("Gathering information");
Info("Looking for other profiles"); Info("Looking for other profiles");
@ -122,20 +129,16 @@ namespace Wabbajack.Lib
Utils.DeleteDirectory(ModListOutputFolder); Utils.DeleteDirectory(ModListOutputFolder);
if (cancel.IsCancellationRequested) return false; if (cancel.IsCancellationRequested) return false;
UpdateTracker.NextStep("Finding Install Files"); UpdateTracker.NextStep("Inferring metas for game file downloads");
Directory.CreateDirectory(ModListOutputFolder); await InferMetas();
var mo2Files = Directory.EnumerateFiles(MO2Folder, "*", SearchOption.AllDirectories)
.Where(p => p.FileExists())
.Select(p => new RawSourceFile(VFS.Index.ByRootPath[p]) { Path = p.RelativeTo(MO2Folder) });
var gameFiles = Directory.EnumerateFiles(GamePath, "*", SearchOption.AllDirectories)
.Where(p => p.FileExists())
.Select(p => new RawSourceFile(VFS.Index.ByRootPath[p])
{ Path = Path.Combine(Consts.GameFolderFilesDir, p.RelativeTo(GamePath)) });
if (cancel.IsCancellationRequested) return false;
UpdateTracker.NextStep("Reindexing downloads after meta inferring");
await VFS.AddRoot(MO2DownloadsFolder);
await VFS.WriteToFile(_vfsCacheName);
if (cancel.IsCancellationRequested) return false;
UpdateTracker.NextStep("Pre-validating Archives");
IndexedArchives = Directory.EnumerateFiles(MO2DownloadsFolder) IndexedArchives = Directory.EnumerateFiles(MO2DownloadsFolder)
.Where(f => File.Exists(f + ".meta")) .Where(f => File.Exists(f + ".meta"))
@ -148,6 +151,21 @@ namespace Wabbajack.Lib
}) })
.ToList(); .ToList();
await CleanInvalidArchives();
UpdateTracker.NextStep("Finding Install Files");
Directory.CreateDirectory(ModListOutputFolder);
var mo2Files = Directory.EnumerateFiles(MO2Folder, "*", SearchOption.AllDirectories)
.Where(p => p.FileExists())
.Select(p => new RawSourceFile(VFS.Index.ByRootPath[p]) { Path = p.RelativeTo(MO2Folder) });
var gameFiles = Directory.EnumerateFiles(GamePath, "*", SearchOption.AllDirectories)
.Where(p => p.FileExists())
.Select(p => new RawSourceFile(VFS.Index.ByRootPath[p])
{ Path = Path.Combine(Consts.GameFolderFilesDir, p.RelativeTo(GamePath)) });
ModMetas = Directory.EnumerateDirectories(Path.Combine(MO2Folder, "mods")) ModMetas = Directory.EnumerateDirectories(Path.Combine(MO2Folder, "mods"))
.Keep(f => .Keep(f =>
{ {
@ -252,7 +270,7 @@ namespace Wabbajack.Lib
ModList = new ModList ModList = new ModList
{ {
GameType = GameRegistry.Games.Values.First(f => f.MO2Name == MO2Ini.General.gameName).Game, GameType = CompilingGame.Game,
WabbajackVersion = WabbajackVersion, WabbajackVersion = WabbajackVersion,
Archives = SelectedArchives.ToList(), Archives = SelectedArchives.ToList(),
ModManager = ModManager.MO2, ModManager = ModManager.MO2,
@ -284,6 +302,66 @@ namespace Wabbajack.Lib
return true; return true;
} }
private async Task CleanInvalidArchives()
{
var remove = (await IndexedArchives.PMap(Queue, async a =>
{
try
{
await ResolveArchive(a);
return null;
}
catch
{
return a;
}
})).Where(a => a != null).ToHashSet();
if (remove.Count == 0)
return;
Utils.Log(
$"Removing {remove.Count} archives from the compilation state, this is probably not an issue but reference this if you have compilation failures");
remove.Do(r => Utils.Log($"Resolution failed for: {r.File}"));
IndexedArchives.RemoveAll(a => remove.Contains(a));
}
private async Task InferMetas()
{
var to_find = Directory.EnumerateFiles(MO2DownloadsFolder)
.Where(f => !f.EndsWith(".meta") && !f.EndsWith(Consts.HashFileExtension))
.Where(f => !File.Exists(f + ".meta"))
.ToList();
if (to_find.Count == 0) return;
var games = new[]{CompilingGame}.Concat(GameRegistry.Games.Values.Where(g => g != CompilingGame));
var game_files = games
.Where(g => g.GameLocation() != null)
.SelectMany(game => Directory.EnumerateFiles(game.GameLocation(), "*", DirectoryEnumerationOptions.Recursive).Select(name => (game, name)))
.GroupBy(f => (Path.GetFileName(f.name), new FileInfo(f.name).Length))
.ToDictionary(f => f.Key);
await to_find.PMap(Queue, f =>
{
var vf = VFS.Index.ByFullPath[f];
if (!game_files.TryGetValue((Path.GetFileName(f), vf.Size), out var found))
return;
var (game, name) = found.FirstOrDefault(ff => ff.name.FileHash() == vf.Hash);
if (name == null)
return;
File.WriteAllLines(f+".meta", new[]
{
"[General]",
$"gameName={game.MO2ArchiveName}",
$"gameFile={name.RelativeTo(game.GameLocation()).Replace("\\", "/")}"
});
});
}
private async Task IncludeArchiveMetadata() private async Task IncludeArchiveMetadata()
{ {

View File

@ -12,7 +12,6 @@ using Wabbajack.Common;
using Wabbajack.Lib.CompilationSteps.CompilationErrors; using Wabbajack.Lib.CompilationSteps.CompilationErrors;
using Wabbajack.Lib.Downloaders; using Wabbajack.Lib.Downloaders;
using Wabbajack.Lib.NexusApi; using Wabbajack.Lib.NexusApi;
using Wabbajack.Lib.StatusMessages;
using Wabbajack.Lib.Validation; using Wabbajack.Lib.Validation;
using Directory = Alphaleonis.Win32.Filesystem.Directory; using Directory = Alphaleonis.Win32.Filesystem.Directory;
using File = Alphaleonis.Win32.Filesystem.File; using File = Alphaleonis.Win32.Filesystem.File;
@ -41,10 +40,10 @@ namespace Wabbajack.Lib
{ {
if (cancel.IsCancellationRequested) return false; if (cancel.IsCancellationRequested) return false;
ConfigureProcessor(18, await RecommendQueueSize()); ConfigureProcessor(18, await RecommendQueueSize());
var game = GameRegistry.Games[ModList.GameType]; var game = ModList.GameType.MetaData();
if (GameFolder == null) if (GameFolder == null)
GameFolder = game.GameLocation(SteamHandler.Instance.Games.Any(g => g.Game == game.Game)); GameFolder = game.GameLocation();
if (GameFolder == null) if (GameFolder == null)
{ {

View File

@ -113,41 +113,47 @@ namespace Wabbajack.Lib.NexusApi
} }
} }
public static async Task<string> SetupNexusLogin(BaseCefBrowser browser, Action<string> updateStatus) public static async Task<string> SetupNexusLogin(BaseCefBrowser browser, Action<string> updateStatus, CancellationToken cancel)
{ {
updateStatus("Please Log Into the Nexus"); updateStatus("Please Log Into the Nexus");
browser.Address = "https://users.nexusmods.com/auth/continue?client_id=nexus&redirect_uri=https://www.nexusmods.com/oauth/callback&response_type=code&referrer=//www.nexusmods.com"; browser.Address = "https://users.nexusmods.com/auth/continue?client_id=nexus&redirect_uri=https://www.nexusmods.com/oauth/callback&response_type=code&referrer=//www.nexusmods.com";
while (true) while (true)
{ {
var cookies = (await Helpers.GetCookies("nexusmods.com")); var cookies = await Helpers.GetCookies("nexusmods.com");
if (cookies.FirstOrDefault(c => c.Name == "member_id") != null) if (cookies.Any(c => c.Name == "member_id"))
break; break;
await Task.Delay(500); cancel.ThrowIfCancellationRequested();
await Task.Delay(500, cancel);
} }
// open a web socket to receive the api key // open a web socket to receive the api key
var guid = Guid.NewGuid(); var guid = Guid.NewGuid();
var _websocket = new WebSocket("wss://sso.nexusmods.com") using (var websocket = new WebSocket("wss://sso.nexusmods.com")
{ {
SslConfiguration = SslConfiguration =
{ {
EnabledSslProtocols = SslProtocols.Tls12 EnabledSslProtocols = SslProtocols.Tls12
} }
}; })
{
updateStatus("Please Authorize Wabbajack to Download Mods");
var api_key = new TaskCompletionSource<string>();
websocket.OnMessage += (sender, msg) => { api_key.SetResult(msg.Data); };
updateStatus("Please Authorize Wabbajack to Download Mods"); websocket.Connect();
var api_key = new TaskCompletionSource<string>(); websocket.Send("{\"id\": \"" + guid + "\", \"appid\": \"" + Consts.AppName + "\"}");
_websocket.OnMessage += (sender, msg) => { api_key.SetResult(msg.Data); }; await Task.Delay(1000, cancel);
_websocket.Connect(); // open a web browser to get user permission
_websocket.Send("{\"id\": \"" + guid + "\", \"appid\": \"" + Consts.AppName + "\"}"); browser.Address = $"https://www.nexusmods.com/sso?id={guid}&application={Consts.AppName}";
await Task.Delay(1000); using (cancel.Register(() =>
{
// open a web browser to get user permission api_key.SetCanceled();
browser.Address = $"https://www.nexusmods.com/sso?id={guid}&application={Consts.AppName}"; }))
{
return await api_key.Task; return await api_key.Task;
}
}
} }
public async Task<UserStatus> GetUserStatus() public async Task<UserStatus> GetUserStatus()
@ -322,13 +328,13 @@ namespace Wabbajack.Lib.NexusApi
public async Task<GetModFilesResponse> GetModFiles(Game game, int modid) public async Task<GetModFilesResponse> GetModFiles(Game game, int modid)
{ {
var url = $"https://api.nexusmods.com/v1/games/{GameRegistry.Games[game].NexusName}/mods/{modid}/files.json"; var url = $"https://api.nexusmods.com/v1/games/{game.MetaData().NexusName}/mods/{modid}/files.json";
return await GetCached<GetModFilesResponse>(url); return await GetCached<GetModFilesResponse>(url);
} }
public async Task<List<MD5Response>> GetModInfoFromMD5(Game game, string md5Hash) public async Task<List<MD5Response>> GetModInfoFromMD5(Game game, string md5Hash)
{ {
var url = $"https://api.nexusmods.com/v1/games/{GameRegistry.Games[game].NexusName}/mods/md5_search/{md5Hash}.json"; var url = $"https://api.nexusmods.com/v1/games/{game.MetaData().NexusName}/mods/md5_search/{md5Hash}.json";
return await Get<List<MD5Response>>(url); return await Get<List<MD5Response>>(url);
} }

View File

@ -14,7 +14,7 @@ namespace Wabbajack.Lib.NexusApi
public static string GetModURL(Game game, string argModId) public static string GetModURL(Game game, string argModId)
{ {
return $"https://nexusmods.com/{GameRegistry.Games[game].NexusName}/mods/{argModId}"; return $"https://nexusmods.com/{game.MetaData().NexusName}/mods/{argModId}";
} }
public static string FixupSummary(string argSummary) public static string FixupSummary(string argSummary)

View File

@ -3,11 +3,11 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Wabbajack.Common.StatusFeed; using Wabbajack.Common;
namespace Wabbajack.Lib.NexusApi namespace Wabbajack.Lib.NexusApi
{ {
public class RequestNexusAuthorization : AStatusMessage, IUserIntervention public class RequestNexusAuthorization : AUserIntervention
{ {
public override string ShortDescription => "Getting User's Nexus API Key"; public override string ShortDescription => "Getting User's Nexus API Key";
public override string ExtendedDescription { get; } public override string ExtendedDescription { get; }
@ -17,10 +17,13 @@ namespace Wabbajack.Lib.NexusApi
public void Resume(string apikey) public void Resume(string apikey)
{ {
Handled = true;
_source.SetResult(apikey); _source.SetResult(apikey);
} }
public void Cancel()
public override void Cancel()
{ {
Handled = true;
_source.SetCanceled(); _source.SetCanceled();
} }
} }

View File

@ -3,24 +3,16 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Wabbajack.Common.StatusFeed; using Wabbajack.Common;
namespace Wabbajack.Lib.StatusMessages namespace Wabbajack.Lib
{ {
public class ConfirmUpdateOfExistingInstall : AStatusMessage, IUserIntervention public class ConfirmUpdateOfExistingInstall : ConfirmationIntervention
{ {
public enum Choice
{
Continue,
Abort
}
public string OutputFolder { get; set; } public string OutputFolder { get; set; }
public string ModListName { get; set; } public string ModListName { get; set; }
public override string ShortDescription { get; } = "Do you want to overwrite existing files?";
private TaskCompletionSource<Choice> _source = new TaskCompletionSource<Choice>(); public override string ShortDescription { get; } = "Do you want to overwrite existing files?";
public Task<Choice> Task => _source.Task;
public override string ExtendedDescription public override string ExtendedDescription
{ {
@ -29,15 +21,5 @@ namespace Wabbajack.Lib.StatusMessages
Any files that exist in {OutputFolder} will be changed to match the files found in the {ModListName} modlist. This means that save games will be removed, custom settings Any files that exist in {OutputFolder} will be changed to match the files found in the {ModListName} modlist. This means that save games will be removed, custom settings
will be reverted. Are you sure you wish to continue?"; will be reverted. Are you sure you wish to continue?";
} }
public void Cancel()
{
_source.SetResult(Choice.Abort);
}
public void Confirm()
{
_source.SetResult(Choice.Continue);
}
} }
} }

View File

@ -146,7 +146,7 @@ namespace Wabbajack.Lib.Validation
} }
}); });
var nexus = NexusApi.NexusApiUtils.ConvertGameName(GameRegistry.Games[modlist.GameType].NexusName); var nexus = NexusApi.NexusApiUtils.ConvertGameName(modlist.GameType.MetaData().NexusName);
modlist.Archives modlist.Archives
.Where(a => a.State is NexusDownloader.State) .Where(a => a.State is NexusDownloader.State)

View File

@ -52,7 +52,7 @@ namespace Wabbajack.Lib
Game = game; Game = game;
GamePath = gamePath; GamePath = gamePath;
GameName = GameRegistry.Games[game].NexusName; GameName = game.MetaData().NexusName;
VortexFolder = vortexFolder; VortexFolder = vortexFolder;
DownloadsFolder = downloadsFolder; DownloadsFolder = downloadsFolder;
StagingFolder = stagingFolder; StagingFolder = stagingFolder;
@ -272,7 +272,7 @@ namespace Wabbajack.Lib
Directory.EnumerateFiles(GamePath, "vortex.deployment.json", SearchOption.AllDirectories) Directory.EnumerateFiles(GamePath, "vortex.deployment.json", SearchOption.AllDirectories)
.Where(File.Exists) .Where(File.Exists)
.Do(f => deploymentFile = f); .Do(f => deploymentFile = f);
var currentGame = GameRegistry.Games[Game]; var currentGame = Game.MetaData();
if (currentGame.AdditionalFolders != null && currentGame.AdditionalFolders.Count != 0) if (currentGame.AdditionalFolders != null && currentGame.AdditionalFolders.Count != 0)
currentGame.AdditionalFolders.Do(f => Directory.EnumerateFiles(f, "vortex.deployment.json", SearchOption.AllDirectories) currentGame.AdditionalFolders.Do(f => Directory.EnumerateFiles(f, "vortex.deployment.json", SearchOption.AllDirectories)
.Where(File.Exists) .Where(File.Exists)
@ -311,7 +311,7 @@ namespace Wabbajack.Lib
/// </summary> /// </summary>
private async Task AddExternalFolder() private async Task AddExternalFolder()
{ {
var currentGame = GameRegistry.Games[Game]; var currentGame = Game.MetaData();
if (currentGame.AdditionalFolders == null || currentGame.AdditionalFolders.Count == 0) return; if (currentGame.AdditionalFolders == null || currentGame.AdditionalFolders.Count == 0) return;
foreach (var f in currentGame.AdditionalFolders) foreach (var f in currentGame.AdditionalFolders)
{ {
@ -484,13 +484,13 @@ namespace Wabbajack.Lib
public static string RetrieveDownloadLocation(Game game, string vortexFolderPath = null) public static string RetrieveDownloadLocation(Game game, string vortexFolderPath = null)
{ {
vortexFolderPath = vortexFolderPath ?? TypicalVortexFolder(); vortexFolderPath = vortexFolderPath ?? TypicalVortexFolder();
return Path.Combine(vortexFolderPath, "downloads", GameRegistry.Games[game].NexusName); return Path.Combine(vortexFolderPath, "downloads", game.MetaData().NexusName);
} }
public static string RetrieveStagingLocation(Game game, string vortexFolderPath = null) public static string RetrieveStagingLocation(Game game, string vortexFolderPath = null)
{ {
vortexFolderPath = vortexFolderPath ?? TypicalVortexFolder(); vortexFolderPath = vortexFolderPath ?? TypicalVortexFolder();
var gameName = GameRegistry.Games[game].NexusName; var gameName = game.MetaData().NexusName;
return Path.Combine(vortexFolderPath, gameName, "mods"); return Path.Combine(vortexFolderPath, gameName, "mods");
} }
@ -520,7 +520,7 @@ namespace Wabbajack.Lib
public static bool IsActiveVortexGame(Game g) public static bool IsActiveVortexGame(Game g)
{ {
return GameRegistry.Games[g].SupportedModManager == ModManager.Vortex && !GameRegistry.Games[g].Disabled; return g.MetaData().SupportedModManager == ModManager.Vortex && !GameRegistry.Games[g].Disabled;
} }
} }

View File

@ -31,7 +31,7 @@ namespace Wabbajack.Lib
IgnoreMissingFiles = true; IgnoreMissingFiles = true;
#endif #endif
GameInfo = GameRegistry.Games[ModList.GameType]; GameInfo = ModList.GameType.MetaData();
} }
protected override async Task<bool> _Begin(CancellationToken cancel) protected override async Task<bool> _Begin(CancellationToken cancel)
@ -104,7 +104,7 @@ namespace Wabbajack.Lib
var manualFilesDir = Path.Combine(OutputFolder, Consts.ManualGameFilesDir); var manualFilesDir = Path.Combine(OutputFolder, Consts.ManualGameFilesDir);
var gameFolder = GameInfo.GameLocation(SteamHandler.Instance.Games.Any(g => g.Game == GameInfo.Game)); var gameFolder = GameInfo.GameLocation();
Info($"Copying files from {manualFilesDir} " + Info($"Copying files from {manualFilesDir} " +
$"to the game folder at {gameFolder}"); $"to the game folder at {gameFolder}");

View File

@ -116,6 +116,8 @@
<Compile Include="CompilationSteps\IStackStep.cs" /> <Compile Include="CompilationSteps\IStackStep.cs" />
<Compile Include="CompilationSteps\PatchStockESMs.cs" /> <Compile Include="CompilationSteps\PatchStockESMs.cs" />
<Compile Include="CompilationSteps\Serialization.cs" /> <Compile Include="CompilationSteps\Serialization.cs" />
<Compile Include="Downloaders\GameFileSourceDownloader.cs" />
<Compile Include="Downloaders\LoversLabDownloader.cs" />
<Compile Include="Downloaders\SteamWorkshopDownloader.cs" /> <Compile Include="Downloaders\SteamWorkshopDownloader.cs" />
<Compile Include="LibCefHelpers\Init.cs" /> <Compile Include="LibCefHelpers\Init.cs" />
<Compile Include="MO2Compiler.cs" /> <Compile Include="MO2Compiler.cs" />
@ -206,7 +208,7 @@
<Version>12.0.3</Version> <Version>12.0.3</Version>
</PackageReference> </PackageReference>
<PackageReference Include="ReactiveUI"> <PackageReference Include="ReactiveUI">
<Version>10.5.30</Version> <Version>11.0.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="SharpCompress"> <PackageReference Include="SharpCompress">
<Version>0.24.0</Version> <Version>0.24.0</Version>
@ -215,7 +217,7 @@
<Version>1.2.1</Version> <Version>1.2.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="System.Reactive"> <PackageReference Include="System.Reactive">
<Version>4.2.0</Version> <Version>4.3.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="WebSocketSharpFork"> <PackageReference Include="WebSocketSharpFork">
<Version>1.0.4</Version> <Version>1.0.4</Version>

View File

@ -246,14 +246,12 @@ namespace Wabbajack.Test
} }
[TestMethod] [TestMethod]
public void CanLoadFromGithub() public async Task CanLoadFromGithub()
{ {
using (var workQueue = new WorkQueue()) using (var workQueue = new WorkQueue())
{ {
new ValidateModlist(workQueue).LoadListsFromGithub(); await new ValidateModlist(workQueue).LoadListsFromGithub();
} }
} }
} }
} }

View File

@ -1,24 +1,36 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Alphaleonis.Win32.Filesystem;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.Common.StatusFeed;
using Wabbajack.Lib; using Wabbajack.Lib;
using Wabbajack.Lib.Downloaders; using Wabbajack.Lib.Downloaders;
using Wabbajack.Lib.LibCefHelpers; using Wabbajack.Lib.LibCefHelpers;
using Wabbajack.Lib.NexusApi; using Wabbajack.Lib.NexusApi;
using Wabbajack.Lib.Validation; using Wabbajack.Lib.Validation;
using File = Alphaleonis.Win32.Filesystem.File; using File = Alphaleonis.Win32.Filesystem.File;
using Game = Wabbajack.Common.Game;
namespace Wabbajack.Test namespace Wabbajack.Test
{ {
[TestClass] [TestClass]
public class DownloaderTests public class DownloaderTests
{ {
public TestContext TestContext { get; set; }
[TestInitialize] [TestInitialize]
public void Setup() public void Setup()
{ {
Helpers.ExtractLibs(); Helpers.ExtractLibs();
Utils.LogMessages.OfType<IInfo>().Subscribe(onNext: msg => TestContext.WriteLine(msg.ShortDescription));
Utils.LogMessages.OfType<IUserIntervention>().Subscribe(msg =>
TestContext.WriteLine("ERROR: User intervetion required: " + msg.ShortDescription));
} }
[TestMethod] [TestMethod]
@ -262,6 +274,59 @@ namespace Wabbajack.Test
Assert.AreEqual("2lZt+1h6wxM=", filename.FileHash()); Assert.AreEqual("2lZt+1h6wxM=", filename.FileHash());
} }
[TestMethod]
public async Task LoversLabDownload()
{
await DownloadDispatcher.GetInstance<LoversLabDownloader>().Prepare();
var ini = @"[General]
directURL=https://www.loverslab.com/files/file/11116-test-file-for-wabbajack-integration/?do=download&r=737123&confirm=1&t=1";
var state = (AbstractDownloadState)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 = "MEGA Test.txt" }, filename);
Assert.AreEqual("eSIyd+KOG3s=", Utils.FileHash(filename));
Assert.AreEqual(File.ReadAllText(filename), "Cheese for Everyone!");
}
[TestMethod]
public async Task GameFileSourceDownload()
{
await DownloadDispatcher.GetInstance<LoversLabDownloader>().Prepare();
var ini = $@"[General]
gameName={Game.SkyrimSpecialEdition.MetaData().MO2ArchiveName}
gameFile=Data/Update.esm";
var state = (AbstractDownloadState)DownloadDispatcher.ResolveArchive(ini.LoadIniString());
Assert.IsNotNull(state);
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 = "Update.esm" }, filename);
Assert.AreEqual("/DLG/LjdGXI=", Utils.FileHash(filename));
CollectionAssert.AreEqual(File.ReadAllBytes(Path.Combine(Game.SkyrimSpecialEdition.MetaData().GameLocation(), "Data/Update.esm")), File.ReadAllBytes(filename));
}
} }

View File

@ -114,7 +114,7 @@ namespace Wabbajack.Test
new List<string> new List<string>
{ {
"[General]", "[General]",
$"gameName={GameRegistry.Games[game].MO2ArchiveName}", $"gameName={game.MetaData().MO2ArchiveName}",
$"modID={modid}", $"modID={modid}",
$"fileID={file.file_id}" $"fileID={file.file_id}"
}); });

View File

@ -46,7 +46,7 @@ namespace Wabbajack.Test
File.WriteAllLines(Path.Combine(MO2Folder, "ModOrganizer.ini"), new [] File.WriteAllLines(Path.Combine(MO2Folder, "ModOrganizer.ini"), new []
{ {
"[General]", "[General]",
$"gameName={GameRegistry.Games[Game].MO2Name}", $"gameName={Game.MetaData().MO2Name}",
$"gamePath={GameFolder.Replace("\\", "\\\\")}", $"gamePath={GameFolder.Replace("\\", "\\\\")}",
$"download_directory={DownloadsFolder}" $"download_directory={DownloadsFolder}"
}); });

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Wabbajack.Common;
namespace Wabbajack.Test
{
[TestClass]
public class UtilsTests
{
[TestMethod]
public void IsInPathTests()
{
Assert.IsTrue("c:\\foo\\bar.exe".IsInPath("c:\\foo"));
Assert.IsFalse("c:\\foo\\bar.exe".IsInPath("c:\\fo"));
Assert.IsTrue("c:\\Foo\\bar.exe".IsInPath("c:\\foo"));
Assert.IsTrue("c:\\foo\\bar.exe".IsInPath("c:\\Foo"));
Assert.IsTrue("c:\\foo\\bar.exe".IsInPath("c:\\fOo"));
Assert.IsTrue("c:\\foo\\bar.exe".IsInPath("c:\\foo\\"));
Assert.IsTrue("c:\\foo\\bar\\".IsInPath("c:\\foo\\"));
}
}
}

View File

@ -112,6 +112,7 @@
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ContentRightsManagementTests.cs" /> <Compile Include="ContentRightsManagementTests.cs" />
<Compile Include="CompilationStackTests.cs" /> <Compile Include="CompilationStackTests.cs" />
<Compile Include="UtilsTests.cs" />
<Compile Include="VortexTests.cs" /> <Compile Include="VortexTests.cs" />
<Compile Include="WebAutomationTests.cs" /> <Compile Include="WebAutomationTests.cs" />
<Compile Include="zEditIntegrationTests.cs" /> <Compile Include="zEditIntegrationTests.cs" />
@ -154,10 +155,10 @@
<Version>12.0.3</Version> <Version>12.0.3</Version>
</PackageReference> </PackageReference>
<PackageReference Include="ReactiveUI"> <PackageReference Include="ReactiveUI">
<Version>10.5.30</Version> <Version>11.0.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="System.Reactive"> <PackageReference Include="System.Reactive">
<Version>4.2.0</Version> <Version>4.3.1</Version>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup /> <ItemGroup />

View File

@ -96,7 +96,7 @@
<Version>2.0.0</Version> <Version>2.0.0</Version>
</PackageReference> </PackageReference>
<PackageReference Include="System.Reactive"> <PackageReference Include="System.Reactive">
<Version>4.2.0</Version> <Version>4.3.1</Version>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" /> <Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />

View File

@ -89,7 +89,7 @@
<Version>2.2.6</Version> <Version>2.2.6</Version>
</PackageReference> </PackageReference>
<PackageReference Include="System.Collections.Immutable"> <PackageReference Include="System.Collections.Immutable">
<Version>1.6.0</Version> <Version>1.7.0</Version>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace Wabbajack
{
public class IsTypeVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(Visibility))
throw new InvalidOperationException($"The target must be of type {nameof(Visibility)}");
if (!(parameter is Type paramType))
{
throw new ArgumentException();
}
if (value == null) return Visibility.Collapsed;
return paramType.Equals(value.GetType()) ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -20,8 +20,8 @@ namespace Wabbajack
public InstallerSettings Installer { get; set; } = new InstallerSettings(); public InstallerSettings Installer { get; set; } = new InstallerSettings();
public CompilerSettings Compiler { get; set; } = new CompilerSettings(); public CompilerSettings Compiler { get; set; } = new CompilerSettings();
[JsonIgnoreAttribute]
private Subject<Unit> _saveSignal = new Subject<Unit>(); private Subject<Unit> _saveSignal = new Subject<Unit>();
[JsonIgnoreAttribute]
public IObservable<Unit> SaveSignal => _saveSignal; public IObservable<Unit> SaveSignal => _saveSignal;
public static bool TryLoadTypicalSettings(out MainSettings settings) public static bool TryLoadTypicalSettings(out MainSettings settings)
@ -58,6 +58,7 @@ namespace Wabbajack
{ {
public string InstallationLocation { get; set; } public string InstallationLocation { get; set; }
public string DownloadLocation { get; set; } public string DownloadLocation { get; set; }
public bool AutomaticallyOverrideExistingInstall { get; set; }
} }
public class CompilerSettings public class CompilerSettings

View File

@ -15,6 +15,7 @@
<local:InverseBooleanConverter x:Key="InverseBooleanConverter" /> <local:InverseBooleanConverter x:Key="InverseBooleanConverter" />
<local:IsNotNullVisibilityConverter x:Key="IsNotNullVisibilityConverter" /> <local:IsNotNullVisibilityConverter x:Key="IsNotNullVisibilityConverter" />
<local:EqualsToBoolConverter x:Key="EqualsToBoolConverter" /> <local:EqualsToBoolConverter x:Key="EqualsToBoolConverter" />
<local:IsTypeVisibilityConverter x:Key="IsTypeVisibilityConverter" />
<!-- Colors --> <!-- Colors -->
<Color x:Key="WindowBackgroundColor">#121212</Color> <Color x:Key="WindowBackgroundColor">#121212</Color>
@ -43,6 +44,7 @@
<Color x:Key="Secondary">#03DAC6</Color> <Color x:Key="Secondary">#03DAC6</Color>
<Color x:Key="DarkSecondary">#0e8f83</Color> <Color x:Key="DarkSecondary">#0e8f83</Color>
<Color x:Key="DarkerSecondary">#095952</Color> <Color x:Key="DarkerSecondary">#095952</Color>
<Color x:Key="SecondaryBackground">#042421</Color>
<Color x:Key="OffWhiteSeconday">#cef0ed</Color> <Color x:Key="OffWhiteSeconday">#cef0ed</Color>
<Color x:Key="LightSecondary">#8cede5</Color> <Color x:Key="LightSecondary">#8cede5</Color>
<Color x:Key="IntenseSecondary">#00ffe7</Color> <Color x:Key="IntenseSecondary">#00ffe7</Color>
@ -115,6 +117,7 @@
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}" /> <SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}" />
<SolidColorBrush x:Key="DarkSecondaryBrush" Color="{StaticResource DarkSecondary}" /> <SolidColorBrush x:Key="DarkSecondaryBrush" Color="{StaticResource DarkSecondary}" />
<SolidColorBrush x:Key="DarkerSecondaryBrush" Color="{StaticResource DarkerSecondary}" /> <SolidColorBrush x:Key="DarkerSecondaryBrush" Color="{StaticResource DarkerSecondary}" />
<SolidColorBrush x:Key="SecondaryBackgroundBrush" Color="{StaticResource SecondaryBackground}" />
<SolidColorBrush x:Key="OffWhiteSecondayBrush" Color="{StaticResource OffWhiteSeconday}" /> <SolidColorBrush x:Key="OffWhiteSecondayBrush" Color="{StaticResource OffWhiteSeconday}" />
<SolidColorBrush x:Key="LightSecondaryBrush" Color="{StaticResource LightSecondary}" /> <SolidColorBrush x:Key="LightSecondaryBrush" Color="{StaticResource LightSecondary}" />
<SolidColorBrush x:Key="IntenseSecondaryBrush" Color="{StaticResource IntenseSecondary}" /> <SolidColorBrush x:Key="IntenseSecondaryBrush" Color="{StaticResource IntenseSecondary}" />
@ -3708,4 +3711,58 @@
</Style> </Style>
<Style BasedOn="{StaticResource MainFilePickerStyle}" TargetType="{x:Type local:FilePicker}" /> <Style BasedOn="{StaticResource MainFilePickerStyle}" TargetType="{x:Type local:FilePicker}" />
<!-- User Intervention Background -->
<Style x:Key="AttentionBorderStyle" TargetType="Border">
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Background" Value="{StaticResource WindowBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource DarkerSecondaryBrush}" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsVisible, RelativeSource={RelativeSource Self}}" Value="True" />
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="False" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.EnterActions>
<BeginStoryboard x:Name="BorderGlow">
<Storyboard>
<ColorAnimation
AutoReverse="True"
RepeatBehavior="Forever"
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
To="{StaticResource Secondary}"
Duration="0:0:1.5" />
</Storyboard>
</BeginStoryboard>
<BeginStoryboard x:Name="BackgroundGlow">
<Storyboard>
<ColorAnimation
AutoReverse="True"
RepeatBehavior="Forever"
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="{StaticResource SecondaryBackground}"
Duration="0:0:1.5" />
</Storyboard>
</BeginStoryboard>
</MultiDataTrigger.EnterActions>
<MultiDataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
To="{StaticResource DarkerSecondary}"
Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="{StaticResource WindowBackgroundColor}"
Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</MultiDataTrigger.ExitActions>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary> </ResourceDictionary>

View File

@ -11,8 +11,11 @@ namespace Wabbajack
{ {
public interface ISubInstallerVM public interface ISubInstallerVM
{ {
InstallerVM Parent { get; }
IReactiveCommand BeginCommand { get; } IReactiveCommand BeginCommand { get; }
AInstaller ActiveInstallation { get; } AInstaller ActiveInstallation { get; }
void Unload(); void Unload();
bool SupportsAfterInstallNavigation { get; }
void AfterInstallNavigation();
} }
} }

View File

@ -19,7 +19,6 @@ using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using Wabbajack.Common.StatusFeed; using Wabbajack.Common.StatusFeed;
using System.Reactive; using System.Reactive;
using Wabbajack.Common.StatusFeed;
namespace Wabbajack namespace Wabbajack
{ {
@ -79,11 +78,19 @@ namespace Wabbajack
private readonly ObservableAsPropertyHelper<ModManager?> _TargetManager; private readonly ObservableAsPropertyHelper<ModManager?> _TargetManager;
public ModManager? TargetManager => _TargetManager.Value; public ModManager? TargetManager => _TargetManager.Value;
private readonly ObservableAsPropertyHelper<IUserIntervention> _ActiveGlobalUserIntervention;
public IUserIntervention ActiveGlobalUserIntervention => _ActiveGlobalUserIntervention.Value;
private readonly ObservableAsPropertyHelper<bool> _Completed;
public bool Completed => _Completed.Value;
// Command properties // Command properties
public IReactiveCommand ShowReportCommand { get; } public IReactiveCommand ShowReportCommand { get; }
public IReactiveCommand OpenReadmeCommand { get; } public IReactiveCommand OpenReadmeCommand { get; }
public IReactiveCommand VisitWebsiteCommand { get; } public IReactiveCommand VisitWebsiteCommand { get; }
public IReactiveCommand BackCommand { get; } public IReactiveCommand BackCommand { get; }
public IReactiveCommand CloseWhenCompleteCommand { get; }
public IReactiveCommand GoToInstallCommand { get; }
public InstallerVM(MainWindowVM mainWindowVM) public InstallerVM(MainWindowVM mainWindowVM)
{ {
@ -293,6 +300,48 @@ namespace Wabbajack
InstallingMode = true; InstallingMode = true;
}) })
.DisposeWith(CompositeDisposable); .DisposeWith(CompositeDisposable);
// Listen for user interventions, and compile a dynamic list of all unhandled ones
var activeInterventions = this.WhenAny(x => x.Installer.ActiveInstallation)
.SelectMany(c => c?.LogMessages ?? Observable.Empty<IStatusMessage>())
.WhereCastable<IStatusMessage, IUserIntervention>()
.ToObservableChangeSet()
.AutoRefresh(i => i.Handled)
.Filter(i => !i.Handled)
.AsObservableList();
// Find the top intervention /w no CPU ID to be marked as "global"
_ActiveGlobalUserIntervention = activeInterventions.Connect()
.Filter(x => x.CpuID == WorkQueue.UnassignedCpuId)
.QueryWhenChanged(query => query.FirstOrDefault())
.ObserveOnGuiThread()
.ToProperty(this, nameof(ActiveGlobalUserIntervention));
_Completed = Observable.CombineLatest(
this.WhenAny(x => x.Installing),
this.WhenAny(x => x.InstallingMode),
resultSelector: (installing, installingMode) =>
{
return installingMode && !installing;
})
.ToProperty(this, nameof(Completed));
CloseWhenCompleteCommand = ReactiveCommand.Create(
canExecute: this.WhenAny(x => x.Completed),
execute: () =>
{
MWVM.ShutdownApplication();
});
GoToInstallCommand = ReactiveCommand.Create(
canExecute: Observable.CombineLatest(
this.WhenAny(x => x.Completed),
this.WhenAny(x => x.Installer.SupportsAfterInstallNavigation),
resultSelector: (complete, supports) => complete && supports),
execute: () =>
{
Installer.AfterInstallNavigation();
});
} }
private void ShowReport() private void ShowReport()

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
@ -15,7 +16,7 @@ namespace Wabbajack
{ {
public class MO2InstallerVM : ViewModel, ISubInstallerVM public class MO2InstallerVM : ViewModel, ISubInstallerVM
{ {
private InstallerVM _installerVM; public InstallerVM Parent { get; }
public IReactiveCommand BeginCommand { get; } public IReactiveCommand BeginCommand { get; }
@ -29,9 +30,14 @@ namespace Wabbajack
public FilePickerVM DownloadLocation { get; } public FilePickerVM DownloadLocation { get; }
public bool SupportsAfterInstallNavigation => true;
[Reactive]
public bool AutomaticallyOverwrite { get; set; }
public MO2InstallerVM(InstallerVM installerVM) public MO2InstallerVM(InstallerVM installerVM)
{ {
_installerVM = installerVM; Parent = installerVM;
Location = new FilePickerVM() Location = new FilePickerVM()
{ {
@ -131,11 +137,31 @@ namespace Wabbajack
if (settingsPair.Current == null) return; if (settingsPair.Current == null) return;
Location.TargetPath = settingsPair.Current.InstallationLocation; Location.TargetPath = settingsPair.Current.InstallationLocation;
DownloadLocation.TargetPath = settingsPair.Current.DownloadLocation; DownloadLocation.TargetPath = settingsPair.Current.DownloadLocation;
AutomaticallyOverwrite = settingsPair.Current.AutomaticallyOverrideExistingInstall;
}) })
.DisposeWith(CompositeDisposable); .DisposeWith(CompositeDisposable);
installerVM.MWVM.Settings.SaveSignal installerVM.MWVM.Settings.SaveSignal
.Subscribe(_ => SaveSettings(CurrentSettings)) .Subscribe(_ => SaveSettings(CurrentSettings))
.DisposeWith(CompositeDisposable); .DisposeWith(CompositeDisposable);
// Hook onto user interventions, and intercept MO2 specific ones for customization
this.WhenAny(x => x.ActiveInstallation.LogMessages)
.Switch()
.Subscribe(x =>
{
switch (x)
{
case ConfirmUpdateOfExistingInstall c:
if (AutomaticallyOverwrite)
{
c.Confirm();
}
break;
default:
break;
}
})
.DisposeWith(CompositeDisposable);
} }
public void Unload() public void Unload()
@ -145,10 +171,16 @@ namespace Wabbajack
private void SaveSettings(Mo2ModlistInstallationSettings settings) private void SaveSettings(Mo2ModlistInstallationSettings settings)
{ {
_installerVM.MWVM.Settings.Installer.LastInstalledListLocation = _installerVM.ModListLocation.TargetPath; Parent.MWVM.Settings.Installer.LastInstalledListLocation = Parent.ModListLocation.TargetPath;
if (settings == null) return; if (settings == null) return;
settings.InstallationLocation = Location.TargetPath; settings.InstallationLocation = Location.TargetPath;
settings.DownloadLocation = DownloadLocation.TargetPath; settings.DownloadLocation = DownloadLocation.TargetPath;
settings.AutomaticallyOverrideExistingInstall = AutomaticallyOverwrite;
}
public void AfterInstallNavigation()
{
Process.Start("explorer.exe", Location.TargetPath);
} }
} }
} }

View File

@ -13,6 +13,8 @@ namespace Wabbajack
{ {
public class VortexInstallerVM : ViewModel, ISubInstallerVM public class VortexInstallerVM : ViewModel, ISubInstallerVM
{ {
public InstallerVM Parent { get; }
public IReactiveCommand BeginCommand { get; } public IReactiveCommand BeginCommand { get; }
[Reactive] [Reactive]
@ -27,8 +29,12 @@ namespace Wabbajack
private readonly ObservableAsPropertyHelper<Game> _TargetGame; private readonly ObservableAsPropertyHelper<Game> _TargetGame;
public Game TargetGame => _TargetGame.Value; public Game TargetGame => _TargetGame.Value;
public bool SupportsAfterInstallNavigation => false;
public VortexInstallerVM(InstallerVM installerVM) public VortexInstallerVM(InstallerVM installerVM)
{ {
Parent = installerVM;
_TargetGame = installerVM.WhenAny(x => x.ModList.SourceModList.GameType) _TargetGame = installerVM.WhenAny(x => x.ModList.SourceModList.GameType)
.ToProperty(this, nameof(TargetGame)); .ToProperty(this, nameof(TargetGame));
@ -91,5 +97,10 @@ namespace Wabbajack
public void Unload() public void Unload()
{ {
} }
public void AfterInstallNavigation()
{
throw new NotImplementedException();
}
} }
} }

View File

@ -12,8 +12,6 @@ using System.Windows.Threading;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.Common.StatusFeed; using Wabbajack.Common.StatusFeed;
using Wabbajack.Lib; using Wabbajack.Lib;
using Wabbajack.Lib.NexusApi;
using Wabbajack.Lib.StatusMessages;
namespace Wabbajack namespace Wabbajack
{ {
@ -37,6 +35,7 @@ namespace Wabbajack
public readonly Lazy<ModListGalleryVM> Gallery; public readonly Lazy<ModListGalleryVM> Gallery;
public readonly ModeSelectionVM ModeSelectionVM; public readonly ModeSelectionVM ModeSelectionVM;
public readonly WebBrowserVM WebBrowserVM; public readonly WebBrowserVM WebBrowserVM;
public readonly UserInterventionHandlers UserInterventionHandlers;
public Dispatcher ViewDispatcher { get; set; } public Dispatcher ViewDispatcher { get; set; }
public MainWindowVM(MainWindow mainWindow, MainSettings settings) public MainWindowVM(MainWindow mainWindow, MainSettings settings)
@ -49,6 +48,7 @@ namespace Wabbajack
Gallery = new Lazy<ModListGalleryVM>(() => new ModListGalleryVM(this)); Gallery = new Lazy<ModListGalleryVM>(() => new ModListGalleryVM(this));
ModeSelectionVM = new ModeSelectionVM(this); ModeSelectionVM = new ModeSelectionVM(this);
WebBrowserVM = new WebBrowserVM(); WebBrowserVM = new WebBrowserVM();
UserInterventionHandlers = new UserInterventionHandlers(this);
// Set up logging // Set up logging
Utils.LogMessages Utils.LogMessages
@ -63,12 +63,11 @@ namespace Wabbajack
.DisposeWith(CompositeDisposable); .DisposeWith(CompositeDisposable);
Utils.LogMessages Utils.LogMessages
.OfType<ConfirmUpdateOfExistingInstall>() .OfType<IUserIntervention>()
.Subscribe(msg => ConfirmUpdate(msg)); .ObserveOnGuiThread()
.SelectTask(msg => UserInterventionHandlers.Handle(msg))
Utils.LogMessages .Subscribe()
.OfType<RequestNexusAuthorization>() .DisposeWith(CompositeDisposable);
.Subscribe(HandleRequestNexusAuthorization);
if (IsStartingFromModlist(out var path)) if (IsStartingFromModlist(out var path))
{ {
@ -82,47 +81,6 @@ namespace Wabbajack
} }
} }
private void HandleRequestNexusAuthorization(RequestNexusAuthorization msg)
{
ViewDispatcher.InvokeAsync(async () =>
{
var oldPane = ActivePane;
var vm = new WebBrowserVM();
ActivePane = vm;
try
{
vm.BackCommand = ReactiveCommand.Create(() =>
{
ActivePane = oldPane;
msg.Cancel();
});
}
catch (Exception e)
{ }
try
{
var key = await NexusApiClient.SetupNexusLogin(vm.Browser, m => vm.Instructions = m);
msg.Resume(key);
}
catch (Exception ex)
{
msg.Cancel();
}
ActivePane = oldPane;
});
}
private void ConfirmUpdate(ConfirmUpdateOfExistingInstall msg)
{
var result = MessageBox.Show(msg.ExtendedDescription, msg.ShortDescription, MessageBoxButton.OKCancel);
if (result == MessageBoxResult.OK)
msg.Confirm();
else
msg.Cancel();
}
private static bool IsStartingFromModlist(out string modlistPath) private static bool IsStartingFromModlist(out string modlistPath)
{ {
string[] args = Environment.GetCommandLineArgs(); string[] args = Environment.GetCommandLineArgs();
@ -144,5 +102,16 @@ namespace Wabbajack
ActivePane = installer; ActivePane = installer;
installer.ModListLocation.TargetPath = path; installer.ModListLocation.TargetPath = path;
} }
public void ShutdownApplication()
{
Dispose();
Settings.PosX = MainWindow.Left;
Settings.PosY = MainWindow.Top;
Settings.Width = MainWindow.Width;
Settings.Height = MainWindow.Height;
MainSettings.SaveSettings(Settings);
Application.Current.Shutdown();
}
} }
} }

View File

@ -98,7 +98,7 @@ namespace Wabbajack
_targetMod = Observable.CombineLatest( _targetMod = Observable.CombineLatest(
modVMs.QueryWhenChanged(), modVMs.QueryWhenChanged(),
selectedIndex, selectedIndex,
resultSelector: (query, selected) => query.Items.ElementAtOrDefault(selected % query.Count)) resultSelector: (query, selected) => query.Items.ElementAtOrDefault(selected % (query.Count == 0 ? 1 : query.Count)))
.StartWith(default(ModVM)) .StartWith(default(ModVM))
.ObserveOn(RxApp.MainThreadScheduler) .ObserveOn(RxApp.MainThreadScheduler)
.ToProperty(this, nameof(TargetMod)); .ToProperty(this, nameof(TargetMod));

View File

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using ReactiveUI;
using Wabbajack.Common;
using Wabbajack.Lib.Downloaders;
using Wabbajack.Lib.NexusApi;
namespace Wabbajack
{
public class UserInterventionHandlers
{
public MainWindowVM MainWindow { get; }
public UserInterventionHandlers(MainWindowVM mvm)
{
MainWindow = mvm;
}
private async Task WrapBrowserJob(IUserIntervention intervention, Func<WebBrowserVM, CancellationTokenSource, Task> toDo)
{
CancellationTokenSource cancel = new CancellationTokenSource();
var oldPane = MainWindow.ActivePane;
var vm = new WebBrowserVM();
MainWindow.ActivePane = vm;
vm.BackCommand = ReactiveCommand.Create(() =>
{
cancel.Cancel();
MainWindow.ActivePane = oldPane;
intervention.Cancel();
});
try
{
await toDo(vm, cancel);
}
catch (TaskCanceledException)
{
intervention.Cancel();
}
catch (Exception ex)
{
Utils.Error(ex);
intervention.Cancel();
}
MainWindow.ActivePane = oldPane;
}
public async Task Handle(IUserIntervention msg)
{
switch (msg)
{
case RequestNexusAuthorization c:
await WrapBrowserJob(msg, async (vm, cancel) =>
{
var key = await NexusApiClient.SetupNexusLogin(vm.Browser, m => vm.Instructions = m, cancel.Token);
c.Resume(key);
});
break;
case RequestLoversLabLogin c:
await WrapBrowserJob(msg, async (vm, cancel) =>
{
var data = await LoversLabDownloader.GetAndCacheLoversLabCookies(vm.Browser, m => vm.Instructions = m, cancel.Token);
c.Resume(data);
});
break;
case ConfirmationIntervention c:
break;
default:
throw new NotImplementedException($"No handler for {msg}");
}
}
}
}

View File

@ -6,7 +6,6 @@ using System.Threading.Tasks;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
using Wabbajack.Lib; using Wabbajack.Lib;
using Xilium.CefGlue.Common;
using Xilium.CefGlue.WPF; using Xilium.CefGlue.WPF;
namespace Wabbajack namespace Wabbajack
@ -16,13 +15,13 @@ namespace Wabbajack
[Reactive] [Reactive]
public string Instructions { get; set; } public string Instructions { get; set; }
public WpfCefBrowser Browser { get; } public WpfCefBrowser Browser { get; } = new WpfCefBrowser();
[Reactive] [Reactive]
public IReactiveCommand BackCommand { get; set; } public IReactiveCommand BackCommand { get; set; }
public WebBrowserVM(string url = "http://www.wabbajack.org") public WebBrowserVM(string url = "http://www.wabbajack.org")
{ {
Browser = new WpfCefBrowser();
Browser.Address = url; Browser.Address = url;
Instructions = "Wabbajack Web Browser"; Instructions = "Wabbajack Web Browser";
} }

View File

@ -1,44 +1,17 @@
<UserControl <UserControl
x:Class="Wabbajack.LogCpuView" x:Class="Wabbajack.CpuView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wabbajack" xmlns:local="clr-namespace:Wabbajack"
xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro" xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="250" d:DesignHeight="450"
d:DesignWidth="800" d:DesignWidth="800"
BorderThickness="0"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Rectangle Fill="{StaticResource HeatedBorderBrush}" Opacity="{Binding ProgressPercent, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="500" />
</Grid.ColumnDefinitions>
<Rectangle
Grid.Column="0"
Fill="{StaticResource HeatedBorderBrush}"
Opacity="{Binding ProgressPercent, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
<ListBox <ListBox
Grid.Column="0"
local:AutoScrollBehavior.ScrollOnNewItem="True"
BorderBrush="Transparent"
BorderThickness="1"
ItemsSource="{Binding Log}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ShortDescription}" TextWrapping="WrapWithOverflow" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Rectangle
Grid.Column="2"
Fill="{StaticResource HeatedBorderBrush}"
Opacity="{Binding ProgressPercent, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
<ListBox
Grid.Column="2"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
BorderBrush="Transparent" BorderBrush="Transparent"
BorderThickness="1" BorderThickness="1"

View File

@ -1,22 +1,34 @@
using System.Windows; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Wabbajack namespace Wabbajack
{ {
/// <summary> /// <summary>
/// Interaction logic for LogCpuView.xaml /// Interaction logic for CpuView.xaml
/// </summary> /// </summary>
public partial class LogCpuView : UserControl public partial class CpuView : UserControl
{ {
public double ProgressPercent public double ProgressPercent
{ {
get => (double)GetValue(ProgressPercentProperty); get => (double)GetValue(ProgressPercentProperty);
set => SetValue(ProgressPercentProperty, value); set => SetValue(ProgressPercentProperty, value);
} }
public static readonly DependencyProperty ProgressPercentProperty = DependencyProperty.Register(nameof(ProgressPercent), typeof(double), typeof(LogCpuView), public static readonly DependencyProperty ProgressPercentProperty = DependencyProperty.Register(nameof(ProgressPercent), typeof(double), typeof(CpuView),
new FrameworkPropertyMetadata(default(double))); new FrameworkPropertyMetadata(default(double)));
public LogCpuView() public CpuView()
{ {
InitializeComponent(); InitializeComponent();
} }

View File

@ -0,0 +1,26 @@
<UserControl
x:Class="Wabbajack.LogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Grid>
<Rectangle Fill="{StaticResource HeatedBorderBrush}" Opacity="{Binding ProgressPercent, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
<ListBox
local:AutoScrollBehavior.ScrollOnNewItem="True"
BorderBrush="Transparent"
BorderThickness="1"
ItemsSource="{Binding Log}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ShortDescription}" TextWrapping="WrapWithOverflow" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for LogView.xaml
/// </summary>
public partial class LogView : UserControl
{
public double ProgressPercent
{
get => (double)GetValue(ProgressPercentProperty);
set => SetValue(ProgressPercentProperty, value);
}
public static readonly DependencyProperty ProgressPercentProperty = DependencyProperty.Register(nameof(ProgressPercent), typeof(double), typeof(LogView),
new FrameworkPropertyMetadata(default(double)));
public LogView()
{
InitializeComponent();
}
}
}

View File

@ -2,6 +2,7 @@
x:Class="Wabbajack.CompilerView" x:Class="Wabbajack.CompilerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:common="clr-namespace:Wabbajack.Common;assembly=Wabbajack.Common"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks" xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:local="clr-namespace:Wabbajack" xmlns:local="clr-namespace:Wabbajack"
@ -240,7 +241,24 @@
Grid.ColumnSpan="5" Grid.ColumnSpan="5"
Margin="5" Margin="5"
Visibility="{Binding Compiling, Converter={StaticResource bool2VisibilityConverter}, FallbackValue=Hidden}"> Visibility="{Binding Compiling, Converter={StaticResource bool2VisibilityConverter}, FallbackValue=Hidden}">
<local:LogCpuView ProgressPercent="{Binding PercentCompleted}" /> <Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<local:LogView Grid.Column="0" ProgressPercent="{Binding PercentCompleted, Mode=OneWay}" />
<local:CpuView
Grid.Column="2"
ProgressPercent="{Binding PercentCompleted, Mode=OneWay}"
Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsNotNullVisibilityConverter}, ConverterParameter=False}" />
<Border
Grid.Column="2"
Style="{StaticResource AttentionBorderStyle}"
Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsNotNullVisibilityConverter}}">
<Grid>
<local:ConfirmationInterventionView DataContext="{Binding ActiveGlobalUserIntervention}" Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsTypeVisibilityConverter}, ConverterParameter={x:Type common:ConfirmationIntervention}}" />
</Grid>
</Border>
</Grid> </Grid>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -0,0 +1,139 @@
<UserControl
x:Class="Wabbajack.InstallationCompleteView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Border Style="{StaticResource AttentionBorderStyle}" ClipToBounds="True">
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="3*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="3"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
FontFamily="Lucida Sans"
FontSize="22"
FontWeight="Black"
Text="Installation Complete">
<TextBlock.Effect>
<DropShadowEffect BlurRadius="25" Opacity="0.5" />
</TextBlock.Effect>
</TextBlock>
<Grid
Grid.Row="1"
Grid.Column="0"
VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button
Grid.Row="0"
Width="55"
Height="55"
Command="{Binding BackCommand}"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconMaterial
Width="28"
Height="28"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="ArrowLeft" />
</Button>
<TextBlock
Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Main Menu" />
</Grid>
<Grid
Grid.Row="1"
Grid.Column="1"
VerticalAlignment="Center"
Visibility="{Binding InstallerSupportsAfterInstallNavigation, Converter={StaticResource bool2VisibilityConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button
Width="55"
Height="55"
Command="{Binding GoToInstallCommand}"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconMaterial
Width="25"
Height="25"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="FolderMove" />
</Button>
<TextBlock
Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Open Install Folder" />
</Grid>
<Grid
Grid.Row="1"
Grid.Column="2"
VerticalAlignment="Center"
Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!--<Button
Width="55"
Height="55"
Background="{StaticResource PrimaryVariantBrush}"
BorderBrush="{StaticResource PrimaryVariantBrush}"
IsHitTestVisible="False"
Style="{StaticResource CircleButtonStyle}">
<Button.Effect>
<BlurEffect Radius="35" />
</Button.Effect>
</Button>
<Button
Width="55"
Height="55"
Background="{StaticResource SecondaryBrush}"
BorderBrush="{StaticResource SecondaryBrush}"
IsHitTestVisible="False"
Style="{StaticResource CircleButtonStyle}">
<Button.Effect>
<BlurEffect Radius="15" />
</Button.Effect>
</Button>-->
<Button
Width="55"
Height="55"
Command="{Binding CloseWhenCompleteCommand}"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconMaterial
Width="30"
Height="30"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="Check" />
</Button>
<TextBlock
Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Close" />
</Grid>
</Grid>
</Border>
</UserControl>

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for InstallationCompleteView.xaml
/// </summary>
public partial class InstallationCompleteView : UserControl
{
public InstallationCompleteView()
{
InitializeComponent();
}
}
}

View File

@ -2,8 +2,10 @@
x:Class="Wabbajack.InstallationView" x:Class="Wabbajack.InstallationView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:common="clr-namespace:Wabbajack.Common;assembly=Wabbajack.Common"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks" xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:lib="clr-namespace:Wabbajack.Lib;assembly=Wabbajack.Lib"
xmlns:local="clr-namespace:Wabbajack" xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DataContext="{d:DesignInstance local:InstallerVM}" d:DataContext="{d:DesignInstance local:InstallerVM}"
@ -24,7 +26,7 @@
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="47" /> <RowDefinition Height="47" />
<RowDefinition Height="4*" /> <RowDefinition Height="4*" />
<RowDefinition Height="*" MinHeight="150" /> <RowDefinition Height="*" MinHeight="175" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Rectangle <Rectangle
x:Name="BorderEdgeFadeDown" x:Name="BorderEdgeFadeDown"
@ -287,49 +289,19 @@
Background="Transparent" Background="Transparent"
VerticalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
Visibility="{Binding InstallingMode, Converter={StaticResource bool2VisibilityConverter}, ConverterParameter=False}"> Visibility="{Binding InstallingMode, Converter={StaticResource bool2VisibilityConverter}, ConverterParameter=False}">
<Grid> <ContentPresenter
<Grid.ColumnDefinitions> Margin="0"
<ColumnDefinition Width="Auto" MinWidth="120" /> VerticalAlignment="Center"
<ColumnDefinition Width="20" /> Content="{Binding Installer}">
<ColumnDefinition Width="*" /> <ContentPresenter.Resources>
</Grid.ColumnDefinitions> <DataTemplate DataType="{x:Type local:MO2InstallerVM}">
<Grid.RowDefinitions> <local:MO2InstallerConfigView />
<RowDefinition Height="*" /> </DataTemplate>
<RowDefinition Height="40" /> <DataTemplate DataType="{x:Type local:VortexInstallerVM}">
<RowDefinition Height="80" /> <local:VortexInstallerConfigView />
<RowDefinition Height="*" /> </DataTemplate>
</Grid.RowDefinitions> </ContentPresenter.Resources>
<TextBlock </ContentPresenter>
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Target Modlist"
TextAlignment="Center" />
<local:FilePicker
Grid.Row="1"
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding ModListLocation}"
FontSize="14" />
<ContentPresenter
Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="3"
Margin="0"
Content="{Binding Installer}">
<ContentPresenter.Resources>
<DataTemplate DataType="{x:Type local:MO2InstallerVM}">
<local:MO2InstallerConfigView />
</DataTemplate>
<DataTemplate DataType="{x:Type local:VortexInstallerVM}">
<local:VortexInstallerConfigView />
</DataTemplate>
</ContentPresenter.Resources>
</ContentPresenter>
</Grid>
</ScrollViewer> </ScrollViewer>
<local:BeginButton <local:BeginButton
Grid.Column="2" Grid.Column="2"
@ -343,7 +315,26 @@
Grid.Row="2" Grid.Row="2"
Margin="5,0,5,5" Margin="5,0,5,5"
Visibility="{Binding InstallingMode, Converter={StaticResource bool2VisibilityConverter}, FallbackValue=Hidden}"> Visibility="{Binding InstallingMode, Converter={StaticResource bool2VisibilityConverter}, FallbackValue=Hidden}">
<local:LogCpuView ProgressPercent="{Binding PercentCompleted, Mode=OneWay}" /> <Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<local:LogView Grid.Column="0" ProgressPercent="{Binding PercentCompleted, Mode=OneWay}" />
<local:CpuView
Grid.Column="2"
ProgressPercent="{Binding PercentCompleted, Mode=OneWay}"
Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsNotNullVisibilityConverter}, ConverterParameter=False}" />
<Border
Grid.Column="2"
Style="{StaticResource AttentionBorderStyle}"
Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsNotNullVisibilityConverter}}">
<Grid>
<local:ConfirmationInterventionView DataContext="{Binding ActiveGlobalUserIntervention}" Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsTypeVisibilityConverter}, ConverterParameter={x:Type common:ConfirmationIntervention}}" />
<local:ConfirmUpdateOfExistingInstallView Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsTypeVisibilityConverter}, ConverterParameter={x:Type lib:ConfirmUpdateOfExistingInstall}}" />
</Grid>
</Border>
<local:InstallationCompleteView Grid.Column="2" Visibility="{Binding Completed, Converter={StaticResource bool2VisibilityConverter}, FallbackValue=Collapsed}" />
</Grid> </Grid>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -3,6 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:local="clr-namespace:Wabbajack" xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450" d:DesignHeight="450"
@ -15,11 +16,29 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="40" /> <RowDefinition Height="40" />
<RowDefinition Height="40" /> <RowDefinition Height="40" />
<RowDefinition Height="40" />
<RowDefinition Height="20" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock <TextBlock
Grid.Row="0" Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Target Modlist"
TextAlignment="Center" />
<local:FilePicker
Grid.Row="1"
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding Parent.ModListLocation}"
FontSize="14" />
<TextBlock
Grid.Row="2"
Grid.Column="0" Grid.Column="0"
HorizontalAlignment="Right" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center"
@ -27,14 +46,14 @@
Text="Installation Location" Text="Installation Location"
TextAlignment="Center" /> TextAlignment="Center" />
<local:FilePicker <local:FilePicker
Grid.Row="0" Grid.Row="2"
Grid.Column="2" Grid.Column="2"
Height="30" Height="30"
VerticalAlignment="Center" VerticalAlignment="Center"
DataContext="{Binding Location}" DataContext="{Binding Location}"
FontSize="14" /> FontSize="14" />
<TextBlock <TextBlock
Grid.Row="1" Grid.Row="3"
Grid.Column="0" Grid.Column="0"
HorizontalAlignment="Right" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center"
@ -42,11 +61,29 @@
Text="Download Location" Text="Download Location"
TextAlignment="Center" /> TextAlignment="Center" />
<local:FilePicker <local:FilePicker
Grid.Row="1" Grid.Row="3"
Grid.Column="2" Grid.Column="2"
Height="30" Height="30"
VerticalAlignment="Center" VerticalAlignment="Center"
DataContext="{Binding DownloadLocation}" DataContext="{Binding DownloadLocation}"
FontSize="14" /> FontSize="14" />
<CheckBox
Grid.Row="4"
Grid.Column="2"
HorizontalAlignment="Right"
Content="Overwrite Installation"
IsChecked="{Binding AutomaticallyOverwrite}"
ToolTip="If installing over an existing installation, automatically replace it without asking permission.">
<CheckBox.Style>
<Style TargetType="CheckBox">
<Setter Property="Opacity" Value="0.6" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="Opacity" Value="1" />
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -9,6 +9,28 @@
d:DesignWidth="800" d:DesignWidth="800"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid> <Grid>
<!-- Nothing so far --> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="120" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Target Modlist"
TextAlignment="Center" />
<local:FilePicker
Grid.Row="0"
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding Parent.ModListLocation}"
FontSize="14" />
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -0,0 +1,58 @@
<UserControl
x:Class="Wabbajack.ConfirmUpdateOfExistingInstallView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="5*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="2*" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="3"
Margin="0,0,0,5"
FontFamily="Lucida Sans"
FontSize="14"
FontWeight="Bold"
Text="{Binding ActiveGlobalUserIntervention.ShortDescription}"
TextWrapping="WrapWithOverflow" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
Text="{Binding ActiveGlobalUserIntervention.ExtendedDescription}"
TextWrapping="WrapWithOverflow" />
<CheckBox
Grid.Row="2"
Grid.Column="2"
Margin="4"
HorizontalAlignment="Right"
Content="Remember"
IsChecked="{Binding Installer.AutomaticallyOverwrite}"
ToolTip="If installing over an existing installation next time, automatically replace it without asking permission." />
<Button
Grid.Row="3"
Grid.Column="0"
Command="{Binding ActiveGlobalUserIntervention.CancelCommand}"
Content="Cancel" />
<Button
Grid.Row="3"
Grid.Column="2"
Command="{Binding ActiveGlobalUserIntervention.ConfirmCommand}"
Content="Confirm" />
</Grid>
</UserControl>

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for ConfirmUpdateOfExistingInstallView.xaml
/// </summary>
public partial class ConfirmUpdateOfExistingInstallView : UserControl
{
public ConfirmUpdateOfExistingInstallView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,49 @@
<UserControl
x:Class="Wabbajack.ConfirmationInterventionView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="4*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="3"
Margin="0,0,0,5"
FontFamily="Lucida Sans"
FontSize="14"
FontWeight="Bold"
Text="{Binding ShortDescription}"
TextWrapping="WrapWithOverflow" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
Text="{Binding ExtendedDescription}"
TextWrapping="WrapWithOverflow" />
<Button
Grid.Row="2"
Grid.Column="0"
Command="{Binding CancelCommand}"
Content="Cancel" />
<Button
Grid.Row="2"
Grid.Column="2"
Command="{Binding ConfirmCommand}"
Content="Confirm" />
</Grid>
</UserControl>

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for ConfirmationInterventionView.xaml
/// </summary>
public partial class ConfirmationInterventionView : UserControl
{
public ConfirmationInterventionView()
{
InitializeComponent();
}
}
}

View File

@ -15,8 +15,6 @@ namespace Wabbajack
private MainWindowVM _mwvm; private MainWindowVM _mwvm;
private MainSettings _settings; private MainSettings _settings;
internal bool ExitWhenClosing = true;
public MainWindow() public MainWindow()
{ {
// Wire any unhandled crashing exceptions to log before exiting // Wire any unhandled crashing exceptions to log before exiting
@ -93,16 +91,7 @@ namespace Wabbajack
private void Window_Closing(object sender, CancelEventArgs e) private void Window_Closing(object sender, CancelEventArgs e)
{ {
_mwvm.Dispose(); _mwvm.ShutdownApplication();
_settings.PosX = Left;
_settings.PosY = Top;
_settings.Width = Width;
_settings.Height = Height;
MainSettings.SaveSettings(_settings);
if (ExitWhenClosing)
{
Application.Current.Shutdown();
}
} }
} }
} }

View File

@ -172,7 +172,21 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>
</ApplicationDefinition> </ApplicationDefinition>
<Compile Include="Converters\IsTypeVisibilityConverter.cs" />
<Compile Include="Views\Installers\InstallationCompleteView.xaml.cs">
<DependentUpon>InstallationCompleteView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\Interventions\ConfirmationInterventionView.xaml.cs">
<DependentUpon>ConfirmationInterventionView.xaml</DependentUpon>
</Compile>
<Compile Include="Converters\EqualsToBoolConverter.cs" /> <Compile Include="Converters\EqualsToBoolConverter.cs" />
<Compile Include="Views\Common\CpuView.xaml.cs">
<DependentUpon>CpuView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\Common\LogView.xaml.cs">
<DependentUpon>LogView.xaml</DependentUpon>
</Compile>
<Compile Include="View Models\UserInterventionHandlers.cs" />
<Compile Include="View Models\WebBrowserVM.cs" /> <Compile Include="View Models\WebBrowserVM.cs" />
<Compile Include="Views\Installers\MO2InstallerConfigView.xaml.cs"> <Compile Include="Views\Installers\MO2InstallerConfigView.xaml.cs">
<DependentUpon>MO2InstallerConfigView.xaml</DependentUpon> <DependentUpon>MO2InstallerConfigView.xaml</DependentUpon>
@ -185,6 +199,9 @@
<Compile Include="Views\Common\HeatedBackgroundView.xaml.cs"> <Compile Include="Views\Common\HeatedBackgroundView.xaml.cs">
<DependentUpon>HeatedBackgroundView.xaml</DependentUpon> <DependentUpon>HeatedBackgroundView.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Views\Interventions\ConfirmUpdateOfExistingInstallView.xaml.cs">
<DependentUpon>ConfirmUpdateOfExistingInstallView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\LinksView.xaml.cs"> <Compile Include="Views\LinksView.xaml.cs">
<DependentUpon>LinksView.xaml</DependentUpon> <DependentUpon>LinksView.xaml</DependentUpon>
</Compile> </Compile>
@ -233,9 +250,6 @@
</Compile> </Compile>
<Compile Include="View Models\Compilers\CompilerVM.cs" /> <Compile Include="View Models\Compilers\CompilerVM.cs" />
<Compile Include="View Models\MainWindowVM.cs" /> <Compile Include="View Models\MainWindowVM.cs" />
<Compile Include="Views\Common\LogCpuView.xaml.cs">
<DependentUpon>LogCpuView.xaml</DependentUpon>
</Compile>
<Compile Include="Converters\LeftMarginMultiplierConverter.cs" /> <Compile Include="Converters\LeftMarginMultiplierConverter.cs" />
<Compile Include="Util\TreeViewItemExtensions.cs" /> <Compile Include="Util\TreeViewItemExtensions.cs" />
<Compile Include="View Models\SlideShow.cs" /> <Compile Include="View Models\SlideShow.cs" />
@ -255,6 +269,22 @@
<Compile Include="Views\WebBrowserView.xaml.cs"> <Compile Include="Views\WebBrowserView.xaml.cs">
<DependentUpon>WebBrowserView.xaml</DependentUpon> <DependentUpon>WebBrowserView.xaml</DependentUpon>
</Compile> </Compile>
<Page Include="Views\Installers\InstallationCompleteView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\Interventions\ConfirmationInterventionView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\Common\CpuView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\Common\LogView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\Installers\MO2InstallerConfigView.xaml"> <Page Include="Views\Installers\MO2InstallerConfigView.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
@ -263,6 +293,10 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
<Page Include="Views\Interventions\ConfirmUpdateOfExistingInstallView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\LinksView.xaml"> <Page Include="Views\LinksView.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
@ -303,10 +337,6 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
<Page Include="Views\Common\LogCpuView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\MainWindow.xaml"> <Page Include="Views\MainWindow.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>
@ -433,7 +463,7 @@
<Version>4.1.0</Version> <Version>4.1.0</Version>
</PackageReference> </PackageReference>
<PackageReference Include="DynamicData"> <PackageReference Include="DynamicData">
<Version>6.13.21</Version> <Version>6.14.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Fody"> <PackageReference Include="Fody">
<Version>6.0.5</Version> <Version>6.0.5</Version>
@ -470,13 +500,13 @@
<Version>2.4.4</Version> <Version>2.4.4</Version>
</PackageReference> </PackageReference>
<PackageReference Include="ReactiveUI.Events.WPF"> <PackageReference Include="ReactiveUI.Events.WPF">
<Version>10.5.30</Version> <Version>11.0.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="ReactiveUI.Fody"> <PackageReference Include="ReactiveUI.Fody">
<Version>10.5.30</Version> <Version>11.0.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="ReactiveUI.WPF"> <PackageReference Include="ReactiveUI.WPF">
<Version>10.5.30</Version> <Version>11.0.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="SharpCompress"> <PackageReference Include="SharpCompress">
<Version>0.24.0</Version> <Version>0.24.0</Version>
@ -488,7 +518,7 @@
<Version>1.2.1</Version> <Version>1.2.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="System.Reactive"> <PackageReference Include="System.Reactive">
<Version>4.2.0</Version> <Version>4.3.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="WebSocketSharpFork"> <PackageReference Include="WebSocketSharpFork">
<Version>1.0.4</Version> <Version>1.0.4</Version>
@ -521,5 +551,8 @@
<ItemGroup> <ItemGroup>
<Resource Include="Resources\Wabba_Ded.png" /> <Resource Include="Resources\Wabba_Ded.png" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<SplashScreen Include="Resources\Wabba_Mouth_Small.png" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>