Can create self-installing modlists

This commit is contained in:
Timothy Baldridge 2019-07-21 16:47:17 -06:00
parent 8b7b64ddd4
commit e4ca3cd01e
14 changed files with 1214 additions and 5 deletions

656
Wabbajack.Common/BSDiff.cs Normal file
View File

@ -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
{
/// <summary>
/// Creates a binary patch (in <a href="http://www.daemonology.net/bsdiff/">bsdiff</a> format) that can be used
/// (by <see cref="Apply"/>) to transform <paramref name="oldData"/> into <paramref name="newData"/>.
/// </summary>
/// <param name="oldData">The original binary data.</param>
/// <param name="newData">The new binary data.</param>
/// <param name="output">A <see cref="Stream"/> to which the patch will be written.</param>
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;
}
/// <summary>
/// Applies a binary patch (in <a href="http://www.daemonology.net/bsdiff/">bsdiff</a> format) to the data in
/// <paramref name="input"/> and writes the results of patching to <paramref name="output"/>.
/// </summary>
/// <param name="input">A <see cref="Stream"/> containing the input data.</param>
/// <param name="openPatchStream">A func that can open a <see cref="Stream"/> positioned at the start of the patch data.
/// This stream must support reading and seeking, and <paramref name="openPatchStream"/> must allow multiple streams on
/// the patch to be opened concurrently.</param>
/// <param name="output">A <see cref="Stream"/> to which the patched data is written.</param>
public static void Apply(Stream input, Func<Stream> 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;
}
/// <summary>
/// Reads exactly <paramref name="count"/> bytes from <paramref name="stream"/>.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="count">The count of bytes to read.</param>
/// <returns>A new byte array containing the data read from the stream.</returns>
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;
}
/// <summary>
/// Reads exactly <paramref name="count"/> bytes from <paramref name="stream"/> into
/// <paramref name="buffer"/>, starting at the byte given by <paramref name="offset"/>.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="buffer">The buffer to read data into.</param>
/// <param name="offset">The offset within the buffer at which data is first written.</param>
/// <param name="count">The count of bytes to read.</param>
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;
}
}

View File

@ -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!";
}
}

View File

@ -94,7 +94,7 @@ namespace Wabbajack.Common
public string From;
}
public class PatchedArchive : FromArchive
public class PatchedFromArchive : FromArchive
{
/// <summary>
/// The file to apply to the source file to patch it
@ -112,6 +112,18 @@ namespace Wabbajack.Common
/// Human friendly name of this archive
/// </summary>
public string Name;
/// <summary>
/// Meta INI for the downloaded archive
/// </summary>
public string Meta;
}
public class NexusMod : Archive
{
public string GameName;
public string ModID;
public string FileID;
}
/// <summary>
@ -157,6 +169,7 @@ namespace Wabbajack.Common
{
public dynamic IniData;
public string Name;
public string Meta;
}
/// <summary>

View File

@ -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);
}
/// <summary>
/// Returns the string compressed via BZip2
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
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();
}
}
/// <summary>
/// Returns the string compressed via BZip2
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
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();
}
}
}
}
}

View File

@ -32,9 +32,15 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="ICSharpCode.SharpZipLib, Version=1.1.0.145, Culture=neutral, PublicKeyToken=1b03e6acf1164f73, processorArchitecture=MSIL">
<HintPath>..\packages\SharpZipLib.1.1.0\lib\net45\ICSharpCode.SharpZipLib.dll</HintPath>
</Reference>
<Reference Include="INIFileParser, Version=2.5.2.0, Culture=neutral, PublicKeyToken=79af7b307b65cf3c, processorArchitecture=MSIL">
<HintPath>..\packages\ini-parser.2.5.2\lib\net20\INIFileParser.dll</HintPath>
</Reference>
<Reference Include="MurmurHash, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\murmurhash.1.0.3\lib\net45\MurmurHash.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
@ -48,6 +54,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="BSDiff.cs" />
<Compile Include="Consts.cs" />
<Compile Include="Data.cs" />
<Compile Include="DynamicIniData.cs" />

View File

