Merge pull request #1132 from wabbajack-tools/more-abstract-compiler

More abstract compiler
This commit is contained in:
Timothy Baldridge 2020-10-22 09:40:48 -06:00 committed by GitHub
commit 85e2f28101
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 961 additions and 474 deletions

View File

@ -130,6 +130,8 @@ namespace Wabbajack.Common
public static AbsolutePath SettingsFile => LocalAppDataPath.Combine("settings.json"); public static AbsolutePath SettingsFile => LocalAppDataPath.Combine("settings.json");
public static RelativePath SettingsIni = (RelativePath)"settings.ini"; public static RelativePath SettingsIni = (RelativePath)"settings.ini";
public static byte SettingsVersion => 2; public static byte SettingsVersion => 2;
public static RelativePath NativeSettingsJson = (RelativePath)"native_compiler_settings.json";
public static bool IsServer = false; public static bool IsServer = false;
public static string CompressedBodyHeader = "x-compressed-body"; public static string CompressedBodyHeader = "x-compressed-body";

View File

@ -19,46 +19,75 @@ namespace Wabbajack.Lib
{ {
public abstract class ACompiler : ABatchProcessor public abstract class ACompiler : ABatchProcessor
{ {
public string? ModListName, ModListAuthor, ModListDescription, ModListWebsite, ModlistReadme; protected readonly Subject<(string, float)> _progressUpdates = new Subject<(string, float)>();
public Version? ModlistVersion;
public List<IndexedArchive> IndexedArchives = new List<IndexedArchive>();
public Dictionary<Hash, IEnumerable<VirtualFile>> IndexedFiles =
new Dictionary<Hash, IEnumerable<VirtualFile>>();
public ModList ModList = new ModList();
public AbsolutePath ModListImage; public AbsolutePath ModListImage;
public bool ModlistIsNSFW; public bool ModlistIsNSFW;
public string? ModListName, ModListAuthor, ModListDescription, ModListWebsite, ModlistReadme;
public Version? ModlistVersion;
protected Version? WabbajackVersion; protected Version? WabbajackVersion;
public abstract AbsolutePath VFSCacheName { get; } public ACompiler(int steps, string modlistName, AbsolutePath sourcePath, AbsolutePath downloadsPath,
AbsolutePath outputModListName)
: base(steps)
{
SourcePath = sourcePath;
DownloadsPath = downloadsPath;
ModListName = modlistName;
ModListOutputFile = outputModListName;
//set in MainWindowVM
WabbajackVersion = Consts.CurrentMinimumWabbajackVersion;
Settings = new CompilerSettings();
ModListOutputFolder = AbsolutePath.EntryPoint.Combine("output_folder", Guid.NewGuid().ToString());
CompilingGame = new GameMetaData();
}
/// <summary>
/// Set to true to include game files during compilation, only ever disabled
/// in testing (to speed up tests)
/// </summary>
public bool UseGamePaths { get; set; } = true;
public CompilerSettings Settings { get; set; }
public AbsolutePath VFSCacheName =>
Consts.LocalAppDataPath.Combine(
$"vfs_compile_cache-2-{Path.Combine((string)SourcePath ?? "Unknown", "ModOrganizer.exe").StringSha256Hex()}.bin");
//protected string VFSCacheName => Path.Combine(Consts.LocalAppDataPath, $"vfs_compile_cache.bin"); //protected string VFSCacheName => Path.Combine(Consts.LocalAppDataPath, $"vfs_compile_cache.bin");
/// <summary> /// <summary>
/// A stream of tuples of ("Update Title", 0.25) which represent the name of the current task /// A stream of tuples of ("Update Title", 0.25) which represent the name of the current task
/// and the current progress. /// and the current progress.
/// </summary> /// </summary>
public IObservable<(string, float)> ProgressUpdates => _progressUpdates; public IObservable<(string, float)> ProgressUpdates => _progressUpdates;
protected readonly Subject<(string, float)> _progressUpdates = new Subject<(string, float)>();
public abstract ModManager ModManager { get; }
public abstract AbsolutePath GamePath { get; } public abstract AbsolutePath GamePath { get; }
public Dictionary<Game, HashSet<Hash>> GameHashes { get; set; } = new Dictionary<Game, HashSet<Hash>>();
public Dictionary<Hash, Game[]> GamesWithHashes { get; set; } = new Dictionary<Hash, Game[]>();
public abstract AbsolutePath ModListOutputFolder { get; } public AbsolutePath SourcePath { get; }
public abstract AbsolutePath ModListOutputFile { get; } public AbsolutePath DownloadsPath { get; }
public GameMetaData CompilingGame { get; set; }
public AbsolutePath ModListOutputFolder { get; }
public AbsolutePath ModListOutputFile { get; }
public bool IgnoreMissingFiles { get; set; } public bool IgnoreMissingFiles { get; set; }
public List<Archive> SelectedArchives { get; protected set; } = new List<Archive>(); public List<Archive> SelectedArchives { get; protected set; } = new List<Archive>();
public List<Directive> InstallDirectives { get; protected set; } = new List<Directive>(); public List<Directive> InstallDirectives { get; protected set; } = new List<Directive>();
public List<RawSourceFile> AllFiles { get; protected set; } = new List<RawSourceFile>(); public List<RawSourceFile> AllFiles { get; protected set; } = new List<RawSourceFile>();
public ModList ModList = new ModList();
public List<IndexedArchive> IndexedArchives = new List<IndexedArchive>(); public Dictionary<AbsolutePath, IndexedArchive> ArchivesByFullPath { get; set; } =
public Dictionary<AbsolutePath, IndexedArchive> ArchivesByFullPath { get; set; } = new Dictionary<AbsolutePath, IndexedArchive>(); new Dictionary<AbsolutePath, IndexedArchive>();
public Dictionary<Hash, IEnumerable<VirtualFile>> IndexedFiles = new Dictionary<Hash, IEnumerable<VirtualFile>>();
public ACompiler(int steps)
: base(steps)
{
//set in MainWindowVM
WabbajackVersion = Consts.CurrentMinimumWabbajackVersion;
}
public static void Info(string msg) public static void Info(string msg)
{ {
@ -100,21 +129,21 @@ namespace Wabbajack.Lib
await ModListOutputFolder.Combine(id).WriteAllTextAsync(data); await ModListOutputFolder.Combine(id).WriteAllTextAsync(data);
return id; return id;
} }
internal async Task<RelativePath> IncludeFile(Stream data) internal async Task<RelativePath> IncludeFile(Stream data)
{ {
var id = IncludeId(); var id = IncludeId();
await ModListOutputFolder.Combine(id).WriteAllAsync(data); await ModListOutputFolder.Combine(id).WriteAllAsync(data);
return id; return id;
} }
internal async Task<RelativePath> IncludeFile(AbsolutePath data) internal async Task<RelativePath> IncludeFile(AbsolutePath data)
{ {
await using var stream = await data.OpenRead(); await using var stream = await data.OpenRead();
return await IncludeFile(stream); return await IncludeFile(stream);
} }
internal async Task<(RelativePath, AbsolutePath)> IncludeString(string str) internal async Task<(RelativePath, AbsolutePath)> IncludeString(string str)
{ {
var id = IncludeId(); var id = IncludeId();
@ -147,7 +176,123 @@ namespace Wabbajack.Lib
return true; return true;
} }
public async Task ExportModList() protected async Task IndexGameFileHashes()
{
if (UseGamePaths)
{
foreach (var ag in Settings.IncludedGames.Cons(CompilingGame.Game))
{
try
{
var files = await ClientAPI.GetExistingGameFiles(Queue, ag);
Utils.Log($"Including {files.Length} stock game files from {ag} as download sources");
GameHashes[ag] = files.Select(f => f.Hash).ToHashSet();
IndexedArchives.AddRange(files.Select(f =>
{
var meta = f.State.GetMetaIniString();
var ini = meta.LoadIniString();
var state = (GameFileSourceDownloader.State)f.State;
return new IndexedArchive(
VFS.Index.ByRootPath[ag.MetaData().GameLocation().Combine(state.GameFile)])
{
IniData = ini, Meta = meta
};
}));
}
catch (Exception e)
{
Utils.Error(e, "Unable to find existing game files, skipping.");
}
}
GamesWithHashes = GameHashes.SelectMany(g => g.Value.Select(h => (g, h)))
.GroupBy(gh => gh.h)
.ToDictionary(gh => gh.Key, gh => gh.Select(p => p.g.Key).ToArray());
}
}
protected async Task CleanInvalidArchivesAndFillState()
{
var remove = (await IndexedArchives.PMap(Queue, async a =>
{
try
{
a.State = (await ResolveArchive(a)).State;
return null;
}
catch
{
return a;
}
})).NotNull().ToHashSet();
if (remove.Count == 0)
{
return;
}
Utils.Log(
$"Removing {remove.Count} archives from the compilation state, this is probably not an issue but reference this if you have compilation failures");
remove.Do(r => Utils.Log($"Resolution failed for: ({r.File.Size} {r.File.Hash}) {r.File.FullPath}"));
IndexedArchives.RemoveAll(a => remove.Contains(a));
}
protected async Task InferMetas()
{
async Task<bool> HasInvalidMeta(AbsolutePath filename)
{
var metaname = filename.WithExtension(Consts.MetaFileExtension);
if (!metaname.Exists)
{
return true;
}
try
{
return await DownloadDispatcher.ResolveArchive(metaname.LoadIniFile()) == null;
}
catch (Exception e)
{
Utils.ErrorThrow(e, $"Exception while checking meta {filename}");
return false;
}
}
var to_find = (await DownloadsPath.EnumerateFiles()
.Where(f => f.Extension != Consts.MetaFileExtension && f.Extension != Consts.HashFileExtension)
.PMap(Queue, async f => await HasInvalidMeta(f) ? f : default))
.Where(f => f.Exists)
.ToList();
if (to_find.Count == 0)
{
return;
}
Utils.Log($"Attempting to infer {to_find.Count} metas from the server.");
await to_find.PMap(Queue, async f =>
{
var vf = VFS.Index.ByRootPath[f];
var meta = await ClientAPI.InferDownloadState(vf.Hash);
if (meta == null)
{
await vf.AbsoluteName.WithExtension(Consts.MetaFileExtension).WriteAllLinesAsync(
"[General]",
"unknownArchive=true");
return;
}
Utils.Log($"Inferred .meta for {vf.FullPath.FileName}, writing to disk");
await vf.AbsoluteName.WithExtension(Consts.MetaFileExtension)
.WriteAllTextAsync(meta.GetMetaIniString());
});
}
protected async Task ExportModList()
{ {
Utils.Log($"Exporting ModList to {ModListOutputFile}"); Utils.Log($"Exporting ModList to {ModListOutputFile}");
@ -157,7 +302,7 @@ namespace Wabbajack.Lib
ModList.Image = (RelativePath)"modlist-image.png"; ModList.Image = (RelativePath)"modlist-image.png";
} }
await using (var of = await ModListOutputFolder.Combine("modlist").Create()) await using (var of = await ModListOutputFolder.Combine("modlist").Create())
ModList.ToJson(of); ModList.ToJson(of);
await ModListOutputFolder.Combine("sig") await ModListOutputFolder.Combine("sig")
@ -170,16 +315,16 @@ namespace Wabbajack.Lib
await using (var fs = await ModListOutputFile.Create()) await using (var fs = await ModListOutputFile.Create())
{ {
using var za = new ZipArchive(fs, ZipArchiveMode.Create); using var za = new ZipArchive(fs, ZipArchiveMode.Create);
await ModListOutputFolder.EnumerateFiles() await ModListOutputFolder.EnumerateFiles()
.DoProgress("Compressing ModList", .DoProgress("Compressing ModList",
async f => async f =>
{ {
var ze = za.CreateEntry((string)f.FileName); var ze = za.CreateEntry((string)f.FileName);
await using var os = ze.Open(); await using var os = ze.Open();
await using var ins = await f.OpenRead(); await using var ins = await f.OpenRead();
await ins.CopyToAsync(os); await ins.CopyToAsync(os);
}); });
// Copy in modimage // Copy in modimage
if (ModListImage.Exists) if (ModListImage.Exists)
@ -207,6 +352,114 @@ namespace Wabbajack.Lib
await Utils.DeleteDirectory(ModListOutputFolder); await Utils.DeleteDirectory(ModListOutputFolder);
} }
/// <summary>
/// Fills in the Patch fields in files that require them
/// </summary>
protected async Task BuildPatches()
{
Info("Gathering patch files");
var toBuild = InstallDirectives.OfType<PatchedFromArchive>()
.Where(p => p.Choices.Length > 0)
.SelectMany(p => p.Choices.Select(c => new PatchedFromArchive
{
To = p.To,
Hash = p.Hash,
ArchiveHashPath = c.MakeRelativePaths(),
FromFile = c,
Size = p.Size
}))
.ToArray();
if (toBuild.Length == 0)
{
return;
}
// Extract all the source files
var indexed = toBuild.GroupBy(f => VFS.Index.FileForArchiveHashPath(f.ArchiveHashPath))
.ToDictionary(f => f.Key);
await VFS.Extract(Queue, indexed.Keys.ToHashSet(),
async (vf, sf) =>
{
// For each, extract the destination
var matches = indexed[vf];
using var iqueue = new WorkQueue(1);
foreach (var match in matches)
{
var destFile = FindDestFile(match.To);
// Build the patch
await VFS.Extract(iqueue, new[] {destFile}.ToHashSet(),
async (destvf, destsfn) =>
{
Info($"Patching {match.To}");
Status($"Patching {match.To}");
await using var srcStream = await sf.GetStream();
await using var destStream = await destsfn.GetStream();
var patchSize =
await Utils.CreatePatchCached(srcStream, vf.Hash, destStream, destvf.Hash);
Info($"Patch size {patchSize} for {match.To}");
});
}
});
// Load in the patches
await InstallDirectives.OfType<PatchedFromArchive>()
.Where(p => p.PatchID == default)
.PMap(Queue, async pfa =>
{
var patches = pfa.Choices
.Select(c => (Utils.TryGetPatch(c.Hash, pfa.Hash, out var data), data, c))
.ToArray();
// Pick the best patch
if (patches.All(p => p.Item1))
{
var (_, bytes, file) = IncludePatches.PickPatch(this, patches);
pfa.FromFile = file;
pfa.FromHash = file.Hash;
pfa.ArchiveHashPath = file.MakeRelativePaths();
pfa.PatchID = await IncludeFile(bytes!);
}
});
var firstFailedPatch =
InstallDirectives.OfType<PatchedFromArchive>().FirstOrDefault(f => f.PatchID == default);
if (firstFailedPatch != null)
{
Utils.Log("Missing data from failed patch, starting data dump");
Utils.Log($"Dest File: {firstFailedPatch.To}");
Utils.Log($"Options ({firstFailedPatch.Choices.Length}:");
foreach (var choice in firstFailedPatch.Choices)
{
Utils.Log($" {choice.FullPath}");
}
Error(
$"Missing patches after generation, this should not happen. First failure: {firstFailedPatch.FullPath}");
}
}
private VirtualFile FindDestFile(RelativePath to)
{
var abs = to.RelativeTo(SourcePath);
if (abs.Exists)
{
return VFS.Index.ByRootPath[abs];
}
if (to.StartsWith(Consts.BSACreationDir))
{
var bsaId = (RelativePath)((string)to).Split('\\')[1];
var bsa = InstallDirectives.OfType<CreateBSA>().First(b => b.TempID == bsaId);
var find = (RelativePath)Path.Combine(((string)to).Split('\\').Skip(2).ToArray());
return VFS.Index.ByRootPath[SourcePath.Combine(bsa.To)].Children.First(c => c.RelativeName == find);
}
throw new ArgumentException($"Couldn't load data for {to}");
}
public void GenerateManifest() public void GenerateManifest()
{ {
var manifest = new Manifest(ModList); var manifest = new Manifest(ModList);
@ -240,7 +493,7 @@ namespace Wabbajack.Lib
public async Task<Archive> ResolveArchive([NotNull] IndexedArchive archive) public async Task<Archive> ResolveArchive([NotNull] IndexedArchive archive)
{ {
if (!string.IsNullOrWhiteSpace(archive.Name)) if (!string.IsNullOrWhiteSpace(archive.Name))
Utils.Status($"Checking link for {archive.Name}", alsoLog: true); Utils.Status($"Checking link for {archive.Name}", alsoLog: true);
if (archive.IniData == null) if (archive.IniData == null)
@ -264,7 +517,7 @@ namespace Wabbajack.Lib
result.Meta = string.Join("\n", result.State!.GetMetaIni()); result.Meta = string.Join("\n", result.State!.GetMetaIni());
return result; return result;
} }
@ -300,6 +553,7 @@ namespace Wabbajack.Lib
{ {
Utils.LogStraightToFile($" {file.To} - {file.Reason}"); Utils.LogStraightToFile($" {file.To} - {file.Reason}");
} }
if (count == max && noMatches.Count > max) if (count == max && noMatches.Count > max)
{ {
Utils.Log($" ..."); Utils.Log($" ...");
@ -325,7 +579,6 @@ namespace Wabbajack.Lib
file.SourceDataID = id; file.SourceDataID = id;
file.SourceDataFile = null; file.SourceDataFile = null;
} }
}); });
} }
@ -344,6 +597,7 @@ namespace Wabbajack.Lib
return true; return true;
} }
} }
return false; return false;
} }
} }

