Merge branch 'master' into compiler-multi-source

This commit is contained in:
Justin Swanson 2019-11-14 20:24:15 -06:00
commit 5e03dfaaed
74 changed files with 2734 additions and 263 deletions

1
.gitignore vendored
View File

@ -365,3 +365,4 @@ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
/.idea

View File

@ -89,10 +89,10 @@
<Version>2.2.6</Version>
</PackageReference>
<PackageReference Include="MSTest.TestAdapter">
<Version>1.3.2</Version>
<Version>2.0.0</Version>
</PackageReference>
<PackageReference Include="MSTest.TestFramework">
<Version>1.3.2</Version>
<Version>2.0.0</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />

View File

@ -44,7 +44,10 @@ namespace VFS
public static void Reconfigure(string root)
{
RootFolder = root;
_stagedRoot = Path.Combine(RootFolder, "vfs_staged_files");
if (RootFolder != null)
_stagedRoot = Path.Combine(RootFolder, "vfs_staged_files");
else
_stagedRoot = "vfs_staged_files";
}
public static void Clean()

View File

@ -112,7 +112,7 @@
<Version>1.2.0</Version>
</PackageReference>
<PackageReference Include="System.Collections.Immutable">
<Version>1.5.0</Version>
<Version>1.6.0</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

View File

@ -122,7 +122,7 @@ namespace Wabbajack.Common.CSP
Monitor.Exit(this);
throw new TooManyHanldersException();
}
_puts.Unshift((handler, val));
_puts.UnboundedUnshift((handler, val));
}
Monitor.Exit(this);
return (AsyncResult.Enqueued, true);
@ -191,7 +191,7 @@ namespace Wabbajack.Common.CSP
throw new TooManyHanldersException();
}
_takes.Unshift(handler);
_takes.UnboundedUnshift(handler);
}
Monitor.Exit(this);
return (AsyncResult.Enqueued, default);

View File

