Merge pull request #180 from erri120/vortex-stuff

Vortex stuff
This commit is contained in:
Timothy Baldridge 2019-11-17 15:23:08 -07:00 committed by GitHub
commit c9e15cf7db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 845 additions and 1045 deletions

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Win32;
@ -54,7 +55,7 @@ namespace Wabbajack.Common
public void LoadAllGames()
{
Games = new HashSet<GOGGame>();
if (this.GOGKey == null) return;
if (GOGKey == null) return;
string[] keys = GOGKey.GetSubKeyNames();
foreach (var key in keys)
{
@ -66,7 +67,9 @@ namespace Wabbajack.Common
};
game.Game = GameRegistry.Games.Values
.FirstOrDefault(g => g.GOGIDs.Contains(game.GameID))?.Game;
.FirstOrDefault(g => g.GOGIDs != null && g.GOGIDs.Contains(game.GameID)
&&
g.RequiredFiles.TrueForAll(s => File.Exists(Path.Combine(game.Path, s))))?.Game;
Games.Add(game);
}

View File

@ -9,7 +9,7 @@ namespace Wabbajack.Common
public enum Game
{
//MO2 GAMES
Morrowind,
//Morrowind,
Oblivion,
[Description("Fallout 3")]
Fallout3,
@ -55,6 +55,8 @@ namespace Wabbajack.Common
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; }
// file to check if the game is present, useful when steamIds and gogIds dont help
public List<string> RequiredFiles { get; internal set; }
public string GameLocation
{
@ -99,7 +101,11 @@ namespace Wabbajack.Common
MO2Name = "Oblivion",
MO2ArchiveName = "oblivion",
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Oblivion",
SteamIDs = new List<int> {22330}
SteamIDs = new List<int> {22330},
RequiredFiles = new List<string>
{
"oblivion.exe"
}
}
},
@ -112,7 +118,12 @@ namespace Wabbajack.Common
MO2Name = "fallout3",
MO2ArchiveName = "fallout3",
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Fallout3",
SteamIDs = new List<int> {22300, 22370} // base game and GotY
SteamIDs = new List<int> {22300, 22370}, // base game and GotY
RequiredFiles = new List<string>
{
"falloutlauncher.exe",
"data\\fallout3.esm"
}
}
},
{
@ -124,7 +135,11 @@ namespace Wabbajack.Common
MO2Name = "New Vegas",
MO2ArchiveName = "falloutnv",
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\falloutnv",
SteamIDs = new List<int> {22380}
SteamIDs = new List<int> {22380},
RequiredFiles = new List<string>
{
"FalloutNV.exe"
}
}
},
{
@ -136,7 +151,11 @@ namespace Wabbajack.Common
MO2Name = "Skyrim",
MO2ArchiveName = "skyrim",
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\skyrim",
SteamIDs = new List<int> {72850}
SteamIDs = new List<int> {72850},
RequiredFiles = new List<string>
{
"tesv.exe"
}
}
},
{
@ -148,7 +167,11 @@ namespace Wabbajack.Common
MO2Name = "Skyrim Special Edition",
MO2ArchiveName = "skyrimse",
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Skyrim Special Edition",
SteamIDs = new List<int> {489830}
SteamIDs = new List<int> {489830},
RequiredFiles = new List<string>
{
"SkyrimSE.exe"
}
}
},
{
@ -160,7 +183,11 @@ namespace Wabbajack.Common
MO2Name = "Fallout 4",
MO2ArchiveName = "fallout4",
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Fallout4",
SteamIDs = new List<int> {377160}
SteamIDs = new List<int> {377160},
RequiredFiles = new List<string>
{
"Fallout4.exe"
}
}
},
/*{
@ -183,7 +210,11 @@ namespace Wabbajack.Common
MO2Name = "Skyrim VR",
MO2ArchiveName = "skyrimse",
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Skyrim VR",
SteamIDs = new List<int> {611670}
SteamIDs = new List<int> {611670},
RequiredFiles = new List<string>
{
"SkyrimVR.exe"
}
}
},
{
@ -193,20 +224,10 @@ namespace Wabbajack.Common
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>
GOGIDs = new List<int>{1450711444},
RequiredFiles = new List<string>
{
"%documents%\\Larian Studios\\Divinity Original Sin 2\\Mods\\",
"_windows\\Darkest.exe"
}
}
},
@ -221,6 +242,28 @@ namespace Wabbajack.Common
AdditionalFolders = new List<string>
{
"%documents%\\Larian Studios\\Divinity Original Sin 2 Definitive Edition\\Mods\\"
},
RequiredFiles = new List<string>
{
"DefEd\\bin\\SupportTool.exe"
}
}
},
{
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\\",
},
RequiredFiles = new List<string>
{
"bin\\SupportTool.exe"
}
}
},
@ -231,7 +274,11 @@ namespace Wabbajack.Common
Game = Game.Starbound,
NexusName = "starbound",
SteamIDs = new List<int>{211820},
GOGIDs = new List<int>{1452598881}
GOGIDs = new List<int>{1452598881},
RequiredFiles = new List<string>
{
"win64\\starbound.exe"
}
}
},
{
@ -241,7 +288,11 @@ namespace Wabbajack.Common
Game = Game.SWKOTOR,
NexusName = "kotor",
SteamIDs = new List<int>{32370},
GOGIDs = new List<int>{1207666283}
GOGIDs = new List<int>{1207666283},
RequiredFiles = new List<string>
{
"swkotor.exe"
}
}
},
{
@ -251,7 +302,11 @@ namespace Wabbajack.Common
Game = Game.SWKOTOR2,
NexusName = "kotor2",
SteamIDs = new List<int>{208580},
GOGIDs = new List<int>{1421404581}
GOGIDs = new List<int>{1421404581},
RequiredFiles = new List<string>
{
"swkotor2.exe"
}
}
},
{
@ -261,7 +316,11 @@ namespace Wabbajack.Common
Game = Game.Witcher,
NexusName = "witcher",
SteamIDs = new List<int>{20900},
GOGIDs = new List<int>{1207658924}
GOGIDs = new List<int>{1207658924},
RequiredFiles = new List<string>
{
"system\\witcher.exe"
}
}
},
{
@ -271,7 +330,12 @@ namespace Wabbajack.Common
Game = Game.Witcher2,
NexusName = "witcher2",
SteamIDs = new List<int>{20920},
GOGIDs = new List<int>{1207658930}
GOGIDs = new List<int>{1207658930},
RequiredFiles = new List<string>
{
"bin\\witcher2.exe",
"bin\\userContentManager.exe"
}
}
},
{
@ -281,7 +345,11 @@ namespace Wabbajack.Common
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
GOGIDs = new List<int>{1207664643, 1495134320, 1207664663, 1640424747}, // normal, GotY and both in packages
RequiredFiles = new List<string>
{
"bin\\x64\\witcher2.exe"
}
}
}
};

View File

@ -105,7 +105,11 @@ namespace Wabbajack.Common
});
steamGame.Game = GameRegistry.Games.Values
.FirstOrDefault(g => g.SteamIDs.Contains(steamGame.AppId))?.Game;
.FirstOrDefault(g =>
g.SteamIDs.Contains(steamGame.AppId)
&&
g.RequiredFiles.TrueForAll(s => File.Exists(Path.Combine(steamGame.InstallDir, s)))
)?.Game;
games.Add(steamGame);
});
});

View File

@ -1,17 +1,27 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reactive.Subjects;
using System.Text;
using System.Threading.Tasks;
using CommonMark;
using Wabbajack.Common;
using Wabbajack.Lib.CompilationSteps;
using Wabbajack.Lib.Downloaders;
using Wabbajack.Lib.ModListRegistry;
using Wabbajack.VirtualFileSystem;
using Directory = Alphaleonis.Win32.Filesystem.Directory;
using File = Alphaleonis.Win32.Filesystem.File;
using Path = Alphaleonis.Win32.Filesystem.Path;
namespace Wabbajack.Lib
{
public abstract class ACompiler
{
public string ModListName, ModListAuthor, ModListDescription, ModListImage, ModListWebsite, ModListReadme;
public string WabbajackVersion;
public StatusUpdateTracker UpdateTracker { get; protected set; }
public WorkQueue Queue { get; protected set; }
@ -41,16 +51,176 @@ namespace Wabbajack.Lib
public List<IndexedArchive> IndexedArchives = new List<IndexedArchive>();
public Dictionary<string, IEnumerable<VirtualFile>> IndexedFiles = new Dictionary<string, IEnumerable<VirtualFile>>();
public abstract void Info(string msg);
public abstract void Status(string msg);
public abstract void Error(string msg);
public void Info(string msg)
{
Utils.Log(msg);
}
internal abstract string IncludeFile(byte[] data);
internal abstract string IncludeFile(string data);
public void Status(string msg)
{
Queue.Report(msg, 0);
}
public void Error(string msg)
{
Utils.Log(msg);
throw new Exception(msg);
}
internal string IncludeFile(byte[] data)
{
var id = Guid.NewGuid().ToString();
File.WriteAllBytes(Path.Combine(ModListOutputFolder, id), data);
return id;
}
internal string IncludeFile(string data)
{
var id = Guid.NewGuid().ToString();
File.WriteAllText(Path.Combine(ModListOutputFolder, id), data);
return id;
}
public void ExportModList()
{
Utils.Log($"Exporting ModList to : {ModListOutputFile}");
//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);
}
public void ShowReport()
{
//if (!ShowReportWhenFinished) return;
var file = Path.GetTempFileName() + ".html";
File.WriteAllText(file, ModList.ReportHTML);
Process.Start(file);
}
public 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);
}
}
ModList.ReportHTML = "<style>" + css + "</style>"
+ CommonMarkConverter.Convert(File.ReadAllText($"{ModList.Name}.md"));
}
public 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(Queue, sha => ResolveArchive(sha, archives));
}
public 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
{
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 != null && !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 abstract bool Compile();
public abstract Directive RunStack(IEnumerable<ICompilationStep> stack, RawSourceFile source);
public 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 abstract IEnumerable<ICompilationStep> GetStack();
public abstract IEnumerable<ICompilationStep> MakeStack();

300
Wabbajack.Lib/AInstaller.cs Normal file
View File

@ -0,0 +1,300 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using Wabbajack.Common;
using Wabbajack.Lib.Downloaders;
using Wabbajack.VirtualFileSystem;
using Context = Wabbajack.VirtualFileSystem.Context;
using Directory = Alphaleonis.Win32.Filesystem.Directory;
using Path = Alphaleonis.Win32.Filesystem.Path;
namespace Wabbajack.Lib
{
public abstract class AInstaller
{
public bool IgnoreMissingFiles { get; internal set; } = false;
public StatusUpdateTracker UpdateTracker { get; protected set; }
public WorkQueue Queue { get; protected set; }
public Context VFS { get; internal set; }
public string OutputFolder { get; set; }
public string DownloadFolder { get; set; }
public ModManager ModManager;
public string ModListArchive { get; internal set; }
public ModList ModList { get; internal set; }
public Dictionary<string, string> HashedArchives { get; set; }
protected AInstaller()
{
Queue = new WorkQueue();
}
public abstract void Install();
public void Info(string msg)
{
Utils.Log(msg);
}
public void Status(string msg)
{
Queue.Report(msg, 0);
}
public 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);
}
}
/// <summary>
/// We don't want to make the installer index all the archives, that's just a waste of time, so instead
/// we'll pass just enough information to VFS to let it know about the files we have.
/// </summary>
public void PrimeVFS()
{
VFS.AddKnown(HashedArchives.Select(a => new KnownFile
{
Paths = new[] { a.Value },
Hash = a.Key
}));
VFS.AddKnown(
ModList.Directives
.OfType<FromArchive>()
.Select(f => new KnownFile { Paths = f.ArchiveHashPath}));
VFS.BackfillMissing();
}
public void BuildFolderStructure()
{
Info("Building Folder Structure");
ModList.Directives
.Select(d => Path.Combine(OutputFolder, Path.GetDirectoryName(d.To)))
.ToHashSet()
.Do(f =>
{
if (Directory.Exists(f)) return;
Directory.CreateDirectory(f);
});
}
public 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(Queue,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}");
List<FromArchive> vFiles = grouping.Select(g =>
{
var file = VFS.Index.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))
{
var fi = new FileInfo(to);
if (fi.IsReadOnly)
fi.IsReadOnly = false;
File.Delete(to);
}
if (File.Exists(from))
{
var fi = new FileInfo(from);
if (fi.IsReadOnly)
fi.IsReadOnly = false;
}
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(OutputFolder, group.First().To);
CopyFile(group.Key.StagedPath, firstDest, true);
foreach (var copy in group.Skip(1))
{
var nextDest = Path.Combine(OutputFolder, copy.To);
CopyFile(firstDest, nextDest, false);
}
});
Status("Unstaging files");
onFinish();
// Now patch all the files from this archive
foreach (var toPatch in grouping.OfType<PatchedFromArchive>())
using (var patchStream = new MemoryStream())
{
Status($"Patching {Path.GetFileName(toPatch.To)}");
// Read in the patch data
byte[] patchData = LoadBytesFromPath(toPatch.PatchID);
var toFile = Path.Combine(OutputFolder, toPatch.To);
var oldData = new MemoryStream(File.ReadAllBytes(toFile));
// Remove the file we're about to patch
File.Delete(toFile);
// Patch it
using (var outStream = File.OpenWrite(toFile))
{
BSDiff.Apply(oldData, () => new MemoryStream(patchData), outStream);
}
Status($"Verifying Patch {Path.GetFileName(toPatch.To)}");
var resultSha = toFile.FileHash();
if (resultSha != toPatch.Hash)
throw new InvalidDataException($"Invalid Hash for {toPatch.To} after patching");
}
}
public 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 outputPath = Path.Combine(DownloadFolder, a.Name);
a.State.Download(a, outputPath);
}
}
missing.Where(a => a.State.GetType() != typeof(ManualDownloader.State))
.PMap(Queue, archive =>
{
Info($"Downloading {archive.Name}");
var outputPath = Path.Combine(DownloadFolder, archive.Name);
if (download)
if (outputPath.FileExists())
File.Delete(outputPath);
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;
}
public void HashArchives()
{
HashedArchives = Directory.EnumerateFiles(DownloadFolder)
.Where(e => !e.EndsWith(".sha"))
.PMap(Queue, 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);
}
public 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

@ -0,0 +1,41 @@
using Alphaleonis.Win32.Filesystem;
using Wabbajack.Common;
namespace Wabbajack.Lib.CompilationSteps
{
public class IgnoreDisabledVortexMods : ACompilationStep
{
private readonly VortexCompiler _vortexCompiler;
public IgnoreDisabledVortexMods(ACompiler compiler) : base(compiler)
{
_vortexCompiler = (VortexCompiler) compiler;
}
public override Directive Run(RawSourceFile source)
{
var b = false;
_vortexCompiler.ActiveArchives.Do(a =>
{
if (source.Path.Contains(a)) b = true;
});
if (b) return null;
var r = source.EvolveTo<IgnoredDirectly>();
r.Reason = "Disabled Archive";
return r;
}
public override IState GetState()
{
return new State();
}
public class State : IState
{
public ICompilationStep CreateStep(ACompiler compiler)
{
return new IgnoreDisabledVortexMods(compiler);
}
}
}
}

View File

@ -2,43 +2,42 @@
using System.Linq;
using Alphaleonis.Win32.Filesystem;
using Newtonsoft.Json;
using Wabbajack.Common;
namespace Wabbajack.Lib.CompilationSteps
{
public class IncludePropertyFiles : ACompilationStep
{
private readonly Compiler _mo2Compiler;
public IncludePropertyFiles(ACompiler compiler) : base(compiler)
{
_mo2Compiler = (Compiler) compiler;
}
public override Directive Run(RawSourceFile source)
{
var files = new HashSet<string>
{
_mo2Compiler.ModListImage, _mo2Compiler.ModListReadme
_compiler.ModListImage, _compiler.ModListReadme
};
if (!files.Any(f => source.AbsolutePath.Equals(f))) return null;
if (!File.Exists(source.AbsolutePath)) return null;
var isBanner = source.AbsolutePath == _mo2Compiler.ModListImage;
var isBanner = source.AbsolutePath == _compiler.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;
_mo2Compiler.ModListImage = result.SourceDataID;
_compiler.ModListImage = result.SourceDataID;
}
else
{
result.Type = PropertyType.Readme;
_mo2Compiler.ModListReadme = result.SourceDataID;
_compiler.ModListReadme = result.SourceDataID;
}
return result;
}
}
public override IState GetState()
{

View File

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Wabbajack.Common;
using System.IO;
namespace Wabbajack.Lib.CompilationSteps
{
@ -16,8 +10,9 @@ namespace Wabbajack.Lib.CompilationSteps
public override Directive Run(RawSourceFile source)
{
if (!source.Path.EndsWith("vortex.deployment.msgpack") &&
!source.Path.EndsWith("\\vortex.deployment.json")) return null;
!source.Path.EndsWith("\\vortex.deployment.json") && Path.GetExtension(source.Path) != ".meta") return null;
var inline = source.EvolveTo<InlineFile>();
inline.SourceDataID = _compiler.IncludeFile(File.ReadAllBytes(source.AbsolutePath));
return inline;

View File

@ -1,29 +1,17 @@
using CommonMark;
using Compression.BSA;
using Compression.BSA;
using System;
using System.CodeDom;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reactive.Subjects;
using System.Reflection;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Lib.CompilationSteps;
using Wabbajack.Lib.Downloaders;
using Wabbajack.Lib.ModListRegistry;
using Wabbajack.Lib.NexusApi;
using Wabbajack.Lib.Validation;
using Wabbajack.VirtualFileSystem;
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
@ -37,16 +25,12 @@ namespace Wabbajack.Lib
public string MO2Folder;
public string MO2Profile;
public string ModListName, ModListAuthor, ModListDescription, ModListWebsite, ModListImage, ModListReadme;
public string WabbajackVersion;
public Compiler(string mo2_folder)
{
UpdateTracker = new StatusUpdateTracker(10);
Queue = new WorkQueue();
VFS = new Context(Queue) {UpdateTracker = UpdateTracker};
VFS = new Context(Queue) {UpdateTracker = UpdateTracker};
ModManager = ModManager.MO2;
MO2Folder = mo2_folder;
@ -87,36 +71,6 @@ namespace Wabbajack.Lib
public HashSet<string> SelectedProfiles { get; set; } = new HashSet<string>();
public override void Info(string msg)
{
Utils.Log(msg);
}
public override void Status(string msg)
{
Queue.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()
{
UpdateTracker.Reset();
@ -298,7 +252,7 @@ namespace Wabbajack.Lib
ValidateModlist.RunValidation(ModList);
GenerateReport();
ExportModlist();
ExportModList();
ResetMembers();
@ -324,87 +278,6 @@ namespace Wabbajack.Lib
});
}
private void ExportModlist()
{
Utils.Log($"Exporting Modlist to : {ModListOutputFile}");
//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 ShowReport()
{
if (!ShowReportWhenFinished) return;
var file = Path.GetTempFileName() + ".html";
File.WriteAllText(file, ModList.ReportHTML);
Process.Start(file);
}
private void GenerateReport()
{
string css = "";
using (Stream cssStream = Utils.GetResourceStream("Wabbajack.Lib.css-min.css"))
{
using (StreamReader 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);
}
}
ModList.ReportHTML = "<style>" + css + "</style>"
+ CommonMarkConverter.Convert(File.ReadAllText($"{ModList.Name}.md"));
}
/// <summary>
/// Clear references to lists that hold a lot of data.
/// </summary>
@ -488,65 +361,6 @@ namespace Wabbajack.Lib
return null;
}
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(Queue, 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 user_config = Path.Combine(MO2ProfileDir, "compilation_stack.yml");

View File

@ -1,10 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using Wabbajack.Common;
using Wabbajack.Lib.Downloaders;
@ -18,90 +15,22 @@ using Path = Alphaleonis.Win32.Filesystem.Path;
namespace Wabbajack.Lib
{
public class Installer
public class Installer : AInstaller
{
private string _downloadsFolder;
private WorkQueue Queue { get; set; }
public Installer(string archive, ModList mod_list, string output_folder)
{
Queue = new WorkQueue();
VFS = new Context(Queue);
UpdateTracker = new StatusUpdateTracker(10);
VFS = new Context(Queue) {UpdateTracker = UpdateTracker};
ModManager = ModManager.MO2;
ModListArchive = archive;
Outputfolder = output_folder;
OutputFolder = output_folder;
DownloadFolder = Path.Combine(OutputFolder, "downloads");
ModList = mod_list;
}
public Context VFS { get; }
public string Outputfolder { get; }
public string DownloadFolder
{
get => _downloadsFolder ?? Path.Combine(Outputfolder, "downloads");
set => _downloadsFolder = value;
}
public string ModListArchive { get; }
public ModList ModList { get; }
public Dictionary<string, string> HashedArchives { get; private set; }
public bool IgnoreMissingFiles { get; internal set; }
public string GameFolder { get; set; }
public void Info(string msg)
{
Utils.Log(msg);
}
public void Status(string msg)
{
Queue.Report(msg, 0);
}
public void Status(string msg, int progress)
{
Queue.Report(msg, progress);
}
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()
public override void Install()
{
var game = GameRegistry.Games[ModList.GameType];
@ -121,10 +50,10 @@ namespace Wabbajack.Lib
ValidateGameESMs();
ValidateModlist.RunValidation(ModList);
Directory.CreateDirectory(Outputfolder);
Directory.CreateDirectory(OutputFolder);
Directory.CreateDirectory(DownloadFolder);
if (Directory.Exists(Path.Combine(Outputfolder, "mods")))
if (Directory.Exists(Path.Combine(OutputFolder, "mods")))
{
if (MessageBox.Show(
"There already appears to be a Mod Organizer 2 install in this folder, are you sure you wish to continue" +
@ -159,7 +88,7 @@ namespace Wabbajack.Lib
BuildFolderStructure();
InstallArchives();
InstallIncludedFiles();
InctallIncludedDownloadMetas();
InstallIncludedDownloadMetas();
BuildBSAs();
zEditIntegration.GenerateMerges(this);
@ -170,7 +99,7 @@ namespace Wabbajack.Lib
//AskToEndorse();
}
private void InctallIncludedDownloadMetas()
private void InstallIncludedDownloadMetas()
{
ModList.Directives
.OfType<ArchiveMeta>()
@ -233,27 +162,6 @@ namespace Wabbajack.Lib
Info("Done! You may now exit the application!");
}
/// <summary>
/// We don't want to make the installer index all the archives, that's just a waste of time, so instead
/// we'll pass just enough information to VFS to let it know about the files we have.
/// </summary>
private void PrimeVFS()
{
VFS.AddKnown(HashedArchives.Select(a => new KnownFile
{
Paths = new[] { a.Value },
Hash = a.Key
}));
VFS.AddKnown(
ModList.Directives
.OfType<FromArchive>()
.Select(f => new KnownFile { Paths = f.ArchiveHashPath}));
VFS.BackfillMissing();
}
private void BuildBSAs()
{
var bsas = ModList.Directives.OfType<CreateBSA>().ToList();
@ -262,7 +170,7 @@ namespace Wabbajack.Lib
bsas.Do(bsa =>
{
Status($"Building {bsa.To}");
var source_dir = Path.Combine(Outputfolder, Consts.BSACreationDir, bsa.TempID);
var source_dir = Path.Combine(OutputFolder, Consts.BSACreationDir, bsa.TempID);
using (var a = bsa.State.MakeBuilder())
{
@ -276,12 +184,12 @@ namespace Wabbajack.Lib
});
Info($"Writing {bsa.To}");
a.Build(Path.Combine(Outputfolder, bsa.To));
a.Build(Path.Combine(OutputFolder, bsa.To));
}
});
var bsa_dir = Path.Combine(Outputfolder, Consts.BSACreationDir);
var bsa_dir = Path.Combine(OutputFolder, Consts.BSACreationDir);
if (Directory.Exists(bsa_dir))
{
Info($"Removing temp folder {Consts.BSACreationDir}");
@ -297,7 +205,7 @@ namespace Wabbajack.Lib
.PMap(Queue, directive =>
{
Status($"Writing included file {directive.To}");
var out_path = Path.Combine(Outputfolder, directive.To);
var out_path = Path.Combine(OutputFolder, directive.To);
if (File.Exists(out_path)) File.Delete(out_path);
if (directive is RemappedInlineFile)
WriteRemappedFile((RemappedInlineFile)directive);
@ -321,7 +229,7 @@ namespace Wabbajack.Lib
$"Cannot patch {filename} from the game folder hashes don't match have you already cleaned the file?");
var patch_data = LoadBytesFromPath(directive.SourceDataID);
var to_file = Path.Combine(Outputfolder, directive.To);
var to_file = Path.Combine(OutputFolder, directive.To);
Status($"Patching {filename}");
using (var output = File.OpenWrite(to_file))
using (var input = File.OpenRead(game_file))
@ -338,209 +246,15 @@ namespace Wabbajack.Lib
data = data.Replace(Consts.GAME_PATH_MAGIC_DOUBLE_BACK, GameFolder.Replace("\\", "\\\\"));
data = data.Replace(Consts.GAME_PATH_MAGIC_FORWARD, GameFolder.Replace("\\", "/"));
data = data.Replace(Consts.MO2_PATH_MAGIC_BACK, Outputfolder);
data = data.Replace(Consts.MO2_PATH_MAGIC_DOUBLE_BACK, Outputfolder.Replace("\\", "\\\\"));
data = data.Replace(Consts.MO2_PATH_MAGIC_FORWARD, Outputfolder.Replace("\\", "/"));
data = data.Replace(Consts.MO2_PATH_MAGIC_BACK, OutputFolder);
data = data.Replace(Consts.MO2_PATH_MAGIC_DOUBLE_BACK, OutputFolder.Replace("\\", "\\\\"));
data = data.Replace(Consts.MO2_PATH_MAGIC_FORWARD, OutputFolder.Replace("\\", "/"));
data = data.Replace(Consts.DOWNLOAD_PATH_MAGIC_BACK, DownloadFolder);
data = data.Replace(Consts.DOWNLOAD_PATH_MAGIC_DOUBLE_BACK, DownloadFolder.Replace("\\", "\\\\"));
data = data.Replace(Consts.DOWNLOAD_PATH_MAGIC_FORWARD, DownloadFolder.Replace("\\", "/"));
File.WriteAllText(Path.Combine(Outputfolder, directive.To), data);
}
private void BuildFolderStructure()
{
Info("Building Folder Structure");
ModList.Directives
.Select(d => Path.Combine(Outputfolder, 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(Queue, 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.Index.FileForArchiveHashPath(g.ArchiveHashPath);
g.FromFile = file;
return g;
}).ToList();
var on_finish = VFS.Stage(vfiles.Select(f => f.FromFile).Distinct());
Status($"Copying files for {archive.Name}");
void CopyFile(string from, string to, bool use_move)
{
if (File.Exists(to))
{
var fi = new FileInfo(to);
if (fi.IsReadOnly)
fi.IsReadOnly = false;
File.Delete(to);
}
if (File.Exists(from))
{
var fi = new FileInfo(from);
if (fi.IsReadOnly)
fi.IsReadOnly = false;
}
if (use_move)
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 first_dest = Path.Combine(Outputfolder, group.First().To);
CopyFile(group.Key.StagedPath, first_dest, true);
foreach (var copy in group.Skip(1))
{
var next_dest = Path.Combine(Outputfolder, copy.To);
CopyFile(first_dest, next_dest, false);
}
});
Status("Unstaging files");
on_finish();
// Now patch all the files from this archive
foreach (var to_patch in grouping.OfType<PatchedFromArchive>())
using (var patch_stream = new MemoryStream())
{
Status($"Patching {Path.GetFileName(to_patch.To)}");
// Read in the patch data
var patch_data = LoadBytesFromPath(to_patch.PatchID);
var to_file = Path.Combine(Outputfolder, to_patch.To);
var old_data = new MemoryStream(File.ReadAllBytes(to_file));
// Remove the file we're about to patch
File.Delete(to_file);
// Patch it
using (var out_stream = File.OpenWrite(to_file))
{
BSDiff.Apply(old_data, () => new MemoryStream(patch_data), out_stream);
}
Status($"Verifying Patch {Path.GetFileName(to_patch.To)}");
var result_sha = to_file.FileHash();
if (result_sha != to_patch.Hash)
throw new InvalidDataException($"Invalid Hash for {to_patch.To} after patching");
}
}
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(Queue, archive =>
{
Info($"Downloading {archive.Name}");
var output_path = Path.Combine(DownloadFolder, archive.Name);
if (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(Queue, 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);
File.WriteAllText(Path.Combine(OutputFolder, directive.To), data);
}
}
}

View File

@ -44,18 +44,26 @@ namespace Wabbajack.Lib
wtr.WriteLine(txt);
}
public void Build(Compiler c, ModList lst)
public void Build(ACompiler c, ModList lst)
{
Compiler compiler = null;
if (lst.ModManager == ModManager.MO2)
compiler = (Compiler) c;
Text($"### {lst.Name} by {lst.Author} - Installation Summary");
Text($"Build with Wabbajack Version {lst.WabbajackVersion}");
Text(lst.Description);
Text($"#### Website:");
Text("#### Website:");
NoWrapText($"[{lst.Website}]({lst.Website})");
Text($"Mod Manager: {lst.ModManager.ToString()}");
var readme_file = Path.Combine(c.MO2ProfileDir, "readme.md");
if (File.Exists(readme_file))
File.ReadAllLines(readme_file)
.Do(NoWrapText);
if (lst.ModManager == ModManager.MO2)
{
var readme_file = Path.Combine(compiler?.MO2ProfileDir, "readme.md");
if (File.Exists(readme_file))
File.ReadAllLines(readme_file)
.Do(NoWrapText);
}
Text(
$"#### Download Summary ({lst.Archives.Count} archives - {lst.Archives.Sum(a => a.Size).ToFileSizeString()})");

View File

@ -1,15 +1,13 @@
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 Newtonsoft.Json;
using Wabbajack.Common;
using Wabbajack.Lib.CompilationSteps;
using Wabbajack.Lib.Downloaders;
using Wabbajack.Lib.ModListRegistry;
using Wabbajack.Lib.NexusApi;
using Wabbajack.VirtualFileSystem;
using File = Alphaleonis.Win32.Filesystem.File;
@ -18,6 +16,14 @@ namespace Wabbajack.Lib
{
public class VortexCompiler : ACompiler
{
/* vortex creates a vortex.deployment.json file that contains information
about all deployed files, parsing that file, we can get a list of all 'active'
archives so we don't force the user to install all archives found in the downloads folder.
Similar to how IgnoreDisabledMods for MO2 works
*/
public VortexDeployment VortexDeployment;
public List<string> ActiveArchives;
public Game Game { get; }
public string GameName { get; }
@ -32,58 +38,32 @@ namespace Wabbajack.Lib
public VortexCompiler(Game game, string gamePath, string vortexFolder, string downloadsFolder, string stagingFolder)
{
ModManager = ModManager.Vortex;
UpdateTracker = new StatusUpdateTracker(10);
VFS = new Context(Queue) {UpdateTracker = UpdateTracker};
// TODO: only for testing
IgnoreMissingFiles = true;
ModManager = ModManager.Vortex;
Game = game;
GamePath = gamePath;
GameName = GameRegistry.Games[game].NexusName;
this.VortexFolder = vortexFolder;
this.DownloadsFolder = downloadsFolder;
this.StagingFolder = stagingFolder;
Queue = new WorkQueue();
VFS = new Context(Queue);
VortexFolder = vortexFolder;
DownloadsFolder = downloadsFolder;
StagingFolder = stagingFolder;
ModListOutputFolder = "output_folder";
// TODO: add custom modlist name
ModListOutputFile = $"VORTEX_TEST_MODLIST{ExtensionManager.Extension}";
}
public override void Info(string msg)
{
Utils.Log(msg);
}
public override void Status(string msg)
{
Queue.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;
ActiveArchives = new List<string>();
}
public override bool Compile()
{
if (string.IsNullOrEmpty(ModListName))
ModListName = $"Vortex ModList for {Game.ToString()}";
ModListOutputFile = $"{ModListName}{ExtensionManager.Extension}";
Info($"Starting Vortex compilation for {GameName} at {GamePath} with staging folder at {StagingFolder} and downloads folder at {DownloadsFolder}.");
ParseDeploymentFile();
Info("Starting pre-compilation steps");
CreateMetaFiles();
@ -195,18 +175,67 @@ namespace Wabbajack.Lib
ModList = new ModList
{
Name = ModListName ?? "",
Author = ModListAuthor ?? "",
Description = ModListDescription ?? "",
Readme = ModListReadme ?? "",
Image = ModListImage ?? "",
Website = ModListWebsite ?? "",
Archives = SelectedArchives,
ModManager = ModManager.Vortex,
Directives = InstallDirectives,
GameType = Game
};
GenerateReport();
ExportModList();
Info("Done Building ModList");
ShowReport();
return true;
}
private void ParseDeploymentFile()
{
Info("Searching for vortex.deployment.json...");
var deploymentFile = "";
Directory.EnumerateFiles(GamePath, "vortex.deployment.json", SearchOption.AllDirectories)
.Where(File.Exists)
.Do(f => deploymentFile = f);
var currentGame = GameRegistry.Games[Game];
if (currentGame.AdditionalFolders != null && currentGame.AdditionalFolders.Count != 0)
currentGame.AdditionalFolders.Do(f => Directory.EnumerateFiles(f, "vortex.deployment.json", SearchOption.AllDirectories)
.Where(File.Exists)
.Do(d => deploymentFile = d));
if (string.IsNullOrEmpty(deploymentFile))
{
Info("vortex.deployment.json not found!");
return;
}
Info("vortex.deployment.json found at "+deploymentFile);
Info("Parsing vortex.deployment.json...");
try
{
VortexDeployment = deploymentFile.FromJSON<VortexDeployment>();
}
catch (JsonSerializationException e)
{
Info("Failed to parse vortex.deployment.json!");
Utils.LogToFile(e.Message);
Utils.LogToFile(e.StackTrace);
}
VortexDeployment.files.Do(f =>
{
var archive = f.source;
if(!ActiveArchives.Contains(archive)) ActiveArchives.Add(archive);
});
}
/// <summary>
/// Some have mods outside their game folder located
/// </summary>
@ -223,77 +252,13 @@ namespace Wabbajack.Lib
});
}
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"))
.Where(f => File.Exists(f) && Path.GetExtension(f) != ".meta" && !File.Exists(f+".meta") && ActiveArchives.Contains(Path.GetFileNameWithoutExtension(f)))
.Do(f =>
{
Utils.Log($"Trying to create meta file for {Path.GetFileName(f)}");
@ -330,65 +295,6 @@ namespace Wabbajack.Lib
});
}
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(Queue, 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;
@ -409,10 +315,11 @@ namespace Wabbajack.Lib
Utils.Log("Generating compilation stack");
return new List<ICompilationStep>
{
//new IncludePropertyFiles(this),
new IncludePropertyFiles(this),
new IgnoreDisabledVortexMods(this),
new IncludeVortexDeployment(this),
new IncludeRegex(this, "^*\\.meta"),
new IgnoreVortex(this),
new IgnoreRegex(this, "^*__vortex_staging_folder$"),
Game == Game.DarkestDungeon ? new IncludeRegex(this, "project\\.xml$") : null,
@ -473,4 +380,19 @@ namespace Wabbajack.Lib
return IsValidBaseStagingFolder(Path.GetDirectoryName(path));
}
}
public class VortexDeployment
{
public string instance;
public int version;
public string deploymentMethod;
public List<VortexFile> files;
}
public class VortexFile
{
public string relPath;
public string source;
public string target;
}
}

