diff --git a/Branding/PNGs/Wabba_Ded.png b/Branding/PNGs/Wabba_Ded.png new file mode 100644 index 00000000..1e8eab47 Binary files /dev/null and b/Branding/PNGs/Wabba_Ded.png differ diff --git a/Branding/Project files/Wabba_Ded.psd b/Branding/Project files/Wabba_Ded.psd new file mode 100644 index 00000000..83e499f6 Binary files /dev/null and b/Branding/Project files/Wabba_Ded.psd differ diff --git a/Wabbajack.Common/Error States/ErrorResponse.cs b/Wabbajack.Common/Error States/ErrorResponse.cs index a9bfec46..66cee028 100644 --- a/Wabbajack.Common/Error States/ErrorResponse.cs +++ b/Wabbajack.Common/Error States/ErrorResponse.cs @@ -18,7 +18,14 @@ namespace Wabbajack { if (Exception != null) { - return Exception.ToString(); + if (string.IsNullOrWhiteSpace(_reason)) + { + return Exception.ToString(); + } + else + { + return $"{_reason}: {Exception.Message}"; + } } return _reason; } @@ -53,9 +60,9 @@ namespace Wabbajack return new ErrorResponse(true, reason); } - public static ErrorResponse Fail(string reason) + public static ErrorResponse Fail(string reason, Exception ex = null) { - return new ErrorResponse(false, reason: reason); + return new ErrorResponse(false, reason: reason, ex: ex); } public static ErrorResponse Fail(Exception ex) diff --git a/Wabbajack.Common/Utils.cs b/Wabbajack.Common/Utils.cs index 3c916f35..f984e6d6 100644 --- a/Wabbajack.Common/Utils.cs +++ b/Wabbajack.Common/Utils.cs @@ -794,7 +794,7 @@ namespace Wabbajack.Common { if (string.IsNullOrWhiteSpace(path)) { - return ErrorResponse.Fail("Path was empty."); + return ErrorResponse.Fail("Path is empty."); } try { @@ -819,7 +819,7 @@ namespace Wabbajack.Common { if (string.IsNullOrWhiteSpace(path)) { - return ErrorResponse.Fail("Path was empty"); + return ErrorResponse.Fail("Path is empty"); } try { diff --git a/Wabbajack.Lib/Downloaders/HTTPDownloader.cs b/Wabbajack.Lib/Downloaders/HTTPDownloader.cs index d38afb1d..1b3e3518 100644 --- a/Wabbajack.Lib/Downloaders/HTTPDownloader.cs +++ b/Wabbajack.Lib/Downloaders/HTTPDownloader.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -95,7 +96,6 @@ namespace Wabbajack.Lib.Downloaders { } - ; if (stream.IsFaulted || response.StatusCode != HttpStatusCode.OK) { Utils.Log($"While downloading {Url} - {stream.Exception.ExceptionToString()}"); @@ -111,6 +111,11 @@ namespace Wabbajack.Lib.Downloaders var contentSize = headerVar != null ? long.Parse(headerVar) : 1; + FileInfo fileInfo = new FileInfo(destination); + if (!fileInfo.Directory.Exists) + { + Directory.CreateDirectory(fileInfo.Directory.FullName); + } using (var webs = stream.Result) using (var fs = File.OpenWrite(destination)) diff --git a/Wabbajack/App.xaml.cs b/Wabbajack/App.xaml.cs index f8dc7a0b..a57a5ea2 100644 --- a/Wabbajack/App.xaml.cs +++ b/Wabbajack/App.xaml.cs @@ -14,19 +14,7 @@ namespace Wabbajack { public App() { - // Wire any unhandled crashing exceptions to log before exiting - AppDomain.CurrentDomain.UnhandledException += (sender, e) => - { - // Don't do any special logging side effects - Utils.Log("Uncaught error:"); - Utils.Log(((Exception)e.ExceptionObject).ExceptionToString()); - }; - - var appPath = Assembly.GetExecutingAssembly().Location; - if (!ExtensionManager.IsAssociated() || ExtensionManager.NeedsUpdating(appPath)) - { - ExtensionManager.Associate(appPath); - } + // Initialization in MainWindow ctor } } } diff --git a/Wabbajack/Resources/Wabba_Ded.png b/Wabbajack/Resources/Wabba_Ded.png new file mode 100644 index 00000000..1e8eab47 Binary files /dev/null and b/Wabbajack/Resources/Wabba_Ded.png differ diff --git a/Wabbajack/Settings.cs b/Wabbajack/Settings.cs index d4d6367d..282c959a 100644 --- a/Wabbajack/Settings.cs +++ b/Wabbajack/Settings.cs @@ -24,11 +24,15 @@ namespace Wabbajack private Subject _saveSignal = new Subject(); public IObservable SaveSignal => _saveSignal; - public static MainSettings LoadSettings() + public static bool TryLoadTypicalSettings(out MainSettings settings) { - string[] args = Environment.GetCommandLineArgs(); - if (!File.Exists(_filename) || args.Length > 1 && args[1] == "nosettings") return new MainSettings(); - return JsonConvert.DeserializeObject(File.ReadAllText(_filename)); + if (!File.Exists(_filename)) + { + settings = default; + return false; + } + settings = JsonConvert.DeserializeObject(File.ReadAllText(_filename)); + return true; } public static void SaveSettings(MainSettings settings) diff --git a/Wabbajack/View Models/FilePickerVM.cs b/Wabbajack/View Models/FilePickerVM.cs index f9c5a55f..33d010ee 100644 --- a/Wabbajack/View Models/FilePickerVM.cs +++ b/Wabbajack/View Models/FilePickerVM.cs @@ -190,10 +190,10 @@ namespace Wabbajack { Title = PromptTitle, IsFolderPicker = PathType == PathTypeOptions.Folder, - InitialDirectory = TargetPath, + InitialDirectory = dirPath, AddToMostRecentlyUsedList = false, AllowNonFileSystemItems = false, - DefaultDirectory = TargetPath, + DefaultDirectory = dirPath, EnsureFileExists = true, EnsurePathExists = true, EnsureReadOnly = false, diff --git a/Wabbajack/View Models/Installers/InstallerVM.cs b/Wabbajack/View Models/Installers/InstallerVM.cs index a4b2bad7..5cfaec6e 100644 --- a/Wabbajack/View Models/Installers/InstallerVM.cs +++ b/Wabbajack/View Models/Installers/InstallerVM.cs @@ -1,4 +1,4 @@ -using Syroot.Windows.IO; +using Syroot.Windows.IO; using System; using ReactiveUI; using System.Diagnostics; @@ -28,11 +28,12 @@ namespace Wabbajack public MainWindowVM MWVM { get; } public BitmapImage WabbajackLogo { get; } = UIUtils.BitmapImageFromStream(Application.GetResourceStream(new Uri("pack://application:,,,/Wabbajack;component/Resources/Wabba_Mouth_No_Text.png")).Stream); + public BitmapImage WabbajackErrLogo { get; } = UIUtils.BitmapImageFromStream(Application.GetResourceStream(new Uri("pack://application:,,,/Wabbajack;component/Resources/Wabba_Ded.png")).Stream); private readonly ObservableAsPropertyHelper _modList; public ModListVM ModList => _modList.Value; - public FilePickerVM ModListPath { get; } + public FilePickerVM ModListLocation { get; } private readonly ObservableAsPropertyHelper _installer; public ISubInstallerVM Installer => _installer.Value; @@ -97,7 +98,7 @@ namespace Wabbajack MWVM = mainWindowVM; - ModListPath = new FilePickerVM() + ModListLocation = new FilePickerVM() { ExistCheckOption = FilePickerVM.ExistCheckOptions.On, PathType = FilePickerVM.PathTypeOptions.File, @@ -133,19 +134,17 @@ namespace Wabbajack MWVM.Settings.SaveSignal .Subscribe(_ => { - MWVM.Settings.Installer.LastInstalledListLocation = ModListPath.TargetPath; + MWVM.Settings.Installer.LastInstalledListLocation = ModListLocation.TargetPath; }) .DisposeWith(CompositeDisposable); - _modList = this.WhenAny(x => x.ModListPath.TargetPath) + _modList = this.WhenAny(x => x.ModListLocation.TargetPath) .ObserveOn(RxApp.TaskpoolScheduler) .Select(modListPath => { if (modListPath == null) return default(ModListVM); if (!File.Exists(modListPath)) return default(ModListVM); - var modList = AInstaller.LoadFromFile(modListPath); - if (modList == null) return default(ModListVM); - return new ModListVM(modList, modListPath); + return new ModListVM(modListPath); }) .ObserveOnGuiThread() .StartWith(default(ModListVM)) @@ -161,6 +160,15 @@ namespace Wabbajack .Select(modList => modList?.ModManager) .ToProperty(this, nameof(TargetManager)); + // Add additional error check on modlist + ModListLocation.AdditionalError = this.WhenAny(x => x.ModList) + .Select(modList => + { + if (modList == null) return ErrorResponse.Fail("Modlist path resulted in a null object."); + if (modList.Error != null) return ErrorResponse.Fail("Modlist is corrupt", modList.Error); + return ErrorResponse.Success; + }); + BackCommand = ReactiveCommand.Create( execute: () => mainWindowVM.ActivePane = mainWindowVM.ModeSelectionVM, canExecute: this.WhenAny(x => x.Installing) @@ -186,6 +194,7 @@ namespace Wabbajack // Set display items to modlist if configuring or complete, // or to the current slideshow data if installing _image = Observable.CombineLatest( + this.WhenAny(x => x.ModList.Error), this.WhenAny(x => x.ModList) .SelectMany(x => x?.ImageObservable ?? Observable.Empty()) .NotNull() @@ -193,7 +202,14 @@ namespace Wabbajack this.WhenAny(x => x.Slideshow.Image) .StartWith(default(BitmapImage)), this.WhenAny(x => x.Installing), - resultSelector: (modList, slideshow, installing) => installing ? slideshow : modList) + resultSelector: (err, modList, slideshow, installing) => + { + if (err != null) + { + return WabbajackErrLogo; + } + return installing ? slideshow : modList; + }) .Select(x => x) .ToProperty(this, nameof(Image)); _titleText = Observable.CombineLatest( @@ -217,8 +233,16 @@ namespace Wabbajack this.WhenAny(x => x.Installing), resultSelector: (modList, mod, installing) => installing ? mod : modList) .ToProperty(this, nameof(Description)); - _modListName = this.WhenAny(x => x.ModList) - .Select(x => x?.Name) + _modListName = Observable.CombineLatest( + this.WhenAny(x => x.ModList.Error) + .Select(x => x != null), + this.WhenAny(x => x.ModList) + .Select(x => x?.Name), + resultSelector: (err, name) => + { + if (err) return "Corrupted Modlist"; + return name; + }) .ToProperty(this, nameof(ModListName)); // Define commands @@ -279,7 +303,7 @@ namespace Wabbajack private void OpenReadmeWindow() { if (string.IsNullOrEmpty(ModList.Readme)) return; - using (var fs = new FileStream(ModListPath.TargetPath, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var fs = new FileStream(ModListLocation.TargetPath, FileMode.Open, FileAccess.Read, FileShare.Read)) using (var ar = new ZipArchive(fs, ZipArchiveMode.Read)) using (var ms = new MemoryStream()) { diff --git a/Wabbajack/View Models/Installers/MO2InstallerVM.cs b/Wabbajack/View Models/Installers/MO2InstallerVM.cs index 167c87c1..997cd60c 100644 --- a/Wabbajack/View Models/Installers/MO2InstallerVM.cs +++ b/Wabbajack/View Models/Installers/MO2InstallerVM.cs @@ -54,9 +54,10 @@ namespace Wabbajack canExecute: Observable.CombineLatest( this.WhenAny(x => x.Location.InError), this.WhenAny(x => x.DownloadLocation.InError), - resultSelector: (loc, download) => + installerVM.WhenAny(x => x.ModListLocation.InError), + resultSelector: (loc, modlist, download) => { - return !loc && !download; + return !loc && !download && !modlist; }) .ObserveOnGuiThread(), execute: async () => @@ -66,7 +67,7 @@ namespace Wabbajack try { installer = new MO2Installer( - archive: installerVM.ModListPath.TargetPath, + archive: installerVM.ModListLocation.TargetPath, modList: installerVM.ModList.SourceModList, outputFolder: Location.TargetPath, downloadFolder: DownloadLocation.TargetPath); @@ -119,7 +120,7 @@ namespace Wabbajack .DisposeWith(CompositeDisposable); // Load settings - _CurrentSettings = installerVM.WhenAny(x => x.ModListPath.TargetPath) + _CurrentSettings = installerVM.WhenAny(x => x.ModListLocation.TargetPath) .Select(path => path == null ? null : installerVM.MWVM.Settings.Installer.Mo2ModlistSettings.TryCreate(path)) .ToProperty(this, nameof(CurrentSettings)); this.WhenAny(x => x.CurrentSettings) @@ -144,7 +145,7 @@ namespace Wabbajack private void SaveSettings(Mo2ModlistInstallationSettings settings) { - _installerVM.MWVM.Settings.Installer.LastInstalledListLocation = _installerVM.ModListPath.TargetPath; + _installerVM.MWVM.Settings.Installer.LastInstalledListLocation = _installerVM.ModListLocation.TargetPath; if (settings == null) return; settings.InstallationLocation = Location.TargetPath; settings.DownloadLocation = DownloadLocation.TargetPath; diff --git a/Wabbajack/View Models/Installers/VortexInstallerVM.cs b/Wabbajack/View Models/Installers/VortexInstallerVM.cs index dccac18a..2372fa82 100644 --- a/Wabbajack/View Models/Installers/VortexInstallerVM.cs +++ b/Wabbajack/View Models/Installers/VortexInstallerVM.cs @@ -33,8 +33,11 @@ namespace Wabbajack .ToProperty(this, nameof(TargetGame)); BeginCommand = ReactiveCommand.CreateFromTask( - canExecute: this.WhenAny(x => x.TargetGame) - .Select(game => VortexCompiler.IsActiveVortexGame(game)), + canExecute: Observable.CombineLatest( + this.WhenAny(x => x.TargetGame) + .Select(game => VortexCompiler.IsActiveVortexGame(game)), + installerVM.WhenAny(x => x.ModListLocation.InError), + resultSelector: (isVortexGame, modListErr) => isVortexGame && !modListErr), execute: async () => { AInstaller installer; @@ -44,7 +47,7 @@ namespace Wabbajack var download = VortexCompiler.RetrieveDownloadLocation(TargetGame); var staging = VortexCompiler.RetrieveStagingLocation(TargetGame); installer = new VortexInstaller( - archive: installerVM.ModListPath.TargetPath, + archive: installerVM.ModListLocation.TargetPath, modList: installerVM.ModList.SourceModList, outputFolder: staging, downloadFolder: download); diff --git a/Wabbajack/View Models/MainWindowVM.cs b/Wabbajack/View Models/MainWindowVM.cs index 4b142a2b..76faa530 100644 --- a/Wabbajack/View Models/MainWindowVM.cs +++ b/Wabbajack/View Models/MainWindowVM.cs @@ -54,7 +54,7 @@ namespace Wabbajack if (IsStartingFromModlist(out var path)) { - Installer.Value.ModListPath.TargetPath = path; + Installer.Value.ModListLocation.TargetPath = path; ActivePane = Installer.Value; } else @@ -83,7 +83,7 @@ namespace Wabbajack var installer = Installer.Value; Settings.Installer.LastInstalledListLocation = path; ActivePane = installer; - installer.ModListPath.TargetPath = path; + installer.ModListLocation.TargetPath = path; } } } diff --git a/Wabbajack/View Models/ModListVM.cs b/Wabbajack/View Models/ModListVM.cs index f9c01bd1..cb235043 100644 --- a/Wabbajack/View Models/ModListVM.cs +++ b/Wabbajack/View Models/ModListVM.cs @@ -12,25 +12,33 @@ namespace Wabbajack public class ModListVM : ViewModel { public ModList SourceModList { get; } + public Exception Error { get; } public string ModListPath { get; } - public string Name => SourceModList.Name; - public string ReportHTML => SourceModList.ReportHTML; - public string Readme => SourceModList.Readme; - public string ImageURL => SourceModList.Image; - public string Author => SourceModList.Author; - public string Description => SourceModList.Description; - public string Website => SourceModList.Website; - public ModManager ModManager => SourceModList.ModManager; + public string Name => SourceModList?.Name; + public string ReportHTML => SourceModList?.ReportHTML; + public string Readme => SourceModList?.Readme; + public string ImageURL => SourceModList?.Image; + public string Author => SourceModList?.Author; + public string Description => SourceModList?.Description; + public string Website => SourceModList?.Website; + public ModManager ModManager => SourceModList?.ModManager ?? ModManager.MO2; // Image isn't exposed as a direct property, but as an observable. // This acts as a caching mechanism, as interested parties will trigger it to be created, // and the cached image will automatically be released when the last interested party is gone. public IObservable ImageObservable { get; } - public ModListVM(ModList sourceModList, string modListPath) + public ModListVM(string modListPath) { ModListPath = modListPath; - SourceModList = sourceModList; + try + { + SourceModList = AInstaller.LoadFromFile(modListPath); + } + catch (Exception ex) + { + Error = ex; + } ImageObservable = Observable.Return(ImageURL) .ObserveOn(RxApp.TaskpoolScheduler) @@ -39,13 +47,13 @@ namespace Wabbajack try { if (!File.Exists(url)) return default(MemoryStream); - if (string.IsNullOrWhiteSpace(sourceModList.Image)) return default(MemoryStream); - if (sourceModList.Image.Length != 36) return default(MemoryStream); + if (string.IsNullOrWhiteSpace(ImageURL)) return default(MemoryStream); + if (ImageURL.Length != 36) return default(MemoryStream); using (var fs = new FileStream(ModListPath, FileMode.Open, FileAccess.Read, FileShare.Read)) using (var ar = new ZipArchive(fs, ZipArchiveMode.Read)) { var ms = new MemoryStream(); - var entry = ar.GetEntry(sourceModList.Image); + var entry = ar.GetEntry(ImageURL); using (var e = entry.Open()) { e.CopyTo(ms); diff --git a/Wabbajack/View Models/SlideShow.cs b/Wabbajack/View Models/SlideShow.cs index adb671b7..4a838259 100644 --- a/Wabbajack/View Models/SlideShow.cs +++ b/Wabbajack/View Models/SlideShow.cs @@ -1,4 +1,4 @@ -using DynamicData; +using DynamicData; using ReactiveUI; using ReactiveUI.Fody.Helpers; using System; @@ -74,7 +74,7 @@ namespace Wabbajack // Whenever modlist changes, grab the list of its slides .Select(modList => { - if (modList == null) + if (modList?.SourceModList?.Archives == null) { return Observable.Empty() .ToObservableChangeSet(x => x.ModID); diff --git a/Wabbajack/Views/Common/TopProgressView.xaml b/Wabbajack/Views/Common/TopProgressView.xaml index 02bb821f..e83cd94b 100644 --- a/Wabbajack/Views/Common/TopProgressView.xaml +++ b/Wabbajack/Views/Common/TopProgressView.xaml @@ -65,7 +65,7 @@ x:Name="LargeProgressBar" Grid.Column="0" Grid.ColumnSpan="4" - Background="Transparent" + Background="#88121212" BorderThickness="0" Maximum="1" Opacity="{Binding ProgressOpacityPercent, Mode=OneWay, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" diff --git a/Wabbajack/Views/Installers/InstallationView.xaml b/Wabbajack/Views/Installers/InstallationView.xaml index e03b4aec..a7aa4066 100644 --- a/Wabbajack/Views/Installers/InstallationView.xaml +++ b/Wabbajack/Views/Installers/InstallationView.xaml @@ -38,8 +38,8 @@ + { + // Don't do any special logging side effects + Wabbajack.Common.Utils.Log("Uncaught error:"); + Wabbajack.Common.Utils.Log(((Exception)e.ExceptionObject).ExceptionToString()); + }; + + var appPath = System.Reflection.Assembly.GetExecutingAssembly().Location; + if (!ExtensionManager.IsAssociated() || ExtensionManager.NeedsUpdating(appPath)) + { + ExtensionManager.Associate(appPath); + } + Wabbajack.Common.Utils.Log($"Wabbajack Build - {ThisAssembly.Git.Sha}"); - this.Loaded += (sender, e) => + // Load settings + string[] args = Environment.GetCommandLineArgs(); + if ((args.Length > 1 && args[1] == "nosettings") + || !MainSettings.TryLoadTypicalSettings(out var settings)) { - Width = _settings.Width; - Height = _settings.Height; - }; + _settings = new MainSettings(); + RunWhenLoaded(DefaultSettings); + } + else + { + _settings = settings; + RunWhenLoaded(LoadSettings); + } + + // Set datacontext + _mwvm = new MainWindowVM(this, _settings); + DataContext = _mwvm; } - internal bool ExitWhenClosing = true; + public void Init(MainWindowVM vm, MainSettings settings) + { + DataContext = vm; + _mwvm = vm; + _settings = settings; + } + + private void RunWhenLoaded(Action a) + { + if (IsLoaded) + { + a(); + } + else + { + this.Loaded += (sender, e) => + { + a(); + }; + } + } + + private void LoadSettings() + { + Width = _settings.Width; + Height = _settings.Height; + Left = _settings.PosX; + Top = _settings.PosY; + } + + private void DefaultSettings() + { + Width = 1300; + Height = 960; + Left = 15; + Top = 15; + } private void Window_Closing(object sender, CancelEventArgs e) { diff --git a/Wabbajack/Wabbajack.csproj b/Wabbajack/Wabbajack.csproj index afe4918e..d67b08f0 100644 --- a/Wabbajack/Wabbajack.csproj +++ b/Wabbajack/Wabbajack.csproj @@ -510,5 +510,8 @@ + + + \ No newline at end of file