View File

@ -28,7 +28,7 @@ namespace Wabbajack.Lib.CompilationSteps
if (general.comments != null && (general.notes.Contains(Consts.WABBAJACK_INCLUDE) || general.notes.Contains(Consts.WABBAJACK_NOMATCH_INCLUDE))) return true; if (general.comments != null && (general.notes.Contains(Consts.WABBAJACK_INCLUDE) || general.notes.Contains(Consts.WABBAJACK_NOMATCH_INCLUDE))) return true;
return false; return false;
}) })
.Select(kv => kv.Key.RelativeTo(_mo2Compiler.MO2Folder)) .Select(kv => kv.Key.RelativeTo(_mo2Compiler.SourcePath))
.ToList(); .ToList();
_microstack = bsa => new List<ICompilationStep> _microstack = bsa => new List<ICompilationStep>
@ -51,7 +51,7 @@ namespace Wabbajack.Lib.CompilationSteps
if (!Consts.SupportedBSAs.Contains(source.Path.Extension)) return null; if (!Consts.SupportedBSAs.Contains(source.Path.Extension)) return null;
var defaultInclude = false; var defaultInclude = false;
if (source.Path.RelativeTo(_mo2Compiler.MO2Folder).InFolder(_mo2Compiler.MO2Folder.Combine(Consts.MO2ModFolderName))) if (source.Path.RelativeTo(_mo2Compiler.SourcePath).InFolder(_mo2Compiler.SourcePath.Combine(Consts.MO2ModFolderName)))
if (_includeDirectly.Any(path => source.Path.StartsWith(path))) if (_includeDirectly.Any(path => source.Path.StartsWith(path)))
defaultInclude = true; defaultInclude = true;

View File