@ -121,6 +121,59 @@ namespace Wabbajack.Common.CSP
}
public static IReadPort<TOut> UnorderedPipelineRx<TIn, TOut>(
this IReadPort<TIn> from,
Func<IObservable<TIn>, IObservable<TOut>> f,
bool propagateClose = true)
{
var parallelism = Environment.ProcessorCount;
var to = Channel.Create<TOut>(parallelism * 2);
var pipeline = from.UnorderedPipeline(parallelism, to, f);
return to;
}
public static IReadPort<TOut> UnorderedPipelineSync<TIn, TOut>(
this IReadPort<TIn> from,
Func<TIn, TOut> f,
bool propagateClose = true)
{
var parallelism = Environment.ProcessorCount;
var to = Channel.Create<TOut>(parallelism * 2);
async Task Pump()
{
while (true)
{
var (is_open, job) = await from.Take();
if (!is_open) break;
try
{
var putIsOpen = await to.Put(f(job));
if (!putIsOpen) return;
}
catch (Exception ex)
{
}
}
}
Task.Run(async () =>
{
await Task.WhenAll(Enumerable.Range(0, parallelism)
.Select(idx => Task.Run(Pump)));
if (propagateClose)
{
from.Close();
to.Close();
}
});
return to;
}
public static async Task UnorderedThreadedPipeline<TIn, TOut>(
this IReadPort<TIn> from,
int parallelism,

View File

@ -1,6 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
namespace Wabbajack.Common.CSP
{
@ -23,7 +25,8 @@ namespace Wabbajack.Common.CSP
public T Pop()
{
if (_length == 0) return default;
if (_length == 0)
throw new InvalidDataException("Pop on empty buffer");
var val = _arr[_tail];
_arr[_tail] = default;
_tail = (_tail + 1) % _size;
@ -45,7 +48,7 @@ namespace Wabbajack.Common.CSP
public void UnboundedUnshift(T x)
{
if (_length == _size)
if (_length + 1 == _size)
Resize();
Unshift(x);
}
@ -67,8 +70,8 @@ namespace Wabbajack.Common.CSP
}
else if (_tail > _head)
{
Array.Copy(_arr, _tail, new_arr, 0, _length - _tail);
Array.Copy(_arr, 0, new_arr, (_length - _tail), _head);
Array.Copy(_arr, _tail, new_arr, 0, _size - _tail);
Array.Copy(_arr, 0, new_arr, (_size - _tail), _head);
_tail = 0;
_head = _length;
_arr = new_arr;

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wabbajack.Common
{
public enum ModManager
{
MO2,
Vortex
}
}

View File

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Win32;

View File

@ -1,16 +1,12 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
using Microsoft.Win32;
namespace Wabbajack.Common
{
public enum Game {
//MO2 GAMES
Morrowind,
Oblivion,
Fallout3,
@ -18,16 +14,33 @@ namespace Wabbajack.Common
Skyrim,
SkyrimSpecialEdition,
Fallout4,
SkyrimVR
SkyrimVR,
//VORTEX GAMES
DarkestDungeon,
DivinityOriginalSin2,
DivinityOriginalSin2DE, //definitive edition has its own nexus page but same Steam/GOG ids
Starbound,
SWKOTOR,
SWKOTOR2,
WITCHER,
WITCHER2,
WITCHER3
}
public class GameMetaData
{
public ModManager SupportedModManager { get; internal set; }
public string MO2ArchiveName { get; internal set; }
public Game Game { get; internal set; }
public string NexusName { get; internal set; }
public string MO2Name { get; internal set; }
public string GameLocationRegistryKey { get; internal set; }
// to get steam ids: https://steamdb.info
public List<int> SteamIDs { get; internal set; }
// to get gog ids: https://www.gogdb.org
public List<int> GOGIDs { get; internal set; }
// these are additional folders when a game installs mods outside the game folder
public List<string> AdditionalFolders { get; internal set; }
public string GameLocation
{
@ -53,6 +66,11 @@ namespace Wabbajack.Common
return Games.Values.FirstOrDefault(g => g.MO2ArchiveName?.ToLower() == gamename);
}
public static GameMetaData GetByNexusName(string gameName)
{
return Games.Values.FirstOrDefault(g => g.NexusName == gameName.ToLower());
}
public static Dictionary<Game, GameMetaData> Games = new Dictionary<Game, GameMetaData>
{
@ -62,72 +80,195 @@ namespace Wabbajack.Common
{
Game.Oblivion, new GameMetaData
{
SupportedModManager = ModManager.MO2,
Game = Game.Oblivion,
NexusName = "oblivion",
MO2Name = "Oblivion",
MO2ArchiveName = "oblivion",
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Oblivion"
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Oblivion",
SteamIDs = new List<int> {22330}
}
},
{
Game.Fallout3, new GameMetaData
{
SupportedModManager = ModManager.MO2,
Game = Game.Fallout3,
NexusName = "fallout3",
MO2Name = "fallout3",
MO2ArchiveName = "fallout3",
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Fallout3"
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Fallout3",
SteamIDs = new List<int> {22300, 22370} // base game and GotY
}
},
{
Game.FalloutNewVegas, new GameMetaData
{
SupportedModManager = ModManager.MO2,
Game = Game.FalloutNewVegas,
NexusName = "newvegas",
MO2Name = "New Vegas",
MO2ArchiveName = "falloutnv",
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\falloutnv"
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\falloutnv",
SteamIDs = new List<int> {22380}
}
},
{
Game.Skyrim, new GameMetaData
{
SupportedModManager = ModManager.MO2,
Game = Game.Skyrim,
NexusName = "skyrim",
MO2Name = "Skyrim",
MO2ArchiveName = "skyrim",
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\skyrim"
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\skyrim",
SteamIDs = new List<int> {72850}
}
},
{
Game.SkyrimSpecialEdition, new GameMetaData
{
SupportedModManager = ModManager.MO2,
Game = Game.SkyrimSpecialEdition,
NexusName = "skyrimspecialedition",
MO2Name = "Skyrim Special Edition",
MO2ArchiveName = "skyrimse",
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Skyrim Special Edition"
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Skyrim Special Edition",
SteamIDs = new List<int> {489830}
}
},
{
Game.Fallout4, new GameMetaData
{
SupportedModManager = ModManager.MO2,
Game = Game.Fallout4,
NexusName = "fallout4",
MO2Name = "Fallout 4",
MO2ArchiveName = "fallout4",
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Fallout4"
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Fallout4",
SteamIDs = new List<int> {377160}
}
},
/*{
Game.Fallout4VR, new GameMetaData
{
SupportedModManager = ModManager.MO2,
Game = Game.Fallout4VR,
NexusName = "fallout4",
MO2Name = "Fallout 4",
MO2ArchiveName = "fallout4",
SteamIDs = new List<int>{611660}
}
},*/
{
Game.SkyrimVR, new GameMetaData
{
SupportedModManager = ModManager.MO2,
Game = Game.SkyrimVR,
NexusName = "skyrimspecialedition",
MO2Name = "Skyrim VR",
MO2ArchiveName = "skyrimse",
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Skyrim VR"
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Skyrim VR",
SteamIDs = new List<int> {611670}
}
},
{
Game.DarkestDungeon, new GameMetaData
{
SupportedModManager = ModManager.Vortex,
Game = Game.DarkestDungeon,
NexusName = "darkestdungeon",
SteamIDs = new List<int> {262060},
GOGIDs = new List<int>{1450711444}
}
},
{
Game.DivinityOriginalSin2, new GameMetaData
{
SupportedModManager = ModManager.Vortex,
Game = Game.DivinityOriginalSin2,
NexusName = "divinityoriginalsin2",
SteamIDs = new List<int> {435150},
GOGIDs = new List<int>{1584823040},
AdditionalFolders = new List<string>
{
"%documents%\\Larian Studios\\Divinity Original Sin 2\\Mods\\",
}
}
},
{
Game.DivinityOriginalSin2DE, new GameMetaData
{
SupportedModManager = ModManager.Vortex,
Game = Game.DivinityOriginalSin2DE,
NexusName = "divinityoriginalsin2definitiveedition",
SteamIDs = new List<int> {435150},
GOGIDs = new List<int>{1584823040},
AdditionalFolders = new List<string>
{
"%documents%\\Larian Studios\\Divinity Original Sin 2 Definitive Edition\\Mods\\"
}
}
},
{
Game.Starbound, new GameMetaData
{
SupportedModManager = ModManager.Vortex,
Game = Game.Starbound,
NexusName = "starbound",
SteamIDs = new List<int>{211820},
GOGIDs = new List<int>{1452598881}
}
},
{
Game.SWKOTOR, new GameMetaData
{
SupportedModManager = ModManager.Vortex,
Game = Game.SWKOTOR,
NexusName = "kotor",
SteamIDs = new List<int>{32370},
GOGIDs = new List<int>{1207666283}
}
},
{
Game.SWKOTOR2, new GameMetaData
{
SupportedModManager = ModManager.Vortex,
Game = Game.SWKOTOR2,
NexusName = "kotor2",
SteamIDs = new List<int>{208580},
GOGIDs = new List<int>{1421404581}
}
},
{
Game.WITCHER, new GameMetaData
{
SupportedModManager = ModManager.Vortex,
Game = Game.WITCHER,
NexusName = "witcher",
SteamIDs = new List<int>{20900},
GOGIDs = new List<int>{1207658924}
}
},
{
Game.WITCHER2, new GameMetaData
{
SupportedModManager = ModManager.Vortex,
Game = Game.WITCHER2,
NexusName = "witcher2",
SteamIDs = new List<int>{20920},
GOGIDs = new List<int>{1207658930}
}
},
{
Game.WITCHER3, new GameMetaData
{
SupportedModManager = ModManager.Vortex,
Game = Game.WITCHER3,
NexusName = "witcher3",
SteamIDs = new List<int>{292030, 499450}, // normal and GotY
GOGIDs = new List<int>{1207664643, 1495134320, 1207664663, 1640424747} // normal, GotY and both in packages
}
}
};

View File

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

View File

@ -125,6 +125,28 @@ namespace Wabbajack.Common
}
}
public static async Task<string> FileHashAsync(this string file, bool nullOnIOError = false)
{
try
{
var hash = new xxHashConfig();
hash.HashSizeInBits = 64;
hash.Seed = 0x42;
using (var fs = File.OpenRead(file))
{
var config = new xxHashConfig();
config.HashSizeInBits = 64;
var value = await xxHashFactory.Instance.Create(config).ComputeHashAsync(fs);
return value.AsBase64String();
}
}
catch (IOException ex)
{
if (nullOnIOError) return null;
throw ex;
}
}
public static void CopyToWithStatus(this Stream istream, long maxSize, Stream ostream, string status)
{
var buffer = new byte[1024 * 64];

View File

@ -91,6 +91,8 @@
<Compile Include="DynamicIniData.cs" />
<Compile Include="Error States\ErrorResponse.cs" />
<Compile Include="Error States\GetResponse.cs" />
<Compile Include="Enums\ModManager.cs" />
<Compile Include="Enums\RunMode.cs" />
<Compile Include="ExtensionManager.cs" />
<Compile Include="Extensions\DictionaryExt.cs" />
<Compile Include="Extensions\HashHelper.cs" />

View File

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VFS;
using Wabbajack.Common;
using Wabbajack.Lib.CompilationSteps;
namespace Wabbajack.Lib
{
public abstract class ACompiler
{
public ModManager ModManager;
public string GamePath;
public string ModListOutputFolder;
public string ModListOutputFile;
public List<Archive> SelectedArchives;
public List<Directive> InstallDirectives;
public List<RawSourceFile> AllFiles;
public ModList ModList;
public VirtualFileSystem VFS;
public List<IndexedArchive> IndexedArchives;
public Dictionary<string, IEnumerable<VirtualFile>> IndexedFiles;
public abstract void Info(string msg);
public abstract void Status(string msg);
public abstract void Error(string msg);
internal abstract string IncludeFile(byte[] data);
internal abstract string IncludeFile(string data);
public abstract bool Compile();
public abstract Directive RunStack(IEnumerable<ICompilationStep> stack, RawSourceFile source);
public abstract IEnumerable<ICompilationStep> GetStack();
public abstract IEnumerable<ICompilationStep> MakeStack();
}
}

View File

@ -2,9 +2,9 @@
{
public abstract class ACompilationStep : ICompilationStep
{
protected Compiler _compiler;
protected ACompiler _compiler;
public ACompilationStep(Compiler compiler)
public ACompilationStep(ACompiler compiler)
{
_compiler = compiler;
}

View File

@ -13,10 +13,12 @@ namespace Wabbajack.Lib.CompilationSteps
private readonly IEnumerable<string> _include_directly;
private readonly List<ICompilationStep> _microstack;
private readonly List<ICompilationStep> _microstackWithInclude;
private readonly Compiler _mo2Compiler;
public DeconstructBSAs(Compiler compiler) : base(compiler)
public DeconstructBSAs(ACompiler compiler) : base(compiler)
{
_include_directly = _compiler.ModInis.Where(kv =>
_mo2Compiler = (Compiler) compiler;
_include_directly = _mo2Compiler.ModInis.Where(kv =>
{
var general = kv.Value.General;
if (general.notes != null && general.notes.Contains(Consts.WABBAJACK_INCLUDE)) return true;
@ -28,16 +30,16 @@ namespace Wabbajack.Lib.CompilationSteps
_microstack = new List<ICompilationStep>
{
new DirectMatch(_compiler),
new IncludePatches(_compiler),
new DropAll(_compiler)
new DirectMatch(_mo2Compiler),
new IncludePatches(_mo2Compiler),
new DropAll(_mo2Compiler)
};
_microstackWithInclude = new List<ICompilationStep>
{
new DirectMatch(_compiler),
new IncludePatches(_compiler),
new IncludeAll(_compiler)
new DirectMatch(_mo2Compiler),
new IncludePatches(_mo2Compiler),
new IncludeAll(_mo2Compiler)
};
}
@ -61,7 +63,7 @@ namespace Wabbajack.Lib.CompilationSteps
var id = Guid.NewGuid().ToString();
var matches = source_files.PMap(e => Compiler.RunStack(stack, new RawSourceFile(e)
var matches = source_files.PMap(e => _mo2Compiler.RunStack(stack, new RawSourceFile(e)
{
Path = Path.Combine(Consts.BSACreationDir, id, e.Paths.Last())
}));
@ -71,7 +73,7 @@ namespace Wabbajack.Lib.CompilationSteps
{
if (match is IgnoredDirectly)
Utils.Error($"File required for BSA {source.Path} creation doesn't exist: {match.To}");
_compiler.ExtraFiles.Add(match);
_mo2Compiler.ExtraFiles.Add(match);
}
CreateBSA directive;
@ -92,7 +94,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("DeconstructBSAs")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new DeconstructBSAs(compiler);
}

View File

@ -6,7 +6,7 @@ namespace Wabbajack.Lib.CompilationSteps
{
public class DirectMatch : ACompilationStep
{
public DirectMatch(Compiler compiler) : base(compiler)
public DirectMatch(ACompiler compiler) : base(compiler)
{
}
@ -34,7 +34,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("DirectMatch")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new DirectMatch(compiler);
}

View File

@ -5,7 +5,7 @@ namespace Wabbajack.Lib.CompilationSteps
{
public class DropAll : ACompilationStep
{
public DropAll(Compiler compiler) : base(compiler)
public DropAll(ACompiler compiler) : base(compiler)
{
}
@ -25,7 +25,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("DropAll")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new DropAll(compiler);
}

View File

@ -8,6 +8,6 @@
public interface IState
{
ICompilationStep CreateStep(Compiler compiler);
ICompilationStep CreateStep(ACompiler compiler);
}
}

View File

@ -9,13 +9,15 @@ namespace Wabbajack.Lib.CompilationSteps
public class IgnoreDisabledMods : ACompilationStep
{
private readonly IEnumerable<string> _allEnabledMods;
private readonly Compiler _mo2Compiler;
public IgnoreDisabledMods(Compiler compiler) : base(compiler)
public IgnoreDisabledMods(ACompiler compiler) : base(compiler)
{
var alwaysEnabled = _compiler.ModInis.Where(f => IsAlwaysEnabled(f.Value)).Select(f => f.Key).ToHashSet();
_mo2Compiler = (Compiler) compiler;
var alwaysEnabled = _mo2Compiler.ModInis.Where(f => IsAlwaysEnabled(f.Value)).Select(f => f.Key).ToHashSet();
_allEnabledMods = _compiler.SelectedProfiles
.SelectMany(p => File.ReadAllLines(Path.Combine(_compiler.MO2Folder, "profiles", p, "modlist.txt")))
_allEnabledMods = _mo2Compiler.SelectedProfiles
.SelectMany(p => File.ReadAllLines(Path.Combine(_mo2Compiler.MO2Folder, "profiles", p, "modlist.txt")))
.Where(line => line.StartsWith("+") || line.EndsWith("_separator"))
.Select(line => line.Substring(1))
.Concat(alwaysEnabled)
@ -55,7 +57,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("IgnoreDisabledMods")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IgnoreDisabledMods(compiler);
}

View File

@ -7,7 +7,7 @@ namespace Wabbajack.Lib.CompilationSteps
private readonly string _postfix;
private readonly string _reason;
public IgnoreEndsWith(Compiler compiler, string postfix) : base(compiler)
public IgnoreEndsWith(ACompiler compiler, string postfix) : base(compiler)
{
_postfix = postfix;
_reason = $"Ignored because path ends with {postfix}";
@ -40,7 +40,7 @@ namespace Wabbajack.Lib.CompilationSteps
public string Postfix { get; set; }
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IgnoreEndsWith(compiler, Postfix);
}

View File

@ -7,7 +7,7 @@ namespace Wabbajack.Lib.CompilationSteps
{
private readonly string _startDir;
public IgnoreGameFiles(Compiler compiler) : base(compiler)
public IgnoreGameFiles(ACompiler compiler) : base(compiler)
{
_startDir = Consts.GameFolderFilesDir + "\\";
}
@ -28,7 +28,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("IgnoreGameFiles")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IgnoreGameFiles(compiler);
}

View File

@ -7,7 +7,7 @@ namespace Wabbajack.Lib.CompilationSteps
private readonly string _pattern;
private readonly string _reason;
public IgnorePathContains(Compiler compiler, string pattern) : base(compiler)
public IgnorePathContains(ACompiler compiler, string pattern) : base(compiler)
{
_pattern = $"\\{pattern.Trim('\\')}\\";
_reason = $"Ignored because path contains {_pattern}";
@ -40,7 +40,7 @@ namespace Wabbajack.Lib.CompilationSteps
public string Pattern { get; set; }
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IgnorePathContains(compiler, Pattern);
}

View File

@ -9,7 +9,7 @@ namespace Wabbajack.Lib.CompilationSteps
private readonly Regex _regex;
private readonly string _pattern;
public IgnoreRegex(Compiler compiler, string pattern) : base(compiler)
public IgnoreRegex(ACompiler compiler, string pattern) : base(compiler)
{
_pattern = pattern;
_reason = $"Ignored because path matches regex {pattern}";
@ -43,7 +43,7 @@ namespace Wabbajack.Lib.CompilationSteps
public string Pattern { get; set; }
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IgnoreRegex(compiler, Pattern);
}

View File

@ -7,7 +7,7 @@ namespace Wabbajack.Lib.CompilationSteps
private readonly string _prefix;
private readonly string _reason;
public IgnoreStartsWith(Compiler compiler, string prefix) : base(compiler)
public IgnoreStartsWith(ACompiler compiler, string prefix) : base(compiler)
{
_prefix = prefix;
_reason = string.Format("Ignored because path starts with {0}", _prefix);
@ -44,7 +44,7 @@ namespace Wabbajack.Lib.CompilationSteps
public string Prefix { get; set; }
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IgnoreStartsWith(compiler, Prefix);
}

View File

@ -0,0 +1,40 @@
using System;
using System.IO;
using Newtonsoft.Json;
using Wabbajack.Common;
namespace Wabbajack.Lib.CompilationSteps
{
public class IgnoreVortex : ACompilationStep
{
private readonly VortexCompiler _vortex;
public IgnoreVortex(ACompiler compiler) : base(compiler)
{
_vortex = (VortexCompiler) compiler;
}
public override Directive Run(RawSourceFile source)
{
if (Path.GetDirectoryName(source.AbsolutePath) != _vortex.DownloadsFolder) return null;
var result = source.EvolveTo<IgnoredDirectly>();
result.Reason = "Ignored because it is a Vortex file";
return result;
}
public override IState GetState()
{
return new State();
}
[JsonObject("IgnoreVortex")]
public class State : IState
{
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IgnoreVortex(compiler);
}
}
}
}

View File

@ -9,7 +9,7 @@ namespace Wabbajack.Lib.CompilationSteps
{
private readonly HashSet<string> _cruftFiles;
public IgnoreWabbajackInstallCruft(Compiler compiler) : base(compiler)
public IgnoreWabbajackInstallCruft(ACompiler compiler) : base(compiler)
{
_cruftFiles = new HashSet<string>
{
@ -34,7 +34,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("IgnoreWabbajackInstallCruft")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IgnoreWabbajackInstallCruft(compiler);
}

View File

@ -5,7 +5,7 @@ namespace Wabbajack.Lib.CompilationSteps
{
public class IncludeAll : ACompilationStep
{
public IncludeAll(Compiler compiler) : base(compiler)
public IncludeAll(ACompiler compiler) : base(compiler)
{
}
@ -24,7 +24,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("IncludeAll")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IncludeAll(compiler);
}

View File

@ -6,7 +6,7 @@ namespace Wabbajack.Lib.CompilationSteps
{
public class IncludeAllConfigs : ACompilationStep
{
public IncludeAllConfigs(Compiler compiler) : base(compiler)
public IncludeAllConfigs(ACompiler compiler) : base(compiler)
{
}
@ -26,7 +26,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("IncludeAllConfigs")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IncludeAllConfigs(compiler);
}

View File

@ -5,7 +5,7 @@ namespace Wabbajack.Lib.CompilationSteps
{
public class IncludeDummyESPs : ACompilationStep
{
public IncludeDummyESPs(Compiler compiler) : base(compiler)
public IncludeDummyESPs(ACompiler compiler) : base(compiler)
{
}
@ -36,7 +36,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("IncludeDummyESPs")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IncludeDummyESPs(compiler);
}

View File

@ -8,7 +8,7 @@ namespace Wabbajack.Lib.CompilationSteps
{
private readonly string _prefix;
public IncludeLootFiles(Compiler compiler) : base(compiler)
public IncludeLootFiles(ACompiler compiler) : base(compiler)
{
_prefix = Consts.LOOTFolderFilesDir + "\\";
}
@ -29,7 +29,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("IncludeLootFiles")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IncludeLootFiles(compiler);
}

View File

@ -5,7 +5,7 @@ namespace Wabbajack.Lib.CompilationSteps
{
public class IncludeModIniData : ACompilationStep
{
public IncludeModIniData(Compiler compiler) : base(compiler)
public IncludeModIniData(ACompiler compiler) : base(compiler)
{
}
@ -25,7 +25,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("IncludeModIniData")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IncludeModIniData(compiler);
}

View File

@ -8,10 +8,13 @@ namespace Wabbajack.Lib.CompilationSteps
public class IgnoreOtherProfiles : ACompilationStep
{
private readonly IEnumerable<string> _profiles;
private readonly Compiler _mo2Compiler;
public IgnoreOtherProfiles(Compiler compiler) : base(compiler)
public IgnoreOtherProfiles(ACompiler compiler) : base(compiler)
{
_profiles = _compiler.SelectedProfiles
_mo2Compiler = (Compiler) compiler;
_profiles = _mo2Compiler.SelectedProfiles
.Select(p => Path.Combine("profiles", p) + "\\")
.ToList();
}
@ -33,7 +36,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("IgnoreOtherProfiles")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IgnoreOtherProfiles(compiler);
}

View File

@ -11,7 +11,7 @@ namespace Wabbajack.Lib.CompilationSteps
{
private readonly Dictionary<string, IGrouping<string, VirtualFile>> _indexed;
public IncludePatches(Compiler compiler) : base(compiler)
public IncludePatches(ACompiler compiler) : base(compiler)
{
_indexed = _compiler.IndexedFiles.Values
.SelectMany(f => f)
@ -47,7 +47,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("IncludePatches")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IncludePatches(compiler);
}

View File

@ -7,31 +7,34 @@ namespace Wabbajack.Lib.CompilationSteps
{
public class IncludePropertyFiles : ACompilationStep
{
public IncludePropertyFiles(Compiler compiler) : base(compiler)
private readonly Compiler _mo2Compiler;
public IncludePropertyFiles(ACompiler compiler) : base(compiler)
{
_mo2Compiler = (Compiler) compiler;
}
public override Directive Run(RawSourceFile source)
{
var files = new HashSet<string>
{
_compiler.ModListImage, _compiler.ModListReadme
_mo2Compiler.ModListImage, _mo2Compiler.ModListReadme
};
if (!files.Any(f => source.AbsolutePath.Equals(f))) return null;
if (!File.Exists(source.AbsolutePath)) return null;
var isBanner = source.AbsolutePath == _compiler.ModListImage;
var isBanner = source.AbsolutePath == _mo2Compiler.ModListImage;
//var isReadme = source.AbsolutePath == ModListReadme;
var result = source.EvolveTo<PropertyFile>();
result.SourceDataID = _compiler.IncludeFile(File.ReadAllBytes(source.AbsolutePath));
if (isBanner)
{
result.Type = PropertyType.Banner;
_compiler.ModListImage = result.SourceDataID;
_mo2Compiler.ModListImage = result.SourceDataID;
}
else
{
result.Type = PropertyType.Readme;
_compiler.ModListReadme = result.SourceDataID;
_mo2Compiler.ModListReadme = result.SourceDataID;
}
return result;
@ -45,7 +48,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("IncludePropertyFiles")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IncludePropertyFiles(compiler);
}

View File

@ -9,7 +9,7 @@ namespace Wabbajack.Lib.CompilationSteps
private readonly string _pattern;
private readonly Regex _regex;
public IncludeRegex(Compiler compiler, string pattern) : base(compiler)
public IncludeRegex(ACompiler compiler, string pattern) : base(compiler)
{
_pattern = pattern;
_regex = new Regex(pattern);
@ -43,7 +43,7 @@ namespace Wabbajack.Lib.CompilationSteps
public string Pattern { get; set; }
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IncludeRegex(compiler, Pattern);
}

View File

@ -7,8 +7,11 @@ namespace Wabbajack.Lib.CompilationSteps
{
public class IncludeStubbedConfigFiles : ACompilationStep
{
public IncludeStubbedConfigFiles(Compiler compiler) : base(compiler)
private readonly Compiler _mo2Compiler;
public IncludeStubbedConfigFiles(ACompiler compiler) : base(compiler)
{
_mo2Compiler = (Compiler) compiler;
}
public override Directive Run(RawSourceFile source)
@ -26,18 +29,18 @@ namespace Wabbajack.Lib.CompilationSteps
var data = File.ReadAllText(source.AbsolutePath);
var originalData = data;
data = data.Replace(_compiler.GamePath, Consts.GAME_PATH_MAGIC_BACK);
data = data.Replace(_compiler.GamePath.Replace("\\", "\\\\"), Consts.GAME_PATH_MAGIC_DOUBLE_BACK);
data = data.Replace(_compiler.GamePath.Replace("\\", "/"), Consts.GAME_PATH_MAGIC_FORWARD);
data = data.Replace(_mo2Compiler.GamePath, Consts.GAME_PATH_MAGIC_BACK);
data = data.Replace(_mo2Compiler.GamePath.Replace("\\", "\\\\"), Consts.GAME_PATH_MAGIC_DOUBLE_BACK);
data = data.Replace(_mo2Compiler.GamePath.Replace("\\", "/"), Consts.GAME_PATH_MAGIC_FORWARD);
data = data.Replace(_compiler.MO2Folder, Consts.MO2_PATH_MAGIC_BACK);
data = data.Replace(_compiler.MO2Folder.Replace("\\", "\\\\"), Consts.MO2_PATH_MAGIC_DOUBLE_BACK);
data = data.Replace(_compiler.MO2Folder.Replace("\\", "/"), Consts.MO2_PATH_MAGIC_FORWARD);
data = data.Replace(_mo2Compiler.MO2Folder, Consts.MO2_PATH_MAGIC_BACK);
data = data.Replace(_mo2Compiler.MO2Folder.Replace("\\", "\\\\"), Consts.MO2_PATH_MAGIC_DOUBLE_BACK);
data = data.Replace(_mo2Compiler.MO2Folder.Replace("\\", "/"), Consts.MO2_PATH_MAGIC_FORWARD);
data = data.Replace(_compiler.MO2DownloadsFolder, Consts.DOWNLOAD_PATH_MAGIC_BACK);
data = data.Replace(_compiler.MO2DownloadsFolder.Replace("\\", "\\\\"),
data = data.Replace(_mo2Compiler.MO2DownloadsFolder, Consts.DOWNLOAD_PATH_MAGIC_BACK);
data = data.Replace(_mo2Compiler.MO2DownloadsFolder.Replace("\\", "\\\\"),
Consts.DOWNLOAD_PATH_MAGIC_DOUBLE_BACK);
data = data.Replace(_compiler.MO2DownloadsFolder.Replace("\\", "/"), Consts.DOWNLOAD_PATH_MAGIC_FORWARD);
data = data.Replace(_mo2Compiler.MO2DownloadsFolder.Replace("\\", "/"), Consts.DOWNLOAD_PATH_MAGIC_FORWARD);
if (data == originalData)
return null;
@ -49,7 +52,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("IncludeStubbedConfigFiles")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IncludeStubbedConfigFiles(compiler);
}

View File

@ -9,12 +9,13 @@ namespace Wabbajack.Lib.CompilationSteps
{
private readonly IEnumerable<string> _includeDirectly;
private readonly string _tag;
private readonly Compiler _mo2Compiler;
public IncludeTaggedMods(Compiler compiler, string tag) : base(compiler)
public IncludeTaggedMods(ACompiler compiler, string tag) : base(compiler)
{
_mo2Compiler = (Compiler) compiler;
_tag = tag;
_includeDirectly = _compiler.ModInis.Where(kv =>
_includeDirectly = _mo2Compiler.ModInis.Where(kv =>
{
var general = kv.Value.General;
if (general.notes != null && general.notes.Contains(_tag))
@ -56,7 +57,7 @@ namespace Wabbajack.Lib.CompilationSteps
public string Tag { get; set; }
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IncludeTaggedMods(compiler, Tag);
}

View File

@ -9,10 +9,12 @@ namespace Wabbajack.Lib.CompilationSteps
public class IncludeThisProfile : ACompilationStep
{
private readonly IEnumerable<string> _correctProfiles;
private readonly Compiler _mo2Compiler;
public IncludeThisProfile(Compiler compiler) : base(compiler)
public IncludeThisProfile(ACompiler compiler) : base(compiler)
{
_correctProfiles = _compiler.SelectedProfiles.Select(p => Path.Combine("profiles", p) + "\\").ToList();
_mo2Compiler = (Compiler) compiler;
_correctProfiles = _mo2Compiler.SelectedProfiles.Select(p => Path.Combine("profiles", p) + "\\").ToList();
}
public override Directive Run(RawSourceFile source)
@ -48,7 +50,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("IncludeThisProfile")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IncludeThisProfile(compiler);
}

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Wabbajack.Common;
namespace Wabbajack.Lib.CompilationSteps
{
public class IncludeVortexDeployment : ACompilationStep
{
public IncludeVortexDeployment(ACompiler compiler) : base(compiler)
{
}
public override Directive Run(RawSourceFile source)
{
if (!source.Path.EndsWith("vortex.deployment.msgpack") &&
!source.Path.EndsWith("\\vortex.deployment.json")) return null;
var inline = source.EvolveTo<InlineFile>();
inline.SourceDataID = _compiler.IncludeFile(File.ReadAllBytes(source.AbsolutePath));
return inline;
}
public override IState GetState()
{
return new State();
}
public class State : IState
{
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IncludeVortexDeployment(compiler);
}
}
}
}

View File

@ -8,14 +8,17 @@ namespace Wabbajack.Lib.CompilationSteps
{
public class PatchStockESMs : ACompilationStep
{
public PatchStockESMs(Compiler compiler) : base(compiler)
private readonly Compiler _mo2Compiler;
public PatchStockESMs(ACompiler compiler) : base(compiler)
{
_mo2Compiler = (Compiler) compiler;
}
public override Directive Run(RawSourceFile source)
{
var filename = Path.GetFileName(source.Path);
var gameFile = Path.Combine(_compiler.GamePath, "Data", filename);
var gameFile = Path.Combine(_mo2Compiler.GamePath, "Data", filename);
if (!Consts.GameESMs.Contains(filename) || !source.Path.StartsWith("mods\\") ||
!File.Exists(gameFile)) return null;
@ -44,7 +47,7 @@ namespace Wabbajack.Lib.CompilationSteps
[JsonObject("PatchStockESMs")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new PatchStockESMs(compiler);
}

View File

@ -13,7 +13,7 @@ namespace Wabbajack.Lib.CompilationSteps
.ToJSON(TypeNameHandling.Auto, TypeNameAssemblyFormatHandling.Simple);
}
public static List<ICompilationStep> Deserialize(string stack, Compiler compiler)
public static List<ICompilationStep> Deserialize(string stack, ACompiler compiler)
{
return stack.FromJSONString<List<IState>>(TypeNameHandling.Auto, TypeNameAssemblyFormatHandling.Simple)
.Select(s => s.CreateStep(compiler)).ToList();

View File

@ -26,7 +26,7 @@ using Path = Alphaleonis.Win32.Filesystem.Path;
namespace Wabbajack.Lib
{
public class Compiler
public class Compiler : ACompiler
{
private string _mo2DownloadsFolder;
@ -42,9 +42,23 @@ namespace Wabbajack.Lib
public Compiler(string mo2_folder)
{
ModManager = ModManager.MO2;
MO2Folder = mo2_folder;
MO2Ini = Path.Combine(MO2Folder, "ModOrganizer.ini").LoadIniFile();
GamePath = ((string)MO2Ini.General.gamePath).Replace("\\\\", "\\");
ModListOutputFolder = "output_folder";
ModListOutputFile = MO2Profile + ExtensionManager.Extension;
SelectedArchives = new List<Archive>();
InstallDirectives = new List<Directive>();
AllFiles = new List<RawSourceFile>();
ModList = new ModList();
VFS = VirtualFileSystem.VFS;
IndexedArchives = new List<IndexedArchive>();
IndexedFiles = new Dictionary<string, IEnumerable<VirtualFile>>();
}
public dynamic MO2Ini { get; }
@ -70,55 +84,43 @@ namespace Wabbajack.Lib
public string MO2ProfileDir => Path.Combine(MO2Folder, "profiles", MO2Profile);
public string ModListOutputFolder => "output_folder";
public string ModListOutputFile => MO2Profile + ExtensionManager.Extension;
public List<Directive> InstallDirectives { get; private set; }
internal UserStatus User { get; private set; }
public List<Archive> SelectedArchives { get; private set; }
public List<RawSourceFile> AllFiles { get; private set; }
public ModList ModList { get; private set; }
public ConcurrentBag<Directive> ExtraFiles { get; private set; }
public Dictionary<string, dynamic> ModInis { get; private set; }
public VirtualFileSystem VFS => VirtualFileSystem.VFS;
public List<IndexedArchive> IndexedArchives { get; private set; }
public Dictionary<string, IEnumerable<VirtualFile>> IndexedFiles { get; private set; }
public HashSet<string> SelectedProfiles { get; set; } = new HashSet<string>();
public void Info(string msg)
public override void Info(string msg)
{
Utils.Log(msg);
}
public void Status(string msg)
public override void Status(string msg)
{
WorkQueue.Report(msg, 0);
}
private void Error(string msg)
public override void Error(string msg)
{
Utils.Log(msg);
throw new Exception(msg);
}
internal string IncludeFile(byte[] data)
internal override string IncludeFile(byte[] data)
{
var id = Guid.NewGuid().ToString();
File.WriteAllBytes(Path.Combine(ModListOutputFolder, id), data);
return id;
}
internal string IncludeFile(string data)
internal override string IncludeFile(string data)
{
var id = Guid.NewGuid().ToString();
File.WriteAllText(Path.Combine(ModListOutputFolder, id), data);
return id;
}
public bool Compile()
public override bool Compile()
{
VirtualFileSystem.Clean();
Info("Looking for other profiles");
@ -283,6 +285,7 @@ namespace Wabbajack.Lib
GameType = GameRegistry.Games.Values.First(f => f.MO2Name == MO2Ini.General.gameName).Game,
WabbajackVersion = WabbajackVersion,
Archives = SelectedArchives,
ModManager = ModManager.MO2,
Directives = InstallDirectives,
Name = ModListName ?? MO2Profile,
Author = ModListAuthor ?? "",
@ -533,7 +536,7 @@ namespace Wabbajack.Lib
}
public static Directive RunStack(IEnumerable<ICompilationStep> stack, RawSourceFile source)
public override Directive RunStack(IEnumerable<ICompilationStep> stack, RawSourceFile source)
{
Utils.Status($"Compiling {source.Path}");
foreach (var step in stack)
@ -545,7 +548,7 @@ namespace Wabbajack.Lib
throw new InvalidDataException("Data fell out of the compilation stack");
}
public IEnumerable<ICompilationStep> GetStack()
public override IEnumerable<ICompilationStep> GetStack()
{
var user_config = Path.Combine(MO2ProfileDir, "compilation_stack.yml");
if (File.Exists(user_config))
@ -566,7 +569,7 @@ namespace Wabbajack.Lib
/// result included into the pack
/// </summary>
/// <returns></returns>
public IEnumerable<ICompilationStep> MakeStack()
public override IEnumerable<ICompilationStep> MakeStack()
{
Utils.Log("Generating compilation stack");
return new List<ICompilationStep>

View File

@ -40,6 +40,11 @@ namespace Wabbajack.Lib
/// </summary>
public List<Archive> Archives;
/// <summary>
/// The Mod Manager used to create the modlist
/// </summary>
public ModManager ModManager;
/// <summary>
/// The game variant to which this game applies
/// </summary>
@ -204,6 +209,7 @@ namespace Wabbajack.Lib
/// </summary>
public string Hash;
/// <summary>
/// Meta INI for the downloaded archive
/// </summary>
public string Meta;

View File

@ -19,7 +19,8 @@ namespace Wabbajack.Lib.Downloaders
if (general.modID != null && general.fileID != null && general.gameName != null)
{
var name = (string)general.gameName;
var game = GameRegistry.GetByMO2ArchiveName(name).Game;
var gameMeta = GameRegistry.GetByMO2ArchiveName(name);
var game = gameMeta != null ? GameRegistry.GetByMO2ArchiveName(name).Game : GameRegistry.GetByNexusName(name).Game;
var info = new NexusApiClient().GetModInfo(game, general.modID);
return new State
{
@ -52,14 +53,8 @@ namespace Wabbajack.Lib.Downloaders
return;
}
if (!status.is_premium)
{
Utils.Error($"Automated installs with Wabbajack requires a premium nexus account. {client.Username} is not a premium account.");
return;
}
client.ClearUpdatedModsInCache();
//var updated = client.GetModsUpdatedSince(Game.Skyrim,DateTime.Now - TimeSpan.FromDays(30));
if (status.is_premium) return;
Utils.Error($"Automated installs with Wabbajack requires a premium nexus account. {client.Username} is not a premium account.");
}
public class State : AbstractDownloadState

View File

@ -46,6 +46,12 @@ namespace Wabbajack.Lib.NexusApi
public bool contains_adult_content;
}
public class MD5Response
{
public ModInfo mod;
public NexusFileInfo file_details;
}
public class EndorsementResponse
{
public string message;

View File

@ -222,7 +222,6 @@ namespace Wabbajack.Lib.NexusApi
}
public string GetNexusDownloadLink(NexusDownloader.State archive, bool cache = false)
{
if (cache && TryGetCachedLink(archive, out var result))
@ -269,6 +268,12 @@ namespace Wabbajack.Lib.NexusApi
return GetCached<GetModFilesResponse>(url).files;
}
public List<MD5Response> GetModInfoFromMD5(Game game, string md5Hash)
{
var url = $"https://api.nexusmods.com/v1/games/{GameRegistry.Games[game].NexusName}/mods/md5_search/{md5Hash}.json";
return Get<List<MD5Response>>(url);
}
public ModInfo GetModInfo(Game game, string modId)
{
var url = $"https://api.nexusmods.com/v1/games/{GameRegistry.Games[game].NexusName}/mods/{modId}.json";
@ -365,4 +370,4 @@ namespace Wabbajack.Lib.NexusApi
}
}
}
}

View File

@ -1,4 +1,5 @@
using Wabbajack.Common;
using System.Text.RegularExpressions;
using Wabbajack.Common;
namespace Wabbajack.Lib.NexusApi
{
@ -6,6 +7,8 @@ namespace Wabbajack.Lib.NexusApi
{
public static string ConvertGameName(string gameName)
{
if (Regex.IsMatch(gameName, @"^[^a-z\s]+\.[^a-z\s]+$"))
return gameName;
return GameRegistry.GetByMO2ArchiveName(gameName)?.NexusName ?? gameName.ToLower();
}

View File

@ -0,0 +1,452 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Microsoft.WindowsAPICodePack.Shell;
using VFS;
using Wabbajack.Common;
using Wabbajack.Lib.CompilationSteps;
using Wabbajack.Lib.Downloaders;
using Wabbajack.Lib.ModListRegistry;
using Wabbajack.Lib.NexusApi;
using File = Alphaleonis.Win32.Filesystem.File;
namespace Wabbajack.Lib
{
public class VortexCompiler : ACompiler
{
public Game Game { get; }
public string GameName { get; }
public string VortexFolder { get; set; }
public string StagingFolder { get; set; }
public string DownloadsFolder { get; set; }
public bool IgnoreMissingFiles { get; set; }
public VortexCompiler(string gameName, string gamePath)
{
ModManager = ModManager.Vortex;
// TODO: only for testing
IgnoreMissingFiles = true;
string[] args = Environment.GetCommandLineArgs();
GamePath = gamePath;
GameName = gameName;
Game = GameRegistry.GetByNexusName(GameName).Game;
//args: wabbajacke.exe gameName gamePath vortexfolder stagingfolder downloadsfolder
VortexFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Vortex");
if (File.Exists(Path.Combine(VortexFolder, gameName, "mods", "__vortex_staging_folder")))
StagingFolder = Path.Combine(VortexFolder, gameName, "mods");
if (File.Exists(Path.Combine(VortexFolder, "downloads", "__vortex_downloads_folder")))
DownloadsFolder = Path.Combine(VortexFolder, "downloads", gameName);
if (args.Length >= 4)
StagingFolder = args[3];
if (args.Length == 5)
DownloadsFolder = args[4];
ModListOutputFolder = "output_folder";
// TODO: add custom modlist name
ModListOutputFile = $"VORTEX_TEST_MODLIST{ExtensionManager.Extension}";
VFS = VirtualFileSystem.VFS;
SelectedArchives = new List<Archive>();
AllFiles = new List<RawSourceFile>();
IndexedArchives = new List<IndexedArchive>();
IndexedFiles = new Dictionary<string, IEnumerable<VirtualFile>>();
}
public override void Info(string msg)
{
Utils.Log(msg);
}
public override void Status(string msg)
{
WorkQueue.Report(msg, 0);
}
public override void Error(string msg)
{
Utils.Log(msg);
throw new Exception(msg);
}
internal override string IncludeFile(byte[] data)
{
var id = Guid.NewGuid().ToString();
File.WriteAllBytes(Path.Combine(ModListOutputFolder, id), data);
return id;
}
internal override string IncludeFile(string data)
{
var id = Guid.NewGuid().ToString();
File.WriteAllText(Path.Combine(ModListOutputFolder, id), data);
return id;
}
public override bool Compile()
{
VirtualFileSystem.Clean();
Info($"Starting Vortex compilation for {GameName} at {GamePath} with staging folder at {StagingFolder} and downloads folder at {DownloadsFolder}.");
Info("Starting pre-compilation steps");
CreateMetaFiles();
Info($"Indexing {StagingFolder}");
VFS.AddRoot(StagingFolder);
Info($"Indexing {GamePath}");
VFS.AddRoot(GamePath);
Info($"Indexing {DownloadsFolder}");
VFS.AddRoot(DownloadsFolder);
AddExternalFolder();
Info("Cleaning output folder");
if (Directory.Exists(ModListOutputFolder)) Directory.Delete(ModListOutputFolder, true);
Directory.CreateDirectory(ModListOutputFolder);
IEnumerable<RawSourceFile> vortexStagingFiles = Directory.EnumerateFiles(StagingFolder, "*", SearchOption.AllDirectories)
.Where(p => p.FileExists() && p != "__vortex_staging_folder")
.Select(p => new RawSourceFile(VFS.Lookup(p))
{Path = p.RelativeTo(StagingFolder)});
IEnumerable<RawSourceFile> vortexDownloads = Directory.EnumerateFiles(DownloadsFolder, "*", SearchOption.AllDirectories)
.Where(p => p.FileExists())
.Select(p => new RawSourceFile(VFS.Lookup(p))
{Path = p.RelativeTo(DownloadsFolder)});
IEnumerable<RawSourceFile> gameFiles = Directory.EnumerateFiles(GamePath, "*", SearchOption.AllDirectories)
.Where(p => p.FileExists())
.Select(p => new RawSourceFile(VFS.Lookup(p))
{ Path = Path.Combine(Consts.GameFolderFilesDir, p.RelativeTo(GamePath)) });
Info("Indexing Archives");
IndexedArchives = Directory.EnumerateFiles(DownloadsFolder)
.Where(f => File.Exists(f+".meta"))
.Select(f => new IndexedArchive
{
File = VFS.Lookup(f),
Name = Path.GetFileName(f),
IniData = (f+".meta").LoadIniFile(),
Meta = File.ReadAllText(f+".meta")
})
.ToList();
Info("Indexing Files");
IDictionary<VirtualFile, IEnumerable<VirtualFile>> grouped = VFS.GroupedByArchive();
IndexedFiles = IndexedArchives.Select(f => grouped.TryGetValue(f.File, out var result) ? result : new List<VirtualFile>())
.SelectMany(fs => fs)
.Concat(IndexedArchives.Select(f => f.File))
.OrderByDescending(f => f.TopLevelArchive.LastModified)
.GroupBy(f => f.Hash)
.ToDictionary(f => f.Key, f => f.AsEnumerable());
Info("Searching for mod files");
AllFiles = vortexStagingFiles.Concat(vortexDownloads)
.Concat(gameFiles)
.DistinctBy(f => f.Path)
.ToList();
Info($"Found {AllFiles.Count} files to build into mod list");
Info("Verifying destinations");
List<IGrouping<string, RawSourceFile>> dups = AllFiles.GroupBy(f => f.Path)
.Where(fs => fs.Count() > 1)
.Select(fs =>
{
Utils.Log($"Duplicate files installed to {fs.Key} from : {string.Join(", ", fs.Select(f => f.AbsolutePath))}");
return fs;
}).ToList();
if (dups.Count > 0)
{
Error($"Found {dups.Count} duplicates, exiting");
}
IEnumerable<ICompilationStep> stack = MakeStack();
Info("Running Compilation Stack");
List<Directive> results = AllFiles.PMap(f => RunStack(stack.Where(s => s != null), f)).ToList();
IEnumerable<NoMatch> noMatch = results.OfType<NoMatch>().ToList();
Info($"No match for {noMatch.Count()} files");
foreach (var file in noMatch)
Info($" {file.To}");
if (noMatch.Any())
{
if (IgnoreMissingFiles)
{
Info("Continuing even though files were missing at the request of the user.");
}
else
{
Info("Exiting due to no way to compile these files");
return false;
}
}
InstallDirectives = results.Where(i => !(i is IgnoredDirectly)).ToList();
// TODO: nexus stuff
/*Info("Getting Nexus api_key, please click authorize if a browser window appears");
if (IndexedArchives.Any(a => a.IniData?.General?.gameName != null))
{
var nexusClient = new NexusApiClient();
if (!nexusClient.IsPremium) Error($"User {nexusClient.Username} is not a premium Nexus user, so we cannot access the necessary API calls, cannot continue");
}
*/
GatherArchives();
ModList = new ModList
{
Archives = SelectedArchives,
ModManager = ModManager.Vortex,
Directives = InstallDirectives,
GameType = Game
};
ExportModList();
Info("Done Building ModList");
return true;
}
/// <summary>
/// Some have mods outside their game folder located
/// </summary>
private void AddExternalFolder()
{
var currentGame = GameRegistry.Games[Game];
if (currentGame.AdditionalFolders == null || currentGame.AdditionalFolders.Count == 0) return;
currentGame.AdditionalFolders.Do(f =>
{
var path = f.Replace("%documents%", KnownFolders.Documents.Path);
if (!Directory.Exists(path)) return;
Info($"Indexing {path}");
VFS.AddRoot(path);
});
}
private void ExportModList()
{
Utils.Log($"Exporting ModList to: {ModListOutputFolder}");
// using JSON for better debugging
ModList.ToJSON(Path.Combine(ModListOutputFolder, "modlist.json"));
//ModList.ToCERAS(Path.Combine(ModListOutputFolder, "modlist"), ref CerasConfig.Config);
if(File.Exists(ModListOutputFile))
File.Delete(ModListOutputFile);
using (var fs = new FileStream(ModListOutputFile, FileMode.Create))
{
using (var za = new ZipArchive(fs, ZipArchiveMode.Create))
{
Directory.EnumerateFiles(ModListOutputFolder, "*.*")
.DoProgress("Compressing ModList",
f =>
{
var ze = za.CreateEntry(Path.GetFileName(f));
using (var os = ze.Open())
using (var ins = File.OpenRead(f))
{
ins.CopyTo(os);
}
});
}
}
Utils.Log("Exporting ModList metadata");
var metadata = new ModlistMetadata.DownloadMetadata
{
Size = File.GetSize(ModListOutputFile),
Hash = ModListOutputFile.FileHash(),
NumberOfArchives = ModList.Archives.Count,
SizeOfArchives = ModList.Archives.Sum(a => a.Size),
NumberOfInstalledFiles = ModList.Directives.Count,
SizeOfInstalledFiles = ModList.Directives.Sum(a => a.Size)
};
metadata.ToJSON(ModListOutputFile + ".meta.json");
Utils.Log("Removing ModList staging folder");
//Directory.Delete(ModListOutputFolder, true);
}
/*private void GenerateReport()
{
string css;
using (var cssStream = Utils.GetResourceStream("Wabbajack.Lib.css-min.css"))
using (var reader = new StreamReader(cssStream))
{
css = reader.ReadToEnd();
}
using (var fs = File.OpenWrite($"{ModList.Name}.md"))
{
fs.SetLength(0);
using (var reporter = new ReportBuilder(fs, ModListOutputFolder))
{
reporter.Build(this, ModList);
}
}
}*/
private void CreateMetaFiles()
{
Utils.Log("Getting Nexus api_key, please click authorize if a browser window appears");
var nexusClient = new NexusApiClient();
Directory.EnumerateFiles(DownloadsFolder, "*", SearchOption.TopDirectoryOnly)
.Where(f => File.Exists(f) && Path.GetExtension(f) != ".meta" && !File.Exists(f+".meta"))
.Do(f =>
{
Utils.Log($"Trying to create meta file for {Path.GetFileName(f)}");
var metaString = $"[General]\n" +
$"repository=Nexus\n" +
$"installed=true\n" +
$"uninstalled=false\n" +
$"paused=false\n" +
$"removed=false\n" +
$"gameName={GameName}\n";
string hash;
using(var md5 = MD5.Create())
using (var stream = File.OpenRead(f))
{
Utils.Log($"Calculating hash for {Path.GetFileName(f)}");
byte[] cH = md5.ComputeHash(stream);
hash = BitConverter.ToString(cH).Replace("-", "").ToLowerInvariant();
Utils.Log($"Hash is {hash}");
}
List<MD5Response> md5Response = nexusClient.GetModInfoFromMD5(Game, hash);
if (md5Response.Count >= 1)
{
var modInfo = md5Response[0].mod;
metaString += $"modID={modInfo.mod_id}\ndescription={NexusApiUtils.FixupSummary(modInfo.summary)}\n" +
$"modName={modInfo.name}\nfileID={md5Response[0].file_details.file_id}";
File.WriteAllText(f+".meta",metaString, Encoding.UTF8);
}
else
{
Error("Error while getting information from nexusmods via MD5 hash!");
}
});
}
private void GatherArchives()
{
Info("Building a list of archives based on the files required");
var shas = InstallDirectives.OfType<FromArchive>()
.Select(a => a.ArchiveHashPath[0])
.Distinct();
var archives = IndexedArchives.OrderByDescending(f => f.File.LastModified)
.GroupBy(f => f.File.Hash)
.ToDictionary(f => f.Key, f => f.First());
SelectedArchives = shas.PMap(sha => ResolveArchive(sha, archives));
}
private Archive ResolveArchive(string sha, IDictionary<string, IndexedArchive> archives)
{
if (archives.TryGetValue(sha, out var found))
{
if(found.IniData == null)
Error($"No download metadata found for {found.Name}, please use MO2 to query info or add a .meta file and try again.");
var result = new Archive();
result.State = (AbstractDownloadState) DownloadDispatcher.ResolveArchive(found.IniData);
if (result.State == null)
Error($"{found.Name} could not be handled by any of the downloaders");
result.Name = found.Name;
result.Hash = found.File.Hash;
result.Meta = found.Meta;
result.Size = found.File.Size;
Info($"Checking link for {found.Name}");
if (!result.State.Verify())
Error(
$"Unable to resolve link for {found.Name}. If this is hosted on the Nexus the file may have been removed.");
return result;
}
Error($"No match found for Archive sha: {sha} this shouldn't happen");
return null;
}
public override Directive RunStack(IEnumerable<ICompilationStep> stack, RawSourceFile source)
{
Utils.Status($"Compiling {source.Path}");
foreach (var step in stack)
{
var result = step.Run(source);
if (result != null) return result;
}
throw new InvalidDataException("Data fell out of the compilation stack");
}
public override IEnumerable<ICompilationStep> GetStack()
{
var s = Consts.TestMode ? DownloadsFolder : VortexFolder;
var userConfig = Path.Combine(s, "compilation_stack.yml");
if (File.Exists(userConfig))
return Serialization.Deserialize(File.ReadAllText(userConfig), this);
IEnumerable<ICompilationStep> stack = MakeStack();
File.WriteAllText(Path.Combine(s, "_current_compilation_stack.yml"),
Serialization.Serialize(stack));
return stack;
}
public override IEnumerable<ICompilationStep> MakeStack()
{
Utils.Log("Generating compilation stack");
return new List<ICompilationStep>
{
//new IncludePropertyFiles(this),
new IncludeVortexDeployment(this),
new IncludeRegex(this, "^*\\.meta"),
new IgnoreVortex(this),
Game == Game.DarkestDungeon ? new IncludeRegex(this, "project\\.xml$") : null,
new IgnoreStartsWith(this, " __vortex_staging_folder"),
new IgnoreEndsWith(this, "__vortex_staging_folder"),
new IgnoreGameFiles(this),
new DirectMatch(this),
new IgnoreGameFiles(this),
new IgnoreWabbajackInstallCruft(this),
new DropAll(this)
};
}
}
}

View File

@ -0,0 +1,307 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using VFS;
using Wabbajack.Common;
using Wabbajack.Lib.Downloaders;
using Directory = Alphaleonis.Win32.Filesystem.Directory;
using File = Alphaleonis.Win32.Filesystem.File;
using FileInfo = Alphaleonis.Win32.Filesystem.FileInfo;
using Path = Alphaleonis.Win32.Filesystem.Path;
namespace Wabbajack.Lib
{
public class VortexInstaller
{
public string ModListArchive { get; }
public ModList ModList { get; }
public Dictionary<string, string> HashedArchives { get; private set; }
public GameMetaData GameInfo { get; internal set; }
public string StagingFolder { get; set; }
public string DownloadFolder { get; set; }
public VirtualFileSystem VFS => VirtualFileSystem.VFS;
public bool IgnoreMissingFiles { get; internal set; }
public VortexInstaller(string archive, ModList modList)
{
ModListArchive = archive;
ModList = modList;
// TODO: only for testing
IgnoreMissingFiles = true;
GameInfo = GameRegistry.Games[ModList.GameType];
}
public void Info(string msg)
{
Utils.Log(msg);
}
public void Status(string msg)
{
WorkQueue.Report(msg, 0);
}
private void Error(string msg)
{
Utils.Log(msg);
throw new Exception(msg);
}
public byte[] LoadBytesFromPath(string path)
{
using (var fs = new FileStream(ModListArchive, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var ar = new ZipArchive(fs, ZipArchiveMode.Read))
using (var ms = new MemoryStream())
{
var entry = ar.GetEntry(path);
using (var e = entry.Open())
e.CopyTo(ms);
return ms.ToArray();
}
}
public static ModList LoadFromFile(string path)
{
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var ar = new ZipArchive(fs, ZipArchiveMode.Read))
{
var entry = ar.GetEntry("modlist");
if (entry == null)
{
entry = ar.GetEntry("modlist.json");
using (var e = entry.Open())
return e.FromJSON<ModList>();
}
using (var e = entry.Open())
return e.FromCERAS<ModList>(ref CerasConfig.Config);
}
}
public void Install()
{
Directory.CreateDirectory(DownloadFolder);
VirtualFileSystem.Clean();
HashArchives();
DownloadArchives();
HashArchives();
var missing = ModList.Archives.Where(a => !HashedArchives.ContainsKey(a.Hash)).ToList();
if (missing.Count > 0)
{
foreach (var a in missing)
Info($"Unable to download {a.Name}");
if (IgnoreMissingFiles)
Info("Missing some archives, but continuing anyways at the request of the user");
else
Error("Cannot continue, was unable to download one or more archives");
}
PrimeVFS();
BuildFolderStructure();
InstallArchives();
InstallIncludedFiles();
//InctallIncludedDownloadMetas();
Info("Installation complete! You may exit the program.");
}
private void BuildFolderStructure()
{
Info("Building Folder Structure");
ModList.Directives
.OfType<FromArchive>()
.Select(d => Path.Combine(StagingFolder, Path.GetDirectoryName(d.To)))
.ToHashSet()
.Do(f =>
{
if (Directory.Exists(f)) return;
Directory.CreateDirectory(f);
});
}
private void InstallArchives()
{
Info("Installing Archives");
Info("Grouping Install Files");
var grouped = ModList.Directives
.OfType<FromArchive>()
.GroupBy(e => e.ArchiveHashPath[0])
.ToDictionary(k => k.Key);
var archives = ModList.Archives
.Select(a => new { Archive = a, AbsolutePath = HashedArchives.GetOrDefault(a.Hash) })
.Where(a => a.AbsolutePath != null)
.ToList();
Info("Installing Archives");
archives.PMap(a => InstallArchive(a.Archive, a.AbsolutePath, grouped[a.Archive.Hash]));
}
private void InstallArchive(Archive archive, string absolutePath, IGrouping<string, FromArchive> grouping)
{
Status($"Extracting {archive.Name}");
var vFiles = grouping.Select(g =>
{
var file = VFS.FileForArchiveHashPath(g.ArchiveHashPath);
g.FromFile = file;
return g;
}).ToList();
var onFinish = VFS.Stage(vFiles.Select(f => f.FromFile).Distinct());
Status($"Copying files for {archive.Name}");
void CopyFile(string from, string to, bool useMove)
{
if(File.Exists(to))
File.Delete(to);
if (useMove)
File.Move(from, to);
else
File.Copy(from, to);
}
vFiles.GroupBy(f => f.FromFile)
.DoIndexed((idx, group) =>
{
Utils.Status("Installing files", idx * 100 / vFiles.Count);
var firstDest = Path.Combine(StagingFolder, group.First().To);
CopyFile(group.Key.StagedPath, firstDest, true);
foreach (var copy in group.Skip(1))
{
var nextDest = Path.Combine(StagingFolder, copy.To);
CopyFile(firstDest, nextDest, false);
}
});
Status("Unstaging files");
onFinish();
}
private void InstallIncludedFiles()
{
Info("Writing inline files");
ModList.Directives.OfType<InlineFile>()
.PMap(directive =>
{
Status($"Writing included file {directive.To}");
var outPath = Path.Combine(StagingFolder, directive.To);
if(File.Exists(outPath)) File.Delete(outPath);
File.WriteAllBytes(outPath, LoadBytesFromPath(directive.SourceDataID));
});
}
private void PrimeVFS()
{
HashedArchives.Do(a => VFS.AddKnown(new VirtualFile
{
Paths = new[] { a.Value },
Hash = a.Key
}));
VFS.RefreshIndexes();
ModList.Directives
.OfType<FromArchive>()
.Do(f =>
{
var updated_path = new string[f.ArchiveHashPath.Length];
f.ArchiveHashPath.CopyTo(updated_path, 0);
updated_path[0] = VFS.HashIndex[updated_path[0]].Where(e => e.IsConcrete).First().FullPath;
VFS.AddKnown(new VirtualFile { Paths = updated_path });
});
VFS.BackfillMissing();
}
private void DownloadArchives()
{
var missing = ModList.Archives.Where(a => !HashedArchives.ContainsKey(a.Hash)).ToList();
Info($"Missing {missing.Count} archives");
Info("Getting Nexus API Key, if a browser appears, please accept");
var dispatchers = missing.Select(m => m.State.GetDownloader()).Distinct();
foreach (var dispatcher in dispatchers)
dispatcher.Prepare();
DownloadMissingArchives(missing);
}
private void DownloadMissingArchives(List<Archive> missing, bool download = true)
{
if (download)
{
foreach (var a in missing.Where(a => a.State.GetType() == typeof(ManualDownloader.State)))
{
var output_path = Path.Combine(DownloadFolder, a.Name);
a.State.Download(a, output_path);
}
}
missing.Where(a => a.State.GetType() != typeof(ManualDownloader.State))
.PMap(archive =>
{
Info($"Downloading {archive.Name}");
var output_path = Path.Combine(DownloadFolder, archive.Name);
if (!download) return DownloadArchive(archive, download);
if (output_path.FileExists())
File.Delete(output_path);
return DownloadArchive(archive, download);
});
}
public bool DownloadArchive(Archive archive, bool download)
{
try
{
archive.State.Download(archive, Path.Combine(DownloadFolder, archive.Name));
}
catch (Exception ex)
{
Utils.Log($"Download error for file {archive.Name}");
Utils.Log(ex.ToString());
return false;
}
return false;
}
private void HashArchives()
{
HashedArchives = Directory.EnumerateFiles(DownloadFolder)
.Where(e => !e.EndsWith(".sha"))
.PMap(e => (HashArchive(e), e))
.OrderByDescending(e => File.GetLastWriteTime(e.Item2))
.GroupBy(e => e.Item1)
.Select(e => e.First())
.ToDictionary(e => e.Item1, e => e.Item2);
}
private string HashArchive(string e)
{
var cache = e + ".sha";
if (cache.FileExists() && new FileInfo(cache).LastWriteTime >= new FileInfo(e).LastWriteTime)
return File.ReadAllText(cache);
Status($"Hashing {Path.GetFileName(e)}");
File.WriteAllText(cache, e.FileHash());
return HashArchive(e);
}
}
}

View File

@ -77,6 +77,7 @@
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="ACompiler.cs" />
<Compile Include="CerasConfig.cs" />
<Compile Include="CompilationSteps\ACompilationStep.cs" />
<Compile Include="CompilationSteps\DeconstructBSAs.cs" />
@ -88,6 +89,7 @@
<Compile Include="CompilationSteps\IgnorePathContains.cs" />
<Compile Include="CompilationSteps\IgnoreRegex.cs" />
<Compile Include="CompilationSteps\IgnoreStartsWith.cs" />
<Compile Include="CompilationSteps\IgnoreVortex.cs" />
<Compile Include="CompilationSteps\IgnoreWabbajackInstallCruft.cs" />
<Compile Include="CompilationSteps\IncludeAll.cs" />
<Compile Include="CompilationSteps\IncludeAllConfigs.cs" />
@ -101,6 +103,7 @@
<Compile Include="CompilationSteps\IncludeStubbedConfigFiles.cs" />
<Compile Include="CompilationSteps\IncludeTaggedMods.cs" />
<Compile Include="CompilationSteps\IncludeThisProfile.cs" />
<Compile Include="CompilationSteps\IncludeVortexDeployment.cs" />
<Compile Include="CompilationSteps\IStackStep.cs" />
<Compile Include="CompilationSteps\PatchStockESMs.cs" />
<Compile Include="CompilationSteps\Serialization.cs" />
@ -131,6 +134,8 @@
<Compile Include="Validation\DTOs.cs" />
<Compile Include="Validation\ValidateModlist.cs" />
<Compile Include="ViewModel.cs" />
<Compile Include="VortexCompiler.cs" />
<Compile Include="VortexInstaller.cs" />
<Compile Include="WebAutomation\WebAutomation.cs" />
<Compile Include="WebAutomation\WebAutomationWindow.xaml.cs">
<DependentUpon>WebAutomationWindow.xaml</DependentUpon>

View File

@ -14,9 +14,12 @@ namespace Wabbajack.Lib
{
public class zEditIntegration
{
public static string FindzEditPath(Compiler compiler)
private static Compiler _mo2Compiler;
public static string FindzEditPath(ACompiler compiler)
{
var executables = compiler.MO2Ini.customExecutables;
_mo2Compiler = (Compiler) compiler;
var executables = _mo2Compiler.MO2Ini.customExecutables;
if (executables.size == null) return null;
foreach (var idx in Enumerable.Range(1, int.Parse(executables.size)))
@ -35,7 +38,7 @@ namespace Wabbajack.Lib
{
private Dictionary<string, zEditMerge> _mergesIndexed;
public IncludeZEditPatches(Compiler compiler) : base(compiler)
public IncludeZEditPatches(ACompiler compiler) : base(compiler)
{
var zEditPath = FindzEditPath(compiler);
var havezEdit = zEditPath != null;
@ -63,7 +66,7 @@ namespace Wabbajack.Lib
_mergesIndexed =
merges.ToDictionary(
m => Path.Combine(compiler.MO2Folder, "mods", m.Key.name, m.Key.filename),
m => Path.Combine(_mo2Compiler.MO2Folder, "mods", m.Key.name, m.Key.filename),
m => m.First());
}
@ -89,12 +92,12 @@ namespace Wabbajack.Lib
return new SourcePatch
{
RelativePath = abs_path.RelativeTo(_compiler.MO2Folder),
RelativePath = abs_path.RelativeTo(_mo2Compiler.MO2Folder),
Hash = _compiler.VFS[abs_path].Hash
};
}).ToList();
var src_data = result.Sources.Select(f => File.ReadAllBytes(Path.Combine(_compiler.MO2Folder, f.RelativePath)))
var src_data = result.Sources.Select(f => File.ReadAllBytes(Path.Combine(_mo2Compiler.MO2Folder, f.RelativePath)))
.ConcatArrays();
var dst_data = File.ReadAllBytes(source.AbsolutePath);
@ -117,7 +120,7 @@ namespace Wabbajack.Lib
[JsonObject("IncludeZEditPatches")]
public class State : IState
{
public ICompilationStep CreateStep(Compiler compiler)
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IncludeZEditPatches(compiler);
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Documents;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Wabbajack.Common;
using Wabbajack.Lib;
@ -14,22 +15,23 @@ namespace Wabbajack.Test.ListValidation
[TestClass]
public class ListValidation
{
[TestInitialize]
public void Setup()
[ClassInitialize]
public static void SetupNexus(TestContext context)
{
Directory.CreateDirectory(Consts.ModListDownloadFolder);
Utils.LogMessages.Subscribe(s => TestContext.WriteLine(s));
Utils.LogMessages.Subscribe(context.WriteLine);
var api = new NexusApiClient();
api.ClearUpdatedModsInCache();
}
private TestContext testContextInstance;
public TestContext TestContext
[TestInitialize]
public void SetupTest()
{
get { return testContextInstance; }
set { testContextInstance = value; }
Directory.CreateDirectory(Consts.ModListDownloadFolder);
Utils.LogMessages.Subscribe(s => TestContext.WriteLine(s));
}
public TestContext TestContext { get; set; }
[TestCategory("ListValidation")]
[DataTestMethod]
[DynamicData(nameof(GetModLists), DynamicDataSourceType.Method)]

View File

@ -76,10 +76,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="MSTest.TestAdapter">
<Version>1.3.2</Version>
<Version>2.0.0</Version>
</PackageReference>
<PackageReference Include="MSTest.TestFramework">
<Version>1.3.2</Version>
<Version>2.0.0</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>12.0.2</Version>

View File

@ -0,0 +1,71 @@
using System;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using VFS;
using Wabbajack.Common;
using Wabbajack.Lib;
namespace Wabbajack.Test
{
public abstract class AVortexCompilerTest
{
public TestContext TestContext { get; set; }
protected TestUtils utils { get; set; }
[TestInitialize]
public void TestInitialize()
{
Consts.TestMode = true;
utils = new TestUtils
{
GameName = "darkestdungeon"
};
Utils.LogMessages.Subscribe(f => TestContext.WriteLine(f));
}
[TestCleanup]
public void TestCleanup()
{
utils.Dispose();
}
protected VortexCompiler ConfigureAndRunCompiler()
{
var vortexCompiler = MakeCompiler();
vortexCompiler.VFS.Reset();
vortexCompiler.DownloadsFolder = utils.DownloadsFolder;
vortexCompiler.StagingFolder = utils.InstallFolder;
Directory.CreateDirectory(utils.InstallFolder);
Assert.IsTrue(vortexCompiler.Compile());
return vortexCompiler;
}
protected VortexCompiler MakeCompiler()
{
VirtualFileSystem.Reconfigure(utils.TestFolder);
var vortexCompiler = new VortexCompiler(utils.GameName, utils.GameFolder);
return vortexCompiler;
}
protected ModList CompileAndInstall()
{
var vortexCompiler = ConfigureAndRunCompiler();
Install(vortexCompiler);
return vortexCompiler.ModList;
}
protected void Install(VortexCompiler vortexCompiler)
{
var modList = Installer.LoadFromFile(vortexCompiler.ModListOutputFile);
var installer = new Installer(vortexCompiler.ModListOutputFile, modList, utils.InstallFolder)
{
DownloadFolder = utils.DownloadsFolder,
GameFolder = utils.GameFolder,
};
installer.Install();
}
}
}

View File

@ -0,0 +1,25 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Wabbajack.Lib.CompilationSteps;
namespace Wabbajack.Test
{
[TestClass]
public class VortexTests : AVortexCompilerTest
{
[TestMethod]
public void TestVortexStackSerialization()
{
utils.AddMod("test");
utils.Configure();
var vortexCompiler = ConfigureAndRunCompiler();
var stack = vortexCompiler.MakeStack();
var serialized = Serialization.Serialize(stack);
var rounded = Serialization.Serialize(Serialization.Deserialize(serialized, vortexCompiler));
Assert.AreEqual(serialized, rounded);
Assert.IsNotNull(vortexCompiler.GetStack());
}
}
}

View File

@ -84,6 +84,7 @@
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Transactions" />
<Reference Include="System.Windows" />
<Reference Include="System.Windows.Forms" />
@ -93,6 +94,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="ACompilerTest.cs" />
<Compile Include="AVortexCompilerTest.cs" />
<Compile Include="CSP\ChannelTests.cs" />
<Compile Include="CSP\CSPTests.cs" />
<Compile Include="DownloaderTests.cs" />
@ -104,6 +106,7 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ContentRightsManagementTests.cs" />
<Compile Include="CompilationStackTests.cs" />
<Compile Include="VortexTests.cs" />
<Compile Include="WebAutomationTests.cs" />
<Compile Include="zEditIntegrationTests.cs" />
</ItemGroup>
@ -137,10 +140,10 @@
<Version>2.2.6</Version>
</PackageReference>
<PackageReference Include="MSTest.TestAdapter">
<Version>1.3.2</Version>
<Version>2.0.0</Version>
</PackageReference>
<PackageReference Include="MSTest.TestFramework">
<Version>1.3.2</Version>
<Version>2.0.0</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>12.0.2</Version>

View File

@ -0,0 +1,19 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("Wabbajack.VirtualFileSystem.Test")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Wabbajack.VirtualFileSystem.Test")]
[assembly: AssemblyCopyright("Copyright © 2019")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("51ceb604-985a-45b9-af0d-c5ba8cfa1bf0")]
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -0,0 +1,209 @@
using System;
using System.Collections.Generic;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Wabbajack.Common;
namespace Wabbajack.VirtualFileSystem.Test
{
[TestClass]
public class VFSTests
{
private const string VFS_TEST_DIR = "vfs_test_dir";
private static readonly string VFS_TEST_DIR_FULL = Path.Combine(Directory.GetCurrentDirectory(), VFS_TEST_DIR);
private Context context;
public TestContext TestContext { get; set; }
[TestInitialize]
public void Setup()
{
Utils.LogMessages.Subscribe(f => TestContext.WriteLine(f));
if (Directory.Exists(VFS_TEST_DIR))
Directory.Delete(VFS_TEST_DIR, true);
Directory.CreateDirectory(VFS_TEST_DIR);
context = new Context();
}
[TestMethod]
public async Task FilesAreIndexed()
{
AddFile("test.txt", "This is a test");
await AddTestRoot();
var file = context.Index.ByFullPath[Path.Combine(VFS_TEST_DIR_FULL, "test.txt")];
Assert.IsNotNull(file);
Assert.AreEqual(file.Size, 14);
Assert.AreEqual(file.Hash, "qX0GZvIaTKM=");
}
private async Task AddTestRoot()
{
await context.AddRoot(VFS_TEST_DIR_FULL);
await context.WriteToFile(Path.Combine(VFS_TEST_DIR_FULL, "vfs_cache.bin"));
await context.IntegrateFromFile(Path.Combine(VFS_TEST_DIR_FULL, "vfs_cache.bin"));
}
[TestMethod]
public async Task ArchiveContentsAreIndexed()
{
AddFile("archive/test.txt", "This is a test");
ZipUpFolder("archive", "test.zip");
await AddTestRoot();
var abs_path = Path.Combine(VFS_TEST_DIR_FULL, "test.zip");
var file = context.Index.ByFullPath[abs_path];
Assert.IsNotNull(file);
Assert.AreEqual(128, file.Size);
Assert.AreEqual(abs_path.FileHash(), file.Hash);
Assert.IsTrue(file.IsArchive);
var inner_file = file.Children.First();
Assert.AreEqual(14, inner_file.Size);
Assert.AreEqual("qX0GZvIaTKM=", inner_file.Hash);
Assert.AreSame(file, file.Children.First().Parent);
}
[TestMethod]
public async Task DuplicateFileHashes()
{
AddFile("archive/test.txt", "This is a test");
ZipUpFolder("archive", "test.zip");
AddFile("test.txt", "This is a test");
await AddTestRoot();
var files = context.Index.ByHash["qX0GZvIaTKM="];
Assert.AreEqual(files.Count(), 2);
}
[TestMethod]
public async Task DeletedFilesAreRemoved()
{
AddFile("test.txt", "This is a test");
await AddTestRoot();
var file = context.Index.ByFullPath[Path.Combine(VFS_TEST_DIR_FULL, "test.txt")];
Assert.IsNotNull(file);
Assert.AreEqual(file.Size, 14);
Assert.AreEqual(file.Hash, "qX0GZvIaTKM=");
File.Delete(Path.Combine(VFS_TEST_DIR_FULL, "test.txt"));
await AddTestRoot();
CollectionAssert.DoesNotContain(context.Index.ByFullPath, Path.Combine(VFS_TEST_DIR_FULL, "test.txt"));
}
[TestMethod]
public async Task UnmodifiedFilesAreNotReIndexed()
{
AddFile("test.txt", "This is a test");
await AddTestRoot();
var old_file = context.Index.ByFullPath[Path.Combine(VFS_TEST_DIR_FULL, "test.txt")];
var old_time = old_file.LastAnalyzed;
await AddTestRoot();
var new_file = context.Index.ByFullPath[Path.Combine(VFS_TEST_DIR_FULL, "test.txt")];
Assert.AreEqual(old_time, new_file.LastAnalyzed);
}
[TestMethod]
public async Task CanStageSimpleArchives()
{
AddFile("archive/test.txt", "This is a test");
ZipUpFolder("archive", "test.zip");
await AddTestRoot();
var abs_path = Path.Combine(VFS_TEST_DIR_FULL, "test.zip");
var file = context.Index.ByFullPath[abs_path + "|test.txt"];
var cleanup = context.Stage(new List<VirtualFile> {file});
Assert.AreEqual("This is a test", File.ReadAllText(file.StagedPath));
cleanup();
}
[TestMethod]
public async Task CanStageNestedArchives()
{
AddFile("archive/test.txt", "This is a test");
ZipUpFolder("archive", "test.zip");
Directory.CreateDirectory(Path.Combine(VFS_TEST_DIR_FULL, @"archive\other\dir"));
File.Move(Path.Combine(VFS_TEST_DIR_FULL, "test.zip"),
Path.Combine(VFS_TEST_DIR_FULL, @"archive\other\dir\nested.zip"));
ZipUpFolder("archive", "test.zip");
await AddTestRoot();
var files = context.Index.ByHash["qX0GZvIaTKM="];
var cleanup = context.Stage(files);
foreach (var file in files)
Assert.AreEqual("This is a test", File.ReadAllText(file.StagedPath));
cleanup();
}
[TestMethod]
public async Task CanRequestPortableFileTrees()
{
AddFile("archive/test.txt", "This is a test");
ZipUpFolder("archive", "test.zip");
Directory.CreateDirectory(Path.Combine(VFS_TEST_DIR_FULL, @"archive\other\dir"));
File.Move(Path.Combine(VFS_TEST_DIR_FULL, "test.zip"),
Path.Combine(VFS_TEST_DIR_FULL, @"archive\other\dir\nested.zip"));
ZipUpFolder("archive", "test.zip");
await AddTestRoot();
var files = context.Index.ByHash["qX0GZvIaTKM="];
var archive = context.Index.ByRootPath[Path.Combine(VFS_TEST_DIR_FULL, "test.zip")];
var state = context.GetPortableState(files);
var new_context = new Context();
await new_context.IntegrateFromPortable(state,
new Dictionary<string, string> {{archive.Hash, archive.FullPath}});
var new_files = new_context.Index.ByHash["qX0GZvIaTKM="];
var close = new_context.Stage(new_files);
foreach (var file in new_files)
Assert.AreEqual("This is a test", File.ReadAllText(file.StagedPath));
close();
}
private static void AddFile(string filename, string thisIsATest)
{
var fullpath = Path.Combine(VFS_TEST_DIR, filename);
if (!Directory.Exists(Path.GetDirectoryName(fullpath)))
Directory.CreateDirectory(Path.GetDirectoryName(fullpath));
File.WriteAllText(fullpath, thisIsATest);
}
private static void ZipUpFolder(string folder, string output)
{
var path = Path.Combine(VFS_TEST_DIR, folder);
ZipFile.CreateFromDirectory(path, Path.Combine(VFS_TEST_DIR, output));
Directory.Delete(path, true);
}
}
}

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Wabbajack.VirtualFileSystem.Test</RootNamespace>
<AssemblyName>Wabbajack.VirtualFileSystem.Test</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">15.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
<IsCodedUITest>False</IsCodedUITest>
<TestProjectType>UnitTest</TestProjectType>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Transactions" />
<Reference Include="System.Windows" />
<Reference Include="System.Windows.Forms" />
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="VirtualFileSystemTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wabbajack.Common\Wabbajack.Common.csproj">
<Project>{B3F3FB6E-B9EB-4F49-9875-D78578BC7AE5}</Project>
<Name>Wabbajack.Common</Name>
</ProjectReference>
<ProjectReference Include="..\Wabbajack.VirtualFileSystem\Wabbajack.VirtualFileSystem.csproj">
<Project>{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}</Project>
<Name>Wabbajack.VirtualFileSystem</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="AlphaFS">
<Version>2.2.6</Version>
</PackageReference>
<PackageReference Include="MSTest.TestAdapter">
<Version>2.0.0</Version>
</PackageReference>
<PackageReference Include="MSTest.TestFramework">
<Version>2.0.0</Version>
</PackageReference>
<PackageReference Include="System.Reactive">
<Version>4.2.0</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -0,0 +1,267 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
using Wabbajack.Common;
using Wabbajack.Common.CSP;
using Directory = Alphaleonis.Win32.Filesystem.Directory;
using File = System.IO.File;
using FileInfo = Alphaleonis.Win32.Filesystem.FileInfo;
using Path = Alphaleonis.Win32.Filesystem.Path;
namespace Wabbajack.VirtualFileSystem
{
public class Context
{
public const ulong FileVersion = 0x02;
public const string Magic = "WABBAJACK VFS FILE";
private readonly string _stagingFolder = "vfs_staging";
public IndexRoot Index { get; private set; } = IndexRoot.Empty;
public TemporaryDirectory GetTemporaryFolder()
{
return new TemporaryDirectory(Path.Combine(_stagingFolder, Guid.NewGuid().ToString()));
}
public async Task<IndexRoot> AddRoot(string root)
{
if (!Path.IsPathRooted(root))
throw new InvalidDataException($"Path is not absolute: {root}");
var filtered = await Index.AllFiles
.ToChannel()
.UnorderedPipelineRx(o => o.Where(file => File.Exists(file.Name)))
.TakeAll();
var byPath = filtered.ToImmutableDictionary(f => f.Name);
var results = Channel.Create<VirtualFile>(1024);
var pipeline = Directory.EnumerateFiles(root, "*", DirectoryEnumerationOptions.Recursive)
.ToChannel()
.UnorderedPipeline(results, async f =>
{
if (byPath.TryGetValue(f, out var found))
{
var fi = new FileInfo(f);
if (found.LastModified == fi.LastWriteTimeUtc.Ticks && found.Size == fi.Length)
return found;
}
return await VirtualFile.Analyze(this, null, f, f);
});
var allFiles = await results.TakeAll();
// Should already be done but let's make the async tracker happy
await pipeline;
var newIndex = await IndexRoot.Empty.Integrate(filtered.Concat(allFiles).ToList());
lock (this)
{
Index = newIndex;
}
return newIndex;
}
public async Task WriteToFile(string filename)
{
using (var fs = File.OpenWrite(filename))
using (var bw = new BinaryWriter(fs, Encoding.UTF8, true))
{
fs.SetLength(0);
bw.Write(Encoding.ASCII.GetBytes(Magic));
bw.Write(FileVersion);
bw.Write((ulong) Index.AllFiles.Count);
var sizes = await Index.AllFiles
.ToChannel()
.UnorderedPipelineSync(f =>
{
var ms = new MemoryStream();
f.Write(ms);
return ms;
})
.Select(async ms =>
{
var size = ms.Position;
ms.Position = 0;
bw.Write((ulong) size);
await ms.CopyToAsync(fs);
return ms.Position;
})
.TakeAll();
Utils.Log($"Wrote {fs.Position.ToFileSizeString()} file as vfs cache file {filename}");
}
}
public async Task IntegrateFromFile(string filename)
{
using (var fs = File.OpenRead(filename))
using (var br = new BinaryReader(fs, Encoding.UTF8, true))
{
var magic = Encoding.ASCII.GetString(br.ReadBytes(Encoding.ASCII.GetBytes(Magic).Length));
var fileVersion = br.ReadUInt64();
if (fileVersion != FileVersion || magic != magic)
throw new InvalidDataException("Bad Data Format");
var numFiles = br.ReadUInt64();
var input = Channel.Create<byte[]>(1024);
var pipeline = input.UnorderedPipelineSync(
data => VirtualFile.Read(this, data))
.TakeAll();
Utils.Log($"Loading {numFiles} files from {filename}");
for (ulong idx = 0; idx < numFiles; idx++)
{
var size = br.ReadUInt64();
var bytes = new byte[size];
await br.BaseStream.ReadAsync(bytes, 0, (int) size);
await input.Put(bytes);
}
input.Close();
var files = await pipeline;
var newIndex = await Index.Integrate(files);
lock (this)
{
Index = newIndex;
}
}
}
public Action Stage(IEnumerable<VirtualFile> files)
{
var grouped = files.SelectMany(f => f.FilesInFullPath)
.Distinct()
.Where(f => f.Parent != null)
.GroupBy(f => f.Parent)
.OrderBy(f => f.Key?.NestingFactor ?? 0)
.ToList();
var paths = new List<string>();
foreach (var group in grouped)
{
var tmpPath = Path.Combine(_stagingFolder, Guid.NewGuid().ToString());
FileExtractor.ExtractAll(group.Key.StagedPath, tmpPath).Wait();
paths.Add(tmpPath);
foreach (var file in group)
file.StagedPath = Path.Combine(tmpPath, file.Name);
}
return () =>
{
paths.Do(p =>
{
if (Directory.Exists(p))
Directory.Delete(p, true, true);
});
};
}
public List<PortableFile> GetPortableState(IEnumerable<VirtualFile> files)
{
return files.SelectMany(f => f.FilesInFullPath)
.Distinct()
.Select(f => new PortableFile
{
Name = f.Parent != null ? f.Name : null,
Hash = f.Hash,
ParentHash = f.Parent?.Hash,
Size = f.Size
}).ToList();
}
public async Task IntegrateFromPortable(List<PortableFile> state, Dictionary<string, string> links)
{
var indexedState = state.GroupBy(f => f.ParentHash)
.ToDictionary(f => f.Key ?? "", f => (IEnumerable<PortableFile>) f);
var parents = await indexedState[""]
.ToChannel()
.UnorderedPipelineSync(f => VirtualFile.CreateFromPortable(this, indexedState, links, f))
.TakeAll();
var newIndex = await Index.Integrate(parents);
lock (this)
{
Index = newIndex;
}
}
}
public class IndexRoot
{
public static IndexRoot Empty = new IndexRoot();
public IndexRoot(ImmutableList<VirtualFile> aFiles,
ImmutableDictionary<string, VirtualFile> byFullPath,
ImmutableDictionary<string, ImmutableStack<VirtualFile>> byHash,
ImmutableDictionary<string, VirtualFile> byRoot)
{
AllFiles = aFiles;
ByFullPath = byFullPath;
ByHash = byHash;
ByRootPath = byRoot;
}
public IndexRoot()
{
AllFiles = ImmutableList<VirtualFile>.Empty;
ByFullPath = ImmutableDictionary<string, VirtualFile>.Empty;
ByHash = ImmutableDictionary<string, ImmutableStack<VirtualFile>>.Empty;
ByRootPath = ImmutableDictionary<string, VirtualFile>.Empty;
}
public ImmutableList<VirtualFile> AllFiles { get; }
public ImmutableDictionary<string, VirtualFile> ByFullPath { get; }
public ImmutableDictionary<string, ImmutableStack<VirtualFile>> ByHash { get; }
public ImmutableDictionary<string, VirtualFile> ByRootPath { get; }
public async Task<IndexRoot> Integrate(List<VirtualFile> files)
{
var allFiles = AllFiles.Concat(files).GroupBy(f => f.Name).Select(g => g.Last()).ToImmutableList();
var byFullPath = Task.Run(() =>
allFiles.SelectMany(f => f.ThisAndAllChildren)
.ToImmutableDictionary(f => f.FullPath));
var byHash = Task.Run(() =>
allFiles.SelectMany(f => f.ThisAndAllChildren)
.ToGroupedImmutableDictionary(f => f.Hash));
var byRootPath = Task.Run(() => allFiles.ToImmutableDictionary(f => f.Name));
return new IndexRoot(allFiles,
await byFullPath,
await byHash,
await byRootPath);
}
}
public class TemporaryDirectory : IDisposable
{
public TemporaryDirectory(string name)
{
FullName = name;
}
public string FullName { get; }
public void Dispose()
{
Directory.Delete(FullName, true, true);
}
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Wabbajack.VirtualFileSystem
{
public static class Extensions
{
public static ImmutableDictionary<TK, TI> ToImmutableDictionary<TI, TK>(this IEnumerable<TI> coll,
Func<TI, TK> keyFunc)
{
var builder = ImmutableDictionary<TK, TI>.Empty.ToBuilder();
foreach (var itm in coll)
builder.Add(keyFunc(itm), itm);
return builder.ToImmutable();
}
public static ImmutableDictionary<TK, ImmutableStack<TI>> ToGroupedImmutableDictionary<TI, TK>(
this IEnumerable<TI> coll, Func<TI, TK> keyFunc)
{
var builder = ImmutableDictionary<TK, ImmutableStack<TI>>.Empty.ToBuilder();
foreach (var itm in coll)
{
var key = keyFunc(itm);
if (builder.TryGetValue(key, out var prev))
builder[key] = prev.Push(itm);
else
builder[key] = ImmutableStack<TI>.Empty.Push(itm);
}
return builder.ToImmutable();
}
}
}

View File

@ -0,0 +1,10 @@
namespace Wabbajack.VirtualFileSystem
{
public class PortableFile
{
public string Name { get; set; }
public string Hash { get; set; }
public string ParentHash { get; set; }
public long Size { get; set; }
}
}

View File

@ -0,0 +1,35 @@
using System.Reflection;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Wabbajack.VirtualFileSystem")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Wabbajack.VirtualFileSystem")]
[assembly: AssemblyCopyright("Copyright © 2019")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("5d6a2eaf-6604-4c51-8ae2-a746b4bc5e3e")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -0,0 +1,251 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Common.CSP;
using Directory = Alphaleonis.Win32.Filesystem.Directory;
using FileInfo = Alphaleonis.Win32.Filesystem.FileInfo;
using Path = Alphaleonis.Win32.Filesystem.Path;
namespace Wabbajack.VirtualFileSystem
{
public class VirtualFile
{
private string _fullPath;
private string _stagedPath;
public string Name { get; internal set; }
public string FullPath
{
get
{
if (_fullPath != null) return _fullPath;
var cur = this;
var acc = new LinkedList<string>();
while (cur != null)
{
acc.AddFirst(cur.Name);
cur = cur.Parent;
}
_fullPath = string.Join("|", acc);
return _fullPath;
}
}
public string Hash { get; internal set; }
public long Size { get; internal set; }
public long LastModified { get; internal set; }
public long LastAnalyzed { get; internal set; }
public VirtualFile Parent { get; internal set; }
public Context Context { get; set; }
public string StagedPath
{
get
{
if (IsNative)
return Name;
if (_stagedPath == null)
throw new UnstagedFileException(FullPath);
return _stagedPath;
}
internal set
{
if (IsNative)
throw new CannotStageNativeFile("Cannot stage a native file");
_stagedPath = value;
}
}
/// <summary>
/// Returns the nesting factor for this file. Native files will have a nesting of 1, the factor
/// goes up for each nesting of a file in an archive.
/// </summary>
public int NestingFactor
{
get
{
var cnt = 0;
var cur = this;
while (cur != null)
{
cnt += 1;
cur = cur.Parent;
}
return cnt;
}
}
public ImmutableList<VirtualFile> Children { get; internal set; } = ImmutableList<VirtualFile>.Empty;
public bool IsArchive => Children != null && Children.Count > 0;
public bool IsNative => Parent == null;
public IEnumerable<VirtualFile> ThisAndAllChildren =>
Children.SelectMany(child => child.ThisAndAllChildren).Append(this);
/// <summary>
/// Returns all the virtual files in the path to this file, starting from the root file.
/// </summary>
public IEnumerable<VirtualFile> FilesInFullPath
{
get
{
var stack = ImmutableStack<VirtualFile>.Empty;
var cur = this;
while (cur != null)
{
stack = stack.Push(cur);
cur = cur.Parent;
}
return stack;
}
}
public static async Task<VirtualFile> Analyze(Context context, VirtualFile parent, string abs_path,
string rel_path)
{
var hasher = abs_path.FileHashAsync();
var fi = new FileInfo(abs_path);
var self = new VirtualFile
{
Context = context,
Name = rel_path,
Parent = parent,
Size = fi.Length,
LastModified = fi.LastWriteTimeUtc.Ticks,
LastAnalyzed = DateTime.Now.Ticks
};
if (FileExtractor.CanExtract(Path.GetExtension(abs_path)))
using (var tempFolder = context.GetTemporaryFolder())
{
await FileExtractor.ExtractAll(abs_path, tempFolder.FullName);
var results = Channel.Create<VirtualFile>(1024);
var files = Directory.EnumerateFiles(tempFolder.FullName, "*", SearchOption.AllDirectories)
.ToChannel()
.UnorderedPipeline(results,
async abs_src => await Analyze(context, self, abs_src, abs_src.RelativeTo(tempFolder.FullName)));
self.Children = (await results.TakeAll()).ToImmutableList();
}
self.Hash = await hasher;
return self;
}
public void Write(MemoryStream ms)
{
using (var bw = new BinaryWriter(ms, Encoding.UTF8, true))
{
Write(bw);
}
}
private void Write(BinaryWriter bw)
{
bw.Write(Name);
bw.Write(Hash);
bw.Write(Size);
bw.Write(LastModified);
bw.Write(LastAnalyzed);
bw.Write(Children.Count);
foreach (var child in Children)
child.Write(bw);
}
public static VirtualFile Read(Context context, byte[] data)
{
using (var ms = new MemoryStream(data))
using (var br = new BinaryReader(ms))
{
return Read(context, null, br);
}
}
private static VirtualFile Read(Context context, VirtualFile parent, BinaryReader br)
{
var vf = new VirtualFile
{
Context = context,
Parent = parent,
Name = br.ReadString(),
Hash = br.ReadString(),
Size = br.ReadInt64(),
LastModified = br.ReadInt64(),
LastAnalyzed = br.ReadInt64(),
Children = ImmutableList<VirtualFile>.Empty
};
var childrenCount = br.ReadInt32();
for (var idx = 0; idx < childrenCount; idx += 1) vf.Children = vf.Children.Add(Read(context, vf, br));
return vf;
}
public static VirtualFile CreateFromPortable(Context context,
Dictionary<string, IEnumerable<PortableFile>> state, Dictionary<string, string> links,
PortableFile portableFile)
{
var vf = new VirtualFile
{
Parent = null,
Context = context,
Name = links[portableFile.Hash],
Hash = portableFile.Hash,
Size = portableFile.Size
};
if (state.TryGetValue(portableFile.Hash, out var children))
vf.Children = children.Select(child => CreateFromPortable(context, vf, state, child)).ToImmutableList();
return vf;
}
public static VirtualFile CreateFromPortable(Context context, VirtualFile parent,
Dictionary<string, IEnumerable<PortableFile>> state, PortableFile portableFile)
{
var vf = new VirtualFile
{
Parent = parent,
Context = context,
Name = portableFile.Name,
Hash = portableFile.Hash,
Size = portableFile.Size
};
if (state.TryGetValue(portableFile.Hash, out var children))
vf.Children = children.Select(child => CreateFromPortable(context, vf, state, child)).ToImmutableList();
return vf;
}
}
public class CannotStageNativeFile : Exception
{
public CannotStageNativeFile(string cannotStageANativeFile) : base(cannotStageANativeFile)
{
}
}
public class UnstagedFileException : Exception
{
private readonly string _fullPath;
public UnstagedFileException(string fullPath) : base($"File {fullPath} is unstaged, cannot get staged name")
{
_fullPath = fullPath;
}
}
}

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Wabbajack.VirtualFileSystem</RootNamespace>
<AssemblyName>Wabbajack.VirtualFileSystem</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Numerics" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Context.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="PortableFile.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="VirtualFile.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wabbajack.Common.CSP\Wabbajack.Common.CSP.csproj">
<Project>{9e69bc98-1512-4977-b683-6e7e5292c0b8}</Project>
<Name>Wabbajack.Common.CSP</Name>
</ProjectReference>
<ProjectReference Include="..\Wabbajack.Common\Wabbajack.Common.csproj">
<Project>{b3f3fb6e-b9eb-4f49-9875-d78578bc7ae5}</Project>
<Name>Wabbajack.Common</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="AlphaFS">
<Version>2.2.6</Version>
</PackageReference>
<PackageReference Include="System.Collections.Immutable">
<Version>1.6.0</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -32,6 +32,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compression.BSA.Test", "Com
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.Common.CSP", "Wabbajack.Common.CSP\Wabbajack.Common.CSP.csproj", "{9E69BC98-1512-4977-B683-6E7E5292C0B8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.VirtualFileSystem", "Wabbajack.VirtualFileSystem\Wabbajack.VirtualFileSystem.csproj", "{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.VirtualFileSystem.Test", "Wabbajack.VirtualFileSystem.Test\Wabbajack.VirtualFileSystem.Test.csproj", "{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug (no commandargs)|Any CPU = Debug (no commandargs)|Any CPU
@ -225,6 +229,42 @@ Global
{9E69BC98-1512-4977-B683-6E7E5292C0B8}.Release|x64.Build.0 = Release|Any CPU
{9E69BC98-1512-4977-B683-6E7E5292C0B8}.Release|x86.ActiveCfg = Release|Any CPU
{9E69BC98-1512-4977-B683-6E7E5292C0B8}.Release|x86.Build.0 = Release|Any CPU
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Debug (no commandargs)|Any CPU.ActiveCfg = Debug|Any CPU
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Debug (no commandargs)|Any CPU.Build.0 = Debug|Any CPU
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Debug (no commandargs)|x64.ActiveCfg = Debug|Any CPU
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Debug (no commandargs)|x64.Build.0 = Debug|Any CPU
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Debug (no commandargs)|x86.ActiveCfg = Debug|Any CPU
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Debug (no commandargs)|x86.Build.0 = Debug|Any CPU
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Debug|x64.ActiveCfg = Debug|x64
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Debug|x64.Build.0 = Debug|x64
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Debug|x86.ActiveCfg = Debug|Any CPU
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Debug|x86.Build.0 = Debug|Any CPU
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Release|Any CPU.Build.0 = Release|Any CPU
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Release|x64.ActiveCfg = Release|Any CPU
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Release|x64.Build.0 = Release|Any CPU
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Release|x86.ActiveCfg = Release|Any CPU
{5D6A2EAF-6604-4C51-8AE2-A746B4BC5E3E}.Release|x86.Build.0 = Release|Any CPU
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Debug (no commandargs)|Any CPU.ActiveCfg = Debug|Any CPU
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Debug (no commandargs)|Any CPU.Build.0 = Debug|Any CPU
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Debug (no commandargs)|x64.ActiveCfg = Debug|Any CPU
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Debug (no commandargs)|x64.Build.0 = Debug|Any CPU
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Debug (no commandargs)|x86.ActiveCfg = Debug|Any CPU
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Debug (no commandargs)|x86.Build.0 = Debug|Any CPU
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Debug|x64.ActiveCfg = Debug|x64
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Debug|x64.Build.0 = Debug|x64
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Debug|x86.ActiveCfg = Debug|Any CPU
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Debug|x86.Build.0 = Debug|Any CPU
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Release|Any CPU.Build.0 = Release|Any CPU
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Release|x64.ActiveCfg = Release|Any CPU
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Release|x64.Build.0 = Release|Any CPU
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Release|x86.ActiveCfg = Release|Any CPU
{51CEB604-985A-45B9-AF0D-C5BA8CFA1BF0}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -53,6 +53,7 @@ namespace Wabbajack
public class InstallationSettings
{
public string InstallationLocation { get; set; }
public string StagingLocation { get; set; }
public string DownloadLocation { get; set; }
}

View File

@ -205,9 +205,30 @@ namespace Wabbajack
private async Task ExecuteBegin()
{
Compiler compiler;
try
if (false)
{
string[] args = Environment.GetCommandLineArgs();
var compiler = new VortexCompiler(args[1], args[2]);
await Task.Run(() =>
{
Compiling = true;
try
{
compiler.Compile();
}
catch (Exception ex)
{
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Log($"Can't continue: {ex.ExceptionToString()}");
}
finally
{
Compiling = false;
}
});
}else{
Compiler compiler;
try {
compiler = new Compiler(this.Mo2Folder)
{
MO2Profile = this.MOProfile,
@ -218,34 +239,35 @@ namespace Wabbajack
ModListWebsite = this.Website,
ModListReadme = this.ReadMeText.TargetPath,
};
}
catch (Exception ex)
{
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Log($"Compiler error: {ex.ExceptionToString()}");
return;
}
await Task.Run(() =>
{
Compiling = true;
try
{
compiler.Compile();
if (compiler.ModList?.ReportHTML != null)
{
this.HTMLReport = compiler.ModList.ReportHTML;
}
}
catch (Exception ex)
{
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Log($"Compiler error: {ex.ExceptionToString()}");
return;
}
finally
await Task.Run(() =>
{
Compiling = false;
}
});
Compiling = true;
try
{
compiler.Compile();
if (compiler.ModList?.ReportHTML != null)
{
this.HTMLReport = compiler.ModList.ReportHTML;
}
}
catch (Exception ex)
{
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Log($"Compiler error: {ex.ExceptionToString()}");
}
finally
{
Compiling = false;
}
});
}
}
}
}

View File

@ -1,4 +1,4 @@
using Syroot.Windows.IO;
using Syroot.Windows.IO;
using System;
using ReactiveUI;
using System.Diagnostics;
@ -50,10 +50,15 @@ namespace Wabbajack
[Reactive]
public bool InstallingMode { get; set; }
[Reactive]
public bool IsMO2ModList { get; set; }
public FilePickerVM Location { get; }
public FilePickerVM DownloadLocation { get; }
public FilePickerVM StagingLocation { get; }
private readonly ObservableAsPropertyHelper<float> _ProgressPercent;
public float ProgressPercent => _ProgressPercent.Value;
@ -114,15 +119,25 @@ namespace Wabbajack
this.DownloadLocation.AdditionalError = this.WhenAny(x => x.DownloadLocation.TargetPath)
.Select(x => Utils.IsDirectoryPathValid(x));
StagingLocation = new FilePickerVM
{
DoExistsCheck = true,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select your Vortex Staging Folder",
AdditionalError = this.WhenAny(x => x.StagingLocation.TargetPath)
.Select(Utils.IsDirectoryPathValid)
};
// Load settings
InstallationSettings settings = this.MWVM.Settings.InstallationSettings.TryCreate(source);
this.Location.TargetPath = settings.InstallationLocation;
this.DownloadLocation.TargetPath = settings.DownloadLocation;
var settings = MWVM.Settings.InstallationSettings.TryCreate(source);
this.MWVM.Settings.SaveSignal
.Subscribe(_ =>
{
settings.InstallationLocation = this.Location.TargetPath;
settings.DownloadLocation = this.DownloadLocation.TargetPath;
settings.DownloadLocation = DownloadLocation.TargetPath;
if (IsMO2ModList)
settings.InstallationLocation = Location.TargetPath;
else
settings.StagingLocation = StagingLocation.TargetPath;
})
.DisposeWith(this.CompositeDisposable);
@ -148,6 +163,38 @@ namespace Wabbajack
});
return default(ModListVM);
}
if (modList.ModManager == ModManager.Vortex)
{
IsMO2ModList = false;
StagingLocation.TargetPath = settings.StagingLocation;
var vortexFolder =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Vortex");
var stagingFolder = Path.Combine(vortexFolder, GameRegistry.Games[modList.GameType].NexusName,
"mods");
var downloadFolder = Path.Combine(vortexFolder, "downloads",
GameRegistry.Games[modList.GameType].NexusName);
MessageBox.Show(
"The ModList you are about to install was compiled from a Vortex installation. " +
"Vortex support is still very bleeding edge and installing this ModList WILL OVERRIDE your existing mods. " +
"If you encounter any errors during installation go to our discord and ping erri120#2285 with your error and a log file.",
"Important information regarding Vortex support", MessageBoxButton.OK, MessageBoxImage.Stop);
if (!Directory.Exists(vortexFolder)) return new ModListVM(modList, modListPath);
if (Directory.Exists(stagingFolder) &&
File.Exists(Path.Combine(stagingFolder, "__vortex_staging_folder")))
StagingLocation.TargetPath = stagingFolder;
if (Directory.Exists(Path.Combine(vortexFolder, "downloads")) &&
File.Exists(Path.Combine(vortexFolder, "downloads", "__vortex_downloads_folder")))
DownloadLocation.TargetPath = downloadFolder;
}
else
{
Location.TargetPath = settings.InstallationLocation;
DownloadLocation.TargetPath = settings.DownloadLocation;
IsMO2ModList = true;
}
return new ModListVM(modList, modListPath);
})
.ObserveOnGuiThread()
@ -219,10 +266,13 @@ namespace Wabbajack
this.WhenAny(x => x.Installing),
this.WhenAny(x => x.Location.InError),
this.WhenAny(x => x.DownloadLocation.InError),
resultSelector: (installing, loc, download) =>
this.WhenAny(x => x.StagingLocation.InError),
resultSelector: (installing, loc, download, staging) =>
{
if (installing) return false;
return !loc && !download;
if (IsMO2ModList)
return !loc && !download;
return !staging && !download;
})
.ObserveOnGuiThread());
this.VisitWebsiteCommand = ReactiveCommand.Create(
@ -289,35 +339,68 @@ namespace Wabbajack
private void ExecuteBegin()
{
this.Installing = true;
this.InstallingMode = true;
var installer = new Installer(this.ModListPath, this.ModList.SourceModList, Location.TargetPath)
Installing = true;
InstallingMode = true;
if (ModList.ModManager == ModManager.Vortex)
{
DownloadFolder = DownloadLocation.TargetPath
};
var th = new Thread(() =>
var installer = new VortexInstaller(ModListPath, ModList.SourceModList)
{
StagingFolder = StagingLocation.TargetPath,
DownloadFolder = DownloadLocation.TargetPath
};
var th = new Thread(() =>
{
try
{
installer.Install();
}
catch (Exception ex)
{
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Log(ex.StackTrace);
Utils.Log(ex.ToString());
Utils.Log($"{ex.Message} - Can't continue");
}
finally
{
Installing = false;
}
})
{
Priority = ThreadPriority.BelowNormal
};
th.Start();
}
else
{
try
var installer = new Installer(this.ModListPath, this.ModList.SourceModList, Location.TargetPath)
{
installer.Install();
}
catch (Exception ex)
{
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Log(ex.StackTrace);
Utils.Log(ex.ToString());
Utils.Log($"{ex.Message} - Can't continue");
}
finally
DownloadFolder = DownloadLocation.TargetPath
};
var th = new Thread(() =>
{
try
{
installer.Install();
}
catch (Exception ex)
{
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Log(ex.StackTrace);
Utils.Log(ex.ToString());
Utils.Log($"{ex.Message} - Can't continue");
}
finally
{
this.Installing = false;
}
})
{
Priority = ThreadPriority.BelowNormal
};
th.Start();
this.Installing = false;
}
})
{
Priority = ThreadPriority.BelowNormal
};
th.Start();
}
}
}
}

View File

@ -1,13 +1,8 @@
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
using Wabbajack.Common;
using Wabbajack.Lib;
@ -18,13 +13,14 @@ namespace Wabbajack
{
public ModList SourceModList { get; }
public string ModListPath { get; }
public string Name => this.SourceModList.Name;
public string ReportHTML => this.SourceModList.ReportHTML;
public string Readme => this.SourceModList.Readme;
public string ImageURL => this.SourceModList.Image;
public string Author => this.SourceModList.Author;
public string Description => this.SourceModList.Description;
public string Website => this.SourceModList.Website;
public string Name => SourceModList.Name;
public string ReportHTML => SourceModList.ReportHTML;
public string Readme => SourceModList.Readme;
public string ImageURL => SourceModList.Image;
public string Author => SourceModList.Author;
public string Description => SourceModList.Description;
public string Website => SourceModList.Website;
public ModManager ModManager => SourceModList.ModManager;
// Image isn't exposed as a direct property, but as an observable.
// This acts as a caching mechanism, as interested parties will trigger it to be created,
@ -33,10 +29,10 @@ namespace Wabbajack
public ModListVM(ModList sourceModList, string modListPath)
{
this.ModListPath = modListPath;
this.SourceModList = sourceModList;
ModListPath = modListPath;
SourceModList = sourceModList;
this.ImageObservable = Observable.Return(this.ImageURL)
ImageObservable = Observable.Return(this.ImageURL)
.ObserveOn(RxApp.TaskpoolScheduler)
.Select(url =>
{

View File

@ -277,43 +277,93 @@
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="40" />
<RowDefinition Height="40" />
<RowDefinition Height="80" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Installation Location"
TextAlignment="Center" />
<local:FilePicker
Grid.Row="1"
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding Location}"
FontSize="14" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Download Location"
TextAlignment="Center" />
<local:FilePicker
Grid.Row="2"
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding DownloadLocation}"
FontSize="14" />
<Grid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Visibility="{Binding IsMO2ModList, Converter={StaticResource bool2VisibilityConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Installation Location"
TextAlignment="Center" />
<local:FilePicker
Grid.Row="1"
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding Location}"
FontSize="14" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Download Location"
TextAlignment="Center" />
<local:FilePicker
Grid.Row="1"
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding DownloadLocation}"
FontSize="14" />
</Grid>
<Grid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Visibility="{Binding IsMO2ModList, Converter={StaticResource bool2VisibilityConverter}, ConverterParameter=False}">
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Staging Folder"
TextAlignment="Center" />
<local:FilePicker
Grid.Row="0"
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding StagingLocation}"
FontSize="14" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Download Folder"
TextAlignment="Center" />
<local:FilePicker
Grid.Row="1"
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding DownloadLocation}"
FontSize="14" />
</Grid>
<local:BeginButton
Grid.Row="1"
Grid.RowSpan="2"
Grid.Column="4"
Margin="0,0,25,0"
HorizontalAlignment="Right"

View File

@ -189,7 +189,6 @@
<DependentUpon>CompilerView.xaml</DependentUpon>
</Compile>
<Compile Include="Converters\IsNotNullVisibilityConverter.cs" />
<Compile Include="Enums\RunMode.cs" />
<Compile Include="Extensions\ReactiveUIExt.cs" />
<Compile Include="Views\DownloadWindow.xaml.cs">
<DependentUpon>DownloadWindow.xaml</DependentUpon>