2019-07-30 03:32:52 +00:00
|
|
|
|
using Compression.BSA;
|
2019-07-26 20:59:14 +00:00
|
|
|
|
using Newtonsoft.Json;
|
2019-07-21 04:40:54 +00:00
|
|
|
|
using SevenZipExtractor;
|
2019-07-28 23:04:23 +00:00
|
|
|
|
using SharpCompress.Archives;
|
2019-07-21 04:40:54 +00:00
|
|
|
|
using System;
|
2019-07-26 20:59:14 +00:00
|
|
|
|
using System.Collections.Concurrent;
|
2019-07-21 04:40:54 +00:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
2019-07-21 22:47:17 +00:00
|
|
|
|
using System.Reflection;
|
2019-07-21 04:40:54 +00:00
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
using System.Threading.Tasks;
|
2019-07-24 04:31:08 +00:00
|
|
|
|
using System.Web;
|
2019-07-21 04:40:54 +00:00
|
|
|
|
using Wabbajack.Common;
|
|
|
|
|
|
|
|
|
|
namespace Wabbajack
|
|
|
|
|
{
|
|
|
|
|
public class Compiler
|
|
|
|
|
{
|
2019-07-23 04:27:26 +00:00
|
|
|
|
|
2019-07-21 04:40:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public string MO2Folder;
|
|
|
|
|
|
|
|
|
|
public dynamic MO2Ini { get; }
|
|
|
|
|
public string GamePath { get; }
|
|
|
|
|
|
2019-07-22 22:17:46 +00:00
|
|
|
|
public string MO2DownloadsFolder
|
|
|
|
|
{
|
2019-07-21 04:40:54 +00:00
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return Path.Combine(MO2Folder, "downloads");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-21 22:47:17 +00:00
|
|
|
|
|
|
|
|
|
|
2019-07-21 12:42:29 +00:00
|
|
|
|
public string MO2Profile;
|
|
|
|
|
|
|
|
|
|
public string MO2ProfileDir
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
2019-07-23 04:27:26 +00:00
|
|
|
|
return Path.Combine(MO2Folder, "profiles", MO2Profile);
|
2019-07-21 12:42:29 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-21 04:40:54 +00:00
|
|
|
|
public Action<string> Log_Fn { get; }
|
2019-07-21 22:47:17 +00:00
|
|
|
|
public List<Directive> InstallDirectives { get; private set; }
|
|
|
|
|
public List<Archive> SelectedArchives { get; private set; }
|
2019-07-22 03:36:25 +00:00
|
|
|
|
public List<RawSourceFile> AllFiles { get; private set; }
|
2019-07-23 04:27:26 +00:00
|
|
|
|
public ModList ModList { get; private set; }
|
2019-07-26 20:59:14 +00:00
|
|
|
|
public ConcurrentBag<Directive> ExtraFiles { get; private set; }
|
2019-07-21 04:40:54 +00:00
|
|
|
|
|
|
|
|
|
public List<IndexedArchive> IndexedArchives;
|
|
|
|
|
|
|
|
|
|
public void Info(string msg, params object[] args)
|
|
|
|
|
{
|
|
|
|
|
if (args.Length > 0)
|
|
|
|
|
msg = String.Format(msg, args);
|
|
|
|
|
Log_Fn(msg);
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-22 22:17:46 +00:00
|
|
|
|
public void Status(string msg, params object[] args)
|
|
|
|
|
{
|
|
|
|
|
if (args.Length > 0)
|
|
|
|
|
msg = String.Format(msg, args);
|
|
|
|
|
WorkQueue.Report(msg, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2019-07-21 22:47:17 +00:00
|
|
|
|
private void Error(string msg, params object[] args)
|
|
|
|
|
{
|
|
|
|
|
if (args.Length > 0)
|
|
|
|
|
msg = String.Format(msg, args);
|
|
|
|
|
Log_Fn(msg);
|
|
|
|
|
throw new Exception(msg);
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-22 22:17:46 +00:00
|
|
|
|
public Compiler(string mo2_folder, Action<string> log_fn)
|
2019-07-21 04:40:54 +00:00
|
|
|
|
{
|
|
|
|
|
MO2Folder = mo2_folder;
|
|
|
|
|
Log_Fn = log_fn;
|
|
|
|
|
MO2Ini = Path.Combine(MO2Folder, "ModOrganizer.ini").LoadIniFile();
|
|
|
|
|
GamePath = ((string)MO2Ini.General.gamePath).Replace("\\\\", "\\");
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-22 22:17:46 +00:00
|
|
|
|
|
2019-07-21 12:42:29 +00:00
|
|
|
|
|
2019-07-21 04:40:54 +00:00
|
|
|
|
public void LoadArchives()
|
|
|
|
|
{
|
|
|
|
|
IndexedArchives = Directory.EnumerateFiles(MO2DownloadsFolder)
|
2019-07-23 04:27:26 +00:00
|
|
|
|
.Where(file => Consts.SupportedArchives.Contains(Path.GetExtension(file)))
|
2019-07-22 22:17:46 +00:00
|
|
|
|
.PMap(file => LoadArchive(file));
|
2019-07-21 04:40:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private IndexedArchive LoadArchive(string file)
|
|
|
|
|
{
|
|
|
|
|
string metaname = file + ".archive_contents";
|
|
|
|
|
|
|
|
|
|
if (metaname.FileExists() && new FileInfo(metaname).LastWriteTime >= new FileInfo(file).LastWriteTime)
|
|
|
|
|
{
|
2019-07-22 22:17:46 +00:00
|
|
|
|
Status("Loading Archive Index for {0}", Path.GetFileName(file));
|
2019-07-21 04:40:54 +00:00
|
|
|
|
var info = metaname.FromJSON<IndexedArchive>();
|
|
|
|
|
info.Name = Path.GetFileName(file);
|
2019-07-22 03:36:25 +00:00
|
|
|
|
info.AbsolutePath = file;
|
2019-07-21 04:40:54 +00:00
|
|
|
|
|
|
|
|
|
var ini_name = file + ".meta";
|
|
|
|
|
if (ini_name.FileExists())
|
2019-07-21 22:47:17 +00:00
|
|
|
|
{
|
2019-07-21 04:40:54 +00:00
|
|
|
|
info.IniData = ini_name.LoadIniFile();
|
2019-07-21 22:47:17 +00:00
|
|
|
|
info.Meta = File.ReadAllText(ini_name);
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-21 04:40:54 +00:00
|
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using (var ar = new ArchiveFile(file))
|
|
|
|
|
{
|
2019-07-22 22:17:46 +00:00
|
|
|
|
Status("Indexing {0}", Path.GetFileName(file));
|
2019-07-21 04:40:54 +00:00
|
|
|
|
var streams = new Dictionary<string, (SHA256Managed, long)>();
|
|
|
|
|
ar.Extract(entry => {
|
|
|
|
|
if (entry.IsFolder) return null;
|
|
|
|
|
|
|
|
|
|
var sha = new SHA256Managed();
|
|
|
|
|
var os = new CryptoStream(Stream.Null, sha, CryptoStreamMode.Write);
|
|
|
|
|
streams.Add(entry.FileName, (sha, (long)entry.Size));
|
|
|
|
|
return os;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var indexed = new IndexedArchiveCache();
|
|
|
|
|
indexed.Hash = file.FileSHA256();
|
|
|
|
|
indexed.Entries = streams.Select(entry =>
|
|
|
|
|
{
|
|
|
|
|
return new IndexedEntry()
|
|
|
|
|
{
|
|
|
|
|
Hash = entry.Value.Item1.Hash.ToBase64(),
|
|
|
|
|
Size = (long)entry.Value.Item2,
|
|
|
|
|
Path = entry.Key
|
|
|
|
|
};
|
|
|
|
|
}).ToList();
|
|
|
|
|
|
|
|
|
|
streams.Do(e => e.Value.Item1.Dispose());
|
|
|
|
|
|
|
|
|
|
indexed.ToJSON(metaname);
|
|
|
|
|
return LoadArchive(file);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Compile()
|
|
|
|
|
{
|
|
|
|
|
var mo2_files = Directory.EnumerateFiles(MO2Folder, "*", SearchOption.AllDirectories)
|
|
|
|
|
.Where(p => p.FileExists())
|
|
|
|
|
.Select(p => new RawSourceFile() { Path = p.RelativeTo(MO2Folder), AbsolutePath = p });
|
|
|
|
|
|
|
|
|
|
var game_files = Directory.EnumerateFiles(GamePath, "*", SearchOption.AllDirectories)
|
|
|
|
|
.Where(p => p.FileExists())
|
|
|
|
|
.Select(p => new RawSourceFile() { Path = Path.Combine(Consts.GameFolderFilesDir, p.RelativeTo(GamePath)), AbsolutePath = p });
|
|
|
|
|
|
2019-07-22 03:36:25 +00:00
|
|
|
|
AllFiles = mo2_files.Concat(game_files).ToList();
|
2019-07-21 04:40:54 +00:00
|
|
|
|
|
2019-07-22 03:36:25 +00:00
|
|
|
|
Info("Found {0} files to build into mod list", AllFiles.Count);
|
2019-07-21 04:40:54 +00:00
|
|
|
|
|
2019-07-26 20:59:14 +00:00
|
|
|
|
ExtraFiles = new ConcurrentBag<Directive>();
|
|
|
|
|
|
2019-07-21 04:40:54 +00:00
|
|
|
|
var stack = MakeStack();
|
|
|
|
|
|
2019-07-22 22:17:46 +00:00
|
|
|
|
Info("Running Compilation Stack");
|
|
|
|
|
var results = AllFiles.PMap(f => RunStack(stack, f)).ToList();
|
2019-07-21 04:40:54 +00:00
|
|
|
|
|
2019-07-26 20:59:14 +00:00
|
|
|
|
// Add the extra files that were generated by the stack
|
|
|
|
|
Info($"Adding {ExtraFiles.Count} that were generated by the stack");
|
|
|
|
|
results = results.Concat(ExtraFiles).ToList();
|
|
|
|
|
|
2019-07-21 04:40:54 +00:00
|
|
|
|
var nomatch = results.OfType<NoMatch>();
|
|
|
|
|
Info("No match for {0} files", nomatch.Count());
|
|
|
|
|
foreach (var file in nomatch)
|
|
|
|
|
Info(" {0}", file.To);
|
2019-07-26 20:59:14 +00:00
|
|
|
|
if (nomatch.Count() > 0)
|
|
|
|
|
Error("Exiting due to no way to compile these files");
|
2019-07-21 04:40:54 +00:00
|
|
|
|
|
2019-07-21 22:47:17 +00:00
|
|
|
|
InstallDirectives = results.Where(i => !(i is IgnoredDirectly)).ToList();
|
|
|
|
|
|
|
|
|
|
GatherArchives();
|
2019-07-22 03:36:25 +00:00
|
|
|
|
BuildPatches();
|
2019-07-23 04:27:26 +00:00
|
|
|
|
|
|
|
|
|
ModList = new ModList()
|
|
|
|
|
{
|
|
|
|
|
Archives = SelectedArchives,
|
|
|
|
|
Directives = InstallDirectives
|
|
|
|
|
};
|
|
|
|
|
|
2019-07-22 22:17:46 +00:00
|
|
|
|
PatchExecutable();
|
2019-07-23 04:27:26 +00:00
|
|
|
|
|
|
|
|
|
ResetMembers();
|
|
|
|
|
|
2019-07-22 22:17:46 +00:00
|
|
|
|
Info("Done Building Modpack");
|
2019-07-21 04:40:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-07-23 04:27:26 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Clear references to lists that hold a lot of data.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void ResetMembers()
|
|
|
|
|
{
|
|
|
|
|
AllFiles = null;
|
|
|
|
|
IndexedArchives = null;
|
|
|
|
|
InstallDirectives = null;
|
|
|
|
|
SelectedArchives = null;
|
2019-07-26 20:59:14 +00:00
|
|
|
|
ExtraFiles = null;
|
2019-07-23 04:27:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-07-22 03:36:25 +00:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Fills in the Patch fields in files that require them
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void BuildPatches()
|
|
|
|
|
{
|
|
|
|
|
var groups = InstallDirectives.OfType<PatchedFromArchive>()
|
|
|
|
|
.GroupBy(p => p.ArchiveHash)
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
Info("Patching building patches from {0} archives", groups.Count);
|
|
|
|
|
var absolute_paths = AllFiles.ToDictionary(e => e.Path, e => e.AbsolutePath);
|
2019-07-22 22:17:46 +00:00
|
|
|
|
groups.PMap(group => BuildArchivePatches(group.Key, group, absolute_paths));
|
2019-07-22 03:36:25 +00:00
|
|
|
|
|
|
|
|
|
if (InstallDirectives.OfType<PatchedFromArchive>().FirstOrDefault(f => f.Patch == null) != null)
|
|
|
|
|
{
|
|
|
|
|
Error("Missing patches after generation, this should not happen");
|
|
|
|
|
}
|
2019-07-22 22:17:46 +00:00
|
|
|
|
|
2019-07-22 03:36:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void BuildArchivePatches(string archive_sha, IEnumerable<PatchedFromArchive> group, Dictionary<string, string> absolute_paths)
|
|
|
|
|
{
|
|
|
|
|
var archive = IndexedArchives.First(a => a.Hash == archive_sha);
|
|
|
|
|
var paths = group.Select(g => g.From).ToHashSet();
|
|
|
|
|
var streams = new Dictionary<string, MemoryStream>();
|
2019-07-26 20:59:14 +00:00
|
|
|
|
Status($"Extracting {paths.Count} patch files from {archive.Name}");
|
2019-07-22 03:36:25 +00:00
|
|
|
|
// First we fetch the source files from the input archive
|
|
|
|
|
using (var a = new ArchiveFile(archive.AbsolutePath))
|
|
|
|
|
{
|
|
|
|
|
a.Extract(entry =>
|
|
|
|
|
{
|
2019-07-22 22:17:46 +00:00
|
|
|
|
if (!paths.Contains(entry.FileName)) return null;
|
2019-07-22 03:36:25 +00:00
|
|
|
|
|
2019-07-22 22:17:46 +00:00
|
|
|
|
var result = new MemoryStream();
|
|
|
|
|
streams.Add(entry.FileName, result);
|
|
|
|
|
return result;
|
2019-07-22 03:36:25 +00:00
|
|
|
|
}, false);
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-28 23:04:23 +00:00
|
|
|
|
/*
|
|
|
|
|
using (var a = ArchiveFactory.Open(archive.AbsolutePath))
|
|
|
|
|
{
|
|
|
|
|
foreach (var entry in a.Entries)
|
|
|
|
|
{
|
|
|
|
|
var path = entry.Key.Replace("/", "\\");
|
|
|
|
|
if (!paths.Contains(path)) continue;
|
|
|
|
|
var result = new MemoryStream();
|
|
|
|
|
streams.Add(path, result);
|
|
|
|
|
Info("Extracting {0}", path);
|
|
|
|
|
using (var stream = entry.OpenEntryStream())
|
|
|
|
|
stream.CopyTo(result);
|
|
|
|
|
}
|
|
|
|
|
}*/
|
|
|
|
|
|
2019-07-22 03:36:25 +00:00
|
|
|
|
var extracted = streams.ToDictionary(k => k.Key, v => v.Value.ToArray());
|
|
|
|
|
// Now Create the patches
|
2019-07-22 22:17:46 +00:00
|
|
|
|
Status("Building Patches for {0}", archive.Name);
|
2019-07-22 03:36:25 +00:00
|
|
|
|
Parallel.ForEach(group, entry =>
|
|
|
|
|
{
|
|
|
|
|
Info("Patching {0}", entry.To);
|
|
|
|
|
var ss = extracted[entry.From];
|
|
|
|
|
using (var origin = new MemoryStream(ss))
|
|
|
|
|
using (var output = new MemoryStream())
|
|
|
|
|
{
|
|
|
|
|
var a = origin.ReadAll();
|
2019-07-26 20:59:14 +00:00
|
|
|
|
var b = LoadDataForTo(entry.To, absolute_paths);
|
2019-07-22 03:36:25 +00:00
|
|
|
|
BSDiff.Create(a, b, output);
|
|
|
|
|
entry.Patch = output.ToArray().ToBase64();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-26 20:59:14 +00:00
|
|
|
|
private byte[] LoadDataForTo(string to, Dictionary<string, string> absolute_paths)
|
|
|
|
|
{
|
|
|
|
|
if (absolute_paths.TryGetValue(to, out var absolute))
|
|
|
|
|
return File.ReadAllBytes(absolute);
|
|
|
|
|
|
|
|
|
|
if (to.StartsWith(Consts.BSACreationDir))
|
|
|
|
|
{
|
|
|
|
|
var bsa_id = to.Split('\\')[1];
|
|
|
|
|
var bsa = InstallDirectives.OfType<CreateBSA>().First(b => b.TempID == bsa_id);
|
|
|
|
|
|
2019-07-30 03:32:52 +00:00
|
|
|
|
using (var a = new BSAReader(Path.Combine(MO2Folder, bsa.To)))
|
2019-07-26 20:59:14 +00:00
|
|
|
|
{
|
2019-07-30 03:32:52 +00:00
|
|
|
|
var file = a.Files.First(e => e.Path == Path.Combine(to.Split('\\').Skip(2).ToArray()));
|
|
|
|
|
return file.GetData();
|
2019-07-26 20:59:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
Error($"Couldn't load data for {to}");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-21 22:47:17 +00:00
|
|
|
|
private void GatherArchives()
|
|
|
|
|
{
|
|
|
|
|
var archives = IndexedArchives.GroupBy(a => a.Hash).ToDictionary(k => k.Key, k => k.First());
|
|
|
|
|
|
|
|
|
|
var shas = InstallDirectives.OfType<FromArchive>()
|
|
|
|
|
.Select(a => a.ArchiveHash)
|
|
|
|
|
.Distinct();
|
|
|
|
|
|
|
|
|
|
SelectedArchives = shas.Select(sha => ResolveArchive(sha, archives)).ToList();
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Archive ResolveArchive(string sha, Dictionary<string, IndexedArchive> archives)
|
|
|
|
|
{
|
2019-07-22 22:17:46 +00:00
|
|
|
|
if (archives.TryGetValue(sha, out var found))
|
2019-07-21 22:47:17 +00:00
|
|
|
|
{
|
|
|
|
|
if (found.IniData == null)
|
|
|
|
|
Error("No download metadata found for {0}, please use MO2 to query info or add a .meta file and try again.", found.Name);
|
|
|
|
|
var general = found.IniData.General;
|
|
|
|
|
if (general == null)
|
|
|
|
|
Error("No General section in mod metadata found for {0}, please use MO2 to query info or add the info and try again.", found.Name);
|
|
|
|
|
|
|
|
|
|
Archive result;
|
|
|
|
|
|
|
|
|
|
if (general.modID != null && general.fileID != null && general.gameName != null)
|
|
|
|
|
{
|
2019-07-22 22:17:46 +00:00
|
|
|
|
result = new NexusMod()
|
|
|
|
|
{
|
2019-07-21 22:47:17 +00:00
|
|
|
|
GameName = general.gameName,
|
|
|
|
|
FileID = general.fileID,
|
2019-07-22 22:17:46 +00:00
|
|
|
|
ModID = general.modID
|
|
|
|
|
};
|
2019-07-21 22:47:17 +00:00
|
|
|
|
}
|
2019-07-26 20:59:14 +00:00
|
|
|
|
else if (general.directURL != null && general.directURL.StartsWith("https://drive.google.com"))
|
|
|
|
|
{
|
|
|
|
|
var regex = new Regex("((?<=id=)[a-zA-Z0-9_-]*)|(?<=\\/file\\/d\\/)[a-zA-Z0-9_-]*");
|
|
|
|
|
var match = regex.Match(general.directURL);
|
|
|
|
|
result = new GoogleDriveMod()
|
|
|
|
|
{
|
|
|
|
|
Id = match.ToString()
|
|
|
|
|
};
|
|
|
|
|
}
|
2019-07-24 04:31:08 +00:00
|
|
|
|
else if (general.directURL != null && general.directURL.StartsWith("https://www.dropbox.com/"))
|
|
|
|
|
{
|
|
|
|
|
var uri = new UriBuilder((string)general.directURL);
|
|
|
|
|
var query = HttpUtility.ParseQueryString(uri.Query);
|
|
|
|
|
|
|
|
|
|
if (query.GetValues("dl").Count() > 0)
|
|
|
|
|
query.Remove("dl");
|
|
|
|
|
|
|
|
|
|
query.Set("dl", "1");
|
|
|
|
|
|
|
|
|
|
uri.Query = query.ToString();
|
|
|
|
|
|
|
|
|
|
result = new DirectURLArchive()
|
|
|
|
|
{
|
|
|
|
|
URL = uri.ToString()
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
else if (general.directURL != null && general.directURL.StartsWith("https://www.moddb.com/downloads/start"))
|
|
|
|
|
{
|
|
|
|
|
result = new MODDBArchive()
|
|
|
|
|
{
|
|
|
|
|
URL = general.directURL
|
|
|
|
|
};
|
|
|
|
|
}
|
2019-07-21 22:47:17 +00:00
|
|
|
|
else if (general.directURL != null)
|
|
|
|
|
{
|
|
|
|
|
result = new DirectURLArchive()
|
|
|
|
|
{
|
|
|
|
|
URL = general.directURL
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Error("No way to handle archive {0} but it's required by the modpack", found.Name);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.Name = found.Name;
|
|
|
|
|
result.Hash = found.Hash;
|
|
|
|
|
result.Meta = found.Meta;
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
Error("No match found for Archive sha: {0} this shouldn't happen", sha);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2019-07-21 04:40:54 +00:00
|
|
|
|
private Directive RunStack(IEnumerable<Func<RawSourceFile, Directive>> stack, RawSourceFile source)
|
|
|
|
|
{
|
2019-07-22 22:17:46 +00:00
|
|
|
|
Status("Compiling {0}", source.Path);
|
2019-07-21 04:40:54 +00:00
|
|
|
|
return (from f in stack
|
|
|
|
|
let result = f(source)
|
|
|
|
|
where result != null
|
|
|
|
|
select result).First();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates a execution stack. The stack should be passed into Run stack. Each function
|
|
|
|
|
/// in this stack will be run in-order and the first to return a non-null result will have its
|
|
|
|
|
/// result included into the pack
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
private IEnumerable<Func<RawSourceFile, Directive>> MakeStack()
|
|
|
|
|
{
|
|
|
|
|
Info("Generating compilation stack");
|
|
|
|
|
return new List<Func<RawSourceFile, Directive>>()
|
|
|
|
|
{
|
|
|
|
|
IgnoreStartsWith("logs\\"),
|
|
|
|
|
IgnoreStartsWith("downloads\\"),
|
|
|
|
|
IgnoreStartsWith("webcache\\"),
|
2019-07-26 20:59:14 +00:00
|
|
|
|
IgnoreStartsWith("overwrite\\"),
|
2019-07-21 04:40:54 +00:00
|
|
|
|
IgnoreEndsWith(".pyc"),
|
2019-07-21 12:42:29 +00:00
|
|
|
|
IgnoreOtherProfiles(),
|
2019-07-23 04:27:26 +00:00
|
|
|
|
IgnoreDisabledMods(),
|
2019-07-21 12:42:29 +00:00
|
|
|
|
IncludeThisProfile(),
|
2019-07-21 04:40:54 +00:00
|
|
|
|
// Ignore the ModOrganizer.ini file it contains info created by MO2 on startup
|
|
|
|
|
IgnoreStartsWith("ModOrganizer.ini"),
|
2019-07-24 04:31:08 +00:00
|
|
|
|
IgnoreStartsWith(Path.Combine(Consts.GameFolderFilesDir, "Data")),
|
|
|
|
|
IgnoreStartsWith(Path.Combine(Consts.GameFolderFilesDir, "Papyrus Compiler")),
|
|
|
|
|
IgnoreStartsWith(Path.Combine(Consts.GameFolderFilesDir, "Skyrim")),
|
2019-07-21 04:40:54 +00:00
|
|
|
|
IgnoreRegex(Consts.GameFolderFilesDir + "\\\\.*\\.bsa"),
|
2019-07-21 12:42:29 +00:00
|
|
|
|
IncludeModIniData(),
|
2019-07-21 04:40:54 +00:00
|
|
|
|
DirectMatch(),
|
2019-07-21 22:47:17 +00:00
|
|
|
|
IncludePatches(),
|
2019-07-21 12:42:29 +00:00
|
|
|
|
|
2019-07-26 20:59:14 +00:00
|
|
|
|
DeconstructBSAs(),
|
|
|
|
|
|
2019-07-21 12:42:29 +00:00
|
|
|
|
// If we have no match at this point for a game folder file, skip them, we can't do anything about them
|
|
|
|
|
IgnoreGameFiles(),
|
2019-07-26 20:59:14 +00:00
|
|
|
|
|
|
|
|
|
// There are some types of files that will error the compilation, because tehy're created on-the-fly via tools
|
|
|
|
|
// so if we don't have a match by this point, just drop them.
|
|
|
|
|
IgnoreEndsWith(".ini"),
|
|
|
|
|
IgnoreEndsWith(".html"),
|
|
|
|
|
IgnoreEndsWith(".txt"),
|
|
|
|
|
// Don't know why, but this seems to get copied around a bit
|
|
|
|
|
IgnoreEndsWith("HavokBehaviorPostProcess.exe"),
|
|
|
|
|
DropAll()
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// This function will search for a way to create a BSA in the installed mod list by assembling it from files
|
|
|
|
|
/// found in archives. To do this we hash all the files in side the BSA then try to find matches and patches for
|
|
|
|
|
/// all of the files.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
private Func<RawSourceFile, Directive> DeconstructBSAs()
|
|
|
|
|
{
|
|
|
|
|
var microstack = new List<Func<RawSourceFile, Directive>>()
|
|
|
|
|
{
|
|
|
|
|
DirectMatch(),
|
|
|
|
|
IncludePatches(),
|
2019-07-21 04:40:54 +00:00
|
|
|
|
DropAll()
|
|
|
|
|
};
|
2019-07-26 20:59:14 +00:00
|
|
|
|
|
|
|
|
|
return source =>
|
|
|
|
|
{
|
|
|
|
|
if (!Consts.SupportedBSAs.Contains(Path.GetExtension(source.Path))) return null;
|
|
|
|
|
|
|
|
|
|
var hashed = HashBSA(source.AbsolutePath);
|
|
|
|
|
|
|
|
|
|
var source_files = hashed.Select(e => new RawSourceFile() {
|
|
|
|
|
Hash = e.Item2,
|
|
|
|
|
Path = e.Item1,
|
|
|
|
|
AbsolutePath = e.Item1
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var matches = source_files.Select(e => RunStack(microstack, e));
|
|
|
|
|
|
|
|
|
|
var id = Guid.NewGuid().ToString();
|
|
|
|
|
|
|
|
|
|
foreach (var match in matches)
|
|
|
|
|
{
|
|
|
|
|
if (match is IgnoredDirectly)
|
|
|
|
|
{
|
|
|
|
|
Error($"File required for BSA creation doesn't exist: {match.To}");
|
|
|
|
|
}
|
|
|
|
|
match.To = Path.Combine(Consts.BSACreationDir, id, match.To);
|
|
|
|
|
ExtraFiles.Add(match);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
CreateBSA directive;
|
2019-07-30 03:32:52 +00:00
|
|
|
|
using (var bsa = new BSAReader(source.AbsolutePath))
|
2019-07-26 20:59:14 +00:00
|
|
|
|
{
|
|
|
|
|
directive = new CreateBSA()
|
|
|
|
|
{
|
2019-07-28 23:04:23 +00:00
|
|
|
|
To = source.Path,
|
2019-07-26 20:59:14 +00:00
|
|
|
|
TempID = id,
|
2019-07-30 03:32:52 +00:00
|
|
|
|
Type = (uint)bsa.HeaderType,
|
|
|
|
|
FileFlags = (uint)bsa.FileFlags,
|
|
|
|
|
ArchiveFlags = (uint)bsa.ArchiveFlags,
|
2019-07-26 20:59:14 +00:00
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return directive;
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Given a BSA on disk, index it and return a dictionary of SHA256 -> filename
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="absolutePath"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
private List<(string, string)> HashBSA(string absolutePath)
|
|
|
|
|
{
|
|
|
|
|
Status($"Hashing BSA: {absolutePath}");
|
|
|
|
|
var results = new List<(string, string)>();
|
2019-07-30 03:32:52 +00:00
|
|
|
|
using (var a = new BSAReader(absolutePath))
|
2019-07-26 20:59:14 +00:00
|
|
|
|
{
|
2019-07-30 03:32:52 +00:00
|
|
|
|
foreach (var entry in a.Files)
|
2019-07-26 20:59:14 +00:00
|
|
|
|
{
|
2019-07-30 03:32:52 +00:00
|
|
|
|
Status($"Hashing BSA: {absolutePath} - {entry.Path}");
|
2019-07-26 20:59:14 +00:00
|
|
|
|
|
2019-07-30 03:32:52 +00:00
|
|
|
|
var data = entry.GetData();
|
|
|
|
|
results.Add((entry.Path, data.SHA256()));
|
2019-07-26 20:59:14 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return results;
|
2019-07-21 04:40:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-07-23 04:27:26 +00:00
|
|
|
|
private Func<RawSourceFile, Directive> IgnoreDisabledMods()
|
|
|
|
|
{
|
|
|
|
|
var disabled_mods = File.ReadAllLines(Path.Combine(MO2ProfileDir, "modlist.txt"))
|
|
|
|
|
.Where(line => line.StartsWith("-") && !line.EndsWith("_separator"))
|
2019-07-28 23:04:23 +00:00
|
|
|
|
.Select(line => Path.Combine("mods", line.Substring(1)) + "\\")
|
2019-07-23 04:27:26 +00:00
|
|
|
|
.ToList();
|
|
|
|
|
return source =>
|
|
|
|
|
{
|
|
|
|
|
if (disabled_mods.FirstOrDefault(mod => source.Path.StartsWith(mod)) != null)
|
|
|
|
|
{
|
|
|
|
|
var r = source.EvolveTo<IgnoredDirectly>();
|
|
|
|
|
r.Reason = "Disabled Mod";
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-21 22:47:17 +00:00
|
|
|
|
private Func<RawSourceFile, Directive> IncludePatches()
|
|
|
|
|
{
|
|
|
|
|
var indexed = (from archive in IndexedArchives
|
|
|
|
|
from entry in archive.Entries
|
|
|
|
|
select new { archive = archive, entry = entry })
|
2019-07-26 20:59:14 +00:00
|
|
|
|
.GroupBy(e => Path.GetFileName(e.entry.Path).ToLower())
|
2019-07-21 22:47:17 +00:00
|
|
|
|
.ToDictionary(e => e.Key);
|
|
|
|
|
|
|
|
|
|
return source =>
|
|
|
|
|
{
|
2019-07-26 20:59:14 +00:00
|
|
|
|
if (indexed.TryGetValue(Path.GetFileName(source.Path.ToLower()), out var value))
|
2019-07-21 22:47:17 +00:00
|
|
|
|
{
|
|
|
|
|
var found = value.First();
|
|
|
|
|
|
|
|
|
|
var e = source.EvolveTo<PatchedFromArchive>();
|
|
|
|
|
e.From = found.entry.Path;
|
|
|
|
|
e.ArchiveHash = found.archive.Hash;
|
|
|
|
|
e.To = source.Path;
|
|
|
|
|
return e;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-21 12:42:29 +00:00
|
|
|
|
private Func<RawSourceFile, Directive> IncludeModIniData()
|
|
|
|
|
{
|
|
|
|
|
return source =>
|
|
|
|
|
{
|
|
|
|
|
if (source.Path.StartsWith("mods\\") && source.Path.EndsWith("\\meta.ini"))
|
|
|
|
|
{
|
|
|
|
|
var e = source.EvolveTo<InlineFile>();
|
|
|
|
|
e.SourceData = File.ReadAllBytes(source.AbsolutePath).ToBase64();
|
|
|
|
|
return e;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Func<RawSourceFile, Directive> IgnoreGameFiles()
|
|
|
|
|
{
|
|
|
|
|
var start_dir = Consts.GameFolderFilesDir + "\\";
|
|
|
|
|
return source =>
|
|
|
|
|
{
|
|
|
|
|
if (source.Path.StartsWith(start_dir))
|
|
|
|
|
{
|
|
|
|
|
var i = source.EvolveTo<IgnoredDirectly>();
|
|
|
|
|
i.Reason = "Default game file";
|
|
|
|
|
return i;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Func<RawSourceFile, Directive> IncludeThisProfile()
|
|
|
|
|
{
|
|
|
|
|
var correct_profile = Path.Combine("profiles", MO2Profile) + "\\";
|
|
|
|
|
return source =>
|
|
|
|
|
{
|
|
|
|
|
if (source.Path.StartsWith(correct_profile))
|
|
|
|
|
{
|
|
|
|
|
byte[] data;
|
|
|
|
|
if (source.Path.EndsWith("\\modlist.txt"))
|
|
|
|
|
data = ReadAndCleanModlist(source.AbsolutePath);
|
|
|
|
|
else
|
|
|
|
|
data = File.ReadAllBytes(source.AbsolutePath);
|
|
|
|
|
|
|
|
|
|
var e = source.EvolveTo<InlineFile>();
|
|
|
|
|
e.SourceData = data.ToBase64();
|
|
|
|
|
return e;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private byte[] ReadAndCleanModlist(string absolutePath)
|
|
|
|
|
{
|
|
|
|
|
var lines = File.ReadAllLines(absolutePath);
|
|
|
|
|
lines = (from line in lines
|
|
|
|
|
where !(line.StartsWith("-") && !line.EndsWith("_separator"))
|
|
|
|
|
select line).ToArray();
|
|
|
|
|
return Encoding.UTF8.GetBytes(String.Join("\r\n", lines));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Func<RawSourceFile, Directive> IgnoreOtherProfiles()
|
|
|
|
|
{
|
|
|
|
|
var correct_profile = Path.Combine("profiles", MO2Profile) + "\\";
|
|
|
|
|
return source =>
|
|
|
|
|
{
|
|
|
|
|
if (source.Path.StartsWith("profiles\\") && !source.Path.StartsWith(correct_profile))
|
|
|
|
|
{
|
|
|
|
|
var c = source.EvolveTo<IgnoredDirectly>();
|
|
|
|
|
c.Reason = "File not for this profile";
|
|
|
|
|
return c;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-21 04:40:54 +00:00
|
|
|
|
private Func<RawSourceFile, Directive> IgnoreEndsWith(string v)
|
|
|
|
|
{
|
|
|
|
|
var reason = String.Format("Ignored because path ends with {0}", v);
|
|
|
|
|
return source =>
|
|
|
|
|
{
|
|
|
|
|
if (source.Path.EndsWith(v))
|
|
|
|
|
{
|
|
|
|
|
var result = source.EvolveTo<IgnoredDirectly>();
|
|
|
|
|
result.Reason = reason;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Func<RawSourceFile, Directive> IgnoreRegex(string p)
|
|
|
|
|
{
|
|
|
|
|
var reason = String.Format("Ignored because path matches regex {0}", p);
|
|
|
|
|
var regex = new Regex(p);
|
|
|
|
|
return source =>
|
|
|
|
|
{
|
|
|
|
|
if (regex.IsMatch(source.Path))
|
|
|
|
|
{
|
|
|
|
|
var result = source.EvolveTo<IgnoredDirectly>();
|
|
|
|
|
result.Reason = reason;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Func<RawSourceFile, Directive> DropAll()
|
|
|
|
|
{
|
|
|
|
|
return source => {
|
|
|
|
|
var result = source.EvolveTo<NoMatch>();
|
|
|
|
|
result.Reason = "No Match in Stack";
|
|
|
|
|
return result;
|
2019-07-22 22:17:46 +00:00
|
|
|
|
};
|
2019-07-21 04:40:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Func<RawSourceFile, Directive> DirectMatch()
|
|
|
|
|
{
|
|
|
|
|
var indexed = (from archive in IndexedArchives
|
|
|
|
|
from entry in archive.Entries
|
|
|
|
|
select new { archive = archive, entry = entry })
|
|
|
|
|
.GroupBy(e => e.entry.Hash)
|
|
|
|
|
.ToDictionary(e => e.Key);
|
|
|
|
|
|
|
|
|
|
return source =>
|
|
|
|
|
{
|
|
|
|
|
if (indexed.TryGetValue(source.Hash, out var found))
|
|
|
|
|
{
|
|
|
|
|
var result = source.EvolveTo<FromArchive>();
|
|
|
|
|
var match = found.FirstOrDefault(f => Path.GetFileName(f.entry.Path) == Path.GetFileName(source.Path));
|
|
|
|
|
if (match == null)
|
|
|
|
|
match = found.First();
|
|
|
|
|
|
|
|
|
|
result.ArchiveHash = match.archive.Hash;
|
|
|
|
|
result.From = match.entry.Path;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Func<RawSourceFile, Directive> IgnoreStartsWith(string v)
|
|
|
|
|
{
|
|
|
|
|
var reason = String.Format("Ignored because path starts with {0}", v);
|
|
|
|
|
return source =>
|
|
|
|
|
{
|
|
|
|
|
if (source.Path.StartsWith(v))
|
|
|
|
|
{
|
|
|
|
|
var result = source.EvolveTo<IgnoredDirectly>();
|
|
|
|
|
result.Reason = reason;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
}
|
2019-07-22 22:17:46 +00:00
|
|
|
|
|
|
|
|
|
internal void PatchExecutable()
|
|
|
|
|
{
|
2019-07-31 03:59:19 +00:00
|
|
|
|
var settings = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Auto };
|
|
|
|
|
var data = JsonConvert.SerializeObject(ModList, settings).BZip2String();
|
2019-07-22 22:17:46 +00:00
|
|
|
|
var executable = Assembly.GetExecutingAssembly().Location;
|
|
|
|
|
var out_path = Path.Combine(Path.GetDirectoryName(executable), MO2Profile + ".exe");
|
|
|
|
|
Info("Patching Executable {0}", Path.GetFileName(out_path));
|
|
|
|
|
File.Copy(executable, out_path, true);
|
|
|
|
|
using (var os = File.OpenWrite(out_path))
|
|
|
|
|
using (var bw = new BinaryWriter(os))
|
|
|
|
|
{
|
|
|
|
|
long orig_pos = os.Length;
|
|
|
|
|
os.Position = os.Length;
|
|
|
|
|
bw.Write(data.LongLength);
|
|
|
|
|
bw.Write(data);
|
|
|
|
|
bw.Write(orig_pos);
|
|
|
|
|
bw.Write(Encoding.ASCII.GetBytes(Consts.ModPackMagic));
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-07-21 04:40:54 +00:00
|
|
|
|
}
|
|
|
|
|
}
|