@ -13,7 +13,7 @@ namespace Wabbajack.Lib.CompilationSteps
{ {
} }
public static int GetFilePriority(MO2Compiler compiler, VirtualFile file) public static int GetFilePriority(ACompiler compiler, VirtualFile file)
{ {
var archive = file.TopParent; var archive = file.TopParent;
var adata = compiler.ArchivesByFullPath[archive.AbsoluteName]; var adata = compiler.ArchivesByFullPath[archive.AbsoluteName];
@ -26,12 +26,11 @@ namespace Wabbajack.Lib.CompilationSteps
public override async ValueTask<Directive?> Run(RawSourceFile source) public override async ValueTask<Directive?> Run(RawSourceFile source)
{ {
var mo2Compiler = (MO2Compiler)_compiler;
if (!_compiler.IndexedFiles.TryGetValue(source.Hash, out var found)) return null; if (!_compiler.IndexedFiles.TryGetValue(source.Hash, out var found)) return null;
var result = source.EvolveTo<FromArchive>(); var result = source.EvolveTo<FromArchive>();
var match = found.Where(f => f.Name.FileName == source.Path.FileName) var match = found.Where(f => f.Name.FileName == source.Path.FileName)
.OrderBy(f => GetFilePriority(mo2Compiler, f)) .OrderBy(f => GetFilePriority(_compiler, f))
.ThenBy(f => f.NestingFactor) .ThenBy(f => f.NestingFactor)
.FirstOrDefault() .FirstOrDefault()
?? found.OrderBy(f => f.NestingFactor).FirstOrDefault(); ?? found.OrderBy(f => f.NestingFactor).FirstOrDefault();

View File

@ -20,7 +20,7 @@ namespace Wabbajack.Lib.CompilationSteps
.Where(f => HasFlagInNotes(f.Value, Consts.WABBAJACK_ALWAYS_DISABLE)).Select(f => f.Key).Distinct(); .Where(f => HasFlagInNotes(f.Value, Consts.WABBAJACK_ALWAYS_DISABLE)).Select(f => f.Key).Distinct();
_allEnabledMods = _mo2Compiler.SelectedProfiles _allEnabledMods = _mo2Compiler.SelectedProfiles
.SelectMany(p => _mo2Compiler.MO2Folder.Combine("profiles", p, "modlist.txt").ReadAllLines()) .SelectMany(p => _mo2Compiler.SourcePath.Combine("profiles", p, "modlist.txt").ReadAllLines())
.Where(line => line.StartsWith("+") || line.EndsWith("_separator")) .Where(line => line.StartsWith("+") || line.EndsWith("_separator"))
.Select(line => line.Substring(1).RelativeTo(_mo2Compiler.MO2ModsFolder)) .Select(line => line.Substring(1).RelativeTo(_mo2Compiler.MO2ModsFolder))
.Concat(alwaysEnabled) .Concat(alwaysEnabled)

View File

@ -11,7 +11,7 @@ namespace Wabbajack.Lib.CompilationSteps
public IgnoreGameFilesIfGameFolderFilesExist(ACompiler compiler) : base(compiler) public IgnoreGameFilesIfGameFolderFilesExist(ACompiler compiler) : base(compiler)
{ {
_gameFolderFilesExists = ((MO2Compiler)compiler).MO2Folder.Combine(Consts.GameFolderFilesDir).IsDirectory; _gameFolderFilesExists = ((MO2Compiler)compiler).SourcePath.Combine(Consts.GameFolderFilesDir).IsDirectory;
_gameFolder = compiler.GamePath; _gameFolder = compiler.GamePath;
} }

View File

@ -11,7 +11,7 @@ namespace Wabbajack.Lib.CompilationSteps
public IgnoreSaveFiles(ACompiler compiler) : base(compiler) public IgnoreSaveFiles(ACompiler compiler) : base(compiler)
{ {
_profilePaths = _profilePaths =
MO2Compiler.SelectedProfiles.Select(p => MO2Compiler.MO2Folder.Combine("profiles", p, "saves")).ToArray(); MO2Compiler.SelectedProfiles.Select(p => MO2Compiler.SourcePath.Combine("profiles", p, "saves")).ToArray();
} }
public override async ValueTask<Directive?> Run(RawSourceFile source) public override async ValueTask<Directive?> Run(RawSourceFile source)

View File

@ -16,7 +16,7 @@ namespace Wabbajack.Lib.CompilationSteps
public IgnoreOtherProfiles(ACompiler compiler) : base(compiler) public IgnoreOtherProfiles(ACompiler compiler) : base(compiler)
{ {
_mo2Compiler = (MO2Compiler) compiler; _mo2Compiler = (MO2Compiler) compiler;
_modProfilesFolder = _mo2Compiler.MO2Folder.Combine("profiles"); _modProfilesFolder = _mo2Compiler.SourcePath.Combine("profiles");
_profiles = _mo2Compiler.SelectedProfiles _profiles = _mo2Compiler.SelectedProfiles
.Select(p => _modProfilesFolder.Combine(p)) .Select(p => _modProfilesFolder.Combine(p))

View File

@ -16,13 +16,13 @@ namespace Wabbajack.Lib.CompilationSteps
private readonly Dictionary<RelativePath, IGrouping<RelativePath, VirtualFile>> _indexed; private readonly Dictionary<RelativePath, IGrouping<RelativePath, VirtualFile>> _indexed;
private VirtualFile? _bsa; private VirtualFile? _bsa;
private Dictionary<RelativePath, IEnumerable<VirtualFile>> _indexedByName; private Dictionary<RelativePath, IEnumerable<VirtualFile>> _indexedByName;
private MO2Compiler _mo2Compiler; private ACompiler _compiler;
private bool _isGenericGame; private bool _isGenericGame;
public IncludePatches(ACompiler compiler, VirtualFile? constructingFromBSA = null) : base(compiler) public IncludePatches(ACompiler compiler, VirtualFile? constructingFromBSA = null) : base(compiler)
{ {
_bsa = constructingFromBSA; _bsa = constructingFromBSA;
_mo2Compiler = (MO2Compiler)compiler; _compiler = compiler;
_indexed = _compiler.IndexedFiles.Values _indexed = _compiler.IndexedFiles.Values
.SelectMany(f => f) .SelectMany(f => f)
.GroupBy(f => f.Name.FileName) .GroupBy(f => f.Name.FileName)
@ -33,7 +33,7 @@ namespace Wabbajack.Lib.CompilationSteps
.GroupBy(f => f.Name.FileName) .GroupBy(f => f.Name.FileName)
.ToDictionary(f => f.Key, f => (IEnumerable<VirtualFile>)f); .ToDictionary(f => f.Key, f => (IEnumerable<VirtualFile>)f);
_isGenericGame = _mo2Compiler.CompilingGame.IsGenericMO2Plugin; _isGenericGame = _compiler.CompilingGame.IsGenericMO2Plugin;
} }
public override async ValueTask<Directive?> Run(RawSourceFile source) public override async ValueTask<Directive?> Run(RawSourceFile source)
@ -53,13 +53,16 @@ namespace Wabbajack.Lib.CompilationSteps
_indexed.TryGetValue(nameWithoutExt, out choices); _indexed.TryGetValue(nameWithoutExt, out choices);
dynamic? modIni = null; dynamic? modIni = null;
if (_bsa == null && source.File.IsNative && source.AbsolutePath.InFolder(_mo2Compiler.MO2ModsFolder)) if (_compiler is MO2Compiler)
((MO2Compiler)_compiler).ModInis.TryGetValue(ModForFile(source.AbsolutePath), out modIni);
else if (_bsa != null)
{ {
var bsaPath = _bsa.FullPath.Base; if (_bsa == null && source.File.IsNative && source.AbsolutePath.InFolder(((MO2Compiler)_compiler).MO2ModsFolder))
((MO2Compiler)_compiler).ModInis.TryGetValue(ModForFile(bsaPath), out modIni); ((MO2Compiler)_compiler).ModInis.TryGetValue(ModForFile(source.AbsolutePath), out modIni);
else if (_bsa != null)
{
var bsaPath = _bsa.FullPath.Base;
((MO2Compiler)_compiler).ModInis.TryGetValue(ModForFile(bsaPath), out modIni);
}
} }
var installationFile = (string?)modIni?.General?.installationFile; var installationFile = (string?)modIni?.General?.installationFile;
@ -105,7 +108,7 @@ namespace Wabbajack.Lib.CompilationSteps
if (patches.All(p => p.Item1)) if (patches.All(p => p.Item1))
{ {
var (_, bytes, file) = PickPatch(_mo2Compiler, patches); var (_, bytes, file) = PickPatch(_compiler, patches);
e.FromHash = file.Hash; e.FromHash = file.Hash;
e.ArchiveHashPath = file.MakeRelativePaths(); e.ArchiveHashPath = file.MakeRelativePaths();
e.PatchID = await _compiler.IncludeFile(bytes!); e.PatchID = await _compiler.IncludeFile(bytes!);
@ -126,7 +129,7 @@ namespace Wabbajack.Lib.CompilationSteps
return e; return e;
} }
public static (bool, byte[], VirtualFile) PickPatch(MO2Compiler mo2Compiler, IEnumerable<(bool foundHash, byte[]? data, VirtualFile file)> patches) public static (bool, byte[], VirtualFile) PickPatch(ACompiler compiler, IEnumerable<(bool foundHash, byte[]? data, VirtualFile file)> patches)
{ {
var ordered = patches var ordered = patches
.Select(f => (f.foundHash, f.data!, f.file)) .Select(f => (f.foundHash, f.data!, f.file))
@ -138,11 +141,11 @@ namespace Wabbajack.Lib.CompilationSteps
var baseHash = itm.file.TopParent.Hash; var baseHash = itm.file.TopParent.Hash;
// If this file doesn't come from a game use it // If this file doesn't come from a game use it
if (!mo2Compiler.GamesWithHashes.TryGetValue(baseHash, out var games)) if (!compiler.GamesWithHashes.TryGetValue(baseHash, out var games))
return true; return true;
// Otherwise skip files that are not from the primary game // Otherwise skip files that are not from the primary game
return games.Contains(mo2Compiler.CompilingGame.Game); return games.Contains(compiler.CompilingGame.Game);
}); });
// If we didn't find a file from an archive or the primary game, use a secondary game file. // If we didn't find a file from an archive or the primary game, use a secondary game file.

View File

@ -28,14 +28,14 @@ namespace Wabbajack.Lib.CompilationSteps
data = data.Replace(((string)_mo2Compiler.GamePath).Replace("\\", "\\\\"), Consts.GAME_PATH_MAGIC_DOUBLE_BACK); data = data.Replace(((string)_mo2Compiler.GamePath).Replace("\\", "\\\\"), Consts.GAME_PATH_MAGIC_DOUBLE_BACK);
data = data.Replace(((string)_mo2Compiler.GamePath).Replace("\\", "/"), Consts.GAME_PATH_MAGIC_FORWARD); data = data.Replace(((string)_mo2Compiler.GamePath).Replace("\\", "/"), Consts.GAME_PATH_MAGIC_FORWARD);
data = data.Replace((string)_mo2Compiler.MO2Folder, Consts.MO2_PATH_MAGIC_BACK); data = data.Replace((string)_mo2Compiler.SourcePath, Consts.MO2_PATH_MAGIC_BACK);
data = data.Replace(((string)_mo2Compiler.MO2Folder).Replace("\\", "\\\\"), Consts.MO2_PATH_MAGIC_DOUBLE_BACK); data = data.Replace(((string)_mo2Compiler.SourcePath).Replace("\\", "\\\\"), Consts.MO2_PATH_MAGIC_DOUBLE_BACK);
data = data.Replace(((string)_mo2Compiler.MO2Folder).Replace("\\", "/"), Consts.MO2_PATH_MAGIC_FORWARD); data = data.Replace(((string)_mo2Compiler.SourcePath).Replace("\\", "/"), Consts.MO2_PATH_MAGIC_FORWARD);
data = data.Replace((string)_mo2Compiler.MO2DownloadsFolder, Consts.DOWNLOAD_PATH_MAGIC_BACK); data = data.Replace((string)_mo2Compiler.DownloadsPath, Consts.DOWNLOAD_PATH_MAGIC_BACK);
data = data.Replace(((string)_mo2Compiler.MO2DownloadsFolder).Replace("\\", "\\\\"), data = data.Replace(((string)_mo2Compiler.DownloadsPath).Replace("\\", "\\\\"),
Consts.DOWNLOAD_PATH_MAGIC_DOUBLE_BACK); Consts.DOWNLOAD_PATH_MAGIC_DOUBLE_BACK);
data = data.Replace(((string)_mo2Compiler.MO2DownloadsFolder).Replace("\\", "/"), Consts.DOWNLOAD_PATH_MAGIC_FORWARD); data = data.Replace(((string)_mo2Compiler.DownloadsPath).Replace("\\", "/"), Consts.DOWNLOAD_PATH_MAGIC_FORWARD);
if (data == originalData) if (data == originalData)
return null; return null;

View File

@ -4,12 +4,12 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.Lib.CompilationSteps; using Wabbajack.Lib.CompilationSteps;
using Wabbajack.Lib.Downloaders; using Wabbajack.Lib.Downloaders;
using Wabbajack.Lib.Validation; using Wabbajack.Lib.Validation;
using Wabbajack.VirtualFileSystem; using Wabbajack.VirtualFileSystem;
using Path = Alphaleonis.Win32.Filesystem.Path;
namespace Wabbajack.Lib namespace Wabbajack.Lib
{ {
@ -17,70 +17,43 @@ namespace Wabbajack.Lib
{ {
private AbsolutePath _mo2DownloadsFolder; private AbsolutePath _mo2DownloadsFolder;
public AbsolutePath MO2Folder; public MO2Compiler(AbsolutePath sourcePath, AbsolutePath downloadsPath, string mo2Profile, AbsolutePath outputFile)
: base(21, mo2Profile, sourcePath, downloadsPath, outputFile)
{
MO2Profile = mo2Profile;
MO2Ini = SourcePath.Combine("ModOrganizer.ini").LoadIniFile();
var mo2game = (string)MO2Ini.General.gameName;
CompilingGame = GameRegistry.Games.First(g => g.Value.MO2Name == mo2game).Value;
GamePath = CompilingGame.GameLocation();
}
public AbsolutePath MO2ModsFolder => MO2Folder.Combine(Consts.MO2ModFolderName); public AbsolutePath MO2ModsFolder => SourcePath.Combine(Consts.MO2ModFolderName);
public string MO2Profile { get; } public string MO2Profile { get; }
public override ModManager ModManager => ModManager.MO2;
public override AbsolutePath GamePath { get; } public override AbsolutePath GamePath { get; }
public GameMetaData CompilingGame { get; }
public CompilerSettings Settings { get; set; }
public override AbsolutePath ModListOutputFolder => ((RelativePath)"output_folder").RelativeToEntryPoint();
public override AbsolutePath ModListOutputFile { get; }
public override AbsolutePath VFSCacheName =>
Consts.LocalAppDataPath.Combine(
$"vfs_compile_cache-2-{Path.Combine((string)MO2Folder ?? "Unknown", "ModOrganizer.exe").StringSha256Hex()}.bin");
public dynamic MO2Ini { get; } public dynamic MO2Ini { get; }
public static AbsolutePath GetTypicalDownloadsFolder(AbsolutePath mo2Folder) => mo2Folder.Combine("downloads"); public AbsolutePath MO2ProfileDir => SourcePath.Combine("profiles", MO2Profile);
public AbsolutePath MO2ProfileDir => MO2Folder.Combine("profiles", MO2Profile);
public ConcurrentBag<Directive> ExtraFiles { get; private set; } = new ConcurrentBag<Directive>(); public ConcurrentBag<Directive> ExtraFiles { get; private set; } = new ConcurrentBag<Directive>();
public Dictionary<AbsolutePath, dynamic> ModInis { get; } = new Dictionary<AbsolutePath, dynamic>(); public Dictionary<AbsolutePath, dynamic> ModInis { get; } = new Dictionary<AbsolutePath, dynamic>();
public HashSet<string> SelectedProfiles { get; set; } = new HashSet<string>(); public HashSet<string> SelectedProfiles { get; set; } = new HashSet<string>();
public MO2Compiler(AbsolutePath mo2Folder, string mo2Profile, AbsolutePath outputFile) public static AbsolutePath GetTypicalDownloadsFolder(AbsolutePath mo2Folder)
: base(steps: 21)
{ {
MO2Folder = mo2Folder; return mo2Folder.Combine("downloads");
MO2Profile = mo2Profile;
MO2Ini = MO2Folder.Combine("ModOrganizer.ini").LoadIniFile();
var mo2game = (string)MO2Ini.General.gameName;
CompilingGame = GameRegistry.Games.First(g => g.Value.MO2Name == mo2game).Value;
GamePath = CompilingGame.GameLocation();
ModListOutputFile = outputFile;
Settings = new CompilerSettings();
}
public AbsolutePath MO2DownloadsFolder
{
get
{
if (_mo2DownloadsFolder != default) return _mo2DownloadsFolder;
if (MO2Ini != null)
if (MO2Ini.Settings != null)
if (MO2Ini.Settings.download_directory != null)
return MO2Ini.Settings.download_directory.Replace("/", "\\");
return GetTypicalDownloadsFolder(MO2Folder);
}
set => _mo2DownloadsFolder = value;
} }
protected override async Task<bool> _Begin(CancellationToken cancel) protected override async Task<bool> _Begin(CancellationToken cancel)
{ {
await Metrics.Send("begin_compiling", MO2Profile ?? "unknown"); await Metrics.Send("begin_compiling", MO2Profile ?? "unknown");
if (cancel.IsCancellationRequested) return false; if (cancel.IsCancellationRequested)
{
return false;
}
DesiredThreads.OnNext(DiskThreads); DesiredThreads.OnNext(DiskThreads);
FileExtractor2.FavorPerfOverRAM = FavorPerfOverRam; FileExtractor2.FavorPerfOverRAM = FavorPerfOverRam;
@ -88,31 +61,36 @@ namespace Wabbajack.Lib
UpdateTracker.Reset(); UpdateTracker.Reset();
UpdateTracker.NextStep("Gathering information"); UpdateTracker.NextStep("Gathering information");
Utils.Log($"Loading compiler Settings"); Utils.Log("Loading compiler Settings");
Settings = await CompilerSettings.Load(MO2ProfileDir); Settings = await CompilerSettings.Load(MO2ProfileDir);
Settings.IncludedGames = Settings.IncludedGames.Add(CompilingGame.Game); Settings.IncludedGames = Settings.IncludedGames.Add(CompilingGame.Game);
Info("Looking for other profiles"); Info("Looking for other profiles");
var otherProfilesPath = MO2ProfileDir.Combine("otherprofiles.txt"); var otherProfilesPath = MO2ProfileDir.Combine("otherprofiles.txt");
SelectedProfiles = new HashSet<string>(); SelectedProfiles = new HashSet<string>();
if (otherProfilesPath.Exists) SelectedProfiles = (await otherProfilesPath.ReadAllLinesAsync()).ToHashSet(); if (otherProfilesPath.Exists)
{
SelectedProfiles = (await otherProfilesPath.ReadAllLinesAsync()).ToHashSet();
}
SelectedProfiles.Add(MO2Profile!); SelectedProfiles.Add(MO2Profile!);
Info("Using Profiles: " + string.Join(", ", SelectedProfiles.OrderBy(p => p))); Info("Using Profiles: " + string.Join(", ", SelectedProfiles.OrderBy(p => p)));
Utils.Log($"Compiling Game: {CompilingGame}"); Utils.Log($"Compiling Game: {CompilingGame.Game}");
Utils.Log($"Games from setting files:"); Utils.Log("Games from setting files:");
foreach (var game in Settings.IncludedGames) foreach (var game in Settings.IncludedGames)
{ {
Utils.Log($"- {game}"); Utils.Log($"- {game}");
} }
Utils.Log($"VFS File Location: {VFSCacheName}"); Utils.Log($"VFS File Location: {VFSCacheName}");
Utils.Log($"MO2 Folder: {MO2Folder}"); Utils.Log($"MO2 Folder: {SourcePath}");
Utils.Log($"Downloads Folder: {MO2DownloadsFolder}"); Utils.Log($"Downloads Folder: {DownloadsPath}");
Utils.Log($"Game Folder: {GamePath}"); Utils.Log($"Game Folder: {GamePath}");
var watcher = new DiskSpaceWatcher(cancel, new []{MO2Folder, MO2DownloadsFolder, GamePath, AbsolutePath.EntryPoint}, (long)2 << 31, var watcher = new DiskSpaceWatcher(cancel,
new[] {SourcePath, DownloadsPath, GamePath, AbsolutePath.EntryPoint}, (long)2 << 31,
drive => drive =>
{ {
Utils.Log($"Aborting due to low space on {drive.Name}"); Utils.Log($"Aborting due to low space on {drive.Name}");
@ -120,40 +98,42 @@ namespace Wabbajack.Lib
}); });
var watcherTask = watcher.Start(); var watcherTask = watcher.Start();
if (cancel.IsCancellationRequested) return false; if (cancel.IsCancellationRequested)
{
return false;
}
List<AbsolutePath> roots; List<AbsolutePath> roots;
if (UseGamePaths) if (UseGamePaths)
{ {
roots = new List<AbsolutePath> roots = new List<AbsolutePath> {SourcePath, GamePath, DownloadsPath};
{
MO2Folder, GamePath, MO2DownloadsFolder
};
roots.AddRange(Settings.IncludedGames.Select(g => g.MetaData().GameLocation())); roots.AddRange(Settings.IncludedGames.Select(g => g.MetaData().GameLocation()));
} }
else else
{ {
roots = new List<AbsolutePath> roots = new List<AbsolutePath> {SourcePath, DownloadsPath};
{
MO2Folder, MO2DownloadsFolder
};
} }
// TODO: make this generic so we can add more paths // TODO: make this generic so we can add more paths
var lootPath = (AbsolutePath)Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), var lootPath = (AbsolutePath)Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LOOT"); "LOOT");
IEnumerable<RawSourceFile> lootFiles = new List<RawSourceFile>(); IEnumerable<RawSourceFile> lootFiles = new List<RawSourceFile>();
if (lootPath.Exists) if (lootPath.Exists)
{ {
roots.Add((AbsolutePath)lootPath); roots.Add(lootPath);
} }
UpdateTracker.NextStep("Indexing folders"); UpdateTracker.NextStep("Indexing folders");
if (cancel.IsCancellationRequested) return false; if (cancel.IsCancellationRequested)
{
return false;
}
await VFS.AddRoots(roots); await VFS.AddRoots(roots);
if (lootPath.Exists) if (lootPath.Exists)
{ {
if (CompilingGame.MO2Name == null) if (CompilingGame.MO2Name == null)
@ -161,7 +141,7 @@ namespace Wabbajack.Lib
throw new ArgumentException("Compiling game had no MO2 name specified."); throw new ArgumentException("Compiling game had no MO2 name specified.");
} }
var lootGameDirs = new [] var lootGameDirs = new[]
{ {
CompilingGame.MO2Name, // most of the games use the MO2 name CompilingGame.MO2Name, // most of the games use the MO2 name
CompilingGame.MO2Name.Replace(" ", "") //eg: Fallout 4 -> Fallout4 CompilingGame.MO2Name.Replace(" ", "") //eg: Fallout 4 -> Fallout4
@ -180,70 +160,58 @@ namespace Wabbajack.Lib
Consts.LOOTFolderFilesDir.Combine(p.RelativeTo(lootPath)))); Consts.LOOTFolderFilesDir.Combine(p.RelativeTo(lootPath))));
if (!lootFiles.Any()) if (!lootFiles.Any())
{
Utils.Log( Utils.Log(
$"Found no LOOT user data for {CompilingGame.HumanFriendlyGameName} at {lootGameDir}!"); $"Found no LOOT user data for {CompilingGame.HumanFriendlyGameName} at {lootGameDir}!");
}
} }
} }
if (cancel.IsCancellationRequested) return false; if (cancel.IsCancellationRequested)
{
return false;
}
UpdateTracker.NextStep("Cleaning output folder"); UpdateTracker.NextStep("Cleaning output folder");
await ModListOutputFolder.DeleteDirectory(); await ModListOutputFolder.DeleteDirectory();
if (cancel.IsCancellationRequested) return false; if (cancel.IsCancellationRequested)
{
return false;
}
UpdateTracker.NextStep("Inferring metas for game file downloads"); UpdateTracker.NextStep("Inferring metas for game file downloads");
await InferMetas(); await InferMetas();
if (cancel.IsCancellationRequested) return false; if (cancel.IsCancellationRequested)
{
return false;
}
UpdateTracker.NextStep("Reindexing downloads after meta inferring"); UpdateTracker.NextStep("Reindexing downloads after meta inferring");
await VFS.AddRoot(MO2DownloadsFolder); await VFS.AddRoot(DownloadsPath);
if (cancel.IsCancellationRequested) return false; if (cancel.IsCancellationRequested)
{
return false;
}
UpdateTracker.NextStep("Pre-validating Archives"); UpdateTracker.NextStep("Pre-validating Archives");
// Find all Downloads // Find all Downloads
IndexedArchives = (await MO2DownloadsFolder.EnumerateFiles() IndexedArchives = (await DownloadsPath.EnumerateFiles()
.Where(f => f.WithExtension(Consts.MetaFileExtension).Exists) .Where(f => f.WithExtension(Consts.MetaFileExtension).Exists)
.PMap(Queue, async f => new IndexedArchive(VFS.Index.ByRootPath[f]) .PMap(Queue,
{ async f => new IndexedArchive(VFS.Index.ByRootPath[f])
Name = (string)f.FileName,
IniData = f.WithExtension(Consts.MetaFileExtension).LoadIniFile(),
Meta = await f.WithExtension(Consts.MetaFileExtension).ReadAllTextAsync()
})).ToList();
if (UseGamePaths)
{
foreach (var ag in Settings.IncludedGames)
{
try
{ {
var files = await ClientAPI.GetExistingGameFiles(Queue, ag); Name = (string)f.FileName,
Utils.Log($"Including {files.Length} stock game files from {ag} as download sources"); IniData = f.WithExtension(Consts.MetaFileExtension).LoadIniFile(),
GameHashes[ag] = files.Select(f => f.Hash).ToHashSet(); Meta = await f.WithExtension(Consts.MetaFileExtension).ReadAllTextAsync()
})).ToList();
IndexedArchives.AddRange(files.Select(f =>
{
var meta = f.State.GetMetaIniString();
var ini = meta.LoadIniString();
var state = (GameFileSourceDownloader.State)f.State;
return new IndexedArchive(
VFS.Index.ByRootPath[ag.MetaData().GameLocation().Combine(state.GameFile)])
{
IniData = ini, Meta = meta,
};
}));
}
catch (Exception e)
{
Utils.Error(e, "Unable to find existing game files, skipping.");
}
}
GamesWithHashes = GameHashes.SelectMany(g => g.Value.Select(h => (g, h))) await IndexGameFileHashes();
.GroupBy(gh => gh.h)
.ToDictionary(gh => gh.Key, gh => gh.Select(p => p.g.Key).ToArray());
}
IndexedArchives = IndexedArchives.DistinctBy(a => a.File.AbsoluteName).ToList(); IndexedArchives = IndexedArchives.DistinctBy(a => a.File.AbsoluteName).ToList();
@ -252,14 +220,16 @@ namespace Wabbajack.Lib
UpdateTracker.NextStep("Finding Install Files"); UpdateTracker.NextStep("Finding Install Files");
ModListOutputFolder.CreateDirectory(); ModListOutputFolder.CreateDirectory();
var mo2Files = MO2Folder.EnumerateFiles() var mo2Files = SourcePath.EnumerateFiles()
.Where(p => p.IsFile) .Where(p => p.IsFile)
.Select(p => .Select(p =>
{ {
if (!VFS.Index.ByRootPath.ContainsKey(p)) if (!VFS.Index.ByRootPath.ContainsKey(p))
{
Utils.Log($"WELL THERE'S YOUR PROBLEM: {p} {VFS.Index.ByRootPath.Count}"); Utils.Log($"WELL THERE'S YOUR PROBLEM: {p} {VFS.Index.ByRootPath.Count}");
}
return new RawSourceFile(VFS.Index.ByRootPath[p], p.RelativeTo(MO2Folder));
return new RawSourceFile(VFS.Index.ByRootPath[p], p.RelativeTo(SourcePath));
}); });
// If Game Folder Files exists, ignore the game folder // If Game Folder Files exists, ignore the game folder
@ -274,14 +244,19 @@ namespace Wabbajack.Lib
Info($"Found {AllFiles.Count} files to build into mod list"); Info($"Found {AllFiles.Count} files to build into mod list");
if (cancel.IsCancellationRequested) return false; if (cancel.IsCancellationRequested)
{
return false;
}
UpdateTracker.NextStep("Verifying destinations"); UpdateTracker.NextStep("Verifying destinations");
var dups = AllFiles.GroupBy(f => f.Path) var dups = AllFiles.GroupBy(f => f.Path)
.Where(fs => fs.Count() > 1) .Where(fs => fs.Count() > 1)
.Select(fs => .Select(fs =>
{ {
Utils.Log($"Duplicate files installed to {fs.Key} from : {String.Join(", ", fs.Select(f => f.AbsolutePath))}"); Utils.Log(
$"Duplicate files installed to {fs.Key} from : {String.Join(", ", fs.Select(f => f.AbsolutePath))}");
return fs; return fs;
}).ToList(); }).ToList();
@ -290,10 +265,14 @@ namespace Wabbajack.Lib
Error($"Found {dups.Count} duplicates, exiting"); Error($"Found {dups.Count} duplicates, exiting");
} }
if (cancel.IsCancellationRequested) return false; if (cancel.IsCancellationRequested)
{
return false;
}
UpdateTracker.NextStep("Loading INIs"); UpdateTracker.NextStep("Loading INIs");
ModInis.SetTo(MO2Folder.Combine(Consts.MO2ModFolderName) ModInis.SetTo(SourcePath.Combine(Consts.MO2ModFolderName)
.EnumerateDirectories() .EnumerateDirectories()
.Select(f => .Select(f =>
{ {
@ -306,19 +285,30 @@ namespace Wabbajack.Lib
ArchivesByFullPath = IndexedArchives.ToDictionary(a => a.File.AbsoluteName); ArchivesByFullPath = IndexedArchives.ToDictionary(a => a.File.AbsoluteName);
if (cancel.IsCancellationRequested) return false; if (cancel.IsCancellationRequested)
{
return false;
}
var stack = MakeStack(); var stack = MakeStack();
UpdateTracker.NextStep("Running Compilation Stack"); UpdateTracker.NextStep("Running Compilation Stack");
var results = await AllFiles.PMap(Queue, UpdateTracker, f => RunStack(stack, f)); var results = await AllFiles.PMap(Queue, UpdateTracker, f => RunStack(stack, f));
// Add the extra files that were generated by the stack // Add the extra files that were generated by the stack
if (cancel.IsCancellationRequested) return false; if (cancel.IsCancellationRequested)
{
return false;
}
UpdateTracker.NextStep($"Adding {ExtraFiles.Count} that were generated by the stack"); UpdateTracker.NextStep($"Adding {ExtraFiles.Count} that were generated by the stack");
results = results.Concat(ExtraFiles).ToArray(); results = results.Concat(ExtraFiles).ToArray();
var noMatch = results.OfType<NoMatch>().ToArray(); var noMatch = results.OfType<NoMatch>().ToArray();
PrintNoMatches(noMatch); PrintNoMatches(noMatch);
if (CheckForNoMatchExit(noMatch)) return false; if (CheckForNoMatchExit(noMatch))
{
return false;
}
foreach (var ignored in results.OfType<IgnoredDirectly>()) foreach (var ignored in results.OfType<IgnoredDirectly>())
{ {
@ -334,10 +324,10 @@ namespace Wabbajack.Lib
UpdateTracker.NextStep("Building Patches"); UpdateTracker.NextStep("Building Patches");
await BuildPatches(); await BuildPatches();
UpdateTracker.NextStep("Gathering Archives"); UpdateTracker.NextStep("Gathering Archives");
await GatherArchives(); await GatherArchives();
UpdateTracker.NextStep("Including Archive Metadata"); UpdateTracker.NextStep("Including Archive Metadata");
await IncludeArchiveMetadata(); await IncludeArchiveMetadata();
@ -357,10 +347,10 @@ namespace Wabbajack.Lib
Readme = ModlistReadme ?? "", Readme = ModlistReadme ?? "",
Image = ModListImage != default ? ModListImage.FileName : default, Image = ModListImage != default ? ModListImage.FileName : default,
Website = !string.IsNullOrWhiteSpace(ModListWebsite) ? new Uri(ModListWebsite) : null, Website = !string.IsNullOrWhiteSpace(ModListWebsite) ? new Uri(ModListWebsite) : null,
Version = ModlistVersion ?? new Version(1,0,0,0), Version = ModlistVersion ?? new Version(1, 0, 0, 0),
IsNSFW = ModlistIsNSFW IsNSFW = ModlistIsNSFW
}; };
UpdateTracker.NextStep("Including required files"); UpdateTracker.NextStep("Including required files");
await InlineFiles(); await InlineFiles();
@ -381,89 +371,17 @@ namespace Wabbajack.Lib
return true; return true;
} }
public Dictionary<Game, HashSet<Hash>> GameHashes { get; set; } = new Dictionary<Game, HashSet<Hash>>();
public Dictionary<Hash, Game[]> GamesWithHashes { get; set; } = new Dictionary<Hash, Game[]>();
public bool UseGamePaths { get; set; } = true;
private async Task CleanInvalidArchivesAndFillState()
{
var remove = (await IndexedArchives.PMap(Queue, async a =>
{
try
{
a.State = (await ResolveArchive(a)).State;
return null;
}
catch
{
return a;
}
})).NotNull().ToHashSet();
if (remove.Count == 0)
return;
Utils.Log(
$"Removing {remove.Count} archives from the compilation state, this is probably not an issue but reference this if you have compilation failures");
remove.Do(r => Utils.Log($"Resolution failed for: ({r.File.Size} {r.File.Hash}) {r.File.FullPath}"));
IndexedArchives.RemoveAll(a => remove.Contains(a));
}
private async Task InferMetas()
{
async Task<bool> HasInvalidMeta(AbsolutePath filename)
{
var metaname = filename.WithExtension(Consts.MetaFileExtension);
if (!metaname.Exists) return true;
try
{
return await DownloadDispatcher.ResolveArchive(metaname.LoadIniFile()) == null;
}
catch (Exception e)
{
Utils.ErrorThrow(e, $"Exception while checking meta {filename}");
return false;
}
}
var to_find = (await MO2DownloadsFolder.EnumerateFiles()
.Where(f => f.Extension != Consts.MetaFileExtension && f.Extension !=Consts.HashFileExtension)
.PMap(Queue, async f => await HasInvalidMeta(f) ? f : default))
.Where(f => f.Exists)
.ToList();
if (to_find.Count == 0) return;
Utils.Log($"Attempting to infer {to_find.Count} metas from the server.");
await to_find.PMap(Queue, async f =>
{
var vf = VFS.Index.ByRootPath[f];
var meta = await ClientAPI.InferDownloadState(vf.Hash);
if (meta == null)
{
await vf.AbsoluteName.WithExtension(Consts.MetaFileExtension).WriteAllLinesAsync(
"[General]",
"unknownArchive=true");
return;
}
Utils.Log($"Inferred .meta for {vf.FullPath.FileName}, writing to disk");
await vf.AbsoluteName.WithExtension(Consts.MetaFileExtension).WriteAllTextAsync(meta.GetMetaIniString());
});
}
private async Task IncludeArchiveMetadata() private async Task IncludeArchiveMetadata()
{ {
Utils.Log($"Including {SelectedArchives.Count} .meta files for downloads"); Utils.Log($"Including {SelectedArchives.Count} .meta files for downloads");
await SelectedArchives.PMap(Queue, async a => await SelectedArchives.PMap(Queue, async a =>
{ {
if (a.State is GameFileSourceDownloader.State) return; if (a.State is GameFileSourceDownloader.State)
{
var source = MO2DownloadsFolder.Combine(a.Name + Consts.MetaFileExtension); return;
}
var source = DownloadsPath.Combine(a.Name + Consts.MetaFileExtension);
var ini = a.State.GetMetaIniString(); var ini = a.State.GetMetaIniString();
var (id, fullPath) = await IncludeString(ini); var (id, fullPath) = await IncludeString(ini);
InstallDirectives.Add(new ArchiveMeta InstallDirectives.Add(new ArchiveMeta
@ -487,111 +405,9 @@ namespace Wabbajack.Lib
ExtraFiles = new ConcurrentBag<Directive>(); ExtraFiles = new ConcurrentBag<Directive>();
} }
/// <summary>
/// Fills in the Patch fields in files that require them
/// </summary>
private async Task BuildPatches()
{
Info("Gathering patch files");
var toBuild = InstallDirectives.OfType<PatchedFromArchive>()
.Where(p => p.Choices.Length > 0)
.SelectMany(p => p.Choices.Select(c => new PatchedFromArchive
{
To = p.To,
Hash = p.Hash,
ArchiveHashPath = c.MakeRelativePaths(),
FromFile = c,
Size = p.Size,
}))
.ToArray();
if (toBuild.Length == 0) return;
// Extract all the source files
var indexed = toBuild.GroupBy(f => (VFS.Index.FileForArchiveHashPath(f.ArchiveHashPath)))
.ToDictionary(f => f.Key);
await VFS.Extract(Queue, indexed.Keys.ToHashSet(),
async (vf, sf) =>
{
// For each, extract the destination
var matches = indexed[vf];
using var iqueue = new WorkQueue(1);
foreach (var match in matches)
{
var destFile = FindDestFile(match.To);
// Build the patch
await VFS.Extract(iqueue, new[] {destFile}.ToHashSet(),
async (destvf, destsfn) =>
{
Info($"Patching {match.To}");
Status($"Patching {match.To}");
await using var srcStream = await sf.GetStream();
await using var destStream = await destsfn.GetStream();
var patchSize = await Utils.CreatePatchCached(srcStream, vf.Hash, destStream, destvf.Hash);
Info($"Patch size {patchSize} for {match.To}");
});
}
});
// Load in the patches
await InstallDirectives.OfType<PatchedFromArchive>()
.Where(p => p.PatchID == default)
.PMap(Queue, async pfa =>
{
var patches = pfa.Choices
.Select(c => (Utils.TryGetPatch(c.Hash, pfa.Hash, out var data), data, c))
.ToArray();
// Pick the best patch
if (patches.All(p => p.Item1))
{
var (_, bytes, file) = IncludePatches.PickPatch(this, patches);
pfa.FromFile = file;
pfa.FromHash = file.Hash;
pfa.ArchiveHashPath = file.MakeRelativePaths();
pfa.PatchID = await IncludeFile(bytes!);
}
});
var firstFailedPatch = InstallDirectives.OfType<PatchedFromArchive>().FirstOrDefault(f => f.PatchID == default);
if (firstFailedPatch != null)
{
Utils.Log($"Missing data from failed patch, starting data dump");
Utils.Log($"Dest File: {firstFailedPatch.To}");
Utils.Log($"Options ({firstFailedPatch.Choices.Length}:");
foreach (var choice in firstFailedPatch.Choices)
{
Utils.Log($" {choice.FullPath}");
}
Error(
$"Missing patches after generation, this should not happen. First failure: {firstFailedPatch.FullPath}");
}
}
private VirtualFile FindDestFile(RelativePath to)
{
var abs = to.RelativeTo(MO2Folder);
if (abs.Exists)
return VFS.Index.ByRootPath[abs];
if (to.StartsWith(Consts.BSACreationDir))
{
var bsaId = (RelativePath)((string)to).Split('\\')[1];
var bsa = InstallDirectives.OfType<CreateBSA>().First(b => b.TempID == bsaId);
var find = (RelativePath)Path.Combine(((string)to).Split('\\').Skip(2).ToArray());
return VFS.Index.ByRootPath[MO2Folder.Combine(bsa.To)].Children.First(c => c.RelativeName == find);
}
throw new ArgumentException($"Couldn't load data for {to}");
}
public override IEnumerable<ICompilationStep> GetStack() public override IEnumerable<ICompilationStep> GetStack()
{ {
return MakeStack(); return MakeStack();
} }
/// <summary> /// <summary>
@ -609,12 +425,12 @@ namespace Wabbajack.Lib
new IncludePropertyFiles(this), new IncludePropertyFiles(this),
//new IncludeSteamWorkshopItems(this), //new IncludeSteamWorkshopItems(this),
new IgnoreSaveFiles(this), new IgnoreSaveFiles(this),
new IgnoreStartsWith(this,"logs\\"), new IgnoreStartsWith(this, "logs\\"),
new IgnoreStartsWith(this, "downloads\\"), new IgnoreStartsWith(this, "downloads\\"),
new IgnoreStartsWith(this,"webcache\\"), new IgnoreStartsWith(this, "webcache\\"),
new IgnoreStartsWith(this, "overwrite\\"), new IgnoreStartsWith(this, "overwrite\\"),
new IgnoreStartsWith(this, "crashDumps\\"), new IgnoreStartsWith(this, "crashDumps\\"),
new IgnorePathContains(this,"temporary_logs"), new IgnorePathContains(this, "temporary_logs"),
new IgnorePathContains(this, "GPUCache"), new IgnorePathContains(this, "GPUCache"),
new IgnorePathContains(this, "SSEEdit Cache"), new IgnorePathContains(this, "SSEEdit Cache"),
new IgnoreOtherProfiles(this), new IgnoreOtherProfiles(this),
@ -625,7 +441,7 @@ namespace Wabbajack.Lib
new IncludeLootFiles(this), new IncludeLootFiles(this),
new IgnoreStartsWith(this, Path.Combine((string)Consts.GameFolderFilesDir, "Data")), new IgnoreStartsWith(this, Path.Combine((string)Consts.GameFolderFilesDir, "Data")),
new IgnoreStartsWith(this, Path.Combine((string)Consts.GameFolderFilesDir, "Papyrus Compiler")), new IgnoreStartsWith(this, Path.Combine((string)Consts.GameFolderFilesDir, "Papyrus Compiler")),
new IgnoreStartsWith(this, Path.Combine((string)Consts.GameFolderFilesDir, "Skyrim")), new IgnoreStartsWith(this, Path.Combine((string)Consts.GameFolderFilesDir, "Skyrim")),
new IgnoreRegex(this, Consts.GameFolderFilesDir + "\\\\.*\\.bsa"), new IgnoreRegex(this, Consts.GameFolderFilesDir + "\\\\.*\\.bsa"),
new IncludeRegex(this, "^[^\\\\]*\\.bat$"), new IncludeRegex(this, "^[^\\\\]*\\.bat$"),
new IncludeModIniData(this), new IncludeModIniData(this),
@ -633,7 +449,8 @@ namespace Wabbajack.Lib
new IncludeTaggedMods(this, Consts.WABBAJACK_INCLUDE), new IncludeTaggedMods(this, Consts.WABBAJACK_INCLUDE),
new IgnoreEndsWith(this, ".pyc"), new IgnoreEndsWith(this, ".pyc"),
new IgnoreEndsWith(this, ".log"), new IgnoreEndsWith(this, ".log"),
new DeconstructBSAs(this), // Deconstruct BSAs before building patches so we don't generate massive patch files new DeconstructBSAs(
this), // Deconstruct BSAs before building patches so we don't generate massive patch files
new IncludePatches(this), new IncludePatches(this),
new IncludeDummyESPs(this), new IncludeDummyESPs(this),
@ -647,12 +464,11 @@ namespace Wabbajack.Lib
// Theme file MO2 downloads somehow // Theme file MO2 downloads somehow
new IgnoreEndsWith(this, "splash.png"), new IgnoreEndsWith(this, "splash.png"),
// File to force MO2 into portable mode // File to force MO2 into portable mode
new IgnoreEndsWith(this, "portable.txt"), new IgnoreEndsWith(this, "portable.txt"),
new IgnoreEndsWith(this, ".bin"), new IgnoreEndsWith(this, ".bin"),
new IgnoreEndsWith(this, ".refcache"), new IgnoreEndsWith(this, ".refcache"),
//Include custom categories //Include custom categories
new IncludeRegex(this, "categories.dat$"), new IncludeRegex(this, "categories.dat$"),
new IgnoreWabbajackInstallCruft(this), new IgnoreWabbajackInstallCruft(this),
//new PatchStockESMs(this), //new PatchStockESMs(this),
@ -660,7 +476,6 @@ namespace Wabbajack.Lib
new IncludeAllConfigs(this), new IncludeAllConfigs(this),
new zEditIntegration.IncludeZEditPatches(this), new zEditIntegration.IncludeZEditPatches(this),
new IncludeTaggedMods(this, Consts.WABBAJACK_NOMATCH_INCLUDE), new IncludeTaggedMods(this, Consts.WABBAJACK_NOMATCH_INCLUDE),
new DropAll(this) new DropAll(this)
}; };
} }

View File

@ -0,0 +1,289 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Lib.CompilationSteps;
using Wabbajack.Lib.Validation;
using Wabbajack.VirtualFileSystem;
namespace Wabbajack.Lib
{
public class NativeCompiler : ACompiler
{
public NativeCompiler(NativeCompilerSettings settings, AbsolutePath sourcePath, AbsolutePath downloadsPath, AbsolutePath outputModListPath)
: base(3, settings.ModListName, sourcePath, downloadsPath, outputModListPath)
{
CompilingGame = settings.CompilingGame.MetaData();
GamePath = CompilingGame.GameLocation();
NativeSettings = settings;
}
public NativeCompilerSettings NativeSettings { get; set; }
protected override async Task<bool> _Begin(CancellationToken cancel)
{
await Metrics.Send("begin_compiling", ModListName ?? "unknown");
if (cancel.IsCancellationRequested)
{
return false;
}
DesiredThreads.OnNext(DiskThreads);
FileExtractor2.FavorPerfOverRAM = FavorPerfOverRam;
UpdateTracker.Reset();
UpdateTracker.NextStep("Gathering information");
Utils.Log($"Compiling Game: {CompilingGame.Game}");
Utils.Log("Games from setting files:");
foreach (var game in Settings.IncludedGames)
{
Utils.Log($"- {game}");
}
Utils.Log($"VFS File Location: {VFSCacheName}");
Utils.Log($"MO2 Folder: {SourcePath}");
Utils.Log($"Downloads Folder: {DownloadsPath}");
Utils.Log($"Game Folder: {GamePath}");
var watcher = new DiskSpaceWatcher(cancel,
new[] {SourcePath, DownloadsPath, GamePath, AbsolutePath.EntryPoint}, (long)2 << 31,
drive =>
{
Utils.Log($"Aborting due to low space on {drive.Name}");
Abort();
});
var watcherTask = watcher.Start();
if (cancel.IsCancellationRequested)
{
return false;
}
List<AbsolutePath> roots = new List<AbsolutePath> {SourcePath, GamePath, DownloadsPath};
roots.AddRange(Settings.IncludedGames.Select(g => g.MetaData().GameLocation()));
UpdateTracker.NextStep("Indexing folders");
if (cancel.IsCancellationRequested)
{
return false;
}
await VFS.AddRoots(roots);
UpdateTracker.NextStep("Cleaning output folder");
await ModListOutputFolder.DeleteDirectory();
if (cancel.IsCancellationRequested)
{
return false;
}
UpdateTracker.NextStep("Inferring metas for game file downloads");
await InferMetas();
if (cancel.IsCancellationRequested)
{
return false;
}
UpdateTracker.NextStep("Reindexing downloads after meta inferring");
await VFS.AddRoot(DownloadsPath);
if (cancel.IsCancellationRequested)
{
return false;
}
UpdateTracker.NextStep("Pre-validating Archives");
// Find all Downloads
IndexedArchives = (await DownloadsPath.EnumerateFiles()
.Where(f => f.WithExtension(Consts.MetaFileExtension).Exists)
.PMap(Queue,
async f => new IndexedArchive(VFS.Index.ByRootPath[f])
{
Name = (string)f.FileName,
IniData = f.WithExtension(Consts.MetaFileExtension).LoadIniFile(),
Meta = await f.WithExtension(Consts.MetaFileExtension).ReadAllTextAsync()
})).ToList();
await IndexGameFileHashes();
IndexedArchives = IndexedArchives.DistinctBy(a => a.File.AbsoluteName).ToList();
await CleanInvalidArchivesAndFillState();
UpdateTracker.NextStep("Finding Install Files");
ModListOutputFolder.CreateDirectory();
var mo2Files = SourcePath.EnumerateFiles()
.Where(p => p.IsFile)
.Select(p =>
{
if (!VFS.Index.ByRootPath.ContainsKey(p))
{
Utils.Log($"WELL THERE'S YOUR PROBLEM: {p} {VFS.Index.ByRootPath.Count}");
}
return new RawSourceFile(VFS.Index.ByRootPath[p], p.RelativeTo(SourcePath));
});
// If Game Folder Files exists, ignore the game folder
IndexedFiles = IndexedArchives.SelectMany(f => f.File.ThisAndAllChildren)
.OrderBy(f => f.NestingFactor)
.GroupBy(f => f.Hash)
.ToDictionary(f => f.Key, f => f.AsEnumerable());
AllFiles.SetTo(mo2Files
.DistinctBy(f => f.Path));
Info($"Found {AllFiles.Count} files to build into mod list");
if (cancel.IsCancellationRequested)
{
return false;
}
UpdateTracker.NextStep("Verifying destinations");
var dups = AllFiles.GroupBy(f => f.Path)
.Where(fs => fs.Count() > 1)
.Select(fs =>
{
Utils.Log(
$"Duplicate files installed to {fs.Key} from : {String.Join(", ", fs.Select(f => f.AbsolutePath))}");
return fs;
}).ToList();
if (dups.Count > 0)
{
Error($"Found {dups.Count} duplicates, exiting");
}
if (cancel.IsCancellationRequested)
{
return false;
}
UpdateTracker.NextStep("Loading INIs");
ArchivesByFullPath = IndexedArchives.ToDictionary(a => a.File.AbsoluteName);
if (cancel.IsCancellationRequested)
{
return false;
}
var stack = MakeStack();
UpdateTracker.NextStep("Running Compilation Stack");
var results = await AllFiles.PMap(Queue, UpdateTracker, f => RunStack(stack, f));
// Add the extra files that were generated by the stack
if (cancel.IsCancellationRequested)
{
return false;
}
var noMatch = results.OfType<NoMatch>().ToArray();
PrintNoMatches(noMatch);
if (CheckForNoMatchExit(noMatch))
{
return false;
}
foreach (var ignored in results.OfType<IgnoredDirectly>())
{
Utils.Log($"Ignored {ignored.To} because {ignored.Reason}");
}
InstallDirectives.SetTo(results.Where(i => !(i is IgnoredDirectly)));
Info("Getting Nexus api_key, please click authorize if a browser window appears");
UpdateTracker.NextStep("Building Patches");
await BuildPatches();
UpdateTracker.NextStep("Gathering Archives");
await GatherArchives();
UpdateTracker.NextStep("Gathering Metadata");
await GatherMetaData();
ModList = new ModList
{
GameType = CompilingGame.Game,
WabbajackVersion = Consts.CurrentMinimumWabbajackVersion,
Archives = SelectedArchives.ToList(),
ModManager = ModManager.MO2,
Directives = InstallDirectives,
Name = ModListName ?? "untitled",
Author = ModListAuthor ?? "",
Description = ModListDescription ?? "",
Readme = ModlistReadme ?? "",
Image = ModListImage != default ? ModListImage.FileName : default,
Website = !string.IsNullOrWhiteSpace(ModListWebsite) ? new Uri(ModListWebsite) : null,
Version = ModlistVersion ?? new Version(1, 0, 0, 0),
IsNSFW = ModlistIsNSFW
};
UpdateTracker.NextStep("Including required files");
await InlineFiles();
UpdateTracker.NextStep("Running Validation");
await ValidateModlist.RunValidation(ModList);
UpdateTracker.NextStep("Generating Report");
GenerateManifest();
UpdateTracker.NextStep("Exporting Modlist");
await ExportModList();
ResetMembers();
UpdateTracker.NextStep("Done Building Modlist");
return true;
}
/// <summary>
/// Clear references to lists that hold a lot of data.
/// </summary>
private void ResetMembers()
{
AllFiles = new List<RawSourceFile>();
InstallDirectives = new List<Directive>();
SelectedArchives = new List<Archive>();
}
public override AbsolutePath GamePath { get; }
public override IEnumerable<ICompilationStep> GetStack()
{
return MakeStack();
}
public override IEnumerable<ICompilationStep> MakeStack()
{
List<ICompilationStep> steps = NativeSettings.CompilationSteps.Select(InterpretStep).ToList();
steps.Add(new DropAll(this));
return steps;
}
public ICompilationStep InterpretStep(string[] step)
{
return step[0] switch
{
"IgnoreStartsWith" => new IgnoreStartsWith(this, step[1]),
"IncludeConfigs" => new IncludeAllConfigs(this),
"IncludeDirectMatches" => new DirectMatch(this),
"IncludePatches" => new IncludePatches(this),
_ => throw new ArgumentException($"No interpretation for step {step[0]}")
};
}
}
}

View File

@ -0,0 +1,14 @@
using Wabbajack.Common;
namespace Wabbajack.Lib
{
public class NativeCompilerSettings : CompilerSettings
{
public Game CompilingGame { get; set; }
public string ModListName { get; set; } = "untitled";
public string[][] CompilationSteps = new string[0][];
}
}

View File

@ -50,21 +50,21 @@ namespace Wabbajack.Lib
return false; return false;
} }
if (settings.managerPath != _mo2Compiler.MO2Folder) if (settings.managerPath != _mo2Compiler.SourcePath)
{ {
Utils.Log($"zEdit settings file {f}: managerPath is not {_mo2Compiler.MO2Folder} but {settings.managerPath}!"); Utils.Log($"zEdit settings file {f}: managerPath is not {_mo2Compiler.SourcePath} but {settings.managerPath}!");
return false; return false;
} }
if (settings.modsPath != _mo2Compiler.MO2Folder.Combine(Consts.MO2ModFolderName)) if (settings.modsPath != _mo2Compiler.SourcePath.Combine(Consts.MO2ModFolderName))
{ {
Utils.Log($"zEdit settings file {f}: modsPath is not {_mo2Compiler.MO2Folder}\\{Consts.MO2ModFolderName} but {settings.modsPath}!"); Utils.Log($"zEdit settings file {f}: modsPath is not {_mo2Compiler.SourcePath}\\{Consts.MO2ModFolderName} but {settings.modsPath}!");
return false; return false;
} }
if (settings.mergePath != _mo2Compiler.MO2Folder.Combine(Consts.MO2ModFolderName)) if (settings.mergePath != _mo2Compiler.SourcePath.Combine(Consts.MO2ModFolderName))
{ {
Utils.Log($"zEdit settings file {f}: modsPath is not {_mo2Compiler.MO2Folder}\\{Consts.MO2ModFolderName} but {settings.modsPath}!"); Utils.Log($"zEdit settings file {f}: modsPath is not {_mo2Compiler.SourcePath}\\{Consts.MO2ModFolderName} but {settings.modsPath}!");
return false; return false;
} }
@ -103,7 +103,7 @@ namespace Wabbajack.Lib
_mergesIndexed = _mergesIndexed =
merges.ToDictionary( merges.ToDictionary(
m => _mo2Compiler.MO2Folder.Combine((string)Consts.MO2ModFolderName, m.Key.name, m.Key.filename), m => _mo2Compiler.SourcePath.Combine((string)Consts.MO2ModFolderName, m.Key.name, m.Key.filename),
m => m.First()); m => m.First());
_disabled = false; _disabled = false;
@ -180,12 +180,12 @@ namespace Wabbajack.Lib
return new SourcePatch return new SourcePatch
{ {
RelativePath = absPath.RelativeTo(_mo2Compiler.MO2Folder), RelativePath = absPath.RelativeTo(_mo2Compiler.SourcePath),
Hash = hash Hash = hash
}; };
})); }));
var srcData = (await result.Sources.SelectAsync(async f => await _mo2Compiler.MO2Folder.Combine(f.RelativePath).ReadAllBytesAsync()).ToList()) var srcData = (await result.Sources.SelectAsync(async f => await _mo2Compiler.SourcePath.Combine(f.RelativePath).ReadAllBytesAsync()).ToList())
.ConcatArrays(); .ConcatArrays();
var dstData = await source.AbsolutePath.ReadAllBytesAsync(); var dstData = await source.AbsolutePath.ReadAllBytesAsync();

