mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge branch 'master' of https://github.com/wabbajack-tools/wabbajack
This commit is contained in:
commit
1f2c62ab87
32
CHANGELOG.md
32
CHANGELOG.md
@ -1,5 +1,37 @@
|
|||||||
### Changelog
|
### Changelog
|
||||||
|
|
||||||
|
#### Version - 2.3.5.1 - 12/23/2020
|
||||||
|
* HOTFIX : Recover from errors in the EGS location detector
|
||||||
|
|
||||||
|
#### Version - 2.3.5.0 - 12/16/2020
|
||||||
|
* Fix tesall.ru download support
|
||||||
|
* Implement MechWarrior 5 support as a native compiler game
|
||||||
|
* Make the title in the WJ gallery (in app) optional for games that want the title to be in the splash screen
|
||||||
|
* Worked a few kinks out of the native game compiler
|
||||||
|
|
||||||
|
#### Version - 2.3.4.3 - 12/6/2020
|
||||||
|
* Disable the back button during install/compilation
|
||||||
|
|
||||||
|
#### Version - 2.3.4.2 - 11/24/2020
|
||||||
|
* Add Support for Kingdom Come : Deliverance (via MO2)
|
||||||
|
* Several other small bug fixes and deps updates
|
||||||
|
|
||||||
|
#### Version - 2.3.4.1 - 11/15/2020
|
||||||
|
* Tell the mod updater to use the existing Nexus Client instead of creating a new one
|
||||||
|
|
||||||
|
#### Version - 2.3.4.0 - 11/15/2020
|
||||||
|
* Removed the internal Manifest Viewer, you can still view the Manifest of any Modlist on the website
|
||||||
|
* Improved Nexus warnings about being low on API calls
|
||||||
|
* Added marker for "utility modlists" we will expand on this feature further in later releases
|
||||||
|
|
||||||
|
#### Version - 2.3.3.0 - 11/5/2020
|
||||||
|
* Game file hashes are now stored on Github instead of on the build server
|
||||||
|
* Added CLI Verb to produce these hash files for the Github repo
|
||||||
|
* When a user runs out of Nexus API calls we no longer bombard the Nexus with download attempts
|
||||||
|
* Check API limits before attempting a modlist download
|
||||||
|
* Logger is less chatty about recoverable download errors
|
||||||
|
* Display integer progress values during install so users know how far along in the process they are #issue-1156
|
||||||
|
|
||||||
#### Version - 2.3.2.0 - 11/2/2020
|
#### Version - 2.3.2.0 - 11/2/2020
|
||||||
* 7Zip errors now re-hash the extracted file to check for file corruption issues. Should provide
|
* 7Zip errors now re-hash the extracted file to check for file corruption issues. Should provide
|
||||||
better feedback in cases that a file is modified after being downloaded (perhaps by a disk failure)
|
better feedback in cases that a file is modified after being downloaded (perhaps by a disk failure)
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Genbox.AlphaFS" Version="2.2.2.1" />
|
<PackageReference Include="Genbox.AlphaFS" Version="2.2.2.1" />
|
||||||
<PackageReference Include="K4os.Compression.LZ4.Streams" Version="1.2.6" />
|
<PackageReference Include="K4os.Compression.LZ4.Streams" Version="1.2.6" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.3.0" />
|
<PackageReference Include="SharpZipLib" Version="1.3.1" />
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0-preview.8.20407.11" />
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Properties\" />
|
<Folder Include="Properties\" />
|
||||||
|
@ -192,12 +192,13 @@ Example structure for a good README:
|
|||||||
Some Modlists that host all their stuff on GitHub:
|
Some Modlists that host all their stuff on GitHub:
|
||||||
|
|
||||||
- the OG GitHub Modlist: [Lotus](https://github.com/erri120/lotus)
|
- the OG GitHub Modlist: [Lotus](https://github.com/erri120/lotus)
|
||||||
|
- [Keizaal](https://github.com/PierreDespereaux/Keizaal)
|
||||||
- [Living Skyrim](https://github.com/ForgottenGlory/Living-Skyrim-2)
|
- [Living Skyrim](https://github.com/ForgottenGlory/Living-Skyrim-2)
|
||||||
- [Eldersouls](https://github.com/jdsmith2816/eldersouls)
|
- [Eldersouls](https://github.com/jdsmith2816/eldersouls)
|
||||||
- [Total Visual Overhaul](https://github.com/NotTotal/Total-Visual-Overhaul)
|
- [Total Visual Overhaul](https://github.com/NotTotal/Total-Visual-Overhaul)
|
||||||
- [Serenity](https://github.com/ixanza/serenity)
|
- [Serenity](https://github.com/ixanza/serenity)
|
||||||
- [MOISE](https://github.com/ForgottenGlory/MOISE)
|
- [MOISE](https://github.com/ForgottenGlory/MOISE)
|
||||||
- [Cupid](https://github.com/ForgottenGlory/MOISE)
|
- [Dungeons & Deviousness](https://github.com/ForgottenGlory/Dungeons-Deviousness)
|
||||||
|
|
||||||
### Meta Files
|
### Meta Files
|
||||||
|
|
||||||
|
@ -30,7 +30,12 @@ namespace Wabbajack.CLI
|
|||||||
typeof(HashVariants),
|
typeof(HashVariants),
|
||||||
typeof(ParseMeta),
|
typeof(ParseMeta),
|
||||||
typeof(NoPatch),
|
typeof(NoPatch),
|
||||||
typeof(NexusPermissions)
|
typeof(NexusPermissions),
|
||||||
|
typeof(ExportServerGameFiles),
|
||||||
|
typeof(HashGamefiles),
|
||||||
|
typeof(Backup),
|
||||||
|
typeof(Restore),
|
||||||
|
typeof(PurgeArchive)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
97
Wabbajack.CLI/Verbs/Backup.cs
Normal file
97
Wabbajack.CLI/Verbs/Backup.cs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommandLine;
|
||||||
|
using Wabbajack.Common;
|
||||||
|
using Wabbajack.Common.IO;
|
||||||
|
|
||||||
|
namespace Wabbajack.CLI.Verbs
|
||||||
|
{
|
||||||
|
|
||||||
|
[Verb("backup", HelpText = @"Copy all encrypted and personal info into a location for transferring to a new machine", Hidden = true)]
|
||||||
|
public class Backup : AVerb
|
||||||
|
{
|
||||||
|
public static Dictionary<string, bool> Keys = new Dictionary<string, bool>()
|
||||||
|
{
|
||||||
|
{"bunnycdn", true},
|
||||||
|
{"nexusapikey", true},
|
||||||
|
{"nexus-cookies", true},
|
||||||
|
{"x-metrics-key", true},
|
||||||
|
{"author-api-key.txt", false}
|
||||||
|
};
|
||||||
|
|
||||||
|
[Option('o', "output", Required = true, HelpText = @"Output folder for the decrypted data")]
|
||||||
|
public string Output { get; set; } = "";
|
||||||
|
|
||||||
|
public AbsolutePath OutputPath => (AbsolutePath)Output;
|
||||||
|
protected override async Task<ExitCode> Run()
|
||||||
|
{
|
||||||
|
foreach(var (name, encrypted) in Keys)
|
||||||
|
{
|
||||||
|
byte[] data;
|
||||||
|
var src = name.RelativeTo(Consts.LocalAppDataPath);
|
||||||
|
if (!src.Exists)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{name} doesn't exist, skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encrypted)
|
||||||
|
{
|
||||||
|
data = await Utils.FromEncryptedData(name);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data = await src.ReadAllBytesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await name.RelativeTo(OutputPath).WriteAllBytesAsync(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExitCode.Ok;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("restore", HelpText = @"Copy all encrypted and personal info into a location for transferring to a new machine", Hidden = true)]
|
||||||
|
public class Restore : AVerb
|
||||||
|
{
|
||||||
|
public static Dictionary<string, bool> Keys = new Dictionary<string, bool>()
|
||||||
|
{
|
||||||
|
{"bunnycdn", true},
|
||||||
|
{"nexusapikey", true},
|
||||||
|
{"nexus-cookies", true},
|
||||||
|
{"x-metrics-key", true},
|
||||||
|
{"author-api-key.txt", false}
|
||||||
|
};
|
||||||
|
|
||||||
|
[Option('i', "input", Required = true, HelpText = @"Input folder for the decrypted data")]
|
||||||
|
public string Input { get; set; } = "";
|
||||||
|
|
||||||
|
public AbsolutePath InputPath => (AbsolutePath)Input;
|
||||||
|
protected override async Task<ExitCode> Run()
|
||||||
|
{
|
||||||
|
foreach(var (name, encrypted) in Keys)
|
||||||
|
{
|
||||||
|
var src = name.RelativeTo(InputPath);
|
||||||
|
if (!src.Exists)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{name} doesn't exist, skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = await src.ReadAllBytesAsync();
|
||||||
|
|
||||||
|
if (encrypted)
|
||||||
|
{
|
||||||
|
await data.ToEcryptedData(name);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await name.RelativeTo(Consts.LocalAppDataPath).WriteAllBytesAsync(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExitCode.Ok;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
Wabbajack.CLI/Verbs/ExportServerGameFiles.cs
Normal file
32
Wabbajack.CLI/Verbs/ExportServerGameFiles.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommandLine;
|
||||||
|
using Wabbajack.Common;
|
||||||
|
using Wabbajack.Lib;
|
||||||
|
|
||||||
|
namespace Wabbajack.CLI.Verbs
|
||||||
|
{
|
||||||
|
[Verb("export-server-game-files", HelpText = "Exports all the game file data from the server to the output folder")]
|
||||||
|
public class ExportServerGameFiles : AVerb
|
||||||
|
{
|
||||||
|
[Option('o', "output", Required = true, HelpText = @"Output folder in which the files will be placed")]
|
||||||
|
public string OutputFolder { get; set; } = "";
|
||||||
|
|
||||||
|
private AbsolutePath _outputFolder => (AbsolutePath)OutputFolder;
|
||||||
|
|
||||||
|
protected override async Task<ExitCode> Run()
|
||||||
|
{
|
||||||
|
var games = await ClientAPI.GetServerGamesAndVersions();
|
||||||
|
foreach (var (game, version) in games)
|
||||||
|
{
|
||||||
|
Utils.Log($"Exporting {game} {version}");
|
||||||
|
var file = _outputFolder.Combine(game.ToString(), version).WithExtension(new Extension(".json"));
|
||||||
|
file.Parent.CreateDirectory();
|
||||||
|
var files = await ClientAPI.GetGameFilesFromServer(game, version);
|
||||||
|
await files.ToJsonAsync(file, prettyPrint:true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExitCode.Ok;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
Wabbajack.CLI/Verbs/HashGameFiles.cs
Normal file
59
Wabbajack.CLI/Verbs/HashGameFiles.cs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommandLine;
|
||||||
|
using Wabbajack.Common;
|
||||||
|
using Wabbajack.Lib;
|
||||||
|
using Wabbajack.Lib.Downloaders;
|
||||||
|
|
||||||
|
namespace Wabbajack.CLI.Verbs
|
||||||
|
{
|
||||||
|
[Verb("hash-game-files", HelpText = "Hashes a game's files for inclusion in the public github repo")]
|
||||||
|
public class HashGamefiles : AVerb
|
||||||
|
{
|
||||||
|
[Option('o', "output", Required = true, HelpText = @"Output folder in which the file will be placed")]
|
||||||
|
public string OutputFolder { get; set; } = "";
|
||||||
|
|
||||||
|
private AbsolutePath _outputFolder => (AbsolutePath)OutputFolder;
|
||||||
|
|
||||||
|
[Option('g', "game", Required = true, HelpText = @"WJ Game to index")]
|
||||||
|
public string Game { get; set; } = "";
|
||||||
|
|
||||||
|
private Game _game => GameRegistry.GetByFuzzyName(Game).Game;
|
||||||
|
|
||||||
|
protected override async Task<ExitCode> Run()
|
||||||
|
{
|
||||||
|
var version = _game.MetaData().InstalledVersion;
|
||||||
|
var file = _outputFolder.Combine(_game.ToString(), version).WithExtension(new Extension(".json"));
|
||||||
|
file.Parent.CreateDirectory();
|
||||||
|
|
||||||
|
using var queue = new WorkQueue();
|
||||||
|
var gameLocation = _game.MetaData().GameLocation();
|
||||||
|
|
||||||
|
Utils.Log($"Hashing files for {_game} {version}");
|
||||||
|
|
||||||
|
var indexed = await gameLocation
|
||||||
|
.EnumerateFiles()
|
||||||
|
.PMap(queue, async f =>
|
||||||
|
{
|
||||||
|
var hash = await f.FileHashCachedAsync();
|
||||||
|
return new Archive(new GameFileSourceDownloader.State
|
||||||
|
{
|
||||||
|
Game = _game,
|
||||||
|
GameFile = f.RelativeTo(gameLocation),
|
||||||
|
Hash = hash,
|
||||||
|
GameVersion = version
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Name = f.FileName.ToString(),
|
||||||
|
Hash = hash,
|
||||||
|
Size = f.Size
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Utils.Log($"Found and hashed {indexed.Length} files");
|
||||||
|
await indexed.ToJsonAsync(file, prettyPrint: true);
|
||||||
|
return ExitCode.Ok;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
65
Wabbajack.CLI/Verbs/PurgeArchive.cs
Normal file
65
Wabbajack.CLI/Verbs/PurgeArchive.cs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommandLine;
|
||||||
|
using Wabbajack.Common;
|
||||||
|
using Wabbajack.Lib;
|
||||||
|
|
||||||
|
namespace Wabbajack.CLI.Verbs
|
||||||
|
{
|
||||||
|
[Verb("purge-archive", HelpText = "Purges an archive and all directives from a .wabbajack file")]
|
||||||
|
public class PurgeArchive : AVerb
|
||||||
|
{
|
||||||
|
[Option('i', "input", Required = true, HelpText = "Input .wabbajack file")]
|
||||||
|
public string Input { get; set; } = "";
|
||||||
|
private AbsolutePath _Input => (AbsolutePath)Input;
|
||||||
|
|
||||||
|
[Option('o', "output", Required = true, HelpText = "Output .wabbajack file")]
|
||||||
|
public string Output { get; set; } = "";
|
||||||
|
private AbsolutePath _Output => (AbsolutePath)Output;
|
||||||
|
|
||||||
|
[Option('h', "hash", Required = true, HelpText = "Hash to purge")]
|
||||||
|
public string ArchiveHash { get; set; } = "";
|
||||||
|
private Hash _Hash => Hash.Interpret(ArchiveHash);
|
||||||
|
|
||||||
|
protected override async Task<ExitCode> Run()
|
||||||
|
{
|
||||||
|
Utils.Log("Copying .wabbajack file");
|
||||||
|
await _Input.CopyToAsync(_Output);
|
||||||
|
|
||||||
|
Utils.Log("Loading modlist");
|
||||||
|
|
||||||
|
await using var fs = await _Output.OpenWrite();
|
||||||
|
using var ar = new ZipArchive(fs, ZipArchiveMode.Update);
|
||||||
|
ModList modlist;
|
||||||
|
await using (var entry = ar.Entries.First(e => e.Name == "modlist").Open())
|
||||||
|
{
|
||||||
|
modlist = entry.FromJson<ModList>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils.Log("Purging archives");
|
||||||
|
modlist.Archives = modlist.Archives.Where(a => a.Hash != _Hash).ToList();
|
||||||
|
modlist.Directives = modlist.Directives.Select(d =>
|
||||||
|
{
|
||||||
|
if (d is FromArchive a)
|
||||||
|
{
|
||||||
|
if (a.ArchiveHashPath.BaseHash == _Hash) return (false, d);
|
||||||
|
}
|
||||||
|
return (true, d);
|
||||||
|
}).Where(d => d.Item1)
|
||||||
|
.Select(d => d.d)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Utils.Log("Writing modlist");
|
||||||
|
|
||||||
|
await using (var entry = ar.Entries.First(e => e.Name == "modlist").Open())
|
||||||
|
{
|
||||||
|
entry.SetLength(0);
|
||||||
|
entry.Position = 0;
|
||||||
|
modlist.ToJson(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExitCode.Ok;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,8 +6,8 @@
|
|||||||
<AssemblyName>wabbajack-cli</AssemblyName>
|
<AssemblyName>wabbajack-cli</AssemblyName>
|
||||||
<Company>Wabbajack</Company>
|
<Company>Wabbajack</Company>
|
||||||
<Platforms>x64</Platforms>
|
<Platforms>x64</Platforms>
|
||||||
<AssemblyVersion>2.3.2.0</AssemblyVersion>
|
<AssemblyVersion>2.3.5.1</AssemblyVersion>
|
||||||
<FileVersion>2.3.2.0</FileVersion>
|
<FileVersion>2.3.5.1</FileVersion>
|
||||||
<Copyright>Copyright © 2019-2020</Copyright>
|
<Copyright>Copyright © 2019-2020</Copyright>
|
||||||
<Description>An automated ModList installer</Description>
|
<Description>An automated ModList installer</Description>
|
||||||
<PublishReadyToRun>true</PublishReadyToRun>
|
<PublishReadyToRun>true</PublishReadyToRun>
|
||||||
@ -19,9 +19,9 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommandLineParser" Version="2.9.0-preview1" />
|
<PackageReference Include="CommandLineParser" Version="2.9.0-preview1" />
|
||||||
<PackageReference Include="F23.StringSimilarity" Version="3.1.0" />
|
<PackageReference Include="F23.StringSimilarity" Version="3.1.0" />
|
||||||
<PackageReference Include="Markdig" Version="0.22.0" />
|
<PackageReference Include="Markdig" Version="0.22.1" />
|
||||||
<PackageReference Include="System.Reactive" Version="4.4.1" />
|
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Reactive.Linq" Version="4.4.1" />
|
<PackageReference Include="System.Reactive.Linq" Version="5.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<RuntimeIdentifier>win10-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win10-x64</RuntimeIdentifier>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="System.Reactive" Version="4.4.1" />
|
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.6.0-preview.18571.3" />
|
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.6.0-preview.18571.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -30,7 +30,7 @@ namespace Wabbajack.Common
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (DateTime.Now - startTime > timeout || token.IsCancellationRequested || isDisposed)
|
if (DateTime.Now - startTime > timeout || token.IsCancellationRequested || isDisposed)
|
||||||
return (false, default);
|
return (false, default)!;
|
||||||
await Task.Delay(100);
|
await Task.Delay(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,6 +84,7 @@ namespace Wabbajack.Common
|
|||||||
|
|
||||||
public static string ServerWhitelistURL = "https://raw.githubusercontent.com/wabbajack-tools/opt-out-lists/master/ServerWhitelist.yml";
|
public static string ServerWhitelistURL = "https://raw.githubusercontent.com/wabbajack-tools/opt-out-lists/master/ServerWhitelist.yml";
|
||||||
public static string ModlistMetadataURL = "https://raw.githubusercontent.com/wabbajack-tools/mod-lists/master/modlists.json";
|
public static string ModlistMetadataURL = "https://raw.githubusercontent.com/wabbajack-tools/mod-lists/master/modlists.json";
|
||||||
|
public static string UtilityModlistMetadataURL = "https://raw.githubusercontent.com/wabbajack-tools/mod-lists/master/utility_modlists.json";
|
||||||
public static string UnlistedModlistMetadataURL = "https://raw.githubusercontent.com/wabbajack-tools/mod-lists/master/unlisted_modlists.json";
|
public static string UnlistedModlistMetadataURL = "https://raw.githubusercontent.com/wabbajack-tools/mod-lists/master/unlisted_modlists.json";
|
||||||
public static string ModlistSummaryURL = "http://build.wabbajack.org/lists/status.json";
|
public static string ModlistSummaryURL = "http://build.wabbajack.org/lists/status.json";
|
||||||
public static string UserAgent
|
public static string UserAgent
|
||||||
|
@ -33,7 +33,7 @@ namespace Wabbajack
|
|||||||
string? reason = null,
|
string? reason = null,
|
||||||
Exception? ex = null)
|
Exception? ex = null)
|
||||||
{
|
{
|
||||||
Value = val;
|
Value = val!;
|
||||||
Succeeded = succeeded;
|
Succeeded = succeeded;
|
||||||
_reason = reason ?? string.Empty;
|
_reason = reason ?? string.Empty;
|
||||||
Exception = ex;
|
Exception = ex;
|
||||||
@ -128,7 +128,7 @@ namespace Wabbajack
|
|||||||
|
|
||||||
public static GetResponse<T> Create(bool successful, T val = default(T), string? reason = null)
|
public static GetResponse<T> Create(bool successful, T val = default(T), string? reason = null)
|
||||||
{
|
{
|
||||||
return new GetResponse<T>(successful, val, reason);
|
return new GetResponse<T>(successful, val!, reason);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ namespace Wabbajack
|
|||||||
{
|
{
|
||||||
// We have another value that came in to fire.
|
// We have another value that came in to fire.
|
||||||
// Reregister for callback
|
// Reregister for callback
|
||||||
dueTimeDisposable.Disposable = scheduler.Schedule(interval, internalCallback);
|
dueTimeDisposable.Disposable = scheduler!.Schedule(interval, internalCallback);
|
||||||
o.OnNext(value!);
|
o.OnNext(value!);
|
||||||
value = default;
|
value = default;
|
||||||
hasValue = false;
|
hasValue = false;
|
||||||
@ -204,7 +204,7 @@ namespace Wabbajack
|
|||||||
var prev = prevStorage;
|
var prev = prevStorage;
|
||||||
prevStorage = i;
|
prevStorage = i;
|
||||||
return (prev, i);
|
return (prev, i);
|
||||||
});
|
})!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IObservable<T> DelayInitial<T>(this IObservable<T> source, TimeSpan delay, IScheduler scheduler)
|
public static IObservable<T> DelayInitial<T>(this IObservable<T> source, TimeSpan delay, IScheduler scheduler)
|
||||||
|
@ -4,6 +4,7 @@ using System.ComponentModel;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Converters;
|
||||||
using Wabbajack.Common.StoreHandlers;
|
using Wabbajack.Common.StoreHandlers;
|
||||||
@ -37,7 +38,9 @@ namespace Wabbajack.Common
|
|||||||
Dishonored,
|
Dishonored,
|
||||||
Witcher3,
|
Witcher3,
|
||||||
[Description("Stardew Valley")]
|
[Description("Stardew Valley")]
|
||||||
StardewValley
|
StardewValley,
|
||||||
|
KingdomComeDeliverance,
|
||||||
|
MechWarrior5Mercenaries
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class GameExtensions
|
public static class GameExtensions
|
||||||
@ -66,6 +69,8 @@ namespace Wabbajack.Common
|
|||||||
|
|
||||||
// to get gog ids: https://www.gogdb.org
|
// to get gog ids: https://www.gogdb.org
|
||||||
public List<int>? GOGIDs { get; internal set; }
|
public List<int>? GOGIDs { get; internal set; }
|
||||||
|
|
||||||
|
public List<string> EpicGameStoreIDs { get; internal set; } = new List<string>();
|
||||||
|
|
||||||
// to get BethNet IDs: check the registry
|
// to get BethNet IDs: check the registry
|
||||||
public int BethNetID { get; internal set; }
|
public int BethNetID { get; internal set; }
|
||||||
@ -98,7 +103,16 @@ namespace Wabbajack.Common
|
|||||||
if (MainExecutable == null)
|
if (MainExecutable == null)
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
|
|
||||||
return FileVersionInfo.GetVersionInfo((string)gameLoc.Combine(MainExecutable)).ProductVersion;
|
var info = FileVersionInfo.GetVersionInfo((string)gameLoc.Combine(MainExecutable));
|
||||||
|
var version = info.ProductVersion;
|
||||||
|
if (string.IsNullOrWhiteSpace(version))
|
||||||
|
{
|
||||||
|
version =
|
||||||
|
$"{info.ProductMajorPart}.{info.ProductMinorPart}.{info.ProductBuildPart}.{info.ProductPrivatePart}";
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
return version;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,6 +504,40 @@ namespace Wabbajack.Common
|
|||||||
},
|
},
|
||||||
MainExecutable = "Stardew Valley.exe"
|
MainExecutable = "Stardew Valley.exe"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Game.KingdomComeDeliverance, new GameMetaData
|
||||||
|
{
|
||||||
|
Game = Game.KingdomComeDeliverance,
|
||||||
|
NexusName = "kingdomcomedeliverance",
|
||||||
|
MO2Name = "Kingdom Come: Deliverance",
|
||||||
|
MO2ArchiveName = "kingdomcomedeliverance",
|
||||||
|
NexusGameId = 2298,
|
||||||
|
SteamIDs = new List<int>{379430},
|
||||||
|
IsGenericMO2Plugin = true,
|
||||||
|
RequiredFiles = new List<string>
|
||||||
|
{
|
||||||
|
@"bin\Win64\KingdomCome.exe"
|
||||||
|
},
|
||||||
|
MainExecutable = @"bin\Win64\KingdomCome.exe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Game.MechWarrior5Mercenaries, new GameMetaData
|
||||||
|
{
|
||||||
|
Game = Game.MechWarrior5Mercenaries,
|
||||||
|
NexusName = "mechwarrior5mercenaries",
|
||||||
|
MO2Name = "Mechwarrior 5: Mercenaries",
|
||||||
|
MO2ArchiveName = "mechwarrior5mercenaries",
|
||||||
|
NexusGameId = 3099,
|
||||||
|
EpicGameStoreIDs = new List<string> {"9fd39d8ac72946a2a10a887ce86e6c35"},
|
||||||
|
IsGenericMO2Plugin = true,
|
||||||
|
RequiredFiles = new List<string>
|
||||||
|
{
|
||||||
|
@"MW5Mercs\Binaries\Win64\MechWarrior-Win64-Shipping.exe"
|
||||||
|
},
|
||||||
|
MainExecutable = @"MW5Mercs\Binaries\Win64\MechWarrior-Win64-Shipping.exe"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,6 +36,15 @@ namespace Wabbajack.Common
|
|||||||
Converters = Converters,
|
Converters = Converters,
|
||||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc
|
DateTimeZoneHandling = DateTimeZoneHandling.Utc
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static JsonSerializerSettings JsonSettingsPretty =>
|
||||||
|
new JsonSerializerSettings {
|
||||||
|
TypeNameHandling = TypeNameHandling.Objects,
|
||||||
|
SerializationBinder = new JsonNameSerializationBinder(),
|
||||||
|
Converters = Converters,
|
||||||
|
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||||
|
Formatting = Formatting.Indented
|
||||||
|
};
|
||||||
|
|
||||||
public static JsonSerializerSettings GenericJsonSettings =>
|
public static JsonSerializerSettings GenericJsonSettings =>
|
||||||
new JsonSerializerSettings
|
new JsonSerializerSettings
|
||||||
@ -51,18 +60,27 @@ namespace Wabbajack.Common
|
|||||||
File.WriteAllText(filename, JsonConvert.SerializeObject(obj, Formatting.Indented, JsonSettings));
|
File.WriteAllText(filename, JsonConvert.SerializeObject(obj, Formatting.Indented, JsonSettings));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void ToJson<T>(this T obj, Stream stream, bool useGenericSettings = false)
|
public static void ToJson<T>(this T obj, Stream stream, bool useGenericSettings = false, bool prettyPrint = false)
|
||||||
{
|
{
|
||||||
using var tw = new StreamWriter(stream, Encoding.UTF8, bufferSize: 1024, leaveOpen: true);
|
using var tw = new StreamWriter(stream, Encoding.UTF8, bufferSize: 1024, leaveOpen: true);
|
||||||
using var writer = new JsonTextWriter(tw);
|
using var writer = new JsonTextWriter(tw);
|
||||||
var ser = JsonSerializer.Create(useGenericSettings ? GenericJsonSettings : JsonSettings);
|
|
||||||
|
JsonSerializerSettings settings = (useGenericSettings, prettyPrint) switch
|
||||||
|
{
|
||||||
|
(true, true) => GenericJsonSettings,
|
||||||
|
(false, true) => JsonSettingsPretty,
|
||||||
|
(false, false) => JsonSettings,
|
||||||
|
(true, false) => GenericJsonSettings
|
||||||
|
};
|
||||||
|
|
||||||
|
var ser = JsonSerializer.Create(settings);
|
||||||
ser.Serialize(writer, obj);
|
ser.Serialize(writer, obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async ValueTask ToJsonAsync<T>(this T obj, AbsolutePath path, bool useGenericSettings = false)
|
public static async ValueTask ToJsonAsync<T>(this T obj, AbsolutePath path, bool useGenericSettings = false, bool prettyPrint = false)
|
||||||
{
|
{
|
||||||
await using var fs = await path.Create();
|
await using var fs = await path.Create();
|
||||||
obj.ToJson(fs, useGenericSettings);
|
obj.ToJson(fs, useGenericSettings, prettyPrint: prettyPrint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ToJson<T>(this T obj, bool useGenericSettings = false)
|
public static string ToJson<T>(this T obj, bool useGenericSettings = false)
|
||||||
|
@ -95,7 +95,7 @@ namespace Wabbajack.Common
|
|||||||
public ValueTask<FileStream> OpenWrite()
|
public ValueTask<FileStream> OpenWrite()
|
||||||
{
|
{
|
||||||
var path = _path;
|
var path = _path;
|
||||||
return CircuitBreaker.WithAutoRetryAsync<FileStream, IOException>(async () => File.OpenWrite(path));
|
return CircuitBreaker.WithAutoRetryAsync<FileStream, IOException>(async () => File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task WriteAllTextAsync(string text)
|
public async Task WriteAllTextAsync(string text)
|
||||||
@ -275,7 +275,7 @@ namespace Wabbajack.Common
|
|||||||
|
|
||||||
public bool InFolder(AbsolutePath folder)
|
public bool InFolder(AbsolutePath folder)
|
||||||
{
|
{
|
||||||
return _path.StartsWith(folder._path + Path.DirectorySeparator);
|
return _path.StartsWith(folder._path + Path.DirectorySeparator, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<byte[]> ReadAllBytesAsync()
|
public async Task<byte[]> ReadAllBytesAsync()
|
||||||
@ -386,7 +386,7 @@ namespace Wabbajack.Common
|
|||||||
|
|
||||||
public int CompareTo(AbsolutePath other)
|
public int CompareTo(AbsolutePath other)
|
||||||
{
|
{
|
||||||
return string.Compare(_path, other._path, StringComparison.Ordinal);
|
return string.Compare(_path, other._path, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ReadAllText()
|
public string ReadAllText()
|
||||||
|
@ -136,12 +136,12 @@ namespace Wabbajack.Common
|
|||||||
|
|
||||||
public bool StartsWith(string s)
|
public bool StartsWith(string s)
|
||||||
{
|
{
|
||||||
return _path.StartsWith(s);
|
return _path.StartsWith(s, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool StartsWith(RelativePath s)
|
public bool StartsWith(RelativePath s)
|
||||||
{
|
{
|
||||||
return _path.StartsWith(s._path);
|
return _path.StartsWith(s._path, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RelativePath Combine(params RelativePath[] paths )
|
public RelativePath Combine(params RelativePath[] paths )
|
||||||
@ -156,7 +156,7 @@ namespace Wabbajack.Common
|
|||||||
|
|
||||||
public int CompareTo(RelativePath other)
|
public int CompareTo(RelativePath other)
|
||||||
{
|
{
|
||||||
return string.Compare(_path, other._path, StringComparison.Ordinal);
|
return string.Compare(_path, other._path, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ namespace Wabbajack.Common
|
|||||||
public class StatusUpdateTracker
|
public class StatusUpdateTracker
|
||||||
{
|
{
|
||||||
private Subject<string> _stepName = new Subject<string>();
|
private Subject<string> _stepName = new Subject<string>();
|
||||||
public IObservable<string> StepName => _stepName;
|
public IObservable<string> StepName => _stepName.Debounce(TimeSpan.FromMilliseconds(100));
|
||||||
|
|
||||||
private Subject<int> _step = new Subject<int>();
|
private Subject<int> _step = new Subject<int>();
|
||||||
public IObservable<int> Step => _step;
|
public IObservable<int> Step => _step;
|
||||||
@ -20,6 +20,7 @@ namespace Wabbajack.Common
|
|||||||
|
|
||||||
private int _internalCurrentStep;
|
private int _internalCurrentStep;
|
||||||
private int _internalMaxStep;
|
private int _internalMaxStep;
|
||||||
|
private string _currentStepName = "";
|
||||||
|
|
||||||
public StatusUpdateTracker(int maxStep)
|
public StatusUpdateTracker(int maxStep)
|
||||||
{
|
{
|
||||||
@ -33,10 +34,11 @@ namespace Wabbajack.Common
|
|||||||
|
|
||||||
public void NextStep(string name)
|
public void NextStep(string name)
|
||||||
{
|
{
|
||||||
|
_currentStepName = name;
|
||||||
_internalCurrentStep += 1;
|
_internalCurrentStep += 1;
|
||||||
Utils.Log(name);
|
Utils.Log(name);
|
||||||
_step.OnNext(_internalCurrentStep);
|
_step.OnNext(_internalCurrentStep);
|
||||||
_stepName.OnNext(name);
|
_stepName.OnNext($"({_internalCurrentStep}/{_internalMaxStep}) {_currentStepName}");
|
||||||
MakeUpdate(Percent.Zero);
|
MakeUpdate(Percent.Zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,6 +64,7 @@ namespace Wabbajack.Common
|
|||||||
public void MakeUpdate(int max, int curr)
|
public void MakeUpdate(int max, int curr)
|
||||||
{
|
{
|
||||||
MakeUpdate(Percent.FactoryPutInRange(curr, max == 0 ? 1 : max));
|
MakeUpdate(Percent.FactoryPutInRange(curr, max == 0 ? 1 : max));
|
||||||
|
_stepName.OnNext($"({_internalCurrentStep}/{_internalMaxStep}) {_currentStepName}, {curr} of {max}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
86
Wabbajack.Common/StoreHandlers/EpicGameStoreHandler.cs
Normal file
86
Wabbajack.Common/StoreHandlers/EpicGameStoreHandler.cs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace Wabbajack.Common.StoreHandlers
|
||||||
|
{
|
||||||
|
public class EpicGameStoreHandler : AStoreHandler
|
||||||
|
{
|
||||||
|
public override StoreType Type { get; internal set; }
|
||||||
|
|
||||||
|
public string BaseRegKey = @"SOFTWARE\Epic Games\EOS";
|
||||||
|
public override bool Init()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool LoadAllGames()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var eosKey = Registry.CurrentUser.OpenSubKey(BaseRegKey);
|
||||||
|
if (eosKey == null)
|
||||||
|
{
|
||||||
|
Utils.Log("Epic Game Store is not installed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = eosKey.GetValue("ModSdkMetadataDir");
|
||||||
|
if (name == null)
|
||||||
|
{
|
||||||
|
Utils.Log("Registry key entry does not exist for Epic Game store");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var byID = GameRegistry.Games.SelectMany(g => g.Value.EpicGameStoreIDs
|
||||||
|
.Select(id => (id, g.Value.Game)))
|
||||||
|
.GroupBy(t => t.id)
|
||||||
|
.ToDictionary(t => t.Key, t => t.First().Game);
|
||||||
|
|
||||||
|
foreach (var itm in ((AbsolutePath)(string)(name!)).EnumerateFiles(false, "*.item"))
|
||||||
|
{
|
||||||
|
var item = itm.FromJson<EpicGameItem>();
|
||||||
|
Utils.Log($"Found Epic Game Store Game: {item.DisplayName} at {item.InstallLocation}");
|
||||||
|
|
||||||
|
if (byID.TryGetValue(item.CatalogItemId, out var game))
|
||||||
|
{
|
||||||
|
Games.Add(new EpicStoreGame(game, item));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (NullReferenceException ex)
|
||||||
|
{
|
||||||
|
Utils.Log("Epic Game Store is does not appear to be installed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EpicStoreGame : AStoreGame
|
||||||
|
{
|
||||||
|
public EpicStoreGame(Game game, EpicGameItem item)
|
||||||
|
{
|
||||||
|
Type = StoreType.EpicGameStore;
|
||||||
|
Game = game;
|
||||||
|
Path = (AbsolutePath)item.InstallLocation;
|
||||||
|
Name = game.MetaData().HumanFriendlyGameName;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Game Game { get; internal set; }
|
||||||
|
public override StoreType Type { get; internal set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EpicGameItem
|
||||||
|
{
|
||||||
|
public string DisplayName { get; set; } = "";
|
||||||
|
public string InstallationGuid { get; set; } = "";
|
||||||
|
public string CatalogItemId { get; set; } = "";
|
||||||
|
public string CatalogNamespace { get; set; } = "";
|
||||||
|
public string InstallSessionId { get; set; } = "";
|
||||||
|
public string InstallLocation { get; set; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,8 @@ namespace Wabbajack.Common.StoreHandlers
|
|||||||
{
|
{
|
||||||
STEAM,
|
STEAM,
|
||||||
GOG,
|
GOG,
|
||||||
BethNet
|
BethNet,
|
||||||
|
EpicGameStore
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StoreHandler
|
public class StoreHandler
|
||||||
@ -25,6 +26,9 @@ namespace Wabbajack.Common.StoreHandlers
|
|||||||
|
|
||||||
private static readonly Lazy<BethNetHandler> _bethNetHandler = new Lazy<BethNetHandler>(() => new BethNetHandler());
|
private static readonly Lazy<BethNetHandler> _bethNetHandler = new Lazy<BethNetHandler>(() => new BethNetHandler());
|
||||||
public BethNetHandler BethNetHandler = _bethNetHandler.Value;
|
public BethNetHandler BethNetHandler = _bethNetHandler.Value;
|
||||||
|
|
||||||
|
private static readonly Lazy<EpicGameStoreHandler> _epicGameStoreHandler = new Lazy<EpicGameStoreHandler>(() => new EpicGameStoreHandler());
|
||||||
|
public EpicGameStoreHandler EpicGameStoreHandler = _epicGameStoreHandler.Value;
|
||||||
|
|
||||||
public List<AStoreGame> StoreGames;
|
public List<AStoreGame> StoreGames;
|
||||||
|
|
||||||
@ -67,6 +71,18 @@ namespace Wabbajack.Common.StoreHandlers
|
|||||||
{
|
{
|
||||||
Utils.Error(new StoreException("Could not Init the BethNetHandler, check previous error messages!"));
|
Utils.Error(new StoreException("Could not Init the BethNetHandler, check previous error messages!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (EpicGameStoreHandler.Init())
|
||||||
|
{
|
||||||
|
if (EpicGameStoreHandler.LoadAllGames())
|
||||||
|
StoreGames.AddRange(EpicGameStoreHandler.Games);
|
||||||
|
else
|
||||||
|
Utils.Error(new StoreException("Could not load all Games from the EpicGameStoreHandler, check previous error messages!"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Utils.Error(new StoreException("Could not Init the EpicGameStoreHandler, check previous error messages!"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public AbsolutePath? TryGetGamePath(Game game)
|
public AbsolutePath? TryGetGamePath(Game game)
|
||||||
|
@ -866,7 +866,7 @@ namespace Wabbajack.Common
|
|||||||
|
|
||||||
public static bool IsInPath(this string path, string parent)
|
public static bool IsInPath(this string path, string parent)
|
||||||
{
|
{
|
||||||
return path.ToLower().TrimEnd('\\').StartsWith(parent.ToLower().TrimEnd('\\') + "\\");
|
return path.ToLower().TrimEnd('\\').StartsWith(parent.ToLower().TrimEnd('\\') + "\\", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task CopyToLimitAsync(this Stream frm, Stream tw, long limit)
|
public static async Task CopyToLimitAsync(this Stream frm, Stream tw, long limit)
|
||||||
|
@ -49,19 +49,19 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Genbox.AlphaFS" Version="2.2.2.1" />
|
<PackageReference Include="Genbox.AlphaFS" Version="2.2.2.1" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.26" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.28" />
|
||||||
<PackageReference Include="ini-parser-netstandard" Version="2.5.2" />
|
<PackageReference Include="ini-parser-netstandard" Version="2.5.2" />
|
||||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0-preview.8.20407.11" />
|
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="Octodiff" Version="1.2.1" />
|
<PackageReference Include="Octodiff" Version="1.2.1" />
|
||||||
<PackageReference Include="RocksDbNative" Version="6.2.2" />
|
<PackageReference Include="RocksDbNative" Version="6.2.2" />
|
||||||
<PackageReference Include="RocksDbSharp" Version="6.2.2" />
|
<PackageReference Include="RocksDbSharp" Version="6.2.2" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.3.0" />
|
<PackageReference Include="SharpZipLib" Version="1.3.1" />
|
||||||
<PackageReference Include="System.Data.HashFunction.xxHash" Version="2.0.0" />
|
<PackageReference Include="System.Data.HashFunction.xxHash" Version="2.0.0" />
|
||||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||||
<PackageReference Include="System.Reactive" Version="4.4.1" />
|
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="5.0.0-preview.8.20407.11" />
|
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Security.Principal.Windows" Version="5.0.0-preview.8.20407.11" />
|
<PackageReference Include="System.Security.Principal.Windows" Version="5.0.0" />
|
||||||
<PackageReference Include="YamlDotNet" Version="8.1.2" />
|
<PackageReference Include="YamlDotNet" Version="8.1.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<AssemblyVersion>2.3.2.0</AssemblyVersion>
|
<AssemblyVersion>2.3.5.1</AssemblyVersion>
|
||||||
<FileVersion>2.3.2.0</FileVersion>
|
<FileVersion>2.3.5.1</FileVersion>
|
||||||
<Copyright>Copyright © 2019-2020</Copyright>
|
<Copyright>Copyright © 2019-2020</Copyright>
|
||||||
<Description>Wabbajack Application Launcher</Description>
|
<Description>Wabbajack Application Launcher</Description>
|
||||||
<PublishReadyToRun>true</PublishReadyToRun>
|
<PublishReadyToRun>true</PublishReadyToRun>
|
||||||
|
@ -181,7 +181,7 @@ namespace Wabbajack.Lib
|
|||||||
await file.To.RelativeTo(OutputFolder).Compact(FileCompaction.Algorithm.XPRESS16K);
|
await file.To.RelativeTo(OutputFolder).Compact(FileCompaction.Algorithm.XPRESS16K);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, tempFolder: OutputFolder);
|
}, tempFolder: OutputFolder, updateTracker: UpdateTracker);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DownloadArchives()
|
public async Task DownloadArchives()
|
||||||
@ -195,6 +195,12 @@ namespace Wabbajack.Lib
|
|||||||
|
|
||||||
await Task.WhenAll(dispatchers.Select(d => d.Prepare()));
|
await Task.WhenAll(dispatchers.Select(d => d.Prepare()));
|
||||||
|
|
||||||
|
var nexusDownloader = dispatchers.OfType<NexusDownloader>().FirstOrDefault();
|
||||||
|
if (nexusDownloader != null && !await nexusDownloader.HaveEnoughAPICalls(missing))
|
||||||
|
{
|
||||||
|
throw new Exception($"Not enough Nexus API calls to download this list, please try again after midnight GMT when your API limits reset");
|
||||||
|
}
|
||||||
|
|
||||||
await DownloadMissingArchives(missing);
|
await DownloadMissingArchives(missing);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,6 +208,7 @@ namespace Wabbajack.Lib
|
|||||||
{
|
{
|
||||||
if (download)
|
if (download)
|
||||||
{
|
{
|
||||||
|
var result = SendDownloadMetrics(missing);
|
||||||
foreach (var a in missing.Where(a => a.State.GetType() == typeof(ManualDownloader.State)))
|
foreach (var a in missing.Where(a => a.State.GetType() == typeof(ManualDownloader.State)))
|
||||||
{
|
{
|
||||||
var outputPath = DownloadFolder.Combine(a.Name);
|
var outputPath = DownloadFolder.Combine(a.Name);
|
||||||
@ -210,8 +217,9 @@ namespace Wabbajack.Lib
|
|||||||
}
|
}
|
||||||
|
|
||||||
DesiredThreads.OnNext(DownloadThreads);
|
DesiredThreads.OnNext(DownloadThreads);
|
||||||
|
|
||||||
await missing.Where(a => a.State.GetType() != typeof(ManualDownloader.State))
|
await missing.Where(a => a.State.GetType() != typeof(ManualDownloader.State))
|
||||||
.PMap(Queue, async archive =>
|
.PMap(Queue, UpdateTracker, async archive =>
|
||||||
{
|
{
|
||||||
Info($"Downloading {archive.Name}");
|
Info($"Downloading {archive.Name}");
|
||||||
var outputPath = DownloadFolder.Combine(archive.Name);
|
var outputPath = DownloadFolder.Combine(archive.Name);
|
||||||
@ -235,6 +243,15 @@ namespace Wabbajack.Lib
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SendDownloadMetrics(List<Archive> missing)
|
||||||
|
{
|
||||||
|
var grouped = missing.GroupBy(m => m.State.GetType());
|
||||||
|
foreach (var group in grouped)
|
||||||
|
{
|
||||||
|
await Metrics.Send($"downloading_{group.Key.FullName!.Split(".").Last().Split("+").First()}", group.Sum(g => g.Size).ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> DownloadArchive(Archive archive, bool download, AbsolutePath? destination = null)
|
public async Task<bool> DownloadArchive(Archive archive, bool download, AbsolutePath? destination = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -268,7 +285,7 @@ namespace Wabbajack.Lib
|
|||||||
|
|
||||||
var hashResults = await
|
var hashResults = await
|
||||||
toHash
|
toHash
|
||||||
.PMap(Queue, async e => (await e.FileHashCachedAsync(), e));
|
.PMap(Queue, UpdateTracker,async e => (await e.FileHashCachedAsync(), e));
|
||||||
|
|
||||||
HashedArchives.SetTo(hashResults
|
HashedArchives.SetTo(hashResults
|
||||||
.OrderByDescending(e => e.Item2.LastModified)
|
.OrderByDescending(e => e.Item2.LastModified)
|
||||||
@ -385,9 +402,6 @@ namespace Wabbajack.Lib
|
|||||||
var path = OutputFolder.Combine(d.To);
|
var path = OutputFolder.Combine(d.To);
|
||||||
if (!existingfiles.Contains(path)) return null;
|
if (!existingfiles.Contains(path)) return null;
|
||||||
|
|
||||||
if (path.Size != d.Size) return null;
|
|
||||||
Status($"Optimizing {d.To}");
|
|
||||||
|
|
||||||
return await path.FileHashCachedAsync() == d.Hash ? d : null;
|
return await path.FileHashCachedAsync() == d.Hash ? d : null;
|
||||||
}))
|
}))
|
||||||
.Do(d =>
|
.Do(d =>
|
||||||
|
@ -157,9 +157,7 @@ using Wabbajack.Lib.Downloaders;
|
|||||||
return new Archive[0];
|
return new Archive[0];
|
||||||
var client = await GetClient();
|
var client = await GetClient();
|
||||||
var metaData = game.MetaData();
|
var metaData = game.MetaData();
|
||||||
var results =
|
var results = await GetGameFilesFromGithub(game, metaData.InstalledVersion);
|
||||||
await client.GetJsonAsync<Archive[]>(
|
|
||||||
$"{Consts.WabbajackBuildServerUri}game_files/{game}/{metaData.InstalledVersion}");
|
|
||||||
|
|
||||||
return (await results.PMap(queue, async file => (await file.State.Verify(file), file))).Where(f => f.Item1)
|
return (await results.PMap(queue, async file => (await file.State.Verify(file), file))).Where(f => f.Item1)
|
||||||
.Select(f =>
|
.Select(f =>
|
||||||
@ -169,6 +167,22 @@ using Wabbajack.Lib.Downloaders;
|
|||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<Archive[]> GetGameFilesFromGithub(Game game, string version)
|
||||||
|
{
|
||||||
|
var url =
|
||||||
|
$"https://raw.githubusercontent.com/wabbajack-tools/indexed-game-files/master/{game}/{version}.json";
|
||||||
|
Utils.Log($"Loading game file definition from {url}");
|
||||||
|
var client = await GetClient();
|
||||||
|
return await client.GetJsonAsync<Archive[]>(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<Archive[]> GetGameFilesFromServer(Game game, string version)
|
||||||
|
{
|
||||||
|
var client = await GetClient();
|
||||||
|
return await client.GetJsonAsync<Archive[]>(
|
||||||
|
$"{Consts.WabbajackBuildServerUri}game_files/{game}/{version}");
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<AbstractDownloadState?> InferDownloadState(Hash hash)
|
public static async Task<AbstractDownloadState?> InferDownloadState(Hash hash)
|
||||||
{
|
{
|
||||||
@ -250,7 +264,7 @@ using Wabbajack.Lib.Downloaders;
|
|||||||
await client.GetStringAsync($"{Consts.WabbajackBuildServerUri}mirror/{archiveHash.ToHex()}");
|
await client.GetStringAsync($"{Consts.WabbajackBuildServerUri}mirror/{archiveHash.ToHex()}");
|
||||||
return new Uri(result);
|
return new Uri(result);
|
||||||
}
|
}
|
||||||
catch (HttpException ex)
|
catch (HttpException)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -262,5 +276,14 @@ using Wabbajack.Lib.Downloaders;
|
|||||||
return await client.GetJsonAsync<Helpers.Cookie[]>(
|
return await client.GetJsonAsync<Helpers.Cookie[]>(
|
||||||
$"{Consts.WabbajackBuildServerUri}site-integration/auth-info/{key}");
|
$"{Consts.WabbajackBuildServerUri}site-integration/auth-info/{key}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<IEnumerable<(Game, string)>> GetServerGamesAndVersions()
|
||||||
|
{
|
||||||
|
var client = await GetClient();
|
||||||
|
var results =
|
||||||
|
await client.GetJsonAsync<(Game, string)[]>(
|
||||||
|
$"{Consts.WabbajackBuildServerUri}game_files");
|
||||||
|
return results;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ namespace Wabbajack.Lib.CompilationSteps
|
|||||||
|
|
||||||
public override async ValueTask<Directive?> Run(RawSourceFile source)
|
public override async ValueTask<Directive?> Run(RawSourceFile source)
|
||||||
{
|
{
|
||||||
if (!((string)source.Path).StartsWith(_startDir)) return null;
|
if (!((string)source.Path).StartsWith(_startDir, System.StringComparison.OrdinalIgnoreCase)) return null;
|
||||||
var i = source.EvolveTo<IgnoredDirectly>();
|
var i = source.EvolveTo<IgnoredDirectly>();
|
||||||
i.Reason = "Default game file";
|
i.Reason = "Default game file";
|
||||||
return i;
|
return i;
|
||||||
|
@ -16,7 +16,6 @@ namespace Wabbajack.Lib.CompilationSteps
|
|||||||
private readonly Dictionary<RelativePath, IGrouping<RelativePath, VirtualFile>> _indexed;
|
private readonly Dictionary<RelativePath, IGrouping<RelativePath, VirtualFile>> _indexed;
|
||||||
private VirtualFile? _bsa;
|
private VirtualFile? _bsa;
|
||||||
private Dictionary<RelativePath, IEnumerable<VirtualFile>> _indexedByName;
|
private Dictionary<RelativePath, IEnumerable<VirtualFile>> _indexedByName;
|
||||||
private ACompiler _compiler;
|
|
||||||
private bool _isGenericGame;
|
private bool _isGenericGame;
|
||||||
|
|
||||||
public IncludePatches(ACompiler compiler, VirtualFile? constructingFromBSA = null) : base(compiler)
|
public IncludePatches(ACompiler compiler, VirtualFile? constructingFromBSA = null) : base(compiler)
|
||||||
|
@ -68,6 +68,16 @@ namespace Wabbajack.Lib.Downloaders
|
|||||||
IsAttachment = true
|
IsAttachment = true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (url.PathAndQuery.StartsWith("/files/download/") && long.TryParse(url.PathAndQuery.Split("/").Last(), out var fileId))
|
||||||
|
{
|
||||||
|
return new TState
|
||||||
|
{
|
||||||
|
FullURL = url.ToString(),
|
||||||
|
IsAttachment = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!url.PathAndQuery.StartsWith("/files/file/"))
|
if (!url.PathAndQuery.StartsWith("/files/file/"))
|
||||||
{
|
{
|
||||||
@ -76,6 +86,7 @@ namespace Wabbajack.Lib.Downloaders
|
|||||||
absolute = false;
|
absolute = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var id = HttpUtility.ParseQueryString(url.Query)["r"];
|
var id = HttpUtility.ParseQueryString(url.Query)["r"];
|
||||||
var file = absolute
|
var file = absolute
|
||||||
? url.AbsolutePath.Split('/').Last(s => s != "")
|
? url.AbsolutePath.Split('/').Last(s => s != "")
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
@ -142,6 +143,16 @@ namespace Wabbajack.Lib.Downloaders
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> HaveEnoughAPICalls(IEnumerable<Archive> archives)
|
||||||
|
{
|
||||||
|
if (await Client!.IsPremium())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var count = archives.Select(a => a.State).OfType<State>().Count();
|
||||||
|
|
||||||
|
return count < Client!.RemainingAPICalls;
|
||||||
|
}
|
||||||
|
|
||||||
[JsonName("NexusDownloader")]
|
[JsonName("NexusDownloader")]
|
||||||
public class State : AbstractDownloadState, IMetaState, IUpgradingState
|
public class State : AbstractDownloadState, IMetaState, IUpgradingState
|
||||||
{
|
{
|
||||||
@ -189,14 +200,16 @@ namespace Wabbajack.Lib.Downloaders
|
|||||||
var client = await NexusApiClient.Get();
|
var client = await NexusApiClient.Get();
|
||||||
url = await client.GetNexusDownloadLink(this);
|
url = await client.GetNexusDownloadLink(this);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (NexusAPIQuotaExceeded ex)
|
||||||
|
{
|
||||||
|
Utils.Log(ex.ExtendedDescription);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
{
|
{
|
||||||
Utils.Log($"{a.Name} - Error getting Nexus download URL - {ex.Message}");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils.Log($"Downloading Nexus Archive - {a.Name} - {Game} - {ModID} - {FileID}");
|
|
||||||
|
|
||||||
return await new HTTPDownloader.State(url).Download(a, destination);
|
return await new HTTPDownloader.State(url).Download(a, destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,7 +252,11 @@ namespace Wabbajack.Lib.Downloaders
|
|||||||
|
|
||||||
public override async Task<(Archive? Archive, TempFile NewFile)> FindUpgrade(Archive a, Func<Archive, Task<AbsolutePath>> downloadResolver)
|
public override async Task<(Archive? Archive, TempFile NewFile)> FindUpgrade(Archive a, Func<Archive, Task<AbsolutePath>> downloadResolver)
|
||||||
{
|
{
|
||||||
var client = await NexusApiClient.Get();
|
var client = DownloadDispatcher.GetInstance<NexusDownloader>().Client ?? await NexusApiClient.Get();
|
||||||
|
await client.IsPremium();
|
||||||
|
|
||||||
|
if (client.RemainingAPICalls <= 0)
|
||||||
|
throw new NexusAPIQuotaExceeded();
|
||||||
|
|
||||||
var mod = await client.GetModInfo(Game, ModID);
|
var mod = await client.GetModInfo(Game, ModID);
|
||||||
if (!mod.available)
|
if (!mod.available)
|
||||||
@ -274,6 +291,7 @@ namespace Wabbajack.Lib.Downloaders
|
|||||||
return (newArchive, new TempFile());
|
return (newArchive, new TempFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Utils.Log($"Downloading possible upgrade {newArchive.State.PrimaryKeyString}");
|
||||||
var tempFile = new TempFile();
|
var tempFile = new TempFile();
|
||||||
|
|
||||||
await newArchive.State.Download(newArchive, tempFile.Path);
|
await newArchive.State.Download(newArchive, tempFile.Path);
|
||||||
@ -281,6 +299,8 @@ namespace Wabbajack.Lib.Downloaders
|
|||||||
newArchive.Size = tempFile.Path.Size;
|
newArchive.Size = tempFile.Path.Size;
|
||||||
newArchive.Hash = await tempFile.Path.FileHashAsync();
|
newArchive.Hash = await tempFile.Path.FileHashAsync();
|
||||||
|
|
||||||
|
Utils.Log($"Possible upgrade {newArchive.State.PrimaryKeyString} downloaded");
|
||||||
|
|
||||||
return (newArchive, tempFile);
|
return (newArchive, tempFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ namespace Wabbajack.Lib.Downloaders
|
|||||||
{
|
{
|
||||||
{"wabbajack.b-cdn.net", "authored-files.wabbajack.org"},
|
{"wabbajack.b-cdn.net", "authored-files.wabbajack.org"},
|
||||||
{"wabbajack-mirror.b-cdn.net", "mirror.wabbajack.org"},
|
{"wabbajack-mirror.b-cdn.net", "mirror.wabbajack.org"},
|
||||||
{"wabbajack-patches.b-cdn-net", "patches.wabbajack.org"},
|
{"wabbajack-patches.b-cdn.net", "patches.wabbajack.org"},
|
||||||
{"wabbajacktest.b-cdn.net", "test-files.wabbajack.org"}
|
{"wabbajacktest.b-cdn.net", "test-files.wabbajack.org"}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ namespace Wabbajack
|
|||||||
{
|
{
|
||||||
return source
|
return source
|
||||||
.ToProperty(vm, property, initialValue, deferSubscription, RxApp.MainThreadScheduler)
|
.ToProperty(vm, property, initialValue, deferSubscription, RxApp.MainThreadScheduler)
|
||||||
.DisposeWith(vm.CompositeDisposable);
|
.DisposeWith(vm.CompositeDisposable)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void ToGuiProperty<TRet>(
|
public static void ToGuiProperty<TRet>(
|
||||||
@ -116,7 +116,7 @@ namespace Wabbajack
|
|||||||
TRet initialValue = default,
|
TRet initialValue = default,
|
||||||
bool deferSubscription = false)
|
bool deferSubscription = false)
|
||||||
{
|
{
|
||||||
source.ToProperty(vm, property, out result, initialValue, deferSubscription, RxApp.MainThreadScheduler)
|
source.ToProperty(vm, property, out result!, initialValue, deferSubscription, RxApp.MainThreadScheduler)
|
||||||
.DisposeWith(vm.CompositeDisposable);
|
.DisposeWith(vm.CompositeDisposable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,8 +15,6 @@ namespace Wabbajack.Lib
|
|||||||
{
|
{
|
||||||
public class MO2Compiler : ACompiler
|
public class MO2Compiler : ACompiler
|
||||||
{
|
{
|
||||||
private AbsolutePath _mo2DownloadsFolder;
|
|
||||||
|
|
||||||
public MO2Compiler(AbsolutePath sourcePath, AbsolutePath downloadsPath, string mo2Profile, AbsolutePath outputFile)
|
public MO2Compiler(AbsolutePath sourcePath, AbsolutePath downloadsPath, string mo2Profile, AbsolutePath outputFile)
|
||||||
: base(21, mo2Profile, sourcePath, downloadsPath, outputFile)
|
: base(21, mo2Profile, sourcePath, downloadsPath, outputFile)
|
||||||
{
|
{
|
||||||
@ -202,7 +200,7 @@ namespace Wabbajack.Lib
|
|||||||
// Find all Downloads
|
// Find all Downloads
|
||||||
IndexedArchives = (await DownloadsPath.EnumerateFiles()
|
IndexedArchives = (await DownloadsPath.EnumerateFiles()
|
||||||
.Where(f => f.WithExtension(Consts.MetaFileExtension).Exists)
|
.Where(f => f.WithExtension(Consts.MetaFileExtension).Exists)
|
||||||
.PMap(Queue,
|
.PMap(Queue, UpdateTracker,
|
||||||
async f => new IndexedArchive(VFS.Index.ByRootPath[f])
|
async f => new IndexedArchive(VFS.Index.ByRootPath[f])
|
||||||
{
|
{
|
||||||
Name = (string)f.FileName,
|
Name = (string)f.FileName,
|
||||||
|
@ -257,7 +257,7 @@ namespace Wabbajack.Lib
|
|||||||
private async Task InstallIncludedDownloadMetas()
|
private async Task InstallIncludedDownloadMetas()
|
||||||
{
|
{
|
||||||
await ModList.Archives
|
await ModList.Archives
|
||||||
.PMap(Queue, async archive =>
|
.PMap(Queue, UpdateTracker, async archive =>
|
||||||
{
|
{
|
||||||
if (HashedArchives.TryGetValue(archive.Hash, out var paths))
|
if (HashedArchives.TryGetValue(archive.Hash, out var paths))
|
||||||
{
|
{
|
||||||
@ -313,7 +313,7 @@ namespace Wabbajack.Lib
|
|||||||
var bsaSize = bsa.FileStates.Select(state => sourceDir.Combine(state.Path).Size).Sum();
|
var bsaSize = bsa.FileStates.Select(state => sourceDir.Combine(state.Path).Size).Sum();
|
||||||
|
|
||||||
await using var a = await bsa.State.MakeBuilder(bsaSize);
|
await using var a = await bsa.State.MakeBuilder(bsaSize);
|
||||||
var streams = await bsa.FileStates.PMap(Queue, async state =>
|
var streams = await bsa.FileStates.PMap(Queue, UpdateTracker, async state =>
|
||||||
{
|
{
|
||||||
Status($"Adding {state.Path} to BSA");
|
Status($"Adding {state.Path} to BSA");
|
||||||
var fs = await sourceDir.Combine(state.Path).OpenRead();
|
var fs = await sourceDir.Combine(state.Path).OpenRead();
|
||||||
@ -344,7 +344,7 @@ namespace Wabbajack.Lib
|
|||||||
Info("Writing inline files");
|
Info("Writing inline files");
|
||||||
await ModList.Directives
|
await ModList.Directives
|
||||||
.OfType<InlineFile>()
|
.OfType<InlineFile>()
|
||||||
.PMap(Queue, async directive =>
|
.PMap(Queue, UpdateTracker, async directive =>
|
||||||
{
|
{
|
||||||
Status($"Writing included file {directive.To}");
|
Status($"Writing included file {directive.To}");
|
||||||
var outPath = OutputFolder.Combine(directive.To);
|
var outPath = OutputFolder.Combine(directive.To);
|
||||||
|
@ -34,6 +34,15 @@ namespace Wabbajack.Lib.ModListRegistry
|
|||||||
|
|
||||||
[JsonProperty("nsfw")]
|
[JsonProperty("nsfw")]
|
||||||
public bool NSFW { get; set; }
|
public bool NSFW { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("utility_list")]
|
||||||
|
public bool UtilityList { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("image_contains_title")]
|
||||||
|
public bool ImageContainsTitle { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("force_down")]
|
||||||
|
public bool ForceDown { get; set; }
|
||||||
|
|
||||||
[JsonProperty("links")]
|
[JsonProperty("links")]
|
||||||
public LinksObject Links { get; set; } = new LinksObject();
|
public LinksObject Links { get; set; } = new LinksObject();
|
||||||
@ -65,9 +74,11 @@ namespace Wabbajack.Lib.ModListRegistry
|
|||||||
var client = new Http.Client();
|
var client = new Http.Client();
|
||||||
Utils.Log("Loading ModLists from GitHub");
|
Utils.Log("Loading ModLists from GitHub");
|
||||||
var metadataResult = client.GetStringAsync(Consts.ModlistMetadataURL);
|
var metadataResult = client.GetStringAsync(Consts.ModlistMetadataURL);
|
||||||
|
var utilityResult = client.GetStringAsync(Consts.UtilityModlistMetadataURL);
|
||||||
var summaryResult = client.GetStringAsync(Consts.ModlistSummaryURL);
|
var summaryResult = client.GetStringAsync(Consts.ModlistSummaryURL);
|
||||||
|
|
||||||
var metadata = (await metadataResult).FromJsonString<List<ModlistMetadata>>();
|
var metadata = (await metadataResult).FromJsonString<List<ModlistMetadata>>();
|
||||||
|
metadata = metadata.Concat((await utilityResult).FromJsonString<List<ModlistMetadata>>()).ToList();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var summaries = (await summaryResult).FromJsonString<List<ModListSummary>>().ToDictionary(d => d.MachineURL);
|
var summaries = (await summaryResult).FromJsonString<List<ModListSummary>>().ToDictionary(d => d.MachineURL);
|
||||||
|
@ -13,5 +13,7 @@ namespace Wabbajack.Lib.NexusApi
|
|||||||
public Task<UserStatus> GetUserStatus();
|
public Task<UserStatus> GetUserStatus();
|
||||||
public Task<bool> IsPremium();
|
public Task<bool> IsPremium();
|
||||||
public bool IsAuthenticated { get; }
|
public bool IsAuthenticated { get; }
|
||||||
|
|
||||||
|
public int RemainingAPICalls { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ namespace Wabbajack.Lib.NexusApi
|
|||||||
public static string? ApiKey { get; set; }
|
public static string? ApiKey { get; set; }
|
||||||
|
|
||||||
public bool IsAuthenticated => ApiKey != null;
|
public bool IsAuthenticated => ApiKey != null;
|
||||||
|
public int RemainingAPICalls => Math.Max(HourlyRemaining, DailyRemaining);
|
||||||
|
|
||||||
private Task<UserStatus>? _userStatus;
|
private Task<UserStatus>? _userStatus;
|
||||||
public Task<UserStatus> UserStatus
|
public Task<UserStatus> UserStatus
|
||||||
@ -70,7 +71,7 @@ namespace Wabbajack.Lib.NexusApi
|
|||||||
{
|
{
|
||||||
return env_key;
|
return env_key;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await RequestAndCacheAPIKey();
|
return await RequestAndCacheAPIKey();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,7 +155,12 @@ namespace Wabbajack.Lib.NexusApi
|
|||||||
public async Task<UserStatus> GetUserStatus()
|
public async Task<UserStatus> GetUserStatus()
|
||||||
{
|
{
|
||||||
var url = "https://api.nexusmods.com/v1/users/validate.json";
|
var url = "https://api.nexusmods.com/v1/users/validate.json";
|
||||||
return await Get<UserStatus>(url);
|
var result = await Get<UserStatus>(url);
|
||||||
|
|
||||||
|
Utils.Log($"Logged into the nexus as {result.name}");
|
||||||
|
Utils.Log($"Nexus calls remaining: {DailyRemaining} daily, {HourlyRemaining} hourly");
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(int, int)> GetRemainingApiCalls()
|
public async Task<(int, int)> GetRemainingApiCalls()
|
||||||
@ -213,24 +219,12 @@ namespace Wabbajack.Lib.NexusApi
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected virtual async Task UpdateRemaining(HttpResponseMessage response)
|
protected virtual async Task UpdateRemaining(HttpResponseMessage response)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var oldDaily = _dailyRemaining;
|
_dailyRemaining = int.Parse(response.Headers.GetValues("x-rl-daily-remaining").First());
|
||||||
var oldHourly = _hourlyRemaining;
|
_hourlyRemaining = int.Parse(response.Headers.GetValues("x-rl-hourly-remaining").First());
|
||||||
var dailyRemaining = int.Parse(response.Headers.GetValues("x-rl-daily-remaining").First());
|
|
||||||
var hourlyRemaining = int.Parse(response.Headers.GetValues("x-rl-hourly-remaining").First());
|
|
||||||
|
|
||||||
lock (RemainingLock)
|
|
||||||
{
|
|
||||||
_dailyRemaining = Math.Min(_dailyRemaining, dailyRemaining);
|
|
||||||
_hourlyRemaining = Math.Min(_hourlyRemaining, hourlyRemaining);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldDaily != _dailyRemaining || oldHourly != _hourlyRemaining)
|
|
||||||
Utils.Log($"Nexus requests remaining: {_dailyRemaining} daily - {_hourlyRemaining} hourly");
|
|
||||||
|
|
||||||
this.RaisePropertyChanged(nameof(DailyRemaining));
|
this.RaisePropertyChanged(nameof(DailyRemaining));
|
||||||
this.RaisePropertyChanged(nameof(HourlyRemaining));
|
this.RaisePropertyChanged(nameof(HourlyRemaining));
|
||||||
@ -317,25 +311,22 @@ namespace Wabbajack.Lib.NexusApi
|
|||||||
var info = await GetModInfo(archive.Game, archive.ModID);
|
var info = await GetModInfo(archive.Game, archive.ModID);
|
||||||
if (!info.available)
|
if (!info.available)
|
||||||
throw new Exception("Mod unavailable");
|
throw new Exception("Mod unavailable");
|
||||||
|
|
||||||
var url = $"https://api.nexusmods.com/v1/games/{archive.Game.MetaData().NexusName}/mods/{archive.ModID}/files/{archive.FileID}/download_link.json";
|
if (await IsPremium())
|
||||||
try
|
|
||||||
{
|
{
|
||||||
|
if (HourlyRemaining <= 0 && DailyRemaining <= 0)
|
||||||
|
{
|
||||||
|
throw new NexusAPIQuotaExceeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
var url =
|
||||||
|
$"https://api.nexusmods.com/v1/games/{archive.Game.MetaData().NexusName}/mods/{archive.ModID}/files/{archive.FileID}/download_link.json";
|
||||||
return (await Get<List<DownloadLink>>(url)).First().URI;
|
return (await Get<List<DownloadLink>>(url)).First().URI;
|
||||||
}
|
}
|
||||||
catch (HttpException ex)
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
if (ex.Code != 403 || await IsPremium())
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Utils.Log($"Requesting manual download for {archive.Name}");
|
Utils.Log($"Requesting manual download for {archive.Name} {archive.PrimaryKeyString}");
|
||||||
return (await Utils.Log(await ManuallyDownloadNexusFile.Create(archive)).Task).ToString();
|
return (await Utils.Log(await ManuallyDownloadNexusFile.Create(archive)).Task).ToString();
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException ex)
|
catch (TaskCanceledException ex)
|
||||||
|
12
Wabbajack.Lib/StatusMessages/NexusAPIQuotaExceeded.cs
Normal file
12
Wabbajack.Lib/StatusMessages/NexusAPIQuotaExceeded.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using Wabbajack.Common.StatusFeed;
|
||||||
|
|
||||||
|
namespace Wabbajack.Lib
|
||||||
|
{
|
||||||
|
public class NexusAPIQuotaExceeded : AErrorMessage
|
||||||
|
{
|
||||||
|
public override string ShortDescription => $"You have exceeded your Nexus API limit for the day";
|
||||||
|
|
||||||
|
public override string ExtendedDescription =>
|
||||||
|
"You have exceeded your Nexus API limit for the day, please try again after midnight GMT";
|
||||||
|
}
|
||||||
|
}
|
@ -8,10 +8,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CefSharp.Common">
|
<PackageReference Include="CefSharp.Common">
|
||||||
<Version>85.3.130</Version>
|
<Version>86.0.241</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="CefSharp.OffScreen">
|
<PackageReference Include="CefSharp.OffScreen">
|
||||||
<Version>85.3.130</Version>
|
<Version>86.0.241</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="F23.StringSimilarity">
|
<PackageReference Include="F23.StringSimilarity">
|
||||||
<Version>3.1.0</Version>
|
<Version>3.1.0</Version>
|
||||||
@ -25,7 +25,7 @@
|
|||||||
<Version>2.2.2.1</Version>
|
<Version>2.2.2.1</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="HtmlAgilityPack">
|
<PackageReference Include="HtmlAgilityPack">
|
||||||
<Version>1.11.26</Version>
|
<Version>1.11.28</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="MegaApiClient">
|
<PackageReference Include="MegaApiClient">
|
||||||
<Version>1.8.2</Version>
|
<Version>1.8.2</Version>
|
||||||
@ -46,16 +46,16 @@
|
|||||||
<Version>0.26.0</Version>
|
<Version>0.26.0</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="System.Collections.Immutable">
|
<PackageReference Include="System.Collections.Immutable">
|
||||||
<Version>5.0.0-preview.8.20407.11</Version>
|
<Version>5.0.0</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="System.Drawing.Common">
|
<PackageReference Include="System.Drawing.Common">
|
||||||
<Version>5.0.0-preview.8.20407.11</Version>
|
<Version>5.0.0</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="System.Net.Http">
|
<PackageReference Include="System.Net.Http">
|
||||||
<Version>4.3.4</Version>
|
<Version>4.3.4</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="System.ServiceModel.Syndication">
|
<PackageReference Include="System.ServiceModel.Syndication">
|
||||||
<Version>5.0.0-preview.8.20407.11</Version>
|
<Version>5.0.0</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="WebSocketSharp-netstandard">
|
<PackageReference Include="WebSocketSharp-netstandard">
|
||||||
<Version>1.0.1</Version>
|
<Version>1.0.1</Version>
|
||||||
|
25
Wabbajack.Server.Test/DiscordFrontentTests.cs
Normal file
25
Wabbajack.Server.Test/DiscordFrontentTests.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Wabbajack.BuildServer.Test;
|
||||||
|
using Wabbajack.Server.Services;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Wabbajack.Server.Test
|
||||||
|
{
|
||||||
|
public class DiscordFrontentTests: ABuildServerSystemTest
|
||||||
|
{
|
||||||
|
public DiscordFrontentTests(ITestOutputHelper output, SingletonAdaptor<BuildServerFixture> fixture) : base(output, fixture)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanLogIn()
|
||||||
|
{
|
||||||
|
var frontend = Fixture.GetService<DiscordFrontend>();
|
||||||
|
frontend.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -189,6 +189,9 @@ namespace Wabbajack.BuildServer.Test
|
|||||||
|
|
||||||
var statusRss = await _client.GetHtmlAsync(MakeURL("lists/status/test_list/broken.rss"));
|
var statusRss = await _client.GetHtmlAsync(MakeURL("lists/status/test_list/broken.rss"));
|
||||||
Assert.Equal(failed, statusRss.DocumentNode.SelectNodes("//item")?.Count ?? 0);
|
Assert.Equal(failed, statusRss.DocumentNode.SelectNodes("//item")?.Count ?? 0);
|
||||||
|
|
||||||
|
var heartBeat = await _client.GetHtmlAsync(MakeURL("heartbeat/report"));
|
||||||
|
Assert.Contains(heartBeat.DocumentNode.Descendants(), c => c.InnerText.StartsWith("test_list"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,6 +67,14 @@ namespace Wabbajack.BuildServer.Controllers
|
|||||||
return Ok(files.ToJson());
|
return Ok(files.ToJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(Roles = "User")]
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAllGames()
|
||||||
|
{
|
||||||
|
var registeredGames = await _sql.GetAllRegisteredGames();
|
||||||
|
return Ok(registeredGames.ToArray().ToJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Nettle;
|
||||||
using Wabbajack.Common.StatusFeed;
|
using Wabbajack.Common.StatusFeed;
|
||||||
using Wabbajack.Server;
|
using Wabbajack.Server;
|
||||||
using Wabbajack.Server.DataLayer;
|
using Wabbajack.Server.DataLayer;
|
||||||
using Wabbajack.Server.DTOs;
|
using Wabbajack.Server.DTOs;
|
||||||
|
using Wabbajack.Server.Services;
|
||||||
|
|
||||||
namespace Wabbajack.BuildServer.Controllers
|
namespace Wabbajack.BuildServer.Controllers
|
||||||
{
|
{
|
||||||
@ -19,12 +23,18 @@ namespace Wabbajack.BuildServer.Controllers
|
|||||||
|
|
||||||
}
|
}
|
||||||
private static DateTime _startTime;
|
private static DateTime _startTime;
|
||||||
|
|
||||||
|
private QuickSync _quickSync;
|
||||||
|
private ListValidator _listValidator;
|
||||||
|
|
||||||
public Heartbeat(ILogger<Heartbeat> logger, SqlService sql, GlobalInformation globalInformation)
|
|
||||||
|
public Heartbeat(ILogger<Heartbeat> logger, SqlService sql, GlobalInformation globalInformation, QuickSync quickSync, ListValidator listValidator)
|
||||||
{
|
{
|
||||||
_globalInformation = globalInformation;
|
_globalInformation = globalInformation;
|
||||||
_sql = sql;
|
_sql = sql;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_quickSync = quickSync;
|
||||||
|
_listValidator = listValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
private const int MAX_LOG_SIZE = 128;
|
private const int MAX_LOG_SIZE = 128;
|
||||||
@ -52,6 +62,53 @@ namespace Wabbajack.BuildServer.Controllers
|
|||||||
LastNexusUpdate = _globalInformation.TimeSinceLastNexusSync,
|
LastNexusUpdate = _globalInformation.TimeSinceLastNexusSync,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly Func<object, string> HandleGetReport = NettleEngine.GetCompiler().Compile(@"
|
||||||
|
<html><body>
|
||||||
|
<h2>Server Status</h2>
|
||||||
|
|
||||||
|
<h3>Service Overview ({{services.Length}}):</h3>
|
||||||
|
<ul>
|
||||||
|
{{each $.services }}
|
||||||
|
{{if $.IsLate}}
|
||||||
|
<li><b>{{$.Name}} - {{$.Time}} - {{$.MaxTime}}</b></li>
|
||||||
|
{{else}}
|
||||||
|
<li>{{$.Name}} - {{$.Time}} - {{$.MaxTime}}</li>
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
<h3>Lists ({{lists.Length}}):</h3>
|
||||||
|
<ul>
|
||||||
|
{{each $.lists }}
|
||||||
|
<li><a href='/lists/status/{{$.Name}}.html'>{{$.Name}}</a> - {{$.Time}}</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</body></html>
|
||||||
|
");
|
||||||
|
|
||||||
|
[HttpGet("report")]
|
||||||
|
public async Task<ContentResult> Report()
|
||||||
|
{
|
||||||
|
var response = HandleGetReport(new
|
||||||
|
{
|
||||||
|
services = (await _quickSync.Report())
|
||||||
|
.Select(s => new {Name = s.Key.Name, Time = s.Value.LastRunTime, MaxTime = s.Value.Delay, IsLate = s.Value.LastRunTime > s.Value.Delay})
|
||||||
|
.OrderBy(s => s.Name)
|
||||||
|
.ToArray(),
|
||||||
|
lists = _listValidator.ValidationInfo.Select(s => new {Name = s.Key, Time = s.Value.ValidationTime})
|
||||||
|
.OrderBy(l => l.Name)
|
||||||
|
.ToArray()
|
||||||
|
});
|
||||||
|
return new ContentResult
|
||||||
|
{
|
||||||
|
ContentType = "text/html",
|
||||||
|
StatusCode = (int) HttpStatusCode.OK,
|
||||||
|
Content = response
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,13 +43,23 @@ namespace Wabbajack.BuildServer.Controllers
|
|||||||
[ResponseCache(Duration = 60 * 60)]
|
[ResponseCache(Duration = 60 * 60)]
|
||||||
public async Task<IActionResult> MetricsReport(string subject)
|
public async Task<IActionResult> MetricsReport(string subject)
|
||||||
{
|
{
|
||||||
var results = (await _sql.MetricsReport(subject))
|
var metrics = (await _sql.MetricsReport(subject)).ToList();
|
||||||
|
var labels = metrics.GroupBy(m => m.Date)
|
||||||
|
.OrderBy(m => m.Key)
|
||||||
|
.Select(m => m.Key)
|
||||||
|
.ToArray();
|
||||||
|
var labelStrings = labels.Select(l => l.ToString("MM-dd-yyy")).ToList();
|
||||||
|
var results = metrics
|
||||||
.GroupBy(m => m.Subject)
|
.GroupBy(m => m.Subject)
|
||||||
.Select(g => new MetricResult
|
.Select(g =>
|
||||||
{
|
{
|
||||||
SeriesName = g.Key,
|
var indexed = g.ToDictionary(m => m.Date, m => m.Count);
|
||||||
Labels = g.Select(m => m.Date.ToString(CultureInfo.InvariantCulture)).ToList(),
|
return new MetricResult
|
||||||
Values = g.Select(m => m.Count).ToList()
|
{
|
||||||
|
SeriesName = g.Key,
|
||||||
|
Labels = labelStrings,
|
||||||
|
Values = labels.Select(l => indexed.TryGetValue(l, out var found) ? found : 0).ToList()
|
||||||
|
};
|
||||||
});
|
});
|
||||||
return Ok(results.ToList());
|
return Ok(results.ToList());
|
||||||
}
|
}
|
||||||
|
@ -255,5 +255,16 @@ namespace Wabbajack.Server.DataLayer
|
|||||||
|
|
||||||
return files.ToArray();
|
return files.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<(Game, string)>> GetAllRegisteredGames()
|
||||||
|
{
|
||||||
|
await using var conn = await Open();
|
||||||
|
var pks = (await conn.QueryAsync<string>(
|
||||||
|
@"SELECT PrimaryKeyString FROM dbo.ArchiveDownloads WHERE PrimaryKeyString like 'GameFileSourceDownloader+State|%'")
|
||||||
|
);
|
||||||
|
return pks.Select(p => p.Split("|"))
|
||||||
|
.Select(t => (GameRegistry.GetByFuzzyName(t[1]).Game, t[2]))
|
||||||
|
.Distinct();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,23 +30,24 @@ namespace Wabbajack.Server.DataLayer
|
|||||||
{
|
{
|
||||||
await using var conn = await Open();
|
await using var conn = await Open();
|
||||||
return (await conn.QueryAsync<AggregateMetric>(@"
|
return (await conn.QueryAsync<AggregateMetric>(@"
|
||||||
SELECT d.Date, d.GroupingSubject as Subject, Count(*) as Count FROM
|
select
|
||||||
(select DISTINCT CONVERT(date, Timestamp) as Date, GroupingSubject, Action, MetricsKey from dbo.Metrics) m
|
datefromparts(datepart(YEAR,Timestamp), datepart(MONTH,Timestamp), datepart(DAY,Timestamp)) as Date,
|
||||||
RIGHT OUTER JOIN
|
GroupingSubject as Subject,
|
||||||
(SELECT CONVERT(date, DATEADD(DAY, number + 1, dbo.MinMetricDate())) as Date, GroupingSubject, Action
|
count(*) as Count
|
||||||
FROM master..spt_values
|
from dbo.metrics where
|
||||||
CROSS JOIN (
|
Action = @Action
|
||||||
SELECT DISTINCT GroupingSubject, Action FROM dbo.Metrics
|
AND GroupingSubject in (select DISTINCT GroupingSubject from dbo.Metrics
|
||||||
WHERE MetricsKey is not null
|
WHERE action = @Action
|
||||||
AND Subject != 'Default'
|
AND MetricsKey is not null
|
||||||
AND TRY_CONVERT(uniqueidentifier, Subject) is null) as keys
|
AND Subject != 'Default'
|
||||||
WHERE type = 'P'
|
AND Subject != 'untitled'
|
||||||
AND DATEADD(DAY, number+1, dbo.MinMetricDate()) <= dbo.MaxMetricDate()) as d
|
AND TRY_CONVERT(uniqueidentifier, Subject) is null
|
||||||
ON m.Date = d.Date AND m.GroupingSubject = d.GroupingSubject AND m.Action = d.Action
|
AND Timestamp >= DATEADD(DAY, -1, GETUTCDATE()))
|
||||||
WHERE d.Action = @action
|
group by
|
||||||
AND d.Date >= DATEADD(month, -1, GETUTCDATE())
|
datefromparts(datepart(YEAR,Timestamp), datepart(MONTH,Timestamp), datepart(DAY,Timestamp)),
|
||||||
group by d.Date, d.GroupingSubject, d.Action
|
GroupingSubject
|
||||||
ORDER BY d.Date, d.GroupingSubject, d.Action", new {Action = action}))
|
Order by datefromparts(datepart(YEAR,Timestamp), datepart(MONTH,Timestamp), datepart(DAY,Timestamp)) asc",
|
||||||
|
new {Action = action}))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,14 +10,27 @@ namespace Wabbajack.Server.Services
|
|||||||
{
|
{
|
||||||
public void Start();
|
public void Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface IReportingService
|
||||||
|
{
|
||||||
|
public TimeSpan Delay { get; }
|
||||||
|
public DateTime LastStart { get; }
|
||||||
|
public DateTime LastEnd { get; }
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public abstract class AbstractService<TP, TR> : IStartable
|
public abstract class AbstractService<TP, TR> : IStartable, IReportingService
|
||||||
{
|
{
|
||||||
protected AppSettings _settings;
|
protected AppSettings _settings;
|
||||||
private TimeSpan _delay;
|
private TimeSpan _delay;
|
||||||
protected ILogger<TP> _logger;
|
protected ILogger<TP> _logger;
|
||||||
protected QuickSync _quickSync;
|
protected QuickSync _quickSync;
|
||||||
|
|
||||||
|
public TimeSpan Delay => _delay;
|
||||||
|
public DateTime LastStart { get; private set; }
|
||||||
|
public DateTime LastEnd { get; private set; }
|
||||||
|
|
||||||
public AbstractService(ILogger<TP> logger, AppSettings settings, QuickSync quickSync, TimeSpan delay)
|
public AbstractService(ILogger<TP> logger, AppSettings settings, QuickSync quickSync, TimeSpan delay)
|
||||||
{
|
{
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
@ -40,7 +53,7 @@ namespace Wabbajack.Server.Services
|
|||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await Setup();
|
await Setup();
|
||||||
|
await _quickSync.Register(this);
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
@ -48,7 +61,9 @@ namespace Wabbajack.Server.Services
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation($"Running: {GetType().Name}");
|
_logger.LogInformation($"Running: {GetType().Name}");
|
||||||
|
LastStart = DateTime.UtcNow;
|
||||||
await Execute();
|
await Execute();
|
||||||
|
LastEnd = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
141
Wabbajack.Server/Services/DiscordFrontend.cs
Normal file
141
Wabbajack.Server/Services/DiscordFrontend.cs
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Discord;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using OMODFramework;
|
||||||
|
using Wabbajack.BuildServer;
|
||||||
|
using Wabbajack.Common;
|
||||||
|
using Wabbajack.Server.DataLayer;
|
||||||
|
using Utils = Wabbajack.Common.Utils;
|
||||||
|
|
||||||
|
namespace Wabbajack.Server.Services
|
||||||
|
{
|
||||||
|
public class DiscordFrontend : IStartable
|
||||||
|
{
|
||||||
|
private ILogger<DiscordFrontend> _logger;
|
||||||
|
private AppSettings _settings;
|
||||||
|
private QuickSync _quickSync;
|
||||||
|
private DiscordSocketClient _client;
|
||||||
|
private SqlService _sql;
|
||||||
|
|
||||||
|
public DiscordFrontend(ILogger<DiscordFrontend> logger, AppSettings settings, QuickSync quickSync, SqlService sql)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_settings = settings;
|
||||||
|
_quickSync = quickSync;
|
||||||
|
|
||||||
|
_client = new DiscordSocketClient();
|
||||||
|
|
||||||
|
_client.Log += LogAsync;
|
||||||
|
_client.Ready += ReadyAsync;
|
||||||
|
_client.MessageReceived += MessageReceivedAsync;
|
||||||
|
|
||||||
|
_sql = sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task MessageReceivedAsync(SocketMessage arg)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(arg.Content);
|
||||||
|
if (arg.Content.StartsWith("!dervenin"))
|
||||||
|
{
|
||||||
|
var parts = arg.Content.Split(" ", StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (parts[0] != "!dervenin")
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (parts.Length == 1)
|
||||||
|
{
|
||||||
|
await ReplyTo(arg, "Wat?");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts[1] == "purge-nexus-cache")
|
||||||
|
{
|
||||||
|
if (parts.Length != 3)
|
||||||
|
{
|
||||||
|
await ReplyTo(arg, "Welp you did that wrong, gotta give me a mod-id or url");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await PurgeNexusCache(arg, parts[2]);
|
||||||
|
}
|
||||||
|
else if (parts[1] == "cyberpunk")
|
||||||
|
{
|
||||||
|
var random = new Random();
|
||||||
|
var releaseDate = new DateTime(2020, 12, 10, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
var r = releaseDate - DateTime.UtcNow;
|
||||||
|
if (r < TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
await ReplyTo(arg, "It's out, what are you doing here?");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var msgs = (await "cyberpunk_message.txt".RelativeTo(AbsolutePath.EntryPoint)
|
||||||
|
.ReadAllLinesAsync()).ToArray();
|
||||||
|
var msg = msgs[random.Next(0, msgs.Length)];
|
||||||
|
var fullmsg = String.Format(msg,
|
||||||
|
$"{r.Days} days, {r.Hours} hours, {r.Minutes} minutes, {r.Seconds} seconds");
|
||||||
|
await ReplyTo(arg, fullmsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PurgeNexusCache(SocketMessage arg, string mod)
|
||||||
|
{
|
||||||
|
if (Uri.TryCreate(mod, UriKind.Absolute, out var url))
|
||||||
|
{
|
||||||
|
mod = Enumerable.Last(url.AbsolutePath.Split("/", StringSplitOptions.RemoveEmptyEntries));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (int.TryParse(mod, out var mod_id))
|
||||||
|
{
|
||||||
|
await _sql.PurgeNexusCache(mod_id);
|
||||||
|
await _quickSync.Notify<ListValidator>();
|
||||||
|
await ReplyTo(arg, $"It is done, {mod_id} has been purged, list validation has been triggered");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReplyTo(SocketMessage socketMessage, string message)
|
||||||
|
{
|
||||||
|
await socketMessage.Channel.SendMessageAsync(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReadyAsync()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LogAsync(LogMessage arg)
|
||||||
|
{
|
||||||
|
switch (arg.Severity)
|
||||||
|
{
|
||||||
|
case LogSeverity.Info:
|
||||||
|
_logger.LogInformation(arg.Message);
|
||||||
|
break;
|
||||||
|
case LogSeverity.Warning:
|
||||||
|
_logger.LogWarning(arg.Message);
|
||||||
|
break;
|
||||||
|
case LogSeverity.Critical:
|
||||||
|
_logger.LogCritical(arg.Message);
|
||||||
|
break;
|
||||||
|
case LogSeverity.Error:
|
||||||
|
_logger.LogError(arg.Exception, arg.Message);
|
||||||
|
break;
|
||||||
|
case LogSeverity.Verbose:
|
||||||
|
_logger.LogTrace(arg.Message);
|
||||||
|
break;
|
||||||
|
case LogSeverity.Debug:
|
||||||
|
_logger.LogDebug(arg.Message);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
_client.LoginAsync(TokenType.Bot, Utils.FromEncryptedJson<string>("discord-key").Result).Wait();
|
||||||
|
_client.StartAsync().Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -27,8 +28,9 @@ namespace Wabbajack.Server.Services
|
|||||||
private NexusKeyMaintainance _nexus;
|
private NexusKeyMaintainance _nexus;
|
||||||
private ArchiveMaintainer _archives;
|
private ArchiveMaintainer _archives;
|
||||||
|
|
||||||
public IEnumerable<(ModListSummary Summary, DetailedStatus Detailed)> Summaries { get; private set; } =
|
public IEnumerable<(ModListSummary Summary, DetailedStatus Detailed)> Summaries => ValidationInfo.Values.Select(e => (e.Summary, e.Detailed));
|
||||||
new (ModListSummary Summary, DetailedStatus Detailed)[0];
|
|
||||||
|
public ConcurrentDictionary<string, (ModListSummary Summary, DetailedStatus Detailed, TimeSpan ValidationTime)> ValidationInfo = new ConcurrentDictionary<string, (ModListSummary Summary, DetailedStatus Detailed, TimeSpan ValidationTime)>();
|
||||||
|
|
||||||
|
|
||||||
public ListValidator(ILogger<ListValidator> logger, AppSettings settings, SqlService sql, DiscordWebHook discord, NexusKeyMaintainance nexus, ArchiveMaintainer archives, QuickSync quickSync)
|
public ListValidator(ILogger<ListValidator> logger, AppSettings settings, SqlService sql, DiscordWebHook discord, NexusKeyMaintainance nexus, ArchiveMaintainer archives, QuickSync quickSync)
|
||||||
@ -50,14 +52,21 @@ namespace Wabbajack.Server.Services
|
|||||||
var stopwatch = new Stopwatch();
|
var stopwatch = new Stopwatch();
|
||||||
stopwatch.Start();
|
stopwatch.Start();
|
||||||
|
|
||||||
var results = await data.ModLists.PMap(queue, async metadata =>
|
var results = await data.ModLists.Where(m => !m.ForceDown).PMap(queue, async metadata =>
|
||||||
{
|
{
|
||||||
|
var timer = new Stopwatch();
|
||||||
|
timer.Start();
|
||||||
var oldSummary =
|
var oldSummary =
|
||||||
oldSummaries.FirstOrDefault(s => s.Summary.MachineURL == metadata.Links.MachineURL);
|
oldSummaries.FirstOrDefault(s => s.Summary.MachineURL == metadata.Links.MachineURL);
|
||||||
|
|
||||||
var listArchives = await _sql.ModListArchives(metadata.Links.MachineURL);
|
var listArchives = await _sql.ModListArchives(metadata.Links.MachineURL);
|
||||||
var archives = await listArchives.PMap(queue, async archive =>
|
var archives = await listArchives.PMap(queue, async archive =>
|
||||||
{
|
{
|
||||||
|
if (timer.Elapsed > Delay)
|
||||||
|
{
|
||||||
|
return (archive, ArchiveStatus.InValid);
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var (_, result) = await ValidateArchive(data, archive);
|
var (_, result) = await ValidateArchive(data, archive);
|
||||||
@ -109,6 +118,24 @@ namespace Wabbajack.Server.Services
|
|||||||
}).ToList()
|
}).ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (timer.Elapsed > Delay)
|
||||||
|
{
|
||||||
|
await _discord.Send(Channel.Ham,
|
||||||
|
new DiscordMessage
|
||||||
|
{
|
||||||
|
Embeds = new[]
|
||||||
|
{
|
||||||
|
new DiscordEmbed
|
||||||
|
{
|
||||||
|
Title =
|
||||||
|
$"Failing {summary.Name} (`{summary.MachineURL}`) because the max validation time expired",
|
||||||
|
Url = new Uri(
|
||||||
|
$"https://build.wabbajack.org/lists/status/{summary.MachineURL}.html")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (oldSummary != default && oldSummary.Summary.Failed != summary.Failed)
|
if (oldSummary != default && oldSummary.Summary.Failed != summary.Failed)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Information, $"Number of failures {oldSummary.Summary.Failed} -> {summary.Failed}");
|
_logger.Log(LogLevel.Information, $"Number of failures {oldSummary.Summary.Failed} -> {summary.Failed}");
|
||||||
@ -151,9 +178,14 @@ namespace Wabbajack.Server.Services
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timer.Stop();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ValidationInfo[summary.MachineURL] = (summary, detailed, timer.Elapsed);
|
||||||
|
|
||||||
return (summary, detailed);
|
return (summary, detailed);
|
||||||
});
|
});
|
||||||
Summaries = results;
|
|
||||||
|
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
_logger.LogInformation($"Finished Validation in {stopwatch.Elapsed}");
|
_logger.LogInformation($"Finished Validation in {stopwatch.Elapsed}");
|
||||||
|
@ -25,7 +25,15 @@ namespace Wabbajack.Server.Services
|
|||||||
var keys = await _sql.GetNexusApiKeysWithCounts(1500);
|
var keys = await _sql.GetNexusApiKeysWithCounts(1500);
|
||||||
foreach (var key in keys.Where(k => k.Key != _selfKey))
|
foreach (var key in keys.Where(k => k.Key != _selfKey))
|
||||||
{
|
{
|
||||||
return new TrackingClient(_sql, key);
|
var client = new TrackingClient(_sql, key);
|
||||||
|
if (!await client.IsPremium())
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"Purging non premium key");
|
||||||
|
await _sql.DeleteNexusAPIKey(key.Key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await NexusApiClient.Get();
|
return await NexusApiClient.Get();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -11,6 +12,7 @@ namespace Wabbajack.Server.Services
|
|||||||
public class QuickSync
|
public class QuickSync
|
||||||
{
|
{
|
||||||
private Dictionary<Type, CancellationTokenSource> _syncs = new Dictionary<Type, CancellationTokenSource>();
|
private Dictionary<Type, CancellationTokenSource> _syncs = new Dictionary<Type, CancellationTokenSource>();
|
||||||
|
private Dictionary<Type, IReportingService> _services = new Dictionary<Type, IReportingService>();
|
||||||
private AsyncLock _lock = new AsyncLock();
|
private AsyncLock _lock = new AsyncLock();
|
||||||
private ILogger<QuickSync> _logger;
|
private ILogger<QuickSync> _logger;
|
||||||
|
|
||||||
@ -19,6 +21,20 @@ namespace Wabbajack.Server.Services
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<Type, (TimeSpan Delay, TimeSpan LastRunTime)>> Report()
|
||||||
|
{
|
||||||
|
using var _ = await _lock.WaitAsync();
|
||||||
|
return _services.ToDictionary(s => s.Key,
|
||||||
|
s => (s.Value.Delay, DateTime.UtcNow - s.Value.LastEnd));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Register<T>(T service)
|
||||||
|
where T : IReportingService
|
||||||
|
{
|
||||||
|
using var _ = await _lock.WaitAsync();
|
||||||
|
_services[service.GetType()] = service;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<CancellationToken> GetToken<T>()
|
public async Task<CancellationToken> GetToken<T>()
|
||||||
{
|
{
|
||||||
using var _ = await _lock.WaitAsync();
|
using var _ = await _lock.WaitAsync();
|
||||||
|
33
Wabbajack.Server/Services/Watchdog.cs
Normal file
33
Wabbajack.Server/Services/Watchdog.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Wabbajack.BuildServer;
|
||||||
|
using Wabbajack.Server.DTOs;
|
||||||
|
|
||||||
|
namespace Wabbajack.Server.Services
|
||||||
|
{
|
||||||
|
public class Watchdog : AbstractService<Watchdog, int>
|
||||||
|
{
|
||||||
|
private DiscordWebHook _discord;
|
||||||
|
|
||||||
|
public Watchdog(ILogger<Watchdog> logger, AppSettings settings, QuickSync quickSync, DiscordWebHook discordWebHook) : base(logger, settings, quickSync, TimeSpan.FromMinutes(5))
|
||||||
|
{
|
||||||
|
_discord = discordWebHook;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<int> Execute()
|
||||||
|
{
|
||||||
|
var report = await _quickSync.Report();
|
||||||
|
foreach (var service in report)
|
||||||
|
{
|
||||||
|
if (service.Value.LastRunTime != default && service.Value.LastRunTime >= service.Value.Delay * 2)
|
||||||
|
{
|
||||||
|
await _discord.Send(Channel.Spam,
|
||||||
|
new DiscordMessage {Content = $"Service {service.Key.Name} has missed it's scheduled execution window"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return report.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -71,6 +71,8 @@ namespace Wabbajack.Server
|
|||||||
services.AddSingleton<NexusPermissionsUpdater>();
|
services.AddSingleton<NexusPermissionsUpdater>();
|
||||||
services.AddSingleton<MirrorUploader>();
|
services.AddSingleton<MirrorUploader>();
|
||||||
services.AddSingleton<MirrorQueueService>();
|
services.AddSingleton<MirrorQueueService>();
|
||||||
|
services.AddSingleton<Watchdog>();
|
||||||
|
services.AddSingleton<DiscordFrontend>();
|
||||||
|
|
||||||
services.AddMvc();
|
services.AddMvc();
|
||||||
services.AddControllers()
|
services.AddControllers()
|
||||||
@ -129,6 +131,8 @@ namespace Wabbajack.Server
|
|||||||
app.UseService<NexusPermissionsUpdater>();
|
app.UseService<NexusPermissionsUpdater>();
|
||||||
app.UseService<MirrorUploader>();
|
app.UseService<MirrorUploader>();
|
||||||
app.UseService<MirrorQueueService>();
|
app.UseService<MirrorQueueService>();
|
||||||
|
app.UseService<Watchdog>();
|
||||||
|
app.UseService<DiscordFrontend>();
|
||||||
|
|
||||||
app.Use(next =>
|
app.Use(next =>
|
||||||
{
|
{
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
<AssemblyVersion>2.3.2.0</AssemblyVersion>
|
<AssemblyVersion>2.3.5.1</AssemblyVersion>
|
||||||
<FileVersion>2.3.2.0</FileVersion>
|
<FileVersion>2.3.5.1</FileVersion>
|
||||||
<Copyright>Copyright © 2019-2020</Copyright>
|
<Copyright>Copyright © 2019-2020</Copyright>
|
||||||
<Description>Wabbajack Server</Description>
|
<Description>Wabbajack Server</Description>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
@ -14,8 +14,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CsvHelper" Version="15.0.8" />
|
|
||||||
<PackageReference Include="Dapper" Version="2.0.35" />
|
<PackageReference Include="Dapper" Version="2.0.35" />
|
||||||
|
<PackageReference Include="Discord.Net.WebSocket" Version="2.2.0" />
|
||||||
<PackageReference Include="FluentFTP" Version="33.0.2" />
|
<PackageReference Include="FluentFTP" Version="33.0.2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Core" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.Core" Version="2.2.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.9" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.9" />
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
return {
|
return {
|
||||||
label: series.seriesName,
|
label: series.seriesName,
|
||||||
fill: false,
|
fill: false,
|
||||||
data: _.last(series.values, 30)
|
data: _.last(series.values, 90)
|
||||||
}});
|
}});
|
||||||
var ctx = document.getElementById(ele).getContext('2d');
|
var ctx = document.getElementById(ele).getContext('2d');
|
||||||
var chart = new Chart(ctx, {
|
var chart = new Chart(ctx, {
|
||||||
@ -60,7 +60,7 @@
|
|||||||
|
|
||||||
// The data for our dataset
|
// The data for our dataset
|
||||||
data: {
|
data: {
|
||||||
labels: _.last(labels, 30),
|
labels: _.last(labels, 90),
|
||||||
datasets: datasets},
|
datasets: datasets},
|
||||||
|
|
||||||
// Configuration options go here
|
// Configuration options go here
|
||||||
|
@ -407,13 +407,12 @@ namespace Wabbajack.Test
|
|||||||
Assert.Equal("Cheese for Everyone!", await filename.Path.ReadAllTextAsync());
|
Assert.Equal("Cheese for Everyone!", await filename.Path.ReadAllTextAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Site is down
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TESAllDownloader()
|
public async Task TESAllDownloader()
|
||||||
{
|
{
|
||||||
await DownloadDispatcher.GetInstance<TESAllDownloader>().Prepare();
|
await DownloadDispatcher.GetInstance<TESAllDownloader>().Prepare();
|
||||||
const string ini = "[General]\n" +
|
const string ini = "[General]\n" +
|
||||||
"directURL=https://tesall.ru/files/getdownload/594545-wabbajack-test-file/";
|
"directURL=https://tesall.ru/files/download/594545";
|
||||||
|
|
||||||
var state = (AbstractDownloadState)await DownloadDispatcher.ResolveArchive(ini.LoadIniString());
|
var state = (AbstractDownloadState)await DownloadDispatcher.ResolveArchive(ini.LoadIniString());
|
||||||
|
|
||||||
@ -431,7 +430,6 @@ namespace Wabbajack.Test
|
|||||||
|
|
||||||
Assert.Equal("Cheese for Everyone!", await filename.Path.ReadAllTextAsync());
|
Assert.Equal("Cheese for Everyone!", await filename.Path.ReadAllTextAsync());
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
/* WAITING FOR APPROVAL BY MODERATOR
|
/* WAITING FOR APPROVAL BY MODERATOR
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -28,9 +28,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CefSharp.Common" Version="85.3.130" />
|
<PackageReference Include="CefSharp.Common" Version="86.0.241" />
|
||||||
<PackageReference Include="CefSharp.OffScreen" Version="85.3.130" />
|
<PackageReference Include="CefSharp.OffScreen" Version="86.0.241" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0-preview-20200812-03" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
||||||
<PackageReference Include="coverlet.collector" Version="1.3.0" />
|
<PackageReference Include="coverlet.collector" Version="1.3.0" />
|
||||||
|
@ -90,7 +90,7 @@ namespace Wabbajack.VirtualFileSystem.Test
|
|||||||
return await s.xxHashAsync();
|
return await s.xxHashAsync();
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal(1, results.Count);
|
Assert.Single(results);
|
||||||
foreach (var (path, hash) in results)
|
foreach (var (path, hash) in results)
|
||||||
{
|
{
|
||||||
Assert.Equal(await temp.Dir.Combine(path).FileHashAsync(), hash);
|
Assert.Equal(await temp.Dir.Combine(path).FileHashAsync(), hash);
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0-preview-20200812-03" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.console" Version="2.4.1" />
|
<PackageReference Include="xunit.runner.console" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
||||||
|
@ -209,7 +209,7 @@ namespace Wabbajack.VirtualFileSystem
|
|||||||
/// <param name="files"></param>
|
/// <param name="files"></param>
|
||||||
/// <param name="callback"></param>
|
/// <param name="callback"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task Extract(WorkQueue queue, HashSet<VirtualFile> files, Func<VirtualFile, IExtractedFile, ValueTask> callback, AbsolutePath? tempFolder = null)
|
public async Task Extract(WorkQueue queue, HashSet<VirtualFile> files, Func<VirtualFile, IExtractedFile, ValueTask> callback, AbsolutePath? tempFolder = null, StatusUpdateTracker updateTracker = null)
|
||||||
{
|
{
|
||||||
var top = new VirtualFile();
|
var top = new VirtualFile();
|
||||||
var filesByParent = files.SelectMany(f => f.FilesInFullPath)
|
var filesByParent = files.SelectMany(f => f.FilesInFullPath)
|
||||||
@ -238,7 +238,7 @@ namespace Wabbajack.VirtualFileSystem
|
|||||||
tempFolder: tempFolder,
|
tempFolder: tempFolder,
|
||||||
onlyFiles: fileNames.Keys.ToHashSet());
|
onlyFiles: fileNames.Keys.ToHashSet());
|
||||||
}
|
}
|
||||||
catch (_7zipReturnError ex)
|
catch (_7zipReturnError)
|
||||||
{
|
{
|
||||||
await using var stream = await sfn.GetStream();
|
await using var stream = await sfn.GetStream();
|
||||||
var hash = await stream.xxHashAsync();
|
var hash = await stream.xxHashAsync();
|
||||||
@ -251,7 +251,8 @@ namespace Wabbajack.VirtualFileSystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
await filesByParent[top].PMap(queue, async file => await HandleFile(file, new ExtractedNativeFile(file.AbsoluteName) {CanMove = false}));
|
updateTracker ??= new StatusUpdateTracker(1);
|
||||||
|
await filesByParent[top].PMap(queue, updateTracker, async file => await HandleFile(file, new ExtractedNativeFile(file.AbsoluteName) {CanMove = false}));
|
||||||
}
|
}
|
||||||
|
|
||||||
#region KnownFiles
|
#region KnownFiles
|
||||||
|
@ -224,11 +224,11 @@ namespace Wabbajack.VirtualFileSystem
|
|||||||
|
|
||||||
self.Children = list.Values.ToImmutableList();
|
self.Children = list.Values.ToImmutableList();
|
||||||
}
|
}
|
||||||
catch (EndOfStreamException ex)
|
catch (EndOfStreamException)
|
||||||
{
|
{
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
Utils.Log($"Error while examining the contents of {relPath.FileName}");
|
Utils.Log($"Error while examining the contents of {relPath.FileName}");
|
||||||
throw;
|
throw;
|
||||||
|
@ -16,9 +16,9 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Genbox.AlphaFS" Version="2.2.2.1" />
|
<PackageReference Include="Genbox.AlphaFS" Version="2.2.2.1" />
|
||||||
<PackageReference Include="K4os.Hash.Crc" Version="1.1.4" />
|
<PackageReference Include="K4os.Hash.Crc" Version="1.1.4" />
|
||||||
<PackageReference Include="OMODFramework" Version="2.0.1" />
|
<PackageReference Include="OMODFramework" Version="2.1.0.2" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.26.0" />
|
<PackageReference Include="SharpCompress" Version="0.26.0" />
|
||||||
<PackageReference Include="System.Collections.Immutable" Version="5.0.0-preview.6.20305.11" />
|
<PackageReference Include="System.Collections.Immutable" Version="5.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="Extractors\7z.dll">
|
<None Update="Extractors\7z.dll">
|
||||||
|
@ -107,6 +107,7 @@ namespace Wabbajack
|
|||||||
|
|
||||||
private bool _useCompression = false;
|
private bool _useCompression = false;
|
||||||
public bool UseCompression { get => _useCompression; set => RaiseAndSetIfChanged(ref _useCompression, value); }
|
public bool UseCompression { get => _useCompression; set => RaiseAndSetIfChanged(ref _useCompression, value); }
|
||||||
|
public bool ShowUtilityLists { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonName("PerformanceSettings")]
|
[JsonName("PerformanceSettings")]
|
||||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
|
using System.Reactive.Subjects;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
@ -17,6 +18,9 @@ namespace Wabbajack
|
|||||||
{
|
{
|
||||||
ViewModel NavigateBackTarget { get; set; }
|
ViewModel NavigateBackTarget { get; set; }
|
||||||
ReactiveCommand<Unit, Unit> BackCommand { get; }
|
ReactiveCommand<Unit, Unit> BackCommand { get; }
|
||||||
|
|
||||||
|
Subject<bool> IsBackEnabledSubject { get; }
|
||||||
|
IObservable<bool> IsBackEnabled { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BackNavigatingVM : ViewModel, IBackNavigatingVM
|
public class BackNavigatingVM : ViewModel, IBackNavigatingVM
|
||||||
@ -27,9 +31,13 @@ namespace Wabbajack
|
|||||||
|
|
||||||
private readonly ObservableAsPropertyHelper<bool> _IsActive;
|
private readonly ObservableAsPropertyHelper<bool> _IsActive;
|
||||||
public bool IsActive => _IsActive.Value;
|
public bool IsActive => _IsActive.Value;
|
||||||
|
|
||||||
|
public Subject<bool> IsBackEnabledSubject { get; } = new Subject<bool>();
|
||||||
|
public IObservable<bool> IsBackEnabled { get; }
|
||||||
|
|
||||||
public BackNavigatingVM(MainWindowVM mainWindowVM)
|
public BackNavigatingVM(MainWindowVM mainWindowVM)
|
||||||
{
|
{
|
||||||
|
IsBackEnabled = IsBackEnabledSubject.StartWith(true);
|
||||||
BackCommand = ReactiveCommand.Create(
|
BackCommand = ReactiveCommand.Create(
|
||||||
execute: () => Utils.CatchAndLog(() =>
|
execute: () => Utils.CatchAndLog(() =>
|
||||||
{
|
{
|
||||||
@ -53,7 +61,8 @@ namespace Wabbajack
|
|||||||
public static IObservable<bool> ConstructCanNavigateBack(this IBackNavigatingVM vm)
|
public static IObservable<bool> ConstructCanNavigateBack(this IBackNavigatingVM vm)
|
||||||
{
|
{
|
||||||
return vm.WhenAny(x => x.NavigateBackTarget)
|
return vm.WhenAny(x => x.NavigateBackTarget)
|
||||||
.Select(x => x != null);
|
.CombineLatest(vm.IsBackEnabled)
|
||||||
|
.Select(x => x.First != null && x.Second);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IObservable<bool> ConstructIsActive(this IBackNavigatingVM vm, MainWindowVM mwvm)
|
public static IObservable<bool> ConstructIsActive(this IBackNavigatingVM vm, MainWindowVM mwvm)
|
||||||
|
@ -10,6 +10,7 @@ using System.Linq;
|
|||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
|
using System.Reactive.Subjects;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using Wabbajack.Common;
|
using Wabbajack.Common;
|
||||||
@ -18,16 +19,13 @@ using Wabbajack.Lib;
|
|||||||
|
|
||||||
namespace Wabbajack
|
namespace Wabbajack
|
||||||
{
|
{
|
||||||
public class CompilerVM : ViewModel, IBackNavigatingVM, ICpuStatusVM
|
public class CompilerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
|
||||||
{
|
{
|
||||||
public MainWindowVM MWVM { get; }
|
public MainWindowVM MWVM { get; }
|
||||||
|
|
||||||
private readonly ObservableAsPropertyHelper<BitmapImage> _image;
|
private readonly ObservableAsPropertyHelper<BitmapImage> _image;
|
||||||
public BitmapImage Image => _image.Value;
|
public BitmapImage Image => _image.Value;
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public ViewModel NavigateBackTarget { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public ModManager SelectedCompilerType { get; set; }
|
public ModManager SelectedCompilerType { get; set; }
|
||||||
|
|
||||||
@ -69,7 +67,7 @@ namespace Wabbajack
|
|||||||
private readonly ObservableAsPropertyHelper<(int CurrentCPUs, int DesiredCPUs)> _CurrentCpuCount;
|
private readonly ObservableAsPropertyHelper<(int CurrentCPUs, int DesiredCPUs)> _CurrentCpuCount;
|
||||||
public (int CurrentCPUs, int DesiredCPUs) CurrentCpuCount => _CurrentCpuCount.Value;
|
public (int CurrentCPUs, int DesiredCPUs) CurrentCpuCount => _CurrentCpuCount.Value;
|
||||||
|
|
||||||
public CompilerVM(MainWindowVM mainWindowVM)
|
public CompilerVM(MainWindowVM mainWindowVM) : base(mainWindowVM)
|
||||||
{
|
{
|
||||||
MWVM = mainWindowVM;
|
MWVM = mainWindowVM;
|
||||||
|
|
||||||
@ -178,6 +176,7 @@ namespace Wabbajack
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
IsBackEnabledSubject.OnNext(false);
|
||||||
var modList = await this.Compiler.Compile();
|
var modList = await this.Compiler.Compile();
|
||||||
Completed = ErrorResponse.Create(modList.Succeeded);
|
Completed = ErrorResponse.Create(modList.Succeeded);
|
||||||
}
|
}
|
||||||
@ -187,6 +186,10 @@ namespace Wabbajack
|
|||||||
while (ex.InnerException != null) ex = ex.InnerException;
|
while (ex.InnerException != null) ex = ex.InnerException;
|
||||||
Utils.Error(ex, $"Compiler error");
|
Utils.Error(ex, $"Compiler error");
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBackEnabledSubject.OnNext(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// When sub compiler begins a compile, mark state variable
|
// When sub compiler begins a compile, mark state variable
|
||||||
|
@ -37,6 +37,9 @@ namespace Wabbajack
|
|||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public bool ShowNSFW { get; set; }
|
public bool ShowNSFW { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public bool ShowUtilityLists { get; set; }
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public string GameType { get; set; }
|
public string GameType { get; set; }
|
||||||
@ -61,6 +64,7 @@ namespace Wabbajack
|
|||||||
{
|
{
|
||||||
GameType = !string.IsNullOrEmpty(settings.Game) ? settings.Game : ALL_GAME_TYPE;
|
GameType = !string.IsNullOrEmpty(settings.Game) ? settings.Game : ALL_GAME_TYPE;
|
||||||
ShowNSFW = settings.ShowNSFW;
|
ShowNSFW = settings.ShowNSFW;
|
||||||
|
ShowUtilityLists = settings.ShowUtilityLists;
|
||||||
OnlyInstalled = settings.OnlyInstalled;
|
OnlyInstalled = settings.OnlyInstalled;
|
||||||
Search = settings.Search;
|
Search = settings.Search;
|
||||||
}
|
}
|
||||||
@ -77,6 +81,7 @@ namespace Wabbajack
|
|||||||
{
|
{
|
||||||
OnlyInstalled = false;
|
OnlyInstalled = false;
|
||||||
ShowNSFW = false;
|
ShowNSFW = false;
|
||||||
|
ShowUtilityLists = false;
|
||||||
Search = string.Empty;
|
Search = string.Empty;
|
||||||
GameType = ALL_GAME_TYPE;
|
GameType = ALL_GAME_TYPE;
|
||||||
});
|
});
|
||||||
@ -150,6 +155,8 @@ namespace Wabbajack
|
|||||||
if (!vm.Metadata.NSFW) return true;
|
if (!vm.Metadata.NSFW) return true;
|
||||||
return vm.Metadata.NSFW && showNSFW;
|
return vm.Metadata.NSFW && showNSFW;
|
||||||
}))
|
}))
|
||||||
|
.Filter(this.WhenAny(x => x.ShowUtilityLists)
|
||||||
|
.Select<bool, Func<ModListMetadataVM, bool>>(showUtilityLists => vm => showUtilityLists ? vm.Metadata.UtilityList : !vm.Metadata.UtilityList))
|
||||||
// Filter by Game
|
// Filter by Game
|
||||||
.Filter(this.WhenAny(x => x.GameType)
|
.Filter(this.WhenAny(x => x.GameType)
|
||||||
.Debounce(TimeSpan.FromMilliseconds(150), RxApp.MainThreadScheduler)
|
.Debounce(TimeSpan.FromMilliseconds(150), RxApp.MainThreadScheduler)
|
||||||
@ -163,12 +170,6 @@ namespace Wabbajack
|
|||||||
return GameType == vm.Metadata.Game.GetDescription<Game>().ToString();
|
return GameType == vm.Metadata.Game.GetDescription<Game>().ToString();
|
||||||
|
|
||||||
}))
|
}))
|
||||||
.Filter(this.WhenAny(x => x.ShowNSFW)
|
|
||||||
.Select<bool, Func<ModListMetadataVM, bool>>(showNSFW => vm =>
|
|
||||||
{
|
|
||||||
if (!vm.Metadata.NSFW) return true;
|
|
||||||
return vm.Metadata.NSFW && showNSFW;
|
|
||||||
}))
|
|
||||||
// Put broken lists at bottom
|
// Put broken lists at bottom
|
||||||
.Sort(Comparer<ModListMetadataVM>.Create((a, b) => a.IsBroken.CompareTo(b.IsBroken)))
|
.Sort(Comparer<ModListMetadataVM>.Create((a, b) => a.IsBroken.CompareTo(b.IsBroken)))
|
||||||
.Bind(ModLists)
|
.Bind(ModLists)
|
||||||
@ -204,6 +205,7 @@ namespace Wabbajack
|
|||||||
settings.Game = GameType;
|
settings.Game = GameType;
|
||||||
settings.Search = Search;
|
settings.Search = Search;
|
||||||
settings.ShowNSFW = ShowNSFW;
|
settings.ShowNSFW = ShowNSFW;
|
||||||
|
settings.ShowUtilityLists = ShowUtilityLists;
|
||||||
settings.OnlyInstalled = OnlyInstalled;
|
settings.OnlyInstalled = OnlyInstalled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@ namespace Wabbajack
|
|||||||
});
|
});
|
||||||
DownloadSizeText = "Download size : " + UIUtils.FormatBytes(Metadata.DownloadMetadata.SizeOfArchives);
|
DownloadSizeText = "Download size : " + UIUtils.FormatBytes(Metadata.DownloadMetadata.SizeOfArchives);
|
||||||
InstallSizeText = "Installation size : " + UIUtils.FormatBytes(Metadata.DownloadMetadata.SizeOfInstalledFiles);
|
InstallSizeText = "Installation size : " + UIUtils.FormatBytes(Metadata.DownloadMetadata.SizeOfInstalledFiles);
|
||||||
IsBroken = metadata.ValidationSummary.HasFailures;
|
IsBroken = metadata.ValidationSummary.HasFailures || metadata.ForceDown;
|
||||||
//https://www.wabbajack.org/#/modlists/info?machineURL=eldersouls
|
//https://www.wabbajack.org/#/modlists/info?machineURL=eldersouls
|
||||||
OpenWebsiteCommand = ReactiveCommand.Create(() => Utils.OpenWebsite(new Uri($"https://www.wabbajack.org/#/modlists/info?machineURL={Metadata.Links.MachineURL}")));
|
OpenWebsiteCommand = ReactiveCommand.Create(() => Utils.OpenWebsite(new Uri($"https://www.wabbajack.org/#/modlists/info?machineURL={Metadata.Links.MachineURL}")));
|
||||||
ExecuteCommand = ReactiveCommand.CreateFromObservable<Unit, Unit>(
|
ExecuteCommand = ReactiveCommand.CreateFromObservable<Unit, Unit>(
|
||||||
|
@ -19,13 +19,14 @@ using DynamicData.Binding;
|
|||||||
using Wabbajack.Common.StatusFeed;
|
using Wabbajack.Common.StatusFeed;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Reactive.Subjects;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Microsoft.WindowsAPICodePack.Dialogs;
|
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||||
using Wabbajack.Common.IO;
|
using Wabbajack.Common.IO;
|
||||||
|
|
||||||
namespace Wabbajack
|
namespace Wabbajack
|
||||||
{
|
{
|
||||||
public class InstallerVM : ViewModel, IBackNavigatingVM, ICpuStatusVM
|
public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
|
||||||
{
|
{
|
||||||
public SlideShow Slideshow { get; }
|
public SlideShow Slideshow { get; }
|
||||||
|
|
||||||
@ -36,9 +37,6 @@ namespace Wabbajack
|
|||||||
|
|
||||||
public FilePickerVM ModListLocation { get; }
|
public FilePickerVM ModListLocation { get; }
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public ViewModel NavigateBackTarget { get; set; }
|
|
||||||
|
|
||||||
private readonly ObservableAsPropertyHelper<ISubInstallerVM> _installer;
|
private readonly ObservableAsPropertyHelper<ISubInstallerVM> _installer;
|
||||||
public ISubInstallerVM Installer => _installer.Value;
|
public ISubInstallerVM Installer => _installer.Value;
|
||||||
|
|
||||||
@ -95,12 +93,15 @@ namespace Wabbajack
|
|||||||
public ReactiveCommand<Unit, Unit> OpenReadmeCommand { get; }
|
public ReactiveCommand<Unit, Unit> OpenReadmeCommand { get; }
|
||||||
public ReactiveCommand<Unit, Unit> VisitModListWebsiteCommand { get; }
|
public ReactiveCommand<Unit, Unit> VisitModListWebsiteCommand { get; }
|
||||||
public ReactiveCommand<Unit, Unit> BackCommand { get; }
|
public ReactiveCommand<Unit, Unit> BackCommand { get; }
|
||||||
|
|
||||||
|
|
||||||
public ReactiveCommand<Unit, Unit> CloseWhenCompleteCommand { get; }
|
public ReactiveCommand<Unit, Unit> CloseWhenCompleteCommand { get; }
|
||||||
public ReactiveCommand<Unit, Unit> GoToInstallCommand { get; }
|
public ReactiveCommand<Unit, Unit> GoToInstallCommand { get; }
|
||||||
public ReactiveCommand<Unit, Unit> BeginCommand { get; }
|
public ReactiveCommand<Unit, Unit> BeginCommand { get; }
|
||||||
|
|
||||||
public InstallerVM(MainWindowVM mainWindowVM)
|
public InstallerVM(MainWindowVM mainWindowVM) : base(mainWindowVM)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (Path.GetDirectoryName(Assembly.GetEntryAssembly().Location.ToLower()) == KnownFolders.Downloads.Path.ToLower())
|
if (Path.GetDirectoryName(Assembly.GetEntryAssembly().Location.ToLower()) == KnownFolders.Downloads.Path.ToLower())
|
||||||
{
|
{
|
||||||
Utils.Error(new CriticalFailureIntervention(
|
Utils.Error(new CriticalFailureIntervention(
|
||||||
@ -306,11 +307,14 @@ namespace Wabbajack
|
|||||||
if (err) return "Corrupted Modlist";
|
if (err) return "Corrupted Modlist";
|
||||||
return name;
|
return name;
|
||||||
})
|
})
|
||||||
|
.Merge(this.WhenAny(x => x.Installer.ActiveInstallation)
|
||||||
|
.Where(c => c != null)
|
||||||
|
.SelectMany(c => c.TextStatus))
|
||||||
.ToGuiProperty(this, nameof(ModListName));
|
.ToGuiProperty(this, nameof(ModListName));
|
||||||
|
|
||||||
ShowManifestCommand = ReactiveCommand.Create(() =>
|
ShowManifestCommand = ReactiveCommand.Create(() =>
|
||||||
{
|
{
|
||||||
new ManifestWindow(ModList.SourceModList).Show();
|
Utils.OpenWebsite(new Uri("https://www.wabbajack.org/#/modlists/manifest"));
|
||||||
}, this.WhenAny(x => x.ModList)
|
}, this.WhenAny(x => x.ModList)
|
||||||
.Select(x => x?.SourceModList != null)
|
.Select(x => x?.SourceModList != null)
|
||||||
.ObserveOnGuiThread());
|
.ObserveOnGuiThread());
|
||||||
@ -366,6 +370,7 @@ namespace Wabbajack
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Utils.Log($"Starting to install {ModList.Name}");
|
Utils.Log($"Starting to install {ModList.Name}");
|
||||||
|
IsBackEnabledSubject.OnNext(false);
|
||||||
var success = await this.Installer.Install();
|
var success = await this.Installer.Install();
|
||||||
Completed = ErrorResponse.Create(success);
|
Completed = ErrorResponse.Create(success);
|
||||||
try
|
try
|
||||||
@ -378,11 +383,15 @@ namespace Wabbajack
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Utils.Error(ex, $"Encountered error, can't continue");
|
Utils.Error(ex, $"Encountered error, can't continue");
|
||||||
while (ex.InnerException != null) ex = ex.InnerException;
|
while (ex.InnerException != null) ex = ex.InnerException;
|
||||||
Completed = ErrorResponse.Fail(ex);
|
Completed = ErrorResponse.Fail(ex);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBackEnabledSubject.OnNext(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// When sub installer begins an install, mark state variable
|
// When sub installer begins an install, mark state variable
|
||||||
|
@ -1,102 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reactive;
|
|
||||||
using System.Reactive.Linq;
|
|
||||||
using ReactiveUI;
|
|
||||||
using ReactiveUI.Fody.Helpers;
|
|
||||||
using Wabbajack.Common;
|
|
||||||
using Wabbajack.Lib;
|
|
||||||
|
|
||||||
namespace Wabbajack
|
|
||||||
{
|
|
||||||
public enum SortBy { Name, Size }
|
|
||||||
|
|
||||||
public class ManifestVM : ViewModel
|
|
||||||
{
|
|
||||||
public Manifest Manifest { get; set; }
|
|
||||||
|
|
||||||
public string Name => !string.IsNullOrWhiteSpace(Manifest.Name) ? Manifest.Name : "Wabbajack Modlist";
|
|
||||||
public string Author => !string.IsNullOrWhiteSpace(Manifest.Author) ? $"Created by {Manifest.Author}" : "Created by Jyggalag";
|
|
||||||
public string Description => !string.IsNullOrWhiteSpace(Manifest.Description) ? Manifest.Description : "";
|
|
||||||
public string InstallSize => $"Install Size: {Manifest.InstallSize.ToFileSizeString()}";
|
|
||||||
public string DownloadSize => $"Download Size: {Manifest.DownloadSize.ToFileSizeString()}";
|
|
||||||
|
|
||||||
public IEnumerable<Archive> Archives => Manifest.Archives;
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public string SearchTerm { get; set; }
|
|
||||||
|
|
||||||
private readonly ObservableAsPropertyHelper<IEnumerable<Archive>> _searchResults;
|
|
||||||
public IEnumerable<Archive> SearchResults => _searchResults.Value;
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public bool SortAscending { get; set; } = true;
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public SortBy SortEnum { get; set; } = SortBy.Name;
|
|
||||||
|
|
||||||
public ReactiveCommand<Unit, Unit> SortByNameCommand;
|
|
||||||
public ReactiveCommand<Unit, Unit> SortBySizeCommand;
|
|
||||||
|
|
||||||
private IEnumerable<Archive> Order(IEnumerable<Archive> list)
|
|
||||||
{
|
|
||||||
if (SortAscending)
|
|
||||||
{
|
|
||||||
return SortEnum switch
|
|
||||||
{
|
|
||||||
SortBy.Name => list.OrderBy(x => x.Name),
|
|
||||||
SortBy.Size => list.OrderBy(x => x.Size),
|
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return SortEnum switch
|
|
||||||
{
|
|
||||||
SortBy.Name => list.OrderByDescending(x => x.Name),
|
|
||||||
SortBy.Size => list.OrderByDescending(x => x.Size),
|
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Swap(SortBy to)
|
|
||||||
{
|
|
||||||
if (SortEnum != to)
|
|
||||||
SortEnum = to;
|
|
||||||
else
|
|
||||||
SortAscending = !SortAscending;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ManifestVM(Manifest manifest)
|
|
||||||
{
|
|
||||||
Manifest = manifest;
|
|
||||||
|
|
||||||
SortByNameCommand = ReactiveCommand.Create(() => Swap(SortBy.Name));
|
|
||||||
|
|
||||||
SortBySizeCommand = ReactiveCommand.Create(() => Swap(SortBy.Size));
|
|
||||||
|
|
||||||
_searchResults =
|
|
||||||
this.WhenAnyValue(x => x.SearchTerm)
|
|
||||||
.CombineLatest(
|
|
||||||
this.WhenAnyValue(x => x.SortAscending),
|
|
||||||
this.WhenAnyValue(x => x.SortEnum),
|
|
||||||
(term, ascending, sort) => term)
|
|
||||||
.Throttle(TimeSpan.FromMilliseconds(800))
|
|
||||||
.Select(term => term?.Trim())
|
|
||||||
//.DistinctUntilChanged()
|
|
||||||
.Select(term =>
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(term))
|
|
||||||
return Order(Archives);
|
|
||||||
|
|
||||||
return Order(Archives.Where(x =>
|
|
||||||
{
|
|
||||||
if (term.StartsWith("hash:"))
|
|
||||||
return x.Hash.ToString().StartsWith(term.Replace("hash:", ""));
|
|
||||||
return x.Name.StartsWith(term);
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
.ToGuiProperty(this, nameof(SearchResults), Order(Archives));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,6 +2,8 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using System.Reactive.Subjects;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CefSharp;
|
using CefSharp;
|
||||||
@ -28,8 +30,12 @@ namespace Wabbajack
|
|||||||
[Reactive]
|
[Reactive]
|
||||||
public ReactiveCommand<Unit, Unit> BackCommand { get; set; }
|
public ReactiveCommand<Unit, Unit> BackCommand { get; set; }
|
||||||
|
|
||||||
|
public Subject<bool> IsBackEnabledSubject { get; } = new Subject<bool>();
|
||||||
|
public IObservable<bool> IsBackEnabled { get; }
|
||||||
|
|
||||||
private WebBrowserVM(string url = "http://www.wabbajack.org")
|
private WebBrowserVM(string url = "http://www.wabbajack.org")
|
||||||
{
|
{
|
||||||
|
IsBackEnabled = IsBackEnabledSubject.StartWith(true);
|
||||||
Instructions = "Wabbajack Web Browser";
|
Instructions = "Wabbajack Web Browser";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,131 +0,0 @@
|
|||||||
<reactiveUi:ReactiveUserControl
|
|
||||||
x:Class="Wabbajack.ManifestView"
|
|
||||||
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"
|
|
||||||
xmlns:reactiveUi="http://reactiveui.net"
|
|
||||||
d:DesignHeight="450"
|
|
||||||
d:DesignWidth="800"
|
|
||||||
x:TypeArguments="local:ManifestVM"
|
|
||||||
mc:Ignorable="d">
|
|
||||||
<Grid>
|
|
||||||
<ScrollViewer
|
|
||||||
MouseDown="ScrollViewer_MouseDown"
|
|
||||||
MouseMove="ScrollViewer_MouseMove"
|
|
||||||
MouseUp="ScrollViewer_MouseUp">
|
|
||||||
<ScrollViewer.Resources>
|
|
||||||
<Style x:Key="HeaderStyle" TargetType="{x:Type TextBlock}">
|
|
||||||
<Setter Property="Foreground" Value="#03DAC6" />
|
|
||||||
</Style>
|
|
||||||
<Style x:Key="HyperlinkStyle" TargetType="{x:Type Hyperlink}">
|
|
||||||
<Setter Property="Foreground" Value="#BB76FC" />
|
|
||||||
</Style>
|
|
||||||
<Style x:Key="ModTitleStyle" TargetType="{x:Type TextBox}">
|
|
||||||
<Setter Property="Foreground" Value="#C7FC86" />
|
|
||||||
<Setter Property="Background" Value="Transparent" />
|
|
||||||
</Style>
|
|
||||||
<SolidColorBrush
|
|
||||||
x:Key="SearchBarBrush"
|
|
||||||
Opacity="50"
|
|
||||||
Color="White" />
|
|
||||||
</ScrollViewer.Resources>
|
|
||||||
<StackPanel x:Name="DynamicStackPanel" Margin="8">
|
|
||||||
<TextBlock
|
|
||||||
x:Name="Name"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
FontSize="32"
|
|
||||||
Style="{StaticResource HeaderStyle}" />
|
|
||||||
<TextBlock
|
|
||||||
x:Name="Version"
|
|
||||||
Padding="0,12,0,3"
|
|
||||||
FontSize="14" />
|
|
||||||
<TextBlock
|
|
||||||
x:Name="Author"
|
|
||||||
Padding="0,3,0,3"
|
|
||||||
FontSize="14" />
|
|
||||||
<TextBlock
|
|
||||||
x:Name="Description"
|
|
||||||
FontSize="18"
|
|
||||||
TextWrapping="Wrap" />
|
|
||||||
|
|
||||||
<TextBlock
|
|
||||||
x:Name="InstallSize"
|
|
||||||
Padding="0,24,0,0"
|
|
||||||
FontSize="24" />
|
|
||||||
<TextBlock
|
|
||||||
x:Name="DownloadSize"
|
|
||||||
Padding="0,0,0,12"
|
|
||||||
FontSize="24" />
|
|
||||||
|
|
||||||
<TextBlock
|
|
||||||
Padding="6,0,0,0"
|
|
||||||
FontSize="20"
|
|
||||||
Text="Search:" />
|
|
||||||
<TextBox
|
|
||||||
x:Name="SearchBar"
|
|
||||||
BorderBrush="{StaticResource SearchBarBrush}"
|
|
||||||
BorderThickness="2"
|
|
||||||
FontSize="16"
|
|
||||||
MaxLength="100" />
|
|
||||||
|
|
||||||
<StackPanel Margin="6,6,0,0" Orientation="Horizontal">
|
|
||||||
<TextBlock Padding="0,1,0,0" FontSize="14">Order by:</TextBlock>
|
|
||||||
<Button x:Name="OrderByNameButton" Padding="4,0,4,0">
|
|
||||||
<TextBlock FontSize="14">Name</TextBlock>
|
|
||||||
</Button>
|
|
||||||
<Button x:Name="OrderBySizeButton">
|
|
||||||
<TextBlock FontSize="14">Size</TextBlock>
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<ItemsControl x:Name="ModsList" Padding="0,3,0,6">
|
|
||||||
<ItemsControl.ItemsPanel>
|
|
||||||
<ItemsPanelTemplate>
|
|
||||||
<StackPanel />
|
|
||||||
</ItemsPanelTemplate>
|
|
||||||
</ItemsControl.ItemsPanel>
|
|
||||||
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<Grid Margin="0,6,0,6">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<TextBox Grid.Row="0" Grid.Column="0"
|
|
||||||
Padding="6,0,0,0"
|
|
||||||
BorderThickness="0"
|
|
||||||
FontSize="18"
|
|
||||||
IsReadOnly="True"
|
|
||||||
Style="{StaticResource ModTitleStyle}"
|
|
||||||
Text="{Binding Path=Name}" />
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1"
|
|
||||||
Padding="3,0,0,0"
|
|
||||||
FontSize="18">
|
|
||||||
<Hyperlink
|
|
||||||
NavigateUri="{Binding Path=Name}"
|
|
||||||
RequestNavigate="Hyperlink_OnRequestNavigate"
|
|
||||||
Style="{StaticResource HyperlinkStyle}">
|
|
||||||
Link
|
|
||||||
</Hyperlink>
|
|
||||||
</TextBlock>
|
|
||||||
<TextBox Grid.Row="1" Grid.Column="0"
|
|
||||||
Padding="6,0,0,0"
|
|
||||||
FontSize="15"
|
|
||||||
IsReadOnly="True"
|
|
||||||
Text="{Binding Path=Hash}" />
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
<Canvas x:Name="TopLayer" IsHitTestVisible="False" />
|
|
||||||
</Grid>
|
|
||||||
</reactiveUi:ReactiveUserControl>
|
|
@ -1,143 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Reactive.Disposables;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using ReactiveUI;
|
|
||||||
using Wabbajack.Lib;
|
|
||||||
|
|
||||||
namespace Wabbajack
|
|
||||||
{
|
|
||||||
public partial class ManifestView
|
|
||||||
{
|
|
||||||
public ModList Modlist { get; set; }
|
|
||||||
|
|
||||||
public ManifestView(ModList modlist)
|
|
||||||
{
|
|
||||||
Modlist = modlist;
|
|
||||||
|
|
||||||
var manifest = new Manifest(modlist);
|
|
||||||
ViewModel ??= new ManifestVM(manifest);
|
|
||||||
|
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
this.WhenActivated(disposable =>
|
|
||||||
{
|
|
||||||
this.OneWayBind(ViewModel, x => x.Name, x => x.Name.Text)
|
|
||||||
.DisposeWith(disposable);
|
|
||||||
this.OneWayBind(ViewModel, x => x.Manifest.Version, x => x.Version.Text)
|
|
||||||
.DisposeWith(disposable);
|
|
||||||
this.OneWayBind(ViewModel, x => x.Author, x => x.Author.Text)
|
|
||||||
.DisposeWith(disposable);
|
|
||||||
this.OneWayBind(ViewModel, x => x.Description, x => x.Description.Text)
|
|
||||||
.DisposeWith(disposable);
|
|
||||||
this.OneWayBind(ViewModel, x => x.SearchResults, x => x.ModsList.ItemsSource)
|
|
||||||
.DisposeWith(disposable);
|
|
||||||
this.OneWayBind(ViewModel, x => x.InstallSize, x => x.InstallSize.Text)
|
|
||||||
.DisposeWith(disposable);
|
|
||||||
this.OneWayBind(ViewModel, x => x.DownloadSize, x => x.DownloadSize.Text)
|
|
||||||
.DisposeWith(disposable);
|
|
||||||
this.Bind(ViewModel, x => x.SearchTerm, x => x.SearchBar.Text)
|
|
||||||
.DisposeWith(disposable);
|
|
||||||
this.BindCommand(ViewModel, x => x.SortByNameCommand, x => x.OrderByNameButton)
|
|
||||||
.DisposeWith(disposable);
|
|
||||||
this.BindCommand(ViewModel, x => x.SortBySizeCommand, x => x.OrderBySizeButton)
|
|
||||||
.DisposeWith(disposable);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e)
|
|
||||||
{
|
|
||||||
if (!(sender is Hyperlink hyperlink)) return;
|
|
||||||
if (!(hyperlink.DataContext is Archive archive)) return;
|
|
||||||
|
|
||||||
var url = archive.State.GetManifestURL(archive);
|
|
||||||
if (string.IsNullOrWhiteSpace(url)) return;
|
|
||||||
|
|
||||||
if (url.StartsWith("https://github.com/"))
|
|
||||||
url = url.Substring(0, url.IndexOf("release", StringComparison.Ordinal));
|
|
||||||
|
|
||||||
//url = url.Replace("&", "^&");
|
|
||||||
url = url.Replace(" ", "%20");
|
|
||||||
Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") {CreateNoWindow = true});
|
|
||||||
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//solution from https://stackoverflow.com/questions/5426232/how-can-i-make-wpf-scrollviewer-middle-click-scroll/5446307#5446307
|
|
||||||
|
|
||||||
private bool _isMoving; //False - ignore mouse movements and don't scroll
|
|
||||||
private bool _isDeferredMovingStarted; //True - Mouse down -> Mouse up without moving -> Move; False - Mouse down -> Move
|
|
||||||
private Point? _startPosition;
|
|
||||||
private const double Slowdown = 10; //smaller = faster
|
|
||||||
|
|
||||||
private void ScrollViewer_MouseDown(object sender, MouseButtonEventArgs e)
|
|
||||||
{
|
|
||||||
if (_isMoving)
|
|
||||||
CancelScrolling();
|
|
||||||
else if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Pressed)
|
|
||||||
{
|
|
||||||
if (_isMoving) return;
|
|
||||||
|
|
||||||
_isMoving = true;
|
|
||||||
_startPosition = e.GetPosition(sender as IInputElement);
|
|
||||||
_isDeferredMovingStarted = true;
|
|
||||||
|
|
||||||
AddScrollSign(e.GetPosition(TopLayer).X, e.GetPosition(TopLayer).Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ScrollViewer_MouseUp(object sender, MouseButtonEventArgs e)
|
|
||||||
{
|
|
||||||
if(e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Released && _isDeferredMovingStarted != true)
|
|
||||||
CancelScrolling();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ScrollViewer_MouseMove(object sender, MouseEventArgs e)
|
|
||||||
{
|
|
||||||
if (!_isMoving || !(sender is ScrollViewer sv))
|
|
||||||
return;
|
|
||||||
|
|
||||||
_isDeferredMovingStarted = false;
|
|
||||||
|
|
||||||
var currentPosition = e.GetPosition(sv);
|
|
||||||
if (_startPosition == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var offset = currentPosition - _startPosition.Value;
|
|
||||||
offset.Y /= Slowdown;
|
|
||||||
offset.X /= Slowdown;
|
|
||||||
|
|
||||||
sv.ScrollToVerticalOffset(sv.VerticalOffset + offset.Y);
|
|
||||||
sv.ScrollToHorizontalOffset(sv.HorizontalOffset + offset.X);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CancelScrolling()
|
|
||||||
{
|
|
||||||
_isMoving = false;
|
|
||||||
_startPosition = null;
|
|
||||||
_isDeferredMovingStarted = false;
|
|
||||||
RemoveScrollSign();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddScrollSign(double x, double y)
|
|
||||||
{
|
|
||||||
const double size = 50.0;
|
|
||||||
var img = ResourceLinks.MiddleMouseButton.Value;
|
|
||||||
var icon = new Image {Source = img, Width = size, Height = size};
|
|
||||||
//var icon = new Ellipse { Stroke = Brushes.Red, StrokeThickness = 2.0, Width = 20, Height = 20 };
|
|
||||||
|
|
||||||
TopLayer.Children.Add(icon);
|
|
||||||
Canvas.SetLeft(icon, x - size / 2);
|
|
||||||
Canvas.SetTop(icon, y - size / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveScrollSign()
|
|
||||||
{
|
|
||||||
TopLayer.Children.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
<mah:MetroWindow
|
|
||||||
x:Class="Wabbajack.ManifestWindow"
|
|
||||||
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:mah="http://metro.mahapps.com/winfx/xaml/controls"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
Title="Manifest"
|
|
||||||
Width="1280"
|
|
||||||
Height="960"
|
|
||||||
MinWidth="850"
|
|
||||||
MinHeight="650"
|
|
||||||
d:DataContext="{d:DesignInstance local:ManifestWindow}"
|
|
||||||
ResizeMode="CanResize"
|
|
||||||
Style="{StaticResource {x:Type Window}}"
|
|
||||||
TitleBarHeight="25"
|
|
||||||
UseLayoutRounding="True"
|
|
||||||
WindowTitleBrush="{StaticResource MahApps.Brushes.Accent}"
|
|
||||||
mc:Ignorable="d">
|
|
||||||
<Grid x:Name="Grid" />
|
|
||||||
</mah:MetroWindow>
|
|
@ -1,22 +0,0 @@
|
|||||||
using Wabbajack.Lib;
|
|
||||||
|
|
||||||
namespace Wabbajack
|
|
||||||
{
|
|
||||||
public partial class ManifestWindow
|
|
||||||
{
|
|
||||||
public ModList Modlist { get; set; }
|
|
||||||
|
|
||||||
public ManifestWindow(ModList modlist)
|
|
||||||
{
|
|
||||||
Modlist = modlist;
|
|
||||||
|
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
var manifestView = new ManifestView(Modlist);
|
|
||||||
|
|
||||||
Grid.Children.Add(manifestView);
|
|
||||||
|
|
||||||
Title = $"{Modlist.Name} by {Modlist.Author}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -116,6 +116,14 @@
|
|||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Content="Show NSFW"
|
Content="Show NSFW"
|
||||||
Foreground="{StaticResource ForegroundBrush}" />
|
Foreground="{StaticResource ForegroundBrush}" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
x:Name="ShowUtilityLists"
|
||||||
|
Margin="10,0,10,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Content="Show Utility Lists"
|
||||||
|
Foreground="{StaticResource ForegroundBrush}" />
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
x:Name="OnlyInstalledCheckbox"
|
x:Name="OnlyInstalledCheckbox"
|
||||||
Margin="10,0,10,0"
|
Margin="10,0,10,0"
|
||||||
|
@ -63,6 +63,8 @@ namespace Wabbajack
|
|||||||
.DisposeWith(dispose);
|
.DisposeWith(dispose);
|
||||||
this.BindStrict(ViewModel, vm => vm.ShowNSFW, x => x.ShowNSFW.IsChecked)
|
this.BindStrict(ViewModel, vm => vm.ShowNSFW, x => x.ShowNSFW.IsChecked)
|
||||||
.DisposeWith(dispose);
|
.DisposeWith(dispose);
|
||||||
|
this.BindStrict(ViewModel, vm => vm.ShowUtilityLists, x => x.ShowUtilityLists.IsChecked)
|
||||||
|
.DisposeWith(dispose);
|
||||||
|
|
||||||
this.WhenAny(x => x.ViewModel.ClearFiltersCommand)
|
this.WhenAny(x => x.ViewModel.ClearFiltersCommand)
|
||||||
.BindToStrict(this, x => x.ClearFiltersButton.Command)
|
.BindToStrict(this, x => x.ClearFiltersButton.Command)
|
||||||
|
@ -36,12 +36,17 @@ namespace Wabbajack
|
|||||||
.Select(p => p.Value)
|
.Select(p => p.Value)
|
||||||
.BindToStrict(this, x => x.DownloadProgressBar.Value)
|
.BindToStrict(this, x => x.DownloadProgressBar.Value)
|
||||||
.DisposeWith(dispose);
|
.DisposeWith(dispose);
|
||||||
this.WhenAny(x => x.ViewModel.Metadata.Title)
|
this.WhenAny(x => x.ViewModel.Metadata)
|
||||||
|
.Where(x => !x.ImageContainsTitle)
|
||||||
|
.Select(x => x.Title)
|
||||||
.BindToStrict(this, x => x.DescriptionTextShadow.Text)
|
.BindToStrict(this, x => x.DescriptionTextShadow.Text)
|
||||||
.DisposeWith(dispose);
|
.DisposeWith(dispose);
|
||||||
this.WhenAny(x => x.ViewModel.Metadata.Title)
|
this.WhenAny(x => x.ViewModel.Metadata)
|
||||||
|
.Where(x => !x.ImageContainsTitle)
|
||||||
|
.Select(x => x.Title)
|
||||||
.BindToStrict(this, x => x.ModListTitleShadow.Text)
|
.BindToStrict(this, x => x.ModListTitleShadow.Text)
|
||||||
.DisposeWith(dispose);
|
.DisposeWith(dispose);
|
||||||
|
|
||||||
this.WhenAny(x => x.ViewModel.IsBroken)
|
this.WhenAny(x => x.ViewModel.IsBroken)
|
||||||
.Select(x => x ? Visibility.Visible : Visibility.Collapsed)
|
.Select(x => x ? Visibility.Visible : Visibility.Collapsed)
|
||||||
.BindToStrict(this, x => x.Overlay.Visibility)
|
.BindToStrict(this, x => x.Overlay.Visibility)
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<Platforms>x64</Platforms>
|
<Platforms>x64</Platforms>
|
||||||
<RuntimeIdentifier>win10-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win10-x64</RuntimeIdentifier>
|
||||||
<AssemblyVersion>2.3.2.0</AssemblyVersion>
|
<AssemblyVersion>2.3.5.1</AssemblyVersion>
|
||||||
<FileVersion>2.3.2.0</FileVersion>
|
<FileVersion>2.3.5.1</FileVersion>
|
||||||
<Copyright>Copyright © 2019-2020</Copyright>
|
<Copyright>Copyright © 2019-2020</Copyright>
|
||||||
<Description>An automated ModList installer</Description>
|
<Description>An automated ModList installer</Description>
|
||||||
<PublishReadyToRun>true</PublishReadyToRun>
|
<PublishReadyToRun>true</PublishReadyToRun>
|
||||||
@ -55,7 +55,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CefSharp.Wpf" Version="85.3.130" />
|
<PackageReference Include="CefSharp.Wpf" Version="86.0.241" />
|
||||||
<PackageReference Include="DynamicData" Version="6.17.14" />
|
<PackageReference Include="DynamicData" Version="6.17.14" />
|
||||||
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.0.1" />
|
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.0.1" />
|
||||||
<PackageReference Include="Fody" Version="6.3.0">
|
<PackageReference Include="Fody" Version="6.3.0">
|
||||||
@ -67,7 +67,7 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="MahApps.Metro" Version="2.3.3" />
|
<PackageReference Include="MahApps.Metro" Version="2.4.3" />
|
||||||
<PackageReference Include="MahApps.Metro.IconPacks" Version="4.8.0" />
|
<PackageReference Include="MahApps.Metro.IconPacks" Version="4.8.0" />
|
||||||
<PackageReference Include="PInvoke.Gdi32" Version="0.7.78" />
|
<PackageReference Include="PInvoke.Gdi32" Version="0.7.78" />
|
||||||
<PackageReference Include="PInvoke.User32" Version="0.7.78" />
|
<PackageReference Include="PInvoke.User32" Version="0.7.78" />
|
||||||
|
Loading…
Reference in New Issue
Block a user