From e4ca3cd01eead121c19c341f38799621b347d64f Mon Sep 17 00:00:00 2001 From: Timothy Baldridge Date: Sun, 21 Jul 2019 16:47:17 -0600 Subject: [PATCH] Can create self-installing modlists --- Wabbajack.Common/BSDiff.cs | 656 +++++++++++++++++++++ Wabbajack.Common/Consts.cs | 1 + Wabbajack.Common/Data.cs | 15 +- Wabbajack.Common/Utils.cs | 38 +- Wabbajack.Common/Wabbajack.Common.csproj | 7 + Wabbajack.Common/packages.config | 4 +- Wabbajack/Compiler.cs | 120 ++++ Wabbajack/FodyWeavers.xml | 3 + Wabbajack/FodyWeavers.xsd | 111 ++++ Wabbajack/Program.cs | 38 ++ Wabbajack/Properties/Resources.Designer.cs | 72 +++ Wabbajack/Properties/Resources.resx | 123 ++++ Wabbajack/Wabbajack.csproj | 28 + Wabbajack/packages.config | 3 + 14 files changed, 1214 insertions(+), 5 deletions(-) create mode 100644 Wabbajack.Common/BSDiff.cs create mode 100644 Wabbajack/FodyWeavers.xml create mode 100644 Wabbajack/FodyWeavers.xsd create mode 100644 Wabbajack/Properties/Resources.Designer.cs create mode 100644 Wabbajack/Properties/Resources.resx diff --git a/Wabbajack.Common/BSDiff.cs b/Wabbajack.Common/BSDiff.cs new file mode 100644 index 00000000..c51eaa19 --- /dev/null +++ b/Wabbajack.Common/BSDiff.cs @@ -0,0 +1,656 @@ +namespace Wabbajack.Common +{ + using ICSharpCode.SharpZipLib.BZip2; + using System; + using System.IO; + /* + The original bsdiff.c source code (http://www.daemonology.net/bsdiff/) is + distributed under the following license: + + Copyright 2003-2005 Colin Percival + All rights reserved + + Redistribution and use in source and binary forms, with or without + modification, are permitted providing that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + */ + public class BSDiff + { + /// + /// Creates a binary patch (in bsdiff format) that can be used + /// (by ) to transform into . + /// + /// The original binary data. + /// The new binary data. + /// A to which the patch will be written. + public static void Create(byte[] oldData, byte[] newData, Stream output) + { + // check arguments + if (oldData == null) + throw new ArgumentNullException("oldData"); + if (newData == null) + throw new ArgumentNullException("newData"); + if (output == null) + throw new ArgumentNullException("output"); + if (!output.CanSeek) + throw new ArgumentException("Output stream must be seekable.", "output"); + if (!output.CanWrite) + throw new ArgumentException("Output stream must be writable.", "output"); + + /* Header is + 0 8 "BSDIFF40" + 8 8 length of bzip2ed ctrl block + 16 8 length of bzip2ed diff block + 24 8 length of new file */ + /* File is + 0 32 Header + 32 ?? Bzip2ed ctrl block + ?? ?? Bzip2ed diff block + ?? ?? Bzip2ed extra block */ + byte[] header = new byte[c_headerSize]; + WriteInt64(c_fileSignature, header, 0); // "BSDIFF40" + WriteInt64(0, header, 8); + WriteInt64(0, header, 16); + WriteInt64(newData.Length, header, 24); + + long startPosition = output.Position; + output.Write(header, 0, header.Length); + + int[] I = SuffixSort(oldData); + + byte[] db = new byte[newData.Length]; + byte[] eb = new byte[newData.Length]; + + int dblen = 0; + int eblen = 0; + + using (BZip2OutputStream bz2Stream = new BZip2OutputStream(output) { IsStreamOwner = false }) + { + // compute the differences, writing ctrl as we go + int scan = 0; + int pos = 0; + int len = 0; + int lastscan = 0; + int lastpos = 0; + int lastoffset = 0; + while (scan < newData.Length) + { + int oldscore = 0; + + for (int scsc = scan += len; scan < newData.Length; scan++) + { + len = Search(I, oldData, newData, scan, 0, oldData.Length, out pos); + + for (; scsc < scan + len; scsc++) + { + if ((scsc + lastoffset < oldData.Length) && (oldData[scsc + lastoffset] == newData[scsc])) + oldscore++; + } + + if ((len == oldscore && len != 0) || (len > oldscore + 8)) + break; + + if ((scan + lastoffset < oldData.Length) && (oldData[scan + lastoffset] == newData[scan])) + oldscore--; + } + + if (len != oldscore || scan == newData.Length) + { + int s = 0; + int sf = 0; + int lenf = 0; + for (int i = 0; (lastscan + i < scan) && (lastpos + i < oldData.Length);) + { + if (oldData[lastpos + i] == newData[lastscan + i]) + s++; + i++; + if (s * 2 - i > sf * 2 - lenf) + { + sf = s; + lenf = i; + } + } + + int lenb = 0; + if (scan < newData.Length) + { + s = 0; + int sb = 0; + for (int i = 1; (scan >= lastscan + i) && (pos >= i); i++) + { + if (oldData[pos - i] == newData[scan - i]) + s++; + if (s * 2 - i > sb * 2 - lenb) + { + sb = s; + lenb = i; + } + } + } + + if (lastscan + lenf > scan - lenb) + { + int overlap = (lastscan + lenf) - (scan - lenb); + s = 0; + int ss = 0; + int lens = 0; + for (int i = 0; i < overlap; i++) + { + if (newData[lastscan + lenf - overlap + i] == oldData[lastpos + lenf - overlap + i]) + s++; + if (newData[scan - lenb + i] == oldData[pos - lenb + i]) + s--; + if (s > ss) + { + ss = s; + lens = i + 1; + } + } + + lenf += lens - overlap; + lenb -= lens; + } + + for (int i = 0; i < lenf; i++) + db[dblen + i] = (byte)(newData[lastscan + i] - oldData[lastpos + i]); + for (int i = 0; i < (scan - lenb) - (lastscan + lenf); i++) + eb[eblen + i] = newData[lastscan + lenf + i]; + + dblen += lenf; + eblen += (scan - lenb) - (lastscan + lenf); + + byte[] buf = new byte[8]; + WriteInt64(lenf, buf, 0); + bz2Stream.Write(buf, 0, 8); + + WriteInt64((scan - lenb) - (lastscan + lenf), buf, 0); + bz2Stream.Write(buf, 0, 8); + + WriteInt64((pos - lenb) - (lastpos + lenf), buf, 0); + bz2Stream.Write(buf, 0, 8); + + lastscan = scan - lenb; + lastpos = pos - lenb; + lastoffset = pos - scan; + } + } + } + + // compute size of compressed ctrl data + long controlEndPosition = output.Position; + WriteInt64(controlEndPosition - startPosition - c_headerSize, header, 8); + + // write compressed diff data + using (BZip2OutputStream bz2Stream = new BZip2OutputStream(output) { IsStreamOwner = false }) + { + bz2Stream.Write(db, 0, dblen); + } + + // compute size of compressed diff data + long diffEndPosition = output.Position; + WriteInt64(diffEndPosition - controlEndPosition, header, 16); + + // write compressed extra data + using (BZip2OutputStream bz2Stream = new BZip2OutputStream(output) { IsStreamOwner = false }) + { + bz2Stream.Write(eb, 0, eblen); + } + + // seek to the beginning, write the header, then seek back to end + long endPosition = output.Position; + output.Position = startPosition; + output.Write(header, 0, header.Length); + output.Position = endPosition; + } + + /// + /// Applies a binary patch (in bsdiff format) to the data in + /// and writes the results of patching to . + /// + /// A containing the input data. + /// A func that can open a positioned at the start of the patch data. + /// This stream must support reading and seeking, and must allow multiple streams on + /// the patch to be opened concurrently. + /// A to which the patched data is written. + public static void Apply(Stream input, Func openPatchStream, Stream output) + { + // check arguments + if (input == null) + throw new ArgumentNullException("input"); + if (openPatchStream == null) + throw new ArgumentNullException("openPatchStream"); + if (output == null) + throw new ArgumentNullException("output"); + + /* + File format: + 0 8 "BSDIFF40" + 8 8 X + 16 8 Y + 24 8 sizeof(newfile) + 32 X bzip2(control block) + 32+X Y bzip2(diff block) + 32+X+Y ??? bzip2(extra block) + with control block a set of triples (x,y,z) meaning "add x bytes + from oldfile to x bytes from the diff block; copy y bytes from the + extra block; seek forwards in oldfile by z bytes". + */ + // read header + long controlLength, diffLength, newSize; + using (Stream patchStream = openPatchStream()) + { + // check patch stream capabilities + if (!patchStream.CanRead) + throw new ArgumentException("Patch stream must be readable.", "openPatchStream"); + if (!patchStream.CanSeek) + throw new ArgumentException("Patch stream must be seekable.", "openPatchStream"); + + byte[] header = ReadExactly(patchStream, c_headerSize); + + // check for appropriate magic + long signature = ReadInt64(header, 0); + if (signature != c_fileSignature) + throw new InvalidOperationException("Corrupt patch."); + + // read lengths from header + controlLength = ReadInt64(header, 8); + diffLength = ReadInt64(header, 16); + newSize = ReadInt64(header, 24); + if (controlLength < 0 || diffLength < 0 || newSize < 0) + throw new InvalidOperationException("Corrupt patch."); + } + + // preallocate buffers for reading and writing + const int c_bufferSize = 1048576; + byte[] newData = new byte[c_bufferSize]; + byte[] oldData = new byte[c_bufferSize]; + + // prepare to read three parts of the patch in parallel + using (Stream compressedControlStream = openPatchStream()) + using (Stream compressedDiffStream = openPatchStream()) + using (Stream compressedExtraStream = openPatchStream()) + { + // seek to the start of each part + compressedControlStream.Seek(c_headerSize, SeekOrigin.Current); + compressedDiffStream.Seek(c_headerSize + controlLength, SeekOrigin.Current); + compressedExtraStream.Seek(c_headerSize + controlLength + diffLength, SeekOrigin.Current); + + // decompress each part (to read it) + using (BZip2InputStream controlStream = new BZip2InputStream(compressedControlStream)) + using (BZip2InputStream diffStream = new BZip2InputStream(compressedDiffStream)) + using (BZip2InputStream extraStream = new BZip2InputStream(compressedExtraStream)) + { + long[] control = new long[3]; + byte[] buffer = new byte[8]; + + int oldPosition = 0; + int newPosition = 0; + while (newPosition < newSize) + { + // read control data + for (int i = 0; i < 3; i++) + { + ReadExactly(controlStream, buffer, 0, 8); + control[i] = ReadInt64(buffer, 0); + } + + // sanity-check + if (newPosition + control[0] > newSize) + throw new InvalidOperationException("Corrupt patch."); + + // seek old file to the position that the new data is diffed against + input.Position = oldPosition; + + int bytesToCopy = (int)control[0]; + while (bytesToCopy > 0) + { + int actualBytesToCopy = Math.Min(bytesToCopy, c_bufferSize); + + // read diff string + ReadExactly(diffStream, newData, 0, actualBytesToCopy); + + // add old data to diff string + int availableInputBytes = Math.Min(actualBytesToCopy, (int)(input.Length - input.Position)); + ReadExactly(input, oldData, 0, availableInputBytes); + + for (int index = 0; index < availableInputBytes; index++) + newData[index] += oldData[index]; + + output.Write(newData, 0, actualBytesToCopy); + + // adjust counters + newPosition += actualBytesToCopy; + oldPosition += actualBytesToCopy; + bytesToCopy -= actualBytesToCopy; + } + + // sanity-check + if (newPosition + control[1] > newSize) + throw new InvalidOperationException("Corrupt patch."); + + // read extra string + bytesToCopy = (int)control[1]; + while (bytesToCopy > 0) + { + int actualBytesToCopy = Math.Min(bytesToCopy, c_bufferSize); + + ReadExactly(extraStream, newData, 0, actualBytesToCopy); + output.Write(newData, 0, actualBytesToCopy); + + newPosition += actualBytesToCopy; + bytesToCopy -= actualBytesToCopy; + } + + // adjust position + oldPosition = (int)(oldPosition + control[2]); + } + } + } + } + + private static int CompareBytes(byte[] left, int leftOffset, byte[] right, int rightOffset) + { + for (int index = 0; index < left.Length - leftOffset && index < right.Length - rightOffset; index++) + { + int diff = left[index + leftOffset] - right[index + rightOffset]; + if (diff != 0) + return diff; + } + return 0; + } + + private static int MatchLength(byte[] oldData, int oldOffset, byte[] newData, int newOffset) + { + int i; + for (i = 0; i < oldData.Length - oldOffset && i < newData.Length - newOffset; i++) + { + if (oldData[i + oldOffset] != newData[i + newOffset]) + break; + } + return i; + } + + private static int Search(int[] I, byte[] oldData, byte[] newData, int newOffset, int start, int end, out int pos) + { + if (end - start < 2) + { + int startLength = MatchLength(oldData, I[start], newData, newOffset); + int endLength = MatchLength(oldData, I[end], newData, newOffset); + + if (startLength > endLength) + { + pos = I[start]; + return startLength; + } + else + { + pos = I[end]; + return endLength; + } + } + else + { + int midPoint = start + (end - start) / 2; + return CompareBytes(oldData, I[midPoint], newData, newOffset) < 0 ? + Search(I, oldData, newData, newOffset, midPoint, end, out pos) : + Search(I, oldData, newData, newOffset, start, midPoint, out pos); + } + } + + private static void Split(int[] I, int[] v, int start, int len, int h) + { + if (len < 16) + { + int j; + for (int k = start; k < start + len; k += j) + { + j = 1; + int x = v[I[k] + h]; + for (int i = 1; k + i < start + len; i++) + { + if (v[I[k + i] + h] < x) + { + x = v[I[k + i] + h]; + j = 0; + } + if (v[I[k + i] + h] == x) + { + Swap(ref I[k + j], ref I[k + i]); + j++; + } + } + for (int i = 0; i < j; i++) + v[I[k + i]] = k + j - 1; + if (j == 1) + I[k] = -1; + } + } + else + { + int x = v[I[start + len / 2] + h]; + int jj = 0; + int kk = 0; + for (int i2 = start; i2 < start + len; i2++) + { + if (v[I[i2] + h] < x) + jj++; + if (v[I[i2] + h] == x) + kk++; + } + jj += start; + kk += jj; + + int i = start; + int j = 0; + int k = 0; + while (i < jj) + { + if (v[I[i] + h] < x) + { + i++; + } + else if (v[I[i] + h] == x) + { + Swap(ref I[i], ref I[jj + j]); + j++; + } + else + { + Swap(ref I[i], ref I[kk + k]); + k++; + } + } + + while (jj + j < kk) + { + if (v[I[jj + j] + h] == x) + { + j++; + } + else + { + Swap(ref I[jj + j], ref I[kk + k]); + k++; + } + } + + if (jj > start) + Split(I, v, start, jj - start, h); + + for (i = 0; i < kk - jj; i++) + v[I[jj + i]] = kk - 1; + if (jj == kk - 1) + I[jj] = -1; + + if (start + len > kk) + Split(I, v, kk, start + len - kk, h); + } + } + + private static int[] SuffixSort(byte[] oldData) + { + int[] buckets = new int[256]; + + foreach (byte oldByte in oldData) + buckets[oldByte]++; + for (int i = 1; i < 256; i++) + buckets[i] += buckets[i - 1]; + for (int i = 255; i > 0; i--) + buckets[i] = buckets[i - 1]; + buckets[0] = 0; + + int[] I = new int[oldData.Length + 1]; + for (int i = 0; i < oldData.Length; i++) + I[++buckets[oldData[i]]] = i; + + int[] v = new int[oldData.Length + 1]; + for (int i = 0; i < oldData.Length; i++) + v[i] = buckets[oldData[i]]; + + for (int i = 1; i < 256; i++) + { + if (buckets[i] == buckets[i - 1] + 1) + I[buckets[i]] = -1; + } + I[0] = -1; + + for (int h = 1; I[0] != -(oldData.Length + 1); h += h) + { + int len = 0; + int i = 0; + while (i < oldData.Length + 1) + { + if (I[i] < 0) + { + len -= I[i]; + i -= I[i]; + } + else + { + if (len != 0) + I[i - len] = -len; + len = v[I[i]] + 1 - i; + Split(I, v, i, len, h); + i += len; + len = 0; + } + } + + if (len != 0) + I[i - len] = -len; + } + + for (int i = 0; i < oldData.Length + 1; i++) + I[v[i]] = i; + + return I; + } + + private static void Swap(ref int first, ref int second) + { + int temp = first; + first = second; + second = temp; + } + + private static long ReadInt64(byte[] buf, int offset) + { + long value = buf[offset + 7] & 0x7F; + + for (int index = 6; index >= 0; index--) + { + value *= 256; + value += buf[offset + index]; + } + + if ((buf[offset + 7] & 0x80) != 0) + value = -value; + + return value; + } + + private static void WriteInt64(long value, byte[] buf, int offset) + { + long valueToWrite = value < 0 ? -value : value; + + for (int byteIndex = 0; byteIndex < 8; byteIndex++) + { + buf[offset + byteIndex] = unchecked((byte)valueToWrite); + valueToWrite >>= 8; + } + + if (value < 0) + buf[offset + 7] |= 0x80; + } + + /// + /// Reads exactly bytes from . + /// + /// The stream to read from. + /// The count of bytes to read. + /// A new byte array containing the data read from the stream. + private static byte[] ReadExactly(Stream stream, int count) + { + if (count < 0) + throw new ArgumentOutOfRangeException("count"); + byte[] buffer = new byte[count]; + ReadExactly(stream, buffer, 0, count); + return buffer; + } + + /// + /// Reads exactly bytes from into + /// , starting at the byte given by . + /// + /// The stream to read from. + /// The buffer to read data into. + /// The offset within the buffer at which data is first written. + /// The count of bytes to read. + private static void ReadExactly(Stream stream, byte[] buffer, int offset, int count) + { + // check arguments + if (stream == null) + throw new ArgumentNullException("stream"); + if (buffer == null) + throw new ArgumentNullException("buffer"); + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException("offset"); + if (count < 0 || buffer.Length - offset < count) + throw new ArgumentOutOfRangeException("count"); + + while (count > 0) + { + // read data + int bytesRead = stream.Read(buffer, offset, count); + + // check for failure to read + if (bytesRead == 0) + throw new EndOfStreamException(); + + // move to next block + offset += bytesRead; + count -= bytesRead; + } + } + + const long c_fileSignature = 0x3034464649445342L; + const int c_headerSize = 32; + } +} diff --git a/Wabbajack.Common/Consts.cs b/Wabbajack.Common/Consts.cs index 4e839f10..d112471f 100644 --- a/Wabbajack.Common/Consts.cs +++ b/Wabbajack.Common/Consts.cs @@ -9,5 +9,6 @@ namespace Wabbajack.Common public static class Consts { public static string GameFolderFilesDir = "Game Folder Files"; + public static string ModPackMagic = "Celebration!, Cheese for Everyone!"; } } diff --git a/Wabbajack.Common/Data.cs b/Wabbajack.Common/Data.cs index 0c463b37..46f98712 100644 --- a/Wabbajack.Common/Data.cs +++ b/Wabbajack.Common/Data.cs @@ -94,7 +94,7 @@ namespace Wabbajack.Common public string From; } - public class PatchedArchive : FromArchive + public class PatchedFromArchive : FromArchive { /// /// The file to apply to the source file to patch it @@ -112,6 +112,18 @@ namespace Wabbajack.Common /// Human friendly name of this archive /// public string Name; + + /// + /// Meta INI for the downloaded archive + /// + public string Meta; + } + + public class NexusMod : Archive + { + public string GameName; + public string ModID; + public string FileID; } /// @@ -157,6 +169,7 @@ namespace Wabbajack.Common { public dynamic IniData; public string Name; + public string Meta; } /// diff --git a/Wabbajack.Common/Utils.cs b/Wabbajack.Common/Utils.cs index 0c450a15..7b7ad49d 100644 --- a/Wabbajack.Common/Utils.cs +++ b/Wabbajack.Common/Utils.cs @@ -1,4 +1,5 @@ -using IniParser; +using ICSharpCode.SharpZipLib.BZip2; +using IniParser; using Newtonsoft.Json; using System; using System.Collections.Generic; @@ -84,5 +85,40 @@ namespace Wabbajack.Common return file.Substring(folder.Length + 1); } + /// + /// Returns the string compressed via BZip2 + /// + /// + /// + public static byte[] BZip2String(this string data) + { + using (var os = new MemoryStream()) + { + using (var bz = new BZip2OutputStream(os)) + { + using (var bw = new BinaryWriter(bz)) + bw.Write(data); + } + return os.ToArray(); + } + } + + /// + /// Returns the string compressed via BZip2 + /// + /// + /// + public static string BZip2String(this byte[] data) + { + using (var s = new MemoryStream(data)) + { + using (var bz = new BZip2InputStream(s)) + { + using (var bw = new BinaryReader(bz)) + return bw.ReadString(); + } + } + } + } } diff --git a/Wabbajack.Common/Wabbajack.Common.csproj b/Wabbajack.Common/Wabbajack.Common.csproj index e1410ca9..095b9fc2 100644 --- a/Wabbajack.Common/Wabbajack.Common.csproj +++ b/Wabbajack.Common/Wabbajack.Common.csproj @@ -32,9 +32,15 @@ 4 + + ..\packages\SharpZipLib.1.1.0\lib\net45\ICSharpCode.SharpZipLib.dll + ..\packages\ini-parser.2.5.2\lib\net20\INIFileParser.dll + + ..\packages\murmurhash.1.0.3\lib\net45\MurmurHash.dll + ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll @@ -48,6 +54,7 @@ + diff --git a/Wabbajack.Common/packages.config b/Wabbajack.Common/packages.config index d4d1fb83..3f87e68c 100644 --- a/Wabbajack.Common/packages.config +++ b/Wabbajack.Common/packages.config @@ -3,8 +3,6 @@ - - - + \ No newline at end of file diff --git a/Wabbajack/Compiler.cs b/Wabbajack/Compiler.cs index 38cb8d1c..6503545b 100644 --- a/Wabbajack/Compiler.cs +++ b/Wabbajack/Compiler.cs @@ -1,9 +1,11 @@ using Murmur; +using Newtonsoft.Json; using SevenZipExtractor; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; @@ -29,6 +31,25 @@ namespace Wabbajack } } + internal void PatchExecutable() + { + + var data = JsonConvert.SerializeObject(InstallDirectives).BZip2String(); + var executable = Assembly.GetExecutingAssembly().Location; + var out_path = Path.Combine(Path.GetDirectoryName(executable), "out.exe"); + 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)); + } + } + public string MO2Profile; public string MO2ProfileDir @@ -41,6 +62,9 @@ namespace Wabbajack public Action Log_Fn { get; } public Action Progress_Function { get; } + public List InstallDirectives { get; private set; } + public List SelectedArchives { get; private set; } + public List IndexedArchives; @@ -51,6 +75,14 @@ namespace Wabbajack Log_Fn(msg); } + private void Error(string msg, params object[] args) + { + if (args.Length > 0) + msg = String.Format(msg, args); + Log_Fn(msg); + throw new Exception(msg); + } + public Compiler(string mo2_folder, Action log_fn, Action progress_function) { MO2Folder = mo2_folder; @@ -82,7 +114,11 @@ namespace Wabbajack var ini_name = file + ".meta"; if (ini_name.FileExists()) + { info.IniData = ini_name.LoadIniFile(); + info.Meta = File.ReadAllText(ini_name); + } + return info; } @@ -140,10 +176,69 @@ namespace Wabbajack foreach (var file in nomatch) Info(" {0}", file.To); + InstallDirectives = results.Where(i => !(i is IgnoredDirectly)).ToList(); + + GatherArchives(); + results.ToJSON("out.json"); } + private void GatherArchives() + { + var archives = IndexedArchives.GroupBy(a => a.Hash).ToDictionary(k => k.Key, k => k.First()); + + var shas = InstallDirectives.OfType() + .Select(a => a.ArchiveHash) + .Distinct(); + + SelectedArchives = shas.Select(sha => ResolveArchive(sha, archives)).ToList(); + + } + + private Archive ResolveArchive(string sha, Dictionary archives) + { + if(archives.TryGetValue(sha, out var found)) + { + 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) + { + result = new NexusMod() { + GameName = general.gameName, + FileID = general.fileID, + ModID = general.modID}; + } + 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; + } + + private Directive RunStack(IEnumerable> stack, RawSourceFile source) { return (from f in stack @@ -176,6 +271,7 @@ namespace Wabbajack IgnoreRegex(Consts.GameFolderFilesDir + "\\\\.*\\.bsa"), IncludeModIniData(), DirectMatch(), + IncludePatches(), // If we have no match at this point for a game folder file, skip them, we can't do anything about them IgnoreGameFiles(), @@ -183,6 +279,30 @@ namespace Wabbajack }; } + private Func IncludePatches() + { + var indexed = (from archive in IndexedArchives + from entry in archive.Entries + select new { archive = archive, entry = entry }) + .GroupBy(e => Path.GetFileName(e.entry.Path)) + .ToDictionary(e => e.Key); + + return source => + { + if (indexed.TryGetValue(Path.GetFileName(source.Path), out var value)) + { + var found = value.First(); + + var e = source.EvolveTo(); + e.From = found.entry.Path; + e.ArchiveHash = found.archive.Hash; + e.To = source.Path; + return e; + } + return null; + }; + } + private Func IncludeModIniData() { return source => diff --git a/Wabbajack/FodyWeavers.xml b/Wabbajack/FodyWeavers.xml new file mode 100644 index 00000000..5029e706 --- /dev/null +++ b/Wabbajack/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Wabbajack/FodyWeavers.xsd b/Wabbajack/FodyWeavers.xsd new file mode 100644 index 00000000..44a53744 --- /dev/null +++ b/Wabbajack/FodyWeavers.xsd @@ -0,0 +1,111 @@ + + + + + + + + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + A list of unmanaged 32 bit assembly names to include, delimited with line breaks. + + + + + A list of unmanaged 64 bit assembly names to include, delimited with line breaks. + + + + + The order of preloaded assemblies, delimited with line breaks. + + + + + + This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. + + + + + Controls if .pdbs for reference assemblies are also embedded. + + + + + Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. + + + + + As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. + + + + + Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. + + + + + Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + A list of unmanaged 32 bit assembly names to include, delimited with |. + + + + + A list of unmanaged 64 bit assembly names to include, delimited with |. + + + + + The order of preloaded assemblies, delimited with |. + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/Wabbajack/Program.cs b/Wabbajack/Program.cs index 8c749794..06f2e4f4 100644 --- a/Wabbajack/Program.cs +++ b/Wabbajack/Program.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Reflection; using System.Text; +using System.Threading; using System.Threading.Tasks; using Wabbajack.Common; @@ -11,11 +14,46 @@ namespace Wabbajack { static void Main(string[] args) { + var modpack = CheckForModPack(); + if (modpack != null) + { + Console.WriteLine(modpack); + Thread.Sleep(10000); + return; + } + var compiler = new Compiler("c:\\Mod Organizer 2", msg => Console.WriteLine(msg), (msg, id, prog) => Console.WriteLine(msg)); compiler.LoadArchives(); compiler.MO2Profile = "Lexy's Legacy of The Dragonborn Special Edition"; compiler.Compile(); + compiler.PatchExecutable(); } + + private static string CheckForModPack() + { + using (var s = File.OpenRead(Assembly.GetExecutingAssembly().Location)) + { + var magic_bytes = Encoding.ASCII.GetBytes(Consts.ModPackMagic); + s.Position = s.Length - magic_bytes.Length; + using (var br = new BinaryReader(s)) + { + var bytes = br.ReadBytes(magic_bytes.Length); + var magic = Encoding.ASCII.GetString(bytes); + if (magic != Consts.ModPackMagic) + { + return null; + } + + s.Position = s.Length - magic_bytes.Length - 8; + var start_pos = br.ReadInt64(); + s.Position = start_pos; + long length = br.ReadInt64(); + + return br.ReadBytes((int)length).BZip2String(); + + } + } + } } } diff --git a/Wabbajack/Properties/Resources.Designer.cs b/Wabbajack/Properties/Resources.Designer.cs new file mode 100644 index 00000000..ab5f14c9 --- /dev/null +++ b/Wabbajack/Properties/Resources.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Wabbajack.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Wabbajack.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to . + /// + internal static string ModPack { + get { + return ResourceManager.GetString("ModPack", resourceCulture); + } + } + } +} diff --git a/Wabbajack/Properties/Resources.resx b/Wabbajack/Properties/Resources.resx new file mode 100644 index 00000000..c1567b33 --- /dev/null +++ b/Wabbajack/Properties/Resources.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + \ No newline at end of file diff --git a/Wabbajack/Wabbajack.csproj b/Wabbajack/Wabbajack.csproj index 97dfcb7d..5a4f2e42 100644 --- a/Wabbajack/Wabbajack.csproj +++ b/Wabbajack/Wabbajack.csproj @@ -1,5 +1,6 @@  + Debug @@ -12,6 +13,8 @@ 512 true true + + x64 @@ -33,9 +36,15 @@ 4 + + ..\packages\Costura.Fody.4.0.0\lib\net40\Costura.dll + ..\packages\murmurhash.1.0.3\lib\net45\MurmurHash.dll + + ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll + @@ -49,6 +58,11 @@ + + True + True + Resources.resx + @@ -67,5 +81,19 @@ Wabbajack.Common + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/Wabbajack/packages.config b/Wabbajack/packages.config index 94093d76..b65cc07a 100644 --- a/Wabbajack/packages.config +++ b/Wabbajack/packages.config @@ -1,4 +1,7 @@  + + + \ No newline at end of file