View File

@ -36,7 +36,8 @@ namespace Wabbajack.Test
protected async Task<MO2Compiler> ConfigureAndRunCompiler(string profile, bool useGameFiles= false) protected async Task<MO2Compiler> ConfigureAndRunCompiler(string profile, bool useGameFiles= false)
{ {
var compiler = new MO2Compiler( var compiler = new MO2Compiler(
mo2Folder: utils.MO2Folder, sourcePath: utils.SourcePath,
downloadsPath: utils.DownloadsPath,
mo2Profile: profile, mo2Profile: profile,
outputFile: OutputFile(profile)); outputFile: OutputFile(profile));
compiler.UseGamePaths = useGameFiles; compiler.UseGamePaths = useGameFiles;
@ -51,13 +52,35 @@ namespace Wabbajack.Test
await Install(compiler); await Install(compiler);
return compiler.ModList; return compiler.ModList;
} }
protected async Task<NativeCompiler> ConfigureAndRunCompiler(AbsolutePath configPath, bool useGameFiles= false)
{
var settings = configPath.FromJson<NativeCompilerSettings>();
var profile = utils.AddProfile();
var compiler = new NativeCompiler(
settings: settings,
sourcePath: utils.SourcePath,
downloadsPath: utils.DownloadsPath,
outputModListPath: OutputFile(profile))
{UseGamePaths = useGameFiles};
Assert.True(await compiler.Begin());
return compiler;
}
protected async Task<ModList> CompileAndInstall(AbsolutePath settingsPath, bool useGameFiles = false)
{
var compiler = await ConfigureAndRunCompiler(settingsPath, useGameFiles: useGameFiles);
Utils.Log("Finished Compiling");
await Install(compiler);
return compiler.ModList;
}
private static AbsolutePath OutputFile(string profile) private static AbsolutePath OutputFile(string profile)
{ {
return ((RelativePath)profile).RelativeToEntryPoint().WithExtension(Consts.ModListExtension); return ((RelativePath)profile).RelativeToEntryPoint().WithExtension(Consts.ModListExtension);
} }
protected async Task Install(MO2Compiler compiler) protected async Task Install(ACompiler compiler)
{ {
Utils.Log("Loading Modlist"); Utils.Log("Loading Modlist");
var modlist = AInstaller.LoadFromFile(compiler.ModListOutputFile); var modlist = AInstaller.LoadFromFile(compiler.ModListOutputFile);
@ -65,8 +88,8 @@ namespace Wabbajack.Test
var installer = new MO2Installer( var installer = new MO2Installer(
archive: compiler.ModListOutputFile, archive: compiler.ModListOutputFile,
modList: modlist, modList: modlist,
outputFolder: utils.InstallFolder, outputFolder: utils.InstallPath,
downloadFolder: utils.DownloadsFolder, downloadFolder: utils.DownloadsPath,
parameters: CreateDummySystemParameters()); parameters: CreateDummySystemParameters());
installer.WarnOnOverwrite = false; installer.WarnOnOverwrite = false;
installer.GameFolder = utils.GameFolder; installer.GameFolder = utils.GameFolder;

View File

@ -52,7 +52,7 @@ namespace Wabbajack.Test
await DownloadAndInstall( await DownloadAndInstall(
"https://github.com/ModOrganizer2/modorganizer/releases/download/v2.2.1/Mod.Organizer.2.2.1.7z", "https://github.com/ModOrganizer2/modorganizer/releases/download/v2.2.1/Mod.Organizer.2.2.1.7z",
"Mod.Organizer.2.2.1.7z"); "Mod.Organizer.2.2.1.7z");
await utils.DownloadsFolder.Combine("Mod.Organizer.2.2.1.7z.meta").WriteAllLinesAsync( await utils.DownloadsPath.Combine("Mod.Organizer.2.2.1.7z.meta").WriteAllLinesAsync(
"[General]", "[General]",
"directURL=https://github.com/ModOrganizer2/modorganizer/releases/download/v2.2.1/Mod.Organizer.2.2.1.7z" "directURL=https://github.com/ModOrganizer2/modorganizer/releases/download/v2.2.1/Mod.Organizer.2.2.1.7z"
); );
@ -75,7 +75,7 @@ namespace Wabbajack.Test
$"matchAll= {modfiles[2].Download.FileName}" $"matchAll= {modfiles[2].Download.FileName}"
); );
await utils.MO2Folder.Combine("startup.bat").WriteAllLinesAsync( await utils.SourcePath.Combine("startup.bat").WriteAllLinesAsync(
"ModOrganizer2.exe SKSE" "ModOrganizer2.exe SKSE"
); );
@ -83,13 +83,13 @@ namespace Wabbajack.Test
await CompileAndInstall(profile); await CompileAndInstall(profile);
await utils.VerifyAllFiles(); await utils.VerifyAllFiles();
await utils.InstallFolder.Combine(Consts.LOOTFolderFilesDir).DeleteDirectory(); await utils.InstallPath.Combine(Consts.LOOTFolderFilesDir).DeleteDirectory();
var compiler = new MO2Compiler( var compiler = new MO2Compiler(
mo2Folder: utils.InstallFolder, sourcePath: utils.InstallPath,
downloadsPath: utils.DownloadsPath,
mo2Profile: profile, mo2Profile: profile,
outputFile: profile.RelativeTo(AbsolutePath.EntryPoint).WithExtension(Consts.ModListExtension)); outputFile: profile.RelativeTo(AbsolutePath.EntryPoint).WithExtension(Consts.ModListExtension));
compiler.MO2DownloadsFolder = utils.DownloadsFolder;
Assert.True(await compiler.Begin()); Assert.True(await compiler.Begin());
} }
@ -105,12 +105,12 @@ namespace Wabbajack.Test
await state.Download(new Archive(state: null!) { Name = "Unknown"}, src); await state.Download(new Archive(state: null!) { Name = "Unknown"}, src);
} }
utils.DownloadsFolder.CreateDirectory(); utils.DownloadsPath.CreateDirectory();
var destFile = utils.DownloadsFolder.Combine(filename); var destFile = utils.DownloadsPath.Combine(filename);
await src.CopyToAsync(destFile); await src.CopyToAsync(destFile);
var modFolder = modName == null ? utils.MO2Folder : utils.ModsFolder.Combine(modName); var modFolder = modName == null ? utils.SourcePath : utils.ModsPath.Combine(modName);
await FileExtractor2.ExtractAll(Queue, src, modFolder); await FileExtractor2.ExtractAll(Queue, src, modFolder);
return (destFile, modFolder); return (destFile, modFolder);
} }
@ -140,12 +140,12 @@ namespace Wabbajack.Test
await state.Download(src); await state.Download(src);
} }
utils.DownloadsFolder.CreateDirectory(); utils.DownloadsPath.CreateDirectory();
var dest = utils.DownloadsFolder.Combine(file.file_name); var dest = utils.DownloadsPath.Combine(file.file_name);
await src.CopyToAsync(dest); await src.CopyToAsync(dest);
var modFolder = utils.ModsFolder.Combine(modName); var modFolder = utils.ModsPath.Combine(modName);
await FileExtractor2.ExtractAll(Queue, src, modFolder); await FileExtractor2.ExtractAll(Queue, src, modFolder);
await dest.WithExtension(Consts.MetaFileExtension).WriteAllTextAsync(ini); await dest.WithExtension(Consts.MetaFileExtension).WriteAllTextAsync(ini);
@ -165,8 +165,8 @@ namespace Wabbajack.Test
var installer = new MO2Installer( var installer = new MO2Installer(
archive: compiler.ModListOutputFile, archive: compiler.ModListOutputFile,
modList: modlist, modList: modlist,
outputFolder: utils.InstallFolder, outputFolder: utils.InstallPath,
downloadFolder: utils.DownloadsFolder, downloadFolder: utils.DownloadsPath,
parameters: ACompilerTest.CreateDummySystemParameters()) parameters: ACompilerTest.CreateDummySystemParameters())
{ {
UseCompression = true UseCompression = true
@ -178,7 +178,8 @@ namespace Wabbajack.Test
private async Task<MO2Compiler> ConfigureAndRunCompiler(string profile) private async Task<MO2Compiler> ConfigureAndRunCompiler(string profile)
{ {
var compiler = new MO2Compiler( var compiler = new MO2Compiler(
mo2Folder: utils.MO2Folder, sourcePath: utils.SourcePath,
downloadsPath: utils.DownloadsPath,
mo2Profile: profile, mo2Profile: profile,
outputFile: profile.RelativeTo(AbsolutePath.EntryPoint).WithExtension(Consts.ModListExtension)); outputFile: profile.RelativeTo(AbsolutePath.EntryPoint).WithExtension(Consts.ModListExtension));
Assert.True(await compiler.Begin()); Assert.True(await compiler.Begin());

View File

@ -54,7 +54,7 @@ namespace Wabbajack.Test
await utils.AddManualDownload( await utils.AddManualDownload(
new Dictionary<string, byte[]> {{"/baz/biz.pex", await testPex.ReadAllBytesAsync()}}); new Dictionary<string, byte[]> {{"/baz/biz.pex", await testPex.ReadAllBytesAsync()}});
await utils.DownloadsFolder.Combine("some_other_file.7z").WriteAllTextAsync("random data"); await utils.DownloadsPath.Combine("some_other_file.7z").WriteAllTextAsync("random data");
await CompileAndInstall(profile); await CompileAndInstall(profile);
@ -90,14 +90,14 @@ namespace Wabbajack.Test
await utils.Configure(); await utils.Configure();
utils.MO2Folder.Combine(Consts.GameFolderFilesDir).CreateDirectory(); utils.SourcePath.Combine(Consts.GameFolderFilesDir).CreateDirectory();
await utils.AddManualDownload( await utils.AddManualDownload(
new Dictionary<string, byte[]> {{"/baz/biz.pex", await testPex.ReadAllBytesAsync()}}); new Dictionary<string, byte[]> {{"/baz/biz.pex", await testPex.ReadAllBytesAsync()}});
await CompileAndInstall(profile); await CompileAndInstall(profile);
Assert.False(utils.InstallFolder.Combine(Consts.GameFolderFilesDir, (RelativePath)@"enbstuff\test.pex").IsFile); Assert.False(utils.InstallPath.Combine(Consts.GameFolderFilesDir, (RelativePath)@"enbstuff\test.pex").IsFile);
} }
[Fact] [Fact]
@ -188,12 +188,12 @@ namespace Wabbajack.Test
var profile = utils.AddProfile(); var profile = utils.AddProfile();
var mod = await utils.AddMod("dummy"); var mod = await utils.AddMod("dummy");
var saveFolder = utils.MO2Folder.Combine("profiles", profile, "saves"); var saveFolder = utils.SourcePath.Combine("profiles", profile, "saves");
saveFolder.CreateDirectory(); saveFolder.CreateDirectory();
await saveFolder.Combine("incompilation").WriteAllTextAsync("ignore this"); await saveFolder.Combine("incompilation").WriteAllTextAsync("ignore this");
var installSaveFolderThisProfile = utils.InstallFolder.Combine("profiles", profile, "saves"); var installSaveFolderThisProfile = utils.InstallPath.Combine("profiles", profile, "saves");
var installSaveFolderOtherProfile = utils.InstallFolder.Combine("profiles", "Other Profile", "saves"); var installSaveFolderOtherProfile = utils.InstallPath.Combine("profiles", "Other Profile", "saves");
installSaveFolderThisProfile.CreateDirectory(); installSaveFolderThisProfile.CreateDirectory();
installSaveFolderOtherProfile.CreateDirectory(); installSaveFolderOtherProfile.CreateDirectory();
@ -215,7 +215,7 @@ namespace Wabbajack.Test
var mod = await utils.AddMod("dummy"); var mod = await utils.AddMod("dummy");
await utils.Configure(); await utils.Configure();
await utils.MO2Folder.Combine("profiles", profile, "somegameprefs.ini").WriteAllLinesAsync( await utils.SourcePath.Combine("profiles", profile, "somegameprefs.ini").WriteAllLinesAsync(
// Beth inis are messy, let's make ours just as messy to catch some parse failures // Beth inis are messy, let's make ours just as messy to catch some parse failures
"[Display]", "[Display]",
"foo=4", "foo=4",
@ -231,7 +231,7 @@ namespace Wabbajack.Test
var modlist = await CompileAndInstall(profile); var modlist = await CompileAndInstall(profile);
var ini = utils.InstallFolder.Combine("profiles", profile, "somegameprefs.ini").LoadIniFile(); var ini = utils.InstallPath.Combine("profiles", profile, "somegameprefs.ini").LoadIniFile();
var sysinfo = CreateDummySystemParameters(); var sysinfo = CreateDummySystemParameters();
@ -511,7 +511,7 @@ namespace Wabbajack.Test
await new CompilerSettings() await new CompilerSettings()
{ {
IncludedGames = new []{Game.Morrowind} IncludedGames = new []{Game.Morrowind}
}.ToJsonAsync(utils.MO2Folder.Combine("profiles", profile, CompilerSettings.FileName), true); }.ToJsonAsync(utils.SourcePath.Combine("profiles", profile, CompilerSettings.FileName), true);
Game.SkyrimSpecialEdition.MetaData().CanSourceFrom = new[] {Game.Morrowind, Game.Skyrim}; Game.SkyrimSpecialEdition.MetaData().CanSourceFrom = new[] {Game.Morrowind, Game.Skyrim};
@ -540,7 +540,7 @@ namespace Wabbajack.Test
await utils.VerifyInstalledFile(mod, @"Data\SkyrimSE\Update.esm.old"); await utils.VerifyInstalledFile(mod, @"Data\SkyrimSE\Update.esm.old");
await utils.VerifyInstalledFile(mod, @"Data\SkyrimSE\Update.esm"); await utils.VerifyInstalledFile(mod, @"Data\SkyrimSE\Update.esm");
Assert.False(utils.InstallFolder.Combine(Consts.GameFolderFilesDir).IsDirectory); Assert.False(utils.InstallPath.Combine(Consts.GameFolderFilesDir).IsDirectory);
} }
@ -554,8 +554,8 @@ namespace Wabbajack.Test
await utils.Configure(); await utils.Configure();
utils.MO2Folder.Combine(Consts.GameFolderFilesDir).CreateDirectory(); utils.SourcePath.Combine(Consts.GameFolderFilesDir).CreateDirectory();
await utils.MO2Folder.Combine(Consts.GameFolderFilesDir).Combine("dx4242.dll") await utils.SourcePath.Combine(Consts.GameFolderFilesDir).Combine("dx4242.dll")
.WriteAllBytesAsync(utils.RandomData()); .WriteAllBytesAsync(utils.RandomData());
await utils.AddManualDownload( await utils.AddManualDownload(
@ -580,7 +580,7 @@ namespace Wabbajack.Test
var disabledMod = await utils.AddMod(); var disabledMod = await utils.AddMod();
var disabledTestPex = await utils.AddModFile(disabledMod, @"Data\scripts\disabledTestPex.pex", 10); var disabledTestPex = await utils.AddModFile(disabledMod, @"Data\scripts\disabledTestPex.pex", 10);
await disabledMod.RelativeTo(utils.ModsFolder).Combine("meta.ini").WriteAllLinesAsync( await disabledMod.RelativeTo(utils.ModsPath).Combine("meta.ini").WriteAllLinesAsync(
"[General]", "[General]",
$"notes={Consts.WABBAJACK_ALWAYS_ENABLE}"); $"notes={Consts.WABBAJACK_ALWAYS_ENABLE}");
@ -602,7 +602,7 @@ namespace Wabbajack.Test
await utils.VerifyInstalledFile(enabledMod, @"Data\scripts\enabledTestPex.pex"); await utils.VerifyInstalledFile(enabledMod, @"Data\scripts\enabledTestPex.pex");
await utils.VerifyInstalledFile(disabledMod, @"Data\scripts\disabledTestPex.pex"); await utils.VerifyInstalledFile(disabledMod, @"Data\scripts\disabledTestPex.pex");
var modlistTxt = await utils.InstallFolder.Combine("profiles", profile, "modlist.txt").ReadAllLinesAsync(); var modlistTxt = await utils.InstallPath.Combine("profiles", profile, "modlist.txt").ReadAllLinesAsync();
Assert.Equal(new string[] Assert.Equal(new string[]
{ {
$"-{disabledMod}", $"-{disabledMod}",
@ -610,5 +610,47 @@ namespace Wabbajack.Test
}, modlistTxt.ToArray()); }, modlistTxt.ToArray());
} }
[Fact]
public async Task CanCompileFromNativeSource()
{
utils.CreatePaths();
var gameFolder = Game.SkyrimSpecialEdition.MetaData().GameLocation();
await gameFolder.Combine("SkyrimSE.exe").CopyToAsync(utils.SourcePath.Combine("SkyrimSE.exe"));
var some_dds = utils.SourcePath.Combine("some_file.dds");
await some_dds.WriteAllBytesAsync(utils.RandomData());
var blerg = utils.SourcePath.Combine("file1.blerg");
await blerg.WriteAllBytesAsync(utils.RandomData());
await utils.AddManualDownload(
new Dictionary<string, byte[]>
{
{"file1.blerg", await some_dds.ReadAllBytesAsync()},
});
var settings = new NativeCompilerSettings
{
CompilingGame = Game.SkyrimSpecialEdition,
CompilationSteps = new []
{
new []{"IgnoreStartsWith", "downloads"},
new []{"IncludeConfigs"},
new []{"IncludeDirectMatches"},
new []{"IncludePatches"}
}
};
var settingsPath = utils.SourcePath.Combine("native_compiler_settings.json");
await settings.ToJsonAsync(utils.SourcePath.Combine("native_compiler_settings.json"), true);
await CompileAndInstall(settingsPath, true);
Assert.Equal(await some_dds.FileHashAsync(), await utils.InstallPath.Combine("some_file.dds").FileHashAsync());
Assert.Equal(await gameFolder.Combine("SkyrimSE.exe").FileHashAsync(),
await utils.InstallPath.Combine("SkyrimSE.exe").FileHashAsync());
}
} }
} }

