mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge pull request #2011 from wabbajack-tools/compiler-ui-errors
Compiler UI errors
This commit is contained in:
commit
e0702d9f17
@ -12,6 +12,8 @@ public static class Consts
|
||||
public static Version CurrentMinimumWabbajackVersion { get; set; } = Version.Parse("2.3.0.0");
|
||||
public static bool UseNetworkWorkaroundMode { get; set; } = false;
|
||||
public static AbsolutePath CefCacheLocation { get; } = KnownFolders.WabbajackAppLocal.Combine("Cef");
|
||||
public static RelativePath ModListTxt { get; } = "modlist.txt".ToRelativePath();
|
||||
public static RelativePath CompilerSettings { get; } = "compiler_settings.json".ToRelativePath();
|
||||
|
||||
public static byte SettingsVersion = 0;
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
@ -95,6 +97,13 @@ namespace Wabbajack
|
||||
}
|
||||
return ErrorResponse.Success;
|
||||
}
|
||||
|
||||
public static ErrorResponse Combine(List<ErrorResponse> errors)
|
||||
{
|
||||
if (errors.All(e => e.Succeeded) || !errors.Any())
|
||||
return Success;
|
||||
return Fail(string.Join("\n", errors.Where(e => e.Failed).Select(e => e.Reason)));
|
||||
}
|
||||
}
|
||||
|
||||
public interface IErrorResponse
|
||||
|
@ -1,14 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Wabbajack.Extensions;
|
||||
using Wabbajack.Interventions;
|
||||
using Wabbajack.Messages;
|
||||
using Wabbajack.RateLimiter;
|
||||
using ReactiveUI;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
@ -17,24 +14,17 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
using DynamicData;
|
||||
using DynamicData.Binding;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||
using ReactiveUI.Fody.Helpers;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Compiler;
|
||||
using Wabbajack.Downloaders;
|
||||
using Wabbajack.Downloaders.GameFile;
|
||||
using Wabbajack.DTOs;
|
||||
using Wabbajack.DTOs.Interventions;
|
||||
using Wabbajack.DTOs.JsonConverters;
|
||||
using Wabbajack.Installer;
|
||||
using Wabbajack.Models;
|
||||
using Wabbajack.Networking.WabbajackClientApi;
|
||||
using Wabbajack.Paths;
|
||||
using Wabbajack.Paths.IO;
|
||||
using Wabbajack.Services.OSIntegrated;
|
||||
using Wabbajack.VFS;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
@ -56,6 +46,7 @@ namespace Wabbajack
|
||||
private readonly ILogger<CompilerVM> _logger;
|
||||
private readonly ResourceMonitor _resourceMonitor;
|
||||
private readonly CompilerSettingsInferencer _inferencer;
|
||||
private readonly Client _wjClient;
|
||||
|
||||
[Reactive]
|
||||
public CompilerState State { get; set; }
|
||||
@ -101,9 +92,12 @@ namespace Wabbajack
|
||||
public LogStream LoggerProvider { get; }
|
||||
public ReadOnlyObservableCollection<CPUDisplayVM> StatusList => _resourceMonitor.Tasks;
|
||||
|
||||
[Reactive]
|
||||
public ErrorResponse ErrorState { get; private set; }
|
||||
|
||||
public CompilerVM(ILogger<CompilerVM> logger, DTOSerializer dtos, SettingsManager settingsManager,
|
||||
IServiceProvider serviceProvider, LogStream loggerProvider, ResourceMonitor resourceMonitor,
|
||||
CompilerSettingsInferencer inferencer) : base(logger)
|
||||
CompilerSettingsInferencer inferencer, Client wjClient) : base(logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_dtos = dtos;
|
||||
@ -112,6 +106,7 @@ namespace Wabbajack
|
||||
LoggerProvider = loggerProvider;
|
||||
_resourceMonitor = resourceMonitor;
|
||||
_inferencer = inferencer;
|
||||
_wjClient = wjClient;
|
||||
|
||||
BackCommand =
|
||||
ReactiveCommand.CreateFromTask(async () =>
|
||||
@ -124,26 +119,27 @@ namespace Wabbajack
|
||||
|
||||
ExecuteCommand = ReactiveCommand.CreateFromTask(async () => await StartCompilation());
|
||||
|
||||
ModlistLocation = new FilePickerVM()
|
||||
ModlistLocation = new FilePickerVM
|
||||
{
|
||||
ExistCheckOption = FilePickerVM.CheckOptions.On,
|
||||
PathType = FilePickerVM.PathTypeOptions.File,
|
||||
PromptTitle = "Select a config file or a modlist.txt file"
|
||||
};
|
||||
|
||||
DownloadLocation = new FilePickerVM()
|
||||
DownloadLocation = new FilePickerVM
|
||||
{
|
||||
ExistCheckOption = FilePickerVM.CheckOptions.On,
|
||||
PathType = FilePickerVM.PathTypeOptions.Folder,
|
||||
PromptTitle = "Location where the downloads for this list are stored"
|
||||
};
|
||||
|
||||
OutputLocation = new FilePickerVM()
|
||||
OutputLocation = new FilePickerVM
|
||||
{
|
||||
ExistCheckOption = FilePickerVM.CheckOptions.On,
|
||||
PathType = FilePickerVM.PathTypeOptions.Folder,
|
||||
ExistCheckOption = FilePickerVM.CheckOptions.Off,
|
||||
PathType = FilePickerVM.PathTypeOptions.File,
|
||||
PromptTitle = "Location where the compiled modlist will be stored"
|
||||
};
|
||||
OutputLocation.Filters.Add(new CommonFileDialogFilter(".wabbajack", "*.wabbajack"));
|
||||
|
||||
ModlistLocation.Filters.AddRange(new []
|
||||
{
|
||||
@ -160,19 +156,53 @@ namespace Wabbajack
|
||||
ModlistLocation.WhenAnyValue(vm => vm.TargetPath)
|
||||
.Subscribe(p => InferModListFromLocation(p).FireAndForget())
|
||||
.DisposeWith(disposables);
|
||||
|
||||
|
||||
this.WhenAnyValue(x => x.DownloadLocation.TargetPath)
|
||||
.CombineLatest(this.WhenAnyValue(x => x.ModlistLocation.TargetPath),
|
||||
this.WhenAnyValue(x => x.OutputLocation.TargetPath),
|
||||
this.WhenAnyValue(x => x.DownloadLocation.ErrorState),
|
||||
this.WhenAnyValue(x => x.ModlistLocation.ErrorState),
|
||||
this.WhenAnyValue(x => x.OutputLocation.ErrorState),
|
||||
this.WhenAnyValue(x => x.ModListName),
|
||||
this.WhenAnyValue(x => x.Version))
|
||||
.Select(_ => Validate())
|
||||
.BindToStrict(this, vm => vm.ErrorState)
|
||||
.DisposeWith(disposables);
|
||||
|
||||
LoadLastSavedSettings().FireAndForget();
|
||||
});
|
||||
}
|
||||
|
||||
private ErrorResponse Validate()
|
||||
{
|
||||
var errors = new List<ErrorResponse>();
|
||||
errors.Add(DownloadLocation.ErrorState);
|
||||
errors.Add(ModlistLocation.ErrorState);
|
||||
errors.Add(OutputLocation.ErrorState);
|
||||
return ErrorResponse.Combine(errors);
|
||||
}
|
||||
|
||||
private async Task InferModListFromLocation(AbsolutePath path)
|
||||
{
|
||||
using var _ = LoadingLock.WithLoading();
|
||||
if (path == default || path.FileName != "modlist.txt".ToRelativePath())
|
||||
|
||||
CompilerSettings settings;
|
||||
if (path == default) return;
|
||||
if (path.FileName.Extension == Ext.CompilerSettings)
|
||||
{
|
||||
await using var fs = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
settings = (await _dtos.DeserializeAsync<CompilerSettings>(fs))!;
|
||||
}
|
||||
else if (path.FileName == "modlist.txt".ToRelativePath())
|
||||
{
|
||||
settings = await _inferencer.InferModListFromLocation(path);
|
||||
if (settings == null) return;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
|
||||
var settings = await _inferencer.InferModListFromLocation(path);
|
||||
if (settings == null) return;
|
||||
}
|
||||
|
||||
BaseGame = settings.Game;
|
||||
ModListName = settings.ModListName;
|
||||
@ -180,6 +210,8 @@ namespace Wabbajack
|
||||
DownloadLocation.TargetPath = settings.Downloads;
|
||||
OutputLocation.TargetPath = settings.OutputFile;
|
||||
SelectedProfile = settings.Profile;
|
||||
PublishUpdate = settings.PublishUpdate;
|
||||
MachineUrl = settings.MachineUrl;
|
||||
OtherProfiles = settings.AdditionalProfiles;
|
||||
AlwaysEnabled = settings.AlwaysEnabled;
|
||||
NoMatchInclude = settings.NoMatchInclude;
|
||||
@ -192,27 +224,31 @@ namespace Wabbajack
|
||||
{
|
||||
try
|
||||
{
|
||||
await SaveSettingsFile();
|
||||
var token = CancellationToken.None;
|
||||
State = CompilerState.Compiling;
|
||||
|
||||
var mo2Settings = new CompilerSettings
|
||||
var mo2Settings = GetSettings();
|
||||
mo2Settings.UseGamePaths = true;
|
||||
|
||||
if (PublishUpdate && !await RunPreflightChecks(token))
|
||||
{
|
||||
Game = BaseGame,
|
||||
ModListName = ModListName,
|
||||
ModListAuthor = Author,
|
||||
ModlistReadme = Readme,
|
||||
Source = Source,
|
||||
Downloads = DownloadLocation.TargetPath,
|
||||
OutputFile = OutputLocation.TargetPath,
|
||||
Profile = SelectedProfile,
|
||||
AdditionalProfiles = OtherProfiles,
|
||||
AlwaysEnabled = AlwaysEnabled,
|
||||
NoMatchInclude = NoMatchInclude,
|
||||
UseGamePaths = true
|
||||
};
|
||||
State = CompilerState.Errored;
|
||||
return;
|
||||
}
|
||||
|
||||
var compiler = MO2Compiler.Create(_serviceProvider, mo2Settings);
|
||||
|
||||
await compiler.Begin(CancellationToken.None);
|
||||
await compiler.Begin(token);
|
||||
|
||||
if (PublishUpdate)
|
||||
{
|
||||
_logger.LogInformation("Publishing List");
|
||||
var downloadMetadata = _dtos.Deserialize<DownloadMetadata>(
|
||||
await mo2Settings.OutputFile.WithExtension(Ext.Meta).WithExtension(Ext.Json).ReadAllTextAsync())!;
|
||||
await _wjClient.PublishModlist(MachineUrl, System.Version.Parse(Version), mo2Settings.OutputFile, downloadMetadata);
|
||||
}
|
||||
_logger.LogInformation("Compiler Finished");
|
||||
|
||||
State = CompilerState.Completed;
|
||||
}
|
||||
@ -225,21 +261,39 @@ namespace Wabbajack
|
||||
|
||||
await tsk;
|
||||
}
|
||||
|
||||
|
||||
private async Task<bool> RunPreflightChecks(CancellationToken token)
|
||||
{
|
||||
var lists = await _wjClient.GetMyModlists(token);
|
||||
if (!lists.Any(x => x.Equals(MachineUrl, StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
_logger.LogError("Preflight Check failed, list {MachineUrl} not found in any repository", MachineUrl);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!System.Version.TryParse(Version, out var v))
|
||||
{
|
||||
_logger.LogError("Bad Version Number {Version}", Version);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task SaveSettingsFile()
|
||||
{
|
||||
if (Source == default) return;
|
||||
await using var st = SettingsOutputLocation.Open(FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
await JsonSerializer.SerializeAsync(st, GetSettings(), _dtos.Options);
|
||||
|
||||
await _settingsManager.Save(LastSavedCompilerSettings, Source);
|
||||
await _settingsManager.Save(LastSavedCompilerSettings, SettingsOutputLocation);
|
||||
}
|
||||
|
||||
private async Task LoadLastSavedSettings()
|
||||
{
|
||||
var lastPath = await _settingsManager.Load<AbsolutePath>(LastSavedCompilerSettings);
|
||||
if (Source == default) return;
|
||||
Source = lastPath;
|
||||
if (lastPath == default || !lastPath.FileExists() || lastPath.FileName.Extension != Ext.CompilerSettings) return;
|
||||
ModlistLocation.TargetPath = lastPath;
|
||||
}
|
||||
|
||||
|
||||
@ -250,11 +304,13 @@ namespace Wabbajack
|
||||
ModListName = ModListName,
|
||||
ModListAuthor = Author,
|
||||
Downloads = DownloadLocation.TargetPath,
|
||||
Source = ModlistLocation.TargetPath,
|
||||
Source = Source,
|
||||
Game = BaseGame,
|
||||
PublishUpdate = PublishUpdate,
|
||||
MachineUrl = MachineUrl,
|
||||
Profile = SelectedProfile,
|
||||
UseGamePaths = true,
|
||||
OutputFile = OutputLocation.TargetPath.Combine(SelectedProfile).WithExtension(Ext.Wabbajack),
|
||||
OutputFile = OutputLocation.TargetPath,
|
||||
AlwaysEnabled = AlwaysEnabled,
|
||||
AdditionalProfiles = OtherProfiles,
|
||||
NoMatchInclude = NoMatchInclude,
|
||||
|
@ -46,7 +46,7 @@ namespace Wabbajack.View_Models.Settings
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var isAuthor = string.IsNullOrWhiteSpace((await _token.Get())?.AuthorKey);
|
||||
var isAuthor = !string.IsNullOrWhiteSpace((await _token.Get())?.AuthorKey);
|
||||
IsVisible = isAuthor ? Visibility.Visible : Visibility.Collapsed;
|
||||
});
|
||||
|
||||
@ -65,7 +65,7 @@ namespace Wabbajack.View_Models.Settings
|
||||
_isUploading.OnNext(true);
|
||||
try
|
||||
{
|
||||
var (progress, task) = _wjClient.UploadAuthorFile(Picker.TargetPath);
|
||||
var (progress, task) = await _wjClient.UploadAuthorFile(Picker.TargetPath);
|
||||
|
||||
var disposable = progress.Subscribe(m =>
|
||||
{
|
||||
|
@ -75,7 +75,8 @@
|
||||
<TextBlock Grid.Row="1"
|
||||
x:Name="ActionText"
|
||||
Margin="0,10,0,0"
|
||||
HorizontalAlignment="Center" />
|
||||
HorizontalAlignment="Center"
|
||||
Text="Output Folder"/>
|
||||
</Grid>
|
||||
<Grid Grid.Row="1" Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
|
@ -213,6 +213,23 @@
|
||||
<local:MO2CompilerConfigView x:Name="CompilerConfigView" Grid.Row="1" Grid.Column="1" />
|
||||
<local:BeginButton Grid.Row="0" Grid.RowSpan="3" Grid.Column="5"
|
||||
x:Name="BeginButton" />
|
||||
<icon:PackIconFontAwesome Grid.Row="2"
|
||||
Grid.Column="5"
|
||||
x:Name="ErrorSummaryIconGlow"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{StaticResource WarningBrush}"
|
||||
Kind="ExclamationTriangleSolid"
|
||||
Opacity="0.6">
|
||||
<icon:PackIconFontAwesome.Effect>
|
||||
<BlurEffect Radius="15" />
|
||||
</icon:PackIconFontAwesome.Effect>
|
||||
</icon:PackIconFontAwesome>
|
||||
<icon:PackIconFontAwesome Grid.Row="2"
|
||||
Grid.Column="5"
|
||||
x:Name="ErrorSummaryIcon"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{StaticResource WarningBrush}"
|
||||
Kind="ExclamationTriangleSolid" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="5"
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.Eventing.Reader;
|
||||
using System;
|
||||
using System.Diagnostics.Eventing.Reader;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
@ -27,6 +28,30 @@ namespace Wabbajack
|
||||
|
||||
this.WhenActivated(disposables =>
|
||||
{
|
||||
ViewModel.WhenAny(vm => vm.State)
|
||||
.Select(x => x == CompilerState.Errored)
|
||||
.BindToStrict(this, x => x.CompilationComplete.AttentionBorder.Failure)
|
||||
.DisposeWith(disposables);
|
||||
|
||||
ViewModel.WhenAny(vm => vm.State)
|
||||
.Select(x => x == CompilerState.Errored)
|
||||
.Select(failed => $"Compilation {(failed ? "Failed" : "Complete")}")
|
||||
.BindToStrict(this, x => x.CompilationComplete.TitleText.Text)
|
||||
.DisposeWith(disposables);
|
||||
|
||||
CompilationComplete.GoToModlistButton.Command = ReactiveCommand.Create(() =>
|
||||
{
|
||||
UIUtils.OpenFolder(ViewModel.OutputLocation.TargetPath.Parent);
|
||||
}).DisposeWith(disposables);
|
||||
|
||||
ViewModel.WhenAnyValue(vm => vm.BackCommand)
|
||||
.BindToStrict(this, view => view.CompilationComplete.BackButton.Command)
|
||||
.DisposeWith(disposables);
|
||||
|
||||
CompilationComplete.CloseWhenCompletedButton.Command = ReactiveCommand.Create(() =>
|
||||
{
|
||||
Environment.Exit(0);
|
||||
}).DisposeWith(disposables);
|
||||
|
||||
|
||||
ViewModel.WhenAnyValue(vm => vm.ExecuteCommand)
|
||||
@ -53,10 +78,10 @@ namespace Wabbajack
|
||||
.DisposeWith(disposables);
|
||||
|
||||
ViewModel.WhenAnyValue(vm => vm.State)
|
||||
.Select(v => v == CompilerState.Completed ? Visibility.Visible : Visibility.Collapsed)
|
||||
.Select(v => v is CompilerState.Completed or CompilerState.Errored ? Visibility.Visible : Visibility.Collapsed)
|
||||
.BindToStrict(this, view => view.CompilationComplete.Visibility)
|
||||
.DisposeWith(disposables);
|
||||
|
||||
|
||||
ViewModel.WhenAnyValue(vm => vm.ModlistLocation)
|
||||
.BindToStrict(this, view => view.CompilerConfigView.ModListLocation.PickerVM)
|
||||
.DisposeWith(disposables);
|
||||
@ -71,6 +96,30 @@ namespace Wabbajack
|
||||
|
||||
UserInterventionsControl.Visibility = Visibility.Collapsed;
|
||||
|
||||
// Errors
|
||||
this.WhenAnyValue(view => view.ViewModel.ErrorState)
|
||||
.Select(x => !x.Failed)
|
||||
.BindToStrict(this, view => view.BeginButton.IsEnabled)
|
||||
.DisposeWith(disposables);
|
||||
|
||||
this.WhenAnyValue(view => view.ViewModel.ErrorState)
|
||||
.Select(x => x.Failed ? Visibility.Visible : Visibility.Hidden)
|
||||
.BindToStrict(this, view => view.ErrorSummaryIcon.Visibility)
|
||||
.DisposeWith(disposables);
|
||||
|
||||
this.WhenAnyValue(view => view.ViewModel.ErrorState)
|
||||
.Select(x => x.Failed ? Visibility.Visible : Visibility.Hidden)
|
||||
.BindToStrict(this, view => view.ErrorSummaryIconGlow.Visibility)
|
||||
.DisposeWith(disposables);
|
||||
|
||||
this.WhenAnyValue(view => view.ViewModel.ErrorState)
|
||||
.Select(x => x.Reason)
|
||||
.BindToStrict(this, view => view.ErrorSummaryIcon.ToolTip)
|
||||
.DisposeWith(disposables);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Settings
|
||||
|
||||
|
@ -34,6 +34,8 @@ namespace Wabbajack
|
||||
.DisposeWith(disposable);
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.Performance, x => x.PerformanceView.ViewModel)
|
||||
.DisposeWith(disposable);
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.AuthorFile, x => x.AuthorFilesView.ViewModel)
|
||||
.DisposeWith(disposable);
|
||||
this.MiscGalleryView.ViewModel = this.ViewModel;
|
||||
});
|
||||
}
|
||||
|
@ -76,8 +76,9 @@ internal class Program
|
||||
services.AddSingleton<IVerb, Extract>();
|
||||
services.AddSingleton<IVerb, DumpZipInfo>();
|
||||
services.AddSingleton<IVerb, Install>();
|
||||
services.AddSingleton<IVerb, Compile>();
|
||||
services.AddSingleton<IVerb, InstallCompileInstallVerify>();
|
||||
services.AddSingleton<IVerb, HashUrlString>();
|
||||
services.AddSingleton<IVerb, HashUrlString>();
|
||||
services.AddSingleton<IVerb, DownloadAll>();
|
||||
|
||||
services.AddSingleton<IUserInterventionHandler, UserInterventionHandler>();
|
||||
|
72
Wabbajack.CLI/Verbs/Compile.cs
Normal file
72
Wabbajack.CLI/Verbs/Compile.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Wabbajack.Compiler;
|
||||
using Wabbajack.Downloaders;
|
||||
using Wabbajack.Downloaders.GameFile;
|
||||
using Wabbajack.DTOs.JsonConverters;
|
||||
using Wabbajack.Networking.WabbajackClientApi;
|
||||
using Wabbajack.Paths;
|
||||
using Wabbajack.VFS;
|
||||
|
||||
namespace Wabbajack.CLI.Verbs;
|
||||
|
||||
public class Compile : IVerb
|
||||
{
|
||||
private readonly ILogger<Compile> _logger;
|
||||
private readonly Client _wjClient;
|
||||
private readonly DownloadDispatcher _dispatcher;
|
||||
private readonly DTOSerializer _dtos;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly FileHashCache _cache;
|
||||
private readonly GameLocator _gameLocator;
|
||||
private readonly CompilerSettingsInferencer _inferencer;
|
||||
|
||||
public Compile(ILogger<Compile> logger, Client wjClient, DownloadDispatcher dispatcher, DTOSerializer dtos,
|
||||
FileHashCache cache, GameLocator gameLocator, IServiceProvider serviceProvider, CompilerSettingsInferencer inferencer)
|
||||
{
|
||||
_logger = logger;
|
||||
_wjClient = wjClient;
|
||||
_dispatcher = dispatcher;
|
||||
_dtos = dtos;
|
||||
_serviceProvider = serviceProvider;
|
||||
_cache = cache;
|
||||
_gameLocator = gameLocator;
|
||||
_inferencer = inferencer;
|
||||
}
|
||||
|
||||
public Command MakeCommand()
|
||||
{
|
||||
var command = new Command("compile");
|
||||
command.Add(new Option<AbsolutePath>(new[] {"-i", "-installPath"}, "Install Path"));
|
||||
command.Add(new Option<AbsolutePath>(new[] {"-o", "-output"}, "Output"));
|
||||
command.Description = "Installs a modlist, compiles it, installs it again, verifies it";
|
||||
command.Handler = CommandHandler.Create(Run);
|
||||
return command;
|
||||
}
|
||||
|
||||
public async Task<int> Run(AbsolutePath installPath, AbsolutePath outputPath,
|
||||
CancellationToken token)
|
||||
{
|
||||
_logger.LogInformation("Inferring settings");
|
||||
var inferredSettings = await _inferencer.InferFromRootPath(installPath);
|
||||
if (inferredSettings == null)
|
||||
{
|
||||
_logger.LogInformation("Error inferencing settings");
|
||||
return 2;
|
||||
}
|
||||
|
||||
inferredSettings.UseGamePaths = true;
|
||||
|
||||
var compiler = MO2Compiler.Create(_serviceProvider, inferredSettings);
|
||||
var result = await compiler.Begin(token);
|
||||
if (!result)
|
||||
return result ? 0 : 3;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -28,6 +28,8 @@ public class CompilerSettings
|
||||
public string ModlistReadme { get; set; } = "";
|
||||
public Uri? ModListWebsite { get; set; }
|
||||
public Version ModlistVersion { get; set; } = Version.Parse("0.0.1.0");
|
||||
public bool PublishUpdate { get; set; } = false;
|
||||
public string MachineUrl { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// The main (default) profile
|
||||
|
@ -5,11 +5,13 @@ using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Octokit;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.DTOs;
|
||||
using Wabbajack.DTOs.CDN;
|
||||
@ -28,6 +30,7 @@ using Wabbajack.Paths.IO;
|
||||
using Wabbajack.RateLimiter;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
using FileMode = System.IO.FileMode;
|
||||
|
||||
namespace Wabbajack.Networking.WabbajackClientApi;
|
||||
|
||||
@ -149,7 +152,7 @@ public class Client
|
||||
$"https://raw.githubusercontent.com/wabbajack-tools/mod-lists/master/reports/{machineURL}/status.json",
|
||||
_dtos.Options))!;
|
||||
}
|
||||
|
||||
|
||||
IEnumerable<PartDefinition> Blocks(long size)
|
||||
{
|
||||
for (long block = 0; block * UploadedFileBlockSize < size; block++)
|
||||
@ -195,13 +198,14 @@ public class Client
|
||||
var featured = await LoadFeaturedLists();
|
||||
|
||||
return await (await repos).PMapAll(async url =>
|
||||
(await _client.GetFromJsonAsync<ModlistMetadata[]>(_limiter, new HttpRequestMessage(HttpMethod.Get, url.Value),
|
||||
(await _client.GetFromJsonAsync<ModlistMetadata[]>(_limiter,
|
||||
new HttpRequestMessage(HttpMethod.Get, url.Value),
|
||||
_dtos.Options))!.Select(meta =>
|
||||
{
|
||||
meta.RepositoryName = url.Key;
|
||||
meta.Official = (meta.RepositoryName == "wj-featured" || featured.Contains(meta.NamespacedName));
|
||||
return meta;
|
||||
}))
|
||||
{
|
||||
meta.RepositoryName = url.Key;
|
||||
meta.Official = (meta.RepositoryName == "wj-featured" || featured.Contains(meta.NamespacedName));
|
||||
return meta;
|
||||
}))
|
||||
.SelectMany(x => x)
|
||||
.ToArray();
|
||||
}
|
||||
@ -210,7 +214,8 @@ public class Client
|
||||
{
|
||||
var data = await _client.GetFromJsonAsync<string[]>(_limiter,
|
||||
new HttpRequestMessage(HttpMethod.Get,
|
||||
"https://raw.githubusercontent.com/wabbajack-tools/mod-lists/master/featured_lists.json"), _dtos.Options);
|
||||
"https://raw.githubusercontent.com/wabbajack-tools/mod-lists/master/featured_lists.json"),
|
||||
_dtos.Options);
|
||||
return data!.ToHashSet(StringComparer.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
@ -231,7 +236,7 @@ public class Client
|
||||
{
|
||||
_logger.LogInformation("Uploading Patch {From} {To}", validated.Original.Hash, validated.PatchedFrom!.Hash);
|
||||
var name = $"{validated.Original.Hash.ToHex()}_{validated.PatchedFrom.Hash.ToHex()}";
|
||||
|
||||
|
||||
var blocks = Blocks(data.Length).ToArray();
|
||||
foreach (var block in blocks)
|
||||
{
|
||||
@ -259,7 +264,8 @@ public class Client
|
||||
|
||||
public async Task AddForceHealedPatch(ValidatedArchive validated)
|
||||
{
|
||||
var oldData = await GetGithubFile<ValidatedArchive[]>("wabbajack-tools", "mod-lists", "configs/forced_healing.json");
|
||||
var oldData =
|
||||
await GetGithubFile<ValidatedArchive[]>("wabbajack-tools", "mod-lists", "configs/forced_healing.json");
|
||||
var content = oldData.Content.Append(validated).ToArray();
|
||||
await UpdateGitHubFile("wabbajack-tools", "mod-lists", "configs/forced_healing.json", content, oldData.Sha);
|
||||
}
|
||||
@ -276,10 +282,11 @@ public class Client
|
||||
throw new HttpException(result);
|
||||
}
|
||||
|
||||
private async Task<(string Sha, T Content)> GetGithubFile<T>(string owner, string repo, string path, CancellationToken? token = null)
|
||||
private async Task<(string Sha, T Content)> GetGithubFile<T>(string owner, string repo, string path,
|
||||
CancellationToken? token = null)
|
||||
{
|
||||
token ??= CancellationToken.None;
|
||||
|
||||
|
||||
var msg = await MakeMessage(HttpMethod.Get,
|
||||
new Uri($"{_configuration.BuildServerUrl}github/?owner={owner}&repo={repo}&path={path}"));
|
||||
using var oldData = await _client.SendAsync(msg, token.Value);
|
||||
@ -295,13 +302,13 @@ public class Client
|
||||
{
|
||||
var hashAsHex = definition.Hash.ToHex();
|
||||
_logger.LogInformation("Starting upload of {Name} ({Hash})", file.FileName, hashAsHex);
|
||||
|
||||
|
||||
using var result = await _client.SendAsync(await MakeMessage(HttpMethod.Put,
|
||||
new Uri($"{_configuration.BuildServerUrl}mirrored_files/create/{hashAsHex}"),
|
||||
new StringContent(_dtos.Serialize(definition), Encoding.UTF8, "application/json")));
|
||||
if (!result.IsSuccessStatusCode)
|
||||
throw new HttpException(result);
|
||||
|
||||
|
||||
_logger.LogInformation("Uploading Parts");
|
||||
|
||||
await using var dataIn = file.Open(FileMode.Open);
|
||||
@ -317,14 +324,14 @@ public class Client
|
||||
using var partResult = await _client.SendAsync(await MakeMessage(HttpMethod.Put,
|
||||
new Uri($"{_configuration.BuildServerUrl}mirrored_files/{hashAsHex}/part/{idx}"),
|
||||
new ByteArrayContent(data)));
|
||||
|
||||
|
||||
if (!partResult.IsSuccessStatusCode)
|
||||
throw new HttpException(result);
|
||||
}
|
||||
|
||||
using var finalResult = await _client.SendAsync(await MakeMessage(HttpMethod.Put,
|
||||
new Uri($"{_configuration.BuildServerUrl}mirrored_files/{hashAsHex}/finish")));
|
||||
|
||||
|
||||
if (!finalResult.IsSuccessStatusCode)
|
||||
throw new HttpException(result);
|
||||
}
|
||||
@ -337,34 +344,102 @@ public class Client
|
||||
|
||||
public async Task<ValidatedArchive[]> GetAllPatches(CancellationToken token)
|
||||
{
|
||||
return (await _client.GetFromJsonAsync<ValidatedArchive[]>("https://raw.githubusercontent.com/wabbajack-tools/mod-lists/master/configs/forced_healing.json", _dtos.Options, token))!;
|
||||
return (await _client.GetFromJsonAsync<ValidatedArchive[]>(
|
||||
"https://raw.githubusercontent.com/wabbajack-tools/mod-lists/master/configs/forced_healing.json",
|
||||
_dtos.Options, token))!;
|
||||
}
|
||||
|
||||
public async Task DeleteMirror(Hash hash)
|
||||
{
|
||||
_logger.LogInformation("Deleting mirror of {Hash}", hash);
|
||||
var msg = await MakeMessage(HttpMethod.Delete, new Uri($"{_configuration.BuildServerUrl}mirrored_files/{hash.ToHex()}"));
|
||||
var msg = await MakeMessage(HttpMethod.Delete,
|
||||
new Uri($"{_configuration.BuildServerUrl}mirrored_files/{hash.ToHex()}"));
|
||||
var result = await _client.SendAsync(msg);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
throw new HttpException(result);
|
||||
}
|
||||
|
||||
|
||||
public (IObservable<(Percent PercentDone, string Message)> Progress, Task<Uri> Task) UploadAuthorFile(AbsolutePath pickerTargetPath)
|
||||
public async Task<(IObservable<(Percent PercentDone, string Message)> Progress, Task<Uri> Task)> UploadAuthorFile(
|
||||
AbsolutePath path)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
var apiKey = (await _token.Get())!.AuthorKey;
|
||||
var report = new Subject<(Percent PercentDone, string Message)>();
|
||||
|
||||
var tsk = Task.Run<Uri>(async () =>
|
||||
{
|
||||
report.OnNext((Percent.Zero, "Generating File Definition"));
|
||||
var definition = await GenerateFileDefinition(path);
|
||||
|
||||
report.OnNext((Percent.Zero, "Creating file upload"));
|
||||
await CircuitBreaker.WithAutoRetryAllAsync(_logger, async () =>
|
||||
{
|
||||
var msg = await MakeMessage(HttpMethod.Put,
|
||||
new Uri($"{_configuration.BuildServerUrl}authored_files/create"));
|
||||
msg.Content = new StringContent(_dtos.Serialize(definition));
|
||||
using var result = await _client.SendAsync(msg);
|
||||
HttpException.ThrowOnFailure(result);
|
||||
definition.ServerAssignedUniqueId = await result.Content.ReadAsStringAsync();
|
||||
});
|
||||
|
||||
report.OnNext((Percent.Zero, "Starting part uploads"));
|
||||
await definition.Parts.PDoAll(_limiter, async part =>
|
||||
{
|
||||
report.OnNext((Percent.FactoryPutInRange(part.Index, definition.Parts.Length),
|
||||
$"Uploading Part ({part.Index}/{definition.Parts.Length})"));
|
||||
var buffer = new byte[part.Size];
|
||||
await using (var fs = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
fs.Position = part.Offset;
|
||||
await fs.ReadAsync(buffer);
|
||||
}
|
||||
|
||||
await CircuitBreaker.WithAutoRetryAllAsync(_logger, async () =>
|
||||
{
|
||||
var msg = await MakeMessage(HttpMethod.Put,
|
||||
new Uri(
|
||||
$"{_configuration.BuildServerUrl}authored_files/{definition.ServerAssignedUniqueId}/part/{part.Index}"));
|
||||
msg.Content = new ByteArrayContent(buffer);
|
||||
using var putResult = await _client.SendAsync(msg);
|
||||
HttpException.ThrowOnFailure(putResult);
|
||||
var hash = Hash.FromBase64(await putResult.Content.ReadAsStringAsync());
|
||||
if (hash != part.Hash)
|
||||
throw new InvalidDataException("Hashes don't match");
|
||||
return hash;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
report.OnNext((Percent.Zero, "Finalizing upload"));
|
||||
return await CircuitBreaker.WithAutoRetryAllAsync(_logger, async () =>
|
||||
{
|
||||
var msg = await MakeMessage(HttpMethod.Put,
|
||||
new Uri(
|
||||
$"{_configuration.BuildServerUrl}authored_files/{definition.ServerAssignedUniqueId}/finish"));
|
||||
msg.Content = new StringContent(_dtos.Serialize(definition));
|
||||
using var result = await _client.SendAsync(msg);
|
||||
HttpException.ThrowOnFailure(result);
|
||||
report.OnNext((Percent.One, "Finished"));
|
||||
return new Uri($"https://authored-files.wabbajack.org/{definition.MungedName}");
|
||||
});
|
||||
});
|
||||
return (report, tsk);
|
||||
}
|
||||
|
||||
public async Task<ForcedRemoval[]> GetForcedRemovals(CancellationToken token)
|
||||
{
|
||||
return (await _client.GetFromJsonAsync<ForcedRemoval[]>("https://raw.githubusercontent.com/wabbajack-tools/mod-lists/master/configs/forced_removal.json", _dtos.Options, token))!;
|
||||
return (await _client.GetFromJsonAsync<ForcedRemoval[]>(
|
||||
"https://raw.githubusercontent.com/wabbajack-tools/mod-lists/master/configs/forced_removal.json",
|
||||
_dtos.Options, token))!;
|
||||
}
|
||||
|
||||
public async Task<SteamManifest[]> GetSteamManifests(Game game, string version)
|
||||
{
|
||||
var url = $"https://raw.githubusercontent.com/wabbajack-tools/indexed-game-files/master/{game}/{version}_steam_manifests.json";
|
||||
var url =
|
||||
$"https://raw.githubusercontent.com/wabbajack-tools/indexed-game-files/master/{game}/{version}_steam_manifests.json";
|
||||
return await _client.GetFromJsonAsync<SteamManifest[]>(url, _dtos.Options) ?? Array.Empty<SteamManifest>();
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> ProxyHas(Uri uri)
|
||||
{
|
||||
var newUri = new Uri($"{_configuration.BuildServerUrl}proxy?uri={HttpUtility.UrlEncode(uri.ToString())}");
|
||||
@ -384,8 +459,9 @@ public class Client
|
||||
{
|
||||
if (archive.State is Manual && !await ProxyHas(uri))
|
||||
return null;
|
||||
|
||||
return new Uri($"{_configuration.BuildServerUrl}proxy?name={archive.Name}&hash={archive.Hash.ToHex()}&uri={HttpUtility.UrlEncode(uri.ToString())}");
|
||||
|
||||
return new Uri(
|
||||
$"{_configuration.BuildServerUrl}proxy?name={archive.Name}&hash={archive.Hash.ToHex()}&uri={HttpUtility.UrlEncode(uri.ToString())}");
|
||||
}
|
||||
|
||||
public async Task<IndexedVirtualFile?> GetCesiVfsEntry(Hash hash, CancellationToken token)
|
||||
@ -395,4 +471,56 @@ public class Client
|
||||
HttpException.ThrowOnFailure(response);
|
||||
return await _dtos.DeserializeAsync<IndexedVirtualFile>(await response.Content.ReadAsStreamAsync(token), token);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<string>> GetMyModlists(CancellationToken token)
|
||||
{
|
||||
var msg = await MakeMessage(HttpMethod.Get, new Uri($"{_configuration.BuildServerUrl}author_controls/lists"));
|
||||
using var response = await _client.SendAsync(msg, token);
|
||||
HttpException.ThrowOnFailure(response);
|
||||
return (await _dtos.DeserializeAsync<string[]>(await response.Content.ReadAsStreamAsync(token), token))!;
|
||||
}
|
||||
|
||||
public async Task PublishModlist(string namespacedName, Version version, AbsolutePath modList, DownloadMetadata metadata)
|
||||
{
|
||||
var pair = namespacedName.Split("/");
|
||||
var wjRepoName = pair[0];
|
||||
var machineUrl = pair[1];
|
||||
|
||||
var repoUrl = (await LoadRepositories())[wjRepoName];
|
||||
|
||||
var decomposed = repoUrl.LocalPath.Split("/");
|
||||
var owner = decomposed[1];
|
||||
var repoName = decomposed[2];
|
||||
var path = string.Join("/", decomposed[4..]);
|
||||
|
||||
_logger.LogInformation("Uploading modlist {MachineUrl}", namespacedName);
|
||||
|
||||
var (progress, uploadTask) = await UploadAuthorFile(modList);
|
||||
progress.Subscribe(x => _logger.LogInformation(x.Message));
|
||||
var downloadUrl = await uploadTask;
|
||||
|
||||
_logger.LogInformation("Publishing modlist {MachineUrl}", namespacedName);
|
||||
|
||||
var creds = new Credentials((await _token.Get())!.AuthorKey);
|
||||
var ghClient = new GitHubClient(new ProductHeaderValue("wabbajack")) {Credentials = creds};
|
||||
|
||||
var oldData =
|
||||
(await ghClient.Repository.Content.GetAllContents(owner, repoName, path))
|
||||
.First();
|
||||
var oldContent = _dtos.Deserialize<ModlistMetadata[]>(oldData.Content);
|
||||
var list = oldContent.First(c => c.Links.MachineURL == machineUrl);
|
||||
list.Version = version;
|
||||
list.DownloadMetadata = metadata;
|
||||
list.Links.Download = downloadUrl.ToString();
|
||||
list.DateUpdated = DateTime.UtcNow;
|
||||
|
||||
|
||||
var newContent = _dtos.Serialize(oldContent, true);
|
||||
// the website requires all names be in lowercase;
|
||||
newContent = GameRegistry.Games.Keys.Aggregate(newContent,
|
||||
(current, g) => current.Replace($"\"game\": \"{g}\",", $"\"game\": \"{g.ToString().ToLower()}\","));
|
||||
|
||||
var updateRequest = new UpdateFileRequest($"New release of {machineUrl}", newContent, oldData.Sha);
|
||||
await ghClient.Repository.Content.UpdateFile(owner, repoName, path, updateRequest);
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2-mauipre.1.22054.8" />
|
||||
<PackageReference Include="Octokit" Version="1.0.0" />
|
||||
<PackageReference Include="YamlDotNet" Version="11.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user