@ -3,8 +3,6 @@
<package id="ini-parser" version="2.5.2" targetFramework="net472" />
<package id="murmurhash" version="1.0.3" targetFramework="net472" />
<package id="Newtonsoft.Json" version="12.0.2" targetFramework="net472" />
<package id="System.Data.HashFunction.Core" version="2.0.0" targetFramework="net472" />
<package id="System.Data.HashFunction.Interfaces" version="2.0.0" targetFramework="net472" />
<package id="System.Data.HashFunction.MurmurHash" version="2.0.0" targetFramework="net472" />
<package id="SharpZipLib" version="1.1.0" targetFramework="net472" />
<package id="System.Runtime.Numerics" version="4.3.0" targetFramework="net472" />
</packages>

View File

@ -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<string> Log_Fn { get; }
public Action<string, long, long> Progress_Function { get; }
public List<Directive> InstallDirectives { get; private set; }
public List<Archive> SelectedArchives { get; private set; }
public List<IndexedArchive> 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<string> log_fn, Action<string, long, long> 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<FromArchive>()
.Select(a => a.ArchiveHash)
.Distinct();
SelectedArchives = shas.Select(sha => ResolveArchive(sha, archives)).ToList();
}
private Archive ResolveArchive(string sha, Dictionary<string, IndexedArchive> 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<Func<RawSourceFile, Directive>> 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<RawSourceFile, Directive> 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<PatchedFromArchive>();
e.From = found.entry.Path;
e.ArchiveHash = found.archive.Hash;
e.To = source.Path;
return e;
}
return null;
};
}
private Func<RawSourceFile, Directive> IncludeModIniData()
{
return source =>

View File

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura />
</Weavers>

111
Wabbajack/FodyWeavers.xsd Normal file
View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="Costura" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="IncludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged32Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="PreloadOrder" type="xs:string">
<xs:annotation>
<xs:documentation>The order of preloaded assemblies, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
<xs:attribute name="CreateTemporaryAssemblies" type="xs:boolean">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeDebugSymbols" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls if .pdbs for reference assemblies are also embedded.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableCompression" type="xs:boolean">
<xs:annotation>
<xs:documentation>Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableCleanup" type="xs:boolean">
<xs:annotation>
<xs:documentation>As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="LoadAtModuleInit" type="xs:boolean">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IgnoreSatelliteAssemblies" type="xs:boolean">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ExcludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="Unmanaged32Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="Unmanaged64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="PreloadOrder" type="xs:string">
<xs:annotation>
<xs:documentation>The order of preloaded assemblies, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@ -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();
}
}
}
}
}

View File

@ -0,0 +1,72 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Wabbajack.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string ModPack {
get {
return ResourceManager.GetString("ModPack", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ModPack" xml:space="preserve">
<value />
</data>
</root>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\Costura.Fody.4.0.0\build\Costura.Fody.props" Condition="Exists('..\packages\Costura.Fody.4.0.0\build\Costura.Fody.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@ -12,6 +13,8 @@
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>x64</PlatformTarget>
@ -33,9 +36,15 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Costura, Version=4.0.0.0, Culture=neutral, PublicKeyToken=9919ef960d84173d, processorArchitecture=MSIL">
<HintPath>..\packages\Costura.Fody.4.0.0\lib\net40\Costura.dll</HintPath>
</Reference>
<Reference Include="MurmurHash, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\murmurhash.1.0.3\lib\net45\MurmurHash.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
@ -49,6 +58,11 @@
<Compile Include="Compiler.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
@ -67,5 +81,19 @@
<Name>Wabbajack.Common</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>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}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\Costura.Fody.4.0.0\build\Costura.Fody.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Costura.Fody.4.0.0\build\Costura.Fody.props'))" />
<Error Condition="!Exists('..\packages\Fody.5.1.1\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Fody.5.1.1\build\Fody.targets'))" />
</Target>
<Import Project="..\packages\Fody.5.1.1\build\Fody.targets" Condition="Exists('..\packages\Fody.5.1.1\build\Fody.targets')" />
</Project>

View File

@ -1,4 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Costura.Fody" version="4.0.0" targetFramework="net472" />
<package id="Fody" version="5.1.1" targetFramework="net472" developmentDependency="true" />
<package id="murmurhash" version="1.0.3" targetFramework="net472" />
<package id="Newtonsoft.Json" version="12.0.2" targetFramework="net472" />
</packages>