mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge pull request #687 from erri120/issue-684
CLI overhaul and Changelog autogen
This commit is contained in:
commit
9e6b1881de
@ -121,4 +121,67 @@ csharp_preserve_single_line_blocks = true
|
||||
# CS4014: Task not awaited
|
||||
dotnet_diagnostic.CS4014.severity = error
|
||||
# CS1998: Async function does not contain await
|
||||
dotnet_diagnostic.CS1998.severity = silent
|
||||
dotnet_diagnostic.CS1998.severity = silent
|
||||
|
||||
###############################
|
||||
# C# Nullability #
|
||||
###############################
|
||||
# CS8602: Dereference of a possibly null reference.
|
||||
dotnet_diagnostic.CS8602.severity = error
|
||||
|
||||
# CS8600: Converting null literal or possible null value to non-nullable type.
|
||||
dotnet_diagnostic.CS8600.severity = error
|
||||
|
||||
# CS8619: Nullability of reference types in value doesn't match target type.
|
||||
dotnet_diagnostic.CS8619.severity = error
|
||||
|
||||
# CS8603: Possible null reference return.
|
||||
dotnet_diagnostic.CS8603.severity = error
|
||||
|
||||
# CS8625: Cannot convert null literal to non-nullable reference type.
|
||||
dotnet_diagnostic.CS8625.severity = error
|
||||
|
||||
# CS8653: A default expression introduces a null value for a type parameter.
|
||||
dotnet_diagnostic.CS8653.severity = silent
|
||||
|
||||
# CS8601: Possible null reference assignment.
|
||||
dotnet_diagnostic.CS8601.severity = error
|
||||
|
||||
# CS8604: Possible null reference argument.
|
||||
dotnet_diagnostic.CS8604.severity = error
|
||||
|
||||
# CS8622: Nullability of reference types in type of parameter doesn't match the target delegate.
|
||||
dotnet_diagnostic.CS8622.severity = error
|
||||
|
||||
# CS8610: Nullability of reference types in type of parameter doesn't match overridden member.
|
||||
dotnet_diagnostic.CS8610.severity = error
|
||||
|
||||
# CS8618: Non-nullable field is uninitialized. Consider declaring as nullable.
|
||||
dotnet_diagnostic.CS8618.severity = error
|
||||
|
||||
# CS8629: Nullable value type may be null.
|
||||
dotnet_diagnostic.CS8629.severity = error
|
||||
|
||||
# CS8620: Argument cannot be used for parameter due to differences in the nullability of reference types.
|
||||
dotnet_diagnostic.CS8620.severity = error
|
||||
|
||||
# CS8614: Nullability of reference types in type of parameter doesn't match implicitly implemented member.
|
||||
dotnet_diagnostic.CS8614.severity = error
|
||||
|
||||
# CS8617: Nullability of reference types in type of parameter doesn't match implemented member.
|
||||
dotnet_diagnostic.CS8617.severity = error
|
||||
|
||||
# CS8611: Nullability of reference types in type of parameter doesn't match partial method declaration.
|
||||
dotnet_diagnostic.CS8611.severity = error
|
||||
|
||||
# CS8597: Thrown value may be null.
|
||||
dotnet_diagnostic.CS8597.severity = error
|
||||
|
||||
# CS8609: Nullability of reference types in return type doesn't match overridden member.
|
||||
dotnet_diagnostic.CS8609.severity = error
|
||||
|
||||
# CS8714: The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
|
||||
dotnet_diagnostic.CS8714.severity = error
|
||||
|
||||
# CS8605: Unboxing a possibly null value.
|
||||
dotnet_diagnostic.CS8605.severity = error
|
@ -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,8 @@ namespace Wabbajack.CLI
|
||||
typeof(ChangeDownload),
|
||||
typeof(ServerLog),
|
||||
typeof(MyFiles),
|
||||
typeof(DeleteFile)
|
||||
typeof(DeleteFile),
|
||||
typeof(Changelog)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ namespace Wabbajack.CLI
|
||||
(ServerLog opts) => opts.Execute(),
|
||||
(MyFiles opts) => opts.Execute(),
|
||||
(DeleteFile opts) => opts.Execute(),
|
||||
(Changelog 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,20 @@ 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; }
|
||||
public string? Mods { get; set; }
|
||||
|
||||
[Option("copy", Default = true, HelpText = "Whether to copy the files", SetName = "copy")]
|
||||
public bool Copy { get; set; }
|
||||
@ -55,27 +58,15 @@ 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(Consts.ModListExtension) && !Modlist.EndsWith("modlist.txt"))
|
||||
return CLIUtils.Exit($"The file {Modlist} is not a valid modlist file!", -1);
|
||||
if (Modlist != null && (!Modlist.EndsWith(Consts.ModListExtension) && !Modlist.EndsWith("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(Consts.ModListExtension);
|
||||
var isModlist = Modlist != null && Modlist.EndsWith(Consts.ModListExtension);
|
||||
|
||||
var list = new List<TransferFile>();
|
||||
|
||||
@ -89,12 +80,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);
|
||||
}
|
||||
|
||||
CLIUtils.Log($"Modlist contains {modlist.Archives.Count} archives.");
|
||||
@ -153,18 +144,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");
|
||||
|
||||
|
346
Wabbajack.CLI/Verbs/Changelog.cs
Normal file
346
Wabbajack.CLI/Verbs/Changelog.cs
Normal file
@ -0,0 +1,346 @@
|
||||
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.ModListExtension)]
|
||||
[Option("original", Required = true, HelpText = "The original/previous modlist")]
|
||||
public string? Original { get; set; }
|
||||
|
||||
[IsFile(CustomMessage = "Modlist %1 does not exist!", Extension = Consts.ModListExtension)]
|
||||
[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()
|
||||
{
|
||||
if (Original == null)
|
||||
return ExitCode.BadArguments;
|
||||
if (Update == null)
|
||||
return ExitCode.BadArguments;
|
||||
|
||||
ModList original, update;
|
||||
|
||||
try
|
||||
{
|
||||
original = AInstaller.LoadFromFile(Original);
|
||||
}
|
||||
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(Update);
|
||||
}
|
||||
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;
|
||||
|
||||
if (!int.TryParse(nexusState.FileID, out var currentFileID))
|
||||
return true;
|
||||
|
||||
if (int.TryParse(originalState.FileID, out var originalFileID))
|
||||
{
|
||||
return currentFileID > originalFileID;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}).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 originalLoadOrderFile = original.Directives
|
||||
.Where(d => d is InlineFile)
|
||||
.Where(d => d.To.EndsWith("loadorder.txt"))
|
||||
.Cast<InlineFile>()
|
||||
.First();
|
||||
|
||||
var updatedLoadOrderFile = update.Directives
|
||||
.Where(d => d is InlineFile)
|
||||
.Where(d => d.To.EndsWith("loadorder.txt"))
|
||||
.Cast<InlineFile>()
|
||||
.First();
|
||||
|
||||
var originalLoadOrder = GetTextFileFromModlist(Original, original, originalLoadOrderFile.SourceDataID).Split("\n");
|
||||
var updatedLoadOrder = GetTextFileFromModlist(Update, update, updatedLoadOrderFile.SourceDataID).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 originalModlistFile = original.Directives
|
||||
.Where(d => d is InlineFile)
|
||||
.Where(d => d.To.EndsWith("modlist.txt"))
|
||||
.Cast<InlineFile>()
|
||||
.First();
|
||||
|
||||
var updatedModlistFile = update.Directives
|
||||
.Where(d => d is InlineFile)
|
||||
.Where(d => d.To.EndsWith("modlist.txt"))
|
||||
.Cast<InlineFile>()
|
||||
.First();
|
||||
|
||||
var originalModlist = GetTextFileFromModlist(Original, original, originalModlistFile.SourceDataID).Split("\n");
|
||||
var updatedModlist = GetTextFileFromModlist(Update, update, updatedModlistFile.SourceDataID).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 string GetTextFileFromModlist(string archive, ModList modlist, string sourceID)
|
||||
{
|
||||
var installer = new MO2Installer(archive, modlist, "", "", null);
|
||||
byte[] bytes = 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; }
|
||||
|
||||
[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,13 @@ 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; }
|
||||
public string? Input { get; set; }
|
||||
|
||||
protected override async Task<int> Run()
|
||||
protected override async Task<ExitCode> Run()
|
||||
{
|
||||
File.ReadAllBytes(Input).ToEcryptedData(Name);
|
||||
return 0;
|
||||
|
@ -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.ModListExtension)]
|
||||
[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(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);
|
||||
|
@ -12,10 +12,13 @@
|
||||
<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="Markdig" Version="0.18.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -90,7 +90,7 @@ namespace Wabbajack.Common
|
||||
|
||||
public static string HashFileExtension => ".xxHash";
|
||||
public static string MetaFileExtension => ".meta";
|
||||
public static string ModListExtension = ".wabbajack";
|
||||
public const string ModListExtension = ".wabbajack";
|
||||
public static string LocalAppDataPath => Path.Combine(KnownFolders.LocalAppData.Path, "Wabbajack");
|
||||
public static string MetricsKeyHeader => "x-metrics-key";
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user