View File

@ -32,11 +32,11 @@ namespace Wabbajack.Test
public AbsolutePath TestFolder => WorkingDirectory.Combine(ID); public AbsolutePath TestFolder => WorkingDirectory.Combine(ID);
public AbsolutePath GameFolder => WorkingDirectory.Combine(ID, "game_folder"); public AbsolutePath GameFolder => WorkingDirectory.Combine(ID, "game_folder");
public AbsolutePath MO2Folder => WorkingDirectory.Combine(ID, "mo2_folder"); public AbsolutePath SourcePath => WorkingDirectory.Combine(ID, "source_folder");
public AbsolutePath ModsFolder => MO2Folder.Combine(Consts.MO2ModFolderName); public AbsolutePath ModsPath => SourcePath.Combine(Consts.MO2ModFolderName);
public AbsolutePath DownloadsFolder => MO2Folder.Combine("downloads"); public AbsolutePath DownloadsPath => SourcePath.Combine("downloads");
public AbsolutePath InstallFolder => TestFolder.Combine("installed"); public AbsolutePath InstallPath => TestFolder.Combine("installed");
public HashSet<string> Profiles = new HashSet<string>(); public HashSet<string> Profiles = new HashSet<string>();
@ -44,20 +44,20 @@ namespace Wabbajack.Test
public async Task Configure(IEnumerable<(string ModName, bool IsEnabled)> enabledMods = null) public async Task Configure(IEnumerable<(string ModName, bool IsEnabled)> enabledMods = null)
{ {
await MO2Folder.Combine("ModOrganizer.ini").WriteAllLinesAsync( await SourcePath.Combine("ModOrganizer.ini").WriteAllLinesAsync(
"[General]", "[General]",
$"gameName={Game.MetaData().MO2Name}", $"gameName={Game.MetaData().MO2Name}",
$"gamePath={((string)GameFolder).Replace("\\", "\\\\")}", $"gamePath={((string)GameFolder).Replace("\\", "\\\\")}",
$"download_directory={DownloadsFolder}"); $"download_directory={DownloadsPath}");
DownloadsFolder.CreateDirectory(); DownloadsPath.CreateDirectory();
GameFolder.Combine("Data").CreateDirectory(); GameFolder.Combine("Data").CreateDirectory();
if (enabledMods == null) if (enabledMods == null)
{ {
Profiles.Do(profile => Profiles.Do(profile =>
{ {
MO2Folder.Combine("profiles", profile, "modlist.txt").WriteAllLinesAsync( SourcePath.Combine("profiles", profile, "modlist.txt").WriteAllLinesAsync(
Mods.Select(s => $"+{s}").ToArray()); Mods.Select(s => $"+{s}").ToArray());
}); });
} }
@ -65,7 +65,7 @@ namespace Wabbajack.Test
{ {
Profiles.Do(profile => Profiles.Do(profile =>
{ {
MO2Folder.Combine("profiles", profile, "modlist.txt").WriteAllLinesAsync( SourcePath.Combine("profiles", profile, "modlist.txt").WriteAllLinesAsync(
enabledMods.Select(s => $"{(s.IsEnabled ? "+" : "-")}{s.ModName}").ToArray()); enabledMods.Select(s => $"{(s.IsEnabled ? "+" : "-")}{s.ModName}").ToArray());
}); });
} }
@ -74,7 +74,7 @@ namespace Wabbajack.Test
public string AddProfile(string name = null) public string AddProfile(string name = null)
{ {
string profile_name = name ?? RandomName(); string profile_name = name ?? RandomName();
MO2Folder.Combine("profiles", profile_name).CreateDirectory(); SourcePath.Combine("profiles", profile_name).CreateDirectory();
Profiles.Add(profile_name); Profiles.Add(profile_name);
return profile_name; return profile_name;
} }
@ -82,7 +82,7 @@ namespace Wabbajack.Test
public async Task<string> AddMod(string name = null) public async Task<string> AddMod(string name = null)
{ {
string mod_name = name ?? RandomName(); string mod_name = name ?? RandomName();
var mod_folder = MO2Folder.Combine(Consts.MO2ModFolderName, (RelativePath)mod_name); var mod_folder = SourcePath.Combine(Consts.MO2ModFolderName, (RelativePath)mod_name);
mod_folder.CreateDirectory(); mod_folder.CreateDirectory();
await mod_folder.Combine("meta.ini").WriteAllTextAsync("[General]"); await mod_folder.Combine("meta.ini").WriteAllTextAsync("[General]");
Mods.Add(mod_name); Mods.Add(mod_name);
@ -99,7 +99,7 @@ namespace Wabbajack.Test
/// <returns></returns> /// <returns></returns>
public async Task<AbsolutePath> AddModFile(string mod_name, string path, int random_fill=128) public async Task<AbsolutePath> AddModFile(string mod_name, string path, int random_fill=128)
{ {
var full_path = ModsFolder.Combine(mod_name, path); var full_path = ModsPath.Combine(mod_name, path);
full_path.Parent.CreateDirectory(); full_path.Parent.CreateDirectory();
await GenerateRandomFileData(full_path, random_fill); await GenerateRandomFileData(full_path, random_fill);
return full_path; return full_path;
@ -161,16 +161,17 @@ namespace Wabbajack.Test
{ {
var name = RandomName() + ".zip"; var name = RandomName() + ".zip";
await using FileStream fs = await DownloadsFolder.Combine(name).Create(); await using FileStream fs = await DownloadsPath.Combine(name).Create();
using ZipArchive archive = new ZipArchive(fs, ZipArchiveMode.Create); using ZipArchive archive = new ZipArchive(fs, ZipArchiveMode.Create);
contents.Do(kv => foreach (var (key, value) in contents)
{ {
var entry = archive.CreateEntry(kv.Key); Utils.Log($"Adding {value.Length.ToFileSizeString()} entry {key}");
using var os = entry.Open(); var entry = archive.CreateEntry(key);
os.Write(kv.Value, 0, kv.Value.Length); await using var os = entry.Open();
}); await os.WriteAsync(value, 0, value.Length);
}
await DownloadsFolder.Combine(name + Consts.MetaFileExtension).WriteAllLinesAsync( await DownloadsPath.Combine(name + Consts.MetaFileExtension).WriteAllLinesAsync(
"[General]", "[General]",
"manualURL=<TESTING>" "manualURL=<TESTING>"
); );
@ -180,10 +181,10 @@ namespace Wabbajack.Test
public async Task VerifyInstalledFile(string mod, string file) public async Task VerifyInstalledFile(string mod, string file)
{ {
var src = MO2Folder.Combine((string)Consts.MO2ModFolderName, mod, file); var src = SourcePath.Combine((string)Consts.MO2ModFolderName, mod, file);
Assert.True(src.Exists); Assert.True(src.Exists);
var dest = InstallFolder.Combine((string)Consts.MO2ModFolderName, mod, file); var dest = InstallPath.Combine((string)Consts.MO2ModFolderName, mod, file);
Assert.True(dest.Exists, $"Destination {dest} doesn't exist"); Assert.True(dest.Exists, $"Destination {dest} doesn't exist");
var srcData = await src.ReadAllBytesAsync(); var srcData = await src.ReadAllBytesAsync();
@ -203,7 +204,7 @@ namespace Wabbajack.Test
var src = GameFolder.Combine(file); var src = GameFolder.Combine(file);
Assert.True(src.Exists); Assert.True(src.Exists);
var dest = InstallFolder.Combine((string)Consts.GameFolderFilesDir, file); var dest = InstallPath.Combine((string)Consts.GameFolderFilesDir, file);
Assert.True(dest.Exists); Assert.True(dest.Exists);
var srcData = await src.ReadAllBytesAsync(); var srcData = await src.ReadAllBytesAsync();
@ -219,7 +220,7 @@ namespace Wabbajack.Test
} }
public AbsolutePath PathOfInstalledFile(string mod, string file) public AbsolutePath PathOfInstalledFile(string mod, string file)
{ {
return InstallFolder.Combine((string)Consts.MO2ModFolderName, mod, file); return InstallPath.Combine((string)Consts.MO2ModFolderName, mod, file);
} }
public async ValueTask VerifyAllFiles(bool gameFileShouldNotExistInGameFolder = true) public async ValueTask VerifyAllFiles(bool gameFileShouldNotExistInGameFolder = true)
@ -228,32 +229,32 @@ namespace Wabbajack.Test
{ {
foreach (var file in Game.MetaData().RequiredFiles!) foreach (var file in Game.MetaData().RequiredFiles!)
{ {
Assert.False(InstallFolder.Combine(Consts.GameFolderFilesDir, (RelativePath)file).Exists); Assert.False(InstallPath.Combine(Consts.GameFolderFilesDir, (RelativePath)file).Exists);
} }
} }
var skipFiles = new []{"portable.txt"}.Select(e => (RelativePath)e).ToHashSet(); var skipFiles = new []{"portable.txt"}.Select(e => (RelativePath)e).ToHashSet();
foreach (var destFile in InstallFolder.EnumerateFiles()) foreach (var destFile in InstallPath.EnumerateFiles())
{ {
var relFile = destFile.RelativeTo(InstallFolder); var relFile = destFile.RelativeTo(InstallPath);
if (destFile.InFolder(Consts.LOOTFolderFilesDir.RelativeTo(MO2Folder)) || destFile.InFolder(Consts.GameFolderFilesDir.RelativeTo(MO2Folder))) if (destFile.InFolder(Consts.LOOTFolderFilesDir.RelativeTo(SourcePath)) || destFile.InFolder(Consts.GameFolderFilesDir.RelativeTo(SourcePath)))
continue; continue;
if (!skipFiles.Contains(relFile)) if (!skipFiles.Contains(relFile))
Assert.True(MO2Folder.Combine(relFile).Exists, $"Only in Destination: {relFile}"); Assert.True(SourcePath.Combine(relFile).Exists, $"Only in Destination: {relFile}");
} }
var skipExtensions = new []{".txt", ".ini"}.Select(e => new Extension(e)).ToHashSet(); var skipExtensions = new []{".txt", ".ini"}.Select(e => new Extension(e)).ToHashSet();
foreach (var srcFile in MO2Folder.EnumerateFiles()) foreach (var srcFile in SourcePath.EnumerateFiles())
{ {
var relFile = srcFile.RelativeTo(MO2Folder); var relFile = srcFile.RelativeTo(SourcePath);
if (relFile.StartsWith("downloads\\")) if (relFile.StartsWith("downloads\\"))
continue; continue;
var destFile = InstallFolder.Combine(relFile); var destFile = InstallPath.Combine(relFile);
Assert.True(destFile.Exists, $"Only in Source: {relFile}"); Assert.True(destFile.Exists, $"Only in Source: {relFile}");
if (!skipExtensions.Contains(srcFile.Extension)) if (!skipExtensions.Contains(srcFile.Extension))
@ -271,5 +272,12 @@ namespace Wabbajack.Test
await GenerateRandomFileData(fullPath, i); await GenerateRandomFileData(fullPath, i);
return fullPath; return fullPath;
} }
public void CreatePaths()
{
SourcePath.CreateDirectory();
DownloadsPath.CreateDirectory();
InstallPath.CreateDirectory();
}
} }
} }

View File

@ -236,7 +236,7 @@ namespace Wabbajack.VirtualFileSystem
} }
} }
await filesByParent[top].PMap(queue, async file => await HandleFile(file, new ExtractedNativeFile(file.AbsoluteName))); await filesByParent[top].PMap(queue, async file => await HandleFile(file, new ExtractedNativeFile(file.AbsoluteName) {CanMove = false}));
} }
#region KnownFiles #region KnownFiles

