mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
commit
c9e15cf7db
@ -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);
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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
300
Wabbajack.Lib/AInstaller.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
41
Wabbajack.Lib/CompilationSteps/IgnoreDisabledVortexMods.cs
Normal file
41
Wabbajack.Lib/CompilationSteps/IgnoreDisabledVortexMods.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()})");
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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" />
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user