Merge master

This commit is contained in:
Timothy Baldridge 2020-04-09 16:36:07 -06:00
commit d4e7311115
27 changed files with 795 additions and 159 deletions

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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;

View File

@ -16,7 +16,9 @@ namespace Wabbajack.CLI
typeof(ChangeDownload),
typeof(ServerLog),
typeof(MyFiles),
typeof(DeleteFile)
typeof(DeleteFile),
typeof(Changelog),
typeof(FindSimilar)
};
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
{
"profiles": {
"Wabbajack.CLI": {
"commandName": "Project"
}
}
}

View File

@ -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();
}
}

View File

@ -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");

View 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;
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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});

View File

@ -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;

View 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;
}
}
}
}

View File

@ -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)

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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>

View File

@ -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";

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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>