View File

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using DynamicData; using DynamicData;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.Lib; using Wabbajack.Lib;
using WebSocketSharp;
namespace Wabbajack namespace Wabbajack
{ {
@ -49,7 +50,7 @@ namespace Wabbajack
PathType = FilePickerVM.PathTypeOptions.File, PathType = FilePickerVM.PathTypeOptions.File,
PromptTitle = "Select a Modlist" PromptTitle = "Select a Modlist"
}; };
ModListLocation.Filters.Add(new CommonFileDialogFilter("MO2 Profile (modlist.txt)", ".txt")); ModListLocation.Filters.Add(new CommonFileDialogFilter("MO2 Profile (modlist.txt) or Native Settings (native_compiler_settings.json)", ".txt,.json"));
DownloadLocation = new FilePickerVM() DownloadLocation = new FilePickerVM()
{ {
@ -63,8 +64,18 @@ namespace Wabbajack
{ {
try try
{ {
var profileFolder = loc.Parent; if (loc.FileName == Consts.ModListTxt)
return profileFolder.Parent.Parent; {
var profileFolder = loc.Parent;
return profileFolder.Parent.Parent;
}
if (loc.FileName == Consts.NativeSettingsJson)
{
return loc.Parent;
}
return default;
} }
catch (Exception) catch (Exception)
{ {
@ -77,6 +88,11 @@ namespace Wabbajack
{ {
try try
{ {
if (loc.FileName == Consts.NativeSettingsJson)
{
var settings = loc.FromJson<NativeCompilerSettings>();
return settings.ModListName;
}
return (string)loc.Parent.FileName; return (string)loc.Parent.FileName;
} }
catch (Exception) catch (Exception)
@ -179,24 +195,45 @@ namespace Wabbajack
try try
{ {
using (ActiveCompilation = new MO2Compiler( ACompiler compiler;
mo2Folder: Mo2Folder,
mo2Profile: MOProfile, if (ModListLocation.TargetPath.FileName == Consts.NativeSettingsJson)
outputFile: outputFile)
{ {
ModListName = ModlistSettings.ModListName, var settings = ModListLocation.TargetPath.FromJson<NativeCompilerSettings>();
ModListAuthor = ModlistSettings.AuthorText, compiler = new NativeCompiler(settings, Mo2Folder, DownloadLocation.TargetPath, outputFile)
ModListDescription = ModlistSettings.Description, {
ModListImage = ModlistSettings.ImagePath.TargetPath, ModListName = ModlistSettings.ModListName,
ModListWebsite = ModlistSettings.Website, ModListAuthor = ModlistSettings.AuthorText,
ModlistReadme = ModlistSettings.Readme, ModListDescription = ModlistSettings.Description,
MO2DownloadsFolder = DownloadLocation.TargetPath, ModListImage = ModlistSettings.ImagePath.TargetPath,
ModlistVersion = ModlistSettings.Version, ModListWebsite = ModlistSettings.Website,
ModlistIsNSFW = ModlistSettings.IsNSFW ModlistReadme = ModlistSettings.Readme,
}) ModlistVersion = ModlistSettings.Version,
ModlistIsNSFW = ModlistSettings.IsNSFW
};
}
else
{
compiler = new MO2Compiler(
sourcePath: Mo2Folder,
downloadsPath: DownloadLocation.TargetPath,
mo2Profile: MOProfile,
outputFile: outputFile)
{
ModListName = ModlistSettings.ModListName,
ModListAuthor = ModlistSettings.AuthorText,
ModListDescription = ModlistSettings.Description,
ModListImage = ModlistSettings.ImagePath.TargetPath,
ModListWebsite = ModlistSettings.Website,
ModlistReadme = ModlistSettings.Readme,
ModlistVersion = ModlistSettings.Version,
ModlistIsNSFW = ModlistSettings.IsNSFW
};
}
using (ActiveCompilation = compiler
)
{ {
Parent.MWVM.Settings.Performance.SetProcessorSettings(ActiveCompilation); Parent.MWVM.Settings.Performance.SetProcessorSettings(ActiveCompilation);
var success = await ActiveCompilation.Begin(); var success = await ActiveCompilation.Begin();
return GetResponse<ModList>.Create(success, ActiveCompilation.ModList); return GetResponse<ModList>.Create(success, ActiveCompilation.ModList);
} }