View File

@ -1,39 +1,21 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Linq;
using Wabbajack.Common;
using Wabbajack.Lib.Downloaders;
using Wabbajack.VirtualFileSystem;
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 class VortexInstaller : AInstaller
{
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 WorkQueue Queue { get; }
public Context VFS { get; }
public bool IgnoreMissingFiles { get; internal set; }
public VortexInstaller(string archive, ModList modList)
{
Queue = new WorkQueue();
VFS = new Context(Queue);
UpdateTracker = new StatusUpdateTracker(10);
VFS = new Context(Queue) {UpdateTracker = UpdateTracker};
ModManager = ModManager.Vortex;
ModListArchive = archive;
ModList = modList;
@ -43,53 +25,7 @@ namespace Wabbajack.Lib
GameInfo = GameRegistry.Games[ModList.GameType];
}
public void Info(string msg)
{
Utils.Log(msg);
}
public void Status(string msg)
{
Queue.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()
public override void Install()
{
Directory.CreateDirectory(DownloadFolder);
@ -113,85 +49,11 @@ namespace Wabbajack.Lib
BuildFolderStructure();
InstallArchives();
InstallIncludedFiles();
//InctallIncludedDownloadMetas();
//InstallIncludedDownloadMetas();
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(Queue,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.Index.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");
@ -199,114 +61,10 @@ namespace Wabbajack.Lib
.PMap(Queue,directive =>
{
Status($"Writing included file {directive.To}");
var outPath = Path.Combine(StagingFolder, directive.To);
var outPath = Path.Combine(OutputFolder, directive.To);
if(File.Exists(outPath)) File.Delete(outPath);
File.WriteAllBytes(outPath, LoadBytesFromPath(directive.SourceDataID));
});
}
/// <summary>
/// We don't want to make the installer index all the archives, that's just a waste of time, so instead
/// we'll pass just enough information to VFS to let it know about the files we have.
/// </summary>
private void PrimeVFS()
{
VFS.AddKnown(HashedArchives.Select(a => new KnownFile
{
Paths = new[] { a.Value },
Hash = a.Key
}));
ModList.Directives
.OfType<FromArchive>()
.Select(f =>
{
var updated_path = new string[f.ArchiveHashPath.Length];
f.ArchiveHashPath.CopyTo(updated_path, 0);
updated_path[0] = VFS.Index.ByHash[updated_path[0]].First(e => e.IsNative).FullPath;
return new KnownFile { 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(Queue, 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(Queue,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

@ -79,12 +79,14 @@
</ItemGroup>
<ItemGroup>
<Compile Include="ACompiler.cs" />
<Compile Include="AInstaller.cs" />
<Compile Include="CerasConfig.cs" />
<Compile Include="CompilationSteps\ACompilationStep.cs" />
<Compile Include="CompilationSteps\DeconstructBSAs.cs" />
<Compile Include="CompilationSteps\DirectMatch.cs" />
<Compile Include="CompilationSteps\DropAll.cs" />
<Compile Include="CompilationSteps\IgnoreDisabledMods.cs" />
<Compile Include="CompilationSteps\IgnoreDisabledVortexMods.cs" />
<Compile Include="CompilationSteps\IgnoreEndsWith.cs" />
<Compile Include="CompilationSteps\IgnoreGameFiles.cs" />
<Compile Include="CompilationSteps\IgnorePathContains.cs" />

View File

@ -169,12 +169,12 @@ namespace Wabbajack.Lib
{
Utils.LogStatus($"Generating zEdit merge: {m.To}");
var src_data = m.Sources.Select(s => File.ReadAllBytes(Path.Combine(installer.Outputfolder, s.RelativePath)))
var src_data = m.Sources.Select(s => File.ReadAllBytes(Path.Combine(installer.OutputFolder, s.RelativePath)))
.ConcatArrays();
var patch_data = installer.LoadBytesFromPath(m.PatchID);
using (var fs = File.OpenWrite(Path.Combine(installer.Outputfolder, m.To)))
using (var fs = File.OpenWrite(Path.Combine(installer.OutputFolder, m.To)))
BSDiff.Apply(new MemoryStream(src_data), () => new MemoryStream(patch_data), fs);
});
}

View File

@ -32,7 +32,8 @@ namespace Wabbajack
public static MainSettings LoadSettings()
{
if (!File.Exists(Filename)) return new MainSettings();
string[] args = Environment.GetCommandLineArgs();
if (!File.Exists(Filename) || args[1] == "nosettings") return new MainSettings();
return JsonConvert.DeserializeObject<MainSettings>(File.ReadAllText(Filename));
}

View File

@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using DynamicData.Binding;
@ -16,25 +14,26 @@ namespace Wabbajack
{
public class VortexCompilerVM : ViewModel, ISubCompilerVM
{
private readonly VortexCompilationSettings settings;
private readonly VortexCompilationSettings _settings;
public IReactiveCommand BeginCommand { get; }
private readonly ObservableAsPropertyHelper<bool> _Compiling;
public bool Compiling => _Compiling.Value;
private readonly ObservableAsPropertyHelper<bool> _compiling;
public bool Compiling => _compiling.Value;
private readonly ObservableAsPropertyHelper<ModlistSettingsEditorVM> _ModlistSettings;
public ModlistSettingsEditorVM ModlistSettings => _ModlistSettings.Value;
private readonly ObservableAsPropertyHelper<ModlistSettingsEditorVM> _modListSettings;
public ModlistSettingsEditorVM ModlistSettings => _modListSettings.Value;
private static ObservableCollectionExtended<GameVM> gameOptions = new ObservableCollectionExtended<GameVM>(
private static readonly ObservableCollectionExtended<GameVM> _gameOptions = new ObservableCollectionExtended<GameVM>(
EnumExt.GetValues<Game>()
.Select(g => new GameVM(g))
.OrderBy(g => g.DisplayName));
.Where(g => GameRegistry.Games[g].SupportedModManager == ModManager.Vortex)
.Select(g => new GameVM(g))
.OrderBy(g => g.DisplayName));
public ObservableCollectionExtended<GameVM> GameOptions => gameOptions;
public ObservableCollectionExtended<GameVM> GameOptions => _gameOptions;
[Reactive]
public GameVM SelectedGame { get; set; } = gameOptions.First(x => x.Game == Game.SkyrimSpecialEdition);
public GameVM SelectedGame { get; set; }
[Reactive]
public FilePickerVM GameLocation { get; set; }
@ -54,19 +53,19 @@ namespace Wabbajack
public VortexCompilerVM(CompilerVM parent)
{
this.GameLocation = new FilePickerVM()
GameLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select Game Folder Location"
};
this.DownloadsLocation = new FilePickerVM()
DownloadsLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select Downloads Folder"
};
this.StagingLocation = new FilePickerVM()
StagingLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.Folder,
@ -74,12 +73,12 @@ namespace Wabbajack
};
// Wire start command
this.BeginCommand = ReactiveCommand.CreateFromTask(
BeginCommand = ReactiveCommand.CreateFromTask(
canExecute: Observable.CombineLatest(
this.WhenAny(x => x.GameLocation.InError),
this.WhenAny(x => x.DownloadsLocation.InError),
this.WhenAny(x => x.StagingLocation.InError),
resultSelector: (g, d, s) => !g && !d && !s)
(g, d, s) => !g && !d && !s)
.ObserveOnGuiThread(),
execute: async () =>
{
@ -87,11 +86,19 @@ namespace Wabbajack
try
{
compiler = new VortexCompiler(
game: this.SelectedGame.Game,
gamePath: this.GameLocation.TargetPath,
vortexFolder: VortexCompiler.TypicalVortexFolder(),
downloadsFolder: this.DownloadsLocation.TargetPath,
stagingFolder: this.StagingLocation.TargetPath);
SelectedGame.Game,
GameLocation.TargetPath,
VortexCompiler.TypicalVortexFolder(),
DownloadsLocation.TargetPath,
StagingLocation.TargetPath)
{
ModListName = ModlistSettings.ModListName,
ModListAuthor = ModlistSettings.AuthorText,
ModListDescription = ModlistSettings.Description,
ModListImage = ModlistSettings.ImagePath.TargetPath,
ModListWebsite = ModlistSettings.Website,
ModListReadme = ModlistSettings.ReadMeText.TargetPath
};
}
catch (Exception ex)
{
@ -117,105 +124,99 @@ namespace Wabbajack
}
});
});
this._Compiling = this.BeginCommand.IsExecuting
.ToProperty(this, nameof(this.Compiling));
_compiling = BeginCommand.IsExecuting
.ToProperty(this, nameof(Compiling));
// Load settings
this.settings = parent.MWVM.Settings.Compiler.VortexCompilation;
this.SelectedGame = gameOptions.First(x => x.Game == settings.LastCompiledGame);
_settings = parent.MWVM.Settings.Compiler.VortexCompilation;
SelectedGame = _gameOptions.FirstOrDefault(x => x.Game == _settings.LastCompiledGame) ?? _gameOptions[0];
parent.MWVM.Settings.SaveSignal
.Subscribe(_ => Unload())
.DisposeWith(this.CompositeDisposable);
.DisposeWith(CompositeDisposable);
// Load custom game settings when game type changes
this.WhenAny(x => x.SelectedGame)
.Select(game => settings.ModlistSettings.TryCreate(game.Game))
.Select(game => _settings.ModlistSettings.TryCreate(game.Game))
.Pairwise()
.Subscribe(pair =>
{
// Save old
if (pair.Previous != null)
var (previous, current) = pair;
if (previous != null)
{
pair.Previous.GameLocation = this.GameLocation.TargetPath;
previous.GameLocation = GameLocation.TargetPath;
}
// Load new
this.GameLocation.TargetPath = pair.Current?.GameLocation ?? null;
if (string.IsNullOrWhiteSpace(this.GameLocation.TargetPath))
GameLocation.TargetPath = current?.GameLocation;
if (string.IsNullOrWhiteSpace(GameLocation.TargetPath))
{
this.SetGameToSteamLocation();
SetGameToSteamLocation();
}
if (string.IsNullOrWhiteSpace(this.GameLocation.TargetPath))
if (string.IsNullOrWhiteSpace(GameLocation.TargetPath))
{
this.SetGameToGogLocation();
SetGameToGogLocation();
}
this.DownloadsLocation.TargetPath = pair.Current?.DownloadLocation ?? null;
if (string.IsNullOrWhiteSpace(this.DownloadsLocation.TargetPath))
DownloadsLocation.TargetPath = current?.DownloadLocation;
if (string.IsNullOrWhiteSpace(DownloadsLocation.TargetPath))
{
this.DownloadsLocation.TargetPath = VortexCompiler.RetrieveDownloadLocation(this.SelectedGame.Game);
DownloadsLocation.TargetPath = VortexCompiler.RetrieveDownloadLocation(SelectedGame.Game);
}
this.StagingLocation.TargetPath = pair.Current?.StagingLocation ?? null;
if (string.IsNullOrWhiteSpace(this.StagingLocation.TargetPath))
StagingLocation.TargetPath = current?.StagingLocation;
if (string.IsNullOrWhiteSpace(StagingLocation.TargetPath))
{
this.StagingLocation.TargetPath = VortexCompiler.RetrieveStagingLocation(this.SelectedGame.Game);
StagingLocation.TargetPath = VortexCompiler.RetrieveStagingLocation(SelectedGame.Game);
}
})
.DisposeWith(this.CompositeDisposable);
.DisposeWith(CompositeDisposable);
// Load custom modlist settings when game type changes
this._ModlistSettings = this.WhenAny(x => x.SelectedGame)
// Load custom ModList settings when game type changes
this._modListSettings = this.WhenAny(x => x.SelectedGame)
.Select(game =>
{
var gameSettings = settings.ModlistSettings.TryCreate(game.Game);
var gameSettings = _settings.ModlistSettings.TryCreate(game.Game);
return new ModlistSettingsEditorVM(gameSettings.ModlistSettings);
})
// Interject and save old while loading new
.Pairwise()
.Do(pair =>
{
pair.Previous?.Save();
pair.Current?.Init();
var (previous, current) = pair;
previous?.Save();
current?.Init();
})
.Select(x => x.Current)
// Save to property
.ObserveOnGuiThread()
.ToProperty(this, nameof(this.ModlistSettings));
.ToProperty(this, nameof(ModlistSettings));
// Find game commands
this.FindGameInSteamCommand = ReactiveCommand.Create(SetGameToSteamLocation);
this.FindGameInGogCommand = ReactiveCommand.Create(SetGameToGogLocation);
FindGameInSteamCommand = ReactiveCommand.Create(SetGameToSteamLocation);
FindGameInGogCommand = ReactiveCommand.Create(SetGameToGogLocation);
// Add additional criteria to download/staging folders
this.DownloadsLocation.AdditionalError = this.WhenAny(x => x.DownloadsLocation.TargetPath)
.Select(path =>
{
if (path == null) return ErrorResponse.Success;
return VortexCompiler.IsValidDownloadsFolder(path);
});
this.StagingLocation.AdditionalError = this.WhenAny(x => x.StagingLocation.TargetPath)
.Select(path =>
{
if (path == null) return ErrorResponse.Success;
return VortexCompiler.IsValidBaseStagingFolder(path);
});
DownloadsLocation.AdditionalError = this.WhenAny(x => x.DownloadsLocation.TargetPath)
.Select(path => path == null ? ErrorResponse.Success : VortexCompiler.IsValidDownloadsFolder(path));
StagingLocation.AdditionalError = this.WhenAny(x => x.StagingLocation.TargetPath)
.Select(path => path == null ? ErrorResponse.Success : VortexCompiler.IsValidBaseStagingFolder(path));
}
public void Unload()
{
settings.LastCompiledGame = this.SelectedGame.Game;
this.ModlistSettings?.Save();
_settings.LastCompiledGame = SelectedGame.Game;
ModlistSettings?.Save();
}
private void SetGameToSteamLocation()
{
var steamGame = SteamHandler.Instance.Games.FirstOrDefault(g => g.Game.HasValue && g.Game == this.SelectedGame.Game);
this.GameLocation.TargetPath = steamGame?.InstallDir;
var steamGame = SteamHandler.Instance.Games.FirstOrDefault(g => g.Game.HasValue && g.Game == SelectedGame.Game);
GameLocation.TargetPath = steamGame?.InstallDir;
}
private void SetGameToGogLocation()
{
var gogGame = GOGHandler.Instance.Games.FirstOrDefault(g => g.Game.HasValue && g.Game == this.SelectedGame.Game);
this.GameLocation.TargetPath = gogGame?.Path;
var gogGame = GOGHandler.Instance.Games.FirstOrDefault(g => g.Game.HasValue && g.Game == SelectedGame.Game);
GameLocation.TargetPath = gogGame?.Path;
}
}
}

View File

@ -17,6 +17,7 @@
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
<IsWebBootstrapper>false</IsWebBootstrapper>
<XamlDebuggingInformation>True</XamlDebuggingInformation>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
@ -31,7 +32,6 @@
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
<XamlDebuggingInformation>True</XamlDebuggingInformation>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>x64</PlatformTarget>