mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge master
This commit is contained in:
commit
d4e7311115
@ -184,4 +184,4 @@ dotnet_diagnostic.CS8609.severity = error
|
||||
dotnet_diagnostic.CS8714.severity = error
|
||||
|
||||
# CS8605: Unboxing a possibly null value.
|
||||
dotnet_diagnostic.CS8605.severity = error
|
||||
dotnet_diagnostic.CS8605.severity = error
|
@ -1,9 +1,13 @@
|
||||
### Changelog
|
||||
|
||||
#### Version - Next
|
||||
#### Version - 1.1.5.0 - 4/6/2020
|
||||
* Included LOOT configs are no longer Base64 encoded
|
||||
* Reworked Wabbajack-cli
|
||||
* Can use a MEGA login (if you have it, not required)
|
||||
* Don't use the buggy Nexus SSO server, instead use the in-browser API key generator
|
||||
* Several fixes for zEdit merge integration, handles several side-cases of improper configuration
|
||||
|
||||
#### Version - 3/30/2020
|
||||
#### Version - 1.1.4.0 - 3/30/2020
|
||||
* Added support for Morrowind on GOG
|
||||
* Fix a bug in the Author file uploader (Sync Error)
|
||||
* Include symbols in the launcher
|
||||
|
@ -7,8 +7,8 @@
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<RuntimeIdentifier>win10-x64</RuntimeIdentifier>
|
||||
<AssemblyVersion>1.1.4.0</AssemblyVersion>
|
||||
<FileVersion>1.1.4.0</FileVersion>
|
||||
<AssemblyVersion>1.1.5.0</AssemblyVersion>
|
||||
<FileVersion>1.1.5.0</FileVersion>
|
||||
<Copyright>Copyright © 2019-2020</Copyright>
|
||||
<Description>Server component for Wabbajack</Description>
|
||||
<AssemblyName>BuildServer</AssemblyName>
|
||||
|
@ -1,9 +1,178 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Alphaleonis.Win32.Filesystem;
|
||||
using CommandLine;
|
||||
using Wabbajack.CLI.Verbs;
|
||||
using Wabbajack.Common;
|
||||
|
||||
namespace Wabbajack.CLI
|
||||
{
|
||||
public enum ExitCode
|
||||
{
|
||||
BadArguments = -1,
|
||||
Ok = 0,
|
||||
Error = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Abstract class to mark attributes which need validating
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
internal abstract class AValidateAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom message if validation failed. Use placeholder %1 to insert the value
|
||||
/// </summary>
|
||||
public string? CustomMessage { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validating if the file exists
|
||||
/// </summary>
|
||||
internal class IsFileAttribute : AValidateAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension the file should have
|
||||
/// </summary>
|
||||
public string? Extension { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validating if the directory exists
|
||||
/// </summary>
|
||||
internal class IsDirectoryAttribute : AValidateAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Create the directory if it does not exists
|
||||
/// </summary>
|
||||
public bool Create { get; set; }
|
||||
}
|
||||
|
||||
internal static class CLIUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates all Attributes of type <see cref="AValidateAttribute"/>
|
||||
/// </summary>
|
||||
/// <param name="verb">The verb to validate</param>
|
||||
/// <returns></returns>
|
||||
internal static bool HasValidArguments(AVerb verb)
|
||||
{
|
||||
var props = verb.GetType().GetProperties().Where(p =>
|
||||
{
|
||||
var hasAttr = p.HasAttribute(typeof(OptionAttribute))
|
||||
&& p.HasAttribute(typeof(AValidateAttribute));
|
||||
if (!hasAttr)
|
||||
return false;
|
||||
|
||||
if (p.PropertyType != typeof(string))
|
||||
return false;
|
||||
|
||||
var value = p.GetValue(verb);
|
||||
if (value == null)
|
||||
return false;
|
||||
|
||||
var stringValue = (string)value;
|
||||
return !string.IsNullOrWhiteSpace(stringValue);
|
||||
});
|
||||
|
||||
var valid = true;
|
||||
|
||||
props.Do(p =>
|
||||
{
|
||||
if (!valid)
|
||||
return;
|
||||
|
||||
var valueObject = p.GetValue(verb);
|
||||
|
||||
// not really possible since we filtered them out but whatever
|
||||
if (valueObject == null)
|
||||
return;
|
||||
|
||||
var value = (string)valueObject;
|
||||
var attribute = (AValidateAttribute)p.GetAttribute(typeof(AValidateAttribute));
|
||||
var isFile = false;
|
||||
|
||||
if (p.HasAttribute(typeof(IsFileAttribute)))
|
||||
{
|
||||
var fileAttribute = (IsFileAttribute)attribute;
|
||||
isFile = true;
|
||||
|
||||
if (!File.Exists(value))
|
||||
valid = false;
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(fileAttribute.Extension))
|
||||
{
|
||||
valid = value.EndsWith(fileAttribute.Extension);
|
||||
if(!valid)
|
||||
Exit($"The file {value} does not have the extension {fileAttribute.Extension}!",
|
||||
ExitCode.BadArguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (p.HasAttribute(typeof(IsDirectoryAttribute)))
|
||||
{
|
||||
var dirAttribute = (IsDirectoryAttribute)attribute;
|
||||
var exists = Directory.Exists(value);
|
||||
|
||||
if (!exists)
|
||||
{
|
||||
if (dirAttribute.Create)
|
||||
{
|
||||
Log($"Directory {value} does not exist and will be created");
|
||||
Directory.CreateDirectory(value);
|
||||
}
|
||||
else
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (valid)
|
||||
return;
|
||||
|
||||
var message = string.IsNullOrWhiteSpace(attribute.CustomMessage)
|
||||
? isFile
|
||||
? $"The file {value} does not exist!"
|
||||
: $"The folder {value} does not exist!"
|
||||
: attribute.CustomMessage.Replace("%1", value);
|
||||
|
||||
var optionAttribute = (OptionAttribute)p.GetAttribute(typeof(OptionAttribute));
|
||||
|
||||
if (optionAttribute.Required)
|
||||
Exit(message, ExitCode.BadArguments);
|
||||
else
|
||||
Log(message);
|
||||
});
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an attribute of a specific type
|
||||
/// </summary>
|
||||
/// <param name="member"></param>
|
||||
/// <param name="attribute"></param>
|
||||
/// <returns></returns>
|
||||
internal static Attribute GetAttribute(this MemberInfo member, Type attribute)
|
||||
{
|
||||
var attributes = member.GetCustomAttributes(attribute);
|
||||
return attributes.ElementAt(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a <see cref="MemberInfo"/> has a custom attribute
|
||||
/// </summary>
|
||||
/// <param name="member"></param>
|
||||
/// <param name="attribute"></param>
|
||||
/// <returns></returns>
|
||||
internal static bool HasAttribute(this MemberInfo member, Type attribute)
|
||||
{
|
||||
var attributes = member.GetCustomAttributes(attribute);
|
||||
return attributes.Count() == 1;
|
||||
}
|
||||
|
||||
internal static void Log(string msg, bool newLine = true)
|
||||
{
|
||||
//TODO: maybe also write to a log file?
|
||||
@ -13,7 +182,7 @@ namespace Wabbajack.CLI
|
||||
Console.Write(msg);
|
||||
}
|
||||
|
||||
internal static int Exit(string msg, int code)
|
||||
internal static ExitCode Exit(string msg, ExitCode code)
|
||||
{
|
||||
Log(msg);
|
||||
return code;
|
||||
|
@ -16,7 +16,9 @@ namespace Wabbajack.CLI
|
||||
typeof(ChangeDownload),
|
||||
typeof(ServerLog),
|
||||
typeof(MyFiles),
|
||||
typeof(DeleteFile)
|
||||
typeof(DeleteFile),
|
||||
typeof(Changelog),
|
||||
typeof(FindSimilar)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ namespace Wabbajack.CLI
|
||||
(ServerLog opts) => opts.Execute(),
|
||||
(MyFiles opts) => opts.Execute(),
|
||||
(DeleteFile opts) => opts.Execute(),
|
||||
(Changelog opts) => opts.Execute(),
|
||||
(FindSimilar opts) => opts.Execute(),
|
||||
errs => 1);
|
||||
}
|
||||
}
|
||||
|
7
Wabbajack.CLI/Properties/launchSettings.json
Normal file
7
Wabbajack.CLI/Properties/launchSettings.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Wabbajack.CLI": {
|
||||
"commandName": "Project"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Wabbajack.CLI.Verbs
|
||||
{
|
||||
@ -8,10 +6,13 @@ namespace Wabbajack.CLI.Verbs
|
||||
{
|
||||
public int Execute()
|
||||
{
|
||||
return Run().Result;
|
||||
if (!CLIUtils.HasValidArguments(this))
|
||||
CLIUtils.Exit("The provided arguments are not valid! Check previous messages for more information",
|
||||
ExitCode.BadArguments);
|
||||
|
||||
return (int)Run().Result;
|
||||
}
|
||||
|
||||
protected abstract Task<int> Run();
|
||||
|
||||
protected abstract Task<ExitCode> Run();
|
||||
}
|
||||
}
|
||||
|
@ -17,17 +17,21 @@ namespace Wabbajack.CLI.Verbs
|
||||
[Verb("change-download", HelpText = "Move or Copy all used Downloads from a Modlist to another directory")]
|
||||
public class ChangeDownload : AVerb
|
||||
{
|
||||
[IsDirectory(CustomMessage = "Downloads folder %1 does not exist!")]
|
||||
[Option("input", Required = true, HelpText = "Input folder containing the downloads you want to move")]
|
||||
public string Input { get; set; }
|
||||
public string Input { get; set; } = "";
|
||||
|
||||
[IsDirectory(Create = true)]
|
||||
[Option("output", Required = true, HelpText = "Output folder the downloads should be transferred to")]
|
||||
public string Output { get; set; }
|
||||
public string Output { get; set; } = "";
|
||||
|
||||
[IsFile(CustomMessage = "Modlist file %1 does not exist!")]
|
||||
[Option("modlist", Required = true, HelpText = "The Modlist, can either be a .wabbajack or a modlist.txt file")]
|
||||
public string Modlist { get; set; }
|
||||
public string Modlist { get; set; } = "";
|
||||
|
||||
[Option("mods", Required = false, HelpText = "Mods folder location if the provided modlist file is an MO2 modlist.txt")]
|
||||
public string Mods { get; set; }
|
||||
[Option("mods", Required = false,
|
||||
HelpText = "Mods folder location if the provided modlist file is an MO2 modlist.txt")]
|
||||
public string Mods { get; set; } = "";
|
||||
|
||||
[Option("copy", Default = true, HelpText = "Whether to copy the files", SetName = "copy")]
|
||||
public bool Copy { get; set; }
|
||||
@ -55,27 +59,16 @@ namespace Wabbajack.CLI.Verbs
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task<int> Run()
|
||||
protected override async Task<ExitCode> Run()
|
||||
{
|
||||
if (!File.Exists(Modlist))
|
||||
return CLIUtils.Exit($"The file {Modlist} does not exist!", -1);
|
||||
|
||||
if (!Directory.Exists(Input))
|
||||
return CLIUtils.Exit($"The input directory {Input} does not exist!", -1);
|
||||
|
||||
if (!Directory.Exists(Output))
|
||||
{
|
||||
CLIUtils.Log($"The output directory {Output} does not exist, it will be created.");
|
||||
Directory.CreateDirectory(Output);
|
||||
}
|
||||
|
||||
if (!Modlist.EndsWith((string)Consts.ModListExtension) && !Modlist.EndsWith("modlist.txt"))
|
||||
return CLIUtils.Exit($"The file {Modlist} is not a valid modlist file!", -1);
|
||||
var modListPath = (AbsolutePath)Modlist;
|
||||
if (modListPath.Extension != Consts.ModListExtension && modListPath.FileName != (RelativePath)"modlist.txt")
|
||||
return CLIUtils.Exit($"The file {Modlist} is not a valid modlist file!", ExitCode.BadArguments);
|
||||
|
||||
if (Copy && Move)
|
||||
return CLIUtils.Exit("You can't set both copy and move flags!", -1);
|
||||
return CLIUtils.Exit("You can't set both copy and move flags!", ExitCode.BadArguments);
|
||||
|
||||
var isModlist = Modlist.EndsWith((string)Consts.ModListExtension);
|
||||
var isModlist = modListPath.Extension == Consts.ModListExtension;
|
||||
|
||||
var list = new List<TransferFile>();
|
||||
|
||||
@ -85,16 +78,16 @@ namespace Wabbajack.CLI.Verbs
|
||||
|
||||
try
|
||||
{
|
||||
modlist = AInstaller.LoadFromFile((AbsolutePath)Modlist);
|
||||
modlist = AInstaller.LoadFromFile(modListPath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return CLIUtils.Exit($"Error while loading the Modlist!\n{e}", 1);
|
||||
return CLIUtils.Exit($"Error while loading the Modlist!\n{e}", ExitCode.Error);
|
||||
}
|
||||
|
||||
if (modlist == null)
|
||||
{
|
||||
return CLIUtils.Exit("The Modlist could not be loaded!", 1);
|
||||
return CLIUtils.Exit("The Modlist could not be loaded!", ExitCode.Error);
|
||||
}
|
||||
|
||||
CLIUtils.Log($"Modlist contains {modlist.Archives.Count} archives.");
|
||||
@ -153,18 +146,18 @@ namespace Wabbajack.CLI.Verbs
|
||||
else
|
||||
{
|
||||
if (!Directory.Exists(Mods))
|
||||
return CLIUtils.Exit($"Mods directory {Mods} does not exist!", -1);
|
||||
return CLIUtils.Exit($"Mods directory {Mods} does not exist!", ExitCode.BadArguments);
|
||||
|
||||
CLIUtils.Log($"Reading modlist.txt from {Modlist}");
|
||||
string[] modlist = File.ReadAllLines(Modlist);
|
||||
|
||||
if (modlist == null || modlist.Length == 0)
|
||||
return CLIUtils.Exit($"Provided modlist.txt file at {Modlist} is empty or could not be read!", -1);
|
||||
return CLIUtils.Exit($"Provided modlist.txt file at {Modlist} is empty or could not be read!", ExitCode.BadArguments);
|
||||
|
||||
var mods = modlist.Where(s => s.StartsWith("+")).Select(s => s.Substring(1)).ToHashSet();
|
||||
|
||||
if (mods.Count == 0)
|
||||
return CLIUtils.Exit("Counted mods from modlist.txt are 0!", -1);
|
||||
return CLIUtils.Exit("Counted mods from modlist.txt are 0!", ExitCode.BadArguments);
|
||||
|
||||
CLIUtils.Log($"Found {mods.Count} mods in modlist.txt");
|
||||
|
||||
|
343
Wabbajack.CLI/Verbs/Changelog.cs
Normal file
343
Wabbajack.CLI/Verbs/Changelog.cs
Normal file
@ -0,0 +1,343 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Alphaleonis.Win32.Filesystem;
|
||||
using CommandLine;
|
||||
using Markdig;
|
||||
using Markdig.Syntax;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Lib;
|
||||
using Wabbajack.Lib.Downloaders;
|
||||
|
||||
namespace Wabbajack.CLI.Verbs
|
||||
{
|
||||
[Verb("changelog", HelpText = "Generate a changelog using two different versions of the same Modlist.")]
|
||||
public class Changelog : AVerb
|
||||
{
|
||||
[IsFile(CustomMessage = "Modlist %1 does not exist!", Extension = Consts.ModListExtensionString)]
|
||||
[Option("original", Required = true, HelpText = "The original/previous modlist")]
|
||||
public string Original { get; set; } = "";
|
||||
|
||||
[IsFile(CustomMessage = "Modlist %1 does not exist!", Extension = Consts.ModListExtensionString)]
|
||||
[Option("update", Required = true, HelpText = "The current/updated modlist")]
|
||||
public string Update { get; set; } = "";
|
||||
|
||||
[Option('o', "output", Required = false, HelpText = "The output file")]
|
||||
public string? Output { get; set; }
|
||||
|
||||
[Option("changes-downloads", Required = false, Default = true, HelpText = "Include download changes")]
|
||||
public bool IncludeDownloadChanges { get; set; }
|
||||
|
||||
[Option("changes-mods", Required = false, Default = false, HelpText = "Include mods changes")]
|
||||
public bool IncludeModChanges { get; set; }
|
||||
|
||||
[Option("changes-loadorder", Required = false, Default = false, HelpText = "Include load order changes")]
|
||||
public bool IncludeLoadOrderChanges { get; set; }
|
||||
|
||||
protected override async Task<ExitCode> Run()
|
||||
{
|
||||
var orignalPath = (AbsolutePath)Original;
|
||||
var updatePath = (AbsolutePath)Update;
|
||||
if (Original == null)
|
||||
return ExitCode.BadArguments;
|
||||
if (Update == null)
|
||||
return ExitCode.BadArguments;
|
||||
|
||||
ModList original, update;
|
||||
|
||||
try
|
||||
{
|
||||
original = AInstaller.LoadFromFile(orignalPath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return CLIUtils.Exit($"Error while loading the original Modlist from {Original}!\n{e}", ExitCode.Error);
|
||||
}
|
||||
|
||||
if(original == null)
|
||||
return CLIUtils.Exit($"The Modlist from {Original} could not be loaded!", ExitCode.Error);
|
||||
|
||||
try
|
||||
{
|
||||
update = AInstaller.LoadFromFile(updatePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return CLIUtils.Exit($"Error while loading the updated Modlist from {Update}!\n{e}", ExitCode.Error);
|
||||
}
|
||||
|
||||
if(update == null)
|
||||
return CLIUtils.Exit($"The Modlist from {Update} could not be loaded!", ExitCode.Error);
|
||||
|
||||
var downloadSizeChanges = original.DownloadSize - update.DownloadSize;
|
||||
var installSizeChanges = original.InstallSize - update.InstallSize;
|
||||
|
||||
var versionRegex = new Regex(@"\s([0-9](\.|\s)?){1,4}");
|
||||
var matchOriginal = versionRegex.Match(original.Name);
|
||||
var matchUpdated = versionRegex.Match(update.Name);
|
||||
|
||||
if (!matchOriginal.Success || !matchUpdated.Success)
|
||||
{
|
||||
return CLIUtils.Exit(
|
||||
!matchOriginal.Success
|
||||
? "The name of the original modlist did not match the version check regex!"
|
||||
: "The name of the updated modlist did not match the version check regex!", ExitCode.Error);
|
||||
}
|
||||
|
||||
var version = matchUpdated.Value.Trim();
|
||||
|
||||
var mdText =
|
||||
$"## {version}\n\n" +
|
||||
$"**Build at:** `{File.GetCreationTime(Update)}`\n\n" +
|
||||
"**Info**:\n\n" +
|
||||
$"- Download Size change: {downloadSizeChanges.ToFileSizeString()} (Total: {update.DownloadSize.ToFileSizeString()})\n" +
|
||||
$"- Install Size change: {installSizeChanges.ToFileSizeString()} (Total: {update.InstallSize.ToFileSizeString()})\n\n";
|
||||
|
||||
if (IncludeDownloadChanges)
|
||||
{
|
||||
var updatedArchives = update.Archives
|
||||
.Where(a => original.Archives.All(x => x.Name != a.Name))
|
||||
.Where(a =>
|
||||
{
|
||||
if (!(a.State is NexusDownloader.State nexusState))
|
||||
return false;
|
||||
|
||||
return original.Archives.Any(x =>
|
||||
{
|
||||
if (!(x.State is NexusDownloader.State originalState))
|
||||
return false;
|
||||
|
||||
if (nexusState.Name != originalState.Name)
|
||||
return false;
|
||||
|
||||
if (nexusState.ModID != originalState.ModID)
|
||||
return false;
|
||||
|
||||
|
||||
return nexusState.FileID > originalState.FileID;
|
||||
});
|
||||
}).ToList();
|
||||
|
||||
var newArchives = update.Archives
|
||||
.Where(a => original.Archives.All(x => x.Name != a.Name))
|
||||
.Where(a => updatedArchives.All(x => x != a))
|
||||
.ToList();
|
||||
|
||||
var removedArchives = original.Archives
|
||||
.Where(a => update.Archives.All(x => x.Name != a.Name))
|
||||
.Where(a => updatedArchives.All(x => x != a))
|
||||
.ToList();
|
||||
|
||||
if(newArchives.Any() || removedArchives.Any())
|
||||
mdText += "**Download Changes**:\n\n";
|
||||
|
||||
updatedArchives.Do(a =>
|
||||
{
|
||||
mdText += $"- Updated [{GetModName(a)}]({a.State.GetManifestURL(a)})\n";
|
||||
});
|
||||
|
||||
removedArchives.Do(a =>
|
||||
{
|
||||
mdText += $"- Removed [{GetModName(a)}]({a.State.GetManifestURL(a)})\n";
|
||||
});
|
||||
|
||||
newArchives.Do(a =>
|
||||
{
|
||||
mdText += $"- Added [{GetModName(a)}]({a.State.GetManifestURL(a)})\n";
|
||||
});
|
||||
|
||||
mdText += "\n";
|
||||
}
|
||||
|
||||
if (IncludeLoadOrderChanges)
|
||||
{
|
||||
var loadorder_txt = (RelativePath)"loadorder.txt";
|
||||
var originalLoadOrderFile = original.Directives
|
||||
.Where(d => d is InlineFile)
|
||||
.Where(d => d.To.FileName == loadorder_txt)
|
||||
.Cast<InlineFile>()
|
||||
.First();
|
||||
|
||||
var updatedLoadOrderFile = update.Directives
|
||||
.Where(d => d is InlineFile)
|
||||
.Where(d => d.To.FileName == loadorder_txt)
|
||||
.Cast<InlineFile>()
|
||||
.First();
|
||||
|
||||
var originalLoadOrder = GetTextFileFromModlist(orignalPath, original, originalLoadOrderFile.SourceDataID).Result.Split("\n");
|
||||
var updatedLoadOrder = GetTextFileFromModlist(updatePath, update, updatedLoadOrderFile.SourceDataID).Result.Split("\n");
|
||||
|
||||
var addedPlugins = updatedLoadOrder
|
||||
.Where(p => originalLoadOrder.All(x => p != x))
|
||||
.ToList();
|
||||
|
||||
var removedPlugins = originalLoadOrder
|
||||
.Where(p => updatedLoadOrder.All(x => p != x))
|
||||
.ToList();
|
||||
|
||||
if(addedPlugins.Any() || removedPlugins.Any())
|
||||
mdText += "**Load Order Changes**:\n\n";
|
||||
|
||||
addedPlugins.Do(p =>
|
||||
{
|
||||
mdText += $"- Added {p}\n";
|
||||
});
|
||||
|
||||
removedPlugins.Do(p =>
|
||||
{
|
||||
mdText += $"- Removed {p}\n";
|
||||
});
|
||||
|
||||
mdText += "\n";
|
||||
}
|
||||
|
||||
if (IncludeModChanges)
|
||||
{
|
||||
var modlistTxt = (RelativePath)"modlist.txt";
|
||||
var originalModlistFile = original.Directives
|
||||
.Where(d => d is InlineFile)
|
||||
.Where(d => d.To.FileName == modlistTxt)
|
||||
.Cast<InlineFile>()
|
||||
.First();
|
||||
|
||||
var updatedModlistFile = update.Directives
|
||||
.Where(d => d is InlineFile)
|
||||
.Where(d => d.To.FileName == modlistTxt)
|
||||
.Cast<InlineFile>()
|
||||
.First();
|
||||
|
||||
var originalModlist = GetTextFileFromModlist(orignalPath, original, originalModlistFile.SourceDataID).Result.Split("\n");
|
||||
var updatedModlist = GetTextFileFromModlist(updatePath, update, updatedModlistFile.SourceDataID).Result.Split("\n");
|
||||
|
||||
var removedMods = originalModlist
|
||||
.Where(m => m.StartsWith("+"))
|
||||
.Where(m => updatedModlist.All(x => m != x))
|
||||
.Select(m => m.Substring(1))
|
||||
.ToList();
|
||||
|
||||
var addedMods = updatedModlist
|
||||
.Where(m => m.StartsWith("+"))
|
||||
.Where(m => originalModlist.All(x => m != x))
|
||||
.Select(m => m.Substring(1))
|
||||
.ToList();
|
||||
|
||||
if (removedMods.Any() || addedMods.Any())
|
||||
mdText += "**Mod Changes**:\n\n";
|
||||
|
||||
addedMods.Do(m =>
|
||||
{
|
||||
mdText += $"- Added {m}\n";
|
||||
});
|
||||
|
||||
removedMods.Do(m =>
|
||||
{
|
||||
mdText += $"- Removed {m}\n";
|
||||
});
|
||||
}
|
||||
|
||||
var output = string.IsNullOrWhiteSpace(Output)
|
||||
? "changelog.md"
|
||||
: Output;
|
||||
|
||||
if (File.Exists(output) && output.EndsWith("md"))
|
||||
{
|
||||
CLIUtils.Log($"Output file {output} already exists and is a markdown file. It will be updated with the newest version");
|
||||
|
||||
var markdown = File.ReadAllLines(output).ToList();
|
||||
var lines = mdText.Split("\n");
|
||||
|
||||
if (lines.All(l => markdown.Contains(l)))
|
||||
{
|
||||
return CLIUtils.Exit("The output file is already up-to-date", ExitCode.Ok);
|
||||
}
|
||||
|
||||
var doc = Markdown.Parse(File.ReadAllText(output));
|
||||
|
||||
var hasToc = false;
|
||||
var tocLine = 0;
|
||||
|
||||
var headers = doc
|
||||
.Where(b => b is HeadingBlock)
|
||||
.Cast<HeadingBlock>()
|
||||
.ToList();
|
||||
|
||||
if (headers.Count < 2)
|
||||
{
|
||||
return CLIUtils.Exit("The provided output file has less than 2 headers!", ExitCode.Error);
|
||||
}
|
||||
|
||||
if (headers[0].Level == 1 && headers[1].Level == 2)
|
||||
{
|
||||
if (headers[1].Line - headers[0].Line > headers.Count - 1)
|
||||
{
|
||||
var listBlocks = doc
|
||||
.Where(b => b.Line > headers[0].Line && b.Line < headers[1].Line)
|
||||
.OfType<ListBlock>()
|
||||
.ToList();
|
||||
|
||||
if (listBlocks.Count == 1)
|
||||
{
|
||||
hasToc = true;
|
||||
tocLine = listBlocks[0].Line;
|
||||
|
||||
CLIUtils.Log($"Toc found at {tocLine}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var firstHeader = headers
|
||||
.First(h => h.Level >= 2);
|
||||
|
||||
var line = firstHeader.Line-1;
|
||||
|
||||
if (hasToc)
|
||||
{
|
||||
markdown.Insert(tocLine, $"- [{version}](#{ToTocLink(version)})");
|
||||
line++;
|
||||
}
|
||||
|
||||
markdown.InsertRange(line+1, lines);
|
||||
|
||||
File.WriteAllLines(output, markdown);
|
||||
CLIUtils.Log($"Wrote {markdown.Count} lines to {output}");
|
||||
|
||||
return ExitCode.Ok;
|
||||
}
|
||||
|
||||
var text = "# Changelog\n\n" +
|
||||
$"- [{version}](#{ToTocLink(version)})\n\n" +
|
||||
$"{mdText}";
|
||||
|
||||
File.WriteAllText(output, text);
|
||||
CLIUtils.Log($"Wrote changelog to {output}");
|
||||
|
||||
return ExitCode.Ok;
|
||||
}
|
||||
|
||||
private static async Task<string> GetTextFileFromModlist(AbsolutePath archive, ModList modlist, RelativePath sourceID)
|
||||
{
|
||||
var installer = new MO2Installer(archive, modlist, default, default, null);
|
||||
byte[] bytes = await installer.LoadBytesFromPath(sourceID);
|
||||
return Encoding.Default.GetString(bytes);
|
||||
}
|
||||
|
||||
private static string ToTocLink(string header)
|
||||
{
|
||||
return header.Trim().Replace(" ", "").Replace(".", "");
|
||||
}
|
||||
|
||||
private static string GetModName(Archive a)
|
||||
{
|
||||
var result = a.Name;
|
||||
|
||||
if (a.State is IMetaState metaState)
|
||||
{
|
||||
result = metaState.Name;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
@ -9,13 +9,12 @@ namespace Wabbajack.CLI.Verbs
|
||||
public class Decrypt : AVerb
|
||||
{
|
||||
[Option('n', "name", Required = true, HelpText = @"Credential to encrypt and store in AppData\Local\Wabbajack")]
|
||||
public string Name { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
|
||||
[Option('o', "output", Required = true, HelpText = @"Output file for the decrypted data")]
|
||||
public string Output { get; set; }
|
||||
public string Output { get; set; } = "";
|
||||
|
||||
protected override async Task<int> Run()
|
||||
protected override async Task<ExitCode> Run()
|
||||
{
|
||||
File.WriteAllBytes(Output, Utils.FromEncryptedData(Name));
|
||||
return 0;
|
||||
|
@ -9,8 +9,9 @@ namespace Wabbajack.CLI.Verbs
|
||||
public class DeleteFile : AVerb
|
||||
{
|
||||
[Option('n', "name", Required = true, HelpText = @"Full name (as returned by my-files) of the file")]
|
||||
public string Name { get; set; }
|
||||
protected override async Task<int> Run()
|
||||
public string? Name { get; set; }
|
||||
|
||||
protected override async Task<ExitCode> Run()
|
||||
{
|
||||
Console.WriteLine(await AuthorAPI.DeleteFile(Name));
|
||||
return 0;
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Alphaleonis.Win32.Filesystem;
|
||||
@ -14,16 +13,16 @@ namespace Wabbajack.CLI.Verbs
|
||||
public class DownloadUrl : AVerb
|
||||
{
|
||||
[Option('u', "url", Required = true, HelpText = "Url to download")]
|
||||
public Uri Url { get; set; }
|
||||
public Uri Url { get; set; } = new Uri("");
|
||||
|
||||
[Option('o', "output", Required = true, HelpText = "Output file name")]
|
||||
public string Output { get; set; }
|
||||
public string Output { get; set; } = "";
|
||||
|
||||
protected override async Task<int> Run()
|
||||
protected override async Task<ExitCode> Run()
|
||||
{
|
||||
var state = await DownloadDispatcher.Infer(Url);
|
||||
if (state == null)
|
||||
return CLIUtils.Exit($"Could not find download source for URL {Url}", 1);
|
||||
return CLIUtils.Exit($"Could not find download source for URL {Url}", ExitCode.Error);
|
||||
|
||||
DownloadDispatcher.PrepareAll(new []{state});
|
||||
|
||||
|
@ -9,12 +9,14 @@ namespace Wabbajack.CLI.Verbs
|
||||
public class Encrypt : AVerb
|
||||
{
|
||||
[Option('n', "name", Required = true, HelpText = @"Credential to encrypt and store in AppData\Local\Wabbajack")]
|
||||
public string Name { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
[IsFile(CustomMessage = "The input file %1 does not exist!")]
|
||||
[Option('i', "input", Required = true, HelpText = @"Source data file name")]
|
||||
public string Input { get; set; }
|
||||
|
||||
protected override async Task<int> Run()
|
||||
public string Input { get; set; } = "";
|
||||
|
||||
protected override async Task<ExitCode> Run()
|
||||
{
|
||||
File.ReadAllBytes(Input).ToEcryptedData(Name);
|
||||
return 0;
|
||||
|
74
Wabbajack.CLI/Verbs/FindSimilar.cs
Normal file
74
Wabbajack.CLI/Verbs/FindSimilar.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
using F23.StringSimilarity;
|
||||
using Wabbajack.Common;
|
||||
using Directory = Alphaleonis.Win32.Filesystem.Directory;
|
||||
using File = Alphaleonis.Win32.Filesystem.File;
|
||||
using Path = Alphaleonis.Win32.Filesystem.Path;
|
||||
|
||||
namespace Wabbajack.CLI.Verbs
|
||||
{
|
||||
[Verb("find-similar", HelpText = "Finds duplicate downloads")]
|
||||
public class FindSimilar : AVerb
|
||||
{
|
||||
[IsDirectory(CustomMessage = "Downloads folder at %1 does not exist!")]
|
||||
[Option('i', "input", HelpText = "Downloads folder", Required = true)]
|
||||
public string DownloadsFolder { get; set; } = "";
|
||||
|
||||
[Option('t', "threshold", HelpText = "Set the threshold for the maximum distance", Default = 0.2, Required = false)]
|
||||
public double Threshold { get; set; }
|
||||
|
||||
protected override async Task<ExitCode> Run()
|
||||
{
|
||||
var downloads = ((AbsolutePath)DownloadsFolder).EnumerateFiles(false)
|
||||
.Where(x => Consts.SupportedArchives.Contains(x.Extension))
|
||||
.Select(x => (string)x.FileNameWithoutExtension)
|
||||
.ToList();
|
||||
|
||||
var similar = downloads
|
||||
.Select(x =>
|
||||
{
|
||||
var pair = new KeyValuePair<string, CompareStruct>(x, downloads
|
||||
.Where(y => y != x)
|
||||
.Select(y =>
|
||||
{
|
||||
var lcs = new MetricLCS();
|
||||
var distance = lcs.Distance(x, y);
|
||||
return new CompareStruct(y, distance);
|
||||
})
|
||||
.Aggregate((smallest, next) => smallest.Distance < next.Distance ? smallest : next));
|
||||
return pair;
|
||||
})
|
||||
.DistinctBy(x => x.Key)
|
||||
.DistinctBy(x => x.Value.Distance)
|
||||
.Where(x => x.Value.Distance <= Threshold)
|
||||
.ToList();
|
||||
|
||||
CLIUtils.Log($"Found {similar.Count} similar files:");
|
||||
|
||||
similar.Do(f =>
|
||||
{
|
||||
var (key, value) = f;
|
||||
CLIUtils.Log($"{key} similar to {value.Name} by {Math.Round(value.Distance, 3)}");
|
||||
});
|
||||
|
||||
return ExitCode.Ok;
|
||||
}
|
||||
|
||||
internal struct CompareStruct
|
||||
{
|
||||
public string Name;
|
||||
public double Distance;
|
||||
|
||||
public CompareStruct(string name, double distance)
|
||||
{
|
||||
Name = name;
|
||||
Distance = distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ namespace Wabbajack.CLI.Verbs
|
||||
[Verb("my-files", HelpText = "List files I have uploaded to the CDN (requires Author API key)")]
|
||||
public class MyFiles : AVerb
|
||||
{
|
||||
protected override async Task<int> Run()
|
||||
protected override async Task<ExitCode> Run()
|
||||
{
|
||||
var files = await AuthorAPI.GetMyFiles();
|
||||
foreach (var file in files)
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
using Wabbajack.Lib.FileUploader;
|
||||
@ -9,7 +8,7 @@ namespace Wabbajack.CLI.Verbs
|
||||
[Verb("server-log", HelpText = @"Get the latest server log entries", Hidden = false)]
|
||||
public class ServerLog : AVerb
|
||||
{
|
||||
protected override async Task<int> Run()
|
||||
protected override async Task<ExitCode> Run()
|
||||
{
|
||||
Console.WriteLine(await AuthorAPI.GetServerLog());
|
||||
return 0;
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
using Wabbajack.Lib.FileUploader;
|
||||
|
||||
@ -8,7 +7,7 @@ namespace Wabbajack.CLI.Verbs
|
||||
[Verb("update-server-modlists", HelpText = "Tell the Build server to update curated modlists (Requires Author API key)")]
|
||||
public class UpdateModlists : AVerb
|
||||
{
|
||||
protected override async Task<int> Run()
|
||||
protected override async Task<ExitCode> Run()
|
||||
{
|
||||
CLIUtils.Log($"Job ID: {await AuthorAPI.UpdateServerModLists()}");
|
||||
return 0;
|
||||
|
@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Lib.FileUploader;
|
||||
|
||||
namespace Wabbajack.CLI.Verbs
|
||||
@ -9,7 +7,7 @@ namespace Wabbajack.CLI.Verbs
|
||||
[Verb("update-nexus-cache", HelpText = "Tell the build server to update the Nexus cache (requires Author API key)")]
|
||||
public class UpdateNexusCache : AVerb
|
||||
{
|
||||
protected override async Task<int> Run()
|
||||
protected override async Task<ExitCode> Run()
|
||||
{
|
||||
CLIUtils.Log($"Job ID: {await AuthorAPI.UpdateNexusCache()}");
|
||||
return 0;
|
||||
|
@ -1,41 +1,25 @@
|
||||
using System;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Lib;
|
||||
using Wabbajack.Lib.Validation;
|
||||
using Wabbajack.VirtualFileSystem;
|
||||
using File = Alphaleonis.Win32.Filesystem.File;
|
||||
|
||||
namespace Wabbajack.CLI.Verbs
|
||||
{
|
||||
[Verb("validate", HelpText = @"Validates a Modlist")]
|
||||
public class Validate : AVerb
|
||||
{
|
||||
[IsFile(CustomMessage = "The modlist file %1 does not exist!", Extension = Consts.ModListExtensionString)]
|
||||
[Option('i', "input", Required = true, HelpText = @"Modlist file")]
|
||||
public string Input { get; set; }
|
||||
public string Input { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Runs the Validation of a Modlist
|
||||
/// </summary>
|
||||
/// <param name="opts"></param>
|
||||
/// <returns>
|
||||
/// <para>
|
||||
/// <c>-1</c> bad Input
|
||||
/// <c>0</c> valid modlist
|
||||
/// <c>1</c> broken modlist
|
||||
/// </para>
|
||||
/// </returns>
|
||||
protected override async Task<int> Run()
|
||||
/// <returns></returns>
|
||||
protected override async Task<ExitCode> Run()
|
||||
{
|
||||
if (!File.Exists(Input))
|
||||
return CLIUtils.Exit($"The file {Input} does not exist!", -1);
|
||||
|
||||
|
||||
if (!Input.EndsWith((string)Consts.ModListExtension))
|
||||
return CLIUtils.Exit($"The file {Input} does not end with {Consts.ModListExtension}!", -1);
|
||||
|
||||
ModList modlist;
|
||||
|
||||
try
|
||||
@ -44,12 +28,12 @@ namespace Wabbajack.CLI.Verbs
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return CLIUtils.Exit($"Error while loading the Modlist!\n{e}", 1);
|
||||
return CLIUtils.Exit($"Error while loading the Modlist!\n{e}", ExitCode.Error);
|
||||
}
|
||||
|
||||
if (modlist == null)
|
||||
{
|
||||
return CLIUtils.Exit($"The Modlist could not be loaded!", 1);
|
||||
return CLIUtils.Exit($"The Modlist could not be loaded!", ExitCode.Error);
|
||||
}
|
||||
|
||||
|
||||
@ -61,7 +45,7 @@ namespace Wabbajack.CLI.Verbs
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return CLIUtils.Exit($"Error during Validation!\n{e}", 1);
|
||||
return CLIUtils.Exit($"Error during Validation!\n{e}", ExitCode.Error);
|
||||
}
|
||||
|
||||
return CLIUtils.Exit("The Modlist passed the Validation", 0);
|
||||
|
@ -6,16 +6,20 @@
|
||||
<AssemblyName>wabbajack-cli</AssemblyName>
|
||||
<Company>Wabbajack</Company>
|
||||
<Platforms>x64</Platforms>
|
||||
<AssemblyVersion>1.1.4.0</AssemblyVersion>
|
||||
<FileVersion>1.1.4.0</FileVersion>
|
||||
<AssemblyVersion>1.1.5.0</AssemblyVersion>
|
||||
<FileVersion>1.1.5.0</FileVersion>
|
||||
<Copyright>Copyright © 2019-2020</Copyright>
|
||||
<Description>An automated ModList installer</Description>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<RuntimeIdentifier>win10-x64</RuntimeIdentifier>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.7.82" />
|
||||
<PackageReference Include="F23.StringSimilarity" Version="3.1.0" />
|
||||
<PackageReference Include="Markdig" Version="0.18.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -21,7 +21,7 @@ namespace Wabbajack.Common
|
||||
|
||||
public static string MegaPrefix = "https://mega.nz/#!";
|
||||
|
||||
public static readonly HashSet<Extension> SupportedArchives = new[]{".zip", ".rar", ".7z", ".7zip", ".fomod", ".omod", ".exe", ".dat"}
|
||||
public static readonly HashSet<Extension> SupportedArchives = new[]{".zip", ".rar", ".7z", ".7zip", ".fomod", ".omod", ".exe", ".dat", ".gz", ".tar"}
|
||||
.Select(s => new Extension(s)).ToHashSet();
|
||||
|
||||
// HashSet with archive extensions that need to be tested before extraction
|
||||
@ -100,7 +100,8 @@ namespace Wabbajack.Common
|
||||
|
||||
public static Extension HashFileExtension = new Extension(".xxHash");
|
||||
public static Extension MetaFileExtension = new Extension(".meta");
|
||||
public static Extension ModListExtension = new Extension(".wabbajack");
|
||||
public const string ModListExtensionString = ".wabbajack";
|
||||
public static Extension ModListExtension = new Extension(ModListExtensionString);
|
||||
public static AbsolutePath LocalAppDataPath => new AbsolutePath(Path.Combine(KnownFolders.LocalAppData.Path, "Wabbajack"));
|
||||
public static string MetricsKeyHeader => "x-metrics-key";
|
||||
|
||||
|
@ -4,8 +4,8 @@
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<AssemblyVersion>1.1.4.0</AssemblyVersion>
|
||||
<FileVersion>1.1.4.0</FileVersion>
|
||||
<AssemblyVersion>1.1.5.0</AssemblyVersion>
|
||||
<FileVersion>1.1.5.0</FileVersion>
|
||||
<Copyright>Copyright © 2019-2020</Copyright>
|
||||
<Description>Wabbajack Application Launcher</Description>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using HtmlAgilityPack;
|
||||
using Newtonsoft.Json;
|
||||
using Wabbajack.Common;
|
||||
@ -46,18 +47,37 @@ namespace Wabbajack.Lib.Downloaders
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(html);
|
||||
var node = doc.DocumentNode;
|
||||
Name = node.SelectNodes("//h1[@class='ipsType_pageTitle ipsContained_container']/span")?.First().InnerHtml;
|
||||
Author = node
|
||||
|
||||
Name = HttpUtility.HtmlDecode(node
|
||||
.SelectNodes(
|
||||
"//h1[@class='ipsType_pageTitle ipsContained_container']/span[@class='ipsType_break ipsContained']")
|
||||
?.First().InnerHtml);
|
||||
|
||||
Author = HttpUtility.HtmlDecode(node
|
||||
.SelectNodes(
|
||||
"//div[@class='ipsBox_alt']/div[@class='ipsPhotoPanel ipsPhotoPanel_tiny ipsClearfix ipsSpacer_bottom']/div/p[@class='ipsType_reset ipsType_large ipsType_blendLinks']/a")
|
||||
?.First().InnerHtml;
|
||||
Version = node.SelectNodes("//section/h2[@class='ipsType_sectionHead']/span[@data-role='versionTitle']")
|
||||
?.First().InnerHtml);
|
||||
|
||||
Version = HttpUtility.HtmlDecode(node
|
||||
.SelectNodes("//section/h2[@class='ipsType_sectionHead']/span[@data-role='versionTitle']")
|
||||
?
|
||||
.First().InnerHtml;
|
||||
ImageURL = node
|
||||
.First().InnerHtml);
|
||||
|
||||
ImageURL = HttpUtility.HtmlDecode(node
|
||||
.SelectNodes(
|
||||
"//div[@class='ipsBox ipsSpacer_top ipsSpacer_double']/section/div[@class='ipsPad ipsAreaBackground']/div[@class='ipsCarousel ipsClearfix']/div[@class='ipsCarousel_inner']/ul[@class='cDownloadsCarousel ipsClearfix']/li[@class='ipsCarousel_item ipsAreaBackground_reset ipsPad_half']/span[@class='ipsThumb ipsThumb_medium ipsThumb_bg ipsCursor_pointer']")
|
||||
?.First().GetAttributeValue("data-fullurl", "none");
|
||||
?.First().GetAttributeValue("data-fullurl", "none"));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ImageURL))
|
||||
return true;
|
||||
|
||||
ImageURL = HttpUtility.HtmlDecode(node
|
||||
.SelectNodes(
|
||||
"//article[@class='ipsColumn ipsColumn_fluid']/div[@class='ipsPad']/section/div[@class='ipsType_richText ipsContained ipsType_break']/p/a/img[@class='ipsImage ipsImage_thumbnailed']")
|
||||
?.First().GetAttributeValue("src", ""));
|
||||
if (string.IsNullOrWhiteSpace(ImageURL))
|
||||
ImageURL = "";
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -99,33 +99,52 @@ namespace Wabbajack.Lib.NexusApi
|
||||
await Task.Delay(500, cancel);
|
||||
}
|
||||
|
||||
// open a web socket to receive the api key
|
||||
var guid = Guid.NewGuid();
|
||||
using (var websocket = new WebSocket("wss://sso.nexusmods.com")
|
||||
{
|
||||
SslConfiguration =
|
||||
{
|
||||
EnabledSslProtocols = SslProtocols.Tls12
|
||||
}
|
||||
})
|
||||
{
|
||||
updateStatus("Please authorize Wabbajack to download Nexus mods");
|
||||
var api_key = new TaskCompletionSource<string>();
|
||||
websocket.OnMessage += (sender, msg) => { api_key.SetResult(msg.Data); };
|
||||
|
||||
websocket.Connect();
|
||||
websocket.Send("{\"id\": \"" + guid + "\", \"appid\": \"" + Consts.AppName + "\"}");
|
||||
await Task.Delay(1000, cancel);
|
||||
await browser.NavigateTo(new Uri("https://www.nexusmods.com/users/myaccount?tab=api"));
|
||||
|
||||
// open a web browser to get user permission
|
||||
await browser.NavigateTo(new Uri($"https://www.nexusmods.com/sso?id={guid}&application={Consts.AppName}"));
|
||||
using (cancel.Register(() =>
|
||||
updateStatus("Looking for API Key");
|
||||
|
||||
|
||||
var apiKey = new TaskCompletionSource<string>();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var key = "";
|
||||
try
|
||||
{
|
||||
api_key.SetCanceled();
|
||||
}))
|
||||
{
|
||||
return await api_key.Task;
|
||||
key = await browser.EvaluateJavaScript(
|
||||
"document.querySelector(\"input[value=wabbajack]\").parentElement.parentElement.querySelector(\"textarea.application-key\").innerHTML");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
{
|
||||
return key;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await browser.EvaluateJavaScript(
|
||||
"var found = document.querySelector(\"input[value=wabbajack]\").parentElement.parentElement.querySelector(\"form button[type=submit]\");" +
|
||||
"found.onclick= function() {return true;};" +
|
||||
"found.class = \" \"; " +
|
||||
"found.click();" +
|
||||
"found.remove(); found = undefined;"
|
||||
);
|
||||
updateStatus("Generating API Key, Please Wait...");
|
||||
|
||||
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ namespace Wabbajack.VirtualFileSystem
|
||||
else if (source.Extension == Consts.OMOD)
|
||||
ExtractAllWithOMOD(source, dest);
|
||||
else if (source.Extension == Consts.EXE)
|
||||
ExtractAllWithInno(source, dest);
|
||||
ExtractAllEXE(source, dest);
|
||||
else
|
||||
ExtractAllWith7Zip(source, dest);
|
||||
}
|
||||
@ -35,8 +35,16 @@ namespace Wabbajack.VirtualFileSystem
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExtractAllWithInno(AbsolutePath source, AbsolutePath dest)
|
||||
private static void ExtractAllEXE(AbsolutePath source, AbsolutePath dest)
|
||||
{
|
||||
var isArchive = TestWith7z(source);
|
||||
|
||||
if (isArchive)
|
||||
{
|
||||
ExtractAllWith7Zip(source, dest);
|
||||
return;
|
||||
}
|
||||
|
||||
Utils.Log($"Extracting {(string)source.FileName}");
|
||||
|
||||
var info = new ProcessStartInfo
|
||||
@ -218,46 +226,50 @@ namespace Wabbajack.VirtualFileSystem
|
||||
if(ext != _exeExtension && !Consts.TestArchivesBeforeExtraction.Contains(ext))
|
||||
return Consts.SupportedArchives.Contains(ext) || Consts.SupportedBSAs.Contains(ext);
|
||||
|
||||
if (ext == _exeExtension)
|
||||
var isArchive = TestWith7z(v);
|
||||
|
||||
if (isArchive)
|
||||
return true;
|
||||
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = @"Extractors\innounp.exe",
|
||||
Arguments = $"-t \"{v}\" ",
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
FileName = @"Extractors\innounp.exe",
|
||||
Arguments = $"-t \"{v}\" ",
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
var p = new Process {StartInfo = info};
|
||||
var p = new Process {StartInfo = info};
|
||||
|
||||
p.Start();
|
||||
ChildProcessTracker.AddProcess(p);
|
||||
p.Start();
|
||||
ChildProcessTracker.AddProcess(p);
|
||||
|
||||
var name = v.FileName;
|
||||
while (!p.HasExited)
|
||||
{
|
||||
var line = p.StandardOutput.ReadLine();
|
||||
if (line == null)
|
||||
break;
|
||||
var name = v.FileName;
|
||||
while (!p.HasExited)
|
||||
{
|
||||
var line = p.StandardOutput.ReadLine();
|
||||
if (line == null)
|
||||
break;
|
||||
|
||||
if (line[0] != '#')
|
||||
continue;
|
||||
if (line[0] != '#')
|
||||
continue;
|
||||
|
||||
Utils.Status($"Testing {(string)name} - {line.Trim()}");
|
||||
}
|
||||
|
||||
p.WaitForExitAndWarn(TimeSpan.FromSeconds(30), $"Testing {name}");
|
||||
return p.ExitCode == 0;
|
||||
Utils.Status($"Testing {(string)name} - {line.Trim()}");
|
||||
}
|
||||
|
||||
p.WaitForExitAndWarn(TimeSpan.FromSeconds(30), $"Testing {name}");
|
||||
return p.ExitCode == 0;
|
||||
}
|
||||
|
||||
public static bool TestWith7z(AbsolutePath file)
|
||||
{
|
||||
var testInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = @"Extractors\7z.exe",
|
||||
Arguments = $"t \"{v}\"",
|
||||
Arguments = $"t \"{file}\"",
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
@ -275,6 +287,7 @@ namespace Wabbajack.VirtualFileSystem
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
@ -285,13 +298,16 @@ namespace Wabbajack.VirtualFileSystem
|
||||
if (line == null)
|
||||
break;
|
||||
}
|
||||
} catch (Exception){}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
testP.WaitForExitAndWarn(TimeSpan.FromSeconds(30), $"Can Extract Check {v}");
|
||||
testP.WaitForExitAndWarn(TimeSpan.FromSeconds(30), $"Can Extract Check {file}");
|
||||
return testP.ExitCode == 0;
|
||||
}
|
||||
|
||||
|
||||
private static Extension _exeExtension = new Extension(".exe");
|
||||
|
||||
public static bool MightBeArchive(AbsolutePath path)
|
||||
|
@ -6,8 +6,8 @@
|
||||
<UseWPF>true</UseWPF>
|
||||
<Platforms>x64</Platforms>
|
||||
<RuntimeIdentifier>win10-x64</RuntimeIdentifier>
|
||||
<AssemblyVersion>1.1.4.0</AssemblyVersion>
|
||||
<FileVersion>1.1.4.0</FileVersion>
|
||||
<AssemblyVersion>1.1.5.0</AssemblyVersion>
|
||||
<FileVersion>1.1.5.0</FileVersion>
|
||||
<Copyright>Copyright © 2019-2020</Copyright>
|
||||
<Description>An automated ModList installer</Description>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
|
Loading…
Reference in New Issue
Block a user